面经

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## 一、C 与 C++

### 1. 嵌入式为什么常用 C,而不是 C++?

C 的运行时简单、代码体积和内存开销通常更小,能直接进行指针、位操作和寄存器访问,执行时间和资源消耗也更容易分析。芯片 SDK、驱动、编译器和调试工具对 C 的支持通常更加成熟,因此适合裸机和小型 RTOS。

C++ 也可以用于嵌入式,类、封装、模板和 RAII 能提高大型项目的可维护性。但异常、RTTI、动态内存、复杂模板和虚函数等特性可能带来额外开销或实时性风险,所以通常按项目约束选择性使用。

### 2. C 编程和 C++ 的区别

C 主要是面向过程;C++ 在此基础上增加了类、继承、多态、函数重载、模板、命名空间、引用和 STL 等能力。C++ 抽象能力和复用能力更强,但并不代表一定更慢,是否有额外开销取决于实际使用的语言特性和编译选项。

## 二、`static`、`const` 与宏

### 1. `static` 的作用

- **函数内局部变量**:作用域仍在函数内,但具有整个程序运行期间的生命周期,只初始化一次。
- **文件级全局变量**:生命周期不变,但只在当前源文件可见,具有内部链接属性。
- **文件级函数**:只在当前源文件可见,避免符号冲突。
- **C++ 类成员**:静态成员属于类,而不是某个对象。

### 2. `const` 的作用

`const` 表示不能通过当前标识符修改对象,是编译期约束,可表达只读接口并帮助类型检查。

