面灵AI→

大疆嵌入式软件岗面经(一面二面)

轮次
多轮合集
时间
2026-10
来源
牛客网

《面试题目》

一面(约 70 分钟)

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

二面(约 80 分钟)

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

《参考解析》

环形缓冲区的单线程实现与并发改造。单线程的核心是三个量:缓冲区、读下标、写下标,判空用 read == write,判满用「写指针 +1 追上读指针」(牺牲一个槽位,避免与判空条件冲突);下标更新用位与代替取模(容量取 2 的幂)。改成「一个任务写、一个任务读」后要额外处理四件事:原子性——读写下标必须是单次原子读写,否则 32 位下标在两次 16 位访问中被中断会读到半截值;内存序——先写数据再更新写指针,两步之间要有写屏障,否则读方可能看到「指针已更新但数据还没写」;可见性——多核或带 cache 的 MCU 上要用 memory barrier 保证数据对读方可见,volatile 只管可见性、不管原子性与顺序;判满语义——读下标只由读方写、写下标只由写方写,这就是最经典的 SPSC 无锁设计。若涉及 DMA,还要与缓存一致性配合。

容量估算、溢出策略与丢数据检测。缓冲区开多大不能拍脑袋:单位时间产生量 = 采样率 × 每次采样字节数;最坏阻塞时间 = 消费任务可能被更高优先级任务或长临界区压住的最长时间(加上 DMA 传输与调度抖动);两者相乘再加裕量才是下限。消费任务被延迟时能撑多久,直接用「剩余空闲槽位 ÷ 产生速率」算,这个数字面试时要能现场报。满了以后丢新的还是覆盖旧的取决于业务语义:实时控制与传感流通常丢旧保新;审计、计数、故障记录这类必须完整的场景要丢新并对上游做背压,绝不能静默覆盖。判断有没有丢数据要给每条数据带序号或时间戳,用「期望序号与实际序号的差值」累计漏计,或者维护溢出计数与水位统计并送监控——只看「功能正常」是发现不了偶发丢数据的。

DMA 与并发读取、临界区代价。DMA 在后台写缓冲区时,任务直接读同一块区域会读到写了一半的数据,而且如果这片内存可缓存,CPU 读到的是 cache 里的旧副本(DMA 绕过 CPU 直接改内存)。解决办法是划分可读区域:用双缓冲或环形缓冲加「已完成块」标记——DMA 只在写侧活动,每完成一段(半满/全满中断)就更新一个只由写侧推进的「已提交位置」,读方只读该位置之前的数据;同时用 dma_sync_single_for_cpu/device 或把缓冲区放到非缓存区来保证一致性。临界区里处理大量数据的影响是实时性变差:关中断或持锁时间被拉长,中断响应延迟增加,高优先级任务被阻塞,严重时丢中断丢数据。所以驱动里的通行做法是「临界区只做指针与标志的交换、不做数据搬运与解析」。

延迟测量口径。只看平均值不够,因为实时系统关心尾部:平均值被大量正常样本拉平,偶发的 100ms 抖动会被完全掩盖,而这类尾延迟恰恰会导致控制环失稳或数据丢失。正确做法是记录每个样本的时延分布,报 P50/P95/P99 与最大值,并同时给出抖动。测量本身要有可信的时间基准:用硬件定时器计数而不是读系统 tick(精度可能只有 1ms);打点位置要贴近真实路径两端(数据产生到处理完成),而不是从任务被调度起来才开始算。要能区分延迟来自调度、处理还是等待。

偶发 bug 的定位方法。丢数据这类问题先确认现象与责任边界:用序号或计数确定丢在哪一段(采集侧、传输侧、接收侧),凭什么排除「发送端少发」——比如发送端也有自增计数、或用示波器看总线上的实际帧数。然后按数据流逐段插桩计数,看每一段的进出数量是否守恒,差异出现的那一段就是嫌疑范围;每一步的结论都要有依据(计数、日志、波形),而不是「应该不是这里」。现场数据非常关键:HardFault 要保留故障状态寄存器(CFSR/HFSR/BFAR/MMFAR)、出错时的 PC/LR、栈内容与最近的关键变量,这些要提前在 handler 里落盘,否则复位后就没了。栈溢出可以用栈填充水印(启动时把栈填成固定模式,运行后看被破坏到哪个深度)、MPU 保护栈边界来确认;其他原因还有堆溢出踩到栈、野指针、DMA 越界写、中断优先级配置错误。开了编译优化才出现的异常,往三处查:依赖未定义行为(有符号溢出、越界、未初始化变量)、volatile 缺失导致共享变量被优化进寄存器、时序假设被重排破坏。验证修改是否有效,「跑几天没复现」是不够的——要把故障条件主动放大(缩短周期、加大负载、注入延迟)来复现,至少说清修改触及了根因且放大后不再出现。看门狗同理:若由低优先级任务喂狗,业务任务卡死时它照样能喂;正确做法是各任务打卡、由独立监督逻辑检查心跳位图,任一超时就不喂狗。

RTOS 基础与 C 高频点。互斥锁与二值信号量表面都是「0/1 的锁」,差别在语义:互斥锁有所有权,谁加锁谁解锁,因此能实现优先级继承、支持递归加锁,用于保护共享资源;二值信号量没有所有权,任何任务都能释放,用于任务间同步与事件通知(中断里 give、任务里 take)。优先级反转是「低优先级持锁、高优先级等锁、中优先级抢占低优先级」导致高优先级被无限期阻塞,缓解靠优先级继承(持锁任务临时提升到等待者中的最高优先级),更彻底的是优先级天花板。任务切换发生在阻塞(等信号量、延时、等 IO)、时间片耗尽或被更高优先级任务抢占时;高优先级任务一直就绪且不让出会把低优先级饿死,说明它在忙等或没有阻塞点。任务栈大小不能猜:按最坏调用深度估算、加上中断嵌套开销与局部大数组,再用栈水印或 uxTaskGetStackHighWaterMark 看实际余量,留 30% 裕量。memcpy 假定源与目标不重叠,重叠时行为未定义;memmove 判断方向后从尾部往前拷,保证正确,代价是多一次分支。局部变量地址不能返回,因为栈帧在函数返回后失效,返回的是悬垂指针;数组作为函数参数会退化成指针,sizeof 拿到的是指针大小,必须额外传长度。