news 2026/5/16 10:10:10

用CircuitPython与KB2040改造复古键盘为USB HID设备

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用CircuitPython与KB2040改造复古键盘为USB HID设备

1. 项目概述:让复古键盘在现代电脑上“复活”

如果你手头有一台像Commodore 16这样的经典老键盘,看着它独特的键帽和手感,却苦于没有对应的主机让它发挥作用,那这个项目就是为你准备的。我们不是要修复一台老电脑,而是要让这块充满时代感的键盘,直接变成你现代Windows、macOS或Linux电脑上即插即用的USB键盘。这听起来像是魔法,但核心原理并不复杂:利用一块小巧的微控制器板(比如Adafruit KB2040),运行CircuitPython程序,读取键盘矩阵的按键信号,并将其翻译成电脑能理解的USB HID(人机接口设备)协议。

键盘矩阵是这类老式键盘乃至现代机械键盘的通用设计。简单来说,几十个按键并非每个都独占一根信号线,而是被排列成网格状的行和列。微控制器通过依次给“行”线发送信号,并监听所有“列”线的反馈,就能以较少的GPIO引脚(比如8x8矩阵只需16个引脚)检测出64个按键中哪一个被按下。我们的任务,就是充当这个“翻译官”,准确识别矩阵中的按键,并发送正确的键码给电脑。

我这次改造的对象是一块Commodore 16的裸键盘。它比更常见的Commodore 64键盘要稀有,最明显的特征是顶排有四个方向键。没有原理图,只有一个20针的接口,盲猜接线组合会是一场噩梦。幸运的是,互联网档案馆和开源社区保存了足够多的资料,让我能逆向出它的矩阵引脚定义,这本身就是一次有趣的考古过程。整个项目,从硬件连接到软件编程,我将带你一步步走通,不仅让键盘工作,还会探讨如何实现防鬼键、FN层切换等高级功能。无论你是复古硬件爱好者、嵌入式新手,还是想深入了解HID设备原理的开发者,这篇记录都能提供一份可靠的“路线图”。

2. 核心思路与硬件选型解析

2.1 为什么选择CircuitPython和KB2040?

面对一个未知的键盘矩阵,我们有几个技术路线可选:用Arduino直接编写C++固件、使用更专业的QMK/VIA固件,或者像本项目一样采用CircuitPython。我选择CircuitPython,首要原因是开发效率。CircuitPython允许我们通过USB线连接板子后,直接像操作U盘一样拖拽.py文件进行编程,并在串行终端(REPL)中实时看到打印信息,调试按键映射、测试矩阵扫描异常方便。这对于需要反复试验引脚定义和键位映射的逆向工程来说,是巨大的优势。

其次,CircuitPython的keypad库封装了矩阵扫描的底层细节。我们不需要自己编写复杂的时序代码去逐行扫描、消抖,只需定义好行、列引脚列表,库就会自动处理,并以事件队列的方式提供“按键按下”和“按键释放”消息。这让我们能把精力集中在逻辑层:如何把物理按键位置映射到标准的USB键值。

硬件方面,Adafruit的KB2040几乎是为此类项目量身定做的。它基于RP2040芯片,外形仿照了在键盘DIY圈流行的Arduino Pro Micro,尺寸小巧,可以直接焊接在键盘PCB内部。更重要的是,它的引脚布局非常适合键盘矩阵:两侧各有9个GPIO,方便将行线和列线分开连接,布线整洁。当然,理论上任何支持CircuitPython、USB HID且GPIO数量足够的板子(如Feather RP2040, QT Py RP2040)都能胜任,但KB2040在物理设计上更“顺手”。

2.2 逆向工程:从20针接口到8x8矩阵

