汇川嵌入式软件开发凉经:八股只是第一刀,追问才会有第二刀
- 轮次
- 一面+二面
- 结果
- 已挂
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面:C 语言、MCU 和项目基础
- 从系统上电开始讲简历里最熟悉的一个 MCU 项目:任务怎么启动、数据怎么流、最后怎么控制执行器
- static 修饰局部变量、全局变量和函数分别有什么作用?
- const 和宏定义有什么区别?
- 指针常量和常量指针怎么区分?
- 函数指针在嵌入式项目里有哪些实际用途?
- 结构体为什么需要内存对齐?
- 给一个结构体,计算它在 32 位平台上的大小
- 大端和小端分别是什么,怎么用 C 代码判断?
- volatile 一般用在什么地方?
- 中断和主循环同时访问一个变量,只加 volatile 是否安全?
- 栈和堆有什么区别?
- MCU 出现栈溢出通常会有什么表现?
- 野指针是怎么产生的,怎么排查?
- memcpy 遇到内存重叠会怎样,应该换成什么?
- MCU 从上电到进入 main 函数之前做了什么?
- 中断响应的完整过程?
- 中断里为什么不建议做耗时操作?
- 中断优先级是怎么配置的?
- DMA 和中断接收串口数据分别适合什么场景?
- 串口数据长度不固定,怎么判断一帧接收完成?
- I²C 为什么需要上拉电阻?
- SPI 四种模式是怎么区分的?
- 看门狗怎么使用,直接在定时器中喂狗有什么问题?
一面手撕
- 不用库函数实现字符串转整数,需要考虑正负号、非法字符和溢出
- 实现一个固定长度的环形缓冲区,写出初始化、写入和读取;追问:如果中断负责写、任务负责读,会不会有并发问题?
二面:RTOS、CAN 和故障定位
- 任务调度的基本原理是什么?
- 抢占式调度和时间片调度有什么区别?
- 同优先级任务一定会轮流运行吗?
- 任务有哪些状态,状态之间怎么切换?
- 为什么不能在中断里调用普通版本的队列发送函数?
- FromISR 版本和普通版本有什么区别?
- 二值信号量、计数信号量和互斥锁分别适合什么场景?
- 互斥锁为什么需要优先级继承?什么是优先级反转?
- 高优先级任务长期得不到运行,可能是什么原因?
- 软件定时器的回调函数在哪个任务里执行?回调阻塞太久会造成什么影响?
- 怎么估算每个任务需要分配多少栈?
- 系统偶发进入 HardFault,怎么判断是不是任务栈溢出?
- 内存碎片是怎么产生的?为什么有些嵌入式项目不允许动态申请内存?
- CAN 总线为什么能够解决多个节点同时发送的问题?
- 显性位和隐性位分别是什么?CAN 仲裁失败算不算发送错误?
- 标准帧和扩展帧有什么区别?CAN ID 越小为什么优先级越高?
- 位填充的作用是什么?ACK 槽由谁发送?
- TEC 和 REC 分别表示什么?Error Active、Error Passive 和 Bus-Off 怎么切换?
- 节点进入 Bus-Off 后应该怎么恢复?
- 总线上偶发大量错误帧,先从软件还是硬件排查?
- 终端电阻为什么一般是 120Ω?两个终端电阻并联后测量为什么接近 60Ω?
- 波特率、采样点和线缆长度之间有什么关系?
- 接收消息很多时,怎么避免某类低优先级报文饿死?
- 多个相同设备使用同一套程序,节点地址怎么分配?
《参考解析》
这轮面试的结构值得单独说。 作者自己的复盘抓到要点:题目之间是连着的。你说项目用了 FreeRTOS,面试官就接着问任务优先级、任务间通信和栈空间;你说通过 CAN 控制设备,他就问错误处理和 Bus-Off;你说做了看门狗,他就问怎么判断究竟是哪一个任务卡死。所以「只能说出用了什么」和「能说清为什么这样设计、异常发生后怎么恢复」在这类面试里是两条完全不同的分数线。准备方法可以固定成每个技术点往下追三层:为什么用它 → 它带来什么代价 → 出故障时现象是什么、怎么定位。
volatile 与并发那问是经典陷阱。 「中断和主循环同时访问一个变量,只加 volatile 是否安全」的答案是不安全:volatile 只保证每次访问真的去内存读写、不被编译器缓存到寄存器或优化掉,它不保证原子性、不提供内存屏障。对单字节标志位,volatile 通常够用;但对 uint32_t 计数器或结构体这类多字节共享数据,读改写仍然可能被打断(比如主循环正在写高 16 位、中断进来读走了半新半旧的值),必须关中断或用原子操作保护。回答时把「volatile 解决什么、不解决什么」两句话分清楚,比堆术语有用。
互斥锁为什么需要优先级继承。 场景是优先级反转:低优先级任务 L 持有互斥锁,高优先级任务 H 等这把锁而被阻塞,此时中优先级任务 M 就绪——M 抢占了 L,L 迟迟释放不了锁,结果 H 被 M 无限期拖延。优先级继承的做法是:当 H 阻塞在 L 持有的锁上时,临时把 L 的优先级提升到 H 的水平,让 L 尽快跑完临界区释放锁,H 才能继续。注意它只是缓解而不是消除,且只对互斥锁有效——二值信号量没有所有者概念,做不了继承,所以保护共享资源要用互斥量而不是信号量。更彻底的办法是把临界区设计得足够短,或用无锁队列传递所有权。
环形缓冲区手撕的追加追问。 写完初始化/写/读之后被问「中断写、任务读会不会有并发问题」,这是嵌入式里最常见的生产者消费者。答案取决于实现细节:单生产者单消费者(只有中断写、只有任务读)下,只要读写指针各自只由一个上下文写、并且用 volatile 或内存屏障保证可见性,就可以做成无锁;麻烦的是判断空和满——习惯上留一个空槽(head == tail 为空、head+1 == tail 为满)或额外维护一个计数。一旦写指针不止一个写者(比如两个中断都可能写),就必须关中断或用临界区保护。答题时把「谁写谁读、各几个上下文、临界区多长」这三点先问清楚,再给方案,比直接写代码得分高。
CAN 的几个必答点。 仲裁能解决多节点同时发送,靠的是显性位覆盖隐性位:节点边发边听,谁发出隐性却读到显性就立刻退出,ID 越小(前导显性位越多)优先级越高,且仲裁失败不算发送错误、可自动重发。位填充是为了让接收方能从跳变沿恢复时钟,连续 5 个相同电平后强制插入一个反相位。ACK 槽由所有正确接收到的其他节点共同拉低,发送方据此确认至少有一个节点收到。错误状态机由 TEC/REC 两个计数器驱动:错误计数超过阈值从 Error Active 降到 Error Passive,再恶化就进入 Bus-Off(节点脱离总线),恢复方式是等待一段静默期后重新初始化并重入总线(单片机通常要求软件显式处理,别指望自动恢复)。终端电阻 120Ω 是匹配双绞线特性阻抗以吸收反射,两个终端并联后测量自然接近 60Ω。总线上偶发大量错误帧时,先怀疑硬件——线缆、终端电阻、共地、屏蔽、分支长度,这些远比软件更常见。
「偶发 HardFault 怎么判断是栈溢出」的思路。 可以分三层取证:① 静态上计算最深调用链加中断嵌套的栈需求,和分配量对比;② 运行时给每个任务的栈填魔术字(如 0xA5),跑一段时间后统计未被打扰的长度得到高水位,这是最直接的证据;③ 崩溃现场取证——从栈帧取出出错时的 PC/LR/PSR,对照 map 和反汇编定位到函数,同时看 SCB 里的异常状态寄存器判断是哪个异常;FreeRTOS 下还可以开 configCHECK_FOR_STACK_OVERFLOW 钩子或栈底 canary。注意区分「任务栈溢出」和「主栈(MSP)溢出」:中断和启动阶段用的是主栈,任务用的是 PSP,两者独立。