
简介全键可编程键盘原理图程序是一套面向客制化键盘开发者、USB协议与HID学习者的完整工程资料。方案基于CH559芯片实现101个按键除Fn切换键外全部可自定义映射配套上位机软件支持不断电实时改键即改即用。压缩包共40个文件约16.72MB包含12个C源文件、9个H头文件组成的固件工程另有8份PDF资料包括USB2.0规范、HID协议及CH559数据手册、1张键盘原理图、可烧录HEX文件及Keil工程文件方便直接编译和硬件制版。附带的参考文档覆盖从底层USB协议到芯片寄存器说明适合进阶阅读已有2246人学习下载。通过这份资料开发者可以掌握HID键盘枚举、按键扫描去抖、自定义映射表存储等核心代码逻辑同时结合原理图与协议文档能够快速移植到自己的客制化键盘项目中省去从零查阅芯片手册和协议细节的时间。 做了这么多年嵌入式我一直觉得机械键盘圈里最值得折腾的玩法就是“全键可编程”。这个项目标题看着很硬核但其实拆开就两件事原理图怎么画、程序怎么写。借助STM32F103C8T6这颗经典主控完全可以自己做一把支持任意改键、宏录制、多层的可编程键盘。这篇文章我尽量按实操顺序讲把我画板子、调固件、踩坑的过程都写出来给想入坑的朋友一条能直接照做的路线。1. 核心需求解析与方案选型1.1 可编程键盘到底在改什么很多人以为可编程键盘是硬件层面的“改线”实际上不是。它的本质是把每个按键的按下状态通过主控芯片转换成USB HID报文再上报给电脑。所谓“可编程”改的是主控内部的“键位映射表”物理按键和最终输出字符之间没有固定绑定的关系。打个比方普通键盘就像一封信每个键格写死了要表达的意思可编程键盘则是一张白纸加一张翻译表你想让A键输出B就在翻译表里把A的位置改成B。这样一整套系统核心硬件部分是一个按键矩阵、一颗MCU、一路USB接口核心软件部分是矩阵扫描、按键去抖、映射查表、HID上报四段逻辑串起来就是完整固件。1.2 主控选型为什么是STM32F103C8T6做可编程键盘的主控选择其实不少但STM32F103C8T6是目前社区资料最全、成本最低、上手门槛最友好的方案之一。这颗芯片是Cortex-M3内核72MHz主频64KB Flash20KB RAM内置USB 2.0 Full-Speed外设。对于键盘这种低速设备来说性能绰绰有余跑起来甚至有些“大材小用”。它最大的优势是资料密度。随便一搜就是大把最小系统板原理图参考、HAL库例程、标准外设库例程甚至连PCB封装都有现成的。相比使用ATmega32U4的方案STM32F103C8T6的Flash大了一倍以上意味着你可以给每一层按键映射都写出非常详尽的配置结构体还能顺带塞进宏存储缓冲区。对比QMK等固件的兼容列表也能看出来这是社区验证过无数次的选择。我自己的项目里选型时还考虑过RP2040那个芯片有PIO外设做矩阵扫描理论上更优雅但Keil环境、入门资料、老帖子讨论度都不如STM32来得厚实。对于从零开始做第一把可编程键盘的场景稳妥路线永远是F103C8T6。1.3 固件方案自己写还是用现成框架固件路线有两条主流直接使用QMK这类现成开源框架或者基于STM32标准外设库/HAL库自己写一套轻量固件。QMK的优点是开箱即用键位映射、多层、宏、RGB灯效全是现成的改个keymap.c就能得到完整功能。缺点是配置体系庞大编译链路复杂出了问题排查起来对新手并不友好。自己写的优点是每一行代码都在掌控之中扫描逻辑、去抖策略、上报时机完全可以按需定制且对原理图和固件的理解会深入几个台阶。缺点是功能开发周期长。我的建议是如果你是第一次做选“自己写”这条路线但把功能边界控制住不要贪多。先实现矩阵扫描、去抖、单层映射、HID上报这几个核心模块跑通之后再考虑加层、加宏。这个项目标题既然叫“原理图程序”重点本来就在完整闭环而不是堆功能。2. 原理图设计从矩阵到MCU2.1 键盘矩阵原理与二极管的作用全键可编程键盘的原理图核心就是按键矩阵。假设键盘是61键一种最直白的设计是每个按键占用一个GPIO口接到MCU上——但这需要至少61个IO口STM32F103C8T6只有48引脚其中可用的GPIO在37个左右明显不够。所以矩阵扫描是必然选择。矩阵扫描的基本思路是把按键排列成行和列。比如8行×8列就可以驱动64个按键。扫描过程是MCU逐行拉低或者逐列输出低电平然后读取每一列的电平状态。如果某个按键被按下它所在的行和列就会导通MCU读到电平变化就能定位到具体是哪个按键。这里有个关键细节每个按键必须串联一个二极管。原因在于矩阵扫描存在“鬼键”问题。举个具体例子如果同时按下(A行B列)、(A行C列)、(B行B列)三个按键那么B行C列的交点虽然在电气上并没有真正被按下但由于三个按键的导通路径MCU会误判它也处于按下状态。串联二极管后电流只能单向通过这些寄生路径就被切断了不会产生串扰和误触发。二极管选1N4148就可以这是键盘DIY圈子的标准选择开关速度、正向压降都合适。2.2 电路细节上拉电阻、去耦电容与USB接口矩阵的IO配置建议使用“开漏输出外部上拉”的方式。具体来说行线设置为推挽输出列线设置为输入。列线必须接上拉电阻阻值选择4.7kΩ到10kΩ比较合适。阻值太大的话走线寄生电容会造成电平翻转变慢影响扫描频率阻值太小则功耗上升而且行输出低电平时灌入电流偏大长期工作会有隐患。我实测下来10kΩ上拉配合1kHz以上的行扫描频率完全够用。MCU供电部分STM32F103C8T6的VDD引脚旁边必须放置0.1μF去耦电容位置尽可能靠近电源引脚。VDDA引脚需要接一个1μF以上的电容模拟域和数字域之间的噪声干扰是USB握手偶发失败的常见原因。复位引脚NRST接10kΩ上拉电阻到3.3V再并联一个0.1μF电容到地组成硬件复位电路。USB接口建议采用Mini-USB或者Type-C母座D和D-走线保持等长线宽做粗一点尽量直接连到MCU的PA11、PA12引脚。USB D上需要接一个1.5kΩ上拉电阻到3.3V这是USB Full-Speed设备标准要求。很多第一次画板的朋友会漏掉这个电阻结果板子插电脑上完全没有枚举反应——不是芯片坏了是USB规范没满足。2.3 PCB布局与走线注意事项PCB布局第一原则是MCU尽量靠近USB座这样高速信号线可以走最短路径。矩阵的行列走线可以用细线不要盲目追求大面积覆铜导致走线绕远。按键排列阶段建议先把矩阵网络标号规划好再映射到PCB封装上不然画到一半会发现走线交叉非常严重。我的习惯是先在原理图中把行线和列线用网络标号分开命名比如ROW0到ROW7COL0到COL7这样PCB端可以按网络名批量放置。还有一点容易被忽略STM32F103C8T6的BOOT0引脚要留一个跳线或者拨码开关的位置方便后续通过串口或DFU方式烧录固件。如果没留就只能靠ST-Link的SWD接口救砖操作起来麻烦不少。晶振方面如果是用芯片内部RC振荡器USB通信会不稳定。F103的USB外设要求时钟误差在一定范围内所以一定要用8MHz外部晶振配合20pF负载电容接到OSC_IN和OSC_OUT。这里真心建议该省的地方可以省晶振千万不要省。3. 固件程序的实现思路3.1 矩阵扫描与按键去抖固件主循环的骨架就是“扫描矩阵-处理按键事件-更新HID报告”。扫描函数的核心逻辑很简单行输出低电平后逐列读取状态保存到二维数组里。uint8_t matrix[MATRIX_ROWS] {0}; void matrix_scan(void) { for (uint8_t row 0; row MATRIX_ROWS; row) { HAL_GPIO_WritePin(GPIOB, row_pin[row], GPIO_PIN_RESET); for (uint8_t col 0; col MATRIX_COLS; col) { matrix[row] ~(1 col); if (HAL_GPIO_ReadPin(GPIOC, col_pin[col]) GPIO_PIN_RESET) { matrix[row] | (1 col); } } HAL_GPIO_WritePin(GPIOB, row_pin[row], GPIO_PIN_SET); } }按键去抖非常重要。机械开关在按下和释放瞬间触点会有机械抖动持续时间通常在几毫秒到十几毫秒。如果不做去抖处理一次按下会被识别成多次触发。最稳妥的方案是连续扫描多次只有当同一按键连续多次处于同一状态时才确认状态变化时间窗口设在10ms左右比较合适。我试过一个“滚动状态机”的做法每个按键维护一个状态位扫描结果先缓存成一个“原始矩阵”和上一个周期的原始矩阵对比连续两个周期状态一致才更新逻辑矩阵。这种方案既不阻塞主循环又能有效滤除抖动。在72MHz主频下一次完整矩阵扫描加状态比较只花几十微秒完全不影响USB上报节奏。3.2 按键映射表与多层支持所有“可编程”的灵活性最终都体现在一张映射表上。typedef struct { uint8_t row; uint8_t col; uint16_t keycode; } keymap_entry_t; static const keymap_entry_t keymap[] { {0, 0, KC_A}, {0, 1, KC_B}, {0, 2, KC_C}, {1, 0, KC_LSFT}, // 按位置定义修饰键 };当时为了让代码更灵活我用了一个二维数组按下标定位行和列static const uint16_t keymap[MATRIX_ROWS][MATRIX_COLS] { {KC_ESC, KC_1, KC_2, KC_3, KC_4}, {KC_TAB, KC_Q, KC_W, KC_E, KC_R}, };这样写的好处是一目了然改键位就是改这个表编译烧录一次就生效。按键码可以定义成统一枚举兼容标准HID Usage ID把A到Z、F1到F12、修饰键全部映射成对应的USB HID键值。多层支持也不复杂本质上是“当前激活层”这个变量的切换。默认在第0层当按下FN键并且其状态为按下时层号切换到第1层。查表时先查当前激活层如果当前层对应的键码不是空值就使用当前层的定义否则回退到第0层。这是键盘圈非常常见的两层逻辑。3.3 USB HID键盘上报机制USB HID键盘上报的基本单位是8字节的报表结构是第0字节是修饰键状态位第1字节是保留字节第2到第7字节是同时按下的普通按键对应的HID键值。一个USB全速的HID键盘设备理论最大同时按键数量就是6个所以市面所谓的“全键无冲”属于另一个话题后面再展开讲。typedef struct { uint8_t modifiers; uint8_t reserved; uint8_t keys[6]; } hid_keyboard_report_t; void report_send(hid_keyboard_report_t* report) { uint8_t buf[8] {report-modifiers, report-reserved, report-keys[0], report-keys[1], report-keys[2], report-keys[3], report-keys[4], report-keys[5]}; // 调用USB设备发送函数 HID_SendReport(hUsbDeviceFS, buf, 8); }每次扫描完成后把当前按下的键码填入数组然后与上一次上报的报表比较内容有变化才发送这样能减少USB总线上的无效传输。修饰键的处理方式也和普通键一样只是放在第0字节的对应比特位上。比如左Shift对应bit0右Shift对应bit1Ctrl对应bit2/bit3。关于全键无冲USB HID协议的Boot协议下一次只能报6个非修饰键这是协议限制。真正意义上的“全键无冲”要么在设备端通过HID Report Descriptor声明键盘支持超过6键的报表长度要么使用NKRON-Key Rollover模式让每个按键使用独立的HID Usage Page来描述。但Windows的Boot Protocol支持有限很多键盘在BIOS界面依然回到6键模式这是正常现象。4. 实操阶段手把手点亮第一块板4.1 烧录前的检查清单画完原理图、做完PCB、焊好板子接下来就是把程序烧进去。在插电之前我强烈建议按这个清单过一遍首先用万用表二极管档检查3.3V和GND之间有没有短路这个检查必须放在第一步。其次检查USB座子的D和D-到MCU引脚之间是否导通确认没有把PA11和PA12画反。然后检查BOOT0跳线的默认状态正常运行时BOOT0应处于低电平。之后用ST-Link连接SWD接口注意SWDIO和SWCLK两根线还有GND。Keil里配置好下载器后先读一次芯片ID能读到就说明芯片和SWD链路都没问题。最后再插上USB线在设备管理器里看是否枚举出一个HID设备。如果枚举成功且没有黄色感叹号硬件链路就通了。我自己第一次测试时USB插入后电脑有提示音但设备管理器里显示的却是“未知USB设备”。排查半天发现是USB D上拉电阻焊的是0Ω导致枚举阶段电平状态不对。换成1.5kΩ后立刻恢复正常。这类问题属于“原理图看着没错、PCB也连对了、但参数用错了”的典型情况。4.2 常见问题速查表现象可能原因排查方式USB无任何反应D上拉电阻缺失或阻值错误测量PA12引脚电平正常空闲状态应接近3.3V插上电脑报未知设备晶振未起振或负载电容不匹配用示波器或逻辑分析仪看8MHz晶振引脚波形部分按键无响应二极管方向焊反或矩阵行列网络标号对应错误先用镊子短接对应行列交点测试MCU GPIO电平变化按键触发串键缺少二极管确认每个开关都串联1N4148且方向一致烧录时提示找不到目标SWDIO/SWCLK接反或BOOT0被拉高检查接线必要时按住RESET再点烧录键盘偶尔断连供电不足或USB走线过长换一根粗短线USB线检查VBUS电容容量排查逻辑有一个很实用的原则先软件后硬件。很多“按键失灵”其实只是映射表写错了不一定真的是电路问题。调试时先用一个简单的测试固件把所有按键的原始行列状态通过串口打印出来。确认矩阵扫描原始数据全部正确后再判断映射表和HID上报的问题。注意修改固件后重新烧录如果设备在系统中已经枚举成功需要拔插USB线或者重启一次系统HID描述符更新后才能生效。这不是设备坏了是操作系统缓存了旧的描述符。4.3 关于程序框架的一个偷懒技巧如果你不想从零开始写USB协议栈可以直接使用STM32CubeMX生成USB HID工程模板。CubeMX里配置USB外设为DeviceClass选择HID然后在生成的回调函数里填充上报数据即可。这个方式能帮你少写几百行底层代码把精力集中在矩阵扫描和按键映射这部分。唯一要注意的是CubeMX生成的报告描述符默认是“鼠标”的你需要手动改成键盘报告描述符。一个标准的键盘Report Descriptor并不长结构化写出来后核心就是把Usage Page设为Generic DesktopUsage设为Keyboard底层Report Count设为8个字节。网上能找到大量现成版本照着抄一遍基本就能跑。5. 拓展思路从“能用的键盘”到“好用的键盘”做完基础版之后想进一步提升可玩性的话有几个方向性价比很高。第一个是状态灯。用两个GPIO驱动LED一个表示当前层号一个表示CapsLock或自定义状态可以直观看到当前键盘处于哪一层。注意LED要串限流电阻一般330Ω到1kΩ都行。第二个是睡眠唤醒。USB HID键盘如果长期无操作Windows默认允许设备进入选择性挂起。如果你的固件没做远端唤醒功能有时候按第一下键是没有反应的。解决办法是在HID报告描述符里声明remote wakeup特性或者在固件里禁止USB进入挂起状态。这个细节很小但日常使用体验影响很大。第三个是宏功能。宏的本质是提前录制好的系列动作按下某一个键后按键映射表返回的不是一个HID键值而是一串需要依次发送的键值序列。实现方式是在上报阶段增加一个宏缓冲区主循环每10ms从缓冲区取一个键值发送出去直到缓冲区清空。注意宏发送期间要禁用其他按键上报不然容易产生状态混乱。我个人的深度体会是可编程键盘最值得投入时间的是“映射层设计”而不是硬件部分。硬件做出来能扫描、能上报就完成了80%的工程剩下20%的体验完全取决于你对键位的理解和布局设计。把常用符号放到不用移动手腕的位置把组合键拆成单键宏这才是可编程键盘真正的价值所在。这也解释了一个现象很多人照着开源项目把原理图和程序抄了一遍板子也能用但始终觉得“不过如此”。区别就在这一步——硬件和固件只是基础键位设计才是灵魂。我的建议是第一把板子不要追求一次到位先做出一个功能完整的原型再用一段时间把不顺手的地方记录下来改映射、改宏、甚至改硬件布局这才是可编程键盘最吸引我的地方。本文还有配套的精品资源点击获取