面灵AI→

大疆 嵌入式软件二面:从 Bootloader 断电升级讲到 RTOS 与内存

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

《面试题目》

一面(80 分钟)

  1. 从传感器到上位机,把数据采集、处理、通信的完整过程讲清楚:采集靠轮询还是中断?
  2. DMA 搬完数据后谁来处理?任务之间传的是数据还是指针?缓冲区什么时候可以重新使用?
  3. 通信任务来不及取数据怎么办?队列满了丢的是新数据还是旧数据,会不会影响后面的处理?
  4. static 修饰局部变量、全局变量和函数分别有什么作用?
  5. volatile 在项目里用过没有?它能不能保证并发访问安全?
  6. 结构体内存对齐怎么算?改变成员顺序后大小会不会变?
  7. 堆和栈有什么区别?任务栈不够会出现什么现象?
  8. 信号量和互斥锁有什么区别?优先级反转怎么发生?
  9. 相同优先级的任务怎么运行?哪些情况下会发生任务切换?
  10. SPI 的四种模式怎么区分?串口发一个字节到底占多少位?
  11. 中断和任务之间怎么通信?中断里为什么用 FromISR 接口?队列发送失败怎么处理?
  12. 互斥锁为什么不能拿来让中断等待?
  13. 串口数据偶发异常是怎么定位的?改完以后怎么证明问题解决了?

二面(60 分钟)

  1. 你说采集任务优先级比较高,怎么证明它能按时完成?通信任务一直很忙的时候呢?如果几个任务共用同一个资源呢?
  2. 内存泄漏和内存碎片有什么区别?长期运行的设备一直申请、释放内存可能碰到什么问题?
  3. 用固定大小的内存池,池子用完了怎么办?分配失败有没有检查?
  4. 升级过程中擦掉旧程序、新的还没写完时断电了怎么办?从哪一步继续?怎么知道前面写到了哪里?如果升级状态本身也没写完整呢?
  5. 新固件能启动但很快死机,怎么恢复?
  6. 收到一个 CRC 正确的包,怎么判断它确实来自可信来源?
  7. 为什么选大疆?了解哪些产品?以后更想做驱动还是应用?
  8. 项目里和别人意见不一致时怎么处理?

《参考解析》

数据链路的问法本质上在考「谁拥有这块内存」。 采集用轮询还是中断取决于采样率与实时性:低频、可容忍抖动用轮询简单;高频或要求确定性响应用中断。DMA 搬完数据后的典型做法是让 DMA 传输完成中断只做「发信号/置标志」,真正的解析交给任务,因为中断里不能做重活。任务之间传数据还是指针是这题的分水岭:传指针省内存但有生命周期风险,传数据安全但有拷贝开销——作者踩的坑正是传了指针,任务还没处理完缓冲区就被接收端复用了,表现成「偶发数据异常」,加日志后才发现根因是内存复用而不是协议解析。正确的做法有三种:① 用双缓冲/环形缓冲区(生产者只写空闲的那一半,消费者读完再归还);② 用队列传值(小数据);③ 用内存池分配所有权明确的块,并在队列里传递所有权。通信任务来不及取数据时不能简单丢弃:要先明确数据用途,丢失要记录(计数器/日志)并让上层能感知;如果要保最新,就覆盖旧数据;如果要保完整,就扩大缓冲或提高通信任务优先级/降频采集。任何一种选择都要写进设计说明,而不是「队列满了就丢」了事。

C 与 RTOS 基础这几道要答准。 static 修饰局部变量是把存储从栈移到静态区(生命周期贯穿程序,只初始化一次,适合做状态保持但要注意不可重入);修饰全局变量/函数是把链接性降为内部(只在本编译单元可见,避免命名冲突);volatile 只保证不被优化掉、每次访问内存,不保证原子性也不提供同步语义——所以它不能替代临界区,flag++ 这类复合操作仍会丢更新。内存对齐规则是「成员偏移是 min(成员自身对齐, 编译器上限) 的整数倍,结构体总大小是最大成员对齐的整数倍」,所以调整成员顺序能减小结构体(这是嵌入式省 RAM 的常用手段),__attribute__((packed)) 能取消填充但可能造成非对齐访问异常。堆和栈的区别在分配方式与生命周期:栈由编译器分配、大小固定、溢出直接踩到相邻内存(表现为随机崩溃、魔数被改、任务跑飞),所以 RTOS 里要给每个任务算好栈深并开栈溢出检测;堆由 malloc 动态分配、有碎片问题。信号量用于「同步/通知」(可以有多个计数,常用于事件通知和资源计数),互斥锁用于「互斥保护共享资源」,且互斥锁有优先级继承机制来缓解优先级反转,信号量没有。优先级反转的经典场景是:低优先级任务持有锁 → 高优先级任务等锁被阻塞 → 中优先级任务抢占了低优先级任务 → 结果高优先级任务被中优先级任务间接拖住;解法是优先级继承(FreeRTOS 的互斥量支持)或优先级天花板。同优先级任务默认按时间片轮转(configUSE_TIME_SLICING 开启时),任务切换发生在时钟节拍、任务主动让出(taskYIELD)、阻塞/被唤醒、以及中断退出时触发 PendSV。SPI 四种模式由 CPOL(时钟空闲电平)和 CPHA(采样边沿)组合而成,模式 0/3 最常用,配错的表现是读到固定值或全 0/全 F、数据整体移位一位,定位手段是先用示波器/逻辑分析仪看四根线的实际波形与从设备手册对照。串口发一个字节通常占 10 位(1 起始 + 8 数据 + 1 停止),有校验位是 11 位——这题考的是你有没有真的按位算过波特率。

