面灵AI→

大疆嵌入式一面二面:从 static、volatile 到缓冲区该开多大、丢数据怎么发现

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

《面试题目》

一面

  1. static 的作用是什么?修饰局部变量和全局变量分别有什么区别?
  2. volatile 用过没有?项目里什么地方用了?它能保证操作的原子性吗?
  3. 指针和数组的区别是什么?数组作为函数参数传进去以后,sizeof 算出来是什么?
  4. 结构体内存对齐是怎么回事?(现场给几个成员算大小,调整顺序后再算)
  5. 堆和栈的区别是什么?局部变量的地址能不能返回?
  6. memcpy 和 memmove 有什么区别?内存区域重叠时怎么处理?
  7. 中断里哪些事情不适合做?耗时操作一般怎么交给任务处理?
  8. 中断和任务之间传数据,用过什么方式?
  9. 互斥锁和二值信号量有什么区别?
  10. 优先级反转是什么?优先级继承怎么缓解?
  11. 任务什么时候会切换?高优先级任务一直处于就绪状态会怎么样?
  12. 任务栈大小怎么定?有没有看过栈的使用情况?
  13. 手撕:链表反转(写完自己检查边界)。

二面

  1. 把数据从进来到出去的整个过程讲清楚:中间经过哪些中断、哪些任务、哪些缓冲区。
  2. 采样频率是多少?一次产生多少数据?
  3. 接收缓冲区为什么开这么大?有没有算过最坏情况下要存多少?
  4. 如果消费数据的任务被延迟了,缓冲区能撑多久?
  5. 缓冲区满了怎么办?丢新数据还是覆盖旧数据,依据是什么?
  6. 怎么判断系统已经开始丢数据?有没有计数或其他检测方式?
  7. 项目里几个任务的优先级怎么排?哪个任务必须及时响应?
  8. 从数据采集完成到处理结束,允许的最大延迟是多少?
  9. 怎么测这段延迟?只看平均值够不够?
  10. 两个任务共用一个缓冲区,怎么避免读到写了一半的数据?
  11. 如果 DMA 还在往缓冲区写,任务能不能直接读?怎么划分可读的部分?
  12. 临界区里处理的数据比较多,会有什么影响?
  13. 讲一个你最难定位的 bug:最开始怎么发现丢数据的?凭什么确定不是发送端少发了?排查中排除了哪些可能,每一步依据是什么?
  14. 有没有留下出问题时的现场数据?加日志以后问题出现的频率有没有变化?
  15. 最后怎么验证修改有效?如果跑几天没复现,能不能说明解决了?
  16. 设备偶发进 HardFault,你会先保留哪些信息?
  17. 怀疑任务栈溢出,怎么确认?还可能是什么原因?
  18. 开了编译优化以后才出现异常,会从哪些地方查?
  19. 看门狗由谁来喂?如果有个业务任务卡死,其他任务还在正常运行,能不能发现?
  20. 手撕:环形缓冲区(先按单线程写,再问一写一读需要额外考虑什么)。

《参考解析》

volatile 与原子性是两回事,这是嵌入式面的高频陷阱。 volatile 告诉编译器「这个变量可能被外部改变,每次都要从内存读、不许优化掉」,用于寄存器映射、中断与主循环共享的标志位、DMA 缓冲区;但它不保证原子性——i++ 在 Cortex-M 上是「读-改-写」三条指令,中断插在中间照样出错,要原子必须用关中断、原子指令(LDREX/STREX)或临界区。static 修饰局部变量时改变的是存储期(放到静态区、函数退出不销毁、只初始化一次),修饰全局变量/函数时改变的是链接属性(内部链接,只在本编译单元可见)。sizeof 那题:数组作为函数参数会退化为指针,sizeof 得到的是指针大小(4 或 8),所以必须在传入时一并传长度。memcpy 不处理重叠,重叠要用 memmove(它判断方向后从后往前或从前往后拷)。

