智元机器人嵌入式软件一二面:I2C 异常恢复、CAN 仲裁与 DMA 缓存一致性
- 轮次
- 一面+二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(项目与基础)
- 项目中的电压、电流怎么采集?采样结果怎么换算成实际值?
- SPI 的时序怎么画?CPOL 和 CPHA 分别影响什么?
- I2C 一次读寄存器的过程,包括起始、地址、应答和停止
- CAN 多个节点同时发送怎么仲裁?仲裁失败算不算传输错误?
- FreeRTOS 任务有哪些状态,优先级和调度怎么影响执行顺序?
- static 修饰局部变量、全局变量和函数,各有什么效果?
- volatile 用在什么场景?主循环和中断共享变量时,仅加 volatile 够不够?
- 进程和线程有什么区别?平时用过哪些同步方式?
- 多个任务访问同一个外设,怎么避免操作交叉?
- 死锁出现需要哪些条件?项目里怎么避免?
二面(场景与深挖)
- I2C 的时钟延展是什么,谁把 SCL 拉低?
- 总线忙和设备没有应答,有什么区别?
- SDA 卡在低电平,主控能怎么尝试恢复?
- 如果卡住的是 SCL,处理方式还是一样吗?
- 恢复总线时,其他任务和正在进行的传输怎么处理?
- 重试次数、等待时间和恢复失败之后的状态,怎么设计?
- 电机节点偶发收不到消息,怎么判断问题在发送、总线还是接收端?
- bus-off 是什么状态?出现后怎么恢复,为什么会反复进入?
- 节点多了之后,有些报文延迟变大,先看哪些信息?
- 通信中断了一阵又恢复,之前排队的控制指令还要不要继续发?
- DMA 访问的缓冲区放在哪里,有哪些限制?换芯片后为什么要重新检查?
- 启用了 D-cache 以后,CPU 和 DMA 访问同一块内存,可能出什么问题?
- 内存踩踏怎么排查?栈溢出和其他越界写,怎么逐步区分?
- Cortex-M 上的任务切换保存了哪些现场?PendSV 在这里起什么作用?
- 优先级反转是什么?互斥量的优先级继承能解决哪些问题?
《参考解析》
「先讲清一条数据的路径」是嵌入式面试极好的开场题,因为它一次性暴露三件事。 一条完整链路要覆盖:传感器在什么时刻、由谁触发采样(定时器触发 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 位触发,本质是「把切换这个动作挂到最空闲的时刻做」。优先级反转则是低优先级任务持有互斥量、高优先级任务等待它、而中优先级任务抢占低优先级任务,导致高优先级任务被间接阻塞;互斥量的优先级继承会临时把持有者提升到等待者的优先级,缩短阻塞时间,但它只解决「单个互斥量」的场景,多个锁嵌套时仍可能死锁,需要靠统一加锁顺序来兜底。