嵌入式面试题库

32 道,校招 14 道(C 语言、GPIO、串口、I2C / SPI、看门狗、启动流程、交叉编译),社招 18 道(RTOS、驱动、量产和工程判断)。每题三段:这题想听什么、要点、大多数人错在哪。
背下来没什么用。面试官会往下追问,第二问答不上,前面背的那些反而暴露得更多。

共 32 题
01数组和指针到底有什么区别?为什么函数里 sizeof 算不对?

这题想听什么

校招最常问的一题,但答得好坏差距很大。想听的不是「数组就是指针」这句错话,而是你知不知道「退化」发生在什么时候。

要点

  • 类型不同:int a[10] 是「10 个 int 的数组」,int *p 是「指向 int 的指针」。sizeof(a) 得 40,sizeof(p) 得 4(32 位机)。
  • 数组名在绝大多数表达式里退化成首元素地址,所以能 p = a;但它本身不是变量,不能 a++。
  • 两个例外:sizeof(a) 和 &a 不退化。&a 的类型是 int (*)[10],加 1 会跳 40 字节。
  • 退化发生在形参:void f(int a[10]) 和 void f(int *a) 完全等价,所以函数内 sizeof 永远是 4。传数组必须同时传长度。
  • 二维数组更绕:int m[3][4] 作为形参要写成 int (*m)[4],第二维不能省。

容易答错的地方

说「数组就是指针,只是大小不同」。它们连类型都不同,只是会退化——这个区别在算偏移、做类型检查、传参时全部暴露出来。

02static 加在不同位置分别是什么意思?

这题想听什么

三种用法各不相同,但只有真正写过模块化代码的人才答得顺。嵌入式里它还有第四种隐含用途:省 RAM。

要点

  • 函数内局部变量:生命周期变成整个程序,存放在 .data(有初值)或 .bss(无初值),不在栈上;只初始化一次,函数退出值保留。
  • 文件作用域的变量或函数:内部链接,别的 .c 文件链接不到。这是 C 语言实现「模块私有」的手段。
  • 多个源文件同名:不加 static 会撞符号(链接期报重复定义);加了 static 各自独立。这在多人协作的 BSP 里天天遇到。
  • 可重入性提醒:static 局部变量让函数不可重入,在中断和主循环共享时会出竞态。
  • RAM 代价:很多人为了「省栈」把大数组改成 static,结果 RAM 占用反而固定住了,栈余量被测出来变小——这两件事不是等价的。

容易答错的地方

「static 变量放在静态存储区」说对了但答完了——面试官接着会问:有初值和无初值分别落在哪个段?为什么 .bss 不占 Flash 空间?

03const 和指针的四种组合,怎么一眼读出来?

这题想听什么

不是考记忆,是考你有没有一套稳定的规则。规则比背四种写法更值钱。

要点

  • 读法:从变量名出发,先看右边,再看左边;const 修饰的是它左边最近的那个东西,左边没有才看右边。
  • const int *p / int const *p:指向常量——不能通过 p 改内容,p 自己可以改。
  • int * const p:常量指针——p 不能再指向别处,但可以改 *p。必须初始化。
  • const int * const p:都锁死。
  • 嵌入式习惯:只读查表、字库、状态名字符串一律 const,让链接器放到 Flash(.rodata),省掉一份 RAM 拷贝。

容易答错的地方

说「const 变量一定在 Flash 里」。它只是「不可改」,能否进 Flash 还得看链接脚本和是否有取地址传给非 const 形参。

04堆和栈有什么区别?嵌入式里为什么尽量不用 malloc?

这题想听什么

这题是「有没有吃过动态内存的苦」的分水岭。答「heap 大 stack 小」基本就结束了。

要点

  • 栈:编译器分配与回收,函数返回自动释放;大小在启动文件里定死(Stack_Size),在 SRAM 顶端向下生长。溢出不会报错,只会踩坏相邻变量或 HardFault。
  • 堆:运行时分配,需要自己管理生命周期;碎片化、分配时间不确定、失败路径难写。
  • 量产项目里 malloc 的四个具体风险:长期运行后碎片导致申请失败;分配耗时不确定破坏实时性;free 后被继续使用(use-after-free)在 MCU 上极难定位;内存被踩时现象随机。
  • 替代方案:编译期固定的静态内存池、定长块内存池(block pool)、环形缓冲区。这些才是量产代码的主流做法。
  • 真要用的场合:初始化阶段一次性分配、生命周期与程序等长、可接受不可恢复的失败。用完不复用就不产生碎片。

容易答错的地方

说「栈溢出会抛异常」。裸机上没有这种东西,栈溢出通常表现为某个全局变量莫名其妙被改写、或者跳进 HardFault。

