面灵AI→

荣耀嵌入式软件面经(一面 + 二面)

轮次
一面+二面
时间
2026-10
来源
牛客网

《面试题目》

一面(C 语言、接口与项目):

  1. static 放在函数内部和外部,分别影响什么?修饰函数又有什么作用?
  2. C 程序运行时,代码、全局变量、局部变量一般放在哪里?
  3. 返回局部变量的地址有什么问题?函数返回以后还能不能继续用?
  4. 段错误通常和哪些非法内存访问有关?拿到一个崩溃现场会先看什么?
  5. 链表和数组各适合什么情况?删除链表节点时,哪些指针操作容易写错?
  6. 项目中用过哪些硬件通信接口,为什么选这一种?
  7. 讲一下 I²C 从开始通信到结束的过程,ACK 在哪里出现?
  8. I²C 的 SDA 一直被拉低,可能有哪些原因?准备怎么查?
  9. SPI 和 I²C 有什么区别?如果接多个外设,连接方式有什么变化?
  10. 中断发生以后,程序怎么进入处理函数,又怎么回到原来的位置?
  11. 中断处理函数里哪些事情不适合做?收到数据后怎么交给任务处理?
  12. 中断和任务共用一个变量,volatile 能解决哪些问题,哪些解决不了?
  13. FreeRTOS 怎么决定接下来运行哪个任务?
  14. 任务切换时需要保存哪些现场?为什么不能只保存一个函数地址?
  15. 高优先级任务一直处于就绪状态,低优先级任务还有没有机会运行?
  16. 项目里的蓝牙模块怎么和 MCU 通信?自己实现了哪一层,哪些由模块完成?
  17. 蓝牙连接上了,但应用收不到数据,会从哪里开始排查?
  18. 回看一道笔试题,解释当时的思路;如果输入规模扩大,有没有更合适的做法?

二面(项目分工、竞赛与个人选择):

  1. 为什么参加这个项目或竞赛,是自己报名还是老师安排的?
  2. 团队有几个人,自己具体负责什么?
  3. 项目中最麻烦的一次问题是什么,当时怎么处理的?
  4. 几个人对方案有分歧时,最后怎么决定?
  5. 如果队友负责的部分没按时完成,自己会怎么办?
  6. 项目最后有什么结果,哪些地方还没有做好?
  7. 觉得自己的优势是什么?有什么需要改的地方?
  8. 最近一次遇到挫折是什么,之后做了什么?
  9. 为什么选择嵌入式,后面更想做哪一类工作?
  10. 选工作主要考虑什么,工作内容、地点和薪资怎么取舍?
  11. 意向工作地点有哪些?
  12. 有什么想了解的?

《参考解析》

static、内存分区与返回局部变量。static 修饰局部变量时改变的是存储期:变量从栈移到静态存储区,函数退出后仍然存在、下次进入保持上次的值,初始化只做一次(默认初值 0);修饰全局变量或函数时改变的是链接属性:作用域被限制在本编译单元内,避免不同文件的同名符号冲突。C 程序的内存布局按段讲:代码段(.text,只读)、已初始化数据段(.data)、未初始化数据段(.bss,启动时清零)、堆(向上增长,malloc)、栈(向下增长,局部变量与函数调用现场)、以及常量区与内存映射区。返回局部变量的地址是典型的悬空指针:函数返回后栈帧被回收,那块内存随时会被后续调用覆盖,值不确定——函数返回后不能再依赖它。能返回的是静态/全局变量的地址、堆上分配的地址、或由调用方传入的缓冲区。想深入一点可以提栈帧布局与「被调用的下一个函数立刻复用同一块栈空间」这个现象,这是初学者最常踩的坑。

段错误的排查。常见来源:空指针或野指针解引用、访问越界(数组下标超范围、strcpy 写超目标缓冲)、使用已释放的内存、栈溢出(大局部数组或深递归)、写只读段(修改字符串字面量)、非对齐访问(部分架构)。拿到崩溃现场的顺序:先用串口或调试器拿到出错 PC 与 LR、故障类型(HardFault 的 CFSR 寄存器能区分总线错误、非对齐、除零);用 addr2line/arm-none-eabi-objdump 把地址映射回源码行;如果是 Linux 上则有 core dump(ulimit -c + gdb ./bin core,看 bt、info registers、info locals);再结合日志与最近改动缩小范围,最后用边界输入复现并做「加断言 + 打印 + 二分注释」定位。

