ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

QMK 固件 I2C 主设备驱动完全指南:统一 API、地址规则与 AVR / ChibiOS 双平台配置

2026/9/13 3:53:04 拓冰建站 浏览量
QMK 固件 I2C 主设备驱动完全指南:统一 API、地址规则与 AVR / ChibiOS 双平台配置 QMK 固件 I2C 主设备驱动完全指南统一 API、地址规则与 AVR / ChibiOS 双平台配置【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmwareQMK 通过一套跨 MCU 的 I2C 主设备I2C Master统一 API让 OLED 屏幕、I2C 触摸传感器、I2C 背光灯驱动和 I2C 外部 EEPROM 等外设能在 AVR 与 ChibiOS/ARM 两大平台上共用同一份调用代码。本文基于仓库中的 I2C 驱动文档 与其源码实现展开读完后你将掌握如何启用驱动、7 位地址的移位规则、config.h的全部可覆盖参数、两套平台的引脚/时钟配置以及完整 API 的参数与返回值语义。1. 为什么需要统一的 I2C 驱动QMK 的 I2C 主设备驱动为一组通用函数目的是在 MCU 之间实现可移植性——上层功能如 OLED 屏幕驱动只需要调用 i2c_master.h 中声明的函数而无需关心底层是 AVR 的 TWI 外设还是 STM32 的 I2Cx 外设。仓库中确实存在两套平台实现AVR 平台platforms/avr/drivers/i2c_master.cChibiOS/ARM 平台platforms/chibios/drivers/i2c_master.c两套实现共享同一个头文件 drivers/i2c_master.h其中定义了状态码与超时宏typedef int16_t i2c_status_t; #define I2C_STATUS_SUCCESS (0) #define I2C_STATUS_ERROR (-1) #define I2C_STATUS_TIMEOUT (-2) #define I2C_TIMEOUT_IMMEDIATE (0) #define I2C_TIMEOUT_INFINITE (0xFFFF)从源码看文档中提到的毫秒级超时参数最终落到这两个特殊值上I2C_TIMEOUT_IMMEDIATE0表示不做等待I2C_TIMEOUT_INFINITE0xFFFF表示无限重试——AVR 实现中当 timeout 为无限时会以每 1ms 一次的节奏反复尝试 START 条件这解释了为什么调用者传入的timeout是uint16_t却可以表达永远等待。2. 启用方式通常自动包含也可手动开启文档指出在大多数情况下只要你启用了依赖 I2C 的功能或驱动如 OLEDI2C 主设备驱动代码就会被自动包含。这一机制可以在构建系统中得到证实——builddefs/common_features.mk 里大量功能分支都会设置I2C_DRIVER_REQUIRED yes例如指点设备azoteq_iqs5xx、cirque_pinnacle_i2c、pimoroni_trackball等 I2C 传感器EEPROM 驱动EEPROM_DRIVER i2c外部 I2C EEPROM会额外编译eeprom_i2c.c以及 OLED、I2C 背光灯如 IS31FL37xx 系列、SNLED27351 等功能开关。而I2C_DRIVER_REQUIRED本身的处理逻辑见 builddefs/common_features.mk是ifeq ($(strip $(I2C_DRIVER_REQUIRED)), yes) OPT_DEFS -DHAL_USE_I2CTRUE QUANTUM_LIB_SRC i2c_master.c endif也就是说该开关一方面把平台对应的i2c_master.c加入编译另一方面定义HAL_USE_I2CTRUE在 ChibiOS 上启用其 I2C HAL 子系统。如果你需要独立使用该驱动例如为自己的 I2C 外设编写用户空间代码只需在键盘的rules.mk中添加I2C_DRIVER_REQUIRED yes之后在代码中包含i2c_master.h即可调用 I2C API。3. I2C 地址规则7 位地址必须左移 1 位这是使用 QMK I2C API 时最容易踩的坑。数据手册和网络资料上列出的 I2C 地址通常是7 位值第 8 位最低位用于区分读/写操作。因此驱动 API 期望的所有地址参数都必须把 7 位地址推向地址字节的高 7 位读/写位由驱动自行设置。用位左移运算符很容易做到。如果你的设备地址是0x18可以定义一个宏方便使用#define MY_I2C_ADDRESS (0x18 1)或者预先移位好#define MY_I2C_ADDRESS 0x30这一点在两套平台实现中都能得到印证AVR 实现中写操作执行i2c_start(address | I2C_ACTION_WRITE)I2C_ACTION_WRITE为0x00、读操作执行address | I2C_ACTION_READ0x01即驱动自己负责最低位的读写方向ChibiOS 实现中调用 ChibiOS 的i2cMasterTransmitTimeout前会先做address 1因为 ChibiOS 底层期望的是标准 7 位地址。两条路径殊途同归QMK 层 API 一律接受已左移的地址。如果你传入了未移位的 7 位地址实际访问到的就是错误的总线地址。4. AVR 平台配置AVR 平台只有一个可覆盖项在config.h中定义config.h覆盖项说明默认值F_SCL时钟频率Hz400000AVR 实现 中F_SCL通过经典公式换算为 TWI 位速率寄存器值#define TWBR_val (((F_CPU / F_SCL) - 16) / 2) __attribute__((weak)) void i2c_init(void) { TWSR 0; /* no prescaler */ TWBR (uint8_t)TWBR_val; ... }注意由于未使用预分频器TWSR 0该公式成立的前提是(F_CPU / F_SCL) - 16的结果能装入 8 位TWBR对 400kHz 快速模式部分低频 F_CPU 的 AVR 器件可能无法满足此时需要改用 SPI 位模拟方式这属于该驱动未覆盖的场景。除F_SCL外AVR 实现还带有一个可配置的宏I2C_START_RETRY_COUNT默认 20发送 START 条件时若对端可能禁用了中断驱动会把总超时切成若干时间片并循环重试 START直到成功或耗尽总超时。这个重试机制提升了与忙设备通信的鲁棒性调用者无需感知。完成配置后无需再做别的——只需把 I2C 设备的SDA、SCL引脚连到 MCU 对应的物理引脚MCUSCLSDAATmega16/32U4D0D1AT90USB64/128D0D1ATmega32AC0C1ATmega328/PC5C4提示ATmega16/32U2 不具备 I2C 功能无法使用此驱动。5. ChibiOS/ARM 平台配置ARM 平台STM32 等需要你确定哪组引脚可用于 I2C——例如 STM32 器件通常有多个 I2C 外设标注为 I2C1、I2C2、I2C3 等。配置分三步5.1 在 halconf.h 中启用 I2C修改板级的halconf.h启用 I2C HAL 子系统#pragma once #define HAL_USE_I2C TRUE #include_next halconf.h5.2 在 mcuconf.h 中启用所选外设修改板级的mcuconf.h启用你选择的 I2C 外设以 I2C2 为例#pragma once #include_next mcuconf.h #undef STM32_I2C_USE_I2C2 #define STM32_I2C_USE_I2C2 TRUEmcuconf.h的相关设置项mcuconf.h设置说明默认值STM32_I2C_BUSY_TIMEOUT若未收到响应I2C 命令中止前的等待时间毫秒50STM32_I2C_XXX_IRQ_PRIORITY硬件驱动 XXX 的中断优先级专家级设置10STM32_I2C_USE_DMA启用/禁用 MCU 将数据传输卸载给 DMA 单元的能力TRUESTM32_I2C_XXX_DMA_PRIORITY硬件驱动 XXX 的 DMA 单元优先级专家级设置15.3 在 config.h 中配置引脚与外设按 MCU 数据手册配置外设引脚——默认值对应 Proton-C即 STM32F303的引脚config.h覆盖项说明默认值I2C_DRIVER要使用的 I2C 外设——I2C1 →I2CD1I2C2 →I2CD2等I2CD1I2C1_SCL_PINSCL 使用的引脚B6I2C1_SCL_PAL_MODESCL 的复用功能alternate function模式4I2C1_SDA_PINSDA 使用的引脚B7I2C1_SDA_PAL_MODESDA 的复用功能模式4提示目前只支持单一 I2C 外设因此无论选择哪个外设配置宏前缀都是I2C1_*。这与 ChibiOS 实现 中的默认值完全一致I2C_DRIVER默认为I2CD1I2C1_SCL_PIN/I2C1_SDA_PIN默认B6/B7PAL_MODE默认4在USE_GPIOV1的旧版 ChibiOS 上则退化为PAL_MODE_ALTERNATE_OPENDRAIN。此外i2c_init()会先把两根引脚短暂设置为输入模式并延时 10ms释放总线再以开漏输出open-drain 复用模式重新配置这是 I2C 总线电气特性开漏 上拉的体现。5.4 I2Cv1 时序配置I2Cv1 结构体适用系列STM32F1xx、STM32F2xx、STM32F4xx、STM32L0xx、STM32L1xx。config.h覆盖项默认值I2C1_OPMODEOPMODE_I2CI2C1_CLOCK_SPEED100000I2C1_DUTY_CYCLESTD_DUTY_CYCLE对应源码中的I2CConfig构造platforms/chibios/drivers/i2c_master.c默认OPMODE_I2C标准模式、100kHz 时钟、标准占空比注释中给出了可改为FAST_DUTY_CYCLE_2与 400kHz 的可选值说明快速模式可通过覆盖这些宏开启。5.5 I2Cv2 时序配置I2Cv2/I2Cv3 结构体适用系列STM32F0xx、STM32F3xx、STM32F7xx、STM32L4xx。config.h覆盖项默认值I2C1_TIMINGR_PRESC0UI2C1_TIMINGR_SCLDEL7UI2C1_TIMINGR_SDADEL0UI2C1_TIMINGR_SCLH38UI2C1_TIMINGR_SCLL129U这组 TIMINGR 值与源码中的注释一致默认时序值在 72MHz 系统时钟假设下把 I2C 时钟配置为 400kHzplatforms/chibios/drivers/i2c_master.c。如果你的板子系统时钟不是 72MHz需要按 STM32 I2C 配置计算器重新计算这五个值。6. API 参考所有函数都声明在 drivers/i2c_master.h返回值统一为i2c_status_tI2C_STATUS_TIMEOUT超时、I2C_STATUS_ERROR其他错误、否则I2C_STATUS_SUCCESS。6.1void i2c_init(void)初始化 I2C 驱动。该函数必须且只能调用一次之后才能调用下列任何函数。该函数是**弱定义weak**的必要时可以按你的具体用途覆盖。一个典型的覆盖示例文档原文用于先释放引脚再重新配置为 I2C 复用功能void i2c_init(void) { gpio_set_pin_input(B6); // Try releasing special pins for a short time gpio_set_pin_input(B7); wait_ms(10); // Wait for the release to happen palSetPadMode(GPIOB, 6, PAL_MODE_ALTERNATE(4) | PAL_STM32_OTYPE_OPENDRAIN | PAL_STM32_PUPDR_PULLUP); // Set B6 to I2C function palSetPadMode(GPIOB, 7, PAL_MODE_ALTERNATE(4) | PAL_STM32_OTYPE_OPENDRAIN | PAL_STM32_PUPDR_PULLUP); // Set B7 to I2C function }注意两套平台自带的弱定义i2c_init()AVR配置TWBR/TWENChibiOS释放引脚 10ms 后设为开漏复用模式在行为上各不相同覆盖时要与你的平台实现保持同语义。6.2i2c_transmit/i2c_transmit_P向选定 I2C 设备发送多个字节i2c_status_t i2c_transmit(uint8_t address, const uint8_t* data, uint16_t length, uint16_t timeout);参数uint8_t address——设备的 7 位 I2C 地址按第 3 节规则已左移const uint8_t* data——要传输的数据指针uint16_t length——要写入的字节数注意不要超出data的实际长度uint16_t timeout——等待目标设备响应的毫秒数。返回值超时期满返回I2C_STATUS_TIMEOUT其他错误返回I2C_STATUS_ERROR否则I2C_STATUS_SUCCESS。i2c_transmit_P与i2c_transmit参数相同区别在于数据来自 PROGMEM程序存储器。在 AVR 上实现用pgm_read_byte逐字节读取 flash 数据在 ARM 设备上它只是一个宏别名#define i2c_transmit_P(address, data, length, timeout) i2c_transmit(address, data, length, timeout)见 drivers/i2c_master.h6.3i2c_receive从选定 I2C 设备接收多个字节i2c_status_t i2c_receive(uint8_t address, uint8_t* data, uint16_t length, uint16_t timeout);参数uint8_t address——设备的 7 位 I2C 地址uint8_t* data——接收缓冲区指针uint16_t length——要读取的字节数注意不要超出缓冲区uint16_t timeout——等待响应的毫秒数。从 AVR 实现platforms/avr/drivers/i2c_master.c可以看到协议细节前length - 1个字节以ACK方式接收最后一个字节以NACK方式接收随后发出 STOP——这正是 I2C 读传输的标准收尾方式调用者无需手动处理。6.4i2c_transmit_and_receive先发送再接收即 I2C 的重复起始repeated start组合传输i2c_status_t i2c_transmit_and_receive(uint8_t address, const uint8_t* tx_data, uint16_t tx_length, uint8_t* rx_data, uint16_t rx_length, uint16_t timeout);参数uint8_t address——设备的 7 位 I2C 地址const uint8_t* tx_data/uint16_t tx_length——要写出的数据及字节数uint8_t* rx_data/uint16_t rx_length——接收缓冲区及要读回的字节数uint16_t timeout——等待响应的毫秒数。ChibiOS 实现把它直接映射到i2cMasterTransmitTimeout的传收一体调用AVR 实现则在一次 START 后先写后读、以 NACK 收尾并 STOP语义与文档一致。6.5 寄存器读写i2c_write_register/i2c_read_register8 位寄存器地址i2c_status_t i2c_write_register(uint8_t devaddr, uint8_t regaddr, const uint8_t* data, uint16_t length, uint16_t timeout); i2c_status_t i2c_read_register(uint8_t devaddr, uint8_t regaddr, uint8_t* data, uint16_t length, uint16_t timeout);参数uint8_t devaddr——设备的 7 位 I2C 地址uint8_t regaddr——要写入/读取的寄存器地址const uint8_t* data写/uint8_t* data读——数据或接收缓冲区uint16_t length——字节数注意不要越界uint16_t timeout——等待响应的毫秒数。读寄存器8 位地址的实现遵循写寄存器指针 重复起始 读数据流程见 AVR 实现先i2c_start(devaddr)写regaddr再i2c_start(devaddr | 0x01)发起读逐字节 ACK、末字节 NACK。6.6 寄存器读写i2c_write_register16/i2c_read_register1616 位寄存器地址大端i2c_status_t i2c_write_register16(uint8_t devaddr, uint16_t regaddr, const uint8_t* data, uint16_t length, uint16_t timeout); i2c_status_t i2c_read_register16(uint8_t devaddr, uint16_t regaddr, uint8_t* data, uint16_t length, uint16_t timeout);参数同 6.5 节只是regaddr变为uint16_t。16 位寄存器地址按大端拆分传输先发高字节regaddr 8再发低字节regaddr 0xFF——两套平台实现AVR 与 ChibiOS在这一点上行为一致ChibiOS 端是先把高、低字节拼进发送包再一次性i2cMasterTransmitTimeout。6.7i2c_ping_address探测 I2C 总线上某个地址是否存在设备i2c_status_t i2c_ping_address(uint8_t address, uint16_t timeout);参数uint8_t address——设备的 7 位 I2C 地址uint16_t timeout——等待响应的毫秒数。返回值同上三种状态。该函数同样是弱定义的可以被覆盖。两个平台的行为差异值得注意AVRi2c_ping_address直接发送 START 地址并检查从设备 ACK这是标准的地址探测ChibiOS由于 ChibiOS 的 I2C 层没有足够底层的控制权去检查 ACK采用尽力而为的策略——从给定地址的寄存器 0 读 1 个字节见 platforms/chibios/drivers/i2c_master.c。对绝大多数设备有效但对不响应寄存器 0 读请求的 I2C 设备会产生假阴性即实际存在却被判定不存在。如果你的目标设备属于后者应覆盖这个弱函数。7. 典型使用场景与验证路径文档提到 OLED 是最常见的自动启用 I2C 的功能。仓库中的 drivers/oled/oled_driver.c 以及大量 LED 驱动drivers/led/issi/ 下的 IS31FL37xx 系列、drivers/led/snled27351.c、I2C 触摸/指点设备drivers/sensors/azoteq_iqs5xx.c、drivers/sensors/pimoroni_trackball.c、drivers/sensors/cirque_pinnacle_i2c.c都通过本文第 6 节的这套 API 访问外设——它们各自关心自己的寄存器协议而 START/STOP、ACK 处理、超时与总线恢复则完全交给平台层i2c_master.c。排查 I2C 问题的实用顺序基于上述源码事实确认I2C_DRIVER_REQUIRED yes生效功能自动包含或手动设置对应i2c_master.c已被编译进固件确认地址已左移 1 位第 3 节AVR 平台核对F_SCL与物理引脚表ChibiOS 平台核对halconf.h/mcuconf.h/config.h三层设置用i2c_ping_address做总线探测并留意 ChibiOS 上寄存器 0 读探测的假阴性限制所有失败路径都返回明确的I2C_STATUS_TIMEOUT/I2C_STATUS_ERROR可据此区分无响应与协议错误。ChibiOS 端在出错时会调用i2cStop硬停止外设因为按 ChibiOS HAL 的说法超时之后总线处于不确定状态驱动必须停止并重启见 platforms/chibios/drivers/i2c_master.c下一次调用会重新i2cStart即该实现具备错误后的自恢复能力。8. 要点小结QMK 的 I2C 主设备驱动是一套 API、两套平台实现的可移植层头文件为 drivers/i2c_master.hAVR/ChibiOS 实现分别位于platforms/avr/drivers/i2c_master.c与platforms/chibios/drivers/i2c_master.c独立启用只需I2C_DRIVER_REQUIRED yes多数功能OLED、I2C 传感器、I2C EEPROM 等会自动带上所有 API 地址参数都是已左移 1 位的 7 位地址读写方向位由驱动处理AVR 侧唯一常规配置是F_SCL默认 400kHz引脚固定ChibiOS 侧需要三层配置halconf.h、mcuconf.h、config.h引脚与时序默认值对应 Proton-C/STM32F303 的 B6/B7i2c_init与i2c_ping_address为弱定义函数可按需覆盖超时参数支持I2C_TIMEOUT_IMMEDIATE与I2C_TIMEOUT_INFINITE两个特殊值。【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考