05#define 和 inline 有什么区别?宏最容易踩什么坑?

这题想听什么

经典送分题,但后面往往接一句「写个安全版的宏」——那才是真考点。

要点

  • #define 是预处理阶段的文本替换:无类型检查、无作用域、调试器看不到符号、出错信息指向展开后的代码。
  • 经典坑:#define SQR(x) x*x 时 SQR(1+1) 展开成 1+1*1+1 = 3。正确写法:每个参数加括号、整体加括号,必要时配 do { } while (0)。
  • 无参宏的坑:#define MAX 100 之后有人 MAX++,报错指向一个看不见的地方。
  • inline 是编译期的语义,有类型检查、能调试、能取地址。
  • 但 inline 只是建议:-O0 下通常不展开;函数体过大、含循环、被取地址时也不会展开。
  • 常量优先 enum 或 static const,函数优先 static inline——这是驱动和 HAL 层的通行写法。

容易答错的地方

说「inline 一定比普通函数快」。它省的是调用开销,代价是代码体积——在 Flash 只有 64KB 的片子上,滥用 inline 会撑爆。

06中断服务函数里为什么不能延时、不能 printf?

这题想听什么

考你有没有「中断是稀缺资源」的概念。答「会卡住」太浅,要说到具体后果。

要点

  • ISR 运行在中断上下文,同优先级及更低的优先级中断被屏蔽。在里面忙等延时,等于把整个系统的实时性拖垮。
  • 丢事件比丢时间更致命:UART 接收中断里延时,FIFO 溢出,数据直接丢;编码器脉冲计数漏掉,位置就错了。
  • printf 三重问题:内部可能阻塞等发送完成;不可重入(多中断/主循环同时调用会撕碎缓冲);栈开销大,容易把中断栈打穿。
  • 正确做法:ISR 只做三件事——读寄存器清标志、把数据塞进环形缓冲区、置事件标志或发信号量,其余交给任务处理。
  • RTOS 里用带 FromISR 后缀的 API,并在退出时处理 portYIELD_FROM_ISR。

容易答错的地方

答「ISR 里什么函数都不能调」。可以调,条件是可重入、执行时间确定、不阻塞——这些都是可以逐条讲清楚的判断标准。

07推挽输出和开漏输出有什么区别?什么时候必须用开漏?

这题想听什么

GPIO 配置里的第一个真问题。答不出来的人,基本没独立画过原理图。

要点

  • 推挽:上管拉高、下管拉低,高低电平均由芯片主动驱动,驱动能力强、边沿快。
  • 开漏:只有下管,只能主动拉低;高电平时管子截止,引脚呈高阻,电平由外部上拉电阻决定。
  • 开漏的四个必要场景:I2C 的 SDA/SCL(多设备线与,靠上拉出高电平);电平转换(3.3V 器件驱动 5V 总线);多个输出并接做线与;需要主动释放总线让从设备应答。
  • 致命错误:两个推挽输出直接对接,一个输出高一个输出低,形成短路,轻则大电流发热,重则烧引脚。
  • 代价:开漏上升沿靠 RC 充电,速度受上拉阻值限制——上拉越小越快,但静态功耗越大。1MHz 以上的 I2C 通常要降到 2.2k 甚至更低(或用有源上拉)。

容易答错的地方

「开漏输出高电平就是高阻」——对,但接着说「所以读回来是悬空值」就错了。输入通道通常仍有效,悬空会被外部上拉拉高,若没上拉才是浮空。

08上拉电阻是干什么的?按键为什么常用上拉而不是下拉?

这题想听什么

这题考的是「有没有被浮空输入坑过」。答「防止悬空」只是及格线。

要点

  • 核心作用:给高阻节点一个确定的默认电平。CMOS 输入悬空时电平不确定,会在中间电平反复翻转,既误触发又增大功耗。
  • 按键接法:上拉 + 按下接地。松开时读到 1,按下读到 0,线断了(断线)读到 1 是安全态;如果反过来用下拉 + 按下接 VCC,线一断就永远读不到按下,故障不暴露。
  • 阻值权衡:太大(1M)抗干扰差、上升沿慢、易受漏电流影响;太小(100Ω)按下时功耗大。常用 4.7k–10k,低功耗产品用 100k + 内部上拉。
  • 内部上拉 vs 外部上拉:内部通常 30k–50k,弱且离散度大,只适合短走线、低干扰场合;I2C 这类必须用外部。
  • 配套的还有软件消抖:机械按键抖动 5–20ms,用定时采样(连续两次间隔 10ms 读到相同值)比 delay 靠谱。

容易答错的地方

说「上拉是为了提高驱动能力」。上拉电阻是弱上拉,驱动能力很小,它解决的是电平确定性和线与,不是驱动。

