我最早接触 USB Host Shield 这个板子,是被一个需求逼的:想把一个 USB 键盘直接接到 Arduino 上,让它变成 90 年代那种“单片机加键盘输入”的终端设备。结果一查资料,Arduino Uno 板载那个 USB 口只能当从机(Device),压根不能接键盘鼠标。后来才知道有个叫 USB Host Shield 的扩展板,能把 Arduino 变成 USB 主机,直接插 U 盘、键盘、鼠标、手柄,甚至能读蓝牙适配器。这篇文章我就把选型、接线、库、代码、踩坑全过程都整理出来,给同样被 USB 主机功能卡住的朋友一条能照抄的路。
1. 先搞清楚 USB Host Shield 解决了什么问题
1.1 Arduino 的 USB 接口到底能干什么
很多人对 Arduino 上的 USB 口有误解,以为能插 U 盘、能接键盘,实际上不是这么回事。以最常见的 Arduino Uno 为例,板载那个方口 USB 其实是把 ATmega16U2 芯片当 USB 转串口芯片用,它的角色是 USB Device,也就是从机,只能跟电脑通信,不能主动去枚举别的 USB 外设。
这跟电脑上的 USB 口有本质区别。电脑 USB 口是 Host(主机),插上键盘后由主机发起枚举流程,给设备分配地址、加载驱动。而 Arduino 的 USB 口从硬件层面就不具备 Host 能力,所以即使你写再复杂的代码,也不可能靠 Uno 自带的 USB 口去识别一个 U 盘或鼠标。
这个限制困扰了不少人。网上经常有人问“怎么让 Arduino 读 U 盘”“怎么接 PS2 键盘转 USB”,答案基本都是要加一块 USB Host Shield 扩展板,或者换用自带 USB Host 功能的板子(比如 Arduino Due、部分 ESP32 开发板,甚至树莓派 Zero)。
1.2 USB Host Shield 的核心原理
USB Host Shield 之所以能让 Arduino 变成 USB 主机,靠的是一颗独立的 USB Host 控制芯片:MAX3421E。这颗芯片内部集成了 USB 收发器、SIE(串行接口引擎)、以及 SPI 从机接口,外部只需要一根 SPI 总线就能跟 MCU 通信。
工作流程大致是这样的:Arduino 通过 SPI 接口向 MAX3421E 发送控制指令,MAX3421E 负责把 USB 协议转换成总线上的差分信号。当外部设备插入时,MAX3421E 会检测到 VBUS 和 D+/D- 上的状态变化,并通过中断引脚通知 Arduino,然后 Arduino 里的库代码就开始执行枚举流程,读取设备描述符、配置描述符,加载对应驱动类。
这里有个关键点:MAX3421E 自身承担了 USB 物理层和协议层的很多细节工作,所以 Arduino 的 CPU 不必去处理高速时序,这也是为什么一片 8 位的 ATmega328P 也能跑 USB Host 功能。不过代价是吞吐量有限,实际传输速度大概在 1.5Mbps(低速)和 12Mbps(全速)之间,读 U 盘的时候能达到几百 KB/s 就算不错了。
1.3 什么时候该用它,什么时候不该用
USB Host Shield 不是万能的,它的适用场景很明确。
适合用的场景:
- 需要把 USB 键盘、鼠标接到 Arduino 上做人机交互终端。
- 需要读取 USB 游戏手柄,做机器人遥控或游戏控制项目。
- 需要读取 U 盘里的配置文件或数据日志,替代 SD 卡方案。
- 需要连接 USB 转串口适配器、USB 蓝牙适配器、USB 网卡等设备。
不适合用的场景:
- 需要高速传输大文件(比如视频流),USB Host Shield 的带宽不够。
- 需要同时挂多个 USB 设备,除非你用 USB Hub,但兼容性会变得复杂。
- 项目空间极其有限,因为 Shield 是叠在 Arduino 上方的。
- 想跑 USB Mass Storage 大量读写,SD 卡往往更快更稳。
我自己的经验是,如果只是键盘鼠标和手柄这类输入设备,USB Host Shield 性价比很高,几十块钱就能搞定。如果要处理大量数据,不如换树莓派 Pico W 或者 ESP32-S3 这类带原生 USB 功能的板子。
2. 硬件选型与接线:别买错也别接错
2.1 官方版与兼容版的真实差距
市面上 USB Host Shield 主要有两个版本:一个是以 circuits@home 原版为核心设计的官方版(现在主要由 OSH Park 等提供开源板),另一个是国内常见的兼容版,核心芯片都是 MAX3421E,电路设计大同小异。
从使用角度说,两种版本差异不大,区别主要体现在做工和接口排布上。官方版通常板载完整的 USB Type-A 母座,兼容版有些则用排针引出 D+/D-/VBUS/GND,需要自己外接 USB 母座,稍微麻烦一点。
选购时重点看三点:芯片一定是 MAX3421E,不能是 MAX3420E(那是纯 Device 芯片);板子必须带 SPI 接口引脚(通常标着 CS、SCK、MISO、MOSI、INT、RST);如果是兼容版,确认有没有把 VBUS 电源飞线处理好,否则接 U 盘可能供电不足。
2.2 适配主板的注意事项
USB Host Shield 设计之初就是给 Arduino Uno 和 Mega 用的,引脚定义已经考虑好了默认兼容。实际上它适用于所有能跑 Arduino 的 AVR 主板,只要 SPI 引脚能对上就行。
以 Arduino Uno 为例,默认接线如下:
- MAX3421E 的 CS 接 数字引脚 10
- SCK 接 13(硬件 SPI SCK)
- MISO 接 12(硬件 SPI MISO)
- MOSI 接 11(硬件 SPI MOSI)
- INT 接 9
- RST 接 7
- 5V 和 GND 接对应电源引脚
Arduino Mega 2560 的接线一模一样,因为 Mega 的 SPI 引脚同样在 50-52 和 53(SS),但库会自动识别。我测过 Uno、Mega 2560、Leonardo 三种板子,Leonardo 稍有不同,它的 SPI 引脚在 ICSP 口上,直接叠盾之后默认针脚也能用,但有些版本的兼容盾需要手动飞线。
更需要注意的是一些非 AVR 板子,比如 ESP32。ESP32 的硬件 SPI 引脚不固定,默认 VSPI 是 MOSI=23、MISO=19、SCK=18,CS 可以指定任意引脚,INT 和 RST 也随意,但库默认配置需要修改。我自己用 ESP32 试过 USB 键盘,需要改 UsbCore.h 里的引脚定义,否则会枚举失败。
2.3 供电与电平问题
USB Host Shield 最容易被忽视的坑就是供电。USB 外设(尤其是 U 盘和无线接收器)启动电流需求比较大,Shield 需要从 Arduino 的 5V 引脚取电,而 Arduino 的 5V 又来自 USB 口或外部电源。
当 U 盘插上去瞬间会产生浪涌电流,如果 Arduino 用电脑 USB 口供电,很可能触发电脑 USB 口的限流保护,导致 Arduino 重启,Shield 也跟着掉线。这个问题不是个案,我实测过多次,U 盘读取时 Arduino 突然复位,串口监控里看到一串乱码。
解决方案有两个:一是给 Arduino 外接 7-12V 直流电源,让板载 5V 稳压器提供充足电流;二是直接给 Shield 单独供 5V 电源,切断 Arduino 5V 到 Shield 的连线(有些板子有跳线帽,可以拔掉)。
另外要特别注意电平匹配。MAX3421E 是 3.3V 逻辑芯片,但 USB Host Shield 板上一般带了电平转换电路,所以接 5V 的 Arduino 没问题。但如果你用 3.3V 的板子(比如某些 ESP32 核心板),就要确认 Shield 是否支持 3.3V 供电,否则可能不稳定。
3. 开发环境:库的选择与搭建
3.1 两个库分支的区别
USB Host Shield 的库主要有两个版本,很多人一开始就栽在选库上面。
第一个是官方原版库,叫做 USB Host Shield Library 2.0,作者是 Oleg Mazurov,在 GitHub 上有维护。这个库功能全面,支持 HID 键盘鼠标、Xbox 手柄、PS3/PS4 手柄、USB 存储、USB 转串口、蓝牙适配器等,社区活跃度也高,是首选。
第二个是 Arduino 官方库管理器里能搜到的 USB Host Shield Library(注意没有 2.0 后缀),这是早期版本的移植,功能少很多,只支持部分 HID 设备,而且多年没更新了。新手在 Library Manager 里搜“USB Host”出来一堆,如果选了旧的这个,编译的时候很容易报错。
我建议直接去 GitHub 搜索 arduino USB Host Shield Library 2.0,找到 OlegMazurov 的仓库,下载 ZIP 后通过 Arduino IDE 的“项目 -> 加载库 -> 添加 .ZIP 库”安装。这样能保证是最新代码,兼容性问题最少。
3.2 库安装与初始化流程
安装步骤其实不难,但这几个细节容易出问题。
第一步,下载 ZIP 包后,解压检查一下文件夹名字。有些 GitHub 下载的 ZIP 解压后文件夹名字带分支后缀,比如 USB_Host_Shield_2.0-master,这种名字 Arduino IDE 无法识别,需要手动改名成 USB_Host_Shield_2.0 再放进 libraries 文件夹。
第二步,确保 Arduino IDE 的库路径正确。Windows 下默认在 C:\Users\你的用户名\Documents\Arduino\libraries,如果之前手动装过别的库,要确认没有重名的文件夹。
第三步,编译时如果报错提示缺少 SPI.h 或 SD.h,一般是 Arduino 核心库不完整,重新安装 Arduino AVR Boards 即可。
初始化代码最简单的写法是这样:
#include <UsbHostShield.h> #include <usbhub.h> USB Usb; USBHub Hub(&Usb); void setup() { Serial.begin(115200); if (Usb.Init() == -1) { Serial.println("USB Host 初始化失败,请检查接线"); } delay(200); } void loop() { Usb.Task(); }这段代码的作用就是初始化 USB Host 芯片,并在循环里持续执行 USB 任务。如果初始化失败,串口会打印错误信息。我第一次跑的时候,初始化一直失败,折腾了半天才发现是 RST 引脚没接对。
3.3 基础初始化代码:检测 USB 设备枚举
初始化完成后,还需要能感知设备的插入和拔出。USB Host Shield 库通过 Usb.Task() 的轮询机制处理枚举,库内部会在设备插入时自动枚举,并加载对应驱动。
为了验证枚举是否成功,可以查询 Usb.getUsbTaskState() 的状态:
void loop() { Usb.Task(); if (Usb.getUsbTaskState() == USB_STATE_RUNNING) { Serial.println("USB 设备已枚举成功"); } else if (Usb.getUsbTaskState() == USB_STATE_ERROR) { Serial.println("USB 枚举错误"); } delay(100); }状态机的几个关键值包括 USB_STATE_DETACHED(无设备)、USB_STATE_ATTACHED(检测到设备)、USB_STATE_ADDRESSING(分配地址)、USB_STATE_RUNNING(正常运行)。如果一直停在 ATTACHED 或 ADDRESSING,说明设备枚举过程中卡住了,大概率是供电不足或兼容性问题。
4. 实操:USB 键盘鼠标数据读取全流程
4.1 读 USB 键盘:按键状态解析
核心目的很简单:让 Arduino 把 USB 键盘按键打印到串口,或者映射成自定义操作。USB 键盘底层走的是 HID 协议,报告格式是固定 8 字节,第一位是修饰键(Ctrl、Shift、Alt),第二位是保留,后面 6 位是同时按下的按键码。
库提供了一个 USBHIDKeyboard 类封装了这些细节,用起来非常直接。我实测时把键盘接到 Shield 上,代码几行就能实现按键输出,不过最初有个坑:库默认在按键按下和释放时都会回调,导致同一个键打出来两次。后来用状态跟踪法,只在“前一次没按下、这次按下”的时候输出,就正常了。
稳定版代码如下:
#include <UsbHostShield.h> #include <usbhub.h> #include <hidboot.h> USB Usb; USBHub Hub(&Usb); HIDBoot<USB_HID_PROTOCOL_KEYBOARD> Kbd(&Usb); uint8_t last_keys[6] = {0, 0, 0, 0, 0, 0}; void KbdRptParser::OnKeyDown(uint8_t mod, uint8_t key) { Serial.print("按键按下,扫描码: 0x"); Serial.println(key, HEX); } void KbdRptParser::OnKeyUp(uint8_t mod, uint8_t key) { // 可以用作按键释放回调 } class KbdParser : public KbdRptParser { void OnKeyDown(uint8_t mod, uint8_t key) override { Serial.print("按下: 0x"); Serial.println(key, HEX); } void OnKeyUp(uint8_t mod, uint8_t key) override { Serial.print("释放: 0x"); Serial.println(key, HEX); } }; KbdParser Parser; void setup() { Serial.begin(115200); if (Usb.Init() == -1) { Serial.println("初始化失败"); } Kbd.SetReportParser(0, &Parser); delay(200); } void loop() { Usb.Task(); }要注意的是,OnKeyDown 只在第一次按下时触发,长按期间不会重复触发,这是 HID 协议的标准行为。如果你想做长按连发,需要自己维护一个按键计时器,在 loop 里判断按键是否被持续按住。
4.2 读 USB 鼠标:移动与按键
鼠标和键盘不同,它不仅有按键状态,还有相对位移数据。HID 鼠标报告通常是 4 字节:按钮状态、X 位移、Y 位移、滚轮。库里的 HIDBoot<USB_HID_PROTOCOL_MOUSE> 会处理这些数据,回调函数直接给出位移量。
位移量是相对值,也就是自上次报告以来的变化量,数值范围通常是 -127 到 127。如果用鼠标控制舵机,直接把位移映射到舵机角度就行。我自己尝试做了个“鼠标控制舵机云台”的项目,效果还不错,就是鼠标移动太快的时候,数据会溢出丢包,需要在代码里做限幅。
鼠标基类的使用代码如下:
#include <UsbHostShield.h> #include <usbhub.h> #include <hidboot.h> USB Usb; USBHub Hub(&Usb); HIDBoot<USB_HID_PROTOCOL_MOUSE> Mouse(&Usb); class MouseParser : public MouseRptParser { void OnMouseMove(uint8_t buttons, int8_t x, int8_t y) override { Serial.print("移动: X="); Serial.print(x); Serial.print(" Y="); Serial.println(y); } void OnLeftButtonUp() { Serial.println("左键释放"); } }; MouseParser Parser; void setup() { Serial.begin(115200); if (Usb.Init() == -1) { Serial.println("初始化失败"); } Mouse.SetReportParser(0, &Parser); delay(200); } void loop() { Usb.Task(); }这里必须注意,鼠标的 OnMouseMove 回调在高速移动时可能连续触发多次,如果里面做串口打印,可能阻塞 USB 轮询,导致丢包。实际项目中最好只做数学运算,不打印,或者把数据压入缓冲区,等空闲时再输出。
4.3 实操现场:USB 键盘输入 Arduino 串口
我实测的时候用了一个最普通的 USB 键盘(不是机械键盘,就是办公用的那种),搭配 Arduino Uno + USB Host Shield。编译烧录后打开串口监视器,按下字母 A,串口立刻输出“按下: 0x04”,按下 B 输出 0x05。
这里扫描码不是 ASCII 码,是 HID 键盘扫描码。0x04 对应键盘上的 A 键,0x05 对应 B 键,不管键盘布局是英文还是中文,扫描码都不变。如果需要转换成 ASCII 字符,得自己维护一张映射表。库的例程里有一个 UsbHidKeyboard 示例,直接实现了 ASCII 映射,可以拿来改。
实测的一个细节:有些键盘在开机时会发送一串初始化报告(比如状态指示灯配置),库可能在一开始触发几个 OnKeyUp 回调,这很正常,不用理会。另外,如果键盘是带背光的,背光不会亮,因为 USB Host Shield 不支持背光控制报告。
5. 进阶玩法:游戏手柄与 U 盘
5.1 手柄读取与实时控制
USB 手柄是 USB Host Shield 的经典应用,尤其是做机器人遥控项目时,比蓝牙手柄稳定得多。库对 Xbox 手柄、PS3、PS4、Switch Pro 手柄都有支持,但不同手柄的驱动类不一样。
以 Xbox 360 有线手柄为例,库提供了 XBOXRECV 驱动,初始化方式如下:
#include <XBOXRECV.h> USB Usb; XBOXRECV Xbox(&Usb); void setup() { Serial.begin(115200); if (Usb.Init() == -1) { Serial.println("初始化失败"); } delay(200); } void loop() { Usb.Task(); if (Xbox.Xbox360Connected) { if (Xbox.getButtonPress(L2) > 0) { Serial.print("L2 按下,力度: "); Serial.println(Xbox.getButtonPress(L2)); } } }这里 getButtonPress 返回的 0-255 的模拟量,适合油门控制。而 getButtonClick 只返回 0 或 1,适合菜单选择。我踩过的坑是:如果手柄是无线手柄,需要先连接接收器,接收器插入后 USB Host Shield 不一定能直接识别,因为它会枚举成一个接收器设备,而不是手柄设备,可能需要改驱动配置。
5.2 U 盘文件系统读取
USB Host Shield 读 U 盘用的是 USB Mass Storage 类,再配合 FAT 文件系统库。Arduino 官方推荐的方式是把 USB Host Shield 2.0 库和 SdFat 库结合,但配置过程稍微复杂。
简化版的做法是用 USB_HOST_SHIELD 的 USBStorage 示例,它读出来的数据可以通过串口发送。不过实际使用中,我发现 U 盘兼容性问题比键盘鼠标多得多。常见的表现有:U 盘插入后枚举成功,但读写超时;老款 U 盘能识别,新款 USB 3.0 高速盘反而不行;U 盘容量太大(64GB 以上)FAT32 分区识别失败。
解决办法一是尽量用 USB 2.0 的 U 盘,容量控制在 16GB 以下;二是确保 U 盘分区是 FAT32,exFAT 和 NTFS 都不支持;三是如果 U 盘带写保护开关,先打开。另外供电问题在 U 盘场景下最严重,我强烈建议外接电源,别靠电脑 USB 口给 Arduino 供电。
6. 常见问题与排查技巧实录
6.1 问题速查表
我把实际使用中遇到的高频问题整理成了一个速查表,方便大家直接对照:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 初始化返回 -1 | RST 引脚没接对 | 确认 RST 连到数字引脚 7(或改代码),排查接线 |
| 枚举一直不成功 | 供电不足 | 外接 7-12V 电源,或单独给 Shield 供 5V |
| 键盘能识别但没输出 | 键盘协议不兼容 | 换一个普通 USB 有线键盘测试 |
| 鼠标移动没有反应 | 中断引脚冲突 | 确认 INT 引脚(9)没有其他设备占用 |
| U 盘读写超时 | U 盘是 USB 3.0 高速盘 | 换 USB 2.0 U 盘 |
| 板子发热严重 | 5V 和 3.3V 短路 | 检查 Shield 背部焊点,尤其是电压转换电路附近 |
| 接上设备后 Arduino 重启 | 浪涌电流引起复位 | 外接电源,或加一个大容量电解电容 |
| 手柄只有部分按键响应 | 手柄不是标准 XInput | 换用库支持的对应驱动类 |
6.2 独家避坑经验
这几个坑是我反复踩过之后才总结出来的,可以说比官方文档值钱。
第一个是 IRQ 引脚冲突。Arduino Uno 的引脚 9 是 PWM 引脚,同时被 USB Host Shield 用作中断输入。如果你在代码里用 analogWrite 控制引脚 9 的 PWM 输出,会直接干扰 USB Host 的中断检测,导致设备插入毫无反应。要避免这个问题,就换用其他 PWM 引脚。
第二个是库版本的雷区。如果你在 Arduino IDE 的库管理器里搜索“USB Host Shield”,可能会看到好几个同名库。我试过其中一个,编译能过,但运行时设备枚举总是失败。后来发现那个库还是老版本,不支持 MAX3421E 的某些新特性。所以建议直接去 GitHub 下载 OlegMazurov 的 2.0 版本,不要用库管理器里那个。
第三个是叠盾后的散热问题。USB Host Shield 叠在 Arduino 上后,两层板子之间的空隙很小,如果 Arduino 本身由 USB 口供电,ATmega328P 长时间满负荷运行温度会偏高。我的做法是给 Arduino 加一个散热片,或者干脆换成 Arduino Mega(空间更大,散热好一些)。
第四个是供电的细节。USB Host Shield 官方文档说可以通过 Arduino 的 5V 引脚获取电源,但实际接大功率设备时,5V 电压会被拉低到 4.6V 左右,这时候 MAX3421E 可能工作不稳定。我在 USB 母座旁边加了一个 100uF 电解电容,瞬间电流的毛刺被吸收了不少,问题明显改善。
7. 实际项目延伸与经验总结
7.1 做一个 Arduino 键盘记录终端
USB Host Shield 最简单的成品项目,就是做一个“键盘输入转串口”的终端。我在这个基础上加了一块 OLED 屏幕,键盘输入直接显示在屏幕上,解决了嵌入式调试时“需要输入设备但没有电脑”的痛点。
具体实现不需要额外硬件,只需要把之前在 4.1 节中的键盘回调函数接一段 OLED 显示代码。这项目看起来简单,但做出来后你会对 USB 枚举、HID 协议、报告解析这些概念有更直观的认识。建议新手从键盘和鼠标入手,因为这两个是 HID 协议里最简单、最稳定的设备,成功率高,反馈明显。
7.2 鼠标做舵机云台追踪
这个项目是我个人最喜欢的:用 USB 鼠标控制一个二自由度舵机云台,配合激光笔或摄像头,可以实现类似“遥控瞄准”的效果。鼠标水平移动映射到水平舵机角度,垂直移动映射到垂直舵机角度,再加上鼠标滚轮调整速度。
核心代码就是把 4.2 节的鼠标位移累加到一个变量,归一化后映射到舵机角度。要注意的是,鼠标位移量是相对值,如果累加不加限幅,舵机会转到机械限位的位置,所以必须要限制角度范围。我实测下来,1000ms 内鼠标移动 200 个像素点,映射到 90 度转角,操作手感还算顺滑。
7.3 这个方案的后续扩展方向
USB Host Shield 的上限其实比很多人想象的高。除了键盘鼠标和手柄,它还支持 USB 转串口适配器(通过 CDC 类)、USB 蓝牙适配器(通过 HCI 层)、甚至支持连接 USB 摄像头(通过 UVC 类,但帧率很低)。
如果项目需要无线,可以试 USB 蓝牙适配器加蓝牙键盘,这样 Arduino 完全脱离有线束缚,不过蓝牙适配器的配对流程会占用不少 Flash 和 RAM,Uno 的 32KB Flash 可能会比较吃紧。
我个人觉得,USB Host Shield 在小型交互设备、特殊输入设备适配、以及学习 USB 协议栈这几个场景里,优势还是很明显的。虽然代码风格有点老,但作为一块学习硬件,它的价值完全对得起价格。
最后分享一个小经验:如果你在项目里要用 USB Host Shield,先别急着写业务代码,先把设备枚举、驱动加载跑通,再逐步加功能。这样即使后面出问题,排查范围也小很多。用 USB Host Shield 这种“带协议的硬件”,耐心比技巧重要,只要你肯花时间看库源码、调试中断,绝大多数问题都能解决。