拿到一块没有标签的键盘,第一步是搞清楚它的引脚定义。Commodore 16键盘使用一个标准的0.1英寸间距、20针单排母座。通过搜索历史资料,我找到了它的官方原理图。从图中可以解读出关键信息:

  • 行线 (Rows): 8根,对应原理图中的R0-R7,在连接器上的引脚号为:6, 16, 1, 13, 11, 12, 8, 19。
  • 列线 (Columns): 8根,对应原理图中的C0-C7,引脚号为:5, 3, 10, 9, 7, 17, 14, 15, 18(注意,原理图显示有9列,但实际键盘矩阵为8x8,其中有一根可能是空置或用于其他功能,需结合测试确认)。
  • 未使用/接地: 引脚4是可选接地,引脚2是防误插的定位键,引脚20未使用。

仅仅知道物理引脚还不够,我们还需要知道每个行、列交叉点对应哪个键。这需要逻辑映射表。我通过分析Commodore 16的Kernel(核心)源代码,找到了键盘扫描码表。这张表以8x8的格式,清晰地列出了每个矩阵坐标对应的键位。例如,第0行第0列是“DEL”键,第0行第1列是“RETURN”键。有了物理引脚定义和逻辑映射表,我们就掌握了连接和编程所需的全部信息。

注意:这里有一个关键点,Commodore 16的键盘矩阵没有使用二极管。这意味着当同时按下三个或更多特定组合的键时,可能会产生“鬼键”(Ghost Key)——即一个并未被按下的键位被错误地检测到。这是无二极管矩阵的固有缺陷,我们必须在软件层面加以处理。后文会详细讨论解决方案。

3. 硬件连接与线缆制作

3.1 连接方案选择

我们需要将键盘的20针接口与KB2040的16个GPIO(8行+8列)连接起来。有三种主流方式:

  1. 飞线连接:用16根杜邦线直接连接。优点是快速、无需额外加工,适合原型验证。缺点是线束混乱,容易松动,不适合长期使用。
  2. 焊接连接:将线缆直接焊接到KB2040的焊盘上。最稳固可靠,适合最终成品内嵌安装。缺点是修改困难。
  3. 制作转接电缆:本项目采用的方法。制作一个一端是20针母头(接键盘),另一端是两个8针杜邦接头(接KB2040)的定制电缆。这样既整洁牢固,又便于插拔和调试。

我推荐使用预压接好的杜邦线可拆式单排排针胶壳来制作电缆。你需要准备:

  • 16根公对母杜邦线(如果KB2040插在面包板上,则需要公对公)。
  • 2个8Pin的杜邦线胶壳。
  • 2个10Pin的杜邦线胶壳(用于组合成20Pin接口)。

3.2 分步制作转接电缆

制作过程的核心是引脚顺序的对应。我们必须严格按照从原理图分析出的行、列引脚顺序来接线。

  1. 准备线束:取出16根杜邦线,将它们的母头端分别插入两个8Pin胶壳中,每个胶壳插满8根。这将成为连接KB2040的一端。
  2. 组合键盘端接口:取一个10Pin胶壳,将其与一个已插好线的8Pin胶壳(母头端已固定)对齐。注意胶壳上通常有三角箭头(▲)标记“Pin 1”的位置。
  3. 顺序接线
    • 从10Pin胶壳的第1孔开始,插入第一个8Pin胶壳中第一根线的公头
    • 跳过10Pin胶壳的第2、4、5孔(根据原理图,这些是定位键、接地和空引脚)。
    • 继续将第一组8根线,依次插入10Pin胶壳的第1, 3, 6, 7, 8, 9, 10, ?孔(具体孔位需根据你的引脚定义表精确计算)。
    • 第一组8根线接完后,将第9根线(即第二个8Pin胶壳的第一根线)插入第二个10Pin胶壳的第1孔
    • 同样,根据引脚定义,将第二组8根线依次插入第二个10Pin胶壳的相应孔位,最后一个孔可能留空。
  4. 固定与连接:为了强度,可以用一点点速干胶将两个10Pin胶壳的侧面粘合,形成一个稳固的20针连接器。最后,将两个8Pin杜邦接头分别插到KB2040的两侧GPIO排针上,确保“Pin 1”方向正确。

这个自制电缆的好处是,它把杂乱的16根线变成了一个整体接口,大大降低了接错线的风险,也使得键盘可以随时拔插。

