GD32VW553-IOT 基于 vscode 的 bootloader 移植(基于Cmake)
前言
主频时钟切换
-
当系统时钟频率从高主频直接切换到低主频或从低主频直接切换到高主频时, 如在Boot+APP架构中,或进出deepsleep需要重新配置时钟等,在这类场景中需要加上阶段式地升频或降频操作,先将频率阶段式地降下来,然后将系统时钟源切换到其他时钟源,如内部高速晶振,再去修改PLL,最后阶段式升到目标频率
-
系统级应用中,在 Boot 程序中,系统时钟的变化为先由内部高速晶振切换到 PLL 最高频率的变化过程,需要应用层注意添加阶段式切频代码,或者在 APP 工程中, 如对目标频率需求没有变化, 无需重新配置时钟。
-
应用层需要避免由高频直接切换到低频,假设当前时钟为 120MHz,调用下面三段式降频代码后,频率的变化为120MHz->60MHz->30MHz->15MHz,接下来再将系统时钟切换到内部高速晶振。
-
应用层需要避免直接由低频(如 IRC8M)直接切到最高频率, 如deepsleep 唤醒之后频率由内部高速晶振倍频到特定频率,系统频率的变化是先由内部高速晶振进行 8 分频,然后配置 PLL 并将时钟源切到 PLL,接下来开始逐级升频。
-
除了上述提供的宏可以参考以外,还可以调用 gd32f30x_rcu.c 中提供的 rcu_ahb_clock_config函数实现 AHB 时钟的变化,注意在每次 AHB 频率变化之后,需要加一点延时保证时钟稳定。要想实现频率的变化,须要先将时钟由 PLL 切到其他时钟源,然后修改 PLL,再将系统时钟源切回到 PLL。对于其他系列,均可以参考类似做法,避免在时钟频率切换时出现异常。
外设时钟切换
- 外设时钟切换需要考虑到外设的工作状态和时钟源的切换时机,避免在外设工作时进行时钟切换导致数据错误或丢失。
- 在进行外设时钟切换时,需要先将外设停止工作,待外设空闲后再进行时钟源的切换。
- 时钟源切换完成后,需要重新配置外设的时钟参数,确保外设能够在新的时钟环境下正常工作。
- 由于外设可以设置不同的时钟源,因此可以在不修改主频的情况下修改外设频率,如下是SPI解算频率的例子, 注意每一级频率都要处于一定的范围,条件要设定清楚。
// 以 MHz 为单位的 PLL2+SPI 求解结果
typedef struct {uint32_t input_mhz; // 4, 25, 32uint32_t pll2m; // 1..63uint32_t pll2n; // 4..512uint32_t pll2p; // 1..128uint32_t prescale; // {2,4,8,16,32,64,128,256}double target_mhz; // 目标 SPI 时钟 (MHz)double actual_mhz; // 计算得到的实际 SPI 时钟 (MHz)double error_mhz; // 绝对误差 (MHz)int exact; // 1=完全相等(按浮点阈值判断),0=近似int found; // 1=找到候选
} pll2_spi_cfg_t;// 求最优解(误差最小)。单位:MHz
// 返回 0=成功,-1=无解。tolerance_mhz>0 且最优误差仍大于公差时返回 -1。
int pll2_spi_solve_best(double target_spi_mhz, pll2_spi_cfg_t* out, double tolerance_mhz)
{if (!out) return -1;// 合法性:目标频率需是正数if (!(target_spi_mhz > 0.0)) return -1;const uint32_t inputs_mhz[] = { 4u, 25u, 32u };const uint32_t presc_list[] = { 2u, 4u, 8u, 16u, 32u, 64u, 128u, 256u };double best_err = DBL_MAX;pll2_spi_cfg_t best = {};int any = 0;for (size_t i = 0; i < sizeof(inputs_mhz)/sizeof(inputs_mhz[0]); ++i){const uint32_t INPUT_MHZ = inputs_mhz[i];for (uint32_t M = 1; M <= 63; ++M){// 1 MHz <= INPUT/M <= 16 MHzconst double fin_mhz = (double)INPUT_MHZ / (double)M;if (fin_mhz < 1.0 || fin_mhz > 16.0) continue;for (size_t pk = 0; pk < sizeof(presc_list)/sizeof(presc_list[0]); ++pk){const uint32_t presc = presc_list[pk];for (uint32_t P = 1; P <= 128; ++P){// 反推 N ≈ target * P * presc * M / INPUTconst double N_est = target_spi_mhz * (double)P * (double)presc * (double)M / (double)INPUT_MHZ;const uint32_t N = (uint32_t)llround(N_est);if (N < 4 || N > 512) continue;// VCO 约束:128 MHz <= (INPUT/M)*N <= 560 MHzconst double vco_mhz = fin_mhz * (double)N;if (vco_mhz < 128.0 || vco_mhz > 560.0) continue;// 实际 SPI 频率const double den = (double)P * (double)presc;const double actual = vco_mhz / den; // = (INPUT/M)*N/(P*presc)const double err = fabs(target_spi_mhz - actual);const bool better = (!any) ||(err < best_err) ||(fabs(err - best_err) < 1e-12 && actual > best.actual_mhz); // 误差相同取更高频if (better){any = 1;best_err = err;best.input_mhz = INPUT_MHZ;best.pll2m = M;best.pll2n = N;best.pll2p = P;best.prescale = presc;best.target_mhz = target_spi_mhz;best.actual_mhz = actual;best.error_mhz = err;best.exact = (err < 1e-9) ? 1 : 0;best.found = 1;}}}}}if (!any) return -1;if (tolerance_mhz > 0.0 && best_err > tolerance_mhz) {*out = best; // 也可以不回填,看你的策略return -1;}*out = best;return 0;
}// 演示/调用样例(单位 MHz)
static void demo_print_spi_solution(double target_mhz)
{pll2_spi_cfg_t sol{};const int rc = pll2_spi_solve_best(target_mhz, &sol, 0.0 /* 无容差限制,返回最近 */);if (rc != 0) {printf("No valid PLL2 config found for target SPI = %.6f MHz\r\n", target_mhz);return;}printf("Target: %.6f MHz, Actual: %.6f MHz, Error: %.6f MHz%s\r\n",sol.target_mhz, sol.actual_mhz, sol.error_mhz,sol.exact ? " (exact)" : "");printf("INPUT=%u MHz, M=%u, N=%u, P=%u, prescale=%u\r\n",sol.input_mhz, sol.pll2m, sol.pll2n, sol.pll2p, sol.prescale);
}
BOOT 探索
注意事项
- 将BOOT工程从MSDK中分离出来,作为独立的模块进行开发和维护。
- 更新CmakeLists.txt,确保新的模块结构被正确识别和构建。
- 注意.gcc文件的链接
- 注意Flash空间和RAM空间的设置分配
- 注意编译选项,newlib,gc-function,ffunction,lto选项
- 注意boot 跳转 app过程中可能出现的问题
编译选项
我们先看一下官方IDE的操作,后续我们通过Cmakelist移植到Cmake工程中
1. RISC-V 体系架构
2. 编译优化选项
-
编译警告选项
-
编译调试选项
-
编译输出选项
6. 编译链接选型
- CMakeLists.txt 中的编译示例
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -ffunction-sections -fdata-sections -flto -Os -Wall -Wextra -Wshadow -Wno-unused-parameter -Wno-missing-field-initializers -g3 -gdwarf-2")
# -ffunction-sections: 将每个函数放入独立段,便于链接时 --gc-sections 回收未用代码
# -fdata-sections: 将每个数据放入独立段,便于链接时 --gc-sections 回收未用数据
# -flto: 启用 LTO(链接时优化),跨文件消除/内联以减小体积
# -Os: 以体积优先进行优化
# -Wall: 启用常用警告
# -Wextra: 启用额外警告
# -Wshadow: 警告变量名遮蔽
# -Wno-unused-parameter: 关闭未使用参数的警告
# -Wno-missing-field-initializers: 关闭结构体部分初始化的警告
# -g3: 生成更丰富的调试信息(包含宏等)
# -gdwarf-2: 使用 DWARF v2 格式的调试信息,兼容性更好set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -nostartfiles -Xlinker")
# -nostartfiles: 不链接标准启动文件,用于裸机/自定义启动场景
# -Xlinker: 直接传递参数给链接器(ld)set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --gc-sections --specs=nano.specs --specs=nosys.specs")
# --gc-sections: 链接器回收未用代码段,减小体积
# --specs=nano.specs: 使用 newlib-nano,精简版 C 库
# --specs=nosys.specs: 使用 libnosys,提供弱 syscalls 实现(可被自定义桩覆盖)set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--just-symbols=${CMAKE_SOURCE_DIR}/ROM-EXPORT/symbol/rom_symbol_m.gcc")
# -Wl,--just-symbols=...: 链接时导入符号表(不实际链接代码),用于与 ROM/Bootloader 交互set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--print-memory-usage")
# -Wl,--print-memory-usage: 构建时输出各段内存占用统计# set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group,-lc_nano,-lgcc,-lnosys,--end-group")
# --start-group/--end-group: 链接分组,解决库间循环依赖
# -lc_nano: 链接 newlib-nano
# -lgcc: 链接 GCC 内部支持库
# -lnosys: 链接 libnosys(弱 syscalls)set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--no-warn-rwx-segment")
# -Wl,--no-warn-rwx-segment: 禁止链接器对 RWX 段发出警告(常见于裸机/自定义链接脚本)
编译瘦身思路和移植过程中的问题解决方案(重要)
- 编译尺寸过大,超过了flash定义的空间
- 编译尺寸过大,超过了ram定义的空间
- 桩函数undefined reference问题
- 指定的数据没有存放到flash指定的位置
- boot程序运行成功,串口log打印正常,但是跳转app失败
1. 确认当前编译和链接选项(如 -Os
、-ffunction-sections
、--gc-sections
、-specs=nano.specs
等),从而确定可直接调整的瘦身开关。随后会据此给出具体修改并验证构建。 代码中是否使用了 printf
及 %f
等会触发浮点格式化的用法,并检查是否定义了最小化或重定向的打印实现,以便决定是否切换到 newlib-nano
2. 定位并修复“桩函数 undefined reference”的链接错误, lib_hook_mbl.c
中桩函数的定义与原型是否正确并与 newlib
匹配。期望结果是找出缺失或签名不匹配的系统调用并修正。
- 修正并补齐 syscalls(文件 lib_hook_mbl.c):
- 新增头文件:
<sys/types.h>
,<sys/stat.h>
,<errno.h>
。 - 补齐
_sbrk(ptrdiff_t incr)
并返回-1
与errno = ENOMEM
(禁止动态内存分配,避免拉入堆)。 - 纠正原型以匹配 newlib:
- 修正
_write
签名(文件 mbl.c): - 由
int _write(int, char*, int)
改为int _write(int, const void*, size_t)
,严格匹配 newlib。
- 新增头文件:
3. RAM 显示 100% 是由链接脚本布局决定的(.stack
被固定放在 ORIGIN(ram) + LENGTH(ram) - __stack_size
,而 LENGTH(ram)
恰好等于保留的“0x600 + 栈(0x1000) + MBL_BUF_SIZE(0x3000)”,所以即便 BSS 变小,整体“已用”也始终显示为 100%),这并不代表真的没有余量。
- 当前 BSS 的大头是
alloc_buf[MBL_BUF_SIZE]
,MBL_BUF_SIZE
在 mbl_region.h 中为0x3000
(12KB),这是 mbedtls 内存池。 - 链接器告警 “LOAD segment with RWX permissions”:现状无功能问题,如需消除可在链接脚本中保证代码段仅 RX、数据段 RW,并避免把可写节与可执行节放同一 LOAD 段;这属于清洁度优化,不影响运行。
4. 为什么 .prefix
段的指令没生效,以及如何让它真正占用 flash0
空间。
- 你原来的匹配规则是
KEEP (*mbl.o* (.data.compat_prefix))
,这要求“对象文件名中包含 mbl.o 的”某个目标里有.data.compat_prefix
段才会被保留搬运到flash0
。但实际上compat_prefix
定义在 mbl.c,编译后目标文件通常叫mbl.c.obj
或mbl.o
(路径和命名由编译器/生成器决定) - 链接脚本 mbl.lds:
.prefix { KEEP (*mbl.o* (.data.compat_prefix)) } >flash0 AT>flash0
- 改为:
.prefix { KEEP (*(.data.compat_prefix)) } >flash0 AT>flash0
- 源文件 mbl.c:
- 给
compat_prefix
添加段属性和 used 标记:__attribute__((section(".data.compat_prefix"), used)) uint32_t compat_prefix[0x400] = {...};
- 作用:
- 指定放到
.data.compat_prefix
段,匹配链接脚本; used
防止 LTO/GC 误删。
- 指定放到
- 给
5. 串口日志正常,但“跳转到 App”失效。我们需要确保跳转前后中断向量、全局中断状态、以及跳转地址都按芯片/应用约定设置正确。
- 你原本通过内联汇编“la a2, reloc_iv; jalr a2”去设置 mtvec,但没有显式传入“目标向量基址”的参数,可能导致 mtvec 设置成了错误地址;跳转后遇到异常/中断就会卡死。
- 我已将跳转流程改为:
- 先关闭全局中断(清除 mstatus.MIE)
- 将 mtvec 设置为应用的向量表基址(即
start_addr
,并携带向量模式 bits,函数reloc_iv
内部会将地址+1从而设置向量模式位) - 最后执行
jr start_addr
跳转
这样做的依据
-
你的
reloc_iv(const uint32_t* address)
实现是csrw mtvec, address + 1
。对 RISC‑V 来说,mtvec
低两位是模式位,写入address+1
等价于mtvec = address | 0x1
,选择 vectored 模式,并且向量基址对齐。Boot 原意就是把 mtvec 指向目标映像向量区首地址。 -
start_addr
的来源:start_addr = FLASH_BASE + image_offset
。而image_offset
是RE_IMG_0_OFFSET + IMG_OVERHEAD_LEN
。此处项目规范是应用镜像在start_addr
放置它自己的向量(或入口),对应你在 ROM-EXPORT/bootloader 的设计。将 mtvec 设置为start_addr
符合「跳转前把异常/中断向量基址切到 App」的意图。 -
关闭 MIE 可以避免在切换 mtvec 过程中触发异常/中断。
-
mbl.c 的
jump_to_main_image()
:- 增加关中断:
li t0, 8; csrc mstatus, t0
- 用
reloc_iv((const uint32_t *)start_addr);
设置 App 的向量基址 - 保留
jr start_addr
执行跳转
- 增加关中断:
为什么这能修复跳转失效
- 之前调用
reloc_iv
的“workaround for long jump”并没有给a0
传入正确的地址参数,reloc_iv
里用的寄存器值未定义,导致mtvec
被写成错误地址,一旦跳转 App 后有中断/异常发生,CPU 会从错误向量进入未知区域,表现为卡住或异常。 - 现在明确地传入了
start_addr
作为mtvec
的基址,避免了这个错误路径。
总结
- 我把跳转流程修正为:关中断 → 设置
mtvec = start_addr|1
(向量模式) →jr start_addr
,解决“跳转后死机/无响应”的常见原因。
BootLoader移植
- 参考官方例程, 将所需文件放到新建的工程下面,并基于之前的cmakelist.txt进行修改
target_compile_definitions(stm32cubemx INTERFACE EXEC_USING_STD_PRINTF
)target_include_directories(stm32cubemx INTERFACEboot/Include/ config/ROM-EXPORT/bootloader/ROM-EXPORT/halcomm/ROM-EXPORT/mbedtls-2.17.0-rom/include/platform/riscv/gd32vw55x/platform/riscv/NMSIS/Core/Includeplatform/GD32VW55x_standard_peripheral/Includeplatform/GD32VW55x_standard_peripheralplatform/src/
)file(GLOB SRC_APP_0 boot/Source/*.c)target_sources(stm32cubemx INTERFACE${SRC_APP_0}platform/GD32VW55x_standard_peripheral/Source/gd32vw55x_eclic.cplatform/GD32VW55x_standard_peripheral/Source/gd32vw55x_fmc.cplatform/GD32VW55x_standard_peripheral/Source/gd32vw55x_gpio.cplatform/GD32VW55x_standard_peripheral/Source/gd32vw55x_rcu.cplatform/GD32VW55x_standard_peripheral/Source/gd32vw55x_usart.cplatform/riscv/gd32vw55x/system_gd32vw55x.cplatform/riscv/env/handlers.cplatform/riscv/env/env_init.cplatform/src/init_rom_symbol.cplatform/riscv/arch/lib/lib_hook_mbl.cplatform/riscv/env/entry.Splatform/riscv/env/start.S
)
- 为了保证第一条指令始终是安全的,我们将第一个可执行程序段固化在 ROM 中,称之为不可变引导(IBL)。当启动方式被锁定为安全启动后,不论是上电还是重启,系统都会跳转到IBL 来运行第一条指令,任何方式不能篡改。因此第一条指令就拥有了根信任。第二个可执行程序段,放在FLASH 开头,称之为主引导(MBL)。 IBL 会负责验证MBL的合法性和完整性,验证通过后,才能跳转到 MBL。后一级可执行程序段,我们称之为 MSDK,将由MBL 来负责验证签名,验证成功后,才能跳转到 MSDK 运行。
-
其中 ROM 使用到的全局变量占用了 512 字节。Shared data 是 ROM 传递给 MBL 的信息, 包括 ROM 版本信息, ROTPK 哈希值以及MBL公钥等信息。MBL used 是 MBL 运行时用到的堆和栈。MSDK used 是 MSDK 可以使用的SRAM 空间。需要注意的是 MSDK 使用的SRAM 跟 Shared data 区域以及MBL区域有重合, 是因为在 MBL 运行之后,这两块空间都可以释放出来给 MSDK 使用。 因此 MSDK 使用的SRAM 起始位置也是0x20000200。
-
mbl.lds中将数据段放到指定 flash0 空间中
-
newlib桩函数串口打印部分原型
-
boot跳转app
-
最开始一段程序是通过指针,找到rom空间,通过mbedtls的函数地址和功能实现的镜像校验功能
验证是否移植成功
- 很简单,首先将官方的image boot和sdk 下载进去,串口会打印正常的log,
- 第二步就是将我们移植好的,编译成功的bin文件下载进去,注意起始地址是0x08000000, 复位上电之后如果还能正常打印相同的LOG说明移植成功,boot跳转app也正常
- 后续就是APP阶段的开发了
git链接
https://github.com/1508912767/gd32vw553_boot