当前位置: 首页 > news >正文

GD32VW553-IOT 基于 vscode 的 bootloader 移植(基于Cmake)

前言

主频时钟切换

  1. 当系统时钟频率从高主频直接切换到低主频或从低主频直接切换到高主频时, 如在Boot+APP架构中,或进出deepsleep需要重新配置时钟等,在这类场景中需要加上阶段式地升频或降频操作,先将频率阶段式地降下来,然后将系统时钟源切换到其他时钟源,如内部高速晶振,再去修改PLL,最后阶段式升到目标频率

  2. 系统级应用中,在 Boot 程序中,系统时钟的变化为先由内部高速晶振切换到 PLL 最高频率的变化过程,需要应用层注意添加阶段式切频代码,或者在 APP 工程中, 如对目标频率需求没有变化, 无需重新配置时钟。

  3. 应用层需要避免由高频直接切换到低频,假设当前时钟为 120MHz,调用下面三段式降频代码后,频率的变化为120MHz->60MHz->30MHz->15MHz,接下来再将系统时钟切换到内部高速晶振。

  4. 应用层需要避免直接由低频(如 IRC8M)直接切到最高频率, 如deepsleep 唤醒之后频率由内部高速晶振倍频到特定频率,系统频率的变化是先由内部高速晶振进行 8 分频,然后配置 PLL 并将时钟源切到 PLL,接下来开始逐级升频。

  5. 除了上述提供的宏可以参考以外,还可以调用 gd32f30x_rcu.c 中提供的 rcu_ahb_clock_config函数实现 AHB 时钟的变化,注意在每次 AHB 频率变化之后,需要加一点延时保证时钟稳定。要想实现频率的变化,须要先将时钟由 PLL 切到其他时钟源,然后修改 PLL,再将系统时钟源切回到 PLL。对于其他系列,均可以参考类似做法,避免在时钟频率切换时出现异常。

外设时钟切换

  1. 外设时钟切换需要考虑到外设的工作状态和时钟源的切换时机,避免在外设工作时进行时钟切换导致数据错误或丢失。
  2. 在进行外设时钟切换时,需要先将外设停止工作,待外设空闲后再进行时钟源的切换。
  3. 时钟源切换完成后,需要重新配置外设的时钟参数,确保外设能够在新的时钟环境下正常工作。
  4. 由于外设可以设置不同的时钟源,因此可以在不修改主频的情况下修改外设频率,如下是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 探索

注意事项

  1. 将BOOT工程从MSDK中分离出来,作为独立的模块进行开发和维护。
  2. 更新CmakeLists.txt,确保新的模块结构被正确识别和构建。
  3. 注意.gcc文件的链接
  4. 注意Flash空间和RAM空间的设置分配
  5. 注意编译选项,newlib,gc-function,ffunction,lto选项
  6. 注意boot 跳转 app过程中可能出现的问题

编译选项

我们先看一下官方IDE的操作,后续我们通过Cmakelist移植到Cmake工程中
1. RISC-V 体系架构
#figure(
image("/resources/pic/编译体系架构.png", width:auto),
caption: "编译体系架构"
)

2. 编译优化选项
#figure(
image("/resources/pic/编译优化选项.png", width:auto),
caption: "编译优化选项"
)

  1. 编译警告选项
    #figure(
image("/resources/pic/编译警告选项.png", width:auto),
caption: "编译警告选项"
)

  2. 编译调试选项
    #figure(
image("/resources/pic/编译调试选项.png", width:auto),
caption: "编译调试选项"
)

  3. 编译输出选项
    #figure(
image("/resources/pic/编译输出选项.png", width:auto),
caption: "编译输出选项"
)

6. 编译链接选型
#figure(
image("/resources/pic/编译链接选型.png", width:auto),
caption: "编译链接选型"
)

  1. 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 段发出警告(常见于裸机/自定义链接脚本)

