面灵AI→

嵌入式秋招挂了十几场,复盘发现反复考的就这四类题

时间
2026-10
来源
牛客网

《面试题目》

  1. 指针作为返回值,为什么会出问题?
  2. 结构体成员换个顺序,大小变不变?
  3. 变量分别放在哪个段?上电后谁初始化?
  4. malloc 出来的内存在任务里没释放,任务删了会怎样?
  5. DMA 传完数据进中断、中断里置标志位、任务里取数据处理——任务还没处理完,下一次 DMA 又写进来了呢?
  6. 谁写谁读、临界区该包哪几行、中断为什么不能阻塞、DMA 缓冲什么时候能复用?
  7. I2C 时钟拉伸是什么场景?
  8. SPI 四种模式怎么区分?你的设备用哪种?
  9. UART 接收用中断还是 DMA,为什么?
  10. 总线上挂多个设备,地址怎么管理?
  11. 链接脚本改了哪几段?为什么改完程序起不来?
  12. 向量表在哪?复位后第一条指令是什么?data 段谁搬、bss 谁清零?HardFault 了怎么定位现场?

《参考解析》

这四类题挑人的逻辑是一样的:简历上不单独写,但哪个项目都绕不开。 面试官不是想考你有没有背过,而是想验证项目里的代码是不是你自己写的——所以每一类都要落到「我项目里哪段代码对应这个知识点」。

指针作返回值挂人的地方几乎都是返回了栈上的地址:函数里开一个局部数组返回指针,函数一返回栈帧就作废,调用方拿到的是悬垂指针,读出来的数据取决于后面谁踩了这块栈——现场表现是「偶尔对、偶尔乱码、加个打印就正常了」,比直接崩更难查。返回字符串字面量、静态区变量、或者 malloc 的堆指针没问题,但堆内存的释放责任要写清楚。嵌入式里更隐蔽的一层是 const 全局数组放在 Flash,返回它的指针完全合法,可一旦调用方想写就会进 HardFault。

结构体换成员顺序,大小会变,因为对齐:每个成员的偏移必须是它自身对齐数的整数倍,结构体总大小要补齐到最大对齐数的整数倍。char + int + char 在 4 字节对齐下是 12 字节,把 int 挪到前面就压成 8 字节。协议解析时用 __attribute__((packed)) 省空间,代价是非对齐访问——Cortex-M0/M0+ 不支持非对齐,直接 UsageFault;M3/M4 支持但会多花总线周期,涉及 DMA 时还要注意编译器不保证 packed 成员的原子访问。

变量在哪个段、谁初始化是启动流程的入口问题。.text 是代码,const 全局在 .rodata(烧在 Flash 里,不占 RAM);已初始化的全局和静态变量在 .data,初值存在 Flash,上电后由启动代码搬到 RAM;未初始化的进 .bss,启动代码整段清零;局部变量在栈上,malloc 在堆上。做这些事的是 Reset_Handler,不是 main——它先设栈顶、把 .data 从 Flash 拷到 RAM、把 .bss 清零,再调 SystemInit 和 main。答不出这一串,基本等于没看过启动文件。

任务里 malloc 不释放、任务被删:vTaskDelete 只回收任务自己的栈和 TCB,任务用 malloc 拿的堆块没人管,泄漏到堆耗尽、malloc 返回 NULL;比泄漏更糟的是任务在持锁或进临界区时被删,锁永远不释放,其他任务一起卡死。工程上的做法是能静态分配就静态分配(xTaskCreateStatic + 静态缓冲),必须动态就用内存池按固定块分配,避免碎片;确实要 malloc,释放责任写在任务退出路径上。

DMA 与任务的并发是这套题里最值钱的一问。任务没处理完、DMA 又写进同一块缓冲,表现是数据被覆盖或前后半帧拼接——低频测试永远撞不到,一上量就偶发。解法是双缓冲(ping-pong):DMA 写 A 区时任务读 B 区,传完半满/全满中断里交换并投递信号量,任务处理完才交还缓冲区;或者用环形缓冲加读写指针,靠 DMA 的传输计数算可读长度。这里还有两个常被追问的点:单生产者单消费者才能不加锁,但读写指针要 volatile 加编译屏障;带 D-Cache 的 MCU 要在 DMA 写之前 clean、读之前 invalidate,否则 CPU 读到的是缓存里的旧数据。至于「中断为什么不能阻塞」——中断上下文没有可让出的任务栈,在里面等信号量会把同级和更低优先级的中断全堵住,正确做法是中断只置标志、发队列,重活丢给任务。

I2C 时钟拉伸是从机来不及处理时把 SCL 拉低,主机得检测并等它释放;坑在有些 MCU 的硬件 I2C 不支持从机拉伸,表现为偶发 NAK 或数据错位,只能降速或改软件模拟。SPI 四种模式由 CPOL(空闲电平)和 CPHA(第几个边沿采样)组合,mode0 和 mode3 最常见,主从配错的表现是整体移一位、读出来全是 0xFF 或 0x00。UART 用中断还是 DMA看波特率和帧长:低速率短帧用中断足够,高速率连续数据必须 DMA 加空闲中断(IDLE)收不定长帧,否则每字节一次中断,高波特率下直接丢字节。多设备地址:I2C 靠 7 位从机地址,撞地址就用多路复用器或换器件;SPI 没有地址概念,每个从机一根独立 CS。

链接脚本与启动是「抄来的东西」最容易露馅的地方。改脚本通常动两块:MEMORY 里的 Flash/RAM 起止和长度,SECTIONS 里各段的摆放——向量表必须在 Flash 起始地址,.data 的加载地址在 Flash、运行地址在 RAM,.bss 和栈堆的大小要留够。改完起不来,八成是向量表挪了位置(复位取不到初始栈顶和 Reset_Handler)、RAM 超了导致栈撞堆、或者 .data 的 LMA/VMA 写反初值全错。HardFault 定位的固定套路:先看 LR 的 EXC_RETURN 判断出错时用的是 MSP 还是 PSP,从对应栈里取出压栈的 R0–R3、R12、LR、PC、xPSR,PC 就是出错指令;再看 CFSR/HFSR 和 MMFAR/BFAR 分清是总线错误、用法错误还是非对齐,最后拿 map 文件或 addr2line 落到函数行。