面灵AI→

智元机器人嵌入式软件一二面:I2C 异常恢复、CAN 仲裁与 DMA 缓存一致性

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

《面试题目》

一面(项目与基础)

  1. 项目中的电压、电流怎么采集?采样结果怎么换算成实际值?
  2. SPI 的时序怎么画?CPOL 和 CPHA 分别影响什么?
  3. I2C 一次读寄存器的过程,包括起始、地址、应答和停止
  4. CAN 多个节点同时发送怎么仲裁?仲裁失败算不算传输错误?
  5. FreeRTOS 任务有哪些状态,优先级和调度怎么影响执行顺序?
  6. static 修饰局部变量、全局变量和函数,各有什么效果?
  7. volatile 用在什么场景?主循环和中断共享变量时,仅加 volatile 够不够?
  8. 进程和线程有什么区别?平时用过哪些同步方式?
  9. 多个任务访问同一个外设,怎么避免操作交叉?
  10. 死锁出现需要哪些条件?项目里怎么避免?

二面(场景与深挖)

  1. I2C 的时钟延展是什么,谁把 SCL 拉低?
  2. 总线忙和设备没有应答,有什么区别?
  3. SDA 卡在低电平,主控能怎么尝试恢复?
  4. 如果卡住的是 SCL,处理方式还是一样吗?
  5. 恢复总线时,其他任务和正在进行的传输怎么处理?
  6. 重试次数、等待时间和恢复失败之后的状态,怎么设计?
  7. 电机节点偶发收不到消息,怎么判断问题在发送、总线还是接收端?
  8. bus-off 是什么状态?出现后怎么恢复,为什么会反复进入?
  9. 节点多了之后,有些报文延迟变大,先看哪些信息?
  10. 通信中断了一阵又恢复,之前排队的控制指令还要不要继续发?
  11. DMA 访问的缓冲区放在哪里,有哪些限制?换芯片后为什么要重新检查?
  12. 启用了 D-cache 以后,CPU 和 DMA 访问同一块内存,可能出什么问题?
  13. 内存踩踏怎么排查?栈溢出和其他越界写,怎么逐步区分?
  14. Cortex-M 上的任务切换保存了哪些现场?PendSV 在这里起什么作用?
  15. 优先级反转是什么?互斥量的优先级继承能解决哪些问题?

《参考解析》

「先讲清一条数据的路径」是嵌入式面试极好的开场题,因为它一次性暴露三件事。 一条完整链路要覆盖:传感器在什么时刻、由谁触发采样(定时器触发 ADC 还是软件轮询)、结果怎么进内存(DMA 搬运还是中断读寄存器)、原始值怎么换算成物理量(参考电压、分辨率、分压/放大倍数、零点与线性度校准)、哪个任务取走它(队列?共享内存加锁?)、以及最终怎么变成发给电机节点的指令(CAN/串口帧、编码方式、周期)。能把这条链路讲顺的人,后面所有问题都有落点——因为 volatile、DMA、cache、同步、实时性都是这条链路上必然出现的问题。

I2C 的异常恢复是这场二面的主线,值得单独理清。 I2C 是开漏总线,SCL 和 SDA 都靠上拉电阻拉高,任何设备都能拉低。几种典型异常要分清:总线忙(Bus Busy) 指 SDA 或 SCL 长时间停在低电平,通常是某个从机复位到一半、恰好卡在发送 ACK 的那一位;设备无应答(NACK) 指主控发完地址后没收到 ACK,说明该地址上没有设备或设备没准备好——这两者性质完全不同,前者是总线状态问题、后者是设备响应问题,混在一起就会像原帖作者那样「想到什么办法就说什么」。时钟延展(Clock Stretching) 是从机拉低 SCL 让主机等待(从机需要时间准备数据),主机必须检测并等待,有些主机控制器不支持或需要超时兜底。恢复手段上:SDA 被拉低而 SCL 还能动时,主控可以手动发送 9 个时钟脉冲,让挂住的从机把剩余位发完从而释放数据线;SCL 本身被持续拉低时,上述办法不适用,只能靠硬件复位(给从机发复位命令)或重新上电,具体取决于器件能力。这类题的高分答法是先判断当前是哪一种故障,再选对应动作,而不是罗列一堆尝试。

