
1. 从零到一为什么选择Python来驱动一把键盘如果你和我一样是个对客制化键盘和嵌入式开发都感兴趣的玩家那你肯定见过琳琅满目的方案从最经典的QMK固件到功能强大的ZMK再到各种基于Arduino、RP2040的现成方案。它们成熟、稳定社区资源丰富。那么为什么还要“自讨苦吃”用Python去手搓一把支持USB和蓝牙双模的键盘呢这听起来就像是用瑞士军刀去砍树——不是不行但总觉得有点奇怪。这正是这个项目的魅力所在也是我决定动手的原因。它不是为了替代那些成熟的方案而是为了探索一种完全不同的可能性。用Python来驱动键盘核心优势在于极致的灵活性与开发效率。QMK和ZMK本质上是C语言固件功能强大但修改和调试的门槛相对较高。当你有一个天马行空的想法比如让键盘的某个按键根据你电脑的CPU温度改变背光颜色或者让键盘宏与某个特定的Python脚本深度联动时用C去实现可能需要翻阅大量文档、理解复杂的回调函数和状态机。而用Python你可能只需要几行熟悉的代码调用几个现成的库就能快速实现原型。这个项目的本质是构建一个运行Python的“微型电脑”作为键盘的主控。它通过USB或蓝牙与主机通信但所有的按键扫描、层切换、宏定义、灯光逻辑全部由你写的Python脚本控制。这意味着你的键盘不再是一个固化的输入设备而是一个可编程的、能与你的数字工作流深度集成的智能终端。你可以轻松地让它执行系统命令、调用API、处理数据甚至运行一个小型的Web服务器来远程控制。这种“软件定义硬件”的体验是传统固件难以比拟的。当然这条路也有明显的挑战。Python是解释型语言运行效率远低于C对于需要高实时性的按键扫描和防抖处理需要更精巧的设计。同时整个系统的功耗管理也会比精心优化的嵌入式固件更复杂。但正是这些挑战让整个过程充满了学习和探索的乐趣。接下来我将带你完整走一遍我的手焊USB/蓝牙双模Python键盘的构建之旅从硬件选型、电路焊接到核心的Python驱动层编写再到高级功能的实现与优化。2. 硬件基石主控、矩阵与连接方案的选择与焊接一把键盘的硬件核心是按键矩阵和主控制器。对于我们的Python键盘主控的选择至关重要它需要能流畅运行Python解释器并具备USB和蓝牙通信能力。2.1 主控芯片选型RP2040与ESP32的权衡市面上能跑Python的微控制器不少但综合性能、生态、成本和双模需求我主要在两个方案间纠结Raspberry Pi PicoRP2040芯片和ESP32系列。Raspberry Pi Pico (RP2040) 这颗芯片是树莓派基金会推出的微控制器双核ARM Cortex-M0主频133MHz性能强劲。它的最大优势是极佳的MicroPython/CircuitPython支持。CircuitPython是Adafruit主导的、对初学者极其友好的Python变种内置了丰富的硬件驱动库包括USB HID人机接口设备和蓝牙库通过BLE。社区活跃资料丰富。缺点是原生不支持经典蓝牙Bluetooth Classic只支持低功耗蓝牙BLE而一些旧设备或特定应用对BLE HID的支持可能不完善。ESP32系列 (如ESP32-S3) 乐鑫的ESP32是物联网领域的明星集成Wi-Fi和双模蓝牙BLE Bluetooth Classic。这意味着它可以兼容几乎所有支持蓝牙的设备。通过MicroPython也能实现USB和蓝牙功能。其优势是无线连接能力更强、更通用。但在纯粹的“作为USB HID设备”的稳定性和社区针对键盘的MicroPython示例上可能略逊于RP2040的CircuitPython生态。我的选择是Raspberry Pi Pico。原因如下这个项目的首要目标是稳定实现一个可编程键盘USB是主要使用场景。CircuitPython对USB HID的支持是“一等公民”插上电脑就能被识别为键盘无需额外配置。对于蓝牙虽然只是BLE但现代操作系统Windows 10/11, macOS, Linux, Android, iOS都对BLE HID有很好的支持足以覆盖绝大多数使用场景。Pico的性价比和开发体验对我来说更友好。注意 如果你必须确保兼容老旧的蓝牙设备如某些旧款电视、游戏主机那么ESP32-S3是更稳妥的选择。你需要研究esp32模块下的bluetooth库并处理经典蓝牙的配对流程复杂度会稍高一些。2.2 手焊键盘矩阵告别飞线拥抱PCB热转印确定了主控接下来是键盘本体。我选择的是60%配列61键。构建矩阵有两种主流方法飞线法和自制PCB。飞线法 用漆包线或杜邦线直接将每个按键的引脚行和列连接起来。这是最灵活、成本最低的方法但也是“线材地狱”调试困难可靠性一般成品杂乱。自制PCB法 使用EDA软件如KiCad设计电路板然后通过热转印或嘉立创等厂家打样。这是我强烈推荐的方法。我使用了KiCad绘制了一个简单的双面板PCB。正面是按键开关的焊盘背面是行线和列线的走线。设计要点矩阵设计 60%键盘通常采用8行 x 8列的矩阵足以覆盖所有按键。每个按键跨接在一条行线和一条列线上。二极管每个按键必须串联一个二极管如1N4148方向从行线指向列线或反之取决于你的扫描逻辑。这是实现“全键无冲NKRO”的关键防止多个按键同时按下时产生“鬼键”。我在PCB上为每个按键焊盘都预留了二极管的位置。连接器 在PCB边缘放置一排排针将所有的行线8根和列线8根引出方便用排线连接到主控板。热转印需要激光打印机、覆铜板和熨斗有一定失败率但对于这种简单电路完全可行。焊接时先焊二极管和按键轴座确保二极管方向一致。焊接完所有轴座后用万用表的蜂鸣档逐一检查每个按键按下时对应的行和列是否导通这是排查虚焊和二极管方向错误的关键步骤。2.3 主控连接与供电将PCB引出的16根线8行8列连接到Pico的GPIO口。你需要仔细规划引脚避免使用那些被USB、系统功能占用的引脚如Pico的GPIO23-25。在CircuitPython中定义一个引脚列表即可。供电方面Pico可通过USB直接供电。为了实现蓝牙无线使用我增加了一块3.7V的锂电池如常见的602530型号和一个简单的充放电管理模块。Pico的VSYS引脚可以接受3-5V的输入电压将电池接在充放电模块的输出端再连接到VSYS即可。这样插上USB时自动为电池充电并供电拔掉USB后由电池供电。3. 软件核心CircuitPython驱动层与按键扫描逻辑硬件准备就绪后就是最核心的软件部分。我们将在Pico上刷入CircuitPython固件并编写code.py作为主程序。3.1 基础环境搭建刷入固件 按住Pico的BOOTSEL按钮插入电脑它会作为一个U盘出现。从CircuitPython官网下载对应Pico的.uf2固件文件拖入该U盘。完成后设备会自动重启并出现一个名为CIRCUITPY的U盘。安装必要库 CircuitPython内置了usb_hid库但为了更好的蓝牙支持我们需要Adafruit的adafruit_ble库。将下载的.mpy库文件复制到CIRCUITPY盘下的lib文件夹内。3.2 按键扫描状态机的实现在code.py中我们需要实现一个高效的按键扫描状态机。直接轮询GPIO在Python中不够高效我会使用keypad模块它是CircuitPython专门为键盘矩阵优化的模块效率很高。import board import keypad import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode # 定义键盘矩阵的行列引脚 rows [board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5, board.GP6, board.GP7] cols [board.GP8, board.GP9, board.GP10, board.GP11, board.GP12, board.GP13, board.GP14, board.GP15] # 初始化键盘矩阵扫描器 keys keypad.KeyMatrix(rows, cols, columns_to_anodesFalse) # 初始化USB HID键盘设备 kbd Keyboard(usb_hid.devices) # 定义键位映射表将每个按键的索引映射到对应的Keycode # 这是一个简单的60%配列示例你需要根据你的PCB布局来填写 KEYMAP [ Keycode.ESCAPE, Keycode.ONE, Keycode.TWO, Keycode.THREE, ... # 第一行 Keycode.TAB, Keycode.Q, Keycode.W, Keycode.E, ... # 第二行 # ... 以此类推 ] # 按键状态跟踪 pressed_keys set() while True: event keys.events.get() if event: keycode KEYMAP[event.key_number] if event.pressed: if keycode not in pressed_keys: kbd.press(keycode) pressed_keys.add(keycode) else: # 释放 if keycode in pressed_keys: kbd.release(keycode) pressed_keys.remove(keycode)这段代码已经可以实现一个基础的USB键盘。keypad模块内部会以优化的方式扫描矩阵并产生按下/释放事件我们只需要处理这些事件并调用HID库的press和release方法即可。3.3 蓝牙HID的集成与双模切换接下来集成蓝牙。目标是让键盘在启动时尝试通过USB连接如果未连接则自动开启蓝牙广播等待配对。import time import digitalio from adafruit_ble import BLERadio from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.standard.hid import HIDService from adafruit_ble.services.standard.device_info import DeviceInfoService # 初始化BLE ble BLERadio() hid HIDService() device_info DeviceInfoService(manufacturerMy Workshop, modelPythonKB v1) advertisement ProvideServicesAdvertisement(hid) advertisement.complete_name Python Keyboard # 检测USB是否连接的简单方法通过检测USB电源引脚状态 usb_power digitalio.DigitalInOut(board.GP24) # 检查VSYS或类似引脚具体需查手册 usb_power.switch_to_input(pulldigitalio.Pull.DOWN) usb_connected usb_power.value ble_active False if not usb_connected: print(USB未连接启动蓝牙...) ble.start_advertising(advertisement) ble_active True while True: if usb_connected: # USB模式使用之前的USB HID键盘逻辑 # ... (上面的按键扫描和kbd.press/release逻辑) pass else: # 蓝牙模式 if not ble.connected and not ble_active: ble.start_advertising(advertisement) ble_active True elif ble.connected: # 蓝牙已连接使用BLE HID发送按键 # 注意BLE HID的发送方式与USB HID略有不同需要调用hid.send_report() # 这里需要将按键事件转换为HID报告字节数组并发送 # 这是一个简化示例实际需要构建完整的报告描述符和数据结构 pass time.sleep(0.01) # 短暂延迟防止忙等待重要提示 上述蓝牙HID发送部分是一个高度简化的示意。实际上CircuitPython的adafruit_ble库中HIDService的使用比USB HID复杂你需要构建符合HID规范的报告描述符Report Descriptor和报告数据。Adafruit官方提供了一些HID设备的示例如鼠标你需要参照它们来适配键盘。这是整个项目中最复杂的部分之一可能需要深入研究BLE HID协议。双模切换逻辑 更完善的逻辑是无论USB是否插入都允许蓝牙被搜索。当USB插入时优先使用USB通信延迟更低。当USB拔出时如果蓝牙已配对则自动切换至蓝牙模式。这需要更精细的状态管理。4. 进阶玩法用Python赋予键盘“灵魂”当基础输入功能实现后Python的威力才真正开始展现。以下是我实现的一些进阶功能示例。4.1 动态宏与层管理在固件键盘中层和宏是预定义的。而在Python中它们可以是动态的、可配置的。import json import storage # 从JSON文件加载层和宏配置 try: with open(/layers.json, r) as f: layer_config json.load(f) except OSError: layer_config {layers: [{name: Base, keymap: [...]}]} current_layer_index 0 macros { email: [Keycode.CONTROL, Keycode.ALT, Keycode.K], # 发送组合键 hello_world: lambda: kbd.send(Keycode.H, Keycode.E, Keycode.L, Keycode.L, Keycode.O), # 发送字符串 run_script: lambda: exec(open(/script.py).read()) # 执行另一个Python脚本 } def handle_special_key(key_index): 处理非字符键如层切换、宏触发 if key_index 60: # 假设这是层切换键 global current_layer_index current_layer_index (current_layer_index 1) % len(layer_config[layers]) print(f切换到层: {layer_config[layers][current_layer_index][name]}) elif key_index 61: # 宏键1 macro_name layer_config[layers][current_layer_index][macros][0] if macro_name in macros: action macros[macro_name] if callable(action): action() else: kbd.send(*action) # 在主循环的按键处理中调用 if event.pressed: if event.key_number 60: # 特殊功能键范围 handle_special_key(event.key_number) else: # 从当前层的键位映射中获取Keycode keycode layer_config[layers][current_layer_index][keymap][event.key_number] kbd.press(keycode)你可以通过USB串口甚至是一个简单的Web服务器如果主控支持Wi-Fi如ESP32来实时修改layers.json文件无需重刷固件就能改变键盘的所有行为。4.2 系统集成与自动化键盘不再只是输入工具而是自动化流程的触发器。读取传感器 我在键盘内部加了一个温湿度传感器如AHT20。现在我的code.py可以每分钟读取一次温度如果温度过高就自动调低RGB背光亮度或者通过USB串口向电脑发送一条通知。与主机通信 通过USB的CDC串行设备功能键盘可以与电脑上的Python脚本进行双向通信。例如电脑脚本告诉键盘“下一个会议在5分钟后”键盘的OLED小屏幕如果安装了就会显示倒计时。或者键盘收到一个特定快捷键后触发电脑上的脚本执行一系列文件操作。状态指示 利用RGB LED用不同的颜色和动态效果显示键盘状态层状态、蓝牙连接状态、电池电量通过ADC读取电池电压、甚至可以是Git仓库的状态通过串口从电脑获取。4.3 性能优化与功耗管理Python的效率是绕不开的话题。以下是我采取的优化措施事件驱动代替忙等待 坚持使用keypad模块它用底层中断或高效定时器触发比用time.sleep循环查询GPIO快得多。减少全局查找 将频繁使用的对象如kbd.press赋值给局部变量。使用_下划线变量 对于不关心的返回值用_接收避免内存分配。功耗管理 在纯蓝牙模式下如果没有按键事件可以让主控进入轻睡眠模式time.sleep(1)并在GPIO中断中唤醒。CircuitPython的alarm模块可以支持深度睡眠但会重置程序需要保存状态。5. 踩坑实录从理论到稳定可用的关键细节这个过程绝非一帆风顺以下是几个让我耗费大量时间的“坑”及其解决方案。5.1 USB枚举失败与HID报告描述符最初电脑无法识别我的键盘。问题出在CircuitPython的HID报告描述符上。默认的HID设备可能不完整。解决方案是自定义报告描述符。我参考了CircuitPython源码中关于键盘的HID描述符并确保在代码中正确初始化了HID设备。# 这是一个更可靠的USB HID初始化示例 import usb_hid # 确保键盘设备在HID设备列表中 usb_hid.enable( (usb_hid.Device.KEYBOARD, usb_hid.Device.MOUSE, usb_hid.Device.CONSUMER_CONTROL) )5.2 蓝牙连接不稳定与配对绑定BLE连接有时会意外断开尤其是在Windows系统上。关键在于正确处理配对和绑定Bonding信息。CircuitPython的BLE库需要你将配对信息存储到非易失性存储中并在重启后恢复。import _bleio try: ble._adapter.load_bonding() # 尝试加载已绑定的设备信息 except: pass # 在连接建立后通常会自动保存绑定信息。确保你的代码没有在每次循环中重复初始化BLERadio。另一个常见问题是广播间隔和连接参数。通过调整广播间隔和连接间隔Connection Interval可以在功耗和响应速度间取得平衡。这需要在Advertisement和连接参数请求中进行设置。5.3 按键抖动与“Chattering”处理即使使用了keypad模块在低质量的轴体或焊接不良时仍可能遇到按键抖动Chattering即一次物理按下产生多个快速的电平变化导致字符连发。keypad模块内置了防抖逻辑但你可能需要调整其去抖时间。keys keypad.KeyMatrix(rows, cols, columns_to_anodesFalse, interval0.02, max_events64)interval参数决定了扫描间隔调大它可以增强防抖能力但会略微增加延迟。如果问题严重需要在软件层面做二次滤波比如记录按键按下时间在短时间内忽略重复事件。5.4 供电与电池管理在电池供电下测量电池电压至关重要。我通过Pico的ADC引脚连接到一个由电池电压分压后的信号。需要定期读取电压并在电压过低时如低于3.3V关闭蓝牙广播和背光进入休眠并通过LED闪烁告警。import analogio vbat_pin analogio.AnalogIn(board.GP26) def read_battery_voltage(): # 根据分压电阻比例计算实际电压 return (vbat_pin.value * 3.3 / 65536) * 2.0 # 假设是1:1分压同时要小心反向电流问题。当USB插入时USB的5V会通过Pico的内部电路反向流向VSYS引脚如果那里接着电池可能会对电池充电如果不是设计为充电电路的话或造成损坏。稳妥的做法是使用一个带有防止反向电流功能的充放电管理模块。构建这把Python驱动的键盘更像是在打造一个专属的硬件编程平台。它不一定是性能最强、续航最久的键盘但它一定是独一无二、完全听命于你思想的工具。从按下按键触发一个复杂的自动化脚本到让键盘本身成为你工作流中的一个智能节点这种可能性让我觉得所有前期的折腾都是值得的。如果你也厌倦了千篇一律渴望完全的控制权那么拿起烙铁和代码编辑器开始创造属于你的那把“活”的键盘吧。