拓竹嵌入式软件开发 二面 面经

1. 介绍一下你做过的最复杂的嵌入式项目,整体架构是怎么设计的?

答:以一个基于 STM32F407 + FreeRTOS 的 OTA 远程升级系统为例。

整体架构分三层:

  • 硬件驱动层:UART(DMA收发)、SPI Flash、I2C EEPROM、GPIO
  • OS 服务层:FreeRTOS 任务调度,包含 WiFi 通信任务、UART 数据处理任务、LED 状态任务、EEPROM 管理任务
  • 应用层:OTA 升级协议解析、固件分包接收、Flash 存储管理、bootloader 跳转逻辑

关键设计决策:

  • 使用 UART + DMA + 空闲中断接收 ESP8266 数据,避免 CPU 轮询,接收完成后通过二值信号量通知 WiFi 任务处理
  • 固件数据先缓存到环形缓冲区,凑满一个扇区(4096字节)再写入 SPI Flash,减少擦写次数
  • bootloader 和 APP 分区隔离,bootloader 固定在 Flash 起始地址,APP 从 0x08010000 开始,升级标志存 EEPROM,断电可恢复
  • 任务间通信全部通过信号量和消息队列,不共享全局变量,避免竞态条件

2. 你在项目中用过 DMA 吗?遇到过什么问题,怎么解决的?

答:用过,主要用在 UART 收发和 SPI Flash 读写上。

遇到的典型问题:

问题一:DMA 接收不定长数据时不知道何时结束。解决:配合 UART 空闲中断,DMA 用循环模式接收,空闲中断触发时读取 DMA 剩余计数器,用总长度减去剩余量得到本次接收字节数,然后把数据搬到环形缓冲区,重启 DMA。

问题二:DMA 发送时,发送函数返回了但数据还没发完,下次调用又启动 DMA,导致数据错乱。解决:发送前检查上一次 DMA 是否完成(检查 DMA 使能状态),用互斥锁保护发送操作,确保上一次传输完成后再启动下一次。

问题三:DMA 缓冲区定义为局部变量,函数返回后栈帧被覆盖,DMA 还在往已经无效的地址写数据,导致 RAM 数据乱。解决:DMA 使用的缓冲区必须定义为全局或静态变量,保证生命周期覆盖整个 DMA 传输过程。

3. 介绍一下你参加过的竞赛,遇到的最难的技术问题是什么,怎么解决的?

答:参加过全国大学生电子设计竞赛(或蓝桥杯嵌入式赛道),以其中一个项目为例。

项目:基于 STM32 的无线数据采集与控制系统。

最难的问题:多路传感器数据采集时,ADC 采样和 UART 发送之间存在时序冲突,高速采样时数据丢失。

分析过程:

  • 最初用中断方式采集 ADC,每次转换完成触发中断,在中断里直接发送数据
  • 当采样率提高后,UART 发送速度跟不上 ADC 中断频率,中断嵌套导致数据覆盖
  • 用示波器和逻辑分析仪抓波形,确认是发送时序问题

解决方案:

  • ADC 改为 DMA 模式,连续采样结果直接搬到缓冲区,不占用 CPU
  • 引入双缓冲机制,ADC 填充一个缓冲区时,CPU 处理另一个缓冲区的数据并发送
  • UART 发送也改为 DMA 模式,发送期间 CPU 可以继续处理其他事务
  • 最终采样率提升了3倍,数据零丢失

4. FreeRTOS 任务优先级如何分配?高优先级任务长期占用 CPU 会导致什么问题,如何避免?

答:优先级分配原则:

  • 实时性要求高、响应时间短的任务优先级高,比如传感器数据采集、通信协议处理
  • 后台任务、显示任务、日志任务优先级低
  • 不要把所有任务设置成相同优先级,会退化成时间片轮转,失去实时性意义
  • 中断服务相关的任务(被 ISR 唤醒的任务)优先级应该较高,保证及时处理

高优先级任务长期占用 CPU 的问题:

  • 低优先级任务得不到执行,出现"饥饿"现象
  • 如果低优先级任务负责喂狗,可能导致系统复位
  • 如果低优先级任务持有某个资源,高优先级任务等待该资源时形成优先级反转

