大疆嵌入式软件岗面经(一面二面)
- 轮次
- 多轮合集
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(约 70 分钟)
- 自我介绍后讲项目:现在主要做什么,用什么芯片和系统,自己负责哪部分?
- static 的作用,修饰局部变量和全局变量分别有什么区别?
- volatile 用过没有,项目里什么地方用了?能不能保证操作的原子性?
- 指针和数组的区别,数组作为函数参数传进去以后,sizeof 算出来是什么?
- 结构体内存对齐,给几个成员算大小,调整顺序后再算。
- 堆和栈的区别,局部变量的地址能不能返回?
- memcpy 和 memmove 有什么区别,内存区域重叠时怎么处理?
- 中断里哪些事情不适合做,耗时操作一般怎么交给任务处理?
- 中断和任务之间传数据,用过什么方式?
- 互斥锁、二值信号量有什么区别?
- 优先级反转是什么,优先级继承怎么缓解?
- 任务什么时候会切换?高优先级任务一直处于就绪状态会怎么样?
- 任务栈大小怎么定,有没有看过栈的使用情况?
- 手撕链表反转,写完自己检查边界(空链表、只有一个节点)。
二面(约 80 分钟)
- 先把数据从进来到出去的整个过程讲清楚,中间经过哪些中断、哪些任务、哪些缓冲区。
- 采样频率是多少,一次产生多少数据?
- 接收缓冲区为什么开这么大,有没有算过最坏情况下要存多少?
- 如果消费数据的任务被延迟了,缓冲区能撑多久?
- 缓冲区满了怎么办,丢新数据还是覆盖旧数据,依据是什么?
- 怎么判断系统已经开始丢数据,有没有计数或者其他检测方式?
- 项目里几个任务的优先级怎么排,哪个任务必须及时响应?
- 从数据采集完成到处理结束,允许的最大延迟是多少?
- 怎么测这段延迟?只看平均值够不够?
- 两个任务共用一个缓冲区,怎么避免读到写了一半的数据?
- 如果 DMA 还在往缓冲区写,任务能不能直接读,怎么划分可读的部分?
- 临界区里处理的数据比较多,会有什么影响?
- 讲一个你最难定位的 bug。
- 最开始怎么发现丢数据的,凭什么确定不是发送端少发了?
- 排查过程中排除了哪些可能,每一步依据是什么?
- 有没有留下出问题时的现场数据?
- 加日志以后,问题出现的频率有没有变化?
- 最后怎么验证修改有效?如果跑几天没复现,能不能说明解决了?
- 设备偶发进 HardFault,你会先保留哪些信息?
- 怀疑任务栈溢出,怎么确认?还可能是什么原因?
- 开了编译优化以后才出现异常,会从哪些地方查?
- 看门狗由谁来喂?如果有个业务任务卡死,其他任务还在正常运行,能不能发现?
- 环形缓冲区:先按单线程情况写,再问如果一个任务写、另一个任务读,需要额外考虑什么?
《参考解析》
环形缓冲区的单线程实现与并发改造。单线程的核心是三个量:缓冲区、读下标、写下标,判空用 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 拿到的是指针大小,必须额外传长度。