恢复期间还得管住并发和状态机。 总线恢复意味着「当前这次传输必然失败」,所以要先把该总线的使用权收回(挂起等待该总线的其他任务、拒绝新请求),处理完再放行;重试要有次数上限和退避,避免在硬件故障时无限打转;恢复失败后设备应进入明确的降级状态(上报错误、周期重试、必要时重启设备),而不是静默地一直失败。简历上写「加个重试就行」的项目,往往就是在这里被问穿的。

CAN 的仲裁与 bus-off。 CAN 是多主总线,靠非破坏性仲裁解决冲突:节点发送时逐位比较总线电平,显性位(0)会覆盖隐性位(1),所以标识符数值越小优先级越高;在仲裁中输掉的节点会立刻退出并转为接收,这不属于错误,只是正常的竞争过程——这是原帖作者差点答错的地方(输掉仲裁和报文传输错误要分开)。真正的错误是位错误、填充错误、CRC 错误、格式错误与应答错误,节点通过 TEC/REC 计数器累积,达到阈值(TEC > 255)就进入 bus-off:节点主动脱离总线、不再收发。恢复方式分快速恢复(协议自动在 128 次连续 11 位隐性位后回到 error active)和人工干预(应用层检测到 bus-off 后重新初始化控制器)。反复进入 bus-off 通常指向物理层问题——终端电阻缺失或阻值不对、线缆过长或分支过长、共模干扰、接地不良、波特率不匹配,所以排查要往硬件看而不是只改软件。多节点下报文延迟变大,先看总线负载率、错误计数、优先级结构是否合理、以及是否有节点在重传风暴。

通信中断恢复后,排队的控制指令不能无脑补发。 这是实时控制系统的经典判断题:控制指令有时效性,中断期间积压的旧指令在恢复后已经过期,补发会导致机构执行错误的动作序列(比如已经过期的目标位置)。正确做法是:恢复后丢弃过期指令、只发最新状态的目标值,或者给指令带时间戳/序号由接收端判断丢弃;同时进入一个明确的「重新同步」流程(读回当前实际状态、重新规划轨迹)。判据是「这条指令是状态型还是事件型」——状态型(目标位置、使能)可以只发最新值,事件型(触发一次动作)则要考虑是否还需要执行。

DMA 与 D-cache 的一致性问题,是 Cortex-M7/A 类上最常见的坑。 DMA 直接读写物理内存、不经过 CPU 的 cache,于是有两种错法:CPU 写的数据还在 cache 里没回写内存,DMA 读到的就是旧值(需要 cache clean);DMA 写入新数据到内存,但 CPU 读到的还是 cache 里的旧副本(需要 cache invalidate)。处理方式有三条:把 DMA 缓冲区放到非 cache 区域(MPU 配置或用不被 cache 的内存)、在传输前后做 clean/invalidate 维护、以及让缓冲区按 cache line 对齐(否则 clean/invalidate 会波及相邻数据,引起别的变量被意外丢弃)。换芯片必须重新检查的原因是 cache line 大小、是否支持 cache 维护指令、总线矩阵与一致性设计都不同,原代码的假设可能直接失效。同理,DMA 缓冲区不能放在栈上或被频繁重分配——它需要在整个传输期间保持有效且地址固定。

Cortex-M 的任务切换与 PendSV。 上下文切换保存的是被调用者保存寄存器(R4–R11)、PSP、以及通过硬件自动压栈的 xPSR/PC/LR/R12/R3–R0(进入异常时硬件自动压入 8 个字);FPU 使能时还会有额外的 S0–S15 与 FPSCR 需要按需保存。PendSV 被设计成最低优先级异常,专门用来做上下文切换:这样当有更高优先级中断正在处理时,PendSV 会被推迟到所有中断处理完再执行,从而避免在中断中途切换任务导致中断被延迟或丢失。它由 SysTick(或其它时基)置起 pending 位触发,本质是「把切换这个动作挂到最空闲的时刻做」。优先级反转则是低优先级任务持有互斥量、高优先级任务等待它、而中优先级任务抢占低优先级任务,导致高优先级任务被间接阻塞;互斥量的优先级继承会临时把持有者提升到等待者的优先级,缩短阻塞时间,但它只解决「单个互斥量」的场景,多个锁嵌套时仍可能死锁,需要靠统一加锁顺序来兜底。