一、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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