QMK 固件 UART 驱动完全指南:跨 AVR/ARM 平台的串口通信配置与 API 实战
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
导读
UART(通用异步收发器)是键盘固件与外部设备(如蓝牙模块、传感器、调试工具)进行串行通信的经典方式。QMK 固件为 AVR 与 ARM(ChibiOS)两大平台封装了一套接口统一的 UART 驱动,让开发者无需关心底层寄存器差异即可收发字节流。本文以官方驱动文档为主体,结合本仓库的 drivers/uart.h 头文件与 AVR、ChibiOS 两套平台实现源码,完整讲解 UART 驱动的启用方式、AVR 引脚接线、ARM 外设配置以及全部 6 个 API 函数的用法与底层原理,读完即可在自己的键盘固件中独立接入 UART 设备。
一、UART 驱动概览与使用前提
QMK 中的 UART 驱动为不同 MCU(单片机)提供了一组通用函数,以实现跨平台的代码可移植性——你写的业务代码调用的是同一套 API,而底层由各平台的实现负责适配。
需要特别说明的一个限制是:当前版本的驱动尚不支持启用硬件流控(RTS/CTS引脚),尽管部分 MCU 硬件上具备该能力,未来版本可能会加入支持。这一点在 drivers/uart.h 与两份平台实现中均有体现(配置项默认值中虽定义了 CTS/RTS 引脚,但实际并未参与收发逻辑)。
1.1 自动引入与手动启用
在大多数情况下,只要你使用的某个功能或驱动依赖 UART,驱动代码会被自动包含到编译中。例如在 builddefs/common_features.mk 中可以看到,当选择rn42蓝牙驱动时,构建系统会自动置位UART_DRIVER_REQUIRED = yes并引入rn42.c:
ifeq ($(strip $(BLUETOOTH_DRIVER)), rn42) UART_DRIVER_REQUIRED = yes SRC += $(DRIVER_PATH)/bluetooth/bluetooth_drivers.c SRC += $(DRIVER_PATH)/bluetooth/rn42.c endif如果你需要独立使用UART 驱动(例如自己编写与外部设备通信的代码),则需在键盘或 keymap 的rules.mk中显式声明:
UART_DRIVER_REQUIRED = yes随后在你的代码中引入头文件即可调用全部 API:
#include "uart.h"1.2 构建系统如何选择底层实现
设置UART_DRIVER_REQUIRED = yes后,builddefs/common_features.mk 会依据当前平台自动决定链接哪个实现文件:
| 平台条件 | 编译选项 | 链接的源文件 |
|---|---|---|
| ChibiOS + RP2040 系列 | -DHAL_USE_SIO=TRUE | platforms/chibios/drivers/uart_sio.c |
| ChibiOS + 其他 MCU(如 STM32) | -DHAL_USE_SERIAL=TRUE | platforms/chibios/drivers/uart_serial.c |
| AVR | 无 | platforms/avr/drivers/uart.c |
也就是说,同一个uart.h头文件背后有三种实现,分别对应AVR 原生 USART 驱动、ChibiOS 串行驱动(SD)与RP2040 的 SIO 驱动。
二、AVR 配置:无需特殊设置,只需接对引脚
在 AVR 平台上,不需要任何特殊配置——只需将 UART 设备的RX/TX引脚连接到 MCU 的相反引脚(即设备的 TX 接 MCU 的 RX,设备的 RX 接 MCU 的 TX)即可。
官方文档给出了各常用 AVR MCU 的 UART 引脚对照表(CTS/RTS标注*n/a*表示该 MCU 无对应引脚或当前不支持):
| MCU | TX | RX | CTS | RTS |
|---|---|---|---|---|
| ATmega16/32U2 | D3 | D2 | D7 | D6 |
| ATmega16/32U4 | D3 | D2 | D5 | B7 |
| AT90USB64/128 | D3 | D2 | n/a | n/a |
| ATmega32A | D1 | D0 | n/a | n/a |
| ATmega328/P | D1 | D0 | n/a | n/a |
2.1 AVR 实现源码要点
从 platforms/avr/drivers/uart.c 可以印证上述引脚差异的底层原因:源码通过预处理器按 MCU 型号将寄存器宏重定向到对应的 USART 外设。
- ATmega16/32U2、ATmega16/32U4、AT90USB64/128 等芯片使用USART1,对应宏被重定义为
UDR1、UBRR1L、UCSR1A等(uart.c#L32-L46); - ATmega32A 使用USART0(命名不带数字后缀,见 uart.c#L47-L61);
- ATmega328/P 使用USART0,显式重定义为
UDR0、UBRR0L等(uart.c#L62-L77)。
uart_init()的波特率计算使用双倍速率(U2X)模式:UBRRnL = (F_CPU / 4 / baud - 1) / 2,并开启RXEN、TXEN与RXCIE(接收中断)三个位(uart.c#L91-L100)。接收与发送采用环形缓冲区实现——RX 缓冲区 64 字节、TX 缓冲区 256 字节(uart.c#L79-L88),通过USARTn_UDRE_vect(发送空中断)与USARTn_RX_vect(接收中断)两个 ISR 驱动,因此uart_read()在缓冲区为空时会阻塞等待(uart.c#L120-L130),uart_available()则通过比较头尾指针判断缓冲区是否有数据(uart.c#L147-L154)。
注意:以上针对的是
UART_DRIVER_REQUIRED = yes且未使用 ChibiOS 的情况。若使用 ChibiOS 平台,请参考下一节。
三、ChibiOS / ARM 配置:启用外设并设置引脚复用
ARM 平台(以 STM32 为代表)通常有多个 UART 外设,例如 USART1、USART2、USART3……你需要在板级配置中明确启用选定的外设。
3.1 在mcuconf.h中启用外设
修改你所用板子的mcuconf.h,将所选外设从禁用改为启用。以 USART2 为例:
#pragma once #include_next <mcuconf.h> #undef STM32_SERIAL_USE_USART2 // [!code focus] #define STM32_SERIAL_USE_USART2 TRUE // [!code focus]3.2 通过config.h覆盖引脚与外设(默认值为 Proton-C 即 STM32F303)
配置层面,你需要按照所选 MCU 的数据手册来设定外设参数。官方文档给出的默认值与Proton-C(STM32F303)板一致:
config.hOverride | 说明 | 默认值 |
|---|---|---|
UART_DRIVER | 要使用的 USART 外设——USART1 对应SD1,USART2 对应SD2,以此类推 | SD1 |
UART_TX_PIN | 用于 TX 的引脚 | A9 |
UART_TX_PAL_MODE | TX 的复用功能(Alternate Function)模式号 | 7 |
UART_RX_PIN | 用于 RX 的引脚 | A10 |
UART_RX_PAL_MODE | RX 的复用功能模式号 | 7 |
UART_CTS_PIN | 用于 CTS 的引脚 | A11 |
UART_CTS_PAL_MODE | CTS 的复用功能模式号 | 7 |
UART_RTS_PIN | 用于 RTS 的引脚 | A12 |
UART_RTS_PAL_MODE | RTS 的复用功能模式号 | 7 |
3.3 实现源码对默认值的印证
platforms/chibios/drivers/uart_serial.c 中可以看到这些默认值正是通过#ifndef逐一定义的(uart_serial.c#L10-L60):
#ifndef UART_DRIVER # define UART_DRIVER SD1 #endif #ifndef UART_TX_PIN # define UART_TX_PIN A9 #endif #ifndef UART_TX_PAL_MODE # ifdef USE_GPIOV1 # define UART_TX_PAL_MODE PAL_MODE_ALTERNATE_PUSHPULL # else # define UART_TX_PAL_MODE 7 # endif #endif // ... 其余引脚依此类推值得注意的是,对于使用GPIOv1 库的旧款 MCU(USE_GPIOV1被定义),PAL 模式默认值会自动切换为PAL_MODE_ALTERNATE_PUSHPULL(发送)与PAL_MODE_INPUT(接收),开发者无需手动指定数字模式号。
在uart_init()内部,驱动会:
- 以
palSetLineMode()将 TX/RX 引脚配置为带PAL_MODE_ALTERNATE() | PAL_OUTPUT_TYPE_PUSHPULL | PAL_OUTPUT_SPEED_HIGHEST的复用推挽输出(uart_serial.c#L119-L125); - 调用
sdStart(&UART_DRIVER, &serialConfig)启动 ChibiOS 串行驱动,其中SerialConfig的波特率被动态设置为传入的baud(uart_serial.c#L105-L128)。
此外,uart_init()内部有static bool is_initialised保护,重复调用会被直接忽略,确保驱动只初始化一次(uart_serial.c#L106-L111)。
3.4 RP2040 的特殊路径
当使用 RP2040 芯片时,构建系统会改用 platforms/chibios/drivers/uart_sio.c。该实现使用 ChibiOS 的SIO 驱动:默认外设为SIOD1(uart_sio.c#L10-L12),默认配置为“38400 波特率、8 位数据位、1 位停止位、无校验、无流控”(uart_sio.c#L74-L82),并通过chnPutTimeout/chnGetTimeout/chnRead/chnWrite实现收发。读取时会额外检查并清除 RX 错误标志(uart_sio.c#L120-L140)。
四、UART API 全解:6 个函数逐一拆解
所有 API 的声明集中在 drivers/uart.h,对应不同平台的三份实现行为一致。以下是完整 API 参考(与官方文档一致,并补充实现细节)。
4.1void uart_init(uint32_t baud)
初始化 UART 驱动。此函数必须且只能调用一次,并且必须先于下面所有函数调用。
- 参数
uint32_t baud:收发波特率,取决于你要通信的设备。常用值:1200、2400、4800、9600、19200、38400、57600、115200。 - 实现说明:AVR 平台据此计算波特率寄存器并开启收发与接收中断(uart.c#L91-L100);ChibiOS 平台将其写入
SerialConfig.speed后调用sdStart;RP2040 平台写入SIOConfig.baud后调用sioStart。
4.2void uart_write(uint8_t data)
发送单个字节。
- 参数
uint8_t data:要写入的字节。 - 实现说明:AVR 平台将字节写入发送环形缓冲区并开启发送中断(uart.c#L103-L117);ChibiOS 平台调用
sdPut(&UART_DRIVER, data);RP2040 平台调用chnPutTimeout(..., TIME_INFINITE)阻塞直到发送完成。
4.3uint8_t uart_read(void)
接收单个字节。
- 返回值:从接收缓冲区读到的字节。若缓冲区为空(无数据可读),此函数会阻塞等待,直到数据到达。
- 实现说明:AVR 平台在
rx_buffer_head == rx_buffer_tail时自旋等待(uart.c#L120-L130);RP2040 平台使用chnGetTimeout(..., TIME_INFINITE)并清除 RX 错误标志。
4.4void uart_transmit(const uint8_t *data, uint16_t length)
发送多个字节。
- 参数
const uint8_t *data:指向待发送数据的指针。 - 参数
uint16_t length:要发送的字节数。注意不要越界访问data数组。 - 实现说明:AVR 平台循环调用
uart_write()(uart.c#L132-L136);ChibiOS 平台直接调用sdWrite(&UART_DRIVER, data, length)一次完成批量发送。
4.5void uart_receive(uint8_t *data, uint16_t length)
接收多个字节。
- 参数
uint8_t *data:指向接收缓冲区的指针(注意头文件签名中为uint8_t *,官方文档此处写作char *属文档笔误,以 drivers/uart.h 为准)。 - 参数
uint16_t length:要读取的字节数。同样注意不要越界。 - 实现说明:AVR 平台循环调用
uart_read()(uart.c#L138-L142);ChibiOS 平台调用sdRead(&UART_DRIVER, data, length)。
4.6bool uart_available(void)
返回接收缓冲区是否已有数据。
- 返回值:
true表示有数据可读。 - 用法:在调用
uart_read()之前先调用此函数,可判断uart_read()是否会立即返回而不阻塞。 - 实现说明:AVR 平台通过比较环形缓冲区头尾指针判断(uart.c#L147-L154);ChibiOS 平台返回
!sdGetWouldBlock(&UART_DRIVER);RP2040 平台返回!sioIsRXEmptyX(&UART_DRIVER)。
五、实战示例:在 QMK 中调用 UART API
5.1 标准调用流程
一个典型的 UART 使用流程如下(例如在键盘的自定义代码或 feature 中):
#include "uart.h" void my_uart_setup(void) { uart_init(115200); // 只初始化一次,选择设备支持的波特率 } void my_uart_loop(void) { if (uart_available()) { // 先检查是否有数据,避免阻塞 uint8_t byte = uart_read(); // 处理接收到的字节... } uart_write(0x55); // 发送单字节 const uint8_t buf[] = {0x01, 0x02, 0x03}; uart_transmit(buf, sizeof(buf)); // 批量发送 }5.2 仓库内的真实用例:RN42 蓝牙模块
本仓库的 drivers/bluetooth/rn42.c 是一个完整的 UART 驱动实战范例——RN42 蓝牙模块正是通过 UART 与主控通信的。可以看到它:
- 调用
uart_init(RN42_BAUD_RATE)完成初始化(rn42.c#L67); - 使用
uart_write()逐字节发送 HID 报告(修饰键、按键码等),例如发送按键报告时的多字节写入(rn42.c#L71-L88)。
这验证了 UART 驱动的典型使用模式:初始化 → 批量/单字节写入 → 读取响应。
六、总结与注意事项
- 启用方式:在
rules.mk中添加UART_DRIVER_REQUIRED = yes,或依赖会自动引入 UART 的功能(如rn42蓝牙驱动); - AVR 平台:无需配置,按 docs/drivers/uart.md 的引脚表接线即可(注意 RX 接 TX、TX 接 RX);
- ARM 平台:先在
mcuconf.h启用对应外设(如STM32_SERIAL_USE_USART2 TRUE),再按需在config.h中覆盖UART_DRIVER及 8 个引脚/复用模式宏,默认值对应 Proton-C(STM32F303)的SD1/A9/A10; - 初始化约束:
uart_init()只能调用一次,ChibiOS 实现内置了重复调用保护,但业务代码仍应保持“一次初始化”的习惯; - 阻塞行为:
uart_read()/uart_receive()在无数据时会阻塞,务必配合uart_available()使用; - 缓冲区大小:AVR 实现 RX 缓冲区为 64 字节、TX 缓冲区为 256 字节,高吞吐场景需评估是否满足需求;
- 流控限制:当前驱动版本不支持硬件流控(RTS/CTS),配置宏仍保留有默认引脚值但未参与实际收发逻辑,使用前请确认你的应用不依赖流控。
若需了解 UART 在蓝牙等具体场景中的应用,可进一步阅读 drivers/bluetooth/rn42.c 与 builddefs/common_features.mk 中与UART_DRIVER_REQUIRED相关的构建逻辑。
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考