尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QMK Firmware 持久化配置(EEPROM)完全指南:从 eeconfig 基础 API 到 Datablock 数据块

QMK Firmware 持久化配置(EEPROM)完全指南:从 eeconfig 基础 API 到 Datablock 数据块 QMK Firmware 持久化配置EEPROM完全指南从 eeconfig 基础 API 到 Datablock 数据块【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware导读本文以 QMK Firmware 官方文档 docs/feature_eeprom.md 为核心骨架系统讲解如何利用主控芯片的 EEPROM 实现键盘固件的持久化配置——即配置在断电后依然保留。你将掌握eeconfig_read_*/eeconfig_update_*基础 API 的用法、基于 union 位域的经典配置读写模式以及可分配更大存储空间的 Datablock 扩展 API并结合 quantum/eeconfig.c、quantum/eeconfig.h 与 drivers/eeprom 驱动源码理解这套机制从键位映射层一路下沉到 EEPROM 硬件驱动的完整调用链最终能在自己的 keymap 中实现“可记忆、可切换、可重置”的个性化功能如按层切换 RGB 指示灯颜色。EEPROM 与固件配置的基本概念EEPROMElectrically Erasable Programmable Read-Only Memory是一种断电后数据不丢失的存储介质非常适合保存键盘的“用户偏好”类配置例如默认层、按键映射、RGB 效果、NKRO 开关、音频模式等。在 QMK 中这套能力被封装为eeconfig 系统入口统一在quantum/eeconfig.h与quantum/eeconfig.c。核心设计是配置数据存储在 EEPROM 中断电后保留系统自带一套内部使用的配置区debug 开关、默认层、keymap 配置、RGB/音频等各功能模块的配置在系统配置区之外为键盘keyboard和用户键位映射keymap/user两个层级各保留一块私有数据供开发者存放自定义设置。必须重视的写入寿命问题EEPROM 的写入次数虽然很高但并非无限而且它不是唯一的写入者。文档明确提醒EEPROM has a limited number of writes. While this is very high, its not the only thing writing to the EEPROM, and if you write too often, you can potentially drastically shorten the life of your MCU.也就是说如果在你自己的逻辑里高频调用写入函数比如在每次按键扫描、每个housekeeping_task_*周期里都写入配置会显著缩短主控寿命。正确做法是只在配置真正发生变化时写入如按键切换开关、修改设置并且避免在循环热路径中反复写。数据有效性标记Magic NumberEEPROM 中的数据需要区分“已初始化”与“随机/未初始化”状态。QMK 通过魔术数字实现这一点见 quantum/nvm/nvm_eeconfig.h#ifndef EECONFIG_MAGIC_NUMBER # define EECONFIG_MAGIC_NUMBER (uint16_t)0xFEE3 // When changing, decrement this value to avoid future re-init issues #endif #define EECONFIG_MAGIC_NUMBER_OFF (uint16_t)0xFFFF而 quantum/nvm/eeprom/nvm_eeconfig.c 则通过读写该标记来判断 EEPROM 是否已被启用/禁用bool nvm_eeconfig_is_enabled(void) { return eeprom_read_word(EECONFIG_MAGIC) EECONFIG_MAGIC_NUMBER; } bool nvm_eeconfig_is_disabled(void) { return eeprom_read_word(EECONFIG_MAGIC) EECONFIG_MAGIC_NUMBER_OFF; } void nvm_eeconfig_enable(void) { eeprom_update_word(EECONFIG_MAGIC, EECONFIG_MAGIC_NUMBER); }一旦检测到 EEPROM 尚未初始化magic 不匹配QMK 会调用eeconfig_init_quantum()定义于 quantum/eeconfig.c重新写入默认配置。Basic API每个层级各 4 字节的自定义配置这是 eeconfig 最基础、最经典的使用方式。官方 API 分为键盘keyboard/revision与用户keymap两级Keyboard/Revision 层void eeconfig_init_kb(void)、uint32_t eeconfig_read_kb(void)、void eeconfig_update_kb(uint32_t val)Keymap 层void eeconfig_init_user(void)、uint32_t eeconfig_read_user(void)、void eeconfig_update_user(uint32_t val)其中val是你要写入 EEPROM 的数据eeconfig_read_*返回一个 32 位DWORD4 字节的值。文档特别指出there are a bunch of ways that you can store and access data via EEPROM, and there is no correct way to do this. However, you only have a DWORD (4 bytes) for each function.也就是说基础 API 每个层级只有 4 字节可用如何在这 4 字节里编排字段完全由你决定——这正是下面 union 位域方案存在的原因。从源码看这组 API 在 quantum/eeconfig.c 中实现且仅在未启用 Datablock 时可用#if (EECONFIG_USER_DATA_SIZE) 0它们最终转发到 nvm 层的nvm_eeconfig_read_user()/nvm_eeconfig_update_user()。实战用 union 位域实现可持久化的开关RGB 层指示官方文档给出了一个非常典型的完整示例在 keymap 中保存一个rgb_layer_change开关开启后按下不同层RAISE/LOWER/PLOVER/ADJUST时自动切换 RGB 灯色关闭后 RGB 则作为普通效果使用且该开关在拔电后依然保留。我们完整复现如下。第 1 步在keymap.c顶部定义 union 结构typedef union { uint32_t raw; struct { bool rgb_layer_change :1; }; } user_config_t; user_config_t user_config;要点union让整个结构既可以作为一个 32 位raw值整体读写也可以按位域访问各字段位域声明的顺序决定了其在raw中的位布局改变顺序会导致读写值错位因此一旦发布就不要轻易调整位宽参考bool占 1 bituint8_t占 8 bitsuint16_t占 16 bits可以混合排列但要保证总位宽不超过 32 位。第 2 步在keyboard_post_init_user()中读取配置并立即生效void keyboard_post_init_user(void) { // Call the keymap level matrix init. // Read the user config from EEPROM user_config.raw eeconfig_read_user(); // Set default layer, if enabled if (user_config.rgb_layer_change) { rgblight_enable_noeeprom(); rgblight_sethsv_noeeprom(HSV_CYAN); rgblight_mode_noeeprom(1); } }这里user_config.raw eeconfig_read_user()把 EEPROM 中存储的 32 位原值装载进 union之后就能用user_config.rgb_layer_change这样的语义化字段直接读取。注意这里特意使用了*_noeeprom版本的 RGB 函数避免初始化阶段再次写 EEPROM。第 3 步在layer_state_set_user()中按层切换颜色layer_state_t layer_state_set_user(layer_state_t state) { switch (get_highest_layer(state)) { case _RAISE: if (user_config.rgb_layer_change) { rgblight_sethsv_noeeprom(HSV_MAGENTA); rgblight_mode_noeeprom(1); } break; case _LOWER: if (user_config.rgb_layer_change) { rgblight_sethsv_noeeprom(HSV_RED); rgblight_mode_noeeprom(1); } break; case _PLOVER: if (user_config.rgb_layer_change) { rgblight_sethsv_noeeprom(HSV_GREEN); rgblight_mode_noeeprom(1); } break; case _ADJUST: if (user_config.rgb_layer_change) { rgblight_sethsv_noeeprom(HSV_WHITE); rgblight_mode_noeeprom(1); } break; default: // for any other layers, or the default layer if (user_config.rgb_layer_change) { rgblight_sethsv_noeeprom(HSV_CYAN); rgblight_mode_noeeprom(1); } break; } return state; }每个 case 分支都先判断user_config.rgb_layer_change只有该开关被启用时才改写 RGB 颜色从而保证“层指示”与“普通 RGB 模式”互不干扰。第 4 步在process_record_user()中新增开关键RGB_LYRbool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case FOO: if (record-event.pressed) { // Do something when pressed } else { // Do something else when release } return false; // Skip all further processing of this key case KC_ENTER: // Play a tone when enter is pressed if (record-event.pressed) { PLAY_SONG(tone_qwerty); } return true; // Let QMK send the enter press/release events case RGB_LYR: // This allows me to use underglow as layer indication, or as normal if (record-event.pressed) { user_config.rgb_layer_change ^ 1; // Toggles the status eeconfig_update_user(user_config.raw); // Writes the new status to EEPROM if (user_config.rgb_layer_change) { // if layer state indication is enabled, layer_state_set(layer_state); // then immediately update the layer color } } return false; case RGB_MODE_FORWARD ... RGB_MODE_GRADIENT: // For any of the RGB codes (see quantum_keycodes.h, L400 for reference) if (record-event.pressed) { //This disables layer indication, as its assumed that if youre changing this ... you want that disabled if (user_config.rgb_layer_change) { // only if this is enabled user_config.rgb_layer_change false; // disable it, and eeconfig_update_user(user_config.raw); // write the settings to EEPROM } } return true; break; default: return true; // Process all other keycodes normally } }这段代码展示了 eeconfig 的经典写入模式按RGB_LYR时用异或翻转开关位并立即调用eeconfig_update_user(user_config.raw)把新值写回 EEPROM只要用户手动调整了任何标准 RGB 键码RGB_MODE_FORWARD ... RGB_MODE_GRADIENT区间就自动把层指示开关关闭确保普通 RGB 效果不再被层切换打断自定义键码返回false阻止进一步处理RGB 键码则返回true交给 QMK 正常处理。第 5 步在eeconfig_init_user()中设置默认值void eeconfig_init_user(void) { // EEPROM is getting reset! user_config.raw 0; user_config.rgb_layer_change true; // We want this enabled by default eeconfig_update_user(user_config.raw); // Write default value to EEPROM now // use the non noeeprom versions, to write these values to EEPROM too rgblight_enable(); // Enable RGB by default rgblight_sethsv(HSV_CYAN); // Set it to CYAN by default rgblight_mode(1); // set to solid by default }eeconfig_init_user()在EEPROM 被重置重新初始化时调用用于写入出厂默认值。这里刻意使用不带_noeeprom的rgblight_*版本把默认 RGB 状态也一并持久化。从源码看eeconfig_init_user()与eeconfig_init_kb()都是__attribute__((weak))弱符号见 quantum/eeconfig.c默认实现只是把用户/键盘的 32 位数据清零一旦你在 keymap 或键盘代码中重新定义同名函数就会覆盖默认实现。这也解释了为什么“在 EEPROM 重置时自定义默认值”只需在用户层定义一个同名函数即可。如何触发 EEPROM 重置文档明确给出两种方式使用EE_CLR键码在 keymap 中放置EE_CLR按下后 QMK 会擦除 EEPROM 并重新初始化触发eeconfig_init_*系列函数使用Bootmagic 功能按住特定键上电如按住左上角键等效于在启动时执行 EEPROM 重置。重置完成后你在eeconfig_init_user()/eeconfig_init_kb()中定义的默认值会被写入。整套流程最终汇入eeconfig_init_quantum()见 quantum/eeconfig.c它会依次擦除 EEPROM、写入 magic 标记、初始化 debug / 默认层 / keymap 配置并在最后调用eeconfig_init_kb()。Datablock突破 4 字节限制的扩展数据块基础 API 每个层级只有 4 字节显然无法满足更复杂的需求如保存多个配置结构体。为此 QMK 提供了Datablock 扩展形式允许分配更大的连续数据块。注意一旦启用 Datablock基础 APIeeconfig_read_user/eeconfig_update_user等将不可用编译期由#if (EECONFIG_USER_DATA_SIZE) 0条件控制见 quantum/eeconfig.h 与 quantum/eeconfig.h。配置宏Keyboard 层在config.h中定义DefineDefaultDescriptionEECONFIG_KB_DATA_SIZE0持久化数据块的大小字节EECONFIG_KB_DATA_VERSIONEECONFIG_KB_DATA_SIZE版本号递增后可令旧存储数据失效Keymap 层在config.h中定义DefineDefaultDescriptionEECONFIG_USER_DATA_SIZE0持久化数据块的大小字节EECONFIG_USER_DATA_VERSIONEECONFIG_USER_DATA_SIZE版本号递增后可令旧存储数据失效暴露的 APIKeyboard 层bool eeconfig_is_kb_datablock_valid(void); uint32_t eeconfig_read_kb_datablock(void *data, uint32_t offset, uint32_t length) __attribute__((nonnull)); uint32_t eeconfig_update_kb_datablock(const void *data, uint32_t offset, uint32_t length) __attribute__((nonnull)); void eeconfig_init_kb_datablock(void); # define eeconfig_read_kb_datablock_field(__object, __field) eeconfig_read_kb_datablock((__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field)) # define eeconfig_update_kb_datablock_field(__object, __field) eeconfig_update_kb_datablock((__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))Keymap 层bool eeconfig_is_user_datablock_valid(void); uint32_t eeconfig_read_user_datablock(void *data, uint32_t offset, uint32_t length) __attribute__((nonnull)); uint32_t eeconfig_update_user_datablock(const void *data, uint32_t offset, uint32_t length) __attribute__((nonnull)); void eeconfig_init_user_datablock(void); # define eeconfig_read_user_datablock_field(__object, __field) eeconfig_read_user_datablock((__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field)) # define eeconfig_update_user_datablock_field(__object, __field) eeconfig_update_user_datablock((__object.__field), offsetof(typeof(__object), __field), sizeof(__object.__field))API 语义说明eeconfig_*_is_*_datablock_valid()校验数据块是否有效与 magic/版本号机制相关eeconfig_read_*_datablock(data, offset, length)从偏移offset处读取length字节到data缓冲区eeconfig_update_*_datablock(data, offset, length)把data中的length字节写入偏移offset处返回实际写入的字节数eeconfig_init_*_datablock()初始化清零/写入默认数据块末尾两个宏是便捷封装基于offsetof自动计算结构体某字段的偏移与长度便于直接读写结构体中的单个字段无需手工计算偏移。这些函数在 quantum/eeconfig.c 中实现同样通过弱符号 转发方式最终落到nvm_eeconfig_*层见 quantum/nvm/nvm_eeconfig.h。Datablock 完整示例官方文档提供了一个计数器示例配置一个 8 字节的数据块每次松开任意键就累加并写回同时在housekeeping_task_user中每秒打印一次当前值。完整复现如下。在config.h中添加#define EECONFIG_USER_DATA_SIZE 8在keymap.c中添加#include debug.h #include timer.h #include eeconfig.h typedef struct my_config_t { uint64_t data; } my_config_t; static my_config_t config; void keyboard_post_init_user(void) { if (!eeconfig_is_user_datablock_valid()) { eeconfig_init_user_datablock(); } eeconfig_read_user_datablock(config, 0, sizeof(my_config_t)); } bool process_record_user(uint16_t keycode, keyrecord_t *record) { if (!record-event.pressed) { config.data 1; eeconfig_update_user_datablock(config, 0, sizeof(my_config_t)); } return true; } void housekeeping_task_user(void) { static uint32_t last_sync 0; if (timer_elapsed32(last_sync) 1000) { last_sync timer_read32(); dprintf(Config: %ld\n, config.data); } }这个示例揭示了 Datablock 的标准使用模式上电后先通过eeconfig_is_user_datablock_valid()判断数据块是否有效无效则调用eeconfig_init_user_datablock()初始化再整体读入config结构体每次按键松开时修改内存中的结构体并整体写回注意在process_record_user热路径中每次按键只写一次不会高频刷写利用housekeeping_task_user周期性输出调试日志验证读写结果。如果数据块中的数据结构需要携带版本信息可以把EECONFIG_USER_DATA_VERSION递增令旧的存储数据失效被判定为 invalid 后重新初始化避免升级固件后读到布局不兼容的旧数据。底层实现与调用链从 API 到硬件驱动要真正理解 eeconfig建议顺着调用链读一遍源码这条链清晰地展示了 QMK 的分层设计键位层调用keymap 中调用eeconfig_read_user()/eeconfig_update_user()/eeconfig_*_datablock()统一封装层quantum/eeconfig.c 中的同名函数只做转发并负责在eeconfig_init_quantum()中完成整体初始化写入 magic、默认层、keymap 配置、各功能模块默认值等NVM 抽象层quantum/nvm/eeprom/nvm_eeconfig.c 把各类配置映射为对eeprom_read_byte/eeprom_update_word/eeprom_read_block等底层原语的读写例如 debug 配置 1 字节、keymap 配置 1 个 wordEEPROM 驱动层drivers/eeprom 目录下的驱动负责与具体硬件打交道——可以是芯片内置 EEPROM/Flash 仿真vendor、外挂 I2C 24xx 芯片、SPI 25xx 芯片、RAM 临时存储transient或基于磨损均衡wear_leveling的 Flash 模拟。以 wear_leveling 驱动为例drivers/eeprom/eeprom_wear_leveling.c 将eeprom_read_block/eeprom_write_block直接映射为wear_leveling_read/wear_leveling_write用算法最小化底层 Flash 的擦写次数而nvm_eeconfig_erase()则通过eeprom_driver_format(false)触发整体擦除见 quantum/nvm/eeprom/nvm_eeconfig.c。EEPROM 驱动选型与配置速查eeconfig 并不强制指定底层 EEPROM 硬件。根据 docs/drivers/eeprom.md在键盘的rules.mk中通过EEPROM_DRIVER选择驱动驱动说明vendor默认使用芯片厂商提供的片上驱动。AVR 由 avr-libc 提供ARM 中 STM32F3xx/F1xx/F072xB 通过写 Flash 模拟STM32L0xx/L1xx 使用片内真 EEPROM其余芯片通常退化为 transient 行为i2c支持 I2C 接口的 24xx 系列 EEPROM 芯片spi支持 SPI 接口的 25xx 系列 EEPROM 芯片transient假 EEPROM读写 RAM断电即丢失适合调试wear_leveling磨损均衡前端驱动可在片内 Flash 或外部 SPI NOR Flash 上模拟 EEPROM其中transient可用#define TRANSIENT_EEPROM_SIZE默认 64 字节配置大小wear_leveling需要额外选择WEAR_LEVELING_DRIVERembedded_flash/spi_flash/rp2040_flash/legacy其实现位于 drivers/eeprom/eeprom_transient.c 与 wear_leveling 相关源码中。需要提醒的是所有 wear_leveling 驱动都要求 RAM 容量与所选逻辑 EEPROM 大小相当——若把 EEPROM 扩展到 32kB就需要 32kB RAM很多 MCU 并不具备。因此设计 Datablock 大小时除了受EECONFIG_*_DATA_SIZE宏约束还要综合考虑所用 EEPROM 驱动与主控资源。总结与最佳实践围绕 docs/feature_eeprom.md本文完整覆盖了 QMK 持久化配置的两套 API基础 APIDWORD 模式每个层级 4 字节适合少量开关类配置用 union 位域组织字段读写raw值即可示例中的 RGB 层指示功能就是最典型的落地场景Datablock 扩展通过EECONFIG_KB_DATA_SIZE/EECONFIG_USER_DATA_SIZE及可选*_VERSION分配更大的连续存储配套eeconfig_*_datablock系列函数与offsetof字段宏适合保存结构体级别的复杂配置且与基础 API 互斥。实战中请牢记几条原则只在配置变化时写入以减少 EEPROM 磨损保持位域字段顺序稳定用EE_CLR键码或 Bootmagic 触发重置并善用eeconfig_init_user()/eeconfig_init_kb()定义默认值对 Datablock 先做有效性校验再读取最后根据硬件选型vendor / i2c / spi / transient / wear_leveling配置合适的EEPROM_DRIVER从而让你的键盘固件拥有稳定、耐用、断电不丢失的个性化记忆能力。【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表