09串口波特率是怎么算的?为什么两边必须一致?

这题想听什么

看你是不是只会调 HAL_UART_Init。能说清误差来源的人,通常自己配过时钟树。

要点

  • UART 是异步的:没有时钟线,靠双方约定的位时间采样。所以「一致」不是协议要求,是物理必然。
  • 采样在起始位下降沿对齐,之后每个位时间采一次,通常在位中间采。收方要在 10 个位时间内累积误差仍小于半个位宽,实践上限是 2%–3%。
  • STM32 的分频:USARTDIV = fCK / (16 × baud),整数部分和小数部分分别写进 BRR。小数位只有 4 bit,所以分频一定存在取整误差。
  • 误差来源排序:总线时钟本身不准(内部 RC 常温 ±1%、全温区可到 ±5%)> BRR 取整误差 > 走线容差。用外部晶振能把第一项压到 ±20ppm。
  • 典型翻车场景:低波特率用内部 RC 正常,切到 115200 就乱码——不是代码问题,是时钟源精度问题。

容易答错的地方

忽略帧格式:波特率只定义位时间,数据位、校验位、停止位不匹配照样收不对。调试时先确认 8N1,再怀疑波特率。

10I2C 和 SPI 怎么选?各自的关键差异是什么?

这题想听什么

选型题,考的是工程判断而不是参数罗列。能说出「为什么这个项目选了它」的加很多分。

要点

  • 线数:I2C 两根(SCL/SDA)可挂多个从机,靠 7 位地址寻址;SPI 至少四根(SCK/MOSI/MISO/CS),每个从机一根独立片选。
  • 速率:I2C 常见 100k/400k,快速模式 1M;SPI 常见几 MHz 到几十 MHz,全双工。
  • 可靠性机制:I2C 有 ACK 和总线仲裁,能发现从机不在;SPI 没有任何应答,器件没接上你也不知道。
  • 硬件代价:I2C 必须外部上拉,总线电容上限约 400pF,走线长、器件多会拖慢边沿;SPI 推挽驱动,走线可以长一些。
  • 选型经验:板内多传感器、引脚紧张 → I2C;大吞吐(Flash、LCD、ADC 连续采样)→ SPI;需要热插拔检测、多主 → I2C;成本极敏感的板子少两根线也是一笔钱。

容易答错的地方

说「I2C 比 SPI 快」。方向反了——I2C 是这两者里慢的那个,它的优势是省引脚和支持多设备寻址。

11看门狗是干什么的?应该在哪里喂狗?

这题想听什么

「喂狗」人人会写,「喂对位置」只有被现场问题教育过的人才知道。

要点

  • 本质:一个独立的倒计时器,超时未喂就复位。用来兜住「程序跑飞、死循环、任务卡死」这类没法靠代码自己恢复的故障。
  • STM32 两套:IWDG 用内部 LSI,独立于主时钟,主时钟挂了照样复位;WWDG 挂在 APB 上,有喂狗窗口,喂早了也会复位。
  • 喂狗位置是核心考点:必须放在「所有关键任务本轮都已正常完成」的地方——比如各任务置标志、看门狗任务检查全部标志后才喂。
  • 错误做法:在定时器中断里喂狗。主循环已经死了,中断还在跑,狗照样被喂饱,等于没有看门狗。
  • 超时时间取值:大于最坏情况的任务周期 2–3 倍,同时小于人能容忍的宕机时间。取 1–2 秒是常见折中。
  • 调试期要能关:用 DBGMCU 的冻结位让断点时暂停计数,否则单步调试会被反复复位。

容易答错的地方

说「看门狗能防所有死机」。它只能兜住「没喂狗」这一类;堆踩坏、栈溢出后仍然正常喂狗的故障,它救不了。

12STM32 从复位到 main 之间发生了什么?

这题想听什么

这题能区分「会写应用」和「理解芯片」。答得出来的人,遇到启动阶段就死机的问题不会束手无策。

要点

  • 复位后硬件固定从向量表取两个值:偏移 0x00 取初始 MSP(栈顶地址),偏移 0x04 取 Reset_Handler 入口。这就是为什么链接脚本里向量表必须放在 Flash 起始。
  • Reset_Handler 里依次:调 SystemInit(配时钟、向量表重定位)→ 把 .data 从 Flash 拷到 RAM → 把 .bss 清零 → 调 __libc_init_array(执行 C++ 全局构造和 __attribute__((constructor)))→ 跳 main。
  • 所以「全局变量默认是 0」不是 C 语言免费送的,是启动代码逐字节清出来的。改链接脚本却没改启动代码,就会出现未初始化变量是随机值。
  • 常见启动阶段故障:栈顶地址写成 RAM 之外(一进 main 前就 HardFault);.data 拷贝长度算错(全局变量初值全是乱码);时钟配置太快导致 Flash 等待周期不足。
  • 能接的第二问:Bootloader 跳转应用前为什么要重设 MSP、关中断、重定位向量表(SCB->VTOR)。