避免方法:

  • 高优先级任务执行完关键操作后主动调用 vTaskDelay 或等待信号量,让出 CPU
  • 避免在高优先级任务里做耗时操作,耗时操作放到低优先级任务里异步处理
  • 使用 uxTaskGetStackHighWaterMark 和 CPU 使用率统计,定期检查各任务的资源占用情况
  • 合理使用互斥锁的优先级继承机制

5. 你的 OTA 升级方案中,如果升级过程中突然断电,如何保证系统能正常恢复?

答:设计思路是:升级状态持久化 + 旧固件保留 + 完整性校验。

具体流程:

  • 升级开始前,在 EEPROM 里写入升级标志和固件总大小,这两个信息在断电后不丢失
  • 固件数据暂存在外部 SPI Flash,不直接写内部 Flash,旧的 APP 固件在整个过程中保持完整
  • 所有数据接收完毕后,先做 CRC 校验,校验通过才允许进入下一步
  • 校验通过后触发系统复位,bootloader 启动时读取 EEPROM 里的标志
  • 如果标志有效,bootloader 从 SPI Flash 把新固件搬运到内部 Flash,搬运完成后清除 EEPROM 标志,跳转执行新固件
  • 如果标志无效(正常启动或上次升级未完成),直接跳转执行现有固件

断电场景分析:

  • 数据接收阶段断电:EEPROM 标志未写入或标志无效,bootloader 直接跳转旧固件,等待重新升级
  • 数据接收完成但复位前断电:同上,标志未写入,跳旧固件
  • bootloader 搬运过程中断电:内部 Flash 数据不完整,但 SPI Flash 里的数据还在,重新上电后 bootloader 检测到标志有效,重新搬运即可
  • 搬运完成但清除标志前断电:bootloader 再次搬运,覆盖写入,幂等操作,不影响结果

6. 环形缓冲区在你的项目中是如何使用的?多任务环境下如何保证线程安全?

答:使用场景:

  • UART 接收:ISR 里把 DMA 搬来的数据写入环形缓冲区,任务里从环形缓冲区读取并处理,解耦生产者(ISR)和消费者(任务)
  • OTA 固件接收:把每包固件数据写入环形缓冲区,凑满一个扇区后批量写入 SPI Flash,减少 Flash 擦写次数

线程安全问题:

  • ISR 写、任务读的场景:如果是单核 MCU,ISR 不会被任务打断,任务可能被 ISR 打断。读操作(任务)需要在修改读指针前关中断或进临界区,写操作(ISR)本身是原子的不需要额外保护
  • 任务写、任务读的场景:需要用互斥锁保护,或者用 FreeRTOS 的队列代替环形缓冲区,队列内部已经处理了线程安全
  • 注意读写指针的更新顺序:先写数据再更新写指针,先读数据再更新读指针,防止另一端看到不完整的数据

实际项目中的做法:ISR 和任务之间的环形缓冲区,写操作在 ISR 里,读操作在任务里,读之前关中断保护读指针更新,操作完立即开中断,临界区尽量短。

7. 你用过逻辑分析仪或示波器调试嵌入式问题吗?举一个具体的例子。

答:用过,举一个 SPI 通信调试的例子。

问题现象:SPI Flash 读出的数据偶尔出错,不是每次都复现,概率性失败。

调试过程:

  • 先用软件打印读出的数据和期望值,确认确实是读数据错误,不是上层逻辑问题
  • 用逻辑分析仪抓 SPI 的 SCK、MOSI、MISO、CS 四根线的波形
  • 分析仪解码后发现,CS 拉低到第一个 SCK 上升沿之间的建立时间不够,偶尔第一个 bit 采样到的是无效数据
  • 对比 Flash 芯片手册,确认 CS 建立时间要求是 5ns,而代码里 CS 拉低后立即开始 SPI 传输,没有任何延时

解决:在 CS 拉低后加一个 NOP 指令延时,保证满足建立时间要

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式面试八股文全集 文章被收录于专栏

这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。

全部评论

相关推荐

03-19 09:58
河海大学 Java
最喜欢春天的奇亚籽很...:同学,是小红书不是小哄书,一眼就能看到的错误
投了多少份简历才上岸
点赞 评论 收藏
分享
评论
1
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务