```c
const int *p; // 不能通过 p 修改所指内容,p 可以改指向
int *const p = &x; // p 不能改指向,但可以修改 x
const int *const p; // p 和所指内容都不能通过 p 修改

如果对象本身定义为 const,再通过强制类型转换去修改属于未定义行为;const 不是硬件级不可修改保护。

3. const#define 的区别

#define 是预处理阶段的文本替换,没有类型和正常作用域;const 是有类型的语言对象,有作用域,可以取地址,也更便于调试。常量优先使用 constenum,宏主要用于条件编译、头文件保护和编译期配置。

4. const 变量存在哪里?

不能简单认为所有 const 都在只读区:

  • 函数内普通 const:通常在栈上,也可能被优化到寄存器或完全消除;
  • 文件级 const:通常位于 .rodata
  • static const:生命周期贯穿程序,通常位于 .rodata
  • 字符串常量:通常位于只读数据区。

static 主要控制生命周期和链接属性,const 主要限制修改,二者是不同维度。

三、变量、内存与指针

类型 常见区域
普通局部变量 栈,或被优化到寄存器
局部static .data.bss
已初始化全局变量 .data
未初始化或初始化为 0 的全局变量 .bss
文件级static .data.bss
全局conststatic const 通常.rodata
malloc 对象

最终位置由编译器、链接脚本、ABI 和优化选项决定。

volatile

用于会被中断、硬件寄存器、DMA 或其他执行流异步修改的对象。它要求编译器保留实际访问,避免缓存变量或删除访问,但不保证原子性、互斥和线程间内存顺序;多线程同步应使用原子操作、锁或信号量。

内存碎片

频繁申请和释放不同大小的堆块会形成不连续空闲空间。嵌入式长期运行且 RAM 小,碎片可能导致申请失败或实时性不可控。优先静态分配、固定块内存池,或只在初始化阶段动态分配。

野指针与悬空指针

未初始化的指针是野指针;对象释放或离开作用域后仍保存旧地址的是悬空指针。free 后应将当前指针置 NULL,但这不能消除其他别名指针。

四、结构体、联合体、位域和字节序

结构体对齐

成员按自身对齐要求放置,成员之间可能有填充;结构体总大小通常是最大对齐数的整数倍。重排字段时一般按大对齐到小对齐排列,可减少填充。协议数据不要直接依赖结构体布局,应显式序列化。

联合体

所有成员共享同一块内存,大小通常等于最大成员大小。嵌入式中可用于节省内存、表示同一寄存器的不同字段、进行状态解释,但要注意大小端、对齐和类型别名规则。

位域

位域可以让成员只占几个 bit,适合标志位和寄存器字段。位的分配方向、跨字节方式和填充属于实现相关,协议解析不要假设不同编译器布局一致。

大小端

小端把低有效字节放低地址,大端相反。网络协议要规定统一字节序,通常使用大端(network byte order),否则不同 CPU 解析同一整数会得到不同结果。

五、函数、宏与编译

函数指针可以实现回调,用于驱动事件、收包、定时器和状态机。回调,尤其是 ISR 回调,应快速返回,复杂工作通过队列或信号量交给任务。

inline 是有类型检查的函数声明,只是建议,编译器不保证一定展开;宏是文本替换,可能没有类型检查且参数可能重复求值。

编译通常经历:预处理、编译、汇编、链接。头文件应放声明,例如 extern int g_value;,变量定义放在一个 .c 文件中,否则多个源文件包含后会产生重复定义。

六、操作系统:进程、线程与通信

进程和线程

进程是资源分配和保护的基本单位,通常拥有独立虚拟地址空间;线程是调度的基本单位,同一进程的线程共享代码段、全局数据、堆和文件描述符,但各自拥有栈、寄存器和程序计数器。

进程隔离性强但创建和切换开销较大;线程共享数据方便但需要处理竞态、死锁和数据可见性问题。

进程通信

常见方式有管道、FIFO、消息队列、共享内存、信号量、信号和 Socket。线程通常直接共享内存,再使用互斥锁、信号量、条件变量、原子变量或线程安全队列进行同步。

mmap 通信过程

一个进程创建或打开文件并用 ftruncate 设置大小,双方使用 mmap(..., MAP_SHARED, ...) 映射同一文件区域。写入方修改共享内存,读取方可以看到修改;同步仍需配合进程共享信号量、互斥锁或 futex。

MAP_PRIVATE 是写时复制,不能实现真正的双向共享。mmap 解决的是共享内存,不自动解决并发和持久化问题;需要落盘时再考虑 msync

多线程常见问题

包括竞态、数据竞争、死锁、活锁、饥饿、优先级反转和内存可见性问题。可用互斥锁、信号量、条件变量、原子变量、统一加锁顺序、短临界区和优先级继承解决。

互斥锁适合保护共享资源,通常有“谁加锁谁解锁”的所有权;信号量适合资源计数或事件通知,例如表示连接池剩余资源,或通知消费者有新数据。

单线程也可能数据不一致

中断、DMA 或硬件可能异步修改数据。例如主循环读取 64 位计数器时,中断在两条指令之间更新它,主循环可能读到高低位不匹配的值。可用关中断、原子访问、双缓冲或环形队列解决。

七、嵌入式与实时系统

嵌入式系统是集成在设备中、面向特定功能的计算机系统,通常受 RAM、Flash、功耗、成本、可靠性和实时性约束。

实时系统关注任务是否在截止时间内完成:硬实时超时可能导致系统失效,软实时超时主要降低服务质量。

普通 Linux 强调吞吐量、功能和兼容性,调度延迟通常没有严格上界;RTOS 通常使用优先级抢占式调度,延迟更可预测。Linux 配合 PREEMPT_RT 等也能增强实时性,但普通 Linux 不等同于硬实时系统。

保证强实时性的措施包括:固定优先级抢占、最坏执行时间分析、静态内存、短 ISR、限制阻塞 I/O、避免缺页和长时间关中断、优先级继承、硬件定时器、看门狗,以及对最坏中断和响应延迟进行测试。

uC/OS-II 调度

uC/OS-II 通常采用基于优先级的抢占式调度,每个任务有唯一优先级,数值越小通常代表优先级越高。调度器从就绪表中找到最高优先级任务;当前任务延时、等待信号量或被挂起后,其他就绪任务才会运行。时钟节拍中断更新延时计数,中断退出时可能触发重新调度。

中断处理过程

外设发出中断请求后,CPU 保存现场并跳转到向量表中的 ISR。ISR 读取状态、清中断标志、保存必要数据,并通过消息、信号量或事件通知任务。退出中断前重新调度,必要时切换到更高优先级任务,再恢复现场返回。

ISR 应尽量短,不能阻塞、不能做复杂计算,也不应长时间关闭中断。uC/OS-II 中通常使用 OSIntEnter()OSIntExit()

八、FreeRTOS 重点

调度和时间片

FreeRTOS 通常运行最高优先级的就绪任务。同优先级任务是否轮转由 configUSE_TIME_SLICING 控制,节拍中断可以触发时间片切换;任务主动阻塞、延时或调用 taskYIELD() 也会产生切换。

PendSV

PendSV 是 Cortex-M 用于延迟处理的系统异常。FreeRTOS 在 SysTick 或其他中断中请求切换,再由最低优先级的 PendSV 保存当前任务上下文、恢复下一任务上下文。它不会抢占真正紧急的中断,且可以在中断退出阶段安全完成切换。

任务栈

栈大小要结合调用深度、局部变量、库函数、FPU 和中断嵌套估算,并通过 uxTaskGetStackHighWaterMark() 观察余量。打开 configCHECK_FOR_STACK_OVERFLOW 可以检测溢出。栈太小会导致内存破坏和随机崩溃,太大则浪费 RAM。

队列、信号量、互斥量

队列传递数据;二值信号量做事件同步;计数信号量表示多个资源;互斥量保护共享资源并支持优先级继承。互斥量有持有者,二值信号量通常没有所有权。

FromISR API

普通 API 可能阻塞或操作任务调度器,不能在中断中使用。FromISR API 不会阻塞,会通过 xHigherPriorityTaskWoken 表示是否唤醒了更高优先级任务,最后用 portYIELD_FROM_ISR() 请求在 ISR 退出后切换。

九、WiFi、BLE、MQTT 与低功耗

WiFi 连接

设备先扫描 AP,再完成认证、关联、WPA 四次握手,随后通过 DHCP 获取 IP,最后进行 ARP、DNS 和 MQTT/HTTP 等应用层连接。

断线重连应采用状态机、指数退避和随机抖动,区分临时网络错误与密码错误,避免弱网下频繁重连,并保存必要的待发送数据。

TCP、UDP 和粘包

TCP 可靠、有序但有连接和重传开销;UDP 延迟低、开销小但不保证到达和顺序。UDP 可在应用层增加序列号、ACK、超时重传、去重和退避。

TCP 是字节流,没有消息边界。应使用固定长度、长度字段、分隔符或“包头 + 长度 + 负载 + CRC”等协议解决拆包和粘包。

BLE

BLE 外设先广播,中心设备扫描后建立连接,再进行配对、绑定和加密。GATT 的层级是 Server、Service、Characteristic、Descriptor;Characteristic 表示具体数据和读写/通知能力。

MQTT QoS

  • QoS 0:最多一次,不确认,可能丢失;
  • QoS 1:至少一次,使用 PUBACK,可能重复;
  • QoS 2:恰好一次,使用 PUBREC、PUBREL、PUBCOMP 等多步握手。

断线后的可靠性还依赖客户端和 Broker 是否持久化会话及未完成消息。

ESP32 睡眠模式

Modem-sleep 主要关闭射频;Light-sleep 暂停 CPU 并保留更多上下文;Deep-sleep 关闭 CPU 和大部分数字外设,唤醒后通常重新启动。普通 RAM 通常无法保留,RTC 内存可按电源域配置保留;长期状态可存 RTC 内存、NVS/Flash 或外部存储。

电池设备省电

降低唤醒次数和无线在线时间,使用批量采集、批量发送、深睡、低功耗传感器、合理 BLE 连接参数、较低发射功率、外设电源管理和中断唤醒,并通过实际电流测量优化平均功耗。

十、工程、OTA 与量产

OTA 防止变砖

使用双 OTA 分区:新固件写入非当前运行分区,下载后校验 Hash 和签名,再更新启动信息。新固件启动后进行自检,成功才确认;启动失败、异常复位或断电时由 Bootloader 回滚到旧版本。

分区和版本回滚

常见分区包括 factoryota_0ota_1otadata。版本信息应包含版本号、硬件兼容性和安全计数器;需要防止回滚到存在漏洞的旧版本时,可以启用反回滚机制。

固件安全

签名保证来源、完整性和真实性;加密保护固件内容;Secure Boot 防止未经授权代码启动;Flash Encryption 防止读取存储内容。密钥应放在安全区域,不能硬编码在普通源码中。

现场定位

记录固件版本、复位原因、看门狗原因、异常寄存器、调用栈、Core Dump、网络状态和关键运行指标。日志应分级、限频、支持远程获取,并避免泄露隐私或影响实时性。

看门狗

不要让一个任务无条件喂狗。应由关键任务上报心跳,由监控任务确认所有任务都正常后再喂任务看门狗;中断看门狗用于检测 ISR 卡死或长时间关闭中断。

代码质量与调试

通过需求澄清、模块化设计、编译告警、静态检查、代码评审、单元测试、集成测试、硬件联调、压力测试、故障注入和 CI 保证质量。调试时先稳定复现,再结合日志、断点、寄存器、波形、栈水位和最小化实验定位。

需求到交付流程

澄清需求和约束,进行架构与接口设计,评估风险,制定测试方案,编码实现,完成单元测试和联调,再经过评审、版本构建、发布验证和现场监控。

十一、面试易错点

  1. volatile 不等于原子操作,也不等于线程安全。
  2. 中断中不能使用会阻塞的普通 RTOS API,应使用 FromISR 版本。
  3. PendSV 设为最低优先级,是为了把任务切换延迟到其他中断处理完成后。
  4. 深睡后普通 RAM 通常丢失,短状态放 RTC 内存,可靠持久状态放 NVS/Flash。
  5. 互斥量用于资源保护,二值信号量更偏向事件同步;互斥量通常支持优先级继承。
  6. TCP 粘包不是 TCP 出错,而是 TCP 本身没有消息边界。
  7. OTA 不能只下载和校验,还要有备用分区、启动确认和回滚机制。

附录:三轮面试逐题口述版

下面按实际面试顺序展开。每题先给出适合口述的答案,再列出面试官可能继续追问的点。

A. 一面:项目与 C/底层基础

A1. ESP32 无线数据透传项目怎么讲?

可以按“目标、数据流、可靠性、异常处理、结果”五步讲:

我的项目是用 ESP32 实现无线数据透传。设备一侧从 UART/SPI 接收数据,先进入环形缓冲区,再由接收任务按协议拆包;协议层给每帧加长度、序号和 CRC,校验通过后放入发送队列,由 WiFi/BLE 发送任务发出去。接收端收到数据后回 ACK,发送端维护未确认队列,超时后重传,并根据序号去重。网络断开时数据先缓存,连接恢复后继续发送;缓存满时按业务优先级丢弃或覆盖。为了定位问题,我还记录了收发计数、丢包数、重传数、队列水位和断线原因。

追问“为什么会丢包”时可以说:无线干扰、信号弱、接收缓冲区溢出、任务调度不及时、协议校验失败和连接切换都可能导致丢包;因此要区分链路丢包、驱动丢包和应用层丢包。

A2. volatile 为什么中断标志位必须加?

例如主循环等待中断修改的标志:

1
2
3
4
5
6
7
8
9
10
11
12
13
volatile bool rx_done;

void UART_IRQHandler(void)
{
rx_done = true;
}

int main(void)
{
while (!rx_done) {
/* wait */
}
}

不加 volatile 时,编译器可能认为 rx_done 不会被当前执行流之外修改,把它读取到寄存器后反复使用,甚至把循环优化成死循环。加上后,每次访问都必须产生实际的内存访问。

volatile 只约束编译器,不保证读写原子性,也不提供线程同步。多字节变量、缓存一致性和多核场景仍需要临界区、原子操作或内存屏障。

A3. static 的生命周期和作用域怎么变化?

要分别看“生命周期”和“链接/可见性”:

写法 生命周期 作用域或可见性
函数内static 整个程序 仍只有当前函数可见
文件级static 变量 整个程序 只有当前源文件可见
文件级static 函数 整个程序 只有当前源文件可调用
普通局部变量 函数调用期间 当前代码块

函数内 static 变量只初始化一次,适合保存状态;文件级 static 适合封装模块内部实现,避免导出不必要的符号。

A4. 结构体大小怎么算?如何重排?

假设 32 位平台对齐要求为 char=1short=2int=4

1
2
3
4
5
struct Example {
char a; // offset 0
int b; // offset 4,a 后补 3 字节
short c; // offset 8
}; // 总大小 12,按最大对齐 4 对齐

把大成员放前面:

1
2
3
4
5
struct Example2 {
int b;
short c;
char a;
}; // 常见平台上大小为 8

不能只背一个固定答案,因为结果取决于 ABI、编译器和 #pragma pack。网络报文或 Flash 持久化格式应使用显式序列化,不要直接 memcpy 一个带填充的结构体。

A5. 位域跨字节怎么排?为什么有风险?

位域的分配顺序、是否允许跨存储单元以及填充方式是实现相关的。即使两个平台都是小端,编译器也可能采用不同的位域布局。因此位域适合本机寄存器映射和状态标志,不适合直接作为跨平台通信协议格式。协议字段应通过移位和掩码解析。

A6. malloc、静态数组与内存碎片

MCU 上优先使用静态数组、栈上固定对象或固定块内存池。malloc 的问题不只是“可能失败”,还包括分配耗时不确定、堆碎片、并发保护和异常路径难以回收。若必须使用,通常在启动阶段一次性分配,运行期间不再频繁申请释放,并对返回值和最大占用进行监控。

A7. 函数指针回调能不能耗时?

普通任务上下文的回调可以耗时,但要确认不会阻塞关键任务;中断上下文的回调应只完成取数、清标志、入队和唤醒任务。不能在 ISR 回调里调用阻塞式日志、动态内存、等待锁或长时间轮询。

A8. SPI 时序和四种模式

SPI 由片选、时钟、发送线和接收线组成。CPOL 决定空闲电平,CPHA 决定第一个数据采样边沿:

模式 CPOL CPHA 空闲电平 常见采样边沿
0 0 0 上升沿采样、下降沿改变
1 0 1 下降沿采样、上升沿改变
2 1 0 下降沿采样、上升沿改变
3 1 1 上升沿采样、下降沿改变

具体定义以从设备手册为准,除此之外还要关注 MSB/LSB、最大时钟、片选保持时间和收发位宽。

A9. SPI 与 I2C 如何选?

SPI 线数多、通常全双工、速度高,适合显示屏、Flash 和大吞吐传输;I2C 只需 SDA/SCL,支持地址寻址和多设备共享,适合传感器、EEPROM 等低速设备。I2C 需要上拉电阻,速度和总线电容相关;SPI 没有统一的寻址和仲裁协议,通常由片选管理设备。

A10. 环形缓冲区手撕题

下面是单生产者、单消费者的字节环形缓冲区。约定“浪费一个槽位”,因此容量为 RB_SIZE - 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdbool.h>
#include <stdint.h>

#define RB_SIZE 256u

typedef struct {
uint8_t buf[RB_SIZE];
volatile uint16_t head; /* 仅生产者修改 */
volatile uint16_t tail; /* 仅消费者修改 */
} ringbuf_t;

static bool rb_empty(const ringbuf_t *rb)
{
return rb->head == rb->tail;
}

static bool rb_full(const ringbuf_t *rb)
{
uint16_t next = (uint16_t)((rb->head + 1u) % RB_SIZE);
return next == rb->tail;
}

static bool rb_write(ringbuf_t *rb, uint8_t value)
{
uint16_t next = (uint16_t)((rb->head + 1u) % RB_SIZE);
if (next == rb->tail) {
return false;
}
rb->buf[rb->head] = value;
/* 先写数据,最后发布 head,避免消费者读到半成品。 */
rb->head = next;
return true;
}

static bool rb_read(ringbuf_t *rb, uint8_t *value)
{
if (rb->head == rb->tail) {
return false;
}
*value = rb->buf[rb->tail];
rb->tail = (uint16_t)((rb->tail + 1u) % RB_SIZE);
return true;
}

如果一个中断写、一个任务读,单生产者/单消费者且索引读写是原子的时,可以不加互斥锁。中断不能睡眠,所以不能使用会阻塞的 mutex。必要时可在很短的临界区内关中断,或者使用原子变量和内存屏障。若存在多个生产者或多个消费者,就需要额外的串行化保护,或直接使用 RTOS 的队列及 FromISR 接口。

B. 二面:FreeRTOS、调度和协议栈

B1. FreeRTOS 抢占式调度和时间片

configUSE_PREEMPTION=1 时,高优先级任务进入就绪态可以抢占低优先级任务。相同优先级的就绪任务是否在每个 tick 轮转,主要由 configUSE_TIME_SLICING 控制;关闭时间片后,同优先级任务通常要主动阻塞、延时或 taskYIELD() 才让出 CPU。实际行为还会受 configUSE_PREEMPTION、tick 配置和端口实现影响。

B2. 任务切换时保存什么,保存在哪里?

Cortex-M 异常进入时,硬件自动把 R0-R3R12LRPCxPSR 压入当前任务栈;PendSV 中再保存需要保留的通用寄存器,使用 FPU 时还要保存浮点上下文。每个任务的上下文放在自己的任务栈中,TCB 保存栈顶指针。恢复下一个任务的栈顶和寄存器后,异常返回会继续执行该任务的 PC。

B3. PendSV 为什么必须用于切换?

PendSV 不是“唯一能切换任务”的异常,而是 Cortex-M 为低优先级延迟处理设计的机制。把切换放到 PendSV 可以让 SysTick 或外设 ISR 只负责记录事件和触发请求,真正切换延后到嵌套中断全部退出后;同时 PendSV 设为最低优先级,不会打断紧急中断,能减少上下文嵌套和时序风险。启动第一个任务时通常还会使用 SVC。

B4. 空闲任务为什么最低优先级?

空闲任务在没有其他就绪任务时运行,负责回收删除任务的资源、执行 idle hook,以及在 tickless 模式下安排低功耗。它必须最低优先级,否则会抢占实际业务任务。应用不能假设 idle task 总能及时运行。

B5. 栈大小怎么定,如何发现栈溢出?

先根据最大调用深度、最大局部数组、库函数、格式化打印、FPU 和中断嵌套做静态估算,再留出余量。运行后用 uxTaskGetStackHighWaterMark() 查看历史最低余量,打开 configCHECK_FOR_STACK_OVERFLOW 并实现 vApplicationStackOverflowHook()。栈溢出可能覆盖 TCB、队列或其他任务数据,表现为随机死机,所以不能只看一次“当前剩余栈”。

B6. 优先级反转、继承和天花板

低优先级任务持有锁,高优先级任务等待锁,而中优先级任务持续运行,会造成高优先级任务反而迟迟不能执行。优先级继承让持锁的低优先级任务临时提升到等待者的优先级,释放锁后恢复;优先级天花板则为资源预设最高使用优先级,任务拿锁时提升到该优先级,实时性分析更严格但配置更复杂。FreeRTOS 互斥量支持优先级继承,普通二值信号量不用于这一目的。

B7. 队列、二值信号量、计数信号量和互斥量

  • 队列:传递数据,发送者和接收者可以解耦;
  • 二值信号量:通知“事件发生”,不携带数据;
  • 计数信号量:表示可用资源数量,例如连接池有多少空闲连接;
  • 互斥量:保护共享资源,有所有权并支持优先级继承。

它们在 FreeRTOS 内部有共同的队列基础结构,但控制块的使用方式和所有权语义不同。

B8. ISR 为什么必须使用 FromISR

普通 API 的等待参数可能让当前执行流进入阻塞态,但 ISR 没有任务控制块,不能睡眠或被调度器挂起;普通 API 还可能在错误的临界区和调度上下文中操作链表。FromISR 版本不阻塞,通过 pxHigherPriorityTaskWoken 返回是否唤醒高优先级任务,并用 portYIELD_FROM_ISR() 在退出中断时请求切换。还要遵守该中断的最大系统调用优先级限制。

B9. 临界区和 ISR 时长

临界区可以通过关中断、屏蔽特定优先级中断或暂停调度器实现。关中断时间过长会增大最坏中断延迟,导致串口溢出、系统 tick 延迟、实时任务超时或看门狗复位。ISR 中只做取数、清标志、入队和唤醒任务;printf 可能加锁、动态分配并阻塞串口,执行时间不可预测,因此不应直接调用。

B10. 软件定时器与硬件定时器

硬件定时器由外设计数,精度和确定性高,适合 PWM、捕获比较和严格周期中断。FreeRTOS 软件定时器由 timer service task 调度,使用方便但存在任务调度延迟。软件定时器回调运行在 timer task 中,不能长时间执行,也不应阻塞等待另一个会依赖 timer task 的事件。

B11. WiFi 从扫描到拿到 IP

设备扫描信道获取 AP 信息,选择目标 AP 后完成 802.11 认证和关联;如果启用 WPA/WPA2/WPA3,还要完成密钥协商(经典 WPA2 是四次握手)。关联成功后通过 DHCP Discover、Offer、Request、ACK 获取 IP、网关和 DNS,之后再进行 ARP、DNS 及 MQTT/HTTP 建连。任何一步失败都应记录原因码,而不是统一当作“连接失败”。

B12. WiFi 断线重连设计

用状态机区分未配置、扫描、认证、已连接、正在重连和错误状态。临时断线采用指数退避并加入随机抖动,限制重试频率;认证失败、密码错误等配置类错误应延长间隔或等待用户处理。重连前清理旧 socket,恢复后重新订阅 MQTT、同步时间和补发可靠数据,避免多个任务同时发起连接。

B13. TCP、UDP 与弱网重传

TCP 适合固件升级、配置和必须可靠有序的数据,可靠性由序号、确认、重传和拥塞控制提供,但它是字节流且可能有较大延迟。UDP 适合实时遥测、广播和自定义协议,但要在应用层增加序列号、ACK、超时重传、重复包过滤、最大重传次数和退避策略。实时数据通常允许丢旧数据,不应无限重传过期数据。

B14. TCP 粘包

TCP 只保证字节顺序,不保留发送端的消息边界。一次 send 可能被多次 recv 读出,也可能多个 send 被一次 recv 合并。接收端必须维护缓存,按固定长度、分隔符或“魔数 + 版本 + 长度 + 类型 + 负载 + CRC”循环解析,不能把一次 recv 当成一帧。

B15. BLE 广播、连接和配对

外设通过广播包公开设备名、服务 UUID 或厂商数据,中心设备扫描后根据地址和信号强度选择并发起连接。连接参数包括连接间隔、从机延迟和监督超时。配对负责建立密钥,绑定负责保存密钥供下次使用,加密负责保护后续链路;这些概念不能混为一谈。

B16. GATT、Service、Characteristic

GATT Server 提供数据模型,一个 Service 聚合一组相关功能,一个 Characteristic 表示具体数据及其属性(读、写、通知、指示),Descriptor 用于描述 Characteristic,例如 CCCD 用于配置通知。客户端通过 UUID 发现服务和特征,再按属性访问。

B17. MQTT QoS

QoS 0 是最多一次,没有确认;QoS 1 是至少一次,发布方收到 PUBACK 后认为完成,但重连或超时可能重复;QoS 2 通过 PUBREC、PUBREL、PUBCOMP 的四步握手实现协议层恰好一次。QoS 2 不代表业务一定只执行一次,业务仍应使用消息 ID、幂等设计和持久会话避免重复副作用。

B18. ESP32 睡眠和保存状态

Modem-sleep 关闭射频但 CPU 可继续运行;Light-sleep 暂停 CPU、保留部分 RAM 和外设状态;Deep-sleep 关闭 CPU 及大部分数字电源域,唤醒后通常从启动流程重新执行。普通 DRAM/内部 RAM 通常不保证保留,RTC slow/fast memory 可按电源域设置保留。少量唤醒原因和计数可放 RTC 内存,必须长期保存的配置应放 NVS/Flash,并考虑磨损均衡和 CRC。

B19. 电池设备运行一年怎么省电?

先测量各状态的电流和持续时间,计算平均功耗,而不是只看芯片标称睡眠电流。然后减少唤醒次数,批量采集和发送,缩短 WiFi 连接时间,选择合适的 BLE 广播/连接参数,关闭不用的外设和传感器电源,降低发射功率,使用中断唤醒和 tickless sleep,并通过重传策略减少弱网耗电。

C. 三面:工程与产品视野

C1. 为什么投 IoT?

我喜欢软硬件结合的开发。IoT 设备不仅要实现功能,还要处理通信、功耗、异常恢复、远程升级和量产定位,这些问题更接近真实产品。我之前做过 ESP32 数据透传,希望继续深入嵌入式系统、RTOS 和无线通信。

不要只回答“行业前景好”,最好结合自己的项目经历和想深入的技术方向。

C2. ESP32 与其他 WiFi 方案的差异

ESP32 集成 WiFi 和 BLE,成本低、资料丰富、ESP-IDF 基于 FreeRTOS,适合快速开发和中小型产品。与 MCU 加独立 WiFi 模块相比,外围硬件更简单、协议栈集成度更高;但在认证、超低功耗、工业温度、长期供货和高性能方面,要根据产品需求与其他方案比较,不能只看价格。

C3. OTA 断电为什么不变砖?

常用 A/B 双分区:新镜像写入非当前运行分区,下载过程中旧分区仍可启动;下载完成后校验长度、Hash、签名和硬件兼容性,再更新 otadata 或启动标志。新固件第一次启动进入 pending 状态,完成自检后确认;若启动失败、看门狗复位或多次未确认,Bootloader 回滚旧分区。启动元数据本身也应有冗余、CRC 和事务性更新。

C4. 固件加密、签名和 Secure Boot

签名验证固件来源和完整性,防止被篡改;加密保护固件机密性,防止读出逆向;Secure Boot 保证启动链上只有授权镜像;Flash Encryption 保护芯片外部存储内容。签名和加密解决的问题不同,量产时还要考虑密钥注入、生命周期管理和调试口关闭。

C5. 出货后现场故障如何定位?

设备至少应保存固件版本、硬件版本、重启原因、看门狗状态、异常寄存器、调用栈、最近日志、网络 RSSI、连接状态、队列水位和关键传感器数据。可按需上传压缩日志或 Core Dump,日志需要分级、限频和采样,避免日志本身造成阻塞、功耗或隐私泄露。

C6. 看门狗如何设计?

不要在一个永远运行的任务中无条件喂狗,否则其他任务死锁时仍可能掩盖故障。让关键任务定期上报心跳,由监控任务检查所有心跳、队列和系统状态,只有整体健康时才喂任务看门狗;中断看门狗用于检测 ISR 卡死或长时间屏蔽中断。复位后要保存复位原因并进入安全恢复流程。

C7. 如何保证代码质量、如何调 Bug?

从需求评审开始明确边界和异常场景;采用模块化接口、编译器高等级告警、静态分析、代码评审、单元测试、集成测试、压力测试和 CI。调 Bug 时先稳定复现,再用日志、断点、寄存器、逻辑分析仪、栈水位和最小化实验缩小范围;修复后增加回归测试,并确认没有引入竞态、栈溢出和资源泄漏。

C8. 看 SDK 或开源源码怎么回答?

不要只说“看过源码”。应说明看了什么、为了解决什么问题、跟踪了哪条调用链以及得出什么结论。例如分析 ESP-IDF 的 WiFi 事件回调、FreeRTOS 队列发送路径或驱动的中断处理:先从公开 API 入手,再跟到任务、队列、ISR 和硬件寄存器,最后用日志或调试验证理解。

C9. 一个需求从拿到手到交付

先确认输入输出、时序、功耗、异常、兼容性和验收标准;再拆模块、定义接口和状态机,评估 RAM/Flash/CPU 与风险,设计测试用例。实现后依次做单元测试、板级联调、异常和压力测试、代码评审、版本打包和发布验证,最后保留升级、回滚和现场诊断手段。

C10. 协议栈还是应用?

可以回答:

我希望先把驱动、RTOS 和通信基础打牢,再逐步深入协议栈。协议栈能让我理解连接、可靠性和资源管理,应用开发能让我接触真实业务和用户场景。目前我更偏向通信和设备状态管理相关的嵌入式开发,也愿意根据团队项目需要调整。

D. 自我介绍和项目追问模板

1. 一分钟自我介绍

面试官您好,我主要准备嵌入式 C、RTOS 和无线通信方向。熟悉 C 语言、数据结构、常见外设驱动以及 FreeRTOS/uC/OS-II 的任务、队列、信号量和中断机制。做过基于 ESP32 的无线数据透传项目,负责数据收发、协议拆包、校验、重传和异常处理。在项目中也关注内存占用、任务栈、功耗和现场定位。希望在 IoT 团队继续提升底层系统和通信协议能力。

2. “最有挑战的项目”回答框架

按以下顺序组织:

  1. 项目背景和目标;
  2. 自己负责的模块;
  3. 关键技术方案和数据流;
  4. 遇到的最难问题;
  5. 如何定位和修复;
  6. 指标结果和后续改进。

不要只说“用了 ESP32、WiFi 和 FreeRTOS”,要能画出任务、队列、缓冲区、中断和网络连接之间的关系。

3. 偶发死机、栈溢出怎么讲?

可以说:

现象是设备运行一段时间后偶发复位,初步排除了供电和看门狗误触发。通过保存复位原因、异常寄存器和调用栈,发现故障集中在某个通信任务。随后查看 uxTaskGetStackHighWaterMark(),发现该任务剩余栈空间很小;任务中又有较大的局部数组和格式化打印,最终确认是栈溢出。修复方式是增大任务栈、把大数组移到静态区、限制日志长度,并加入栈溢出检测和回归压力测试。

栈大小不能只凭经验,要结合最大调用深度、局部变量、库函数和中断嵌套估算,再用运行时水位和故障注入验证。

E. 高频追问速答

free 后为什么置 NULL

防止当前指针被再次误用或重复释放;但其他指向同一块内存的指针仍是悬空指针,真正解决需要清晰的所有权设计。

inline 一定展开吗?

不一定。inline 只是建议,编译器会综合函数大小、调用频率、优化级别、调试选项和寄存器压力决定是否展开。

volatile 能解决多线程问题吗?

不能。它解决的是编译器不能假设值不变的问题,不保证复合操作原子性,也不保证锁语义和内存顺序。

中断里能不能加锁?

不能使用会阻塞或睡眠的普通互斥锁。若只是保护极短的共享数据,可关中断或使用 ISR 安全的原子操作;更常见的是 ISR 入队,任务加锁处理。

二值信号量和互斥量看起来一样,核心区别是什么?

二值信号量用于通知或同步,没有严格的持有者;互斥量用于资源所有权,有谁持有谁释放的语义,并通常支持优先级继承。

深睡后状态放哪里?

少量、临时、允许掉电保护范围内的数据放 RTC 内存;需要跨掉电、可靠保存和长期使用的数据放 NVS/Flash,并加版本、长度、CRC 和磨损均衡。

单生产者单消费者环形队列为什么可以无锁?

因为生产者只修改 head,消费者只修改 tail,彼此只读取对方索引;只要索引读写是原子的、写数据后再发布 head,就不需要双方同时修改同一变量。多生产者/多消费者则不满足这个前提。


附录 F:乐鑫应用方案方向高概率专项题

说明:当前环境无法访问外部网页,以下不是对某一条实时面经的逐字转录,而是根据乐鑫公开技术栈(ESP-IDF、FreeRTOS、Wi-Fi/BLE、lwIP、NVS、OTA、低功耗和应用方案支持)以及嵌入式软件开发实习/校招岗位常见要求整理出的高概率补充题。面试官会根据简历和项目选择其中一部分追问。

F1. ESP-IDF 和工程基础

1. ESP-IDF 是什么?和 Arduino-ESP32 有什么区别?

ESP-IDF 是乐鑫官方开发框架,包含芯片 HAL、驱动、FreeRTOS、Wi-Fi/Bluetooth 协议栈、网络、存储、OTA、电源管理和构建系统等。Arduino-ESP32 在 ESP-IDF 之上提供更简单的 Arduino API,适合快速验证;ESP-IDF 对底层配置、性能、内存、任务和产品化控制更强。

如果是量产产品或需要精确控制资源,我会优先选择 ESP-IDF;如果只是快速验证传感器和业务想法,Arduino API 可以提高开发速度。二者也不能简单理解为“一个快一个慢”,最终取决于调用的组件和配置。

2. ESP-IDF 的启动流程大概是什么?

芯片复位后先进入 ROM Bootloader,读取启动配置并加载二级 Bootloader;二级 Bootloader 读取分区表,选择 factory 或 OTA 分区,完成镜像校验后启动应用。应用启动后进行 C 运行时初始化、堆初始化、驱动和系统组件初始化,最后进入 app_main()

面试时可以补充:启动阶段还涉及 Flash 映射、分区表、Secure Boot、Flash Encryption 和 OTA 回滚状态。应用不能假设一进入 app_main() 所有外设都已经按业务需要初始化。

3. app_main() 和普通 main() 有什么不同?

在 ESP-IDF 中,系统启动代码会创建 FreeRTOS 环境和主任务,然后调用 app_main()app_main() 不是裸机意义上永远不返回的 main,它运行在一个任务上下文中,可以创建其他任务、初始化组件,结束后返回;但实际应用通常让系统任务继续运行,或由其他任务承担业务。

4. ESP-IDF 的组件和 CMakeLists.txt 怎么组织?

一个组件通常包含源文件、头文件和 CMakeLists.txt,通过 idf_component_register(SRCS ... INCLUDE_DIRS ... REQUIRES ...) 声明。组件之间通过 REQUIRESPRIV_REQUIRES 表达依赖,避免所有文件都依赖全局头文件。配置项一般放在 Kconfig,通过 menuconfig 生成 sdkconfig,代码中使用 CONFIG_... 宏。

这样做的好处是模块边界、依赖和编译配置清晰,便于为不同芯片或产品裁剪功能。

5. menuconfigsdkconfig 和 Kconfig 的关系?

Kconfig 定义配置项、默认值、依赖和可见条件;menuconfig 是交互式配置工具;用户选择结果保存到 sdkconfig,构建系统再生成 sdkconfig.h 中的 CONFIG_... 宏。sdkconfig 通常是项目配置产物,团队应通过 sdkconfig.defaults 管理可复现的默认配置,避免只依赖某个开发者本地的配置。

6. 为什么有些函数要放 IRAM?

ESP32 执行代码通常通过 Flash cache 映射访问。擦写 Flash 或某些缓存被禁用期间,依赖 Flash 的代码不能执行;必须在此期间运行的中断服务函数和调用链需要放入 IRAM,相关只读数据放入 DRAM。使用 IRAM_ATTR 只是第一步,被它调用的函数、常量和数据也必须满足同样约束,否则仍可能出现 Cache disabled 错误。

7. ISR 中使用 ESP-IDF API 有什么限制?

先确认 API 是否标注为 ISR-safe 或有 FromISR 版本。ISR 中不能调用会阻塞、分配堆、访问 Flash、打印大量日志或等待互斥锁的 API。若 ISR 需要通知任务,应使用队列、任务通知、信号量的 ISR 版本,并在退出前根据返回标志请求任务切换。

8. ESP32 的堆为什么要关注 capability?

ESP-IDF 的堆可以按能力分配,例如要求 8-bit 可访问、DMA 可访问、内部 RAM 或外部 PSRAM。普通 malloc 不一定满足 DMA 或中断使用要求;驱动缓冲区应根据硬件要求使用带 capability 的分配接口,并确认缓存、对齐和生命周期。

9. 内部 RAM、PSRAM 怎么选?

内部 RAM 速度和确定性更好,适合中断、DMA 描述符、实时任务栈和频繁访问的数据;PSRAM 容量大,适合图片、缓存和非实时大对象,但访问延迟更高、受 cache 影响,不能默认满足所有 DMA 或 ISR 场景。任务栈放 PSRAM 前要确认端口和组件支持。

10. 如何排查 ESP-IDF 的内存问题?

先区分堆不足、栈溢出、内存泄漏、越界和碎片。可以记录 heap_caps_get_free_size()heap_caps_get_largest_free_block()、各能力堆余量和任务栈 high-water mark;再使用 heap tracing、堆完整性检查、栈溢出检测、地址消毒或 GDB。不要只看“剩余堆大小”,最大连续块更能反映碎片问题。

F2. Wi-Fi、网络和应用方案

11. Wi-Fi 事件回调怎么设计?

Wi-Fi 驱动事件和 IP 事件应由事件循环接收,再转换成应用自己的状态事件。例如 STA_START 触发连接,STA_DISCONNECTED 进入退避重连,GOT_IP 才允许启动 MQTT。不要在 Wi-Fi 事件回调中做长时间网络操作;回调只更新状态或投递消息,业务由独立任务处理。

12. esp_netif、lwIP 和应用层是什么关系?

Wi-Fi 驱动负责无线链路,esp_netif 把底层接口接入网络栈,lwIP 提供 IP、TCP、UDP、DHCP、DNS 等协议,应用通过 BSD socket、HTTP、MQTT 等接口使用它们。排查网络问题时要区分射频/关联问题、IP 获取问题、TCP 建连问题和应用协议问题。

13. Wi-Fi 已连接但应用访问不了服务器,怎么定位?

按层排查:

  1. 是否真的完成关联,RSSI、信道和断线原因是什么;
  2. 是否拿到有效 IP、网关、DNS;
  3. 是否能 ARP 到网关,是否能解析域名;
  4. TCP 三次握手是否成功,是否被防火墙拒绝;
  5. TLS 证书、时间和 SNI 是否正确;
  6. MQTT/HTTP 的认证、主题或 URL 是否正确。

这样可以避免把所有问题都归因于“Wi-Fi 信号不好”。

14. 为什么设备拿到 IP 后还要同步时间?

TLS 证书有效期校验依赖正确系统时间;设备上电后 RTC 可能没有准确时间,所以通常在网络可用后通过 SNTP 同步,再建立 HTTPS/MQTT TLS 连接。时间同步失败时应有重试和合理的错误提示,不能简单关闭证书校验作为长期方案。

15. TLS 在 IoT 设备上要注意什么?

需要验证服务器证书链、域名、有效期和系统时间,保护设备私钥,合理设置接收缓冲区和任务栈,并评估握手时的 CPU、RAM 和 Flash 占用。证书更新应通过安全 OTA 或受控配置完成,不能为了“先连上”而永久跳过证书验证。

16. 如何设计 Wi-Fi 配网?

可以使用 SoftAP、SmartConfig、BLE provisioning 或设备配网协议。配网数据应写入 NVS,密码不要通过明文日志输出;配网失败、超时和恢复出厂要有明确状态。量产产品要考虑多个设备同时配网、手机权限、超时重试和凭据擦除。

17. ESP-NOW 适合什么场景?

ESP-NOW 是低开销、无需传统 Wi-Fi 关联的 ESP 设备间通信方式,适合局部遥控、传感器数据和快速发现。它不是完整的 IP 网络,包长、可靠性、加密、设备数量和距离都有限;需要可靠传输时仍应在应用层设计序号、ACK、重传和去重。

18. Wi-Fi 和 BLE 共存要注意什么?

二者可能共享 2.4 GHz 射频资源,吞吐量、延迟和功耗会互相影响。应根据业务调整扫描、广播、连接间隔和 Wi-Fi 发射策略,避免 BLE 长时间扫描与 Wi-Fi 高吞吐同时运行;还要关注协议栈任务优先级和内存峰值。

F3. 驱动、硬件和调试

19. GPIO 中断和电平/边沿触发怎么选?

边沿触发适合按钮变化、脉冲和数据到达;电平触发适合设备保持请求直到软件处理的场景。电平中断若不及时清除或条件未消失,退出 ISR 后可能立即再次进入。机械按键还需要消抖,可以用定时器、状态机或软件滤波,不要在 ISR 里延时消抖。

20. GPIO 上拉、下拉和悬空输入有什么问题?

悬空输入容易受到噪声影响,产生随机电平和多次中断。上拉或下拉要结合外部电路、电平有效逻辑和功耗选择;I2C 等开漏总线必须有合适的上拉,电阻过大上升沿慢,过小会增大低电平电流。

21. UART 接收为什么常用 DMA 或环形缓冲区?

逐字节中断在高波特率或突发数据下会占用大量 CPU,也容易因任务调度不及时而溢出。DMA 可以批量搬运数据,ISR 只处理半满、满或空闲线事件,再由任务从环形缓冲区按协议解析。解析层必须处理半包、粘包、长度异常和 CRC 错误。

22. SPI 读写全是 0xFF0x00 怎么查?

检查片选时序、SPI 模式、时钟频率、位序、引脚复用、电源和复位;确认从设备是否要求先发命令和地址,读操作是否需要 dummy byte;再用逻辑分析仪观察 CS、SCLK、MOSI、MISO。0xFF 可能是 MISO 没有被设备驱动,0x00 可能是线路被拉低,不能仅凭数据值下结论。

23. DMA 缓冲区有什么要求?

需要满足硬件支持的地址区域、对齐、长度和缓存一致性要求。CPU 与 DMA 共享缓存时,要按平台进行 cache clean/invalidate,或使用非缓存、DMA-capable 内存。DMA 完成前不能释放或修改缓冲区,完成后还要处理所有权转移。

24. 看门狗复位和普通崩溃怎么区分?

读取复位原因寄存器和 ESP-IDF 启动日志,区分任务看门狗、中断看门狗、软件复位、Brownout 和 Panic。看门狗通常意味着某任务长时间不让出 CPU、死锁、阻塞在不可恢复操作,或 ISR/临界区过长;不应只把看门狗时间调大来掩盖根因。

25. ESP-IDF Panic 日志怎么看?

关注异常类型、PC、EXCVADDR、寄存器、当前任务名和回溯。用对应版本的 ELF 文件进行地址解析,确认编译优化和固件版本一致;根据回溯判断空指针、非法访问、栈溢出、Cache disabled 或看门狗。现场上报 Core Dump 时要限制大小、版本和隐私风险。

26. GDB、JTAG 和逻辑分析仪分别解决什么问题?

GDB/JTAG 适合观察 CPU 寄存器、任务栈、断点和调用链;逻辑分析仪适合验证 GPIO、UART、SPI、I2C 时序;示波器适合电源波动、复位和模拟信号。复杂问题通常需要软件回溯和硬件波形结合,而不是只靠串口打印。

F4. 存储、升级和安全

27. NVS 和直接写 Flash 有什么区别?

NVS 提供键值存储、版本管理和一定的磨损均衡,适合配置、配网凭据和少量状态。直接写 Flash 需要自己处理扇区擦除、对齐、掉电一致性、校验和磨损。频繁更新计数器不应简单每次写同一个地址,应使用 NVS 或日志式/双备份设计。

28. 掉电时如何保证配置不损坏?

使用 A/B 两份记录或日志追加方式,每条记录包含 magic、版本、长度、数据和 CRC;写入新记录并校验成功后再将其视为最新,启动时选择版本最高且 CRC 正确的一份。Flash 擦除期间掉电时,要保证旧记录仍可恢复。

29. OTA 包除了 CRC 还需要什么?

CRC 主要检测传输或存储中的随机错误,不能防止恶意篡改。产品 OTA 至少应有镜像长度、版本、硬件兼容性、Hash 和数字签名;还要做反回滚、下载断点/重试、启动确认和失败回滚。签名校验必须在切换启动分区前完成。

30. OTA 过程中不同阶段断电会怎样?

  • 下载中断:备用分区镜像不完整,旧分区继续运行;
  • 下载完成但未校验:启动标志不应更新;
  • 已切换但新固件未确认:Bootloader 应保留旧分区并允许回滚;
  • 元数据更新时断电:使用冗余记录、CRC 和事务状态恢复。

31. 如何做版本兼容和回滚?

固件头中记录产品型号、硬件版本、最低 Bootloader 版本、协议版本和安全计数器。升级前校验兼容性,升级后做外设、配置和网络自检;配置格式变化应有迁移或回退策略。安全要求不允许回滚到有漏洞的版本时,使用单调递增安全版本号。

F5. C 语言手撕和算法题

32. 实现 memcpy 时要注意什么?

普通 memcpy 要求源和目的区域不重叠,逐字节或按机器字长复制;重叠区域必须使用 memmove,根据地址方向决定从前向后还是从后向前复制。还要考虑空指针约束、长度为零、对齐和别名规则,不能为了“快”而无条件强转成大字长指针。

33. 如何实现一个判断大小端的函数?

1
2
3
4
5
static int is_little_endian(void)
{
uint16_t value = 0x0001;
return *(const uint8_t *)&value == 0x01;
}

实际协议代码中不应依赖本机大小端,而要使用显式的 get_u16_be()put_u32_le() 等函数进行转换。

34. CRC 和校验和有什么区别?

简单累加校验实现成本低,但对位错、交换错和连续突发错误的检测能力有限。CRC 基于多项式除法,对常见突发错误检测能力更强,适合串口、Flash 和无线帧。回答 CRC 时要说明多项式、初值、输入/输出反射和最终异或值,否则同名 CRC 算法也可能算出不同结果。

35. 如何设计一个非阻塞状态机?

用枚举表示状态,用事件和时间戳驱动状态转移,每次 run_once() 只执行一小步,不在状态函数中 delay 或长时间轮询。例如 Wi-Fi 配网可分为 IDLESCANNINGCONNECTINGGOT_IPRETRY_WAITERROR,超时由定时器事件推进。非阻塞状态机更适合 RTOS、多外设和断线重连。

36. 生产者消费者如何避免丢数据?

先定义满载策略:阻塞等待、丢最新、丢最旧或覆盖;再确定队列深度,依据峰值生产速率、最坏消费延迟和数据大小计算,而不是凭感觉。中断生产者使用 ISR-safe 队列,消费者任务处理复杂逻辑;同时记录队列最大水位和丢弃计数,方便现场判断。

37. 看到一段 C 代码如何判断未定义行为?

重点检查越界、释放后使用、重复释放、未初始化读取、整数溢出、移位超出位宽、错误类型别名、返回局部变量地址、修改同一变量的无序副作用和数据竞争。嵌入式中还要额外检查寄存器访问宽度、对齐、DMA 生命周期和 ISR/任务上下文限制。

F6. 应用方案场景题

38. 客户说“偶发连不上 Wi-Fi”,你怎么处理?

先收集可复现条件:距离、路由器型号、信道、加密方式、重试次数、供电和温度。设备端记录扫描结果、认证/关联失败原因、RSSI、断线原因、DHCP 和 DNS 状态。再做分层实验:固定信道、关闭省电、换 AP、抓取空口或驱动日志,判断是射频环境、协议兼容、凭据、功耗还是应用超时。

39. 客户要求“连接必须稳定”,如何把需求量化?

把“稳定”拆成指标,例如首次连接成功率、平均建连时间、断线检测时间、重连 P95、连续运行时长、丢包率、功耗和可接受的离线缓存量。只有指标可测,才能设计测试和判断改动是否有效。

40. 如何处理用户现场无法复现的问题?

增加版本、设备 ID、复位原因、网络环境、关键状态转换、计数器和限速日志;对敏感数据脱敏,对关键路径保留环形日志。通过远程配置打开特定诊断开关,问题发生后上传最小必要数据,避免量产版本长期打开高频 debug 日志。

41. 你如何平衡功能、功耗、成本和稳定性?

先识别硬约束,再做可量化的权衡。例如增加缓存可能提高弱网可靠性但占用 RAM;提高重传次数可能降低丢包但增加功耗;更高日志级别有助于定位但影响实时性。通过测试数据而不是主观判断选择方案,并给出降级和故障恢复路径。

F7. 乐鑫方向最后冲刺清单

建议至少能不看资料解释以下关键词:

  • ESP-IDF、app_main、组件、CMake、Kconfig、menuconfig
  • ROM Bootloader、二级 Bootloader、分区表、otadata、A/B OTA;
  • FreeRTOS 任务、TCB、PendSV、SysTick、队列、任务通知、FromISR
  • IRAM、DRAM、PSRAM、Flash cache、DMA capability;
  • Wi-Fi 扫描、认证、关联、四次握手、DHCP、DNS、SNTP、TLS;
  • esp_netif、lwIP、socket、TCP 字节流、MQTT QoS;
  • BLE 广播、连接参数、配对、绑定、GATT、Service、Characteristic、CCCD;
  • NVS、磨损均衡、掉电一致性、Secure Boot、Flash Encryption;
  • 栈水位、堆碎片、heap tracing、Panic、Core Dump、任务/中断看门狗;
  • SPI/I2C/UART/DMA、GPIO 中断、逻辑分析仪、JTAG、非阻塞状态机。

面试时的诚实回答模板

遇到没用过的 ESP-IDF API,不要硬说“用过”。可以回答:

这个具体 API 我没有在项目中直接使用过,但我理解它解决的是……。如果实际接手,我会先看官方组件文档和示例,再沿着调用链确认任务上下文、内存来源、阻塞属性和错误码,最后用最小 demo 和异常场景验证。

这比编造使用经历更容易获得面试官信任,也能把问题引导到你真正掌握的原理上。

1