容易答错的地方

说「复位后直接跳 main」。中间的启动代码是汇编写的,就在 startup_xxx.s 里,翻一遍成本很低但收益很大。

13RTOS 里任务有哪几种状态?任务栈大小怎么定?

这题想听什么

入门题,但第二问是实战题。答「估不出来就开大点」的人,通常没被 RAM 卡过。

要点

  • 四态:运行 Running(单核同时只有一个)、就绪 Ready、阻塞 Blocked(等信号量/延时/队列)、挂起 Suspended(显式挂起,不参与调度)。
  • 状态迁移的关键点:调用 vTaskDelay 是主动让出并进入阻塞;优先级更高的任务就绪会立刻抢占。
  • 栈大小难估的原因:MCU 上任务栈要容纳调用链上所有帧 + 局部数组 + 中断嵌套上下文。最坏路径通常发生在一个不常走的错误分支里。
  • 定量方法:uxTaskGetStackHighWaterMark() 看历史最低余量;或初始化时把栈填 0xA5,跑完压力测试数还有多少没被改写。
  • 工程习惯:先给 512–1024 字(不是字节)跑起来,压力测试后按实测余量收到 1.5 倍。栈溢出在 FreeRTOS 里可以用 configCHECK_FOR_STACK_OVERFLOW 钩住。
  • 别把大数组放任务栈里——那是把 2KB 的局部缓冲变成 2KB 的栈需求,改成 static 或内存池更合理。

容易答错的地方

混淆「栈深度单位」。FreeRTOS 的栈大小参数单位是 字(StackType_t),不是字节,写 128 是 512 字节。

14什么是交叉编译?编译好的程序拷到板子上为什么跑不起来?

这题想听什么

嵌入式 Linux 的第一道门槛。答「跑不起来是因为架构不同」只是第一层,第二层是动态库依赖。

要点

  • 交叉编译:在 x86_64 主机上生成目标架构(aarch64 / armhf)的可执行文件。因为目标板 CPU 弱、内存小,跑不动编译器。
  • 工具链命名规则:aarch64-linux-gnu-gcc = 架构-厂商/系统-ABI-工具。前缀不写全,很容易拿错工具链。
  • 编译只是第一步,还要处理 sysroot:头文件、库都要用目标板的,不能链接到主机的 /usr/lib。
  • 跑不起来的三个典型原因:架构不对(Exec format error);动态加载器路径不存在(报 No such file or directory,但文件明明在——其实是 /lib/ld-linux-aarch64.so.1 缺);glibc 版本低于编译机(报 GLIBC_2.xx not found)。
  • 排查顺序:file 程序 看架构 → readelf -l 看 interpreter 路径 → ldd 或 objdump -T 看依赖版本。
  • 规避手段:工具链用与根文件系统同源的(厂商 SDK 自带的那套);必要时静态链接,代价是体积和升级困难。

容易答错的地方

遇到 No such file or directory 就去检查文件权限或路径。这个报错的十次里有九次是动态加载器缺失,不是文件不存在。

01volatile 有三个用途,什么时候不加会出事?

这题想听什么

不是让你背「告诉编译器不要优化」,而是看你知不知道编译器会做哪些合法假设,以及这些假设在什么时候是错的。

要点

  • 必须用的三种场景:硬件寄存器(每次访问都要真的读总线)、ISR 与主循环共享的标志、多任务间共享的简单标志。
  • 不加的后果:编译器认为「这个变量只有当前执行流会改」,于是把它缓存进寄存器,循环里不再回读,while (flag) 变成死循环。
  • 指针声明的区别:volatile uint32_t *p 是指向的数据 volatile;uint32_t * volatile p 是指针本身 volatile。面试常在这里挖坑。
  • 它只禁止缓存与合并访问,不保证原子性,也不替代内存屏障。跨核或跨线程的顺序问题要另外处理。

容易答错的地方

说「volatile 用来做多线程同步」。它不是同步原语,不解决竞态——两个核同时自增一个 volatile 变量,照样丢更新。

02结构体内存对齐怎么算?#pragma pack 的代价是什么?

这题想听什么

能不能手算 sizeof,以及知不知道「省空间」在嵌入式里要拿什么换。

