1. 一场由 USB 控制权引发的血案
那天下午,我盯着屏幕上那行usb 1-1: device descriptor read/64, error -71,心里只有一个念头:完了,键盘没了。
事情是这样的。我手头有一台老旧的 ThinkPad E14,平时拿来跑一些底层调试的活儿。那天我突发奇想,想试试在 BIOS 层面接管 USB 控制权,看看能不能在操作系统加载之前就完成一些自定义的 USB 设备枚举操作。想法很美好,现实很骨感——我写了一段直接操作 EHCI 控制器的代码,刷进去之后,重启,键盘当场去世。
不是那种“设备管理器里有个黄色感叹号”的去世,是那种按什么键都没反应、连 BIOS 界面都进不去的彻底死亡。屏幕亮着,但键盘就像一块砖头,敲什么都没用。我试了外接 USB 键盘,一样没反应。那一刻我才意识到,我可能把 8042 控制器和 USB 控制器之间的握手逻辑给搞崩了。
接下来的 11 天,我几乎把能查的资料都翻了个遍。从 USB 协议规范到 EHCI 控制器手册,从 8042 键盘控制器的寄存器定义到 PS/2 兼容模式的工作机制,甚至还去翻了几个魔改 BIOS 的论坛帖子。最后硬是靠着一根 USB 转 TTL 线和一个 STM32 开发板,把键盘从死亡线上拉了回来。
这篇文章就是这 11 天的完整记录。我会把 USB 控制权接管的核心原理、BIOS 下 USB 枚举的流程、8042 与 USB 的兼容层机制、以及最后救活键盘的实操步骤全部拆开讲清楚。如果你也在折腾 BIOS 层面的 USB 控制,或者单纯好奇为什么“抢了 USB 控制权”会导致键盘失灵,这篇内容应该能帮你少走不少弯路。
2. 为什么抢 USB 控制权会让键盘“去世”
2.1 BIOS 下 USB 键盘的枚举流程
要理解键盘为什么会死,得先搞清楚在 BIOS 环境下,USB 键盘是怎么被识别的。
传统上,BIOS 对键盘的支持走的是 PS/2 兼容路线。主板上的 8042 控制器(也叫键盘控制器)负责处理 PS/2 接口的键盘输入。后来 USB 键盘普及了,但 BIOS 不可能立刻抛弃 PS/2 兼容性,于是就有了SMM(System Management Mode)下的 USB 传统支持——BIOS 通过 SMI 中断来模拟 8042 的行为,让 USB 键盘看起来像一个 PS/2 键盘。
具体流程是这样的:
- 上电后,BIOS 初始化 EHCI 或 XHCI 控制器。
- BIOS 枚举 USB 总线上的设备,找到 HID 类设备(键盘、鼠标)。
- 如果开启了“USB Legacy Support”,BIOS 会为 USB 键盘创建一个虚拟的 PS/2 设备节点。
- 8042 控制器被配置为接受来自 USB 键盘的输入,并通过 SMI 中断转发给 BIOS 的键盘处理程序。
- 操作系统加载后,如果它有自己的 USB 驱动,就会接管 USB 控制器,BIOS 的 Legacy Support 退出。
关键点在于第 3 步和第 4 步:BIOS 的 USB Legacy Support 依赖于 8042 控制器的存在和正确配置。如果你直接接管了 USB 控制权,但没有正确处理 8042 的虚拟化逻辑,BIOS 就收不到键盘输入了。
2.2 8042 控制器与 USB 的兼容层机制
8042 控制器是一个很古老的芯片,原本是给 PS/2 键盘用的。它的寄存器很少,主要就是数据端口 0x60 和命令端口 0x64。BIOS 通过读写这两个端口来和键盘通信。
当 USB Legacy Support 开启时,BIOS 会在 SMM 里维护一个虚拟的 8042 状态机。USB 键盘的按键事件会被转换成 PS/2 扫描码,然后写入 8042 的输出缓冲区,模拟成 PS/2 键盘的输入。
我当时的错误操作是:直接禁用了 EHCI 控制器的 SMI 中断,然后自己写了一套 USB 枚举代码。结果就是,BIOS 的 USB Legacy Support 被彻底切断,8042 控制器收不到任何键盘输入,而我自己写的枚举代码又没有实现 HID 报告解析和扫描码转换,键盘自然就“去世”了。
更麻烦的是,因为我在 BIOS 层面就把 USB 控制器接管了,操作系统加载后也无法正常识别键盘——因为 USB 控制器的状态已经被我搞乱了。
2.3 为什么外接 USB 键盘也救不了
有人可能会问:内置键盘死了,插个外接 USB 键盘不就行了?
不行。因为问题不在键盘本身,而在 USB 控制器的状态。我接管了 EHCI 控制器后,没有正确释放它,导致控制器处于一种“半初始化”的状态。外接键盘插上去,控制器无法完成枚举,设备描述符读取失败,自然也用不了。
这就像是你把家里的总电闸换了,但接线接错了,不管插什么电器都没电。
3. 抢救键盘的 11 天:从绝望到复活
3.1 第一阶段的错误尝试
前三天我基本在做无用功。我试过:
- 拔电池、放电、重置 BIOS——没用,因为问题不在 CMOS 设置,而在 USB 控制器的硬件状态。
- 用外接 PS/2 键盘——这台 ThinkPad E14 没有 PS/2 接口。
- 用 USB 转 PS/2 转接头——试了,没用,因为 BIOS 的 USB Legacy Support 已经被我搞崩了,转接头也走的是 USB 协议。
- 盲刷 BIOS——微星 B550M 那种一键盲刷功能这台机器没有,而且盲刷需要键盘操作,死循环。
那几天我几乎把“无法进入 BIOS 界面”“ThinkPad E14 进 BIOS 设置 U 盘启动”“戴尔 BIOS 设置 U 盘启动”这些关键词搜了个遍,但大部分内容都是针对正常情况的,没人像我这样把 USB 控制器搞崩过。
3.2 转折点:用 STM32 做 USB 设备
第四天,我换了个思路:既然内置键盘和外接 USB 键盘都用不了,那我能不能自己做一个 USB 设备,让 BIOS 重新枚举它?
我手头有一块 STM32F103 开发板,正好可以用来做 USB HID 设备。STM32 的 USB 外设可以配置成 HID 键盘,如果我能让 BIOS 重新枚举这个设备,说不定就能恢复键盘输入。
但问题来了:BIOS 的 USB 控制器已经被我搞乱了,它还能正常枚举新设备吗?
我查了一下 EHCI 控制器的规范,发现如果控制器处于 Halt 状态,需要先发送 Reset 信号才能重新初始化。但 BIOS 启动时已经初始化过控制器了,我没办法在 BIOS 层面发送 Reset。
不过,我注意到一个细节:EHCI 控制器在检测到端口状态变化时,会触发 Port Change 中断。如果我能让 STM32 模拟一个 USB 设备插入事件,触发端口状态变化,BIOS 可能会重新枚举这个端口。
3.3 用 USB 转 TTL 线抓包分析
在动手之前,我先用 USB 转 TTL 线(FT231X 芯片)抓了一下 USB 总线的数据。FT231X 是一款常用的 USB 转 UART 芯片,配合 USB 抓包软件可以分析 USB 通信。
我抓到的数据证实了我的猜测:EHCI 控制器处于一种异常状态,端口状态寄存器显示端口被禁用,但控制器没有触发重新枚举。BIOS 的 USB Legacy Support 代码在等待一个永远不会到来的 SMI 中断。
这让我确定了抢救方案:通过 STM32 模拟 USB 设备插入,强制触发端口状态变化,让 BIOS 重新枚举。
3.4 具体抢救步骤
下面是完整的抢救流程,如果你也遇到了类似问题,可以按这个步骤来。
准备工具:
- STM32F103 开发板(或其他支持 USB HID 的 MCU)
- USB 转 TTL 线(FT231X 或 CP2102 均可)
- 杜邦线若干
- 一台能正常工作的电脑(用来编译和烧录 STM32 代码)
步骤一:编写 STM32 USB HID 键盘代码
我用的是 STM32CubeMX 生成的 USB HID 代码,核心配置如下:
// USB HID 报告描述符 __ALIGN_BEGIN static uint8_t HID_ReportDesc[] __ALIGN_END = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0xE0, // Usage Minimum (224) 0x29, 0xE7, // Usage Maximum (231) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Constant) 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x05, // Usage Maximum (5) 0x91, 0x02, // Output (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x01, // Output (Constant) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0x65, // Usage Maximum (101) 0x81, 0x00, // Input (Data, Array) 0xC0 // End Collection };这段描述符定义了一个标准的 USB HID 键盘,BIOS 应该能识别。
步骤二:配置 STM32 的 USB 外设为设备模式
在 STM32CubeMX 里,把 USB 外设配置为 Device 模式,选择 HID 类。时钟配置要注意:USB 需要 48MHz 时钟,如果用的是外部晶振,确保 PLL 配置正确。
步骤三:烧录代码并连接
把 STM32 的 USB 接口(通常是 PA11 和 PA12)连接到 ThinkPad 的 USB 口。注意,不要用 USB 转 TTL 线,直接用 STM32 的 USB 接口。
步骤四:触发端口状态变化
STM32 上电后,会模拟一个 USB 设备插入事件。如果一切正常,BIOS 应该会检测到端口状态变化,重新枚举这个端口。
我当时的操作是:先给 STM32 上电,然后快速插拔 USB 线,模拟一个“设备插入-拔出-再插入”的序列。这个序列会触发 EHCI 控制器的 Port Change 中断,让 BIOS 重新枚举。
步骤五:进入 BIOS 恢复设置
如果 STM32 被成功枚举为 HID 键盘,你就可以用 STM32 发送按键码来操作 BIOS 了。我写了一个简单的按键发送函数:
void send_key(uint8_t key) { uint8_t report[8] = {0}; report[2] = key; // 按键码 USBD_HID_SendReport(&hUsbDeviceFS, report, 8); HAL_Delay(50); report[2] = 0; // 释放按键 USBD_HID_SendReport(&hUsbDeviceFS, report, 8); HAL_Delay(50); }用这个函数,我发送了F2键(进入 BIOS 设置),然后发送F9(恢复默认设置),最后发送F10(保存并退出)。
步骤六:恢复 BIOS 设置
进入 BIOS 后,我做了两件事:
- 关闭“USB Legacy Support”再重新开启,强制 BIOS 重新初始化 USB 控制器。
- 恢复默认设置,清除我之前刷入的自定义代码。
重启后,内置键盘终于复活了。
4. 常见问题与排查技巧实录
4.1 为什么 STM32 模拟的键盘 BIOS 识别不了
我一开始用 STM32 模拟键盘时,BIOS 死活识别不了。后来用 USB 抓包工具分析,发现是报告描述符的 Usage Maximum 设置错了。我把0x25, 0x65写成了0x25, 0x64,导致按键码范围不对,BIOS 认为这不是一个标准键盘。
排查方法:用 USB 抓包工具(如 Teledyne LeCroy USB Protocol Suite 或开源的 Wireshark + USBPcap)抓取枚举过程,对比标准 HID 键盘的描述符。
4.2 EHCI 控制器 Halt 状态怎么恢复
如果 EHCI 控制器处于 Halt 状态,需要发送 Reset 信号。但在 BIOS 层面,你没法直接操作控制器寄存器。我的做法是:通过模拟 USB 设备插入,触发 Port Change 中断,让 BIOS 自己重新初始化控制器。
如果这个方法不行,可以尝试短接 USB 端口的 D+ 和 D- 线,模拟一个 USB 设备插入。但要注意,短接时间不能太长,否则可能损坏控制器。
4.3 8042 控制器被禁用怎么办
有些 BIOS 允许在设置里禁用 8042 控制器。如果你不小心禁用了,键盘会完全失灵。恢复方法是:清除 CMOS。具体操作是拔掉主板电池,短接 CMOS 跳线,等几分钟再装回去。
但注意,清除 CMOS 会重置所有 BIOS 设置,包括你之前配置的启动项。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 键盘完全无反应 | USB 控制器状态异常 | 用 STM32 模拟 USB 设备触发重新枚举 |
| BIOS 识别不到 STM32 键盘 | HID 描述符错误 | 检查报告描述符,确保符合 HID 规范 |
| 外接 USB 键盘也无法使用 | EHCI 控制器 Halt | 触发 Port Change 中断,或短接 D+/D- |
| 进入 BIOS 后无法保存设置 | 8042 控制器被禁用 | 清除 CMOS,恢复默认设置 |
| USB 转 TTL 线抓不到数据 | 抓包工具配置错误 | 检查 USBPcap 驱动和 Wireshark 过滤器 |
4.5 避坑技巧
- 不要直接禁用 SMI 中断:BIOS 的 USB Legacy Support 依赖 SMI,禁用后键盘必死。
- 保留 PS/2 兼容模式:即使你要接管 USB 控制权,也要确保 8042 控制器仍然可用。
- 备份原始 BIOS:在刷入任何自定义代码之前,先用 BIOS Backup Toolkit 备份原始 BIOS。
- 准备一个备用键盘:如果你要折腾 BIOS 层面的 USB 控制,最好准备一个 PS/2 键盘或一个已知能工作的 USB 键盘。
5. 折腾完这 11 天,我学到了什么
这 11 天的经历让我对 BIOS 下的 USB 控制有了完全不一样的理解。以前我觉得 USB 就是“插上就能用”,现在我知道背后有 EHCI 控制器、8042 兼容层、SMI 中断、HID 报告描述符这一整套复杂的机制在支撑。
如果你也想在 BIOS 层面折腾 USB 控制,我的建议是:先备份,再动手。备份 BIOS、备份原始代码、备份一个能用的键盘。然后,不要直接禁用 SMI 中断,那是 BIOS USB Legacy Support 的生命线。
最后再分享一个小技巧:如果你不小心把 USB 控制器搞崩了,可以试试用 STM32 做一个 USB HID 设备,通过模拟插入事件来触发 BIOS 重新枚举。这个方法我实测有效,但需要一点 USB 协议的基础知识。如果你手头没有 STM32,也可以用 Arduino 的 USB HID 库,原理是一样的。
至于我那个键盘,它现在还在正常工作。每次看到它,我都会想起那 11 天的折腾——以及那行让我心跳停止的error -71。