紫光同芯嵌入式一面:OTA 掉电保护、volatile 与 STM32 启动
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 如果 OTA 升级过程中突然掉电,怎么保证设备还能启动?
- 讲一讲堆和栈的区别?
- 讲一讲 volatile 关键字的作用是什么?
- PC 寄存器是什么?STM32 中常见的 CPU 寄存器有哪些?
- 讲一讲 STM32 启动后,它的栈地址是怎么配置的?
int *p、int **p、int (*p)[10]分别表示什么?- 请做一下自我介绍,并介绍实习项目
《参考解析》
OTA 掉电保护:A/B 双分区 + 完整性校验 + 回滚
核心是解决固件升级的原子性和可回滚,第一条纪律就是绝不在原地覆盖当前正在运行的唯一 APP。典型布局是 Bootloader + APP_A(当前运行区)+ APP_B(下载/备份区):
- 新固件下载到 APP_B,全程不碰 APP_A;
- 下载完成后做完整性校验(CRC32 / SHA-256,配合固件头里的长度、版本、签名);
- 校验通过才把「APP_B 有效」的标记写进一块独立的元数据区(这一步必须原子:用双备份 + 序号/CRC,或写到 flash 的独立扇区,防止标记本身写坏);
- 重启,Bootloader 读标记选择 APP_B。
下载途中掉电时 APP_B 不完整、校验必然失败,Bootloader 自动继续启动 APP_A,设备照常可用。再进一步是「启动确认」机制:APP_B 第一次启动后要在若干秒内(或连续 N 次启动内)主动写「确认成功」,否则 Bootloader 计数超限就回滚到 APP_A——这样能覆盖「固件完整但跑不起来」的情况。细节上还要注意:元数据区要用两个 slot 轮流写加序号比较,避免写标记时掉电导致两边都坏;跨版本升级要禁止降级或校验版本号;回滚路径本身也要做掉电测试,用断电夹具在下载、校验、写标记、重启各阶段反复打断来验证。
堆与栈:从管理方式、生命周期、大小、使用场景四个维度答
栈由编译器/CPU 按函数调用自动管理,存放局部变量、函数参数、返回地址和部分上下文,生命周期跟随函数调用(返回即释放),访问快(SP 相对寻址、命中 cache),但空间有限且溢出后果严重(踩坏相邻数据或触发 HardFault)。堆由程序动态申请释放(malloc/free、pvPortMalloc),生命周期由程序员控制,适合大小不确定、跨函数存活的动态数据,代价是分配开销、内存碎片和泄漏风险。
嵌入式语境下还要补两点:MCU/FreeRTOS 中每个任务有自己独立的任务栈(创建任务时指定栈深度,溢出会破坏 TCB 或别的任务),而 FreeRTOS 动态创建任务时 TCB 和任务栈本身又从 RTOS heap 里分配,所以堆用尽时表现为「任务创建失败」而不是 malloc 失败;此外栈一般是向下增长、堆向上增长,两者相向而行,中间的剩余空间就是系统真正的余量,可以通过栈填充魔数(如 0xA5)测量高水位来评估。
volatile:告诉编译器「这个值可能在程序之外被改变」
语义上它只保证每次访问都真的去内存读/写,不做寄存器缓存、不合并、不删除,不保证原子性、不保证顺序、更不等于线程安全。三个典型场景:① 中断与主流程共享的标志位,例如 volatile uint8_t flag 在中断里置 1、主循环 while (!flag);,不加 volatile 编译器可能把 flag 优化进寄存器变成死循环;② 硬件寄存器,如 volatile uint32_t *reg = (uint32_t *)0x40000000;,寄存器值可能被硬件自己改变,且连续两次写同一地址不能被合并;③ DMA 等硬件异步改写的数据,如 DMA 完成标志。
面试常追问的边界:多字节共享数据(uint32_t 计数器、结构体)只加 volatile 仍会有读改写竞态,需要关中断或用原子操作;volatile 也不提供内存屏障语义,在 Cortex-M 上若涉及外设与内存的可见性顺序,要配合 __DMB()/__DSB();DMA 缓冲区还要考虑 cache 一致性(clean/invalidate 或把缓冲区放到非 cache 区)。
STM32 启动:MSP 来自向量表第一项
Cortex-M 上电/复位后硬件固定地从地址 0x00000000(由 BOOT 引脚映射到 Flash、系统存储器或 SRAM)取两个 32 位字:第一个是初始 MSP(主栈指针),第二个是 Reset_Handler 的入口地址,随后跳转执行。所以栈地址不是代码里 SP = xxx 设出来的,而是由启动文件在向量表头部放进去的:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
__initial_sp 是栈顶(Cortex-M 栈向下增长,所以它是这段空间的高地址端),把它放在向量表第一项,链接脚本保证 __initial_sp 紧跟 RAM 顶部或按要求对齐。Reset_Handler 里接着做 SystemInit(时钟、向量表重定位)、把 .data 从 Flash 拷到 RAM、把 .bss 清零,最后 bl main。补充:RTOS 启动后主栈(MSP)通常只用于异常处理和内核,任务各自用 PSP,切换时由 PendSV 保存/恢复。
int *p / int **p / int (*p)[10]
int *p 是指向 int 的指针;int **p 是指向「int 指针」的指针(二级指针,常见于函数内修改调用方的指针、或指针数组的数组名);int (*p)[10] 是数组指针——p 指向一个含 10 个 int 的数组,(*p)[3] 取该数组第 4 个元素,p + 1 会跳过 40 字节。对比易混的 int *p[10],因为没有括号,它是指针数组:10 个元素的数组,每个元素是 int *。记忆口诀是先看括号和结合性——(*p) 先结合说明 p 是「指向数组的指针」,*p[10] 说明 p 先和 [] 结合成数组、元素类型才是 int *。