面灵AI→

海信嵌入式一面:C 语言八股、RTOS 优先级翻转与 ADC 采样调试

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 自我介绍(2 分钟)。
  2. 项目整体架构、使用场景、你具体负责哪一部分?(项目沟通环节,面试官顺着简历问)
  3. 说说你对 volatile 关键字的理解?什么场景必须加?不加会出现什么问题?
  4. 结构体内存对齐的规则是什么?为什么编译器要对齐?怎么手动指定对齐?
  5. 宏定义和内联函数有什么区别?宏有哪些坑?
  6. 什么是栈、堆、全局静态区?局部变量、static 变量、字符串常量分别存在哪里?
  7. 指针和数组名有什么不一样?数组名什么时候不是首元素地址?
  8. RTOS 里任务优先级是怎么设计的?优先级是不是越高越好?什么是优先级翻转?
  9. 什么是死循环阻塞和忙等?工程里你怎么避免忙等?
  10. Linux 下用户态和内核态有什么区别?为什么要区分?系统调用是什么?
  11. 串口 UART 通信一帧包含哪些部分?波特率误差大会怎么样?奇偶校验有什么用?
  12. 中断服务函数里有哪些禁忌?为什么不能在中断里面 printf、malloc?
  13. 野指针是什么?常见的产生场景有哪些?怎么规避?
  14. 用过哪些传感器(温湿度、加速度、ADC 采集类)?ADC 采集时怎么做软件滤波,滑动平均和中值滤波怎么实现?采集数据跳变、噪声大又该怎么排查?
  15. 为什么选择海信?
  16. 反问:团队目前主要做哪一类产品?日常开发以 RTOS 为主还是 Linux 居多?新人前期主要负责哪一块?后面什么时候出结果?

《参考解析》

这场一面的结构是「自我介绍 → 聊项目 → 现场笔试 → 八股 + 实操」:项目沟通只确认项目是你真做过的,笔试是 C 语言基础卷加一份找规律行测,约一小时。考的是基础广度和调试经验,把内存模型、指针、宏讲到能当场画图,比背框架有效。

volatile 的准确定位是「别把这个变量优化进寄存器」,每次访问都从内存重新读。必须加的三类:映射到固定地址的外设寄存器(值被硬件改,编译器不知情)、中断里改而主循环读的标志、多任务共享的标志变量。不加的后果是变量被提到寄存器缓存,while 等标志的循环可能被优化成死循环。两个边界要主动说清:它不保证原子性,也不提供内存屏障语义;跨核同步要上 atomic 加 acquire/release,别拿它当锁。

对齐两条规则:成员偏移是「自身对齐值与指定对齐值取小」的整数倍;结构体总大小是最宽成员对齐值的整数倍,不够补尾部 padding。目的是让 CPU 用一条对齐访存指令取数——不少 MCU 的 DMA、外设 FIFO 不支持非对齐访问,硬来直接 HardFault。手动指定用 #pragma pack 或 __attribute__((packed)),协议报文常这么省空间;代价是取成员会生成逐字节拼装代码,取成员地址再解引用还可能触发非对齐异常,所以 packed 只用于序列化,算之前先拷进对齐的局部变量。

宏是预处理期的文本替换,内联函数是编译器处理的真函数。宏没有类型检查、没有作用域,两个经典坑:少括号让 SQUARE(a+1) 展开成 a+1*a+1;参数带副作用时被求值多次(MAX(i++, j) 里 i 可能加两次)。内联有类型检查和调试信息,inline 也只是建议、最终由优化器按代价定。常量用 constexpr,短小且频繁调用的功能代码用 inline。

数组名不是指针变量:多数场景退化为首元素地址,但它是数组类型的常量,不能自增、不能赋值;sizeof(数组名) 得到整个数组大小,&数组名 拿到「指向整个数组的指针」。容易漏的第三点:数组作函数参数会退化,函数里 sizeof 永远是指针大小,所以必须同时传长度。

优先级按「事件紧急程度和时限」排:中断服务、控制环、采样、通信收发、日志、后台业务依次降低。也不是越高越好——高优先级任务一多,低优先级就饿死,它一旦忙等或临界区过长还会拖住整机。优先级翻转是这组最值钱的一问:低优先级 L 持锁、高优先级 H 来抢被阻塞,此时中优先级 M 就绪,M 不被锁挡又比 L 高,于是 M 一直跑、L 拿不到 CPU 放不掉锁,H 被间接拖住(火星探路者号就栽在这上面)。解法是优先级继承:H 阻塞时把 L 临时提到 H 的优先级,跑完临界区再降回;FreeRTOS 的 xSemaphoreCreateMutex 自带继承,二值信号量没有。

忙等和阻塞的差别是占不占 CPU:while 空转烧满 CPU、同级任务全让路;阻塞是把任务挂进等待队列、主动让出,事件到了由内核唤醒。有 RTOS 就用队列、信号量、事件组、任务通知做阻塞等待并给超时;中断里给信号要用 ...FromISR 版本,退出时按需触发调度。

中断里不能 printf、malloc,根因是「不可重入 + 可能阻塞」:printf 内部有 stdio 锁和缓冲区,打断一个正在打印的任务再进来就是死锁或输出错乱;malloc 要拿堆锁、遍历空闲链表,和被打断的分配撞上会把堆结构弄坏。正确姿势是快进快出:清标志、读数据、丢进队列,业务交给任务。

UART 一帧是「起始位 + 数据位(5~9 位,常用 8)+ 可选校验位 + 停止位」,空闲为高电平,起始位的下降沿是同步点,之后按约定波特率在每位中点采样。波特率误差是累积的:一帧约 10 位,误差超过 2%~3% 就会采到相邻位,表现为乱码或整帧丢弃,所以两边要用同一时钟源,内部 RC 振荡器温漂大时最容易翻车。奇偶校验只能发现奇数个 bit 出错、不能纠错,工业上更常直接上 CRC;很多「串口不稳」最后查出来是共地和电平标准(TTL、RS-232、RS-485 差分)的问题。

ADC 滤波的思路是「先剔野值,再平滑」:中值滤波取奇数窗口排序后的中间值,专打偶发尖峰;滑动平均用环形缓冲区维护窗口和,进一个出一个、O(1) 更新,压随机噪声但会引入相位滞后,窗口越大越迟钝。跳变排查顺序:先用示波器看引脚上的真实波形(区分信号脏还是代码错),再查参考电压与地、电源纹波、采样时间是否够(源阻抗高时采样电容充不满,读出来偏小)、通道串扰和 DMA 搬错长度,最后才怀疑滤波参数——被滤掉的尖峰可能正是真实告警。

野指针指向不确定、已失效或越界的内存:未初始化、free 后没置空、越界写坏、返回局部变量地址都是常见来源。规避靠纪律加工具:定义即初始化为空、free 后立刻置空、解引用前判空、明确所有权,开 -Wall -Wextra 和静态分析,有条件上 ASan。