QMK Joystick 功能完全指南:从规则配置、轴映射到 HID 手柄 API
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
本篇技术指南以 QMK Firmware(qmk_firmware)中内置的 Joystick(游戏手柄)功能为核心,讲解如何把键盘固件扩展为一个符合 HID 规范的 joystick 设备——支持最多 6 个轴(axis)、32 个按键(button)与 1 个 8 方向帽式开关(hat switch)。读完本文,你将掌握:在rules.mk中开启功能的正确姿势、在config.h中定制轴数与分辨率的参数边界、通过JOYSTICK_AXIS_IN/JOYSTICK_AXIS_VIRTUAL两种方式声明物理轴与虚拟轴、在keymap.c中编写虚拟轴控制代码,以及直接调用joystick_set_axis()、joystick_set_hat()、register_joystick_button()等底层 API 的完整方法。
功能概览:键盘如何变成游戏手柄
Joystick 功能让 QMK 固件在 USB 枚举时呈现为一个 game controller 设备。来自模拟摇杆等外设的模拟量通过微控制器的 ADC(模数转换器)读取,也可以由用户代码直接“注入”虚拟轴数据,最终以 HID joystick report 的形式发送给主机。
一个典型的模拟摇杆轴(例如电位器)本质上是一个分压电路:滑动触点的位置决定了输出电压,MCU 的 ADC 采样该电压即可推算轴的位置。这就是analog驱动的工作基础;而digital驱动则完全不读 ADC,轴的数值完全由用户代码决定。
与功能直接相关的核心源码文件如下:
- quantum/joystick.c:功能主实现,包含轴采样、量程换算、按键/轴/帽状态维护与发送逻辑;
- quantum/joystick.h:公开的数据结构、配置宏与函数声明;
- quantum/process_keycode/process_joystick.c:负责把
QK_JOYSTICK_*键码转换为按键按下/释放调用; - builddefs/common_features.mk:构建系统侧的功能开关与驱动合法性校验。
启用功能与选择驱动
在键盘目录(或 keymap 目录)的rules.mk中添加:
JOYSTICK_ENABLE = yes默认使用的驱动是analog,若需要改为digital:
JOYSTICK_DRIVER = digital从 builddefs/common_features.mk 可以看到构建期的具体行为:
JOYSTICK_ENABLE默认为no;- 合法驱动仅有
analog与digital两种,写入其他值会在编译期触发CATASTROPHIC_ERROR; - 启用后会自动追加编译
process_joystick.c与joystick.c; - 当驱动为
analog时,会额外把ANALOG_DRIVER_REQUIRED置为yes,即同时依赖 ADC 驱动; - 驱动选择最终通过
-DJOYSTICK_ANALOG或-DJOYSTICK_DIGITAL传递给 C 编译器,从而在 quantum/joystick.c 中决定是否#include "analog.h"。
analog 驱动与 ARM 板的电压注意点
使用analog驱动时,ADC 采样依赖 ADC 驱动(原文档链接为../drivers/adc,已转换为仓库根目录相对路径)。在 ARM 平台上需要注意:必须使用 3.3V 为摇杆供电。虽然部分 ARM 板卡(如 Helios)带有 5V 引脚输出,但 QMK 的 ADC 驱动并不支持 5V 采样,超出量程的电压会导致读数失真甚至损坏引脚。
在 config.h 中定制按键数、轴数与分辨率
默认情况下,Joystick 功能定义了2 个轴与8 个按键,轴报告分辨率为8 位(取值范围 -127 到 +127)。这些默认值可以在config.h中修改:
// 最小值 0,最大值 32 #define JOYSTICK_BUTTON_COUNT 16 // 最小值 0,最大值 6:分别对应 X、Y、Z、Rx、Ry、Rz #define JOYSTICK_AXIS_COUNT 3 // 最小值 8,最大值 16 #define JOYSTICK_AXIS_RESOLUTION 10这些边界并非文档约定,而是硬性的编译期检查,见 quantum/joystick.h:
JOYSTICK_BUTTON_COUNT > 32时直接#error;JOYSTICK_AXIS_COUNT > 6时直接#error;- 当轴数与按键数同时为 0时,会报错
Joystick feature requires at least one axis or button,即功能至少要声明一个轴或一个按键; JOYSTICK_AXIS_RESOLUTION超出 8~16 范围时报错。
分辨率直接决定了轴值范围:JOYSTICK_MAX_VALUE在 quantum/joystick.h 中被定义为(1L << (JOYSTICK_AXIS_RESOLUTION - 1)) - 1。例如 8 位分辨率对应最大值为 127(范围 -127~+127),10 位分辨率则对应 -511~+511。
分辨率与 MCU ADC 精度的匹配
需要注意:ADC 采样精度上限受芯片约束。文档明确提示:受支持的 AVR MCU 其 ADC 最大为 10 位,多数 STM32 MCU 为 12 位。若JOYSTICK_AXIS_RESOLUTION设置得比 ADC 实际精度更高,多出的位数只是数值换算的放大,并不会提升真实采样精度。
帽式开关(Hat Switch)
启用 8 方向帽式开关,在config.h中添加:
#define JOYSTICK_HAS_HAT从 quantum/joystick.h 可以看到,只有定义了JOYSTICK_HAS_HAT,joystick_t结构体中才会包含int8_t hat成员;quantum/joystick.c 中的joystick_set_hat()也仅在宏开启时才会被编译。
位置通过joystick_set_hat(value)设置。数值从顶部(正北)开始顺时针递增,默认“居中”位置用-1表示,布局如下:
0 7 N 1 NW .--'--. NE / \ 6 W | -1 | E 2 \ / SW '--.--' SE 5 S 3 4也可以直接使用下表这些预定义名称:
| Define | Value | Angle |
|---|---|---|
JOYSTICK_HAT_CENTER | -1 | |
JOYSTICK_HAT_NORTH | 0 | 0° |
JOYSTICK_HAT_NORTHEAST | 1 | 45° |
JOYSTICK_HAT_EAST | 2 | 90° |
JOYSTICK_HAT_SOUTHEAST | 3 | 135° |
JOYSTICK_HAT_SOUTH | 4 | 180° |
JOYSTICK_HAT_SOUTHWEST | 5 | 225° |
JOYSTICK_HAT_WEST | 6 | 270° |
JOYSTICK_HAT_NORTHWEST | 7 | 315° |
这些常量同样定义在 quantum/joystick.h。
轴的定义与两种配置宏
当使用物理轴时,必须在代码中提供轴配置数组,通常写在keymap.c中:
joystick_config_t joystick_axes[JOYSTICK_AXIS_COUNT] = { JOYSTICK_AXIS_IN(A4, 900, 575, 285), JOYSTICK_AXIS_VIRTUAL };该示例定义了 2 个轴:X 轴从A4引脚读取,在默认 8 位分辨率下,900~575 的模拟值被线性映射为 -127~0,575~285 映射为 0~127;Y 轴是虚拟轴,不从任何引脚读取,其值需要由用户代码主动更新。
轴的两种配置宏(见 quantum/joystick.h):
JOYSTICK_AXIS_IN(input_pin, low, rest, high)展开为{INPUT_PIN, LOW, REST, HIGH},即让 ADC 采样指定引脚。low、high、rest分别对应该轴模拟量的最小值、最大值、静止(居中)值;JOYSTICK_AXIS_VIRTUAL展开为{NO_PIN, 0, JOYSTICK_MAX_VALUE / 2, JOYSTICK_MAX_VALUE}。input_pin为NO_PIN,因此不会被 ADC 读取,数值完全由用户代码提供。
反转轴的小技巧:将low与high对调即可实现轴方向反转。
对应地,joystick_config_t结构体包含 4 个成员(见 quantum/joystick.h):
pin_t input_pin:读取模拟值的引脚,虚拟轴为NO_PIN;uint16_t min_digit:模拟最小值;uint16_t mid_digit:静止/中点模拟值;uint16_t max_digit:模拟最大值。
虚拟轴(Virtual Axes)
下面的例子基于小键盘按键调整 X、Y 两个虚拟轴,KC_P0作为“精细模式”修饰键(按下后减小单步幅度):
joystick_config_t joystick_axes[JOYSTICK_AXIS_COUNT] = { JOYSTICK_AXIS_VIRTUAL, // x JOYSTICK_AXIS_VIRTUAL // y }; static bool precision = false; static uint16_t precision_mod = 64; static uint16_t axis_val = 127; bool process_record_user(uint16_t keycode, keyrecord_t *record) { int16_t precision_val = axis_val; if (precision) { precision_val -= precision_mod; } switch (keycode) { case KC_P8: joystick_set_axis(1, record->event.pressed ? -precision_val : 0); return false; case KC_P2: joystick_set_axis(1, record->event.pressed ? precision_val : 0); return false; case KC_P4: joystick_set_axis(0, record->event.pressed ? -precision_val : 0); return false; case KC_P6: joystick_set_axis(0, record->event.pressed ? precision_val : 0); return false; case KC_P0: precision = record->event.pressed; return false; } return true; }按下KC_P8/KC_P2分别把轴 1(Y)设为负/正方向的值,KC_P4/KC_P6同理控制轴 0(X),松开时归零;KC_P0按住时单步幅度从 127 降至 63,实现精细操控。
从源码看轴换算的内部流程
物理轴的采样与量程换算逻辑位于 quantum/joystick.c 的joystick_read_axis():
- 通过
joystick_axis_sample()(默认实现为analogReadPin(),见 quantum/joystick.c)读取原始模拟值; - 先以
mid_digit为参考点,用(axis_val - ref) * -JOYSTICK_MAX_VALUE / (min_digit - ref)计算下半程映射; - 若结果为正,说明处于上半程,改用
max_digit计算正向映射; - 最后把结果裁剪到
[-JOYSTICK_MAX_VALUE, +JOYSTICK_MAX_VALUE],即饱和钳位。
此外,joystick_axes[]数组本身以__attribute__((weak))定义(quantum/joystick.c),默认全部为虚拟轴;你在keymap.c中定义的强符号数组会覆盖它。初始化时(joystick_init()→joystick_init_axes())会对每个非虚拟轴执行gpio_set_pin_input()将引脚配置为输入模式。
按键键码表
Joystick 按键可以直接放入 keymap,作为普通键码使用。按键按下时process_joystick()会调用register_joystick_button(),释放时调用unregister_joystick_button()(见 quantum/process_keycode/process_joystick.c)。键码基址定义在 quantum/keycodes.h(QK_JOYSTICK = 0x7400,QK_JOYSTICK_MAX = 0x743F),共 32 个按键可用:
| Key | Aliases | Description |
|---|---|---|
QK_JOYSTICK_BUTTON_0 | JS_0 | Button 0 |
QK_JOYSTICK_BUTTON_1 | JS_1 | Button 1 |
QK_JOYSTICK_BUTTON_2 | JS_2 | Button 2 |
QK_JOYSTICK_BUTTON_3 | JS_3 | Button 3 |
QK_JOYSTICK_BUTTON_4 | JS_4 | Button 4 |
QK_JOYSTICK_BUTTON_5 | JS_5 | Button 5 |
QK_JOYSTICK_BUTTON_6 | JS_6 | Button 6 |
QK_JOYSTICK_BUTTON_7 | JS_7 | Button 7 |
QK_JOYSTICK_BUTTON_8 | JS_8 | Button 8 |
QK_JOYSTICK_BUTTON_9 | JS_9 | Button 9 |
QK_JOYSTICK_BUTTON_10 | JS_10 | Button 10 |
QK_JOYSTICK_BUTTON_11 | JS_11 | Button 11 |
QK_JOYSTICK_BUTTON_12 | JS_12 | Button 12 |
QK_JOYSTICK_BUTTON_13 | JS_13 | Button 13 |
QK_JOYSTICK_BUTTON_14 | JS_14 | Button 14 |
QK_JOYSTICK_BUTTON_15 | JS_15 | Button 15 |
QK_JOYSTICK_BUTTON_16 | JS_16 | Button 16 |
QK_JOYSTICK_BUTTON_17 | JS_17 | Button 17 |
QK_JOYSTICK_BUTTON_18 | JS_18 | Button 18 |
QK_JOYSTICK_BUTTON_19 | JS_19 | Button 19 |
QK_JOYSTICK_BUTTON_20 | JS_20 | Button 20 |
QK_JOYSTICK_BUTTON_21 | JS_21 | Button 21 |
QK_JOYSTICK_BUTTON_22 | JS_22 | Button 22 |
QK_JOYSTICK_BUTTON_23 | JS_23 | Button 23 |
QK_JOYSTICK_BUTTON_24 | JS_24 | Button 24 |
QK_JOYSTICK_BUTTON_25 | JS_25 | Button 25 |
QK_JOYSTICK_BUTTON_26 | JS_26 | Button 26 |
QK_JOYSTICK_BUTTON_27 | JS_27 | Button 27 |
QK_JOYSTICK_BUTTON_28 | JS_28 | Button 28 |
QK_JOYSTICK_BUTTON_29 | JS_29 | Button 29 |
QK_JOYSTICK_BUTTON_30 | JS_30 | Button 30 |
QK_JOYSTICK_BUTTON_31 | JS_31 | Button 31 |
按键数上限 32 与JOYSTICK_BUTTON_COUNT的编译期上限一致,即使只启用了 8 个按键,超出范围的键码也不会被处理(register_joystick_button()内部有越界保护,见 quantum/joystick.c)。
编程 API 参考
以下 API 均声明于 quantum/joystick.h,供用户在keymap.c或自定义代码中直接调用。
struct joystick_t
保存 joystick 的完整状态:
uint8_t buttons[]:按位打包的按键状态数组,长度按(JOYSTICK_BUTTON_COUNT - 1) / 8 + 1计算(见 quantum/joystick.h);int16_t axes[]:每个已定义轴的模拟值数组;int8_t hat:帽式开关位置(仅在启用JOYSTICK_HAS_HAT时存在);bool dirty:当前状态是否有变更、需要发送给主机。
struct joystick_config_t
描述单个轴,成员已在“轴的定义”一节说明:input_pin、min_digit、mid_digit、max_digit。
void joystick_flush(void)
若joystick_state.dirty为真,则把报告发送给主机并清除 dirty 标记(quantum/joystick.c)。内部调用host_joystick_send(&joystick_state)。
void register_joystick_button(uint8_t button)
将指定按钮置为按下状态并立即发送报告。
- 参数
button:按键索引,取值范围 0~31。 - 实现:
joystick_state.buttons[button / 8] |= 1 << (button % 8),置 dirty 后 flush(quantum/joystick.c)。
void unregister_joystick_button(uint8_t button)
将指定按钮复位为释放状态并立即发送报告。
- 参数
button:按键索引,取值范围 0~31。 - 实现:
joystick_state.buttons[button / 8] &= ~(1 << (button % 8))(quantum/joystick.c)。
int16_t joystick_read_axis(uint8_t axis)
采样并处理指定轴的模拟值。
- 参数
axis:要读取的轴。 - 返回值:有符号 16 位整数,
0表示静止/中点。
注意:joystick_read_axis()返回的是“按当前分辨率换算后的值”,它通过joystick_axis_sample()读取原始 ADC 值并完成量程映射。
void joystick_set_axis(uint8_t axis, int16_t value)
设置指定轴的值。
- 参数
axis:要设置的轴; - 参数
value:要设置的值(有符号 16 位整数,受JOYSTICK_MAX_VALUE约束)。
实现上仅在新值与旧值不同时才置 dirty(quantum/joystick.c),避免无谓的 USB 上报。虚拟轴的日常控制主要就靠这个函数。
void joystick_set_hat(int8_t value)
设置帽式开关的位置。
- 参数
value:要设置的帽式开关位置(使用JOYSTICK_HAT_*常量,见上文表格)。 - 仅在定义
JOYSTICK_HAS_HAT时可用(quantum/joystick.c)。
后台任务:joystick_task()
固件主循环中会周期调用joystick_task(),它遍历所有非虚拟轴并调用joystick_read_axes()完成一轮采样、换算与上报(quantum/joystick.c)。也就是说,物理轴的值不需要你手动刷新——只要正确配置了joystick_axes[],主循环会自动保持轴数据更新;虚拟轴则需要你自己在按键处理或自定义函数中调用joystick_set_axis()驱动。
典型落地步骤小结
- 在
rules.mk中写入JOYSTICK_ENABLE = yes,并按需设置JOYSTICK_DRIVER = analog或digital; - 在
config.h中按需调整JOYSTICK_BUTTON_COUNT、JOYSTICK_AXIS_COUNT、JOYSTICK_AXIS_RESOLUTION,需要帽式开关时定义JOYSTICK_HAS_HAT; - 在
keymap.c中定义joystick_config_t joystick_axes[JOYSTICK_AXIS_COUNT]数组,用JOYSTICK_AXIS_IN声明物理轴、JOYSTICK_AXIS_VIRTUAL声明虚拟轴; - 在 keymap 中直接使用
JS_0~JS_31键码映射手柄按键,或在process_record_user()中调用joystick_set_axis()/joystick_set_hat()实现自定义操控逻辑; - 编译刷写后,主机系统即会识别出一个 HID joystick 设备。
需要进一步理解 ADC 采样底层原理时,可继续阅读 docs/drivers/adc.md 与 ADC 驱动源码;Joystick 功能的全部实现细节可对照 quantum/joystick.c 与 quantum/joystick.h 研读。
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考