编译瘦身思路和移植过程中的问题解决方案(重要)

  1. 编译尺寸过大,超过了flash定义的空间
  2. 编译尺寸过大,超过了ram定义的空间
  3. 桩函数undefined reference问题
  4. 指定的数据没有存放到flash指定的位置
  5. 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) 并返回 -1errno = 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.objmbl.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_offsetRE_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移植

  1. 参考官方例程, 将所需文件放到新建的工程下面,并基于之前的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
)
  1. 为了保证第一条指令始终是安全的,我们将第一个可执行程序段固化在 ROM 中,称之为不可变引导(IBL)。当启动方式被锁定为安全启动后,不论是上电还是重启,系统都会跳转到IBL 来运行第一条指令,任何方式不能篡改。因此第一条指令就拥有了根信任。第二个可执行程序段,放在FLASH 开头,称之为主引导(MBL)。 IBL 会负责验证MBL的合法性和完整性,验证通过后,才能跳转到 MBL。后一级可执行程序段,我们称之为 MSDK,将由MBL 来负责验证签名,验证成功后,才能跳转到 MSDK 运行。

#figure(
image("/resources/pic/flash区域规划.png", width:8cm),
caption: "flash区域规划"
)

  1. 其中 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。
    #figure(
image("/resources/pic/RAM区域规划.png", width:10cm),
caption: "RAM区域规划"
)

  2. mbl.lds中将数据段放到指定 flash0 空间中
    #figure(
image("/resources/pic/指定data段位置.png", width:auto),
caption: "指定data段位置"
)

#figure(
image("/resources/pic/data段数据.png", width:auto),
caption: "data段数据"
)

  1. newlib桩函数串口打印部分原型
    #figure(
image("/resources/pic/write桩函数.png", width:auto),
caption: "write桩函数"
)

  2. boot跳转app
    #figure(
image("/resources/pic/boot程序跳转app.png", width:auto),
caption: "boot程序跳转app"
)

  3. 最开始一段程序是通过指针,找到rom空间,通过mbedtls的函数地址和功能实现的镜像校验功能在这里插入图片描述

验证是否移植成功

  1. 很简单,首先将官方的image boot和sdk 下载进去,串口会打印正常的log,
  2. 第二步就是将我们移植好的,编译成功的bin文件下载进去,注意起始地址是0x08000000, 复位上电之后如果还能正常打印相同的LOG说明移植成功,boot跳转app也正常
  3. 后续就是APP阶段的开发了

git链接

https://github.com/1508912767/gd32vw553_boot

http://www.xdnf.cn/news/1400203.html

相关文章:

  • 微论-突触的作用赋能思考(可能是下一代人工智能架构的启发式理论)
  • 响应式编程框架Reactor【5】
  • Spring代理的特点
  • AI-调查研究-65-机器人 机械臂控制技术的前世今生:从PLC到MPC
  • 【MCP系列教程】 Python 实现 FastMCP StreamableHTTP MCP:在通义灵码 IDE 开发并部署至阿里云百炼
  • JsMind 常用配置项
  • 【计算机网络】HTTP是什么?
  • 基于Docker部署的Teable应用
  • Linux驱动开发重要操作汇总
  • “人工智能+”政策驱动下的技术重构、商业变革与实践路径研究 ——基于国务院《关于深入实施“人工智能+”行动的意见》的深度解读
  • wpf之依赖属性
  • 桌面GIS软件FlatGeobuf转Shapefile代码分享
  • 学习游戏制作记录(视觉上的优化)
  • 第三弹、AI、LLM大模型是什么?
  • Visual Studio(vs)免费版下载安装C/C++运行环境配置
  • openEuler2403安装部署Redis8
  • FPGA学习笔记——SPI读写FLASH
  • 【运维篇第三弹】《万字带图详解分库分表》从概念到Mycat中间件使用再到Mycat分片规则,详解分库分表,有使用案例
  • 小迪Web自用笔记7
  • 【Linux】如何使用 Xshell 登录 Linux 操作系统
  • SC税务 登录滑块 分析
  • 拦截器Intercepter
  • hello算法笔记 01
  • Isaac Lab Newton 人形机器人强化学习 Sim2Real 训练与部署
  • 下一代 AI 交互革命:自然语言对话之外,“意念控制” 离商用还有多远?
  • 在 .NET Core 中实现基于策略和基于角色的授权
  • HarmonyOS应用的多Module设计机制:构建灵活高效的应用程序
  • 【瑞吉外卖】手机号验证码登录(用QQ邮件发送代替)
  • python制作一个股票盯盘系统
  • NV032NV037美光固态闪存NV043NV045