基于RP2040 PIO与CircuitPython的IBM Model F键盘USB转接方案

发布时间:2026/5/16 23:07:11

基于RP2040 PIO与CircuitPython的IBM Model F键盘USB转接方案 1. 项目概述让经典IBM键盘在现代电脑上重生如果你和我一样对老式机械键盘那种扎实、清脆的“咔嗒”声和独特手感念念不忘同时又对它们无法直接插在现代电脑上感到惋惜那么这个项目就是为你准备的。我最近从朋友的一堆旧物里淘到了一块经典的IBM Model F键盘就是那种左侧有一排功能键、沉甸甸的大家伙。它的手感无与伦比但那个古老的5针DIN接口让它与现代USB世界格格不入。与其让它躺在角落里吃灰不如动手改造让它重获新生。这个项目的核心目标很明确将一台使用旧式XT协议的IBM PC键盘改造为即插即用的标准USB HID键盘。听起来像是需要复杂的硬件逆向工程其实不然。得益于像Raspberry Pi RP2040这类现代微控制器的强大能力特别是其独特的PIO可编程输入输出外设我们可以在软件层面优雅地解决协议转换问题。再结合CircuitPython这种对开发者极其友好的嵌入式Python实现整个开发过程变得直观且高效。最终你得到的不仅是一个可用的转接器更是一个深入理解键盘通信协议、微控制器实时编程和USB HID规范的绝佳学习案例。无论你是复古硬件爱好者、嵌入式开发新手还是想为客制化键盘项目寻找灵感这个过程都充满了乐趣和收获。2. 核心硬件解析与选型思路动手之前我们需要理清整个系统的硬件构成。这不仅仅是简单的连线每一个元件的选择都关系到项目的稳定性、兼容性和最终体验。2.1 主角剖析IBM PC/XT键盘与它的通信协议我们改造的对象是IBM PC/XT时代的83键Model F键盘。识别它的关键特征左侧垂直排列的10个功能键F1-F10没有独立的数字小键盘以及那个标志性的5针DIN接口。后来更常见的IBM Model M或PC/AT的101/102键键盘虽然接口外形一样但协议已升级为发送多字节扫描码与本项目代码不兼容这点务必注意。它的通信协议是整套改造的基石理解它才能写好解码程序。这是一种时钟同步的串行协议具有以下几个关键特点单向通信数据只能从键盘流向主机键盘不会接收来自主机的指令除了复位。信号线主要使用两根线——时钟Clock和数据Data。时钟线由键盘产生用于同步每一位数据的读取。数据帧格式每一帧包含11个位。1个起始位总是0。8个数据位扫描码。1个“按下/释放”标志位0表示键按下1表示键释放。1个停止位总是1。电气特性键盘端输出为开集电极Open Collector。这意味着键盘内部的电路只能将数据线或时钟线“拉低”到地GND而不能主动将其驱动到高电平5V。线路的高电平状态依靠主机端的上拉电阻实现。注意协议细节是后续编写PIO状态机的直接依据。特别是“开集电极”和5V电平的特性直接决定了我们必须在RP2040的输入引脚上添加下拉电阻进行电平转换否则有损坏微控制器引脚的风险。2.2 大脑与桥梁微控制器开发板选型我们需要一个既能精确捕捉古老键盘时序又能扮演现代USB键盘角色的核心。基于RP2040微控制器的开发板是近乎完美的选择。为什么是RP2040其独一无二的PIO可编程输入输出外设是本项目的“杀手锏”。PIO是可以独立于CPU核心运行的可编程状态机能够以极高的时间精度和极低的延迟处理简单的、时序严格的数字协议如我们键盘的串行协议。这意味着CPU可以被解放出来处理更复杂的逻辑如USB通信而不会因为忙于轮询引脚状态而丢失任何一次按键事件。RP2040的PIO甚至内置了FIFO缓冲区可以缓存多个按键事件确保“全键无冲”成为可能。开发板选择Adafruit QT Py RP2040。我选择了这款板子原因如下尺寸小巧最终成品可以做得非常紧凑。引脚布局合理所需的GPIO引脚用于时钟和数据以及5V、GND电源引脚都便于连接。原生USB-C接口连接现代电脑更方便。完美的CircuitPython支持Adafruit官方维护软硬件兼容性有保障。 当然这并不是唯一选择。任何基于RP2040且带有USB接口的开发板理论上都可以例如KB2040、Raspberry Pi Pico等。你只需要根据板子的引脚定义在代码中稍作调整即可。2.3 接口与连接电平转换的奥秘键盘的5针DIN接口与MIDI设备使用的接口物理上完全相同这给我们带来了便利——可以很容易地买到面包板兼容的5针DIN母座进行连接。引脚定义如下面对母座焊脚视图引脚1时钟Clock引脚2数据Data引脚3未连接/复位本项目未使用引脚4地GND引脚5电源5V最关键的电路设计在于电平转换。如前所述键盘在空闲时时钟和数据线通过其内部或历史上主机内的上拉电阻处于5V高电平。然而RP2040的GPIO引脚最高耐受电压约为3.3V长期接入5V信号会损坏芯片。解决方案是创建一个电阻分压网络。我们在RP2040的每个输入引脚时钟和数据到地GND之间连接一个下拉电阻。这样当键盘将线路拉低时RP2040看到的是0V当键盘释放线路线路被上拉到5V时电流会流经键盘的上拉电阻约2.2kΩ和我们添加的下拉电阻在RP2040引脚处形成一个分压后的电压。计算一下假设键盘内部上拉电阻R_keyboard 2.2kΩ我们添加的下拉电阻R_pull_down 4.7kΩ。当线路被释放时RP2040引脚处的电压 V_rp2040 5V * (R_pull_down / (R_keyboard R_pull_down)) ≈ 5V * (4.7 / (2.24.7)) ≈ 3.4V。这个电压在RP2040的安全输入范围内。我手头正好有2.2kΩ的电阻计算下来电压约为2.5V同样安全且能可靠识别高电平。因此使用2.2kΩ至4.7kΩ的下拉电阻都是可行的。最终接线方案QT Py RP2040的5V引脚 - DIN座引脚5为键盘供电。QT Py RP2040的GND引脚 - DIN座引脚4共地。QT Py RP2040的SCK(GPIO6) 引脚 - DIN座引脚1时钟并接一个2.2kΩ - 4.7kΩ电阻到GND。QT Py RP2040的MISO(GPIO4) 引脚 - DIN座引脚2数据并接一个2.2kΩ - 4.7kΩ电阻到GND。使用面包板可以方便地搭建测试电路。对于想制作永久性转接器的朋友完全可以设计一个小型PCB将DIN座、电阻和QT Py集成在一起体积可以做到非常小巧。3. 软件环境搭建与CircuitPython固件部署硬件连接好后我们需要为RP2040开发板注入“灵魂”——刷入CircuitPython固件并配置开发环境。3.1 为什么选择CircuitPython对于此类硬件交互项目我们通常有几种编程选择C/C如Arduino、Pico SDK、MicroPython、CircuitPython。我强烈推荐CircuitPython原因在于极简的开发流程无需安装复杂的IDE或编译工具链。板子刷入固件后会作为一个名为CIRCUITPY的U盘出现。直接用任何文本编辑器编辑盘里的code.py文件保存后代码自动运行。这种“编辑-保存-运行”的循环效率极高。丰富的硬件抽象库Adafruit提供了大量高质量的驱动库本例中用到的adafruit_hid,adafruit_pioasm等封装了底层细节让我们能专注于业务逻辑。交互式REPL通过串口终端可以实时与板子交互打印调试信息非常方便。3.2 刷写CircuitPython固件详细步骤获取固件访问CircuitPython官网找到Adafruit QT Py RP2040的页面下载最新的.uf2格式固件文件。进入Bootloader模式按住QT Py板上的BOOTSEL按钮通常标记为“BOOT”。在保持按住的同时短按一下Reset按钮。继续按住BOOTSEL按钮约1-2秒直到电脑上出现一个名为RPI-RP2的可移动磁盘。备选方法在板子未通电时按住BOOTSEL按钮然后插入USB线等待RPI-RP2磁盘出现后松开。刷写固件将下载好的.uf2文件直接拖拽或复制到RPI-RP2磁盘中。磁盘会自动弹出。稍等片刻一个新的名为CIRCUITPY的磁盘会出现。这表明CircuitPython已成功运行。实操心得很多USB线仅能充电无法传输数据。如果你始终看不到RPI-RP2磁盘首先怀疑你的USB线。务必使用一条已知良好的数据线。3.3 安全模式与故障恢复在开发过程中你可能会遇到代码写错导致板子无响应、CIRCUITPY磁盘无法访问的情况。这时就需要用到安全模式。进入方法在板子启动或复位后的最初1秒内此时板载LED可能闪烁黄色快速按一下Reset按钮。这相当于一个“慢速双击”。作用安全模式会跳过用户代码boot.py,code.py的执行并禁用自动重载但保持CIRCUITPY磁盘可读写。这样你就可以删除或修改有问题的代码文件了。终极恢复如果板子状态异常到连安全模式都无法进入或CIRCUITPY磁盘根本不出现可以使用“核弹”UF2文件。这是一个特殊的固件会彻底擦除Flash。去Adafruit的下载页面找到对应板子的“erase”或“nuke”UF2文件用同样的Bootloader方式刷入。这会清空所有数据让你可以重新开始刷入CircuitPython固件。4. 核心代码实现与协议解析逻辑一切准备就绪现在我们来深入核心看看代码如何让古老的键盘“说”出现代USB的语言。我们将项目文件包包含code.py和必要的库解压后全部复制到CIRCUITPY磁盘的根目录即可。4.1 项目代码结构概览主要的逻辑都在code.py文件中。它主要做了三件事配置PIO状态机实时读取键盘的时钟和数据线。维护扫描码映射表将IBM键盘的扫描码转换为USB HID键值。主循环从PIO读取数据查表转换并通过USB HID接口发送按键报告。4.2 PIO状态机硬件级协议抓取这是整个项目最精妙的部分。我们使用adafruit_pioasm库来编写一段简单的PIO汇编程序。program adafruit_pioasm.Program( wait 0 pin 2 in pins, 1 wait 1 pin 2 , build_debuginfoTrue)这三行汇编代码构成了一个状态机wait 0 pin 2等待时钟线变为低电平0。这是键盘发送每一比特数据的起始条件。in pins, 1当时钟为低时将数据线pin的状态0或1采样并移入内部移位寄存器。in pins, 1表示每次从指定的输入引脚组中移入1个比特。wait 1 pin 2等待时钟线恢复为高电平1。这完成了对当前数据比特的读取并准备读取下一个比特。这个状态机会在每个时钟下降沿读取数据线循环执行。我们通过rp2pio.StateMachine来配置和启动这个状态机sm rp2pio.StateMachine(program.assembled, first_in_pin board.MISO, # 指定第一个输入引脚数据线 in_pin_count 3, # 总共监控3个引脚数据、时钟、但程序只用了pin 2作为条件 pull_in_pin_up 0b111, # 内部上拉可选我们已有外部下拉 auto_pushTrue, # 当移位寄存器满时自动推送数据到RX FIFO push_threshold10, # 满10比特一帧数据后自动推送 in_shift_rightTrue, # 数据右移便于处理 frequency8_000_000, # 状态机运行频率 **program.pio_kwargs)关键参数push_threshold10设定为10是因为我们键盘的每一帧数据是11位1起始8数据1按下/释放1停止。PIO程序会持续采样当累积了10个比特从起始位后的第一个数据位开始算这里需要结合协议理解实际代码能工作证明其配置正确后会自动将这10个比特的数据作为一个16位整数的低10位推送到FIFO缓冲区等待主程序读取。4.3 扫描码映射表从XT到USB的翻译官键盘每次按键或释放都会发送一个唯一的扫描码。IBM PC/XT的扫描码集Scan Code Set 1与USB HID使用的键值Keycode完全不同。因此我们需要一个映射表来进行翻译。xt_keycodes [ None, K.ESCAPE, K.ONE, K.TWO, K.THREE, K.FOUR, K.FIVE, K.SIX, K.SEVEN, K.EIGHT, K.NINE, K.ZERO, K.MINUS, K.EQUALS, K.BACKSPACE, K.TAB, # ... 中间省略 ... K.F5, K.F6, K.F7, K.F8, K.F9, K.F10, K.KEYPAD_NUMLOCK, K.SCROLL_LOCK, # ... 后续键位 ... ]这个列表xt_keycodes的索引对应XT扫描码的数值。例如扫描码0x01对应K.ESCAPE列表索引1因为索引0是None。当PIO读取到扫描码0x01时我们就知道用户按下了ESC键并向电脑发送USB键值K.ESCAPE。注意事项这个映射表是针对83键Model F键盘的。如果你手头是其他布局的XT兼容键盘可能需要根据其实际发送的扫描码调整此表。你可以通过REPL打印出原始的扫描码数值来辅助调试。4.4 主循环事件处理与USB报告发送主循环不断从PIO状态机的FIFO中读取数据进行解码和转发。buf array.array(H, [0]) # 创建一个缓冲区存放读取的数据 kbd Keyboard(usb_hid.devices) # 初始化USB HID键盘设备 while True: sm.readinto(buf, swapFalse) # 从PIO读取一个数据到buf val buf[0] # 解析数据最高位是按下/释放标志低7位是扫描码经过移位 pressed not val 0x8000 # 根据协议标志位为0表示按下 key_number (val 8) 0x7f # 提取扫描码部分 if key_number len(xt_keycodes): print(f无效的键值: {key_number}) continue keycode xt_keycodes[key_number] # 查表获取USB键值 if keycode is None: continue # 忽略未定义的键如某些特殊键 # 根据按下或释放状态发送对应的USB HID报告 if pressed: kbd.press(keycode) else: kbd.release(keycode)循环中的关键点sm.readinto()是一个阻塞调用。如果FIFO中没有数据程序会停在这里等待。这比不断轮询效率高得多。解析出的key_number需要与映射表长度比较防止数组越界。最终通过adafruit_hid库的Keyboard对象调用press()或release()方法将标准的USB HID报告发送给电脑。操作系统会将其识别为一个普通键盘的输入。5. 调试技巧、优化与扩展思路项目基本功能完成后我们可能会遇到一些问题或者想让它变得更好。这里分享一些实战中的经验和想法。5.1 常见问题与排查指南问题现象可能原因排查步骤电脑完全无反应不识别为键盘1. USB数据线问题。2. CircuitPython固件未正确刷入。3.code.py代码有语法错误未执行。1. 更换USB线。2. 检查CIRCUITPY磁盘是否存在并重新拖入UF2文件。3. 连接串口终端如Mu编辑器、PuTTY查看REPL是否有错误输出。进入安全模式检查code.py。电脑识别为“CircuitPython设备”但按键无输入1. 键盘供电或接线错误。2. 电平转换电阻未接或值不对。3. PIO引脚配置错误。1. 用万用表测量DIN座引脚5是否有5V输出引脚4是否接地良好。2. 检查时钟和数据线到GND的下拉电阻2.2k-4.7k是否焊接/插接牢固。3. 在代码中增加print(val)在REPL中查看当按下按键时是否有数据输出。如果没有检查board.MISO和board.SCK的引脚定义是否与你的实际接线匹配。按键输入混乱或按一个键出现多个字符1. 扫描码映射表xt_keycodes与你的键盘不匹配。2. 按键抖动或接触不良。1. 在REPL中打印出原始的key_number值对照IBM XT扫描码表修正映射表。2. 老键盘可能需要清洁。在软件上可以增加简单的防抖逻辑例如在press事件后延迟几毫秒再处理后续事件但PIO读取是硬件级的通常不需要。部分组合键如CtrlC无效CircuitPython HID库的默认配置可能限制了同时按下的键数通常为6个普通键3个修饰键。这是库层面的限制。对于大多数应用足够。如果需要真正的全键无冲NKRO需要修改底层HID报告描述符这涉及更深入的USB知识已超出本基础项目范围。5.2 性能与稳定性优化降低CPU占用当前主循环使用sm.readinto()阻塞等待CPU利用率很低这是好设计。确保没有在循环内进行不必要的复杂计算或打印大量调试信息仅调试时开启。电源稳定性老键盘可能功耗不低。确保你的USB端口或集线器能提供足够的500mA电流。如果键盘工作不稳定如偶尔失灵尝试直接连接电脑主板后置USB口避免使用延长线或低功率集线器。代码健壮性现有的代码已经包含了基本的错误检查如无效键值。可以进一步增加对异常数据帧的过滤例如检查起始位和停止位是否正确但这需要更精细地解析PIO读取的原始10位数据。5.3 功能扩展与创意改造这个项目是一个完美的起点你可以在此基础上进行各种扩展支持更多键盘型号研究IBM PC/AT101/102键的扫描码集多字节。你需要修改PIO程序或主循环解析逻辑以处理多字节数据包并建立新的映射表。同理可以尝试适配其他使用类似串行协议的古老键盘如Apple II, Commodore 64等。增加键位重映射与宏功能在查表xt_keycodes之后press/release事件之前加入一层逻辑。你可以创建一个配置字典将物理扫描码映射到任意其他USB键值甚至映射到一串字符宏。将配置保存到CIRCUITPY磁盘的一个JSON文件中实现免编程配置。添加状态指示灯利用QT Py RP2040上的板载LED或额外连接的LED来显示Caps Lock、Num Lock、Scroll Lock的状态。这需要从电脑接收LED状态控制命令但原始的XT协议是单向的。一个变通方法是在电脑端运行一个后台程序通过串口CircuitPython的REPL向板子发送指令控制LED。制作一体化转接器放弃面包板使用小巧的Perfboard或直接设计PCB将DIN母座、电阻、QT Py RP2040甚至可以使用更小的RP2040模组集成在一个小盒子里形成一个坚固耐用的成品。融合其他输入设备RP2040有多个PIO状态机和足够多的GPIO。你可以设想一个“复古输入中心”同时接入一个IBM键盘和一个老式串口鼠标通过同一个USB接口模拟成复合HID设备键盘鼠标。改造完成后当我第一次用这台沉重的Model F在现代电脑上敲出这些文字时那熟悉的“咔嗒”声和扎实的反馈感瞬间带来了巨大的满足感。它不仅仅是一个可用的工具更是一段被重新激活的科技历史。整个项目最迷人的地方在于它用非常现代的、易于理解的工具CircuitPython搭建了一座通往过去技术的桥梁。你不需要是电子或固件专家只要跟着步骤理解其原理就能亲手完成这次跨越时代的对话。希望你的改造之旅同样顺利且充满乐趣。

相关新闻