
QMK 固件 Userspace 完全指南在多个键盘 Keymap 之间共享代码【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware导读QMK Firmware 的 Userspace用户空间机制允许你为多个键盘的 Keymap 建立一份共享代码目录把重复的层切换逻辑、RGB 控制、宏定义、编译辅助工具统一维护在users/name/下从而实现一处编写、多键盘复用。本文将基于 docs/feature_userspace.md 完整讲解 Userspace 的目录结构、rules.mk与config.h的自动处理机制、USER_NAME覆盖技巧、_quantum/_kb/_user自定义函数链与弱符号weak symbol实现并结合本仓库源码builddefs/build_keyboard.mk、users/_example印证其底层原理。读完本文你将能搭建自己的 Userspace并在任意键盘上通过make keyboard:name一键复用。注意上游qmk/qmk_firmware仓库已不再接受新的 Userspace 提交但该功能本身仍然可用可在本地自行配置使用见 docs/feature_userspace.md 开头警告。一、Userspace 目录结构以 Keymap 名字为入口如果你有多把布局相似的键盘可以在users/下创建与 Keymap 同名的目录建议直接用你的 GitHub 用户名name其推荐结构如下/users/name/该目录会自动加入编译搜索路径readme.md可选推荐rules.mk自动包含config.h自动包含name.h可选name.c可选cool_rgb_stuff.c可选自定义功能源文件cool_rgb_stuff.h可选本仓库自带的示例目录 users/_example 正是这一结构的缩影_example.c、_example.h、readme.md与rules.mk四个文件各司其职。其中rules.mk内容仅为SRC _example.c而 users/readme.md 也明确说明如果你构建名为mine的 Keymap/users/mine/rules.mk会被包含进构建/users/mine/会进入头文件搜索路径——这也是命名与引用文件时需要牢记的两点。触发条件只有在构建名为name的 Keymap 时这一切才生效make planck:name # 例如 make planck:jack执行make planck:jack会把/users/jack/目录加入路径并包含/users/jack/rules.mk。从源码看这一机制的核心实现在 builddefs/build_keyboard.mk#L410-L429构建系统首先以USER_NAME定位USER_PATH : users/$(USER_NAME)随后# Pull in user level rules.mk -include $(USER_PATH)/rules.mk ifneq ($(wildcard $(USER_PATH)/config.h),) CONFIG_H $(USER_PATH)/config.h endif ifneq ($(wildcard $(USER_PATH)/post_config.h),) POST_CONFIG_H $(USER_PATH)/post_config.h endif同时builddefs/build_keyboard.mk#L489 通过VPATH $(USER_PATH)把用户目录加入编译器搜索路径使得name.h可以被各 Keymap 直接#include。二、Rules.mk向构建加入源文件rules.mk是两个自动处理的文件之一用于向编译流程添加额外的源文件例如name.c。强烈建议以name.c作为默认源文件并通过SRC加入SRC name.c其他文件可以按同样方式添加但建议先以name.c/name.h作为起点。加载顺序/users/name/rules.mk会在 Keymap 自身的rules.mk之后被包含。这允许你在 Userspace 的rules.mk中编写依赖具体 QMK 特性的条件逻辑——因为此时各特性的开关如RGBLIGHT_ENABLE已经确定。例如只想在多把支持 RGB 的键盘上加入共享 RGB 控制代码ifeq ($(strip $(RGBLIGHT_ENABLE)), yes) # Include my fancy rgb functions source here SRC cool_rgb_stuff.c endif或者你也可以在 Keymap 的rules.mk中定义自定义变量再到 Userspace 的rules.mk中判断# Keymap 的 rules.mk RGB_ENABLE yes # Userspace 的 rules.mk ifdef RGB_ENABLE # Include my fancy rgb functions source here SRC cool_rgb_stuff.c endif这两种方式分别适用于跟随键盘固有能力与由 Keymap 显式声明两种场景。覆盖默认 UserspaceUSER_NAME默认情况下Userspace 名与 Keymap 名相同。但在某些场景下这并不合适例如使用 feature_layouts 功能时不同布局如 ANSI 与 ISO的 Keymap 不能同名。此时可以把布局命名为mylayout-ansi、mylayout-iso并在布局的rules.mk中指定USER_NAME : mylayout这样make keyboard:mylayout-ansi与make keyboard:mylayout-iso会共用users/mylayout/下的同一份 Userspace 代码。这一技巧同样适用于多把键盘虽然 Keymap 同名但板上硬件能力不同例如一把有 RGB、另一把只有 Audio或 LED 数量不同、灯效引脚不同。通过USER_NAME指向不同的 Userspace 目录即可在同一 Keymap 名下挂载不同的共享代码。从源码看默认值由 builddefs/build_keyboard.mk#L411-L414 决定ifeq ($(USER_NAME),) USER_NAME : $(KEYMAP) endif USER_PATH : users/$(USER_NAME)即只要在 Keymap或布局的rules.mk中提前定义了USER_NAME构建系统就会优先使用它定位 Userspace 目录。三、Configuration Optionsconfig.h全局配置的落点Userspace 的config.h会像 Keymap 目录中的同名文件一样被处理且与name.h是两套独立机制见 builddefs/build_keyboard.mk#L430-L432 中CONFIG_H $(USER_PATH)/config.h的合并逻辑。这样设计的原因是name.h被加入编译的时间点太晚来不及提供配置宏例如#define TAPPING_TERM 100而如果在任何config.h中#include name.h又会引发编译问题。经验法则config.h用于存放 configuration options如TAPPING_TERM、MOUSEKEY_ENABLE相关设置等name.h用于存放用户或 Keymap 专属设置如层号 enum、自定义键码 enum。四、Readmereadme.md署名与文档化readme.md用于记录作者信息姓名、GitHub 用户名、邮箱并建议附带 GPL 兼容许可证。文档提供了标准模板Copyright year name email github_username及 GPL v2-or-later 全文见 docs/feature_userspace.md 与 users/_example/readme.md。只需替换年份、姓名、邮箱与 GitHub 用户名即可。此外这也是记录你的代码用法、方便他人了解你的共享实现的好地方。五、一次性构建所有支持该 Keymap 的键盘想在一条命令里验证所有键盘都能编译你的 Keymap运行make all:name # 例如 make all:jack这在准备 Pull Request、确保所有目标平台编译通过时非常理想。make all:name会遍历仓库内所有键盘目录为每一把键盘尝试构建名为name的 Keymap从而快速暴露某把键盘不满足某特性前提之类的兼容性问题。六、自定义函数在 Userspace 与 Keymap 之间接力QMK 提供了大量具有_quantum、_kb、_user三个层级版本的函数见 custom_quantum_functions 中关于 core vs keyboard vs keymap 的说明。绝大多数情况下你希望使用_user版本——但问题在于如果_user版本在 Userspace 中定义了Keymap 中就没有可再覆写的入口了。解决方案利用 C 的弱符号__attribute__ ((weak))机制在 Userspace 中提供接力函数让 Keymap 仍可覆写。例如下面的代码在所有键盘上启用 Tri Layer State层 2、3 同时按下时跳转到层 5同时保留 Keymap 中的 Tri Layer 自定义逻辑。在name.c中__attribute__ ((weak)) layer_state_t layer_state_set_keymap (layer_state_t state) { return state; } layer_state_t layer_state_set_user (layer_state_t state) { state update_tri_layer_state(state, 2, 3, 5); return layer_state_set_keymap (state); }__attribute__ ((weak))告诉编译器这是一个占位函数可以被keymap.c中的同名版本替换你不必在每个 Keymap 里都写它但如果写了也不会因函数重名而产生冲突。后缀名不必是_keymap只要不是已被占用的_quantum、_kb、_user即可——layer_state_set_mine、layer_state_set_fn等都可以。update_tri_layer_state的底层实现在 quantum/action_layer.c#L373函数声明位于 quantum/action_layer.h#L137它接收当前层状态与三个层号判断layer1与layer2是否同时激活若是则返回加入layer3的新状态否则按需移除layer3从而形成三角层切换逻辑。这正是上面layer_state_set_user中调用的核心。七、自定义 Features按键盘开关 Userspace 中的代码块Userspace 面向大量键盘你可能只想在部分键盘上启用某些功能。为此可以创建自己的特性开关。例如想把一批宏只编译进部分键盘节省空间可以用#ifdef MACROS_ENABLED包裹它们再按键盘启用。首先在 Userspace 的rules.mk中加入ifeq ($(strip $(MACROS_ENABLED)), yes) OPT_DEFS -DMACROS_ENABLED endifOPT_DEFS中的-D会让MACROS_ENABLED宏被定义到编译器命令行供所有.c/.h文件通过#ifdef MACROS_ENABLED判断。然后在想要启用该特性的 Keymap 的rules.mk中写MACROS_ENABLED yes最后在 Userspace 的process_record_user中按需编译宏代码bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { #ifdef MACROS_ENABLED case MACRO1: if (!record-event.pressed) { SEND_STRING(This is macro 1!); } break; case MACRO2: if (!record-event.pressed) { SEND_STRING(This is macro 2!); } break; #endif } return true; }这样宏代码只会在声明了MACROS_ENABLED yes的键盘上被编译进固件其余键盘则不会占用宝贵的 Flash 空间。八、Consolidated Macros把宏集中到 Userspace在自定义函数示例的基础上进一步可以把宏和其他常用逻辑全部收拢到 Userspace供所有 Keymap 共享同时保留键盘专属宏的能力。第一步把所有keymap.c中的process_record_user改名为process_record_keymap。这样键盘专属键码仍可在对应键盘上使用同时你的全局自定义键码也能工作。同样地把SAFE_RANGE替换为NEW_SAFE_RANGE避免自定义键码范围重叠。第二步在所有keymap.c中加入#include name.h从而无需在每个 Keymap 里重复定义这些新键码。第三步在name.h中定义键码枚举#pragma once #include quantum.h #include action.h #include version.h // Define all of enum custom_keycodes { KC_MAKE SAFE_RANGE, NEW_SAFE_RANGE //use NEW_SAFE_RANGE for keymap specific codes };第四步在name.c中实现共享逻辑#include name.h __attribute__ ((weak)) bool process_record_keymap(uint16_t keycode, keyrecord_t *record) { return true; } bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case KC_MAKE: // Compiles the firmware, and adds the flash command based on keyboard bootloader if (!record-event.pressed) { uint8_t temp_mod get_mods(); uint8_t temp_osm get_oneshot_mods(); clear_mods(); clear_oneshot_mods(); SEND_STRING(make QMK_KEYBOARD : QMK_KEYMAP); #ifndef FLASH_BOOTLOADER if ((temp_mod | temp_osm) MOD_MASK_SHIFT) #endif { SEND_STRING(:flash); } if ((temp_mod | temp_osm) MOD_MASK_CTRL) { SEND_STRING( -j8 --output-sync); } tap_code(KC_ENT); set_mods(temp_mod); } break; } return process_record_keymap(keycode, record); }对于没有 Shift 键的键盘如宏键盘需要一个方式让刷写选项始终出现。在 Userspace 的rules.mk中加入ifeq ($(strip $(FLASH_BOOTLOADER)), yes) OPT_DEFS -DFLASH_BOOTLOADER endif然后在该键盘 Keymap 的rules.mk中声明FLASH_BOOTLOADER yes。由此得到的新键码KC_MAKE可用于任意 Keymap它会自动输出make keyboard:keymap免去每次手动输入——因为它直接使用QMK_KEYBOARD与QMK_KEYMAP这两个构建期宏输出当前板子与 Keymap 信息按下KC_MAKE输出编译命令并回车按住Shift再按追加:flash目标直接刷写按住Ctrl再按追加-j8 --output-sync利用多核并行加速编译在声明了FLASH_BOOTLOADER yes的键盘上无条件追加:flash适合无 Shift 键的板子。提示该操作会依据键盘的 bootloader 设置自动选择刷写工具或退化为仅生成 HEX 文件但并非在所有系统上都保证可用——例如 AVRDUDE 在 WSL 中无法正常工作。九、底层机制小结Userspace 之所以自动生效核心逻辑集中在 builddefs/build_keyboard.mk#L410-L494定位USER_NAME为空时默认取KEYMAP据此得到USER_PATH : users/$(USER_NAME)builddefs/build_keyboard.mk#L411-L414外部 Userspace 优先如果配置了QMK_USERSPACE且其中存在同名目录则优先使用外部目录覆盖仓库内目录builddefs/build_keyboard.mk#L421-L426包含规则-include $(USER_PATH)/rules.mk自动引入rules.mkCONFIG_H $(USER_PATH)/config.h把config.h并入全局配置builddefs/build_keyboard.mk#L428-L435搜索路径VPATH $(USER_PATH)让编译器能直接找到name.h等头文件builddefs/build_keyboard.mk#L489。理解这四点就能明白为什么rules.mk、config.h会被自动处理而name.c/.h需要你手动通过SRC 加入——这正是整个 Userspace 机制的关键边界。【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考