APP代码移植(2)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

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。

本课的操作顺序是:

  1. 在 Keil 中修改 APP 的 IROM1 起始地址和大小;
  2. 增加条件编译宏 USER_VECT_TAB_ADDRESS;
  3. 在 system_stm32f1xx.c 中把 SCB->VTOR 改为 APP 起始地址;
  4. 增加链接后的 fromelf 命令,生成 APP.bin;
  5. 重新烧写并测试完整升级流程。

1.2 删除oled字体数组的code

从 51 单片机工程移植 OLED 字库时,源文件中的数组声明带有 code。这里的 code 是 Keil C51 的存储空间扩展关键字,用来把对象放进 8051 的代码存储空间;它不是标准 C 关键字,也不是 ARM Compiler 的通用关键字,所以移到 STM32 工程后必须删除。

配套源码中的实际变化可概括为:

字库对象51 工程STM32 工程含义
F6x8const unsigned char code ...const unsigned char ...删除 C51 专用关键字,保留只读属性
F8X16const unsigned char code ...const unsigned char ...删除 C51 专用关键字,保留只读属性
Hzkunsigned 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 设置为:

字段原值新值
Start0x080000000x08008000
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.exeArm/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 HEXBIN
文件形态带记录结构的 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 中执行整片擦除,再按下表烧写三个文件:

顺序文件文件类型起始地址说明
1bootloader.hexHEX文件记录为 0x08000000烧录器从 HEX 中读取地址,界面中的地址不可手改属正常现象
2info.binBIN手工填写 0x08006000写入 12 字节升级参数;原笔记的 0x60000少了 Flash 基址
3APP1.binBIN手工填写 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

验证步骤如下:

  1. 复位后先看到 APP1 的启动信息;
  2. 经 USART1 发送升级控制帧,APP 把 Flash 参数区中的升级标志置 1,然后执行系统复位;
  3. Bootloader 再次启动,读取到升级标志后不跳旧 APP,而是在 USART2 上等待 YMODEM;
  4. 在 Xshell 中选择 YMODEM,发送 10Can_DHT11_OLED_IAP/MDK-ARM/APP.bin;
  5. Bootloader 从 0x08008000开始写入收到的数据,成功后清除升级标志并运行新 APP;
  6. OLED 显示 APP Can Test,串口打印新的启动信息,证明第 10 个工程已经运行;
  7. 再发一次升级控制帧,可以重新进入 Bootloader,并在 APP1、APP2 和新 APP 之间反复升级验证。

视频中重新连接 Xshell 是因为 USART2 接触不良,并不是 YMODEM 每次传输都必须执行的协议步骤。新 APP 运行后循环打印的 recv eorro是课程 CAN 接收代码中的原始拼写,实际表示暂时没有收到 CAN 数据;它不表示 YMODEM 固件传输失败。

视频口述在最后曾把约 14 KiB 的文件说成 HEX,随后马上更正为 BIN;本课刚生成并经 YMODEM 发送的是 APP.bin。

二、三个地址为什么必须一致

本课最容易混淆的是“链接、写入、跳转、向量表”四个动作。它们分别由不同模块负责:

动作负责者本项目地址
给函数和向量表分配绝对地址Keil 链接器的 IROM1/分散加载文件0x08008000
把 BIN 字节写入内部 FlashBootloader 的 Flash 写入逻辑0x08008000
读取初始栈并跳到复位函数Bootloader 的 APP 跳转逻辑读取 0x08008000、0x08008004
运行后分派异常和中断Cortex-M3 的 SCB->VTOR0x08008000

典型错误及结果:

  • 只改写入地址,不改链接地址: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替换。