黑芝麻智能科技嵌入式一面:C 语言底座与 Cortex-M 现场排查
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 做一下自我介绍,并说明和嵌入式最相关的一段经历
- const 和宏定义定义「常量」有什么区别?什么时候必须用 const?
- static 用在局部变量、全局变量、函数上分别是什么含义?
- 解释一下大小端,并说明网络字节序和芯片字节序不一致时怎么处理
- 结构体对齐规则是什么?强制 1 字节对齐有什么代价?
- Cortex-M 上电到进入 main,大致经历哪些步骤?
- HardFault 来了,你第一时间怎么查?
- 中断嵌套和优先级分组怎么理解?什么叫抢占优先级?
- SysTick、定时器中断、软件定时器,使用上怎么选?
- UART 空闲中断 + DMA 收变长帧,基本怎么做?
- SPI 全双工通信时,发送和接收是怎样的关系?
《参考解析》
const 与宏:类型检查与作用域是分水岭。 宏是预处理期的文本替换,不做类型检查,容易出现优先级和副作用问题(#define SQ(x) x*x 传 1+2 就错),调试时也看不到符号。const 有类型、受编译器检查、有明确作用域,需要取地址、做函数参数约束、表达只读接口时必须用它。宏仍有不可替代的场合:条件编译、需要字符串化的场景、以及在老标准下当数组长度。嵌入式里有个必答的细节:只读寄存器映射通常写成 volatile const——volatile 保证每次都真的去读、不被优化掉,const 表达程序不写它,两者缺一不可,宏替代不了。
static 的三种含义要分清「生命周期」和「链接属性」。 ① 局部 static:生命周期延续到程序结束(存放在静态存储区而非栈上),但可见性仍限于该作用域,常用于状态保持;注意它不是天然线程安全的,多任务或中断并发访问同一个局部 static 变量需要加保护。② 文件/全局作用域 static:把符号变成内部链接,其他编译单元不可见,用来避免多文件重名冲突。③ 函数加 static:同样是内部链接,避免对外导出。后两种在模块化固件和驱动里很常用(用 static 隐藏内部 API)。
大小端与结构体对齐。 大端是高字节存低地址,小端相反;Cortex-M 多数默认小端,但协议字段、文件格式和部分外设常按大端定义。跨端序处理的原则是按协议规定显式转换或逐字节拼装,绝不能把结构体直接强转成字节流发送——那会同时踩端序和对齐两个坑。判断当前平台可以用 union { uint32_t i; uint8_t c[4]; } 取首字节,或直接用编译器的 __BYTE_ORDER__。结构体对齐的规则是每个成员按自身对齐要求放置、结构体整体对齐取最大成员对齐值;#pragma pack(1) 能紧凑布局便于协议映射,代价是可能产生非对齐访问——性能下降,个别平台直接触发 fault。所以对外通信更稳妥的做法是显式序列化,而不是依赖编译器的内存布局。
Cortex-M 上电到 main 的步骤。 复位后硬件从向量表头取两个 32 位字:第一个是初始 MSP(主栈指针),第二个是 Reset_Handler 的入口地址,然后跳转执行。Reset_Handler 里接着做:拷贝 .data 段从 Flash 到 RAM、把 .bss 清零、做可选的系统时钟初始化与 FPU/Cache 早期配置、调用 C 运行时初始化,最后 bl main。一个常被追问的点:如果应用不在 0 地址运行(有 Bootloader),必须保证 VTOR 指向正确的向量表,否则中断会跳到错误的处理函数。
HardFault 的第一时间排查。 顺序是:先确认确实处在 HardFault 处理函数里(别把别的异常看错),然后从栈帧中取出出错时的 PC、LR、PSR——Cortex-M 在进入异常时会自动压栈 8 个寄存器,判断用的是 MSP 还是 PSP(看 LR 的 EXC_RETURN 位)才能找对栈。拿到 PC 后对照 map 文件和反汇编,能定位到具体函数甚至 C 源码行。常见根因分四类:空指针/野指针、栈溢出、非对齐访问、跳到非法地址执行。如果是栈溢出,还要进一步查任务/主栈的高水位、有没有大的局部数组、以及递归深度。
中断优先级与定时器选型。 NVIC 里通常数值越小优先级越高(以芯片手册为准);抢占优先级决定能否打断正在执行的中断,亚优先级只在同一抢占级内排队。设计原则是把强实时中断放到更高抢占级并尽量缩短其 ISR,因为临界区(关中断)时间过长会让高优先级中断实际「变慢」。定时器选型:SysTick 一般留给 OS tick 或简单节拍,资源唯一,不要往里塞业务逻辑以免拖垮调度节拍;通用硬件定时器适合精确周期采样、PWM、输入捕获;RTOS 软件定时器跑在定时器守护任务上下文,实现方便但精度和实时性弱于硬件定时器,回调里绝不能阻塞(会拖住整个定时器任务)。强实时控制闭环优先用硬件定时器触发再唤醒控制任务。
UART 空闲中断 + DMA 收变长帧。 基本做法是:DMA 在循环或 Normal 模式下把接收数据搬到缓冲区,线路空闲触发 IDLE 中断表示「一帧暂时结束」,在 IDLE 里根据 DMA 的剩余传输计数反算出本帧长度,再把这段数据投递给解析任务。要注意的坑有四个:半包/粘包仍要靠帧头帧尾或长度字段处理(IDLE 只是提示,不是协议保证);IDLE 标志的清除方式因系列而异,清不干净会反复进中断;缓冲区在投递前不能被下一帧 DMA 覆盖(要有双缓冲或及时拷贝);如果平台有 Cache,还要处理 DMA 与 Cache 的一致性(clean/invalidate 或把缓冲区放到非 Cache 区)。SPI 全双工那一问的核心答案是:发送和接收是同一个时钟驱动的同一过程——主机每输出一位就同时移入一位,所以「读」必须靠发送(通常是空字节)来产生时钟,收发长度恒等,不能用「先发完再读」的思路理解。