要点

  • 规则:每个成员的偏移必须是 min(自身对齐, pack 值) 的整数倍;结构体总大小必须是最大对齐的整数倍。
  • 算例:struct {char a; int b; char c;} → a@0,补 3 字节,b@4,c@8,尾部补 3 → 12,不是 9。
  • 调整成员顺序可以省空间:{int b; char a; char c;} → 8。
  • #pragma pack(1) 或 __attribute__((packed)) 会留下非对齐成员。Cortex-M3/M4 对单字 LDR/STR 容忍非对齐(慢),但 LDM/STM、双字访问、部分外设的 DMA 描述符要求对齐,可能直接 UsageFault。
  • 嵌入式典型用途:协议帧解析、Flash 存储布局、DMA 描述符。这三处对布局有硬要求,所以对齐不是「编译器的事」。

容易答错的地方

把 sizeof 当成成员大小相加。也有人答「packed 一定更快」——恰恰相反,非对齐访问在多数核上更慢。

03static 的三个作用,在嵌入式里分别对应什么场景?

这题想听什么

基础语法背后是链接属性和内存布局,答不出存储位置说明你只是会写,不理解它落在哪。

要点

  • 修饰局部变量:生命周期延长到整个程序,放在 .data / .bss,但作用域仍只在函数内。
  • 修饰全局变量或函数:链接属性变成内部链接(internal linkage),只在本编译单元可见,用来做模块私有状态、避免符号冲突。
  • 修饰类成员(C++):属于类而不是对象,常用于单例式的硬件实例。
  • 嵌入式实际用法:中断里的计数器、模块内部的上下文、状态机当前状态——都该是 static,别污染全局符号表。
  • 附带考点:未初始化的 static 自动清零(.bss),局部变量不清零。

容易答错的地方

只说「静态变量」。面试官接着问「它在哪个段、占不占 Flash」就答不上来了——初始化为非零的 static 会占 Flash(初值存在 .data 的加载副本里)。

04中断服务函数里能不能 malloc、printf、加锁?

这题想听什么

这是最能区分「调过库」和「出过事」的一道题。三个问题的答案都是「不能」,但理由各不相同。

要点

  • malloc:堆管理不可重入。中断打断正在进行的 malloc,空闲链表被改坏,堆静默损坏,症状是很久之后的随机死机。
  • printf:阻塞等串口 FIFO,格式化开销大(浮点能到毫秒级),多数实现内部有锁且不可重入。轻则丢字符,重则死锁。
  • 加锁:如果主循环已经持有这把锁,中断里再取就是自死锁——中断不能睡眠等待,CPU 卡在那里等一个永远不会释放的锁。
  • 正确做法:ISR 里只做最小动作——清标志、读数据进缓冲区、发信号量或置标志;真正的处理放到任务或主循环。
  • 日志的正解:环形缓冲区 + 后台 DMA 输出,ISR 只写指针。

容易答错的地方

答「可以,只要够快」。速度不是理由,可重入性和阻塞语义才是。也有人忘了一点:Newlib 的 printf 需要 _sbrk,重定向不当直接进 HardFault。

05STM32 从复位到 main,中间发生了什么?

这题想听什么

你排不排得了「程序进不去 main」这类问题。答得出流程,才谈得上定位。

要点

  • BOOT0 / BOOT1 决定启动映射:主 Flash、系统存储器(内置 bootloader)、还是 SRAM。
  • 从映射地址取前两个字:第一个是初始 MSP(栈顶),第二个是 Reset_Handler 地址。
  • Reset_Handler 依次做:调 SystemInit(时钟、FPU、向量表位置)→ 把 .data 从 Flash 拷到 RAM → 清零 .bss → 调 __libc_init_array(C++ 全局构造、init 段)→ main。
  • 踩坑对应关系:.data 没拷 → 全局变量初值不对;VTOR 没设 → 重定位向量表后中断跑飞;启动文件里的 Stack_Size 被改小 → 一进深调用就崩。
  • 延伸:.bss 不占 Flash,只记录大小,启动时整段清零——这就是为什么「大数组不初始化」省空间。

容易答错的地方

只说「初始化外设然后进 main」。外设初始化是 main 里的事,复位到 main 之间只有启动文件和 SystemInit。

06局部数组越界,为什么有时候没事,有时候直接 HardFault?

这题想听什么

你知不知道栈上有什么,以及「没崩」和「没 bug」是两件事。

要点

  • 越界写在栈上,可能覆盖三类东西:相邻的局部变量(值莫名变化)、保存的寄存器(函数返回后跑飞)、返回地址(跳到非法地址 → HardFault)。
  • 「没事」的原因通常是:写到未使用区域;被覆盖的变量之后又被重新赋值;或者只是还没被执行路径碰到。
  • 更隐蔽的一类:堆越界破坏 malloc 元数据,故障点在几百毫秒之后、在完全无关的函数里。
  • 排查手段:栈填充固定图案(如 0xDEADBEEF)看破坏范围;开 -fstack-protector;用 MPU 给栈划保护区;在 HardFault_Handler 里取出压栈的 PC / LR 定位出错指令。

