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