QMK Joystick 功能完全指南:从规则配置、轴映射到 HID 手柄 API
2026/9/14 5:02:07 网站建设 项目流程

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
  • 合法驱动仅有analogdigital两种,写入其他值会在编译期触发CATASTROPHIC_ERROR
  • 启用后会自动追加编译process_joystick.cjoystick.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_HATjoystick_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

也可以直接使用下表这些预定义名称:

DefineValueAngle
JOYSTICK_HAT_CENTER-1
JOYSTICK_HAT_NORTH0
JOYSTICK_HAT_NORTHEAST145°
JOYSTICK_HAT_EAST290°
JOYSTICK_HAT_SOUTHEAST3135°
JOYSTICK_HAT_SOUTH4180°
JOYSTICK_HAT_SOUTHWEST5225°
JOYSTICK_HAT_WEST6270°
JOYSTICK_HAT_NORTHWEST7315°

这些常量同样定义在 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 采样指定引脚。lowhighrest分别对应该轴模拟量的最小值、最大值、静止(居中)值
  • JOYSTICK_AXIS_VIRTUAL展开为{NO_PIN, 0, JOYSTICK_MAX_VALUE / 2, JOYSTICK_MAX_VALUE}input_pinNO_PIN,因此不会被 ADC 读取,数值完全由用户代码提供。

反转轴的小技巧:将lowhigh对调即可实现轴方向反转。

对应地,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()

  1. 通过joystick_axis_sample()(默认实现为analogReadPin(),见 quantum/joystick.c)读取原始模拟值;
  2. 先以mid_digit为参考点,用(axis_val - ref) * -JOYSTICK_MAX_VALUE / (min_digit - ref)计算下半程映射;
  3. 若结果为正,说明处于上半程,改用max_digit计算正向映射;
  4. 最后把结果裁剪到[-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 = 0x7400QK_JOYSTICK_MAX = 0x743F),共 32 个按键可用:

KeyAliasesDescription
QK_JOYSTICK_BUTTON_0JS_0Button 0
QK_JOYSTICK_BUTTON_1JS_1Button 1
QK_JOYSTICK_BUTTON_2JS_2Button 2
QK_JOYSTICK_BUTTON_3JS_3Button 3
QK_JOYSTICK_BUTTON_4JS_4Button 4
QK_JOYSTICK_BUTTON_5JS_5Button 5
QK_JOYSTICK_BUTTON_6JS_6Button 6
QK_JOYSTICK_BUTTON_7JS_7Button 7
QK_JOYSTICK_BUTTON_8JS_8Button 8
QK_JOYSTICK_BUTTON_9JS_9Button 9
QK_JOYSTICK_BUTTON_10JS_10Button 10
QK_JOYSTICK_BUTTON_11JS_11Button 11
QK_JOYSTICK_BUTTON_12JS_12Button 12
QK_JOYSTICK_BUTTON_13JS_13Button 13
QK_JOYSTICK_BUTTON_14JS_14Button 14
QK_JOYSTICK_BUTTON_15JS_15Button 15
QK_JOYSTICK_BUTTON_16JS_16Button 16
QK_JOYSTICK_BUTTON_17JS_17Button 17
QK_JOYSTICK_BUTTON_18JS_18Button 18
QK_JOYSTICK_BUTTON_19JS_19Button 19
QK_JOYSTICK_BUTTON_20JS_20Button 20
QK_JOYSTICK_BUTTON_21JS_21Button 21
QK_JOYSTICK_BUTTON_22JS_22Button 22
QK_JOYSTICK_BUTTON_23JS_23Button 23
QK_JOYSTICK_BUTTON_24JS_24Button 24
QK_JOYSTICK_BUTTON_25JS_25Button 25
QK_JOYSTICK_BUTTON_26JS_26Button 26
QK_JOYSTICK_BUTTON_27JS_27Button 27
QK_JOYSTICK_BUTTON_28JS_28Button 28
QK_JOYSTICK_BUTTON_29JS_29Button 29
QK_JOYSTICK_BUTTON_30JS_30Button 30
QK_JOYSTICK_BUTTON_31JS_31Button 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_pinmin_digitmid_digitmax_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()驱动。

典型落地步骤小结

  1. rules.mk中写入JOYSTICK_ENABLE = yes,并按需设置JOYSTICK_DRIVER = analogdigital
  2. config.h中按需调整JOYSTICK_BUTTON_COUNTJOYSTICK_AXIS_COUNTJOYSTICK_AXIS_RESOLUTION,需要帽式开关时定义JOYSTICK_HAS_HAT
  3. keymap.c中定义joystick_config_t joystick_axes[JOYSTICK_AXIS_COUNT]数组,用JOYSTICK_AXIS_IN声明物理轴、JOYSTICK_AXIS_VIRTUAL声明虚拟轴;
  4. 在 keymap 中直接使用JS_0JS_31键码映射手柄按键,或在process_record_user()中调用joystick_set_axis()/joystick_set_hat()实现自定义操控逻辑;
  5. 编译刷写后,主机系统即会识别出一个 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询