ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CPU不认识main:STM32F411从复位到main启动解析

CPU不认识main:STM32F411从复位到main启动解析 1. 先把这句话翻译成人话CPU 到底认不认识 main()CPU不认识main()这句话第一次听像是在抬杠但它其实是理解整个嵌入式启动过程的一把钥匙。我在 WeAct STM32F411 这块小板子上来回折腾过很多次上电流程从一开始烧进去就能跑的懵懂到后来能徒手写向量表、写链接脚本、把main()从工程里删掉照样点灯中间踩的坑基本都绕不开这个认知。这篇就把从按下复位到执行 main 第一行代码之间发生的事完整拆开讲适合刚上手 STM32 或者一直在用 CubeMX 生成工程、但说不清工程里那堆启动文件到底在干嘛的人。看完你应该能自己回答为什么我的板子上电不跑、为什么 J-Link 点一下 Run 就能跑、为什么main前面还有一大堆看不懂的汇编。1.1 CPU 眼里只有地址、数据和时序把一个 Cortex-M4 内核想象成一个极其死板的门卫它不认识函数名不认识变量名不认识printf甚至不认识程序这个概念。它认的只有三件事——从哪个地址取指令、从哪个地址读写数据、什么时候采样。地址总线给它一个数它就把那个地址上的内容拿回来当作指令或者数据时钟沿来了它就往前走一步。至于这段内容是人写的main()还是编译器顺手塞进去的一条bx lr它根本不关心也关心不了。main()这个名字对 CPU 来说没有任何特殊含义。你在 C 文件里写下int main(void)编译器在生成符号表的时候会记一笔这里有个叫 main 的符号入口地址是 0x08001234链接器会把这条记录塞进可执行文件。整个过程全是软件层面的约定编译器负责生成链接器负责摆放启动代码负责在某个时刻跳过去。硬件从头到尾没参与过任何一步决策。这就是为什么你可以把main改名成app_entry只要启动文件里对应的那句bl main改成bl app_entry程序照样跑。1.2 main() 是编译器和链接器之间的一个约定不是硬件概念再往深一层说main这个名字之所以重要是因为C 语言标准规定了它必须是程序入口。这条规定约束的是编译器、链接器和 C 运行时库不是芯片。你换一门语言比如用 Rust 写裸机入口是#[entry]标注的函数用汇编写入口就是你自己的标签。芯片厂商给的启动文件之所以跳main是因为绝大多数人用 C工具链默认这样接。这里有个很容易被忽略的细节所谓跳转到 main跳过去的其实是地址。启动文件里的bl main在汇编后变成一条相对跳转指令偏移量是链接阶段算出来的。所以如果链接脚本把 Flash 区域改错了、或者你用了-flto之类的优化把符号搞没了跳转就会飞到未知地址直接 HardFault。这类问题在实际项目里并不少见尤其是自己改链接脚本的时候。1.3 为什么这个话题值得从 WeAct STM32F411 讲起选 STM32F411 来讲是因为它足够典型又足够简单。Cortex-M4F 内核、512KB Flash、128KB SRAM、板载一路 HSE 晶振没有外部 SDRAM 要初始化没有复杂的多级 Boot ROM也没有 Linux 那种几十毫秒的引导链。WeAct 这块核心板的资料又非常透明原理图、例程、引脚定义都摆在那里非常适合拿来当解剖对象。上电到main()之间的距离在这块板子上是几百个时钟周期的事但要把这几百个周期讲透涉及的知识面一点不少复位电路、BOOT 引脚、存储器映射、向量表、链接脚本、C 运行时初始化、时钟树、Flash 等待周期——一个都跑不掉。反过来说这些知识一旦在 F411 上建立起来往 F103、F407、H7 甚至 GD32、AT32 上迁移都只是换几个参数的事。真正内核层面的机制Cortex-M 系列是通用的。2. 上电那一刻STM32F411 到底做了什么按下复位键或者刚插上 USB 供电的那一瞬间芯片内部发生的事比大多数人想象的要笨得多。它不会去 Flash 里找main也不会加载文件系统它做的第一件事是从两个固定地址读两个字。理解这两个字就理解了整个启动链的起点。2.1 电源、复位与 BOOT 引脚的三角关系先看硬件层面。STM32F411 上电后内部的 POR/PDR上电复位/掉电复位电路会监控 VDD 电压只有电压爬升到阈值以上并稳定复位才会释放。这里第一个坑就来了电源爬升斜率太慢或者带载太重导致电压塌陷POR 就会反复触发或者干脆不释放表现就是板子上电没反应。用 USB 口供电的开发板一般问题不大但如果你的板上挂了大电容、或者用了输出能力很弱的 LDO就要留意这一条。复位释放之后芯片会去采样 BOOT 引脚决定从哪里启动。STM32F411 的规则大致是BOOT0nBOOT1选项位/ PB2启动区域典型用途0任意主 Flash0x08000000正常运行用户程序10系统存储器出厂 Bootloader串口 ISP 下载11内嵌 SRAM调试或特殊场景WeAct F411 板子上 BOOT0 通过电阻下拉到地同时接了一个按键按下时拉高。正常情况下你不需要动它。但如果你从别的板子上抄了个电路、BOOT0 悬空那芯片每一次上电都可能因为引脚电平漂移随机进入系统 Bootloader现象就是程序烧进去了但就是不跑用 J-Link 点 Run 反而能跑——因为调试器直接改 PC压根不管 BOOT 引脚。注意BOOT0 悬空是新手板最容易犯的错误之一。哪怕芯片内部有弱下拉也要老老实实加一个 10k 到地的硬下拉别赌芯片。2.2 从 0x00000000 取出的第一个字栈顶地址复位释放的一刻Cortex-M4 内核做的第一件事是读地址0x00000000处的 32 位数据把它装进主栈指针 MSP。第二件事是读0x00000004处的 32 位数据把它装进程序计数器 PC然后开始执行。这里有个容易绕晕的点0x00000000在 STM32F4 上并不是真的物理地址 0而是一个别名区。芯片内部有个 Remap 机制BOOT 配置决定了哪块物理存储器被映射到0x00000000开始的位置。当 BOOT00 时映射过来的是主 Flash也就是物理地址0x08000000开始的内容。所以你写在链接脚本里放在 Flash 最开头的那两个word就是内核上电后拿到的 SP 和 PC。这也解释了为什么向量表必须放在 Flash 的起始位置。你要是把向量表挪到中间前两个字就变成别的指令了内核会把那条指令当作栈顶地址装进 SP然后一切就全乱了——通常是 HardFault 或者直接锁死调试器连上就停在某个莫名其妙的地方。提示调试器连不上、只能看到 PC 停在一个乱地址第一反应就去看向量表位置和链接脚本而不是怀疑芯片坏了。2.3 Reset_Handler真正意义上的第一条指令0x00000004处放的那个字指向的是向量表里的第二项——复位向量。它写入 PC 后内核就跳过去执行这就是Reset_Handler你人生中在 STM32 上执行的第一条用户代码。CubeMX 生成的startup_stm32f411xe.s里Reset_Handler 大致长这样Reset_Handler: ldr sp, _estack /* 再设一遍栈顶保险 */ bl SystemInit /* 系统时钟、向量表偏移等 */ bl __libc_init_array /* C 运行时初始化 */ bl main /* 终于到 main 了 */ bx lr注意在bl main之前还有一堆事。很多人以为 Reset_Handler 直接就跳 main其实中间夹着SystemInit和__libc_init_array这两个函数一旦出问题main根本没机会执行。实际项目里我在 Reset_Handler 和 main 之间插过 GPIO 翻转做时间测量你会发现从复位到进 main快的几百微秒慢的几毫秒差别就在于SystemInit里等晶振起振花了多久。2.4 时钟还没配好代码却在跑——HSI 16MHz 的意义这里有个反直觉的事实上面这些代码执行的时候芯片用的不是 HSE 晶振而是内部 HSI。STM32F411 复位后默认时钟源是 HSI频率 16MHz。因为 HSE 需要外部晶振起振起振时间通常是几百微秒到几毫秒芯片总不能干等着不干活所以先拿内部 RC oscillator 顶着跑。这就是为什么SystemInit里配 PLL、等 HSERDY、切 SYSCLK 这一整套流程是必要的也是为什么要配 Flash 等待周期。HSI 16MHz 下Flash 不需要任何等待周期一旦切到 96MHzFlash 控制器必须插入等待周期否则读 Flash 的时序跟不上内核速度取回来的指令就是错的。这个衔接点如果处理不当表现非常典型程序能跑但结果诡异或者切换时钟那一瞬间就 HardFault。Flash 等待周期和主频的对应关系VDD 在 2.7V 到 3.6V 之间大致是这样HCLK 范围等待周期LATENCY0 ~ 30 MHz0 WS30 ~ 60 MHz1 WS60 ~ 90 MHz2 WS90 ~ 100 MHz3 WS100 ~ 120 MHz4 WS96MHz 落在 90~100 的区间里所以必须配 3 个等待周期同时把指令缓存、数据缓存、预取都打开才不至于把性能全丢在等 Flash 上。3. 启动文件与链接脚本把 main() 供上神坛的两份合同向量表、栈顶、堆栈边界这些信息不可能靠编译器自己猜出来必须由链接脚本和启动文件这两份合同约定好。链接脚本决定各个段放在哪启动文件决定上电后按什么顺序干活。这两份东西看懂了CubeMX 生成的工程对你来说就不再是黑盒。3.1 中断向量表为什么必须放在 Flash 起始中断向量表本质就是一个 32 位数组第 0 项是栈顶地址第 1 项是复位处理函数地址之后依次是 NMI、HardFault、MemManage、BusFault、UsageFault再往后是外部中断。每一项存的是函数入口地址而且最低位必须是 1因为这个位表示 Thumb 状态忘了置 1 或者被链接器抹掉跳过去立刻 HardFault。在 GCC 工具链里这个数组一般这样声明__attribute__((section(.isr_vector))) void (* const g_pfnVectors[])(void) { (void (*)(void))(_estack), Reset_Handler, NMI_Handler, HardFault_Handler, /* ... */ };然后链接脚本里把.isr_vector段固定在 Flash 最开头.isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) . ALIGN(4); } FLASHKEEP这个关键字很关键。没有它链接器做垃圾回收的时候可能判定这个段没人引用直接扔掉然后你的程序就再也起不来了。我在一次精简工程的时候删了KEEP烧进去板子一点反应都没有查了半天才想起来这一条。3.2 链接脚本里的 Flash/RAM 分区STM32F411CEU6 的存储器布局是固定的Flash 从0x08000000开始共 512KBSRAM 从0x20000000开始共 128KB。链接脚本里的 MEMORY 块就是把这些数字写死MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 512K RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K }这里有两个数值必须绝对准确ORIGIN 错了程序烧到不该烧的地方LENGTH 错了链接器可能在 Flash 里摆超出容量的东西。后者更隐蔽因为链接器不会报错只是在你烧录的时候工具会提示地址越界或者干脆默默丢掉一部分。我见过有人把 F411 的脚本套到 F401 上F401 只有 256KB Flash结果程序跑到一半突然乱飞原因就是代码段超了实际容量。注意换芯片型号的时候链接脚本和启动文件一定要成对替换。只改工程里的芯片型号、不改这两个文件是最典型的能编译通过但跑不起来的坑。3.3 __libc_init_array 与 .data/.bss现在来看__libc_init_array到底做了什么。在进 main 之前必须保证 C 语言的运行环境是完整的。具体来说有三件事第一已初始化的全局变量要从 Flash 拷到 RAM。这些变量属于.data段。你写int counter 5;的时候初值 5 存在 Flash 里变量本体在 RAM 里上电后必须把 5 从 Flash 搬过来。链接脚本里通过_sidata、_sdata、_edata三个符号标记搬运的源和目标。第二未初始化的全局变量要清零。这些变量属于.bss段C 标准要求它们初值为 0。链接脚本用_sbss和_ebss标记范围启动代码里一个循环清掉。第三调用.init_array里的函数指针。这一条最容易被忽略但它就是__libc_init_array名字的来源。C 的全局对象构造函数、__attribute__((constructor))标注的函数都放在这个数组里必须在这里被逐个调用否则你的 C 全局对象永远处于未构造状态。顺序非常重要先搬.data、再清.bss、最后调__libc_init_array。要是顺序反了构造函数里访问的全局变量就是垃圾值。这个顺序在 CubeMX 生成的启动文件里是正确的但如果你自己手写启动代码超大概率会搞错。另外还有一个常见的误解不少人以为__libc_init_array会顺便初始化栈指针或者配置时钟。它不会。栈指针是在向量表第一项里设的时钟是SystemInit干的。各司其职。3.4 堆栈_estack、_Min_Stack_Size_estack这个符号在链接脚本里通常定义成 RAM 的末尾地址_estack ORIGIN(RAM) LENGTH(RAM);Cortex-M 的栈是向下生长的所以栈顶取最高地址0x20020000往里压数据时地址递减。这样安排的好处是整个 RAM 从底部开始放.data、.bss、堆从顶部开始放栈两边相向生长空间利用率最高。链接脚本里还会有一段专门的栈空间检查逻辑._user_heap_stack : { . ALIGN(8); PROVIDE ( end . ); PROVIDE ( _end . ); . . _Min_Heap_Size; . . _Min_Stack_Size; . ALIGN(8); } RAM_Min_Stack_Size的作用是在链接阶段占位如果 RAM 放不下这么多栈空间链接器直接报错而不是等到运行的时候栈溢出把数据踩烂。这个机制很多人不知道我建议把它设大一点比如 0x800 起步跑 RTOS 或者用深递归的时候再往上加。真等到运行期栈溢出现象千奇百怪排查成本极高。4. 手写一段不经过 main() 的裸机代码讲了这么多原理最好的验证方式就是把main从整个链路里删掉看 CPU 是不是照样让灯亮起来。下面这套代码我在 WeAct F411 上实测过工具链是arm-none-eabi-gcc完全不用 CubeMX也不用 HAL 库。4.1 目标与思路目标很简单上电后让板载的 PC13 LED 以肉眼可见的频率闪烁整个过程不出现任何叫 main 的符号。思路是自己写一个.isr_vector段第 0 项放栈顶第 1 项放复位处理函数复位处理函数里直接配时钟、配 GPIO、进死循环翻转电平写一份对应的链接脚本指定 Flash 和 RAM 布局编译、烧录、断电重上电验证。这样一来CPU 不认识 main就从一句口号变成了可运行的事实。4.2 向量表与 Reset_Handler 汇编先看汇编部分文件叫start.s.syntax unified .cpu cortex-m4 .fpu softvfp .thumb .section .isr_vector,a,%progbits .global g_pfnVectors g_pfnVectors: .word _estack /* 第 0 项栈顶地址 */ .word Reset_Handler /* 第 1 项复位入口 */ .word Default_Handler /* NMI */ .word Default_Handler /* HardFault */ .word Default_Handler /* MemManage */ .word Default_Handler /* BusFault */ .word Default_Handler /* UsageFault */ .word 0 .word 0 .word 0 .word 0 .word Default_Handler /* SVCall */ .word Default_Handler /* DebugMonitor */ .word 0 .word Default_Handler /* PendSV */ .word Default_Handler /* SysTick */ .section .text.Reset_Handler .global Reset_Handler .type Reset_Handler, %function Reset_Handler: bl clock_init /* 配到 96MHz */ bl gpio_init /* PC13 输出 */ blink_loop: bl led_toggle bl delay_ms b blink_loop .size Reset_Handler, .-Reset_Handler Default_Handler: b Default_Handler注意这里压根没有bl main。复位向量直接指到Reset_Handler进去之后就是配时钟、点灯、死循环。整份代码里搜不到main这四个字母。提示向量表数组里那些留给异常的空位直接填 0 是可以的因为 Cortex-M 在没有开对应异常的情况下不会去读它们。但 HardFault 那一项一定要填个死循环否则一旦出错PC 会跳到 0 地址现象更难查。4.3 时钟配置25MHz HSE 到 96MHz 的 PLL 计算过程接下来是时钟。WeAct F411 板上贴的是 25MHz 无源晶振要配到 96MHz 的 SYSCLK。为什么不用常见的 100MHz因为 F411 的 USB 模块需要精确的 48MHz 时钟而 96MHz 这个方案能同时满足系统时钟和 USB 时钟。计算过程如下参数取值计算说明HSE25 MHz板载无源晶振PLLM25VCO 输入 25 / 25 1 MHz落在 1~2MHz 推荐区间PLLN384VCO 输出 1 × 384 384 MHz落在 100~432MHz 区间PLLP4SYSCLK 384 / 4 96 MHzPLLQ8384 / 8 48 MHz正好喂给 USBAHB 预分频1HCLK 96 MHzAPB1 预分频2PCLK1 48 MHz不超过 50MHz 上限APB2 预分频1PCLK2 96 MHz不超过 100MHz 上限Flash 等待周期3 WS96MHz 落在 90~100MHz 区间对应的寄存器操作大致是#define RCC_BASE 0x40023800UL #define RCC_CR (*(volatile uint32_t *)(RCC_BASE 0x00)) #define RCC_PLLCFGR (*(volatile uint32_t *)(RCC_BASE 0x04)) #define RCC_CFGR (*(volatile uint32_t *)(RCC_BASE 0x08)) #define FLASH_ACR (*(volatile uint32_t *)0x40023C00UL) void clock_init(void) { RCC_CR | (1U 16); /* HSEON */ while (!(RCC_CR (1U 17))); /* 等 HSERDY */ FLASH_ACR (3U 0) | (1U 8) | (1U 9) | (1U 10); /* 3WS 缓存全开 */ RCC_PLLCFGR 25U /* PLLM */ | (384U 6) /* PLLN */ | (1U 16)/* PLLP 4编码是 01 */ | (1U 22)/* PLLSRC HSE */ | (8U 24);/* PLLQ */ RCC_CFGR (0U 4) /* HPRE 不分频 */ | (5U 10) /* PPRE1 除以 2 */ | (0U 13); /* PPRE2 不分频 */ RCC_CR | (1U 24); /* PLLON */ while (!(RCC_CR (1U 25))); /* 等 PLLRDY */ RCC_CFGR | (2U 0); /* SW PLL */ while (((RCC_CFGR 2) 0x3U) ! 2U); /* 等 SWS 确认 */ }这段代码里有几个细节值得拎出来说。第一先开 HSE 再等 HSERDY等待循环里最好加超时否则晶振硬件有问题的时候代码就卡死在这里看起来像芯片挂了。第二Flash 等待周期必须在切到高频之前设好顺序反了会直接取错指令。第三切 SW 之后一定要轮询 SWS 确认直接往下跑不确认的话后面的寄存器配置可能还在用旧时钟节奏。你手上的板子如果是 8MHz 晶振的版本整套参数要重算PLLM 8PLLN 384PLLP 4PLLQ 8结果同样是 96MHz。所以关键是先确认晶振频率再套公式别抄参数。判断方法也简单拿示波器看或者看原理图上标的型号。4.4 点灯与验证GPIO 部分更简单。要点亮 PC13需要开 GPIOC 时钟、把 PC13 配成推挽输出#define RCC_AHB1ENR (*(volatile uint32_t *)(RCC_BASE 0x30)) #define GPIOC_MODER (*(volatile uint32_t *)0x40020800UL) #define GPIOC_ODR (*(volatile uint32_t *)0x40020814UL) void gpio_init(void) { RCC_AHB1ENR | (1U 2); /* GPIOC 时钟 */ GPIOC_MODER ~(3U 26); /* 清 PC13 模式位 */ GPIOC_MODER | (1U 26); /* 通用推挽输出 */ } void led_toggle(void) { GPIOC_ODR ^ (1U 13); /* PC13 翻转 */ }WeAct 板载 LED 是接在 PC13 上的而且是低电平点亮。所以ODR写 0 的时候灯亮写 1 的时候灯灭翻转的效果就是一亮一灭。之前有人跟我争论我这块板子是高电平点亮最后发现是他把 LED 焊反了所以别急着改代码先用万用表量一下。验证的方式很简单烧录之后拔掉调试器只留 USB 供电重新上电。灯按预期闪烁说明从复位到点灯的整条链路是通的而且跟main一点关系都没有。这一步做完你对CPU 不认识 main这句话的理解就不再是纸面上的了。5. 上电不跑、J-Link 点 Run 才跑排查思路实录这个现象在嵌入式新手群里出现的频率极高几乎每周都有人问。它有意思的地方在于它把启动链路和运行链路的差异暴露得非常清楚。调试器点 Run 的时候它跳过了复位后的很多环节直接改 PC、直接改 SP所以能跑起来不代表你的板子启动链路是通的。5.1 先分清复位后跑和调试器拉起来得先把两件事分开看。调试器连接后点 Run 的动作大致包含通过 SWD 停住内核、把 PC 和 SP 设成你要的位置、再放开运行。也就是说BOOT 引脚、复位电路、上电斜率、甚至向量表前半部分的某些环节它都绕过去了。而真正的上电运行必须完整走一遍POR 释放、BOOT 采样、读向量表、跳 Reset_Handler、配时钟、初始化 C 运行时、进应用。所以这个现象的含义是你的代码本身大概率没问题出问题的是启动前置条件。排查方向立刻就明确了不用去翻应用逻辑。5.2 高频原因速查表我把这些年遇到过的原因整理成一张表按出现频率从高到低排现象可能原因快速定位方法上电完全无反应接调试器能跑BOOT0 电平异常进了系统 Bootloader万用表量 BOOT0 常态电平应为低断电重上电才不跑热复位正常NRST 上拉电阻过大或复位电容过大量复位脚电压爬升时间偶尔能跑偶尔不跑HSE 起振时间不足代码等超时就往下走检查 HSERDY 超时处理逻辑一运行就 HardFault链接脚本与芯片容量不匹配或向量表没放对位置看 HardFault 时压栈的 PC 值程序跑几毫秒后停住独立看门狗在选项字节里被硬件使能用工具读选项字节看 IWDG_SW 位用手碰一下才跑松手就停晶振负载电容不匹配或虚焊换不同容值电容实测或换晶振内核电压异常跑飞VCAP1/VCAP2 外接电容缺失或焊接不良查原理图F411 需要 2.2µF 电容注意独立看门狗一旦在选项字节里被设为硬件使能上电后软件无法关闭只能通过烧录器改选项字节。这是上电跑一下就死、调试器点 Run 反而正常的经典原因之一因为调试器停住内核时看门狗计数也会暂停。5.3 HSE 不起振与 Flash 等待周期这两个问题经常一起出现因为都发生在切时钟的那一瞬间。HSE 不起振的典型表现是程序卡在等待 HSERDY 的循环里。如果你写了超时处理并且超时后改用 HSI 继续跑那程序是能跑的但主频只有 16MHz串口波特率、延时函数全部不对。这时候现象就很迷惑灯会闪但频率不对串口能发但全是乱码。排查方法是拿示波器直接看晶振引脚起振正常的话能看到干净的 25MHz 正弦波。Flash 等待周期配错的典型表现是程序能跑但结果随机错误或者干脆进去就 HardFault。因为 CPU 跑得比 Flash 快取回来的指令错位。这个错误在 16MHz 下不会暴露只在 96MHz 下暴露所以如果你从默认时钟改到高频必须同步改FLASH_ACR。顺序是先设等待周期再切时钟源。还有一条经验开启预取和指令、数据缓存。这三个位在 96MHz 下能显著提升性能而且开启缓存后即使等待周期设得保守一点实际取指效率也不会差太多。我一般直接全开。5.4 工程化建议上面这些坑靠事后排查效率很低更好的做法是在工程里加自检。我在自己的模板里加了三样东西一是启动打点。在 Reset_Handler 之后、SystemInit 之前和之后、进 main 之后分别翻转一个空闲 GPIO。拿示波器或者逻辑分析仪抓一下启动到哪一步一眼就看出来了比用调试器单步快十倍。二是时钟自检。进 main 之后读一下RCC_CFGR的 SWS 位确认 SYSCLK 确实切到了 PLL而不是意外停在 HSI。这个检查只花几行代码但能省掉很多波特率为什么不对的困惑。三是栈哨兵。在栈顶附近填一个魔数运行一段时间后检查有没有被改写。提前发现栈溢出比等到堆栈踩烂中断向量表要好得多。这些手段都不复杂但需要你对启动流程有清晰认知才能想到放在哪。这就是为什么我觉得从复位到 main这一段值得每个人都自己走一遍。6. 同一个道理在更大尺度上的体现CPU 不认识 main这件事不只是 STM32 的冷知识它其实是所有计算平台共通的规律。把视野放大一点看你会发现同一套逻辑在不同层次反复出现。6.1 IAP 与 Bootloader 里的 VTOR做产品升级功能的时候通常会把 Flash 划成两半前面放 Bootloader后面放应用。Bootloader 负责接收新固件、校验、写入然后跳到应用。这个跳转过程里就有个非常典型的坑。应用固件的向量表默认放在0x08000000但现在它被烧到了0x08004000。Bootloader 跳过去的时候得先把SCB-VTOR改成0x08004000否则中断向量还是指向 Bootloader 的向量表中断一响应就飞到 Bootloader 的处理函数里去了。这个错误的现象是主循环正常跑一开中断就乱。跳转代码大致是这样typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_addr) { uint32_t sp *(volatile uint32_t *)app_addr; uint32_t pc *(volatile uint32_t *)(app_addr 4); SCB-VTOR app_addr; __set_MSP(sp); ((app_entry_t)pc)(); }注意这里读的还是应用固件的前两个字栈顶和复位入口。跟芯片上电时的逻辑一模一样只是执行的人从硬件换成了软件。所以你要是在应用固件里把向量表挪走、或者链接脚本没把向量表放在最前面Bootloader 就跳不进去。这又一次说明main后面那套约定只在工具链内部成立换一层执行者你就得自己动手把栈顶和入口读出来。6.2 从 RTOS 到 Linux谁在替 CPU 找到 main再往上走一层。跑 FreeRTOS 的时候main里做的是初始化硬件、创建任务、启动调度器。此后 CPU 执行的是任务函数main这个符号基本就消失了只剩栈空间还占着。调度器切换任务时改的是 PSP跟main无关。到了 Linux 这种带内核的系统链路更长上电后先跑固件里的引导代码把内核镜像搬到内存跳进内核入口内核再初始化内存管理、中断控制器、调度器最后才启动第一个用户态进程。整条链路上没有任何一步是CPU 认识某个函数名全是「从某个地址取数、跳到某个地址」。区别在于裸机上的地址是链接器写死的Linux 上的地址是运行期地址重定位算出来的。但本质一样CPU 只认地址。所谓程序入口是软件约定加硬件寄存器共同营造的错觉。理解这一点很多看起来玄乎的问题就变得朴素了。为什么换块板子程序跑不起来因为地址布局变了。为什么加了个 bootloader 中断就乱因为向量表基址变了。为什么量产时偶发启动失败因为触发条件藏在复位时序或者上电斜率里。这些都是同一个道理在不同层面上的投影。我个人的习惯是拿到一块新板子第一件事不是写业务代码而是把启动链路走一遍量 BOOT 引脚、看复位波形、确认晶振频率、读一下链接脚本的地址区间。这几步做完后面写代码就很少会遇到莫名其妙的问题了。真遇到的话也知道该往哪个方向查而不是把代码翻来覆去改。
返回列表