链表与数组、I²C 与 SPI、中断、volatile、FreeRTOS 调度与任务切换。数组适合「大小已知、随机访问多」的场景,内存连续因而缓存友好、可用下标 O(1) 访问,但插入删除要搬数据;链表适合「频繁插入删除、大小动态」的场景,代价是额外指针开销、内存不连续、随机访问 O(n)。删除链表节点写错的地方:先移动指针再释放导致丢失后继、没有处理头节点、删除后没把前驱的 next 接上、对已释放节点再次解引用(free 后未置 NULL)、以及在多线程/中断环境下没有保护。I²C 两根线(SCL/SDA,开漏加上拉)走「起始 → 地址 + 读写位 → 每字节后 ACK → 停止」,特点是支持多从机、速率较低、有 ACK 机制;SPI 四根线(SCLK/MOSI/MISO/CS)全双工、速率高、无地址与 ACK,靠片选选中从机,接多个外设时每增加一个从机就要多一根 CS(也可以菊花链)。中断的硬件过程是:外设触发中断请求 → 中断控制器判优先级 → CPU 保存现场(部分寄存器自动压栈)→ 查中断向量表跳到处理函数 → 执行 ISR → 清中断标志 → 恢复现场返回(LR 中的特殊值告诉 CPU 是返回到被中断的线程还是另一个中断)。ISR 里不适合做耗时或可能阻塞的事:动态内存分配、printf/浮点格式化、等待信号量、长循环与延时,正确做法是「短活立即干、长活丢队列」,用 FromISR 版本的 API 把数据交给任务处理。volatile 保证的是「每次访问都去内存读/写、不被编译器优化掉」,它解决的是中断或硬件寄存器改写导致编译器读到旧值的问题;它不保证原子性(多字节变量的读改写仍可能被打断)、不保证顺序(需要内存屏障)、也不保证多核缓存一致性(需要硬件保证或用原子操作)。所以共享的计数器要用临界区或原子操作保护,volatile 只是必要条件之一。FreeRTOS 的调度:任务有优先级,同优先级按时间片轮转,高优先级就绪立即抢占(configUSE_PREEMPTION),调度器维护就绪链表/位图(uxTopReadyPriority 找最高优先级),vTaskDelay/等信号量会让任务进入阻塞态并触发切换。任务切换保存的现场包括通用寄存器、程序计数器、状态寄存器(PSR)、以及浮点寄存器(如果开了 FPU 且任务用到浮点);栈指针交给任务控制块保存。不能只保存一个函数地址,是因为任务被切走时可能停在函数中途——局部变量、中间计算结果都在寄存器和栈上,恢复时必须把整套现场还原,任务才能从被中断的那条指令继续执行,这也是每个任务需要独立栈空间的原因。

高优先级任务一直就绪,低优先级还有机会吗。没有抢占式调度的公平性可言:如果最高优先级任务永不阻塞,低优先级任务会被饿死(这是实时系统的设计意图——按优先级保证时限)。低优先级能得到执行机会的情况有:高优先级任务进入阻塞(等队列/信号量/延时)、被挂起、被删除、或者通过时间片(仅同优先级之间轮转)。所以工程上要避免「优先级倒置」和「忙等」,典型做法是让高优先级任务在无事可做时阻塞在队列上,把长计算拆成分片或在空闲任务里做;反过来如果确实需要给低优先级留出时间,可以用优先级继承的互斥量、或者让高优先级任务周期性 vTaskDelay 让出 CPU(但要评估是否破坏时限)。

蓝牙链路排查。先分层定位:模块层(供电与复位是否正常、AT 指令能否响应、固件版本、是否处于可连接状态)、链路层(是否真的连上、连接参数、有没有掉线重连)、配置层(服务与特征 UUID 是否与对端匹配、通知/订阅是否开启、MTU 大小)、数据层(发送方有没有写成功、接收方有没有注册回调、数据长度是否超出 MTU 需要分包)。排查手段:串口日志打印每一步状态机、用手机端调试 App 或抓包工具看链路事件、检查中断/DMA 接收缓冲区是否溢出或没有被及时取走。原帖作者的提醒很关键:简历写「实现蓝牙通信」,如果实际只是通过串口给模块发命令,就要把「自己实现了哪一层」讲清楚,模块内部怎么建连与传输不属于你的工作,不要顺嘴揽过来——被追问一层就会穿帮。

综合面(二面)怎么准备。行为面的核心是「具体」。回答团队协作类问题要给出一次真实分歧:分歧点是什么、双方各自的理由、最后用什么标准决定、结果如何;回答「队友没按时完成」要讲你实际做了什么(调整排期、拆分任务、自己补位、向上反馈),而不是「我会帮他做」这种标准答案。项目结果要能说出「哪做得好、哪没做好」,只讲成绩会被认为没有反思。选择类问题(为什么选嵌入式、更想做哪类工作、地点与薪资怎么取舍)要给出真实排序与理由,含糊的「都可以」是最差的答案。作者复盘时也提到,准备的关键是翻自己当时的代码、顺着一条数据从接收到处理走一遍,比背项目介绍管用。