APP代码移植(2)
198.2.6 APP代码移植(2)
#if defined(USER_VECT_TAB_ADDRESS) /* 只有 Keil 工程定义该宏后,下面的向量表重定位代码才参与编译。 */
//SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* 课程源码注释掉 STM32 模板原有的赋值语句。 */
SCB->VTOR = 0x08008000; /* 把中断向量表基址改为本项目 APP 的实际起始地址。 */
#endif /* 结束 USER_VECT_TAB_ADDRESS 条件编译区域。 */
本课继续处理 10Can_DHT11_OLED_IAP,完成 APP 移植的最后两项:改变 APP 的链接地址和中断向量表地址,并在链接成功后生成供 Bootloader 接收的 APP.bin。最后重新烧写 Bootloader、参数文件和测试 APP,再用 YMODEM 传输刚生成的新 APP,验证整条升级链路。
这一步最重要的不是“把地址改成一个能编译的数”,而是让下面三个地址完全一致:
Keil 的 APP 链接起始地址 0x08008000
APP 运行后的中断向量表地址 0x08008000
Bootloader 写入和跳转的地址 0x08008000
任意一项不一致,都可能出现“传输成功但不能运行”、一开中断就异常或直接 HardFault。
一、任务代码移植
1.1 修改flash的运行地址和大小
先区分本课涉及的两类片上存储器:
| 存储器 | 掉电后是否保留 | 在本工程中的典型内容 |
|---|---|---|
| Flash | 保留 | Bootloader、参数页、APP 的机器指令、只读常量及向量表 |
| SRAM | 不保留 | 运行时栈、堆、可写全局变量、局部变量和接收缓冲区 |
视频把 Flash 类比成电脑硬盘、把 SRAM 类比成内存,有助于入门理解,但还要补充两点:
- MCU 上通常从 Flash 取指执行,并不需要先把全部程序复制进 SRAM;
- “变量都在 RAM、数组都在 Flash”也不准确,最终位置由变量属性和链接脚本共同决定。例如只读的
const数组通常进入 Flash,可写的已初始化全局数组运行时要占用 SRAM,其初值还会保存在固件映像中。
本项目使用的 STM32F103ZE 带 512 KiB Flash。它的用户 Flash 地址范围为 0x08000000~0x0807FFFF,总长度为:
0x00080000 = 524288 B = 512 KiB
Bootloader 工程的实际分散加载文件把自身区域设为 0x08000000 / 0x00006000,即前 24 KiB。参数文件放在 0x08006000;APP 从 0x08008000 开始,因此整个前 32 KiB 都不再属于 APP。APP 可使用的最大链接区域为:
0x00080000 - 0x00008000 = 0x00078000
0x00078000 = 491520 B = 480 KiB
对应布局如下:
0x08000000 ┌──────────────────────────────┐
│ Bootloader,最大 0x6000 字节 │
0x08006000 ├──────────────────────────────┤
│ 参数区及预留空间 │
0x08008000 ├──────────────────────────────┤ ← APP 链接、写入、向量表基址
│ APP,最大 0x78000 字节 │
0x0807FFFF └──────────────────────────────┘ ← 512 KiB Flash 最后一个字节
这里的 0x08080000 是 Flash 末端之后的第一个地址,不是一个仍可写入的 Flash 字节;最后一个有效地址是 0x0807FFFF。
本课的操作顺序是:
- 在 Keil 中修改 APP 的 IROM1 起始地址和大小;
- 增加条件编译宏
USER_VECT_TAB_ADDRESS; - 在
system_stm32f1xx.c中把SCB->VTOR改为 APP 起始地址; - 增加链接后的
fromelf命令,生成APP.bin; - 重新烧写并测试完整升级流程。
1.2 删除oled字体数组的code
从 51 单片机工程移植 OLED 字库时,源文件中的数组声明带有 code。这里的 code 是 Keil C51 的存储空间扩展关键字,用来把对象放进 8051 的代码存储空间;它不是标准 C 关键字,也不是 ARM Compiler 的通用关键字,所以移到 STM32 工程后必须删除。
配套源码中的实际变化可概括为:
| 字库对象 | 51 工程 | STM32 工程 | 含义 |
|---|---|---|---|
F6x8 | const unsigned char code ... | const unsigned char ... | 删除 C51 专用关键字,保留只读属性 |
F8X16 | const unsigned char code ... | const unsigned char ... | 删除 C51 专用关键字,保留只读属性 |
Hzk | unsigned char code ... | unsigned char ... | 删除 C51 专用关键字,但对象成为可写数组 |
因此,原笔记“STM32 直接存 RAM”的说法不严谨:
F6x8、F8X16仍有const,在当前链接配置下通常进入只读区并保存在 Flash;Hzk没有const,会按可写全局数据处理,运行时需要 SRAM;如果字库不需要动态修改,是否也应声明为const属于另一个代码优化问题,本课没有修改它;- 删除
code解决的是 C51 到 ARM 的语法和存储模型兼容问题,并不是把所有字库强制改放到 SRAM。
1.3 修改代码运行地址
1.3.1 修改APP运行地址
STM32 上电或复位后,硬件首先从 Flash 映射起点 0x08000000取得初始栈指针和复位入口,所以该位置仍由 Bootloader 占用。APP 不能继续按普通裸机工程那样链接到 0x08000000,而要整体后移到 0x08008000。
“修改运行地址”在本课实际包含两件事:
- 链接地址改变:链接器按
0x08008000安排 APP 的向量表、代码和只读数据; - 中断向量表改变:APP 启动后把 Cortex-M3 的
SCB->VTOR指向0x08008000,让中断入口从 APP 的向量表中查找。
配套 APP.bin的开头也能验证这一点:首个 32 位小端字为 0x20000900,是初始主栈指针;第二个字为 0x080081E1,是带 Thumb 状态位的 Reset_Handler入口。也就是说,BIN 偏移 0 就是 APP 向量表,Bootloader 必须把它写到 0x08008000。
原笔记写成 0x80000000和 0x80080000,都多了一个十六进制零。正确地址分别是:
普通工程/Bootloader 起点:0x08000000
本课 APP 起点: 0x08008000
两者相差: 0x00008000 = 32 KiB
课程源码中的 0x8008000U省略了最高位后的一个前导零,与 0x08008000U数值相同;为了阅读地址分区,通常写成 8 位十六进制数更清楚。
1.3.2 按照思维导图改
按照思维导图第 6、7 步,本课改的是新目标工程 10Can_DHT11_OLED_IAP,不是作为代码来源的 09Ymodem_APP,也不是最初的 04Can_DHT11_OLED。
可按下面的依赖关系检查:
Target/IROM1 改链接区域
↓
重新链接后,APP 向量表位于 0x08008000
↓
SystemInit 设置 VTOR = 0x08008000
↓
After Build 把已正确链接的 AXF 转为 APP.bin
↓
Bootloader 从 0x08008000 开始顺序写入并跳转
顺序本身不是唯一要求,真正的验收标准是最终工程中四处配置相互一致,而不是照着画面机械点击。
1.3.3 更改目标地址
在 Keil 中打开目标工程,点击 Options for Target(魔术棒),进入 Target页,将 IROM1 设置为:
| 字段 | 原值 | 新值 |
|---|---|---|
| Start | 0x08000000 | 0x08008000 |
| Size | 原普通工程容量 | 0x00078000 |
视频画面和保存后的工程文件均显示起始地址为 0x08008000。原笔记的“将 8000000 改为 80000”“倒数第四位增加 8”既不准确也不利于排错,应直接按完整地址核对,避免漏零。
保存后,Keil 生成的分散加载配置实际为:
LR_IROM1 0x08008000 0x00078000
ER_IROM1 0x08008000 0x00078000
LR_IROM1是装载区域,ER_IROM1是执行区域;本工程两者起点相同,表示 APP 代码从其在 Flash 中的链接位置直接执行。
1.3.4 修改size
Size不是随意填写的示例值,而是由芯片总 Flash 容量和 APP 起点计算出来:
芯片 Flash 总长度 0x80000
APP 前预留长度 - 0x08000
---------
APP IROM1 Size 0x78000
在 Keil 中输入 0x78000和0x00078000数值相同。点击 OK后应重新打开 Target 页面复核,或查看生成的 .sct文件;只修改起点而不缩小 Size,会使链接器误以为 APP 可以越过物理 Flash 末端。
1.3.5 添加define
打开 Options for Target → C/C++ → Preprocessor Symbols → Define,在原有宏后用英文逗号加入:
USE_HAL_DRIVER,STM32F103xE,USER_VECT_TAB_ADDRESS
其中前两个是工程已有定义,本课新增的是 USER_VECT_TAB_ADDRESS。它是一个预处理宏,不是普通 C 变量。编译器看到它后,system_stm32f1xx.c里的下面这段条件编译才会生效:
#if defined(USER_VECT_TAB_ADDRESS)
...设置 SCB->VTOR...
#endif
逗号必须使用英文半角字符;中文逗号不会被 Keil 当作多个宏的合法分隔符。添加后还要确认没有误删 USE_HAL_DRIVER和STM32F103xE。
1.3.6 修改system文件
真正修改的是 Core/Src/system_stm32f1xx.c,不是启动汇编文件。课程把 STM32 模板原来的赋值注释掉,再写入:
SCB->VTOR = 0x08008000; /* 让 APP 运行时从自己的向量表分派异常和中断。 */
SCB是 Cortex-M3 的系统控制块,VTOR是向量表偏移寄存器。APP 中的 USART、SysTick、CAN 等中断发生时,处理器会以 VTOR 指定的位置为向量表基址。若仍指向 0x08000000,取到的将是 Bootloader 的中断入口,而不是 APP 的入口。
标准 STM32 模板原来通过 VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET计算地址;本课源码中的 VECT_TAB_OFFSET仍为 0,所以课程选择直接写入 0x08008000。不论采用哪一种写法,最终写入 VTOR 的值都必须与 APP 链接起点一致。本笔记保留课程配套源码的直接赋值方式,没有擅自改成另一套实现。
1.3.6.1 找到startup文件
视频打开 startup_stm32f103xe.s,是为了从复位流程中的 SystemInit调用定位到其 C 语言定义,不是在汇编文件第 147 行修改向量表。实际调用链为:
硬件复位
→ 硬件从 0x08000000 读取 Bootloader 的向量表并进入 Bootloader
→ Bootloader 读取 0x08008000、0x08008004 处的 APP 初始栈和 Reset_Handler
→ Bootloader 设置主栈并跳到 APP Reset_Handler
→ APP 的 Reset_Handler
→ SystemInit()
→ 写 SCB->VTOR = 0x08008000
→ C/C++ 运行库初始化
→ main()
因此应在 Keil 中对 SystemInit使用“转到定义”,进入 system_stm32f1xx.c再修改。这样在 main()和各业务中断运行前,向量表已经完成切换。
编译后至少要核对:
- 没有地址区域重叠或超出 IROM1 的链接错误;
USER_VECT_TAB_ADDRESS确实出现在当前 target 的 Define 列表;- 修改的是第 10 个目标工程的
system_stm32f1xx.c; - VTOR 值、IROM1 Start 和 Bootloader 的
INTER_FLASH_APP_ADDR均为0x08008000。
1.4 添加编译后指令
Keil 默认生成的 AXF/ELF 文件包含代码、地址、符号及调试信息;课程还需要一个只有装载字节的 BIN 文件。打开:
Options for Target → User → After Build/Rebuild
勾选第一个 Run,并填入工程文件中实际保存的命令:
fromelf.exe --bin -o "APP.bin" "#L"
各部分含义如下:
| 参数 | 作用 |
|---|---|
fromelf.exe | Arm/Keil 提供的映像转换工具 |
--bin | 输出原始二进制映像 |
-o "APP.bin" | 把输出文件命名为 APP.bin |
"#L" | Keil 占位符,构建时展开为当前链接输出的 AXF 文件 |
本地工程构建记录中的实际展开结果为:
fromelf.exe --bin -o "APP.bin" "01Can_Test\01Can_Test.axf"
链接成功且 After Build 命令执行后,MDK-ARM目录会出现新的 APP.bin。配套工程中该文件大小为 14,364 字节;应同时查看修改时间,避免误把旧 BIN 当成刚生成的固件。fromelf --bin的用法也可在 Arm/Keil fromelf 用户指南中核对。
HEX 与 BIN 都能表达固件数据,但并非“本质完全一样”:
| 项目 | Intel HEX | BIN |
|---|---|---|
| 文件形态 | 带记录结构的 ASCII 文本 | 连续的原始字节 |
| 是否携带绝对地址 | 是 | 否 |
| 是否含记录校验和 | 每条记录包含校验和 | 文件格式本身没有 |
| 常见用途 | 调试器/烧录器按记录地址编程 | Bootloader 从约定地址连续写入 |
本工程 APP HEX 的开头含有扩展线性地址记录 :020000040800F2,后续数据记录从偏移 0x8000开始,两者组合后就是 0x08008000。而 APP.bin的第 0 字节没有携带“应写到哪里”的信息,烧录器或 Bootloader 必须另行知道起始地址。
所以准确结论是:
- 使用 ST-LINK 给芯片直接编程时,正确链接的 APP HEX 可以按其自带地址烧到
0x08008000; - 若烧录 BIN,必须手工指定
0x08008000; - 本课程的 YMODEM Bootloader 只把收到的字节从
INTER_FLASH_APP_ADDR开始顺序写入,不解析 Intel HEX 文本,因此 OTA 传输必须使用APP.bin; - “所有 OTA 都不能用 HEX”并非通用规律,若另一个 Bootloader 实现了 Intel HEX 解析器,它也可以接收 HEX,但这不是本课程的实现。
1.5 烧写Bootloader
视频为了建立已知初始状态,先在 ST-LINK Utility 中执行整片擦除,再按下表烧写三个文件:
| 顺序 | 文件 | 文件类型 | 起始地址 | 说明 |
|---|---|---|---|---|
| 1 | bootloader.hex | HEX | 文件记录为 0x08000000 | 烧录器从 HEX 中读取地址,界面中的地址不可手改属正常现象 |
| 2 | info.bin | BIN | 手工填写 0x08006000 | 写入 12 字节升级参数;原笔记的 0x60000少了 Flash 基址 |
| 3 | APP1.bin | BIN | 手工填写 0x08008000 | 先放入一个可启动的测试 APP |
注意:整片擦除会删除芯片上原有 Bootloader、参数和 APP,只应在确实需要重建测试环境时执行。正常 OTA 更新不需要每次用 ST-LINK 整片擦除。
BIN 打开后,烧录工具可能先给出默认地址 0x08000000;必须在点击 Start前改成对应区域。尤其不能把 APP1.bin写到 0x08000000,否则会覆盖 Bootloader。
烧写结束后按复位键,预置的 APP1 能运行,说明下面这条启动路径已经打通:
Bootloader 从 0x08000000 启动
→ 参数表明当前不升级
→ 检查 0x08008000 处 APP 的初始栈和复位向量
→ 设置栈并跳到 APP Reset_Handler
→ APP1 运行
1.6 发送并运行APP
完整实验使用两个串口角色:
| 通道 | 作用 |
|---|---|
| APP 的 USART1 | 接收 0xAF ... 0xFA升级控制帧,设置参数区升级标志并复位 |
| Bootloader 的 USART2 | 输出等待提示,并通过 YMODEM 接收真正的 APP.bin |
验证步骤如下:
- 复位后先看到 APP1 的启动信息;
- 经 USART1 发送升级控制帧,APP 把 Flash 参数区中的升级标志置 1,然后执行系统复位;
- Bootloader 再次启动,读取到升级标志后不跳旧 APP,而是在 USART2 上等待 YMODEM;
- 在 Xshell 中选择 YMODEM,发送
10Can_DHT11_OLED_IAP/MDK-ARM/APP.bin; - Bootloader 从
0x08008000开始写入收到的数据,成功后清除升级标志并运行新 APP; - OLED 显示
APP Can Test,串口打印新的启动信息,证明第 10 个工程已经运行; - 再发一次升级控制帧,可以重新进入 Bootloader,并在 APP1、APP2 和新 APP 之间反复升级验证。
视频中重新连接 Xshell 是因为 USART2 接触不良,并不是 YMODEM 每次传输都必须执行的协议步骤。新 APP 运行后循环打印的 recv eorro是课程 CAN 接收代码中的原始拼写,实际表示暂时没有收到 CAN 数据;它不表示 YMODEM 固件传输失败。
视频口述在最后曾把约 14 KiB 的文件说成 HEX,随后马上更正为 BIN;本课刚生成并经 YMODEM 发送的是 APP.bin。
二、三个地址为什么必须一致
本课最容易混淆的是“链接、写入、跳转、向量表”四个动作。它们分别由不同模块负责:
| 动作 | 负责者 | 本项目地址 |
|---|---|---|
| 给函数和向量表分配绝对地址 | Keil 链接器的 IROM1/分散加载文件 | 0x08008000 |
| 把 BIN 字节写入内部 Flash | Bootloader 的 Flash 写入逻辑 | 0x08008000 |
| 读取初始栈并跳到复位函数 | Bootloader 的 APP 跳转逻辑 | 读取 0x08008000、0x08008004 |
| 运行后分派异常和中断 | Cortex-M3 的 SCB->VTOR | 0x08008000 |
典型错误及结果:
- 只改写入地址,不改链接地址:BIN 内部的函数绝对地址仍按
0x08000000生成,搬到0x08008000后不能正确运行; - 只改链接地址,不改 Bootloader 写入地址:新固件覆盖错误区域,严重时会覆盖 Bootloader;
- 链接和写入正确,但 VTOR 未改:
main()可能暂时运行,一旦 SysTick、USART 或 CAN 中断到来就会取错中断入口; - IROM1 大小仍是完整 Flash 大小:小程序可能暂时看不出问题,但链接器没有替你守住芯片的物理末端。
Arm CMSIS 对 VTOR 的定义和向量表入口含义可参考 CMSIS-Core NVIC 文档。本课所用芯片的 Flash、SRAM 等容量可在 ST 的 STM32F103ZE 产品页和数据手册中核对。
三、本课完成检查表
- 修改的是
10Can_DHT11_OLED_IAP,不是参考工程。 - Keil IROM1 Start 为
0x08008000。 - Keil IROM1 Size 为
0x00078000。 - C/C++ Define 中存在
USER_VECT_TAB_ADDRESS,且原有宏未丢失。 - 修改位置是
system_stm32f1xx.c,不是startup_stm32f103xe.s。 - APP 运行时设置
SCB->VTOR = 0x08008000。 - After Build 已勾选并成功执行
fromelf.exe --bin -o "APP.bin" "#L"。 -
APP.bin的修改时间与本次构建一致;配套文件大小为 14,364 字节。 - ST-LINK 烧录
info.bin时地址为0x08006000。 - ST-LINK 烧录测试 APP BIN 时地址为
0x08008000。 - Xshell/YMODEM 发送的是新生成的 BIN,不是 HEX、AXF 或旧文件。
-
recv eorro仅代表 CAN 暂无接收数据,不误判为升级失败。
完成以上检查后,第 197~198 课的 APP 移植才算闭环:业务工程既能从 Bootloader 指定区域独立运行,也能接收升级命令、复位进入 Bootloader,并通过 YMODEM 被新的 APP.bin替换。