虽然选择了触摸屏,我仍选择为机械键盘嵌入摇杆模块,这本质上是对"操作连续性"的执着。
值得深思的是,本次开发过程中借助DeepSeek的代码生成与逻辑推理,其展现的能力已然颠覆传统编程范式,需求描述可自动转化为功能实现,算法优化能自主完成多目标博弈,这昭示着技术生产关系的根本性变革。
技术演进正在重构价值坐标系,边缘计算设备通过蒸馏更好的模型实现端侧智能,使AI能力呈指数级渗透产业格局,算力资本形成的新型生产资料,正在重塑技术话语权分配机制,工程师的核心竞争力将从代码实现转向需求抽象、系统架构与伦理把控在这场人机协同的认知革命,真正的危机并非技术替代,而是思维范式的停滞。
当AI解构了执行层的技术壁垒,人类智慧的战场必将向更高维度迁移------那些涉及跨领域创新、价值判断与复杂系统设计的领域,正是技术人亟待开垦的新边疆,但愿技术人可以在这样的狭缝中获得存在的意义。
言归正传还是回到我们的QMK键盘增加摇杆功能,首先要了解一下QMK 生成键盘的整体文件结构:
qmk_firmware/keyboards/demo_keyboard/
├── config.h
├── keymaps/
│ └── default/
│ ├── keymap.c
├── rules.mk
└── keyboard.json
keyboard.json功能:定义键盘的硬件配置、布局、功能和元数据。示例
rules.mk功能:定义编译选项和功能开关。
config.h功能:定义键盘的硬件配置和宏。
keymaps/default/keymap.c功能:定义默认键位布局,开启自定义功能。
所以增加摇杆和鼠标就需要在这些文件里面进行修改
在rules.mk中启用摇杆和鼠标按键功能
python
POINTING_DEVICE_ENABLE = yes
POINTING_DEVICE_DRIVER = analog_joystick
MOUSEKEY_ENABLE = yes
# DEBUG_ENABLE = yes
# CONSOLE_ENABLE = yes # 启用调试输出
在config.h中添加摇杆和鼠标键的读取对应端口
c
/*
Copyright 2025 <JohnsonLv>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#define ANALOG_JOYSTICK_X_AXIS_PIN GP26
#define ANALOG_JOYSTICK_Y_AXIS_PIN GP27
#define MOUSE_BTN1_PIN GP15
在keymap.c中添加,保留以前的键盘键的映射,然后添加一些关于摇杆的函数
c
/// Copyright 2023 QMK
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include QMK_KEYBOARD_H
#include "pointing_device.h"
#include "print.h"
#include "timer.h" // 用于 timer_read32 和 timer_elapsed32
// 定义摇杆的灵敏度
#define JOYSTICK_SENSITIVITY 1
static bool debounce = false;
static uint32_t debounce_timer = 0;
// 初始化独立按键的 GPIO
void keyboard_post_init_user(void) {
setPinInputHigh(MOUSE_BTN1_PIN); // 设置为输入模式,启用内部上拉电阻
}
// 新增参数定义(需根据实际需求调整)
#define MAX_CURSOR_SPEED 10 // 最大光标速度
#define SPEED_REGULATOR 3 // 速度调节系数
// 非线性映射函数(基于 IBM 专利逻辑)
void joystick_ibm_algorithm(int16_t x, int16_t y, int16_t* x_out, int16_t* y_out) {
static int16_t z_prev = 0; // 静态变量保存上一次的 z 值
// --- 核心算法逻辑 ---
// 1. 计算近似平方根的 z 值
int16_t ax = abs(x);
int16_t ay = abs(y);
int16_t z = ax + ay - ((2 * (ax < ay ? ax : ay)) / 3);
// 2. 动态调整光标移动
if (z > 4) {
// 计算动态变化的 zi 值(包含释放补偿)
int16_t zi = (z - z_prev) * 6 + z;
// 计算最终坐标(避免除以零)
int16_t x_calc = (zi == 0) ? 0 : (x * z * MAX_CURSOR_SPEED) / (zi * SPEED_REGULATOR);
int16_t y_calc = (zi == 0) ? 0 : (y * z * MAX_CURSOR_SPEED) / (zi * SPEED_REGULATOR);
*x_out = x_calc;
*y_out = y_calc;
} else {
*x_out = 0;
*y_out = 0;
}
// 3. 保存当前 z 值供下次使用
z_prev = z;
}
// 处理独立按键和摇杆的函数
void my_process_joystick(void) {
// 获取摇杆的 X/Y 轴值
int16_t x_raw= joystick_state.axes[0];
int16_t y_raw= joystick_state.axes[1];
// 创建鼠标报告
report_mouse_t mouse_report = {0};
// 应用 IBM 算法
int16_t x_mapped, y_mapped;
joystick_ibm_algorithm(x_raw, y_raw, &x_mapped, &y_mapped);
// 检测独立按键状态(按下时为低电平)
bool btn_state = !readPin(MOUSE_BTN1_PIN); // 按下时为 true
// 消抖逻辑
if (btn_state && !debounce) {
debounce = true;
debounce_timer = timer_read32();
mouse_report.buttons |= KC_BTN1; // 触发左键
} else if (!btn_state && debounce) {
if (timer_elapsed32(debounce_timer) > 5) { // 消抖时间 5ms
debounce = false;
mouse_report.buttons &= ~KC_BTN1; // 释放左键
}
}
// 发送鼠标报告
pointing_device_set_report(mouse_report);
pointing_device_send();
}
// 键盘矩阵扫描后的钩子函数
void matrix_scan_user(void) {
my_process_joystick();
}
// 键盘布局定义(无需为独立按键分配矩阵键位)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[0] = LAYOUT_numpad_4x4(
KC_1, KC_2, KC_3, KC_4,
KC_5, KC_6, KC_7, KC_8,
KC_9, KC_0, KC_A, KC_B,
KC_C, KC_D, KC_BTN2, KC_BTN3 // 保持矩阵中的键位不变
)
};
在上面的这段算法中借用了 https://patents.google.com/patent/US5570111A, IBM的一个专利技术
以下是这段基于 IBM 专利的摇杆算法的逐层解析,我将用 「物理直觉 → 数学实现 → 代码表达」 的逻辑链解释其精妙之处:
1. 近似矢量长度(模拟平方根)物理需求:
需要计算摇杆偏移的「矢量长度」,但避免耗时的真实平方根运算。数学实现:
通过线性组合近似替代 x 2 + y 2 \sqrt{x² + y²} x2+y2 ,专利给出的公式: z = ∣ x ∣ + ∣ y ∣ − ( 2 ∗ m i n ( ∣ x ∣ , ∣ y ∣ ) ) / 3 z = |x| + |y| - (2 * min(|x|, |y|)) / 3 z=∣x∣+∣y∣−(2∗min(∣x∣,∣y∣))/3
代码实现:
C
int16_t ax = abs(x);
int16_t ay = abs(y);
int16_t z = ax + ay - ((2 * (ax < ay ? ax : ay)) / 3);
效果说明:当摇杆沿对角线移动时(x=y),公式简化为 ( 4 / 3 ) ∗ x (4/3) * x (4/3)∗x,接近真实平方根 2 ∗ x ≈ 1.414 x \sqrt2*x ≈ 1.414x 2 ∗x≈1.414x当摇杆沿单轴移动时(如 x=0),公式退化为 y,与真实值一致平衡了计算效率和准确性
2. 动态响应补偿(预测释放动作)
物理需求 :
当用户松开摇杆时,光标会因惯性继续移动,需要模拟「减速回弹」效果。
数学实现 :
通过差分计算摇杆速度变化:
z i = z + 6 ∗ ( z − z p r e v ) zi = z + 6*(z - z_{prev}) zi=z+6∗(z−zprev)
( z p r e v z_{prev} zprev是上一帧的 z 值)
代码实现:
c
int16_t zi = (z - z_prev) * 6 + z; // 放大变化量的影响
效果说明:
- 快速释放时 (z 急剧减小): z i zi zi会远小于 z z z,导致 x c a l c / y c a l c x_{calc}/y_{calc} xcalc/ycalc分母增大,光标减速
- 保持摇杆时 (z 稳定): z i ≈ z zi ≈ z zi≈z,光标匀速移动
- 推动摇杆时 (z 增大): z i > z zi > z zi>z,分母增大,光标加速更平缓
3. 非线性速度映射
物理需求 :
摇杆偏移量与光标速度呈非线性关系(小偏移精细控制,大偏移快速移动)。
数学实现 :
速度公式:
x_calc = (x * z * MAX_CURSOR_SPEED) / (zi * SPEED_REGULATOR)
代码实现:
c
int16_t x_calc = (zi == 0) ? 0 : (x * z * MAX_CURSOR_SPEED) / (zi * SPEED_REGULATOR);
参数控制:
参数 | 作用 | 调整建议 |
---|---|---|
MAX_CURSOR_SPEED |
最大移动速度 | 值越大光标移动越快 |
SPEED_REGULATOR |
整体灵敏度调节 | 值越大光标移动越慢 |
6 (zi的系数) |
惯性响应强度 | 值越大释放时的减速越明显 |
4. 死区处理与噪声过滤
物理需求 :
消除摇杆中心位置的微小抖动。
数学实现 :
当 z ≤ 4
时强制归零:
c
if (z > 4) { ... } else { *x_out=0; *y_out=0; }
效果说明:
- 过滤摇杆电阻器的噪声
- 提供明确的中心死区
整体算法流程图
原始输入 (x,y)
↓
计算近似矢量长度 z
↓
动态补偿 → 计算 zi (包含惯性预测)
↓
非线性映射 → 输出 (x_calc, y_calc)
↓
保存 z 值 → 供下一帧使用
实际调试技巧
-
参数联动调整:
- 先固定
SPEED_REGULATOR=1
,调整MAX_CURSOR_SPEED
确定最大速度 - 然后增大
SPEED_REGULATOR
微调灵敏度 - 最后调整
zi
的系数(代码中的6
)控制惯性效果
- 先固定
-
边界保护 :
添加范围限制防止溢出:
x_calc = MAX(-127, MIN(x_calc, 127)); // 确保在鼠标协议范围内
这个算法通过巧妙的近似和差分计算,在极低的计算开销下实现了符合人体工学的光标控制特性,正是这种「用简单数学模拟复杂物理直觉」的设计,让它成为经典。