容易答错的地方

答「因为编译器优化」。真正的原因是内存布局和栈深度在变,跟优化级别只是间接相关。

07看门狗应该在哪里喂?为什么放定时器中断里是错的?

这题想听什么

你有没有想过「看门狗本身也会失效」。

要点

  • 放定时器中断里喂是典型的假喂狗:主循环卡死、任务死锁,定时器照跑,狗照喂,系统永远不会复位——现场表现为「死机了但没重启」。
  • 正确做法:在主循环或一个专门的健康检查任务里喂,喂之前汇总各任务的运行心跳(每个任务周期置位,全部置位才喂,然后一起清零)。
  • 调试期可以用 DBGMCU 冻结看门狗,但别在产品代码里留「检测到调试器就关狗」的分支。
  • 延伸:窗口看门狗(WWDG)要求在规定时间窗口内喂,喂太早也复位,能抓「流程被跳过、程序跑太快」这类问题。

容易答错的地方

答「随便哪里,只要定期喂」。面试官会追问:「那如果喂狗的那个任务自己挂了呢?」

08I2C 从设备把 SDA 拉低不放,总线卡死怎么恢复?

这题想听什么

这题几乎只出现在「做过量产」的人身上。没踩过就答不出恢复手法。

要点

  • 成因:从机在传输中途被复位或掉电,它停在「等下一个时钟」的状态并保持 SDA 低电平。主机想发 START 需要拉低 SDA,但 SDA 已经被拉死,发不出去,BUSY 位永远不清。
  • 恢复:把 SCL 和 SDA 切成 GPIO 开漏,手动补 9 个以上时钟脉冲,直到从机把数据位吐完、释放 SDA;然后按「SCL 高时 SDA 上升」补一个 STOP,再 deinit / init 外设。
  • 预防:上下拉电阻合理(4.7k 起步,看总线电容)、上电顺序控制、每次传输加超时、软件模拟 I2C 时给关键段加临界区保护。
  • 延伸:时钟延展(clock stretching)处理不当、多主竞争仲裁失败,也会表现为「卡住」,但成因不同。

容易答错的地方

答「重新初始化外设」。硬件已经把 SDA 拉死了,reinit 也没用——必须用时钟脉冲把从机状态机推出来。

09DMA 传输时为什么要处理 Cache?哪些芯片其实没这个问题?

这题想听什么

这题一半人答错在第一句:先确认平台有没有 D-Cache。

要点

  • 先分清:Cortex-M3 / M4 没有 D-Cache(只有 I-Cache,不影响数据),所以裸机 M4 上不存在这个问题。Cortex-M7、A 系列、带 D-Cache 的 SoC 才有。
  • 发送方向:CPU 写完缓冲区后启动 DMA,必须先 clean(按 cache line 写回内存),否则 DMA 从内存读到的是旧数据。
  • 接收方向:DMA 写完后 CPU 去读,必须先 invalidate,否则读到的是 Cache 里的旧值。
  • 注意 cache line 对齐:缓冲区不在行首尾时,invalidate 会把相邻数据一起丢掉。要么 __attribute__((aligned(32))) 并补齐长度,要么把缓冲区放到 non-cacheable 区(用 MPU 配),后者最简单可靠。

容易答错的地方

答「加内存屏障就行」。屏障解决的是访问顺序,不解决「Cache 里是旧数据」。

10为什么说 printf 会拖慢嵌入式系统?怎么改?

这题想听什么

你有没有算过一行日志的代价。

要点

  • 阻塞:等 TXE / TC 标志。115200 波特率下一个字符约 87 µs,40 字符一行就是 3.5 ms——控制循环直接被拖垮。
  • 格式化开销:浮点转换在软件浮点实现下能到几百 µs;%f 是最贵的一个。
  • 半主机模式(semihosting):每次输出触发调试器断点,慢几个数量级,脱机运行还会卡死。
  • 重定向 fputc 的常见错误:只写 DR 不判 TXE,高速输出时丢字符,看起来像「日志随机缺行」。
  • 改法:环形缓冲 + DMA 空闲中断发送;日志分级在编译期裁剪(NDEBUG);禁用浮点格式化;高频路径只记计数或哈希;调试期用 SWO / ITM。

容易答错的地方

答「把波特率调高就行」。这只是把 3.5 ms 变成 0.3 ms,在高频控制回路里依然不可接受。

11什么是优先级反转?FreeRTOS 怎么解决的?

这题想听什么

这个概念的经典题,但真正区分人的是最后一句:你用的是互斥量还是信号量。

