USB-HID
usb-hid详解
一、USB-HID 是什么
HID = Human Interface Device(人机接口设备)
常见设备:
-
键盘
-
鼠标
-
游戏手柄
-
触摸板
-
遥控器
USB 协议把这些设备统一为 HID 类设备。
特点:
1️⃣ 免驱动
操作系统自带驱动(Windows / Linux / macOS)
2️⃣ 通信简单
不用复杂协议
3️⃣ 适合小数据通信
例如:
| 设备 | 数据 |
|---|---|
| 键盘 | 按键 |
| 鼠标 | 坐标 |
| 手柄 | 按键+摇杆 |
二、USB-HID 的核心结构
理解 HID 只需要理解 3 个东西:
USB设备
│
├─ Endpoint(端点)
│
└─ HID Report(数据报告)
1 USB Endpoint(端点)
USB通信通过 端点 Endpoint
HID通常使用:
Interrupt IN
Interrupt OUT(可选)
例如:
Device → PC
鼠标移动数据
周期:
1ms / 10ms / 20ms
2 HID Report(报告)
HID的核心就是:
Report(报告)
比如鼠标报告:
|按钮|X|Y|
键盘报告:
|修饰键|按键1|按键2|...|
HID设备每次发送一个 Report 数据包。
3 Report Descriptor(报告描述符)
最重要的东西:
HID Report Descriptor
它告诉电脑:
我的数据格式是什么
例如:
我有:
3个按键
2个坐标
电脑根据这个解析数据。
三、HID 数据流
例如鼠标:
鼠标移动
↓
MCU生成Report
↓
USB发送
↓
PC HID驱动解析
↓
光标移动
四、一个最简单 HID Report
举例:
3字节鼠标
Byte0 按键
Byte1 X
Byte2 Y
例如:
00 05 00
含义:
无按键
X+5
Y不动
五、HID Report Descriptor 示例
例如鼠标描述符:
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Buttons
0x19, 0x01,
0x29, 0x03,
0x15, 0x00,
0x25, 0x01,
0x95, 0x03,
0x75, 0x01,
0x81, 0x02,
0xC0,
0xC0
不用怕。
理解成一句话:
声明设备的数据格式
六、嵌入式 HID 开发流程
真实开发流程:
1 定义 HID Descriptor
2 定义 Report Descriptor
3 配置 USB Endpoint
4 定义 Report 数据结构
5 定时发送 Report
例如 STM32:
uint8_t report[3];
report[0] = 0; // 按键
report[1] = 5; // X
report[2] = 0; // Y
USBD_HID_SendReport(&hUsbDeviceFS, report, 3);
电脑就会移动鼠标。
七、HID 有两种模式
1 Boot HID(简单)
系统启动时用:
-
键盘
-
鼠标
结构固定。
2 Report HID(自定义)
最常用:
你可以自定义:
按键
坐标
数据
控制信号
例如:
自定义USB通信
很多项目用 HID 做:
USB串口替代
MCU通信
控制设备
因为:
免驱动
八、HID 与 CDC 的区别
| 类型 | 用途 |
|---|---|
| HID | 免驱动通信 |
| CDC | 虚拟串口 |
| MSC | U盘 |
例如:
USB串口 = CDC
鼠标键盘 = HID
U盘 = MSC
九、工程中 HID 常见用途
嵌入式项目里常见:
1 USB鼠标
MCU模拟鼠标。
2 USB键盘
做键盘设备。
3 自定义USB通信
很多设备:
MCU ↔ PC软件
用 HID 传数据。
因为:
无需驱动
例如:
示波器
键盘配置软件
手柄配置
其实USB通信本质非常简单。我用一个工程师能真正理解的模型给你讲清楚:
USB = 主机(电脑)主动轮询,从机(单片机)被动响应
只要理解这一句话,你就理解了 70% 的 USB。
一、USB通信的核心规则(最重要)
USB里有一个绝对规则:
只有主机能发起通信。
电脑是 Host
单片机是 Device
通信过程永远是:
PC → MCU :你有数据吗?
MCU → PC :有 / 没有
MCU 不能主动给电脑发数据。
必须等电脑问。
二、USB通信真实流程
假设你的单片机是 HID鼠标。
当你移动鼠标时,真实流程是:
PC 每 1ms 询问一次
PC → MCU
"有鼠标数据吗?"
MCU → PC
"有,移动了5像素"
PC → 操作系统
光标移动
所以 HID 看起来像 MCU 在发数据,其实是:
PC一直在轮询
三、USB通信结构(像快递系统)
USB通信有三个层:
设备 Device
│
├── Endpoint(端点)
│
└── 数据 Packet
1 Device(设备)
你的单片机。
例如:
-
鼠标
-
键盘
-
U盘
2 Endpoint(端点)
USB设备内部有多个 Endpoint(端点)。
可以理解为:
端点 = 数据通道
例如:
Endpoint0 控制端点(必须)
Endpoint1 数据端点
Endpoint2 数据端点
3 Packet(数据包)
真正发送的数据:
Packet
例如:
00 05 00
四、USB枚举(电脑识别设备)
当你把单片机插入电脑时,会发生一个过程:
USB枚举(Enumeration)
流程:
插入USB
↓
PC发现新设备
↓
PC读取设备信息
↓
PC加载驱动
↓
设备开始通信
PC 会问 MCU:
你是谁?
MCU回答:
我是 HID 鼠标
然后 PC 就加载 HID驱动。
五、枚举时电脑会读取这些信息
1 Device Descriptor
设备描述符
告诉电脑:
厂商ID
产品ID
USB版本
2 Configuration Descriptor
配置描述符
告诉电脑:
我有几个接口
3 Interface Descriptor
接口描述符
告诉电脑:
我是 HID / CDC / MSC
4 Endpoint Descriptor
端点描述符
告诉电脑:
我有哪些数据通道
例如:
Endpoint1 IN
六、USB通信例子(HID)
例如你做一个 USB键盘。
键盘按下 A。
流程:
PC → MCU
"键盘有数据吗?"
MCU → PC
发送 Report
00 04 00 00 00 00 00 00
含义:
04 = A键
PC解析后:
输入字符 A
七、四种USB传输类型
USB有四种通信方式:
| 类型 | 用途 |
|---|---|
| Control | 控制通信 |
| Interrupt | 小数据 |
| Bulk | 大数据 |
| Isochronous | 音视频 |
1 Control(控制传输)
用于:
设备枚举
设备配置
所有设备必须有:
Endpoint0
2 Interrupt(中断传输)
HID设备使用。
特点:
小数据
低延迟
周期查询
例如:
鼠标
键盘
3 Bulk(批量传输)
特点:
大数据
可靠
例如:
USB串口
USB打印机
4 Isochronous(同步传输)
用于:
摄像头
音频
特点:
实时
可能丢包
八、USB通信总结(最重要)
你只要记住这 5 件事:
1
PC = Host
MCU = Device
2
只有PC能发起通信
3
通信通过:
Endpoint
4
数据通过:
Packet
5
插入USB时会:
枚举设备
九、嵌入式开发实际代码
例如 STM32 HID:
发送数据:
uint8_t report[3] = {0, 5, 0};
USBD_HID_SendReport(&hUsbDeviceFS, report, 3);
其实底层发生的是:
PC轮询
↓
MCU返回report
一个非常典型的 STM32 USB-HID 通信实现
// 包含头文件
#include "stm32f10x.h" // STM32F10x标准外设库头文件
#include "USB_HID.h" // USB HID设备头文件
#include "USB_config.h" // USB配置文件
//#include "USB_init.h" // USB初始化头文件(已注释掉)
#include "usb_lib.h" // USB库头文件
#include "delay.h" // 延时函数头文件
#include "ANO_DT.h" // ANO数据协议头文件
// USB接收标志位,当接收到数据时置1
u8 USB_ReceiveFlg = 0;
// USB接收缓冲区,最大64字节(HID报告描述符中定义的最大包大小)
u8 Hid_RxData[64];
// HID发送超时计数器,当数据不足一帧时,等待HID_SEND_TIMEOUT个周期后强制发送
u8 HID_SEND_TIMEOUT = 5;
// HID环形缓冲区,大小256字节,用于暂存待发送的数据
u8 hid_datatemp[256];
// 环形缓冲区读指针,指向应当发送的数据位置
u8 hid_datatemp_begin = 0;
// 环形缓冲区写指针,指向数据写入的结尾位置
u8 hid_datatemp_end = 0;
//bool USB_ReceiveFlg; // 布尔类型接收标志(已注释掉,使用u8类型)
/**
* @brief USB HID设备初始化函数
* @param 无
* @return 无
* @note 初始化USB时钟、USB外设、配置中断,并连接USB设备
*/
void USB_HID_Init(void)
{
// USB_GPIO_Configuration(); // USB GPIO配置(已注释掉)
Set_USBClock(); // 设置USB时钟(48MHz)
USB_Init(); // 初始化USB外设
USB_ReceiveFlg = 0; // 清除接收标志
USB_Connect(0); // 先断开USB连接(软件或硬件重启USB)
delay_ms(100); // 延时100毫秒,确保USB复位完成
USB_Connect(1); // 重新连接USB设备
USB_Interrupts_Config(); // 配置USB中断
}
/**
* @brief 向HID发送缓冲区添加数据
* @param dataToSend 指向要发送的数据的指针
* @param length 数据长度
* @return 无
* @note 将数据写入环形缓冲区,供后续发送使用
*/
void Usb_Hid_Adddata(u8 *dataToSend , u8 length)
{
u8 i;
// 循环将数据写入环形缓冲区
for(i=0; i<length; i++)
{
hid_datatemp[hid_datatemp_end++] = dataToSend[i]; // 写入数据并移动写指针
}
}
/**
* @brief USB HID数据接收处理函数
* @param 无
* @return 无
* @note 需要不断调用此函数查询是否有新数据,解析ANO协议数据帧
*/
void Usb_Hid_Receive()//不断查询
{
// 检查是否接收到新数据
if (USB_ReceiveFlg)
{
// 检查数据长度和帧头(0xAA)
if(Hid_RxData[0] < 33 && Hid_RxData[1]==0xaa)
{
// Hid_RxData[0]为数据总长度,从Hid_RxData[1]开始为一帧完整数据
// 检查帧头是否正确(0xAA 0xAF)
if(Hid_RxData[1] == 0xaa && Hid_RxData[2] == 0xaF ) //帧头正确
{
u8 i;
u8 check_sum = 0;
// 计算校验和(从第2字节到数据末尾)
for(i=1;i<Hid_RxData[0];i++) // buf[0]为收到PC来的数据总长度
{
check_sum+= Hid_RxData[i];
}
// 校验和验证:比较计算出的校验和与接收到的校验和
if(check_sum == Hid_RxData[Hid_RxData[0]]) // buf[0]为数据总长度,最后一个字节为PC发来的校验和
{
// 检查是否为PID数据(命令字范围0x10-0x15)
if(Hid_RxData[4] >= 0x10 && Hid_RxData[5]<=0x15) //如果收到的是PID数据则要返回校验值
{
checkPID = Hid_RxData[4]<<8 | check_sum; // 返回PID校验数据(高8位为命令字,低8位为校验和) ANTO_Send(ANTO_CHECK); // 收到HID发来的PID则要马上返回校验值给上位机
}
// 调用ANO数据接收处理函数处理数据(跳过第1个字节,从实际数据开始)
ANO_Recive((int8_t*)(Hid_RxData+1)); // hid接收到数据会调用此函数
}
Hid_RxData[1] = 0;// 帧头清0,防止重复处理
}
}
USB_ReceiveFlg = 0; // 清除接收标志,准备接收下一帧数据
}
}
/**
* @brief USB HID数据发送函数
* @param 无
* @return 无
* @note 从环形缓冲区读取数据并发送,支持环形缓冲区回绕处理
*/
void Usb_Hid_Send (void)
{
static u8 notfull_timeout=0; // 数据不足时的超时计数器
u8 i;
// 情况1:写指针大于读指针(缓冲区未回绕)
if(hid_datatemp_end > hid_datatemp_begin)
{
// 检查是否有足够的数据填满一帧(63字节,第1字节为长度)
if((hid_datatemp_end - hid_datatemp_begin) >= 63)
{
notfull_timeout = 0; // 重置超时计数器
Transi_Buffer[0] = 63; // 设置数据长度为63字节
// 复制63字节数据到发送缓冲区
for( i=0; i<63; i++)
{
Transi_Buffer[i+1] = hid_datatemp[hid_datatemp_begin++];
}
// 将数据复制到USB端点2的发送缓冲区
UserToPMABufferCopy(Transi_Buffer, ENDP2_TXADDR, 64);
SetEPTxValid(ENDP2); // 设置端点2为有效,启动发送
}
else
{
// 数据不足一帧,等待超时后发送
notfull_timeout++;
if(notfull_timeout == HID_SEND_TIMEOUT)
{
notfull_timeout = 0;
Transi_Buffer[0] = hid_datatemp_end - hid_datatemp_begin; // 设置实际数据长度
// 复制数据,不足部分补0
for( i=0; i<63; i++)
{
if(i<hid_datatemp_end - hid_datatemp_begin)
Transi_Buffer[i+1] = hid_datatemp[hid_datatemp_begin+i];
else
Transi_Buffer[i+1] = 0; // 数据不足时补0
}
hid_datatemp_begin = hid_datatemp_end; // 更新读指针
UserToPMABufferCopy(Transi_Buffer, ENDP2_TXADDR, 64);
SetEPTxValid(ENDP2);
}
}
}
// 情况2:写指针小于读指针(缓冲区已回绕)
else if(hid_datatemp_end < hid_datatemp_begin)
{
// 计算环形缓冲区中的数据总量(256字节 - 读指针 + 写指针)
if((256 - hid_datatemp_begin + hid_datatemp_end) >= 63)
{
notfull_timeout = 0;
Transi_Buffer[0] = 63;
// 复制数据(注意回绕处理)
for( i=0; i<63; i++)
{
Transi_Buffer[i+1] = hid_datatemp[hid_datatemp_begin++];
}
UserToPMABufferCopy(Transi_Buffer, ENDP2_TXADDR, 64);
SetEPTxValid(ENDP2);
}
else
{
// 数据不足,等待超时
notfull_timeout++;
if(notfull_timeout == HID_SEND_TIMEOUT)
{
notfull_timeout = 0;
Transi_Buffer[0] = 256 - hid_datatemp_begin + hid_datatemp_end; // 设置实际数据长度
// 复制数据,注意索引回绕(使用u8类型自动取模)
for( i=0; i<63; i++)
{
if(i<256 - hid_datatemp_begin + hid_datatemp_end)
Transi_Buffer[i+1] = hid_datatemp[(u8)(hid_datatemp_begin+i)];
else
Transi_Buffer[i+1] = 0;
}
hid_datatemp_begin = hid_datatemp_end; // 更新读指针
UserToPMABufferCopy(Transi_Buffer, ENDP2_TXADDR, 64);
SetEPTxValid(ENDP2);
}
}
}
}
我直接结合代码一步一步带你理解:单片机是怎么和电脑通信的。你看完基本就明白 USB HID 在干什么了。
我按真实通信流程讲,而不是按代码顺序讲。
一、电脑插上 USB 后发生什么
当你把单片机插入电脑:
电脑会做一件事:
USB 枚举(Enumeration)
电脑会问:
PC:你是谁?
单片机回答:
我是 HID 设备
这个过程就是:
void USB_HID_Init(void)
{
Set_USBClock(); // USB时钟48MHz
USB_Init(); // 初始化USB
USB_ReceiveFlg = 0;
USB_Connect(0); // 断开USB
delay_ms(100);
USB_Connect(1); // 重新连接
USB_Interrupts_Config(); // USB中断
}
作用就是:
启动USB设备
让电脑重新识别
流程:
MCU启动
↓
USB_Init()
↓
PC识别为 HID设备
↓
加载系统驱动
因为是 HID设备:
Windows自带驱动
所以 不用安装驱动。
二、USB数据是怎么收的(PC → 单片机)
关键函数:
Usb_Hid_Receive()
代码:
if (USB_ReceiveFlg)
这个标志说明:
电脑发数据来了
数据已经被 USB 中断接收到:
PC → USB → Hid_RxData[]
缓冲区:
u8 Hid_RxData[64];
为什么是 64?
因为:
HID最大包 = 64字节
PC发来的数据格式
代码里有:
if(Hid_RxData[1] == 0xaa && Hid_RxData[2] == 0xaF )
说明 PC 发的数据格式是:
长度
AA
AF
命令
数据
校验
例如:
10 AA AF 01 02 03 04 xx
结构:
|len|AA|AF|cmd|data|checksum|
校验数据
代码:
for(i=1;i<Hid_RxData[0];i++)
{
check_sum+= Hid_RxData[i];
}
意思:
计算校验和
然后验证:
if(check_sum == Hid_RxData[Hid_RxData[0]])
如果正确:
调用协议解析
代码:
ANO_Recive((int8_t*)(Hid_RxData+1));
也就是:
USB只负责传输
真正的协议是ANO协议
三、单片机发送数据给电脑
关键函数:
Usb_Hid_Send()
发送流程是:
数据 → 缓冲区 → USB端点 → PC
第一步:写入发送缓冲区
代码:
void Usb_Hid_Adddata(u8 *dataToSend , u8 length)
{
for(i=0; i<length; i++)
{
hid_datatemp[hid_datatemp_end++] = dataToSend[i];
}
}
作用:
把数据写进环形缓冲区
缓冲区:
hid_datatemp[256]
为什么要缓冲?
因为:
USB一次只能发64字节
四、为什么63字节
代码:
Transi_Buffer[0] = 63;
发送结构:
|长度|数据63字节|
总共:
64字节
这是 HID报告大小。
64 bytes
五、真正发送 USB 数据
关键代码:
UserToPMABufferCopy(Transi_Buffer, ENDP2_TXADDR, 64);
SetEPTxValid(ENDP2);
意思:
第一步:
把数据复制到USB内存
第二步:
告诉USB控制器可以发送
USB硬件就会:
PC轮询
↓
USB发送
↓
PC接收
六、USB通信真实流程
结合这段代码,真实流程是:
电脑发送数据
PC
↓
USB
↓
Hid_RxData[64]
↓
USB_ReceiveFlg = 1
↓
Usb_Hid_Receive()
↓
ANO协议解析
单片机发送数据
ANO_Send()
↓
Usb_Hid_Adddata()
↓
hid_datatemp[]
↓
Usb_Hid_Send()
↓
Transi_Buffer
↓
USB Endpoint2
↓
PC收到
七、USB端点
这段代码用了:
Endpoint2
代码:
ENDP2
说明:
Endpoint0 → 控制
Endpoint1 → HID
Endpoint2 → 数据发送
八、为什么用环形缓冲区
关键变量:
hid_datatemp_begin
hid_datatemp_end
作用:
防止USB发送速度 < MCU产生数据速度
否则会:
数据丢失
所以:
先存缓冲区
慢慢发
九、USB HID 在这个项目里干什么
其实这个USB只是:
飞控 ↔ 上位机通信
例如:
上位机:
ANO地面站
发送:
PID参数
飞控返回:
姿态
传感器
PID
通信通道:
USB HID
十、总结(这段代码干了什么)
整个USB通信系统:
PC
│
│ HID
│
STM32
│
│ ANO协议
│
飞控系统
USB只负责:
传输数据
协议是:
ANO协议
十一、最关键理解(很多人不知道)
USB HID 在嵌入式里其实只是:
一个免驱动的数据通道
就像:
USB串口
但:
HID不用驱动
所以很多飞控用:
HID
更多推荐



所有评论(0)