面灵AI→

科大讯飞嵌入式软件二面:MCU 选型、I2C 排查与 FreeRTOS 调度

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

《面试题目》

  1. 自我介绍 + 讲项目。
  2. 项目中 MCU 是如何选型的?你会重点考虑哪些指标?CPU 主频和 RAM/Flash 是不是越大越好?
  3. 遇到过比较典型的内存问题吗?举个例子。
  4. SPI 和 I2C、CAN 三者的区别?分别适合什么场景?
  5. 具体讲一下 I2C 通信过程。I2C 通信不通,你是怎么排查的?
  6. 你项目里面创建了哪些任务?为什么要这么划分?
  7. 项目中任务之间是怎么通信的?讲一讲用过哪些 API。
  8. FreeRTOS 是怎么进行任务调度的?调度配置和时间片是怎么工作的?

《参考解析》

MCU 选型:按需求维度排优先级

选型的正确姿势是按”功能需求 → 性能 → 外设 → 内存 → 功耗 → 成本 → 生态与供应链”逐层筛,而不是先看主频。第一层看外设资源是否够用,这一步往往比主频更决定性:项目需要几路 UART、SPI、I2C、CAN、ADC、PWM,是否需要 USB、Ethernet、DMA、硬件加密、CAN-FD。外设不够就得软件模拟(I2C 软模拟在时序敏感场景会出问题)或加外扩芯片,成本和风险都上去了。

第二层看内核与主频:按任务复杂度判断需要 M0/M3/M4/M7 还是 R 系列,是否需要 FPU、DSP 指令(做电机控制、音频处理、滤波算法时 FPU 直接影响能不能跑得动),主频要留出裕量但不必性能过剩——为简单传感器采集选高主频 MCU 是浪费成本也浪费功耗。

第三层看存储:Flash 要装得下代码、常量表、字库、以及升级用的双分区(很多项目初期算漏这一块,做 OTA 时才发现不够);RAM 要覆盖全局变量、通信 Buffer、每个任务的栈、堆、DMA 缓冲区,并且必须留余量(经验上留 20%~30%),因为栈溢出是最难查的一类问题。所以”是不是越大越好”的答案是:RAM/Flash 满足需求 + 合理余量即可,越大越贵、封装越大、功耗和 BOM 成本都上升,而且大 RAM 容易掩盖设计缺陷(比如随手开大 buffer 而不去优化内存使用)。

最后三层是功耗(电池供电看运行电流、睡眠电流、低功耗模式与唤醒时间,主频越高动态功耗越大,性能和功耗要权衡)、成本与封装 IO 数、以及生态与供应链(开发工具与 SDK 成熟度、工作温度范围、芯片供货稳定性、团队已有的技术积累——换一个不熟的平台,学习成本也算成本)。

典型内存问题与定位手段

常被点名的五类是:野指针(指向非法地址)、悬空指针(free 之后继续用)、内存泄漏(申请未释放,长期运行后耗尽)、数组越界(写坏相邻内存)、栈溢出(任务栈不够,多由大局部数组或深递归引起)。举一个具体例子最有说服力:

int *p = malloc(100);
free(p);
*p = 10;      /* Use-After-Free:写已释放内存 */

这类问题在嵌入式上的表现往往是间接的:数据异常 → HardFault → 任务卡死 → 看门狗复位。所以定位时不能只看崩溃位置(崩溃点常常离真正的错误点很远),要按证据链查:看 HardFault 时压栈的现场(PC 指向出错指令、LR 指出从哪调用过来、SP 确认栈是否越界)、读 CFSR(Configurable Fault Status Register,能区分是总线错误、用法错误还是存储器管理错误)与 HFSR(HardFault 状态)、再检查 Buffer 边界和各任务的实际栈使用(FreeRTOS 的 uxTaskGetStackHighWaterMark() 能给出历史最小剩余栈,这是判断栈够不够的标准手段)。工程上的预防比定位更值钱:开 configCHECK_FOR_STACK_OVERFLOW、把 malloc 替换成带边界和统计的实现(或干脆在嵌入式里避免动态分配)、关键 buffer 前后加哨兵值、用 MPU 把非法访问提前挡下来。

SPI、I2C、CAN 的区别与场景

I2C 是两线(SCL + SDA)同步串行总线,有设备地址机制、支持多从机、速度通常 100k/400k/1M/3.4M Hz,走板级短距离,适合挂多个低速器件:EEPROM、RTC、温度传感器、IMU。特点是线少、可挂多设备,代价是速度低、总线电容限制走线长度、且总线被某个从机拉死时整条总线都挂(需要总线恢复逻辑)。

SPI 是四线(SCK、MOSI、MISO、CS)同步全双工,速度可达几十 MHz,靠片选 CS 选从机,没有统一的地址机制,因此每多一个从机就多一根 CS(引脚消耗大)。适合高速外设:SPI Flash、LCD 屏、高速 ADC、图像/雷达传感器。要注意 CPOL/CPHA 四种模式必须与从机手册一致,这是最常见的调不通原因。

CAN 是差分两线(CAN_H/CAN_L)的多主机总线,带非破坏性仲裁(ID 越小优先级越高)、CRC 校验、自动重发和错误帧机制,可靠性高、抗干扰强,适合多节点、较长距离的场景:汽车电子、工业控制、储能/BMS。典型拓扑是多个 MCU 挂在一条 CAN 总线上,两端各接 120Ω 终端电阻(漏接或重复接是新手常见故障)。