要点

  • 场景:低优先级任务 L 持有锁 → 高优先级任务 H 就绪,等锁被阻塞 → 中优先级任务 M 就绪,抢占 L → 结果 H 被 M 间接拖住,等待时间不可预测。
  • 危害:表现为「偶尔超时、偶尔丢帧」,负载一高就出现,很难复现。
  • 解决一:优先级继承——持锁任务临时被提升到等待者中最高的优先级,释放后恢复原优先级。FreeRTOS 的 xSemaphoreCreateMutex() 自带这个机制。
  • 解决二:优先级天花板——锁的优先级固定设为可能使用它的最高任务优先级,谁拿锁谁就升上去。
  • 关键差别:xSemaphoreCreateBinary() 没有优先级继承。拿二值信号量当互斥锁用,是工程里最常见的事故来源。

容易答错的地方

只背定义,答不出「所以互斥量和二值信号量不能互换」。

12任务栈给多大合适?怎么确认有没有溢出?

这题想听什么

你有没有真的量过,还是拍脑袋给了个 512。

要点

  • 单位陷阱:FreeRTOS 的 usStackDepth 单位是字(word),不是字节。写 128 表示 512 字节,很多人以为给了 128 字节。
  • 组成:局部变量 + 调用栈帧 × 调用深度 + 中断嵌套(ISR 用的是被中断任务的栈)+ 上下文保存 + 库函数开销(printf / sprintf 吃栈很凶)。
  • 测量:uxTaskGetStackHighWaterMark(),跑完最坏路径(最深的调用、最长的日志)后看剩余,留 30% 以上余量。
  • 溢出后果:踩到相邻任务的 TCB 或堆,表现是随机死机、任务莫名消失,极难定位。
  • 手段:configCHECK_FOR_STACK_OVERFLOW = 2、栈初始填充图案、有条件时用 MPU 给栈加保护区。

容易答错的地方

答「给大一点就行」。RAM 有限的芯片上,栈给多了别的任务就没得用;而且「大一点」不能替代测量。

13写一个字符设备驱动,注册流程是什么?

这题想听什么

你写过没有。写过的人会自然说出「反序注销」,没写过的人只会背前半段。

要点

  • 拿设备号:alloc_chrdev_region() 动态分配,或 register_chrdev_region() 指定。
  • 绑定操作集:cdev_init() 绑定 file_operations,然后 cdev_add() 注册进内核。
  • 生成设备节点:class_create() + device_create(),让 mdev / udev 自动建 /dev 下的节点。
  • 入口出口:module_init / module_exit,MODULE_LICENSE("GPL")。
  • 退出必须反序释放:device_destroy → class_destroy → cdev_del → unregister_chrdev_region。
  • 延伸考点:read / write 返回值与 errno 约定;copy_to_user 的返回值必须检查;open 里用 container_of 拿私有数据。

容易答错的地方

只答前半段,不说注销顺序。面试官会追问「rmmod 之后再 insmod 报设备号被占用,为什么」——就是因为漏了 unregister。

14设备树到底解决了什么问题?

这题想听什么

知其然,还是知道它替代了什么。

要点

  • 它替代的东西:以前板级信息(寄存器基址、中断号、时钟、引脚复用)硬编码在 arch/arm/mach-xxx 里,一块板一份 C 代码,改板子就要改内核。
  • 现在:硬件描述抽到 .dts,编译成 .dtb 由 bootloader 传给内核,内核解析成 device_node 树。
  • 匹配机制:驱动的 of_match_table 里的 compatible 与节点的 compatible 匹配,匹配成功才进 probe。
  • 收益:一个内核镜像支持多种板;代价:多一层调试——dtc 反编译看实际生效的树、/proc/device-tree 查节点、probe 没进先查 compatible 拼写和 status。
  • 常见事故:compatible 写错一个字母、pinctrl 没配、reg 地址与手册不一致、status 还是 disabled、外设时钟没在 dts 里 enable。

容易答错的地方

答「设备树就是配置文件」。它是内核与 bootloader 之间的硬件描述接口,有版本和绑定(binding)规范,不是随便写的配置。

15内核态什么时候不能睡眠?违反了会怎样?

这题想听什么

你对「上下文」这个概念有没有实感。这是驱动里最常见的崩溃来源之一。

要点

  • 可以睡:进程上下文。kmalloc(GFP_KERNEL)、mutex、copy_to_user 触发缺页,都可能睡眠。
  • 不能睡:中断上下文、软中断 / tasklet、持有 spinlock 时、已关抢占或关中断时。
  • 判断手段:in_interrupt()、in_atomic()、读 preempt_count。
  • 违规后果:打印 BUG: scheduling while atomic,或者直接死锁、崩溃。开了 CONFIG_DEBUG_ATOMIC_SLEEP 会主动报出来。
  • 正确写法:中断里用 GFP_ATOMIC、用 spinlock、把耗时工作丢到工作队列或线程化中断的下半部。