4. 基础固件编写与键位映射

4.1 环境搭建与项目包

首先,确保你的KB2040已经刷入了最新版的CircuitPython固件。将板子通过USB连接电脑,它会显示为一个名为CIRCUITPY的U盘。

本项目代码依赖于两个核心库:adafruit_hid(用于发送USB键值)和CircuitPython内置的keypad。最简单的方法是下载Adafruit提供的项目捆绑包(Project Bundle),它包含了所有必要的库文件和主程序code.py。你只需解压后,将CIRCUITPY驱动器中已有的lib文件夹合并(或覆盖),并把code.py拖进去即可。

4.2 代码逐行解析

让我们看看最基础版本的code.py是如何工作的。

import board import keypad from adafruit_hid.keycode import Keycode as K from adafruit_hid.keyboard import Keyboard import usb_hid

导入部分board用于访问板子上的特定引脚(如board.D2)。keypad是核心,提供KeyMatrix类。adafruit_hid系列库负责将我们的操作转换为标准的USB键盘信号。

rows = [board.A3, board.D6, board.D10, board.D9, board.MOSI, board.D2, board.A0, board.D4] cols = [board.A2, board.SCK, board.MISO, board.A1, board.D5, board.D7, board.D8, board.D3]

引脚定义:这是整个项目的“地图”,必须与你实际的硬件连接完全一致。列表顺序对应着逻辑上的行0-7和列0-7。这里的顺序来源于逆向出的原理图,并与之前制作的电缆连接顺序相匹配。如果某个键不工作,首先应检查这两组列表是否与物理连接对应。

