荣耀嵌入式面经(一面 + 二面)
- 轮次
- 一面+二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面:C 语言、接口和项目
- static 放在函数内部和外部,分别影响什么?修饰函数又有什么作用?
- C 程序运行时,代码、全局变量、局部变量一般放在哪里?
- 返回局部变量的地址有什么问题?函数返回以后还能不能继续用?
- 段错误通常和哪些非法内存访问有关?拿到一个崩溃现场会先看什么?
- 链表和数组各适合什么情况?删除链表节点时,哪些指针操作容易写错?
- 项目中用过哪些硬件通信接口,为什么选这一种?
- 讲一下 I²C 从开始通信到结束的过程,ACK 在哪里出现?
- I²C 的 SDA 一直被拉低,可能有哪些原因?准备怎么查?
- SPI 和 I²C 有什么区别?如果接多个外设,连接方式有什么变化?
- 中断发生以后,程序怎么进入处理函数,又怎么回到原来的位置?
- 中断处理函数里哪些事情不适合做?收到数据后怎么交给任务处理?
- 中断和任务共用一个变量,volatile 能解决哪些问题,哪些解决不了?
- FreeRTOS 怎么决定接下来运行哪个任务?
- 任务切换时需要保存哪些现场?为什么不能只保存一个函数地址?
- 高优先级任务一直处于就绪状态,低优先级任务还有没有机会运行?
- 项目里的蓝牙模块怎么和 MCU 通信?自己实现了哪一层,哪些由模块完成?
- 蓝牙连接上了,但应用收不到数据,会从哪里开始排查?
- 回看一道笔试题,解释当时的思路。如果输入规模扩大,有没有更合适的做法?
二面:项目分工、竞赛和个人选择
- 为什么参加这个项目或竞赛,是自己报名还是老师安排的?
- 团队有几个人,自己具体负责什么?
- 项目中最麻烦的一次问题是什么,当时怎么处理的?
- 几个人对方案有分歧时,最后怎么决定?
- 如果队友负责的部分没按时完成,自己会怎么办?
- 项目最后有什么结果,哪些地方还没有做好?
- 觉得自己的优势是什么?有什么需要改的地方?
- 最近一次遇到挫折是什么,之后做了什么?
- 为什么选择嵌入式,后面更想做哪一类工作?
- 选工作主要考虑什么,工作内容、地点和薪资怎么取舍?
- 意向工作地点有哪些?
- 有什么想了解的?
《参考解析》
-
static 的三种位置:函数内的 static 变量存在静态存储区、只初始化一次、函数退出后值保留,作用域仍限于函数内;文件作用域的 static 变量/函数把链接属性收成内部链接,别的编译单元看不到,用来避免重名污染。修饰函数就是后者——限制在本文件可见。
-
C 程序的内存布局:从低地址到高地址大致是代码段(
.text,只读)、只读数据段(.rodata)、已初始化全局/静态区(.data)、未初始化全局/静态区(.bss)、堆(向上增长)、栈(向下增长)。局部变量和函数参数在栈上,malloc出来的在堆上,字符串字面量在只读段,改它会段错误。 -
返回局部变量地址:局部变量随函数栈帧回收,返回的指针指向已经失效的栈空间;函数返回后再解引用属于未定义行为,可能读到时序上还没被覆盖的旧值,也可能是垃圾数据。返回
static变量或堆内存才合法(堆内存要明确谁负责释放)。 -
段错误的常见来源:解引用空指针或野指针、越界访问数组、写只读内存(字符串字面量、代码段)、栈溢出(递归过深或用大局部数组)、访问已经
free过的内存。拿到崩溃现场先看故障地址是「接近 0」还是「看似合法」——接近 0 通常是空指针或小偏移,看似合法的地址往往是野指针或越界;然后看调用栈和寄存器、必要时开 core dump 或加内存保护定位。 -
I²C 一次完整通信:起始条件(SCL 高时 SDA 由高变低)→ 发送 7 位从机地址 + 读写位 → 从机在第 9 个时钟拉低 SDA 回应 ACK → 传输数据字节,每个字节后同样有一个 ACK/NACK → 最后主机发停止条件(SCL 高时 SDA 由低变高)。ACK 出现在每个字节之后的第 9 个时钟周期;读操作的最后一个字节主机回 NACK 表示「不再要了」。
-
SDA 一直被拉低怎么查:先分硬件和软件两条线。硬件上查总线上拉电阻是否缺失或阻值过大、供电是否正常、有没有器件把线焊短路;软件上查本机 GPIO 是否被误配成输出低、上一次通信被打断后从机没释放总线(需要补时钟脉冲让它退出)。排查顺序建议先示波器/逻辑分析仪看波形,确认是哪个器件在拉低、还是根本没被释放。
-
中断的进入与返回:中断源触发后,硬件压栈保存部分现场、取中断向量表的入口地址跳到处理函数;处理完执行中断返回指令,从栈里恢复现场,回到被中断的那条指令继续。Cortex-M 上这部分压栈是硬件自动完成的(xPSR、PC、LR、R12、R3-R0),所以裸写汇编处理函数时要和编译器约定对齐。
-
中断里不能做的事:不能长时间阻塞或延时、不能调用可能睡眠的 API(
malloc、带信号量的阻塞调用、printf这类重入性差的操作)、不能在裸机里做耗时循环。标准做法是中断里只做「取数据 + 置标志/发队列」,实际处理放到任务里:FreeRTOS 下用xQueueSendFromISR配合portYIELD_FROM_ISR。 -
volatile 能解决什么:它只保证编译器每次都从内存重新读取、不做寄存器缓存或优化掉访问,解决的是「变量可能被中断或硬件悄悄改掉,而编译器不知道」这一类问题。它不解决原子性(
i++仍然会被打断)、不解决多核缓存一致性、也不能替代内存屏障。 -
FreeRTOS 的任务选择:同优先级任务按时间片轮转,高优先级就绪任务会抢占当前任务;调度发生在 SysTick、任务主动让出(
taskYIELD、阻塞式 API)以及中断退出时的上下文切换点。空闲任务优先级最低,保证总有任务可跑。 -
上下文切换保存什么:保存 CPU 寄存器现场(通用寄存器、PC、LR、状态寄存器),其中调用者保存寄存器由硬件或 PendSV 压栈、被调用者保存寄存器由编译后的函数自己负责;再保存任务栈指针到 TCB。只保存一个函数地址不够,因为返回后还要继续用当时的局部变量、中间计算结果和栈帧——这些都在寄存器和栈里。
-
高优先级任务一直就绪:如果它不阻塞、不让出,低优先级任务会被彻底饿死,所以高优先级任务里必须有阻塞点(等队列、等事件、
vTaskDelay)。同优先级时靠时间片轮转还能轮到。 -
I²C 与 SPI 的区别和多外设接法:I²C 两线(SCL/SDA)、支持多主多从、靠地址寻址、速率较低(标准 100k、快速 400k、高速可达数 M),每个字节有 ACK;SPI 四线(SCK/MOSI/MISO/CS)、全双工、速率高,但没有地址机制,接多个外设要靠片选 CS 逐个选中,多一个外设多一根 CS(或者用菊花链)。
-
蓝牙连上了但应用收不到数据:从链路上逐段排查——先确认模块和 MCU 之间的串口/SPI 通信是否真的在收发(抓波形或用调试串口打印),再确认模块是否已经进入透传模式、有没有被 AT 指令改掉配置;然后看 MCU 侧的接收缓冲和中断是否正常触发、任务有没有被更高优先级任务饿死、队列是否已满导致丢弃。分层上讲清楚「自己实现的只是串口收发,建连和传输是模块内部做的」比顺嘴揽下整条链路更安全。