面灵AI→

大疆嵌入式软件一面二面:从 C 语言基础到项目细节连环追问

轮次
多轮面试合集
时间
2026-09
来源
牛客网

《面试题目》

一面(约 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. 设备偶发进 HardFault,你会先保留哪些信息?
  19. 怀疑任务栈溢出,怎么确认?还可能是什么原因?
  20. 开了编译优化以后才出现异常,会从哪些地方查?
  21. 看门狗由谁来喂?如果有个业务任务卡死,其他任务还在正常运行,能不能发现?
  22. 手撕:环形缓冲区,先写单线程版本,再问一个任务写、另一个任务读需要额外考虑什么。

《参考解析》

  1. static、volatile 与指针数组:static 修饰局部变量是把存储期从栈挪到静态区、函数退出后值保留,修饰全局变量或函数是把链接属性收到本文件内部;回答时最好各给一个项目里的真实用法。volatile 只保证编译器每次都从内存重新读取、不把它优化进寄存器,它既不提供原子性、也不建立内存屏障;中断或 DMA 改写的标志、寄存器映射变量必须加 volatile,但共享计数器的自增仍要临界区或原子指令。数组作函数参数会退化成指针,函数里的 sizeof 拿到的是指针大小而不是数组长度,所以长度必须在调用处一起传进来。
  2. 内存对齐与 memcpy/memmove:结构体按成员中最大的对齐数补齐,成员顺序会影响总大小——把对齐要求大的成员排在前面通常最小;现场改顺序再算一次,考的就是这个。memcpy 不处理重叠,源和目标重叠时行为未定义;memmove 会先比较地址方向,重叠时从尾部往前拷贝。要手写的话,把「目标地址低于源地址就正序、否则倒序」这条判据说清楚即可。
  3. 中断、锁与优先级反转:中断里不能做耗时、阻塞、动态内存分配和可能拿锁的操作,通常只置标志或往队列丢一份数据,真正的处理交给任务;中断到任务传递数据用消息队列、信号量或环形缓冲区都可以,关键是说清哪个 API 是可中断安全(FromISR)的。互斥锁带所有权与优先级继承,二值信号量只做同步、没有所有者,这是两者的本质区别。优先级反转是低优先级任务持锁挡住高优先级任务运行,靠优先级继承(必要时加优先级天花板)缓解;高优先级任务若长期处于就绪态,会把同优先级以下的任务饿死,需要靠主动让出、延时或时间片轮转。
  4. 缓冲区容量估算与丢数据检测:容量要按最坏情况算——采样率 × 单次数据量 × 最长阻塞时间 × 余量,而不是「以前没丢就说够」;把生产者与消费者索引的差值对容量取模,就能算出消费被延迟多久会溢出。丢数据的检测必须有明确的计数器(溢出次数、被覆盖的样本数),并把它暴露到日志或状态接口,否则只能靠概率复现。面试官连问「满了丢新还是覆盖旧」,考的是你有没有想过这个策略的依据:实时系统通常保留最新数据,但要有明确的丢弃计数和告警。
  5. 难定位 bug 的排查与验证:排查要说清「怎么发现的、排除了哪些可能、每一步依据是什么」,并把现场数据(寄存器、栈回溯、丢包计数、日志时间线)保留下来。HardFault 先保住 LR/PC/PSR 和故障状态寄存器、栈里的返回地址,再判断是空指针、越界还是栈溢出;怀疑栈溢出可以填魔数(0x5A5A5A5A)看水位线、加栈检查钩子,或看 MPU/栈保护是否触发。开了优化才出问题,重点查 volatile 缺失、未定义行为、时序假设和对齐问题;看门狗要单独一个高优先级任务喂,并且让业务任务定期上报心跳,否则某个任务卡死而其他任务照常运行时,喂狗照旧,狗就失效了。最后「跑几天没复现」只能证明概率下降,不能证明修好,要能说出根因和对应的验证手段。
  6. 环形缓冲区的单线程与一写一读实现:单线程版本维护 head/tail,判空是 head == tail,判满可以牺牲一个槽位((tail+1) % cap == head)或额外维护元素个数(这样能用满整块空间)。一写一读、各自只修改自己的指针时,单核上通常不需要关中断,但索引要声明为 volatile,并且写指针必须在数据写完后再更新(必要时加编译屏障),读侧先读指针再读数据;如果索引宽度导致访问不是原子的,就要靠临界区或原子操作保护。这套题还常被追问「能不能用满缓冲区」,回答时把两种判满方式的取舍一起说掉。