keycodes = [ K.BACKSPACE, K.ENTER, K.LEFT_ARROW, K.F8, K.F1, K.F2, K.F3, K.LEFT_BRACKET, K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT, # ... 后续键码 ]

键码映射表:这是最需要耐心调整的部分。这个列表有64个元素,顺序对应矩阵中从(行0,列0)到(行7,列7)的每一个交叉点。我采用的是位置映射(Positional Mapping),即根据键在键盘上的物理位置,映射到现代键盘上最接近的键。例如,Commodore键盘右上角的“@”键,被我映射成了左方括号K.LEFT_BRACKET。你也可以采用符号映射(Logical Mapping),即根据键帽上的字符来映射,但这需要处理更多Shift组合键的情况,后文会详述。

kbd = Keyboard(usb_hid.devices) with keypad.KeyMatrix(rows, cols) as keys: while True: if ev := keys.events.get(): keycode = keycodes[ev.key_number] if ev.pressed: kbd.press(keycode) else: kbd.release(keycode)

主循环:程序初始化一个USB键盘对象,并创建一个键盘矩阵扫描器。然后进入无限循环,不断检查是否有新的按键事件。ev.key_number就是被按下键在矩阵中的索引(0-63),我们用它作为下标从keycodes列表中取出对应的USB键码。如果是按下事件,就调用kbd.press()发送“按下”信号;如果是释放事件,就调用kbd.release()发送“释放”信号。这个循环极其高效,keypad库在底层处理了所有扫描和消抖。

至此,一个最基本的、可用的USB键盘适配器就完成了。将代码保存到KB2040,插上键盘和电脑,你应该就能用它来打字了。如果出现键位错乱或某些键无响应,请进入下一部分的排查环节。

5. 高级功能实现与优化

基础版本能工作,但体验并不完美。Commodore键盘缺少F4-F12功能键,无二极管设计会导致“鬼键”,且一些符号键(如@、”)的位置与现代键盘不同。接下来,我们通过增强版代码来解决这些问题。

5.1 使用异步编程(asyncio)管理多任务

基础版本的主循环在忙等待按键事件。如果我们想同时控制键盘上的LED,或者响应其他输入,就需要引入异步编程。

import asyncio class AsyncEventQueue: # ... 包装keypad的事件队列,使其可被await async def key_task(): kbd = Keyboard(usb_hid.devices) with keypad.KeyMatrix(rows, cols) as keys, AsyncEventQueue(keys.events) as q: while True: ev = await q # 异步等待按键事件,不阻塞CPU # 处理按键事件 async def led_animation_task(): while True: # 控制LED的代码 await asyncio.sleep(0.05) # 让出控制权 async def main(): key_task_instance = asyncio.create_task(key_task()) led_task_instance = asyncio.create_task(led_animation_task()) await asyncio.gather(key_task_instance, led_task_instance) asyncio.run(main())

通过asyncio,键盘扫描和LED动画(或其他任何任务)可以“同时”运行,微控制器会在它们之间高效切换,而不是被一个死循环独占。这对于构建功能复杂的输入设备非常有用。

5.2 实现FN修饰键与层功能

现代键盘常有FN键配合其他键实现多媒体控制或额外功能键。我们可以将Commodore键盘上不常用的“CLEAR/HOME”键定义为FN键。

class FnState: def __init__(self): self.state = False def fn_event(self, event): self.state = event.pressed # FN键按下时设为True,释放时设为False def fn_modify(self, keycode): if self.state: return self.mods.get(keycode, keycode) # 如果FN按下,查找替换键值 return keycode # 否则返回原键值 fn_state = FnState() # 定义FN组合键映射:当FN按下时,数字键1变成F1,上箭头变成PageUp等。 fn_state.mods = { K.ONE: K.F1, K.TWO: K.F2, # ... K.UP_ARROW: K.PAGE_UP, K.DOWN_ARROW: K.PAGE_DOWN, } # 在键码表中,将CLEAR/HOME键映射为FN键的处理函数 keycodes[某个索引] = fn_state.fn_event # 在主循环中,处理按键事件时加入FN判断 processed_keycode = fn_state.fn_modify(raw_keycode)

这样,当你按住“CLEAR/HOME”键再按“1”,电脑接收到的就是F1键,极大地扩展了键盘的功能。

5.3 防鬼键(Anti-Ghosting)算法

由于键盘没有二极管,当同时按下三个键如W、A、R(它们位于一个矩形的三个角上)时,由于矩阵扫描的电气特性,会导致第四个点(D键)也被错误地导通,这就是“鬼键”。

软件解决方案是实施N键无冲(NKRO)过滤,但受硬件限制,我们只能实现2键无冲(2KRO)。思路是:跟踪当前按下的真实键数,当第三个键被按下时,程序将其视为“幽灵”并忽略,直到有键释放,数量回到2以下。

class XKROFilter: def __init__(self, rollover=2): self._count = 0 self._rollover = rollover self._pressed_keys = [False] * 64 def __call__(self, event): if event.pressed: if self._count < self._rollover: self._pressed_keys[event.key_number] = True self._count += 1 yield event # 允许此事件通过 else: # 超过无冲限制,忽略此次按下事件 pass else: # 释放事件 if self._pressed_keys[event.key_number]: self._pressed_keys[event.key_number] = False self._count -= 1 yield event twokey_filter = XKROFilter(2) # 在主循环中 for ev in twokey_filter(raw_event): # 只处理被过滤器放行的真实按键事件

这个过滤器会阻止任何可能导致鬼键的第三键按下,代价是牺牲了一些合法的三键组合(如Z+F+U)。对于大多数打字和游戏场景,2KRO已经足够。

5.4 处理复杂的Shift组合键(逻辑映射)

Commodore键盘的符号布局很特别。例如,双引号"在Shift+2上,单引号'在Shift+7上,@键是独立键。要实现符合键帽字符的逻辑映射,需要更精细的控制。

  1. 独立键处理:将@键直接映射为一个动作元组(K.SHIFT, K.TWO)。当检测到这个键按下时,程序会先清除当前的Shift状态,然后模拟按下Shift+2,再释放所有键,最后恢复之前的Shift状态。这确保了无论Caps Lock或Shift键是否被锁定,都能输出@

    K_AT = (K.SHIFT, K.TWO) keycodes[对应索引] = K_AT
  2. Shift覆盖映射:对于Shift+2本应输出双引号这种情况,我们维护一个shifted字典。

    shifted = { K.TWO: (K.SHIFT, K.QUOTE), # 当Shift已按下,再按2时,发送Shift+引号(即") K.SIX: (K.SHIFT, K.SEVEN), # Shift+6 -> & # ... }

    在主循环中,如果检测到Shift键被按住,就先查这个字典,如果找到覆盖映射,就使用新的键元组。

通过结合位置映射和逻辑映射,并利用POSITIONAL = True/False这样的配置变量,你可以轻松在“保持键位肌肉记忆”和“符合键帽字符”两种使用习惯间切换。

6. 调试技巧与“键盘矩阵侦探”工具

在项目开始阶段,或者当你面对一个完全未知的键盘时,最头疼的就是确定哪根线是行,哪根是列,以及它们的顺序。手动用万用表测试64个点效率极低。为此,我编写了一个称为“Key Matrix Whisperer”(键盘矩阵侦探)的辅助调试脚本。

6.1 “侦探”脚本原理与使用

这个脚本的核心思想是自动化扫描。它将你指定的所有GPIO引脚(或板子上所有可用引脚)两两配对,轮流将一个设为输出高/低电平,另一个设为输入并上拉/下拉。当按下某个键时,这两个引脚会被短接,输入引脚的电平会被输出引脚拉高或拉低,从而被检测到。

  1. 接线:将键盘矩阵所有可能的引脚(比如20根线)全部连接到微控制器的GPIO上。
  2. 运行脚本:将脚本作为code.py上传。打开串行监视器(REPL)。
  3. 交互测试:脚本会提示“Press keys now”。然后你一次只按一个键,并按住直到REPL中打印出检测到的引脚对。重复这个过程,按遍所有键。
  4. 分析结果:脚本会自动分析所有检测到的连接关系,聚类出两组引脚,并分别标记为“Rows”和“Cols”。它给出的列表就是你的键盘矩阵行和列引脚。

重要提示:对于无二极管的矩阵,必须严格一次只按一个键。同时按多个键会导致脚本误判连接关系,因为鬼键现象会让它“看到”不存在的连接。

6.2 常见问题排查清单

即使有了“侦探”脚本,在实际制作中仍可能遇到问题。下面是一个快速排查指南:

现象可能原因解决方案
所有键均无反应1. USB未正确枚举。
2.code.py运行错误。
3. 电源或接地问题。
4. 行/列引脚列表全错。
1. 检查电脑设备管理器,确认HID设备出现。
2. 打开REPL查看是否有Python错误输出。
3. 确认键盘和板子共地,且板子供电充足。
4. 用“侦探”脚本重新确认引脚。
部分键无反应1. 单个行或列线连接不良。
2. 键码映射表中该位置键码为None或错误。
3. 该键物理损坏。
1. 检查对应引脚的电线、焊点。
2. 核对keycodes列表索引与物理位置。
3. 用万用表导通档测试该键开关。
按一个键出现多个字符1. 键码映射错误,一个索引对应了多个键?
2. 鬼键现象(无二极管矩阵按了特定多键)。
1. 仔细检查keycodes列表,确保64个元素一一对应。
2. 启用XKROFilter(2键无冲过滤)。
键位输出错误1. 行/列列表顺序与物理连接不匹配。
2. 键码映射表顺序错误。
1. 这是最常见原因。交换rowscols列表试试,或调整列表内引脚顺序。一个技巧:按顺序按下第一行的键,看输出是否连续,来验证映射。
按键响应迟钝或连发1. 消抖参数不合适(CircuitPythonkeypad库通常内置消抖)。
2. 主循环中有耗时操作阻塞。
1. 检查keypad.KeyMatrix初始化是否有intervaldebounce参数可调(本例未使用)。
2. 确保主循环或异步任务中没有time.sleep()长延时,改用await asyncio.sleep(0)释放控制权。
FN键或Shift组合键无效1. FN键未在键码表中正确映射为处理函数。
2.shifted字典或元组处理逻辑未生效。
3. 修饰键状态跟踪有误。
1. 确认K_FN(即fn_state.fn_event)被放在了键码表正确位置。
2. 调试打印shift_pressed状态和查找shifted字典的结果。
3. 检查MASK_ANY_SHIFT等修饰键位掩码计算是否正确。

6.3 从原型到成品:一些实用建议

当所有功能调试完毕后,你可以考虑将它变成一个整洁的成品:

  1. 内部安装:KB2040尺寸小巧,可以放进许多老键盘的壳体内部。使用短排针或直接焊接,将连接线固定。
  2. 供电:大多数情况下,USB的5V供电足以驱动键盘和微控制器。如果键盘有LED,需检查总电流是否超过USB端口限额(通常500mA)。
  3. 外壳改造:你可能需要在键盘外壳上开一个微小的孔,让USB-C线缆引出。使用直角USB-C接头可以更美观。
  4. 固件固化:CircuitPython的code.py在每次上电都会运行。如果你希望更接近“即插即用”的普通键盘,可以考虑将代码编译成UF2固件直接刷入RP2040,但这会失去CircuitPython的可编程灵活性,需要权衡。

这个项目最大的乐趣在于,它融合了硬件考古、嵌入式编程和软件调试。当你用一块三十多年前的键盘,在今天的电脑上流畅地敲出这些文字时,那种跨越时空的连接感,正是DIY精神的精髓所在。希望这份详细的记录,能帮你顺利唤醒沉睡的经典设备。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 10:10:06

Arduino机器人进阶:从避障到动态调速的传感器融合与控制闭环实践

1. 项目概述与核心价值玩Arduino机器人的朋友&#xff0c;估计都经历过从让轮子转起来&#xff0c;到尝试让机器人“聪明”一点的阶段。今天要聊的&#xff0c;就是这个进阶过程里绕不开的两个核心&#xff1a;环境感知和精准控制。说白了&#xff0c;就是怎么让机器人不仅会动…

作者头像 李华
网站建设 2026/5/16 10:03:54

OBS多平台直播插件:obs-multi-rtmp终极使用指南与架构解析

OBS多平台直播插件&#xff1a;obs-multi-rtmp终极使用指南与架构解析 【免费下载链接】obs-multi-rtmp OBS複数サイト同時配信プラグイン 项目地址: https://gitcode.com/gh_mirrors/ob/obs-multi-rtmp 在当今内容创作者蓬勃发展的时代&#xff0c;多平台同步直播已成为…

作者头像 李华
网站建设 2026/5/16 10:00:17

RuoYi框架国产化迁移实战:SpringBoot项目适配达梦数据库的完整路径

1. 为什么需要国产数据库迁移 最近几年&#xff0c;国产数据库的发展速度确实让人眼前一亮。作为技术负责人&#xff0c;我去年接手了一个重要任务&#xff1a;把公司核心的RuoYi框架SpringBoot应用从MySQL迁移到达梦数据库。说实话&#xff0c;刚开始心里也没底&#xff0c;毕…

作者头像 李华
网站建设 2026/5/16 9:59:07

YOLOv8s的C2F结构到底怎么工作的?结合代码与ONNX图给你画明白

YOLOv8s的C2F结构到底怎么工作的&#xff1f;结合代码与ONNX图给你画明白 在目标检测领域&#xff0c;YOLO系列模型一直以其高效的推理速度和良好的检测精度著称。YOLOv8作为该系列的最新成员&#xff0c;引入了一个名为C2F的核心模块&#xff0c;这个结构的设计理念和实现细节…

作者头像 李华
网站建设 2026/5/16 9:58:04

如何用淘金币自动化脚本每天节省20分钟?完整指南揭秘

如何用淘金币自动化脚本每天节省20分钟&#xff1f;完整指南揭秘 【免费下载链接】taojinbi 淘宝淘金币自动执行脚本&#xff0c;包含蚂蚁森林收取能量&#xff0c;芭芭农场全任务&#xff0c;解放你的双手 项目地址: https://gitcode.com/gh_mirrors/ta/taojinbi 淘金币…

作者头像 李华