QMK ChibiOS 早期硬件初始化深入解析:early_hardware_init_pre、early_hardware_init_post 与 board_init 三级 API
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
本文聚焦 QMK 中仅面向键盘硬件设计者的一个进阶概念——Arm/ChibiOS 平台的早期初始化(Early Initialization)。QMK 使用 ChibiOS 作为底层 RTOS 来支撑大量 Arm 系列 MCU,每台 ChibiOS 键盘都有一份低层板级定义(board definition),负责时钟与 GPIO 等硬件外设的初始化。读完本文,你将理解 QMK 如何用三个可覆盖的弱符号 API 替代“整份复制板级定义”的老做法,知道每个初始化时点能做什么、不能做什么,以及如何通过config.h配置在QK_BOOT按下时于最早时机跳转到 bootloader。
背景:为什么需要“早期初始化”API
早期 QMK 版本中,如果你需要在板级定义初始化时钟、GPIO 之前就执行自己的代码,唯一办法是把你所用板级定义的整份文件复制进自己键盘的目录,然后在副本里修改初始化点。这种做法代价高昂:板级定义更新时你的副本会漂移,维护负担极重。
现在的做法是:直接复用 ChibiOS 自带的官方板级定义(官方定义位于 QMK 仓库中 ChibiOS 子模块的lib/chibios/os/hal/boards目录,完整检出后可查阅),仅在你确实需要在极早阶段执行额外初始化时,提供键盘级别的 API 覆盖。这些 API 的定义与调用链都集中在 tmk_core/protocol/chibios/chibios.c 中。
初始化总流程:从源码看三个 API 的插入位置
QMK 对 ChibiOS 的两个板级入口做了“包装式覆盖”。从 chibios.c 的源码可以直接看到完整调用顺序:
// This overrides what's normally in ChibiOS board definitions void __early_init(void) { early_hardware_init_pre(); // This is the renamed equivalent of __early_init in the board.c file void __chibios_override___early_init(void); __chibios_override___early_init(); early_hardware_init_post(); } // This overrides what's normally in ChibiOS board definitions void boardInit(void) { // This is the renamed equivalent of boardInit in the board.c file void __chibios_override_boardInit(void); __chibios_override_boardInit(); board_init(); }即:
__early_init(ChibiOS 上电后最早执行的函数,负责清 RAM、配时钟/GPIO)被拆成三段:先调early_hardware_init_pre(),再执行原板级定义的__early_init(被重命名为__chibios_override___early_init以避免符号冲突),最后调early_hardware_init_post();boardInit(ChibiOS RTOS 内核初始化完成后调用)先执行原板级定义的boardInit(重命名为__chibios_override_boardInit),再调board_init()。
三个 API 在 chibios.c 中均以__attribute__((weak))弱符号形式给出默认实现,键盘可以在自己的源文件里定义同名函数完成覆盖,无需改动 QMK 核心代码:
__attribute__((weak)) void early_hardware_init_pre(void) { /* ... */ } __attribute__((weak)) void early_hardware_init_post(void) {} __attribute__((weak)) void board_init(void) {}下面按时间先后逐一说明每个时点的能力边界。
early_hardware_init_pre:固件能执行的最早代码
early_hardware_init_pre是键盘固件中最早可能执行的代码,等价于在 ChibiOS 板级定义__early_init函数的开头执行。它的时序约束非常严格:
- 此时RAM 尚未被清零,时钟与 GPIO 也尚未配置;
- ChibiOS 的延时(
wait_ms等)此时基本不可能工作; - 函数返回后,MCU 的 RAM 可能被清零——在这段代码里给变量赋的值可能被覆盖。
因此官方建议:若覆盖此 API,把用途限制在直接写低层寄存器这一类操作上。
默认实现:可选的 QK_BOOT 早期 bootloader 跳转
默认实现并非空函数,而是提供了可配置的能力:在检测到QK_BOOT键被按下时,于最早的初始化阶段直接跳转 bootloader 刷写固件。相关宏均在键盘的config.h中配置:
config.h宏 | 说明 | 默认值 |
|---|---|---|
#define EARLY_INIT_PERFORM_BOOTLOADER_JUMP | 是否在 QMK 早期初始化代码中执行 bootloader 跳转逻辑 | FALSE |
#define STM32_BOOTLOADER_DUAL_BANK | 双 bank 的 STM32 MCU 专用:进入 bootloader 模式时需要翻转一个 GPIO(硬件方案) | FALSE |
#define STM32_BOOTLOADER_DUAL_BANK_GPIO | 双 bank STM32 专用:要翻转的引脚,例如B8 | 未定义 |
#define STM32_BOOTLOADER_DUAL_BANK_POLARITY | 双 bank STM32 专用:触发 RC 电路充电时该引脚要设置的电平,0或1 | 0 |
#define STM32_BOOTLOADER_DUAL_BANK_DELAY | 双 bank STM32 专用:复位前等待的任意时间度量值,数值越大延时越长 | 100 |
Kinetis MCU 无可配置项。
从源码看,默认实现的开关逻辑在 chibios.c:
__attribute__((weak)) void early_hardware_init_pre(void) { #if EARLY_INIT_PERFORM_BOOTLOADER_JUMP void enter_bootloader_mode_if_requested(void); enter_bootloader_mode_if_requested(); #endif }注意EARLY_INIT_PERFORM_BOOTLOADER_JUMP未定义时默认为FALSE(见 chibios.c 的条件编译,其中源码注释也明确提醒修改默认值时须同步更新本文对应的文档页)。
深入:STM32 DFU 的两种跳转机制
enter_bootloader_mode_if_requested在 STM32 平台的具体实现在 platforms/chibios/bootloaders/stm32_dfu.c,它按STM32_BOOTLOADER_DUAL_BANK分两套路径:
单 bank(可直接跳转)路径:利用一块“RAM 标记”机制。bootloader_marker_enable()在STM32_BOOTLOADER_RAM_SYMBOL(默认为__ram0_end__)前 4 字节写入魔数0xDEADBEEF,bootloader_jump()置标记后调用NVIC_SystemReset(),复位处理例程检测到标记即跳转 bootloader。enter_bootloader_mode_if_requested()只在标记有效时执行跳转,并先关闭 Cortex-M7 的 D-Cache/I-Cache(如适用)、禁用 MPU、清零 SysTick 与 NVIC 中断使能/挂起寄存器,恢复 MSP 与入口点后跳入。
双 bank(硬件 GPIO 方案)路径:双 bank 的 STM32 复位后无条件先执行第一个有效 flash bank,无法靠软件跳转进 ROM bootloader,除非 BOOT0 拉高。QMK 的对策是用硬件 RC 电路模拟 BOOT0:把配置引脚设为推挽输出并按STM32_BOOTLOADER_DUAL_BANK_POLARITY置位或清零,wait_ms(STM32_BOOTLOADER_DUAL_BANK_DELAY)等电容充好电,再NVIC_SystemReset()——复位瞬间 BOOT0 为高,ROM bootloader 得以接管。若定义了DUAL_BANK却没给引脚,编译会直接#error报错提示:
# ifndef STM32_BOOTLOADER_DUAL_BANK_GPIO # error "No STM32_BOOTLOADER_DUAL_BANK_GPIO defined, don't know which pin to toggle" # endif双 bank 分支中enter_bootloader_mode_if_requested被刻意实现为空函数(见 stm32_dfu.c),因为该硬件路径的触发时机由板级/键盘代码显式决定。仓库中 platforms/chibios/boards/GENERIC_STM32_G474XE/configs/config.h 就是一个启用双 bank 方案的真实配置示例。除 STM32 外,platforms/chibios/bootloaders/ 目录下还有uf2boot.c、rp2040.c、at32_dfu.c、gd32v_dfu.c等各 MCU 家族的 bootloader 实现。
自定义 pre 阶段代码
在你键盘的源文件中直接定义即可覆盖弱符号:
void early_hardware_init_pre(void) { // do things with registers(只操作寄存器) }early_hardware_init_post:RAM 已清、时钟已配后的早期窗口
early_hardware_init_post是次早可执行的代码,等价于在板级定义__early_init函数的末尾执行。此时 RAM 已清零、时钟与 GPIO 已配置,比 pre 阶段安全得多,但 ChibiOS 本身仍未初始化,所以延时与计时类 API 的限制与 pre 阶段相同。
官方建议把此阶段的功能限制在:寄存器写入、变量初始化、GPIO 翻转。默认实现为空函数。自定义写法:
void early_hardware_init_post(void) { // toggle GPIO pins and write to variables }board_init:ChibiOS 初始化完成后的“正常”初始化
board_init在 ChibiOS 初始化例程完成之后立即执行。此时包括定时器、延时在内的全部常规底层功能都可用,唯一的限制是USB 尚未连接(USB 驱动要等到protocol_setup/protocol_pre_init阶段才启动,见 chibios.c)。它对应板级定义boardInit的末尾位置,默认实现为空函数。需要用到 ChibiOS API 的初始化(如编码器、OLED 外设驱动等)应放在这里:
void board_init(void) { // initialize anything that requires ChibiOS }三个时点如何选择:实践速查
| 时点 | RAM 状态 | 时钟/GPIO | ChibiOS API | USB | 适合做什么 |
|---|---|---|---|---|---|
early_hardware_init_pre | 未清零,可能被清零 | 未配置 | 不可用 | 未连接 | 仅寄存器写入(如早期 bootloader 跳转触发) |
early_hardware_init_post | 已清零 | 已配置 | 不可用(延时仍受限) | 未连接 | 寄存器写入、变量初始化、GPIO 翻转 |
board_init | 可用 | 可用 | 可用 | 未连接 | 一切需要 ChibiOS 的外设初始化 |
选择建议:能用晚的时点就不要用早的时点——pre/post 阶段的代码在时钟未稳、RAM 未定的环境下运行,调试困难且风险高;只有“必须在时钟配置前完成”的需求(典型如 BOOT0 硬件电路的 GPIO 翻转)才值得占用early_hardware_init_pre。
小结
QMK 的 ChibiOS 早期初始化机制通过 tmk_core/protocol/chibios/chibios.c 中的弱符号三件套(early_hardware_init_pre/early_hardware_init_post/board_init),把“必须整份复制板级定义才能改初始化”升级为“按初始化时点精准覆盖”,让键盘开发者可以直接复用 ChibiOS 官方板级定义。配套的EARLY_INIT_PERFORM_BOOTLOADER_JUMP及STM32_BOOTLOADER_DUAL_BANK*系列配置宏,则为在最早时机进入 DFU/bootloader 提供了成熟的单 bank 魔数跳转与双 bank 硬件 RC 电路两种路径,其完整实现可在 platforms/chibios/bootloaders/stm32_dfu.c 中逐行核对。
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考