基于CircuitPython与KB2040的复古键盘USB化改造实战 1. 项目概述让经典键盘在现代计算机上重生手头有一块Commodore 16的裸键盘没有外壳也没有任何线缆和说明书。看着这排布独特的键帽和那个20针的老式接口一个念头冒了出来能不能让它摆脱那台早已退役的Commodore 16主机直接变成一台能在我的Mac或PC上使用的USB键盘这不仅仅是为了怀旧更是对一种经典输入设备工作原理的深度探索和实用化改造。键盘矩阵这个从早期计算机时代沿用至今的技术是理解绝大多数键盘如何工作的钥匙。通过微控制器扫描这些行列交叉点我们就能捕捉每一次按键再通过USB HID协议与计算机对话。这次我选择用CircuitPython和一块专为键盘项目设计的Adafruit KB2040开发板来解开这个谜题并完成这次跨越时代的连接。这个项目非常适合对嵌入式开发、硬件交互或复古计算感兴趣的爱好者。无论你是想复活家里吃灰的老键盘还是想深入了解键盘矩阵和USB HID协议的工作原理甚至是为某个特殊项目定制输入设备这个过程都能提供从硬件连接到软件逻辑的完整视角。你不需要是电子工程专家但需要一点动手焊接的勇气和阅读代码的耐心。最终你将得到一台独一无二、带有浓厚复古气息的可用键盘更重要的是你将完全掌握其从物理信号到逻辑按键的整个转换链条。2. 核心硬件解析与方案选型2.1 理解键盘矩阵一切的基础在深入接线和编码之前我们必须先理解手中的这块键盘是如何工作的。Commodore 16键盘像绝大多数老式键盘一样使用的是无源矩阵方案。它内部没有为每个按键单独配备微动开关和控制器而是用一种更聪明也更节省成本和引脚的方式矩阵扫描。你可以把键盘想象成一个8行8列的网格总共64个交叉点每个交叉点放置一个按键开关。微控制器在这里是KB2040的工作就是不断地、快速地“询问”每个交叉点“嗨你被按下了吗”但它不是一个个问那样需要64根信号线。它采用行列扫描法先将所有行线设置为输出模式并依次将每一行拉低或拉高同时将所有列线设置为输入模式并读取其电平。当某一行被激活时如果这一行上的某个按键被按下那么这个按键所在的列线就会因为与行线连通而被拉低或拉高微控制器通过检测列线的电平变化就能精确定位是哪一个按键被触发。这种设计的精妙之处在于用MN根线本例中是8816根就能检测M*N个按键64个。但它的一个著名缺陷是“鬼键”问题当同时按下三个或更多特定位置的按键时可能会产生一个虚假的、本未被按下的按键信号。这是因为在无二极管的矩阵中电流可以沿多条路径形成意外的回路。我们后文的代码会专门处理这个问题。2.2 硬件选型为什么是Adafruit KB2040工欲善其事必先利其器。为这个项目选择主控板时我主要考虑了以下几点而Adafruit KB2040几乎是为这类项目量身定做的CircuitPython原生支持KB2040预装了CircuitPython这是一个基于Python 3的微控制器编程环境。其最大的优势是开发体验极其友好无需复杂的编译、烧录工具链像操作U盘一样拖放代码文件即可运行。对于快速原型开发和学习来说这大大降低了门槛。丰富的GPIO与USB HID支持本项目需要16个GPIO引脚来连接键盘矩阵8行8列。KB2040基于RP2040芯片提供了充足的GPIO。更重要的是CircuitPython的usb_hid库和keypad库已经过充分优化可以稳定、高效地将键盘事件转换为标准的USB HID报告描述符让电脑将其识别为即插即用的键盘设备。“键盘友好”的物理设计KB2040的板型仿照了流行的Arduino Pro Micro尺寸小巧可以直接嵌入键盘外壳。其引脚排列也考虑到了键盘矩阵的典型接线需求将GPIO分列两侧方便布线。强大的社区与文档Adafruit提供了极其详尽的学习指南、库文档和示例代码。当你在开发中遇到问题时有很大概率能在其社区或已有的教程中找到答案。注意虽然本项目以KB2040为例但核心原理适用于任何支持CircuitPython、具备足够GPIO并能运行keypad.KeyMatrix和usb_hid库的开发板例如QT Py RP2040、Feather RP2040等。你需要根据所选板卡的引脚定义调整代码中的rows和cols列表。2.3 物料清单与连接规划除了核心的KB2040我们还需要一些“桥梁”来连接它和那个20针的键盘接口Adafruit KB2040开发板项目的大脑。20针母头转接件或杜邦线键盘端的接口是0.1英寸间距的20针单排母座。我们需要与之匹配的20针公头或者直接用16根杜邦线因为只用到其中16个针脚焊接上去。连接线为了可靠性和整洁我推荐使用预制的杜邦线。你需要决定KB2040端的连接方式方案A推荐购买公对母杜邦线。将母头端插在KB2040的排针上公头端连接键盘。这样无需焊接可随时拔插修改。方案B购买公对公杜邦线并配合一个微型面包板。将KB2040和键盘线都插在面包板上进行连接。适合频繁测试。方案C直接焊接。将线缆直接焊接到KB2040的焊盘上最为牢固适合最终成品。USB数据线一根Type-C数据线用于给KB2040供电并与电脑通信。最关键的一步是弄清楚键盘20针接口上哪16个针脚对应着我们需要的8行8列。这需要一点“侦探工作”。幸运的是对于Commodore 16这类经典机型互联网上通常能找到其维修手册或原理图。通过查阅资料我确定了引脚定义如下行线 (Rows)对应键盘连接器的引脚 6, 16, 1, 13, 11, 12, 8, 19列线 (Cols)对应键盘连接器的引脚 5, 3, 10, 9, 7, 17, 14, 15, 18未使用/其他引脚4是可选接地引脚2是防插反的定位键引脚5和20未使用。有了这个映射表我们就可以规划KB2040上的GPIO分配了。我选择的引脚分配方案兼顾了电路板布局的便利性和CircuitPython库的兼容性最终确定的连接关系如下表所示键盘引脚 (功能)连接到 KB2040 引脚在代码中对应的对象19 (Row 0)D4board.D48 (Row 1)A0board.A012 (Row 2)D2board.D211 (Row 3)MOSIboard.MOSI13 (Row 4)D9board.D91 (Row 5)D10board.D1016 (Row 6)D6board.D66 (Row 7)A3board.A318 (Col 0)D3board.D315 (Col 1)D8board.D814 (Col 2)D7board.D717 (Col 3)D5board.D57 (Col 4)A1board.A19 (Col 5)MISOboard.MISO10 (Col 6)SCKboard.SCK3 (Col 7)A2board.A2实操心得在焊接或插线前强烈建议用万用表的蜂鸣档或电阻档逐一验证每个按键按下时对应的行和列引脚是否导通。这能提前发现键盘本身可能的接触不良问题避免软件调试时走弯路。同时给每根连接线贴上标签能极大减少接错线的概率。3. 基础固件开发从扫描到按键事件硬件连接妥当后我们就可以开始编写让键盘“活过来”的代码了。CircuitPython的keypad库为我们封装了矩阵扫描的复杂细节让开发变得异常简单。3.1 项目初始化与库准备首先我们需要准备好开发环境。将KB2040通过USB线连接到电脑它应该会显示为一个名为CIRCUITPY的U盘。我们需要将必要的库文件放入该磁盘的lib文件夹中。对于这个基础版本我们需要两个核心库adafruit_hid用于实现USB键盘功能。keypad.mpy通常已内置于CircuitPython中无需单独放置。最方便的方法是下载项目捆绑包Project Bundle它包含了所有必需的库文件和主程序code.py。将捆绑包解压后把lib文件夹里的内容全部复制到KB2040的CIRCUITPY磁盘的lib文件夹内同时将code.py文件复制到磁盘根目录。完成后安全弹出磁盘KB2040会自动重启并运行新代码。3.2 核心代码逐行解析让我们打开code.py看看最基础的键盘实现是如何工作的。# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries # SPDX-License-Identifier: MIT 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模块是扫描键盘矩阵的核心。adafruit_hid相关的导入让我们能发送标准的USB键盘键值。usb_hid模块用于初始化USB HID设备。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]这里定义了行和列引脚列表。顺序至关重要。这个顺序不是随意排列的它必须与键盘矩阵的物理扫描顺序以及我们后续定义的键值映射表keycodes的顺序严格对应。keypad.KeyMatrix会按照这个列表的顺序来扫描行和列并为每个交叉点按键分配一个唯一的key_number从0开始按行优先顺序递增。keycodes [ K.BACKSPACE, K.ENTER, K.LEFT_ARROW, K.F8, K.F1, K.F2, K.F3, K.LEFT_BRACKET, # 第0-7个按键 K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT, # 第8-15个按键 K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X, # 第16-23个按键 # ... 后续键值定义 ]这是整个项目的灵魂——键值映射表。它是一个包含64个元素的列表每个元素对应矩阵中一个特定位置的按键所应发送的USB键值。例如列表第一个元素K.BACKSPACE对应着key_number为0的按键即第一行第一列。如何确定哪个键值对应哪个物理按键这需要结合Commodore 16的键盘布局图和我们的扫描顺序来逐个确定是一个需要耐心核对的过程。在这个“基础映射”中我采用了一种“位置映射”策略尽量让键帽上印的字符在标准US键盘布局的相同相对位置上触发。例如Commodore键盘左上角的键就映射为PC键盘左上角的ESC或Backspace。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)主循环简洁而高效。首先我们创建一个Keyboard对象。然后使用keypad.KeyMatrix上下文管理器初始化键盘矩阵扫描器。在无限循环中我们不断检查是否有新的按键事件keys.events.get()。keypad库会在后台自动进行扫描当检测到按键状态变化按下或释放时就会产生一个事件。一旦获取到事件ev我们通过ev.key_number得到是哪个按键然后用这个编号作为索引从keycodes列表中查找对应的USB键值。最后根据ev.pressed是True按下还是False释放调用kbd.press()或kbd.release()函数将事件转发给电脑。至此一个最基本的USB键盘适配器就完成了。注意事项如果上电后部分按键无反应或触发错误键值首先检查硬件连接确保没有虚焊或错线。如果硬件无误则很可能是rows/cols列表顺序或keycodes映射表与物理矩阵不匹配。你可以尝试交换rows或cols列表中两个引脚的位置或者在keycodes列表中调整键值的顺序来修正。使用下一节介绍的“键盘矩阵探测仪”工具可以极大地帮助诊断。4. 高级功能实现超越基础输入基础版本已经能让键盘工作但为了更好的使用体验和应对复杂场景我们需要增加一些高级功能。这些功能展示了如何利用CircuitPython的特性构建一个更健壮、更灵活的输入系统。4.1 使用Asyncio实现非阻塞并发在基础版本中while True循环会阻塞地等待按键事件。这意味着如果你想同时让板载LED呼吸闪烁或者响应其他输入会非常困难。CircuitPython 8及以上版本支持asyncio库它允许我们以协作多任务的方式编写代码。高级版本代码中创建了一个AsyncEventQueue适配器类它让keypad的事件队列可以被await。这样键盘扫描任务就可以在等待按键时“让出”控制权让其他任务如LED动画、旋钮检测得以运行。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 # 异步等待按键事件期间其他任务可运行 # ... 处理事件 ... async def forever_task(): while True: await asyncio.sleep(.1) # 一个简单的后台任务示例 # 这里可以控制LED或其他外设 async def main(): forever asyncio.create_task(forever_task()) key asyncio.create_task(key_task()) await asyncio.gather(forever, key) asyncio.run(main())这种模式极大地提高了程序的灵活性和可扩展性是构建复杂交互设备的基石。4.2 实现FN修饰键与键位映射层Commodore 16键盘缺少现代键盘常见的F4-F12功能键、Page Up/Down等键。一个常见的解决方案是引入FN修饰键。当FN键被按住时其他按键的映射会发生改变这本质上实现了一个简单的“映射层”。在代码中我定义了一个FnState类来管理FN键的状态并建立了一个mods字典定义了当FN激活时哪些键应该被替换成其他键值例如数字1变成F1上箭头变成Page Up。class FnState: def __init__(self): self.state False def fn_event(self, event): self.state event.pressed # 更新FN键状态 def fn_modify(self, keycode): if self.state: return self.mods.get(keycode, keycode) # 如果FN按下查找替换键值 return keycode mods { K.ONE: K.F1, K.TWO: K.F2, K.UP_ARROW: K.PAGE_UP, K.DOWN_ARROW: K.PAGE_DOWN, # ... } fn_state FnState() K_FN fn_state.fn_event # 将FN键的事件处理函数赋给一个变量便于放入keycodes列表在主事件处理循环中在查找基础键值后会调用fn_state.fn_modify(keycode)来根据FN键状态决定最终发送的键值。这种设计模式可以轻松扩展出更多的映射层如Layer 1, Layer 2是实现高度自定义键盘逻辑的核心。4.3 幽灵按键的成因与软件防鬼影策略如前所述无二极管的键盘矩阵存在“鬼键”问题。其根本原因在于当按下三个特定位置的按键例如位于矩形三个角上的键时会形成一个回路使得第四个角上的按键即使未被按下也会在扫描中表现为“导通”。为了解决这个问题高级代码中实现了一个XKROFilter类X键无冲过滤器。其算法原理是跟踪当前按下的真实按键数量。在无二极管矩阵中理论上的安全无冲键数是22-Key Rollover, 2KRO。因此当过滤器检测到已有2个键被按下时它会将后续新按下的按键事件标记为“幽灵”并过滤掉只放行前2个真实按键的释放事件。class XKROFilter: def __init__(self, rollover2): self._count 0 self._rollover rollover ... def __call__(self, event): if event.pressed: if self._count self._rollover: # 如果已按下的键少于2个 yield event # 放行这个按下事件 self._count 1 else: # 按键释放 ... # 处理释放事件 self._count - 1重要权衡这种软件防鬼影策略是有效的但它是以牺牲部分多键组合为代价的。它会阻止所有超过2个键同时按下的情况即使其中一些组合在物理上并不会产生鬼键例如同时按下“Z”、“F”、“U”这三个不构成矩形的键。对于游戏或需要大量快捷键的专业软件这可能是个问题。最彻底的解决方案是在键盘矩阵的每个按键上串联一个二极管从硬件上杜绝鬼键实现全键无冲NKRO。但对于复古键盘改造软件方案是一个简单实用的折中。4.4 处理特殊键与Shift组合键Commodore键盘的键位布局与现代PC标准不同。例如“”符号在一个独立的按键上而双引号“”需要按Shift2。为了在“位置映射”模式下正确输出这些字符我们需要更精细地控制Shift修饰键的状态。代码中采用了一种巧妙的机制对于需要特殊Shift处理的键不在keycodes列表中直接放一个键值而是放一个元组例如K_AT (K.SHIFT, K.TWO)。在主循环中当检测到要发送的keycode是一个元组时它会执行一系列特殊操作临时清除当前所有已按下的修饰键主要是Shift。依次按下元组中指定的所有键对于(K.SHIFT, K.TWO)就是先按下Shift再按下2。释放所有按键。恢复之前被清除的修饰键状态。这样无论当前Shift键是否被按住按下“”键都能稳定输出“”符号。同时代码中还定义了一个shifted字典用于处理那些在Shift按下时需要输出不同字符的键例如Shift7输出单引号‘而不是PC标准的。主循环在检测到Shift键被按住时会优先从这个字典中查找替换的键值或元组。if shift_pressed: keycode shifted.get(keycode, keycode) # 如果Shift按下查找替换映射 if isinstance(keycode, tuple): # 处理需要发送组合键的特殊情况 if ev.pressed: kbd.report_modifier[0] old_report_modifier ~MASK_ANY_SHIFT # 临时清除Shift kbd.press(*keycode) # 发送组合键如(SHIFT, TWO) kbd.release_all() kbd.report_modifier[0] old_report_modifier # 恢复Shift状态5. 调试与排错从无声到畅打即使按照指南操作第一次尝试也难免遇到问题。以下是几个常见问题及其排查思路希望能帮你快速定位。5.1 硬件连接检查清单在怀疑代码之前先彻底检查硬件。供电与连接KB2040的电源灯是否亮起电脑是否识别到新的USB输入设备可在系统设置-键盘中查看如果没有检查USB线和数据接口。引脚连接这是最易出错的地方。使用万用表蜂鸣档在断电状态下逐一测试当按下某个键时对应的行引脚和列引脚之间是否导通电阻接近0欧姆确保16根连接线每一根都从键盘插座牢固地连接到了KB2040的正确引脚上。对照引脚分配表反复核对。接触不良老键盘的弹片或触点可能氧化。尝试多次按下有问题的按键或者用电子清洁剂喷一下键盘插座内部需谨慎并完全干燥后再通电。5.2 软件与配置问题排查如果硬件无误问题可能出在软件或配置上。CircuitPython版本确保你的KB2040上运行的是较新版本的CircuitPython8.0或以上以支持高级功能。你可以通过访问CIRCUITPY盘根目录下的boot_out.txt文件查看版本号。库文件缺失确认lib文件夹内包含了adafruit_hid及其依赖库。如果库缺失或版本不兼容键盘可能无法被识别或代码无法运行。串口输出调试在代码开头添加import supervisor并在主循环中加入print(ev)语句然后将KB2040连接到电脑使用串口终端工具如Mu编辑器、Thonny或screen/putty打开对应的串口如/dev/cu.usbmodemXX或COMX。当你按下按键时观察终端是否有事件打印出来。这能直接验证keypad库是否成功扫描到了按键。如果有事件打印但电脑没反应问题出在USB HID部分或键值映射。检查usb_hid初始化是否成功以及keycodes列表中的键值常量是否正确。如果没事件打印问题出在矩阵扫描。极有可能是rows和cols引脚列表定义错误与物理连接不匹配。5.3 使用“键盘矩阵探测仪”逆向工程引脚如果你手头是一个引脚定义完全未知的键盘那么“键盘矩阵探测仪”代码就是你的救星。它的原理是暴力扫描所有可能的GPIO引脚对当你按下按键时它会检测是哪两个引脚之间被短接从而逐步推断出哪些引脚属于行哪些属于列。将键盘的所有可能引脚或你猜测的引脚用杜邦线连接到KB2040的GPIO上。将“键盘矩阵探测仪”的代码复制为code.py并运行。打开串口终端程序会提示“Press keys now”。非常重要由于键盘无二极管你必须一次只按下一个键并按住直到终端打印出识别信息。依次按下键盘上不同位置的按键最好能覆盖所有行和列程序会逐步学习并输出它识别出的“行”引脚列表和“列”引脚列表。这个工具输出的引脚列表其物理顺序是任意的它只告诉你哪些引脚被归为行组哪些被归为列组。你需要将这个结果与代码中的rows和cols列表对应起来。通常你可以直接将探测仪输出的行列表和列列表复制到你的代码中作为初始值。如果按键映射完全错乱可以尝试将rows和cols两个列表整体交换。5.4 键位映射校准与个性化当所有按键都能触发事件但输出的字符不对时就需要校准键位映射。这是一个系统性的工作制作测试工具写一个简单的测试程序只打印按下的key_number不发送HID键值。将键盘按键网格画在纸上依次按下每个键记录下其对应的key_number。填充映射表根据你记录的key_number和键盘上实际的键帽字符对照adafruit_hid.keycode.Keycode中的常量重新构建keycodes列表。确保列表索引key_number与键值正确对应。处理特殊键对于Shift、Ctrl、Alt、GUIWin/Cmd等修饰键使用对应的K.LEFT_SHIFT,K.LEFT_CONTROL等常量。对于方向键、功能键等也有相应的常量如K.UP_ARROW,K.F1。个性化调整这是最有趣的部分。你可以完全重新定义每个键的功能。例如将不常用的键定义为宏快捷键或者为特定游戏配置一套键位。只需修改keycodes列表中相应位置的常量即可。6. 项目总结与扩展思路完成Commodore 16键盘的USB化改造不仅仅是为一个老物件赋予了新生命更是一次对底层硬件交互和协议转换的深刻实践。你亲手搭建了从物理开关到USB数据包的完整链路理解了键盘矩阵扫描、防鬼影、键值映射、USB HID协议等多个核心概念。这个项目的代码框架具有很强的通用性。只要你能确定键盘矩阵的行列引脚并构建出正确的键值映射表这套方案可以应用于绝大多数基于矩阵扫描的键盘无论是复古的Apple II、ZX Spectrum键盘还是你自己用机械轴焊接的客制化键盘。我个人在实际操作中的体会是耐心和系统性测试是关键。硬件连接要一丝不苟最好用颜色或标签区分行列线。软件调试要分步进行先用探测仪或简单打印程序验证硬件层再逐步添加HID功能和完善映射。遇到问题时善用串口打印输出它能提供最直接的内部状态信息。这个项目还可以向多个方向扩展添加背光与层指示利用KB2040剩余的GPIO和PWM功能驱动LED灯条并通过不同的灯光颜色或模式来指示当前激活的FN层或配置层。集成旋钮或滑块添加一两个旋转编码器或模拟摇杆通过asyncio任务同时读取它们的状态实现音量调节、鼠标滚轮或游戏控制功能。使用KMK固件如果你需要更强大的功能如复杂的多层切换、宏定义、RGB灯光效果、VIA/VIAL图形化配置支持可以考虑迁移到基于CircuitPython的KMK固件。它是一个功能完整的键盘固件项目提供了模块化配置和丰富的社区支持。设计外壳与PCB为你的复古键盘设计一个3D打印的新外壳甚至为KB2040和连接器设计一块定制的小PCB让整个改造项目看起来更精致、更专业。最后别忘了享受这个过程。每一次按键的清脆回响都是对计算历史的一次致敬也是你创造力的直接体现。祝你改造顺利打字愉快