大疆 嵌入式软件二面:从 Bootloader 断电升级讲到 RTOS 与内存
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(80 分钟)
- 从传感器到上位机,把数据采集、处理、通信的完整过程讲清楚:采集靠轮询还是中断?
- DMA 搬完数据后谁来处理?任务之间传的是数据还是指针?缓冲区什么时候可以重新使用?
- 通信任务来不及取数据怎么办?队列满了丢的是新数据还是旧数据,会不会影响后面的处理?
static修饰局部变量、全局变量和函数分别有什么作用?volatile在项目里用过没有?它能不能保证并发访问安全?- 结构体内存对齐怎么算?改变成员顺序后大小会不会变?
- 堆和栈有什么区别?任务栈不够会出现什么现象?
- 信号量和互斥锁有什么区别?优先级反转怎么发生?
- 相同优先级的任务怎么运行?哪些情况下会发生任务切换?
- SPI 的四种模式怎么区分?串口发一个字节到底占多少位?
- 中断和任务之间怎么通信?中断里为什么用
FromISR接口?队列发送失败怎么处理? - 互斥锁为什么不能拿来让中断等待?
- 串口数据偶发异常是怎么定位的?改完以后怎么证明问题解决了?
二面(60 分钟)
- 你说采集任务优先级比较高,怎么证明它能按时完成?通信任务一直很忙的时候呢?如果几个任务共用同一个资源呢?
- 内存泄漏和内存碎片有什么区别?长期运行的设备一直申请、释放内存可能碰到什么问题?
- 用固定大小的内存池,池子用完了怎么办?分配失败有没有检查?
- 升级过程中擦掉旧程序、新的还没写完时断电了怎么办?从哪一步继续?怎么知道前面写到了哪里?如果升级状态本身也没写完整呢?
- 新固件能启动但很快死机,怎么恢复?
- 收到一个 CRC 正确的包,怎么判断它确实来自可信来源?
- 为什么选大疆?了解哪些产品?以后更想做驱动还是应用?
- 项目里和别人意见不一致时怎么处理?
《参考解析》
数据链路的问法本质上在考「谁拥有这块内存」。 采集用轮询还是中断取决于采样率与实时性:低频、可容忍抖动用轮询简单;高频或要求确定性响应用中断。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)。
面试的收尾建议:作者最后总结「以前准备项目主要准备用了什么芯片、分了几个任务、实现了哪些功能,真被问到通信堵了、内存不够了、升级断电了,才发现好多情况只是觉得『应该能处理』,并没有留下验证结果」。这话值得记住——嵌入式面试的核心区分点不在功能实现,而在异常分支和验证证据。准备时把自己的代码翻一遍,重点找队列发送失败、超时、分配失败、异常复位这些平时没走到的路径,每条都要能说出「怎么处理、怎么测过」。