嵌入式软件企业面
1. 自我介绍(侧重 MCU 项目)
面试官您好,我是××,××学校××专业。
我主要做 MCU 嵌入式,最熟的一个项目是×××。硬件用的是GD32/STM32,软件上了FreeRTOS。我负责的是××模块,比如串口通信、传感器采集、协议解析这块。
项目里我独立写过/改过××驱动,也处理过一些实际问题,比如通信不稳定、任务卡死之类,后面都定位并修掉了。
我对 MCU 底层和外设比较感兴趣,看到贵司做 GD32,和之前用的平台也比较接近,所以想来试试这个岗位。
2. 为什么选 GD32 / STM32?选型看哪些?
为什么选:
我选 STM32 或 GD32,主要是因为生态成熟:例程多、工具链完整、资料好找,开发效率比较高。GD32 和 STM32 外设用法很像,之前做过 STM32 的项目,迁到 GD32 上手会快一些。具体看项目对成本、供货和原厂支持的要求。
选型我会先看:
第一,Flash 和 RAM够不够,代码和数据能不能放下。
第二,外设数量,比如要几路 UART、有没有 CAN、ADC 路数够不够。
第三,主频和算力,有没有 RTOS、要不要跑复杂算法。
第四,封装和引脚,板子能不能放下。
第五,如果是量产,还会看供货、价格和温度等级。
不会只盯着一颗芯片性能,而是先看项目需求再反推型号。
3.volatile和const在寄存器和中断共享变量里怎么用?
volatile:
它的作用是告诉编译器:这个变量随时可能变,不要优化,每次都要从内存里读。
在寄存器操作上,访问硬件寄存器一般都要用volatile,不然编译器可能把读寄存器优化掉,导致状态判断不对。
在中断和主程序都会访问的变量上也要加,比如中断里置位的rx_flag,主循环里读它,如果不加volatile,主循环可能一直读到旧值。
不过我要补充一点:volatile只解决编译器优化问题,不保证多字节操作的原子性。如果是一个结构体或者head++这种复合操作,还要关中断或者用互斥保护。
const:
它表示只读。像配置表、查表数组、只读参数我会用const,可以放在 Flash 里,也防止后面被误改。
如果是只读寄存器,可能会看到volatile const这种写法,表示硬件寄存器只读。
4. 结构体对齐是什么?#pragma pack用不对会怎样?
结构体对齐是编译器为了让 CPU 访问更高效,会在成员之间插入空白字节。
比如一个char加一个int,char占 1 字节,后面可能补 3 字节,再排int,所以整个结构体大小往往比成员简单相加要大。
#pragma pack(1)是把对齐关掉,成员紧凑排列。这个在通信协议打包时经常用,因为协议是按字节定义的。
如果用不对,会有几个问题:
一是和协议定义对不上,解析出来的数据错。
二是有些 MCU 对非对齐访问支持不好,可能直接 HardFault。
三是不同编译器、不同平台结构体大小可能不一致,联调容易出问题。
所以我的做法是:协议结构体单独pack(1),普通业务结构体用默认对齐,不随便全局改 pack。
5. 推挽输出和开漏输出区别?I2C 为什么用开漏?
推挽输出可以主动拉高或拉低,驱动能力强,适合 LED、片选这类需要明确高低的场景。
开漏输出只能主动拉低,拉高要靠外部上拉电阻,高电平本质是“释放总线”。
I2C 用开漏,核心原因有三个:
第一,I2C 是多设备共总线,需要线与,开漏才能多个设备同时挂在一起而不冲突。
第二,方便电平兼容,不同电压的器件可以通过上拉统一到同一高电平。
第三,从机可以做时钟拉伸,把 SCL 拉低延长时序。
所以 I2C 不仅软件上要配开漏,硬件上还必须接上拉电阻。
6. 定时器 PWM 怎么配?占空比怎么改?
我一般会这几步:
先开定时器时钟,然后配PSC 和 ARR,确定 PWM 频率。
再把通道设成PWM 模式,使能输出。
最后通过CCR来控制占空比。
频率大概是:
定时器时钟除以(PSC+1)再除以(ARR+1)。
占空比就是:
CCR / (ARR+1)。
如果只想改占空比,不动频率,运行中改CCR就行;不要去动 ARR,不然频率也会变。
实际项目里我还会注意 GPIO 复用、极性、是否需要互补输出和死区,比如电机控制就会用到这些。
7. IWDG 和 WWDG 区别?什么时候用哪个?
IWDG用的是独立低速时钟,就算主时钟出问题,它通常还能工作,所以更像最后一道保险。喂狗条件也简单,只要在溢出前喂就行。
WWDG用的是系统时钟分频出来的,它有个窗口,喂太早或太晚都会复位,所以不仅能看程序死没死,还能看程序是不是在预期时间范围内运行。
我一般的经验是:
正常运行靠IWDG做兜底;
如果项目对任务时序要求很严,比如功能安全相关,会考虑WWDG。
很多普通项目只开 IWDG 就够了。
8. UART 波特率怎么算?两边偏差会怎样?
波特率本质就是把定时器时钟分频到目标通信速率。
常见算法是:
波特率 = fPCLK / (16 × USARTDIV)。
实际开发里我一般不会手算,直接用 CubeMX 或 HAL 配置,它会根据时钟树算出 BRR。
如果两边波特率偏差太大,典型现象就是乱码、丢字节、偶发通信失败。UART 对误差是有容忍范围的,一般总共几个百分点以内还能接受,具体看位数和采样方式。
所以硬件上我会尽量用精度高的晶振,软件上关键数据会加校验,不能裸 UART 硬扛误差。
9. CAN 标准帧和扩展帧区别?仲裁怎么做?
标准帧ID 是 11 位,扩展帧是 29 位。扩展帧能表示更多 ID,适合节点多、ID 规划复杂的系统。
CAN 仲裁的核心是:ID 越小优先级越高,而且是非破坏性仲裁。
多个节点同时发时,大家都发 ID 位,发 1 但读到 0 的节点就知道有更高优先级节点在发,自己退出发送。这样总线上的数据不会被破坏。
实际项目里我会特别注意 ID 规划,关键报文 ID 设小一点,保证实时性。
10. SPI 和 I2C 选传感器怎么取舍?
我会先看三个点:速度、线数、拓扑。
SPI 是全双工,速度高,但线多,每个从机通常还要一根 CS。
I2C 是半双工,速度低一些,但只需要两根线,多设备靠地址区分。
如果是高速 ADC、Flash、屏幕这种大数据量,我倾向 SPI。
如果是温湿度、气压、EEPROM这种低速传感器,I2C 更省引脚,布线也简单。
同一个传感器如果两种接口都有,我会结合板子引脚资源和速率要求来选,不是单纯看哪个“更好”。
11. DMA 内存递增、外设地址固定是什么意思?
这个在 UART、ADC 接收里非常常见。
外设地址固定,是因为像USART->DR、ADC->DR这种寄存器地址不会变,DMA 每次都来同一个寄存器取/放数据。
内存地址递增,是因为我要把收到的数据依次存进数组,比如buf[0]、buf[1]、buf[2]...,所以内存指针要往后走。
UART RX 典型就是:外设不递增,内存递增。
UART TX 则反过来:内存递增,外设固定。
理解这个,DMA 配置就不容易配反。
12. Cortex-M 中断优先级分组怎么理解?
Cortex-M 的中断优先级可以分成抢占优先级和子优先级,具体各占几位由PRIGROUP决定。
我的理解是:
抢占优先级决定能不能打断别人。
子优先级只是在同时 pending 时决定谁先响应,不能互相打断。
数值越小,优先级越高。
实际项目里我会把时间要求最紧的中断,比如高速采样、关键通信,抢占优先级设高一点;不太紧急的放低一点,避免中断嵌套太深。
13. 程序跑飞或 HardFault 怎么查?
我一般会按这个顺序:
第一步,先看Fault 寄存器,比如 CFSR、HFSR,判断是 Bus Fault、MemManage 还是 Usage Fault。
第二步,看栈里保存的PC 和 LR,定位是哪条指令出问题。
第三步,用 IDE 或addr2line找到对应源码行。
常见原因我会优先怀疑:
空指针、数组越界、栈溢出、访问没开时钟的外设、非对齐访问、除零。
如果是 FreeRTOS 项目,我还会查任务栈高水位,看是不是某个任务栈给小了。
定位后不会只改一行就完事,我会做复现、修复、长时间跑稳定性测试,确认不是偶发。
14. FreeRTOS 任务栈怎么估?栈溢出什么现象?
我一般不会一开始就算得很准,而是先给大一点,再用高水位反推。
开发阶段会开uxTaskGetStackHighWaterMark(),看任务运行后最少还剩多少栈。如果剩余很少,就加大。
估算时会考虑:
局部变量大不大、调用层深不深、有没有printf、有没有浮点运算、会不会被中断打断。
栈溢出我遇到过几种典型现象:
系统偶发 HardFault;
某个变量值突然不对;
任务逻辑乱掉;
开configCHECK_FOR_STACK_OVERFLOW后直接进钩子函数。
所以我现在习惯在调试版把栈监控打开。
15. 互斥量和二值信号量区别?
我会这么区分:
互斥量是用来保护共享资源的,谁加锁谁解锁,而且 FreeRTOS 的互斥量支持优先级继承。
二值信号量更像一个同步标志,用来通知“某个事件发生了”,不一定强调资源归属。
比如保护一个全局缓冲区和 UART 发送资源,我会用Mutex。
如果是中断通知任务“数据到了”,我更倾向信号量或任务通知。
面试里我常记一句:保护资源用 Mutex,传递事件用信号量。
16. 什么是优先级反转?FreeRTOS 怎么解决?
优先级反转就是:高优先级任务在等低优先级任务手里的资源,结果中优先级任务把低优先级抢走了,导致高优先级反而被间接堵住。
FreeRTOS 里我主要用互斥量的优先级继承来缓解。低优先级任务持有 Mutex 时,如果高优先级任务在等,会临时把低优先级抬上去,让它尽快释放资源。
另外从设计上,我会尽量缩短持锁时间,能不在锁里做阻塞操作就不做。
17. Sleep、Stop、Standby 区别?唤醒怎么配?
我的理解可以简单记:
Sleep功耗降一些,CPU 停,很多外设还在,唤醒快。
Stop功耗更低,大部分时钟停了,但 RAM 通常还在,唤醒后要重新配时钟。
Standby最省电,但唤醒后往往相当于复位重启,RAM 内容一般保不住。
唤醒源常见有EXTI、RTC、串口某些模式、WKUP 引脚,具体要看芯片手册。
低功耗项目里我会先关不用外设时钟,再配唤醒源,进低功耗前把 IO 状态也处理好,避免漏电。
18. 项目里最难的 bug 是什么?怎么定位、怎么确认修好?
我举一个比较有代表性的:
之前有个 FreeRTOS 项目,运行几小时后会偶发 HardFault,不是必现。
我一开始以为是硬件干扰,后来通过 Fault 寄存器和 PC 定位,发现总是在某个通信任务附近出问题。再查栈高水位,发现这个任务栈剩余几乎为 0。
根因是任务里加了较大的局部数组,还调了printf,栈给 128 word 明显不够,溢出后踩到了别的内存,所以表现成偶发 HardFault。
修复就是把栈加大,去掉不必要的printf,并打开栈溢出检测。
验证不是只跑 10 分钟,而是连续跑 24~72 小时,同时看 High Water Mark,确认栈还有余量,再做回归测试。