中断与任务的通信规矩:中断里只能调带 FromISR 后缀的 API(如 xQueueSendFromISR),因为它们不阻塞、且需要知道是否有更高优先级任务被唤醒,以便退出时触发一次上下文切换;在中断里直接调普通队列 API 会破坏调度器。队列发送失败要分情况:满队列时选择丢弃并计数(不能让中断重试,会拉长中断时间)、或用覆盖模式、或用一个更高优先级的「应急队列」;无论哪种都要把丢失记录下来,因为「程序还在跑」不等于「数据没丢」。互斥锁不能在中断里等待,是因为它可能阻塞,而中断上下文没有任务控制块、不能挂起、必须尽快返回——中断要用互斥保护的资源,应该改成「关中断做临界区」或把操作下推到任务里。

二面那几个「改条件」的问题考的是工程严谨性。 证明任务能按时完成不能只看运行日志(日志本身改变时序),正确做法是用 GPIO 翻转 + 示波器/逻辑分析仪测任务的实际执行时间和周期抖动,并且要在负载上来以后测(内存压力、通信繁忙、其他任务抢占都叠加进去),看最坏情况而不是平均情况;进一步可以做 WCET(最坏执行时间)分析或用 RTOS 的 trace 工具(FreeRTOS+Trace、SEGGER SystemView)看调度时间线。内存泄漏与碎片的区别:泄漏是「分配了不释放,总可用量持续下降」,碎片是「总量够但没有一块足够大的连续内存」——长期运行的设备两者都致命,前者靠成对释放和分配计数审计,后者靠固定块内存池(同一大小的块天然无外部碎片)或定期整理。用内存池就要回答池子用完怎么办:可以分级(不同块大小多级池)、可以预留应急块给关键路径、可以拒绝并上报告警,但必须检查 malloc/池分配 的返回值——不检查返回值是嵌入式里最常见的真 bug。

Bootloader 与 OTA 的部分是这份面经最有价值的地方,也是很多候选人的弱项。正常的升级流程是:接收固件 → 写入备份分区(或外部 Flash)→ 校验(CRC/哈希/签名)→ 置升级标志 → 复位 → Bootloader 检查标志与校验 → 擦写主分区 → 再次校验 → 清标志 → 跳转 App。断电恢复的关键是「每一步都要留下可判定的状态」:用「固件头部记录长度 + 分块写入时更新已写偏移」的方式记录进度(写数据前先写进度到独立的元数据区,且元数据要带 CRC 和双副本),这样重新上电后能知道写到哪、继续或者重来;如果升级状态本身也没写完整,就要靠双副本 + 版本号选最新的有效副本。「新固件能启动但很快死机怎么恢复」的答案是把「升级成功」的定义从「写入完成」改成「新固件被确认为可用」——即新版本先试运行(在 Bootloader 里设置「试运行标志 + 启动计数」,App 启动后稳定运行一段时间由应用主动确认),确认后才清除回滚标志;如果启动计数超过阈值仍未确认,就回滚到旧分区。「CRC 正确的包怎么判断可信」要区分「完整性」和「真实性」:CRC/哈希只能防传输损坏,不能防伪造,要防伪造必须用数字签名(用私钥对固件摘要签名,设备内置公钥验签),配合安全启动(BootROM 验 Bootloader、Bootloader 验 App 的信任链)、版本号防降级(防回滚攻击)、以及密钥的安全存储(放在 OTP/安全芯片里而不是普通 Flash)。

面试的收尾建议:作者最后总结「以前准备项目主要准备用了什么芯片、分了几个任务、实现了哪些功能,真被问到通信堵了、内存不够了、升级断电了,才发现好多情况只是觉得『应该能处理』,并没有留下验证结果」。这话值得记住——嵌入式面试的核心区分点不在功能实现,而在异常分支和验证证据。准备时把自己的代码翻一遍,重点找队列发送失败、超时、分配失败、异常复位这些平时没走到的路径,每条都要能说出「怎么处理、怎么测过」。