容易答错的地方

答「内核态都不能睡眠」。这个误解在写驱动的第一年很常见,会导致到处用 GFP_ATOMIC,内存紧张时大量失败。

16交叉编译出来的程序在板子上跑不了,怎么查?

这题想听什么

这是嵌入式 Linux 的日常,答得清楚说明你独立部署过。

要点

  • 先分类:file 看架构,readelf -h 看 ABI,readelf -d 看动态依赖,到板上跑 ldd。
  • 四类常见原因:架构或 ABI 不匹配(armel vs armhf、gnueabi vs gnueabihf);动态库版本与 rootfs 不符;缺动态加载器(/lib/ld-linux-armhf.so.3);内核太老,不支持程序用到的新 syscall 或更高版本 glibc。
  • 浮点约定最容易踩:硬浮点编出来的库不能和软浮点混链,典型报错是 uses VFP register arguments。
  • 快速二分:静态链接编译一次,能跑就说明是库和加载器的问题,不是代码问题。
  • 根本建议:用芯片厂商提供的 toolchain + sysroot,别用系统源里的交叉 gcc 随便凑。

容易答错的地方

答「重新编译一下」。这题考的是分类能力,不是运气。

17产品偶发死机,你的排查顺序是什么?

这题想听什么

纯开放题。答得越有结构,说明你越独立处理过现场问题。这题没有标准答案,但有明显的「层次高低」。

要点

  • 先分清是「死机」还是「复位」:读 RCC_CSR 的复位标志,用备份寄存器记上电次数。这两条路径完全不同,很多人一开始就搞错了。
  • 固化现场:HardFault 里保存 CFSR / HFSR / MMFAR / BFAR,以及压栈的 R0–R3、R12、LR、PC、xPSR。没有现场,后面全是猜。
  • 缩小触发条件:温度、供电电压、特定外设组合、运行时长、通信负载、是否只在某批板子上出现。
  • 二分:关模块、降频、换电源、换板、换晶振。先把变量减到一个。
  • 工具顺序:示波器看电源纹波和复位脚毛刺 → 逻辑分析仪看总线时序 → SWO / ETM trace → 内存填充找越界。
  • 经验排序:电源(纹波、上电时序、复位毛刺)> 栈溢出 > 指针越界 > 中断优先级配置 > Flash 时序 > 看门狗误复位。

容易答错的地方

一上来就说「加日志」。日志会改变时序,很多时候把它藏起来——先固化现场再动手,顺序不能反。

18从样机到量产,要补哪些东西?

这题想听什么

这是高级岗和普通岗的分水岭。答得出来的人,通常已经跟过量产。

要点

  • 电源与复位:上电时序、复位电路 RC 参数与毛刺、欠压检测(BOR)阈值。
  • 时钟:外部晶振起振余量、负载电容取值、低温频偏、内部 RC 的出厂校准与温漂。
  • 存储:Flash 分区规划、参数掉电保存(双备份 + CRC)、擦写寿命与磨损均衡。
  • 升级:Bootloader + 双区 OTA、断电续传、版本回滚、完整性校验。
  • 可靠性:看门狗策略、异常日志、上电自检、复位原因记录。
  • 生产:序列号、校准参数烧录、产测模式、一键自检、日志导出。
  • 合规:EMC 预测试、ESD、高低温与老化试验。

容易答错的地方

只答「优化代码、加注释」。样机只要求「能跑」,量产要求「在别人的电、别人的温度、别人的干扰下,三年不坏」——这是两个工程学科。

顺着往下问:三条常见的追问链

面试官很少只问一题。下面三条是我问过、也被问过的路径,从第二问开始才见真章。

volatile

  1. 它是干什么的?
  2. 能保证原子性吗?
  3. DMA 或多核场景够用吗?

一路问下去,考的是「编译器优化」和「硬件可见性」是两件事。第三问答不出来,前两问基本靠背。

任务栈多大

  1. 栈给了多少?
  2. 怎么量的?
  3. 溢出了会出什么现象?
  4. 怎么防止?

报得出数字的人不少,报得出测量方法的不多。能说清溢出症状的,通常真被咬过。

偶发死机

  1. 第一步做什么?
  2. 怎么固化现场?
  3. 定位到以后怎么防再犯?

第一问答「加日志」就危险了。日志本身会改时序,很多问题一加日志就消失,现场没了。

怎么用这个题库:先自己答一遍,再展开对照。看得懂但说不出来的,就是会挂的地方。读得懂和讲得清,中间隔着好几层理解。

留言 / 提问

写你卡住的地方:什么芯片、什么现象、已经试过什么。频率高的问题我会写进页面。昵称不填也能发。