
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个GPIO8行8列连接起来。有三种主流方式飞线连接用16根杜邦线直接连接。优点是快速、无需额外加工适合原型验证。缺点是线束混乱容易松动不适合长期使用。焊接连接将线缆直接焊接到KB2040的焊盘上。最稳固可靠适合最终成品内嵌安装。缺点是修改困难。制作转接电缆本项目采用的方法。制作一个一端是20针母头接键盘另一端是两个8针杜邦接头接KB2040的定制电缆。这样既整洁牢固又便于插拔和调试。我推荐使用预压接好的杜邦线和可拆式单排排针胶壳来制作电缆。你需要准备16根公对母杜邦线如果KB2040插在面包板上则需要公对公。2个8Pin的杜邦线胶壳。2个10Pin的杜邦线胶壳用于组合成20Pin接口。3.2 分步制作转接电缆制作过程的核心是引脚顺序的对应。我们必须严格按照从原理图分析出的行、列引脚顺序来接线。准备线束取出16根杜邦线将它们的母头端分别插入两个8Pin胶壳中每个胶壳插满8根。这将成为连接KB2040的一端。组合键盘端接口取一个10Pin胶壳将其与一个已插好线的8Pin胶壳母头端已固定对齐。注意胶壳上通常有三角箭头(▲)标记“Pin 1”的位置。顺序接线从10Pin胶壳的第1孔开始插入第一个8Pin胶壳中第一根线的公头。跳过10Pin胶壳的第2、4、5孔根据原理图这些是定位键、接地和空引脚。继续将第一组8根线依次插入10Pin胶壳的第1, 3, 6, 7, 8, 9, 10, ?孔具体孔位需根据你的引脚定义表精确计算。第一组8根线接完后将第9根线即第二个8Pin胶壳的第一根线插入第二个10Pin胶壳的第1孔。同样根据引脚定义将第二组8根线依次插入第二个10Pin胶壳的相应孔位最后一个孔可能留空。固定与连接为了强度可以用一点点速干胶将两个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, rollover2): 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): # 只处理被过滤器放行的真实按键事件这个过滤器会阻止任何可能导致鬼键的第三键按下代价是牺牲了一些合法的三键组合如ZFU。对于大多数打字和游戏场景2KRO已经足够。5.4 处理复杂的Shift组合键逻辑映射Commodore键盘的符号布局很特别。例如双引号在Shift2上单引号在Shift7上键是独立键。要实现符合键帽字符的逻辑映射需要更精细的控制。独立键处理将键直接映射为一个动作元组(K.SHIFT, K.TWO)。当检测到这个键按下时程序会先清除当前的Shift状态然后模拟按下Shift2再释放所有键最后恢复之前的Shift状态。这确保了无论Caps Lock或Shift键是否被锁定都能输出。K_AT (K.SHIFT, K.TWO) keycodes[对应索引] K_ATShift覆盖映射对于Shift2本应输出双引号这种情况我们维护一个shifted字典。shifted { K.TWO: (K.SHIFT, K.QUOTE), # 当Shift已按下再按2时发送Shift引号即 K.SIX: (K.SHIFT, K.SEVEN), # Shift6 - # ... }在主循环中如果检测到Shift键被按住就先查这个字典如果找到覆盖映射就使用新的键元组。通过结合位置映射和逻辑映射并利用POSITIONAL True/False这样的配置变量你可以轻松在“保持键位肌肉记忆”和“符合键帽字符”两种使用习惯间切换。6. 调试技巧与“键盘矩阵侦探”工具在项目开始阶段或者当你面对一个完全未知的键盘时最头疼的就是确定哪根线是行哪根是列以及它们的顺序。手动用万用表测试64个点效率极低。为此我编写了一个称为“Key Matrix Whisperer”键盘矩阵侦探的辅助调试脚本。6.1 “侦探”脚本原理与使用这个脚本的核心思想是自动化扫描。它将你指定的所有GPIO引脚或板子上所有可用引脚两两配对轮流将一个设为输出高/低电平另一个设为输入并上拉/下拉。当按下某个键时这两个引脚会被短接输入引脚的电平会被输出引脚拉高或拉低从而被检测到。接线将键盘矩阵所有可能的引脚比如20根线全部连接到微控制器的GPIO上。运行脚本将脚本作为code.py上传。打开串行监视器REPL。交互测试脚本会提示“Press keys now”。然后你一次只按一个键并按住直到REPL中打印出检测到的引脚对。重复这个过程按遍所有键。分析结果脚本会自动分析所有检测到的连接关系聚类出两组引脚并分别标记为“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. 启用XKROFilter2键无冲过滤。键位输出错误1. 行/列列表顺序与物理连接不匹配。2. 键码映射表顺序错误。1. 这是最常见原因。交换rows和cols列表试试或调整列表内引脚顺序。一个技巧按顺序按下第一行的键看输出是否连续来验证映射。按键响应迟钝或连发1. 消抖参数不合适CircuitPythonkeypad库通常内置消抖。2. 主循环中有耗时操作阻塞。1. 检查keypad.KeyMatrix初始化是否有interval或debounce参数可调本例未使用。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 从原型到成品一些实用建议当所有功能调试完毕后你可以考虑将它变成一个整洁的成品内部安装KB2040尺寸小巧可以放进许多老键盘的壳体内部。使用短排针或直接焊接将连接线固定。供电大多数情况下USB的5V供电足以驱动键盘和微控制器。如果键盘有LED需检查总电流是否超过USB端口限额通常500mA。外壳改造你可能需要在键盘外壳上开一个微小的孔让USB-C线缆引出。使用直角USB-C接头可以更美观。固件固化CircuitPython的code.py在每次上电都会运行。如果你希望更接近“即插即用”的普通键盘可以考虑将代码编译成UF2固件直接刷入RP2040但这会失去CircuitPython的可编程灵活性需要权衡。这个项目最大的乐趣在于它融合了硬件考古、嵌入式编程和软件调试。当你用一块三十多年前的键盘在今天的电脑上流畅地敲出这些文字时那种跨越时空的连接感正是DIY精神的精髓所在。希望这份详细的记录能帮你顺利唤醒沉睡的经典设备。