一句话总结:I2C 管板级多设备低速传感器,SPI 管板级高速外设,CAN 管多节点长距离可靠通信。实际选型还要综合数据量、距离、实时性、可靠性和成本——比如同一个板子上既挂 EEPROM(I2C)又挂 Flash(SPI)是常态。

I2C 通信流程与排查顺序

以主机写寄存器为例的时序是:START → 从机地址 + 写位 → 从机 ACK → 寄存器地址 → ACK → 数据 → ACK → STOP。读寄存器则是:START → 从机地址 + 写位 → ACK → 寄存器地址 → ACK → Repeated START → 从机地址 + 读位 → ACK → 读数据(主机回 NACK 表示读完)→ STOP。要能说清 START/Repeated START/STOP 的电气含义(SCL 高时 SDA 拉低为 START,拉高为 STOP),以及 ACK 是接收方在第 9 个时钟把 SDA 拉低。

通信不通时的排查顺序建议按”硬件 → 地址 → 时序 → 寄存器配置 → 软件流程”走,从最便宜的证据开始收集:① 先用示波器/逻辑分析仪看 SDA/SCL 有没有波形——没波形说明引脚复用/时钟使能/GPIO 模式配错,或者从机没上电;② 有波形但电平不对,检查上拉电阻(I2C 是开漏输出,必须有上拉,常见 4.7k,速率高时要用 2.2k,长走线要算总线电容)和电平匹配(3.3V 器件与 5V 混挂要用电平转换);③ 抓完整帧看是 START 之后就 NACK 还是地址之后 NACK:地址后立刻 NACK,重点查从机地址本身以及 7bit/8bit 地址的移位差异(数据手册常给 8bit 形式,HAL 里要右移一位)、从机是否上电复位完成、是否处于未就绪状态(有些器件上电后需要等待时间);④ 数据阶段 NACK 则查寄存器地址是否存在、写保护位、器件是否在忙;⑤ SDA 一直被拉低(总线死锁)常见于从机异常或通信中途被打断,需要发 9 个时钟脉冲做总线恢复,再补一个 STOP;⑥ 最后才怀疑速度/时序:把速率降到 100k 试,确认时序参数(建立/保持时间)是否满足手册,DMA 与中断的抢占也可能打乱时序。这套顺序的价值在于每一步都能排除一整类原因,避免盲目改代码。

任务划分与任务间通信

划分原则四条:实时性要求不同的功能分开(否则一个慢操作会拖垮硬实时任务);阻塞型操作不能放在高实时性任务里(等待 I2C/网络应答要单独放低优先级任务);一个任务负责一个相对独立的功能(职责单一才好测);任务之间通过队列、通知、信号量通信,而不是大量共享全局变量。举个可复述的结构:采集任务(高优先级、周期触发、只负责把传感器数据丢进队列)、处理任务(中优先级、从队列取数据做滤波和判断)、通信任务(低优先级、把结果打包发出)、以及监控/喂狗任务。

通信机制按场景选:队列传数据(xQueueCreate / xQueueSend / xQueueReceive,中断里用 xQueueSendFromISR 并配合 portYIELD_FROM_ISR);二值/计数信号量做事件同步(xSemaphoreCreateBinary / xSemaphoreGive / xSemaphoreTake,中断里用 xSemaphoreGiveFromISR),例如 DMA 传输完成中断给处理任务发信号;互斥量保护共享资源(xSemaphoreCreateMutex + Take/Give),例如多个任务共用同一个 UART 或 SPI 总线——注意互斥量有优先级继承,二值信号量没有,保护资源必须用 Mutex;任务通知是最轻量的机制(xTaskNotify / xTaskNotifyFromISR / xTaskNotifyWait / ulTaskNotifyTake),如果只是”通知某个任务有事件发生”,它比专门建一个队列省 RAM、也快得多,缺点是一个任务只有一个通知值、不能多对多。经验原则:能用任务通知就别用信号量,能传值就别传指针,中断里永远只用 FromISR 版本。

FreeRTOS 调度与时间片

FreeRTOS 默认(最常见的配置)是抢占式调度 + 同优先级时间片轮转,由两个宏控制:

#define configUSE_PREEMPTION    1
#define configUSE_TIME_SLICING  1

调度器永远选当前 Ready 任务里优先级最高的运行。configUSE_PREEMPTION = 1 时,只要有更高优先级的任务从 Blocked/Suspended 变成 Ready,当前任务就可能立刻被抢占(典型场景:高优先级任务在等信号量,中断 Give 之后它马上抢占低优先级任务);设为 0 则是非抢占式,当前任务不会被”仅仅因为别人优先级更高”而切走,而要等它自己阻塞、让出(taskYIELD)或被时间片耗尽。

configUSE_TIME_SLICING = 1 控制的是同一最高优先级下多个 Ready 任务之间是否按 tick 轮转:开时每个 tick 中断里会在同优先级任务间切换,保证公平;关掉后同优先级任务一旦运行就一直跑到阻塞或让出(能减少上下文切换开销,适合同优先级任务都能主动让出的场景)。调度触发点主要有三个:tick 中断(时间片与延时到期)、系统调用(vTaskDelay、队列/信号量阻塞与唤醒导致就绪表变化)、以及中断里的 FromISR API(末尾触发 portYIELD_FROM_ISR 决定是否立即切换)。补充两点容易加分的:configTICK_RATE_HZ 决定时间片粒度(常见 1000Hz,即 1ms 一个 tick);configUSE_TICKLESS_IDLE 可在空闲时关掉 tick 中断省电;空闲任务是优先级 0、始终 Ready 的存在,如果你想做低功耗或后台回收,就挂在 idle hook 里。