结构体对齐要会算,也要会说怎么省空间。 规则是每个成员相对结构体起始地址的偏移必须是该成员大小的整数倍(#pragma pack 可改),结构体总大小是最大成员对齐数的整数倍。所以 char + int + char 在 32 位下是 12 字节(1 + 3 填充 + 4 + 1 + 3 尾部填充),调整成 int + char + char 就是 8 字节。工程意义不只是省内存:DMA 与协议解析里如果把结构体直接按字节发送,填充字节会变成脏数据,正确做法是逐字段序列化或用 packed 并注意非对齐访问在某些架构上会触发异常。

中断上下文的限制与「怎么把活交出去」要成套答。 中断里不能做耗时操作(阻塞等待、长循环、打印日志、动态内存分配、文件/网络 IO),也不能调用可能睡眠的接口;原因是中断会屏蔽同优先级或更低优先级的中断,时间一长就丢中断、抖时序。正确做法是「上半部只做最小必要动作」(读寄存器、清标志、把数据放进环形缓冲区、发信号量/事件),下半部用任务处理。中断与任务传数据的常用方式:环形缓冲区(单生产单消费,无锁)、消息队列(RTOS 提供,注意队列深度与拷贝开销)、信号量/事件标志(只做通知,数据放共享缓冲)。互斥锁与二值信号量的区别关键在「所有权」:互斥锁有持有者、支持优先级继承、不能在中段使用;二值信号量没有所有权,任何任务都能 give,适合做同步与中断通知。

优先级反转与任务切换是 RTOS 的必考题。 经典三任务场景:低优先级任务持有锁,高优先级任务等锁被阻塞,中优先级任务抢占低优先级任务,结果高优先级被中优先级间接拖住。解法是优先级继承(持锁者临时提升到等待者的优先级,释放后恢复)或优先级天花板(持锁者直接提到该锁可能被访问的最高优先级)。任务切换发生在:时间片用完(同优先级轮转)、更高优先级任务就绪、当前任务阻塞(等信号量/延时)、中断退出时触发调度。如果高优先级任务一直就绪且不阻塞,低优先级任务会被饿死——回答时要补一句「所以高优先级任务里不能有忙等,必须有阻塞点」。栈大小的确定方法是「静态估算 + 运行时水位检测」:把栈填成固定图案(0xA5),跑完最坏场景后看还剩多少,再留 30% 余量;RTOS 一般提供 uxTaskGetStackHighWaterMark。

二面那组缓冲区问题,本质是「你有没有算过」。 采样频率 × 单次字节数 = 数据速率,乘以「消费任务最长被延迟多久」就是最坏情况要存的量,缓冲区大小要覆盖这个最坏值,而不是拍脑袋开 4KB。满了之后的策略要说清依据:实时控制类丢新数据(旧数据是连续的,丢新只是丢一个采样点,覆盖旧会造成时间轴断裂);日志/审计类不能丢,要落 Flash 或阻塞生产者(但要评估阻塞对上游的影响)。检测丢数据靠计数器(写入总数 vs 读出总数、溢出次数)并上报,不能靠「看起来正常」。延迟不能只看平均值——要测 P99 和最坏值,用 GPIO 翻转 + 示波器或高精度计时器打点,因为偶发的大延迟才是实时系统失效的原因。

并发读写的正确姿势是「所有权转移 + 序号」。 单生产单消费的环形缓冲区不需要锁:写者只改 head、读者只改 tail,用「写后读屏障」保证数据先写入再更新索引;判满用「留一个空位」((head+1)%N == tail)或额外计数器,判空用 head == tail。DMA 场景下要划分可读区域:DMA 用「半传输/传输完成」中断把已写完的一段标记为可读,任务只读这段,不能读正在写的部分;更稳的做法是双缓冲(ping-pong),DMA 写 A 时任务读 B,切换时交换。临界区里处理大量数据会把中断延迟拉长,正确做法是「临界区只做索引交换与拷贝关键字段,重活放到临界区外」。

HardFault 与看门狗是「故障可诊断性」的题。 进 HardFault 先保留现场:栈里的 R0-R3、R12、LR、PC、xPSR(从 MSP/PSP 取),以及 CFSR/HFSR/MMFAR/BFAR 这些故障状态寄存器,能判断是总线错误、用法错误还是内存管理错误;再把 PC 值反查符号表定位到具体函数。栈溢出可以先看栈水位图案是否被破坏、看 SP 是否越界、以及 MPU 是否能拦住。开优化后才出现的异常,常见原因是 volatile 缺失导致的读写被优化、内联后时序变化、以及 UB(越界、有符号溢出)被优化放大。看门狗的关键是「谁喂」:只在主循环或一个专门任务里喂、且喂之前要确认所有关键任务都上报过心跳,否则一个业务任务卡死而其他任务还在跑,看门狗照样被喂、系统看起来「活着」却已经失效。