面灵AI→

江波龙 嵌入式软件 一面面经

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 讲一下实习项目。
  3. 软件架构是怎么设计的?从数据采集到最终算法计算,中间经历了哪些环节?
  4. 为什么项目选择这个 MCU?有没有考虑过其他型号?
  5. 项目中的多个任务同时访问 SPI、I2C 或其他共享资源,你怎么保证数据不会冲突?
  6. 音频数据是连续采集的,怎么保证数据不会丢失?
  7. 你们是怎么实现音频数据的分帧的?
  8. 如果现场环境噪声特别大,会不会影响检测效果?你们怎么处理的?
  9. 算法处理的数据量比较大,CPU 性能够用吗?有没有做过优化?
  10. OTA、bootloader 具体是怎么做的?有分区吗?
  11. FreeRTOS 是如何进行内存管理的?静态分配和动态分配有什么区别?常见的 heap 实现有哪些?
  12. ARM Cortex-M 有哪些常用寄存器?
  13. 如果给你一段 ARM 汇编,你怎么分析它?
  14. 我看你也做过 Linux 项目,是主要用的 C 还是 C++?两种语言有什么区别?
  15. 反问。

《参考解析》

  1. MCU 选型:回答的落点应该是”需求匹配”而不是参数排名。按维度讲:性能(主频、内核、有无 FPU/DSP 能否支撑算法负载)、内存(Flash/RAM 是否放得下程序、任务栈和数据缓冲)、外设(ADC/SPI/I2C/UART/DMA/定时器是否够用)、低功耗(电池设备重点看 Sleep/Stop 与唤醒机制)、成本与封装(量产时的价格、IO 数量)、开发生态(SDK/HAL/工具链/社区)以及供货与生命周期。以 STM32L 系列为例,它属于超低功耗 Cortex-M4,带 FPU、DSP、DMA、SAI 等资源,能同时兼顾低功耗和一定算力——选它是为了匹配场景,不是因为性能高。
  2. 共享外设的互斥:核心是共享资源必须做互斥保护。多个任务访问同一个 SPI/I2C 时用 FreeRTOS Mutex 保证同一时刻只有一个持有者;再配合统一封装驱动接口(不让业务直接碰寄存器)、尽量缩短临界区(拿锁后不要做大量计算)。如果 DMA 或中断也参与传输,还要设计任务与 ISR 的同步机制,别在中断里抢同一把锁。多个设备挂同一条 SPI 总线时,除了总线互斥还要单独管好各设备的 CS 时序,否则会出现”总线没冲突但数据串了设备”的问题。
  3. 连续采集不丢数据:这是典型的生产者消费者速度匹配问题,链路是”采集 → DMA → 缓冲区 → 通知 → 数据处理”。DMA 让 CPU 不参与每个采样点的搬运;双缓冲或环形缓冲让一块在算、另一块在收;中断只做通知(半传输/传输完成),不在 ISR 里跑算法;采集与处理用队列、信号量或环形缓冲区解耦。要注意边界:如果处理速度长期低于采集速度,再大的缓冲也会溢出,这时候要优化算法、降低处理量或扩缓冲,而不是继续加 DMA。
  4. 音频分帧:分帧就是把连续采样按固定时长或采样点数切成算法可处理的最小单位:连续采样 → DMA 缓冲 → 累积固定采样点 → 形成一帧 → 交给算法。按采样率算,16 kHz、20 ms 一帧就是 320 个采样点。设计时要一并确定帧长、采样率、单双声道、位宽、DMA 缓冲大小,以及是否需要帧重叠(例如 20 ms 帧长配 10 ms 帧移)来保证帧间连续性。
  5. 噪声环境下的检测:分三层答。采集端先把信号质量做上去——传感器选型、模拟前端、滤波、采样率、增益调整与屏蔽抗干扰;信号处理层做带通滤波、去直流、降噪、FFT 频域分析和特征提取,目的是压低环境噪声、突出目标特征;算法判断层不要只看单一阈值,要结合信号强度、频率特征、时域特征和多次采样结果,用阈值或模型综合判断。
  6. OTA 分区与回滚:OTA 分下载、校验、存储、切换、回滚五段。设备先从服务器取版本信息并下载固件,下载过程中分包加 CRC 校验,落盘到预留的 OTA 区;下载完整后再对整个镜像做 SHA256 并检查版本与大小;通过后往升级状态区写标志并重启进 Bootloader。Bootloader 校验镜像有效就切到新 App,新 App 首次启动自检成功后写确认标志;如果启动异常或连续看门狗复位导致确认没写成,Bootloader 就判定升级失败并回到旧版本。典型分区是 Bootloader(64 KB)+ Application A/B(各 256 KB)+ OTA Metadata(4 KB)+ 参数与升级状态(4 KB),A/B 双区是回滚能力的前提。
  7. FreeRTOS 内存管理:先分静态与动态两层。静态用 xTaskCreateStatic()、xQueueCreateStatic(),内存在编译/链接期就确定,确定性好、运行时不申请;动态走 pvPortMalloc() / vPortFree(),由 xTaskCreate() 这类接口在运行时申请,两者可以在同一项目里混用。动态分配有 heap_1~heap_5 五种官方实现:heap_1 只申请不释放、最简单确定;heap_2 支持释放但碎片管理弱;heap_3 是对标准库 malloc/free 的封装;heap_4 支持释放并合并相邻空闲块,一般场景首选;heap_5 在 heap_4 基础上支持多个不连续 RAM 区域。频繁申请释放选 heap_4,对动态内存极敏感就尽量静态分配。
  8. Cortex-M 寄存器与汇编阅读:重点记 R0~R12 通用寄存器、R13 是 SP、R14 是 LR(保存返回地址)、R15 是 PC,另有 xPSR 程序状态寄存器。读汇编按固定顺序走:先看寄存器、再看内存访问、再看 SP/LR/PC、然后看跳转指令和函数调用关系、最后对照 C 代码理解。例如 PUSH {R4, LR} / MOV R4, R0 / BL xxx / POP {R4, PC} 这一段,就是在保存 R4 和返回地址、留给被调用函数用、调用 xxx,最后恢复 R4 并把返回地址弹回 PC 完成返回。
  9. C 与 C++ 的区别:按项目模块选,不是越”高级”越好。C 偏过程和底层控制,用函数加结构体组织代码,运行时开销可控,常见于 MCU 固件、内核、驱动、RTOS;C++ 在兼容 C 的基础上加入类与对象、继承多态、模板、STL、异常、Lambda 和智能指针,能把数据与操作封装在一起,配合 RAII 和 unique_ptr / shared_ptr 管理资源,适合 Linux 应用、音视频、图形界面这类代码规模和架构复杂度都高的场景。实际项目里两者并不割裂,C++ 项目底层经常直接调 C 的系统接口。