
前言
客户在做 USB 通讯的时候,基本的需求就是发送某些数据到 USB host 端,同时接收一些数据从 USB Host 端,那么如何快速的建立一个工程并验证数据是否正确呢?下边我们就结合 STM32F072 的评估板(其他的 STM32xx 系列的实现方式都是类似的)来快速实现一个简单的数据收发实验。
问题分析
USB Host 软件
PC 端软件使用 ST 免费提供的 Usb Hid Demonstrator。这个软件可以在 ST 官网上免费下载到。连接
地址:STSW-STM32084,此软件调用的是 windows 标准的 HID 类驱动,所以无需安装任何驱动程序及可运行。

下载安装完这个软件之后,我们就可以开始开发 STM32 的 USB 从机程序了。
首先,打开 STM32CubeMX,新建工程,选择 STM32F072B-DISCOVERY 开发板。

其次,在 Pinout 选项中,开打 USB 的 device 功能。

并在 Middleware 中选择开启 class for IP 中的 custom Human Interface Device(HID)。

点击“保存”后直接生成工程。我们这里以生成 IAR 工程为例,项目名叫做 HID

这样我们的工程就基本成功了,但是还缺少最最关键的一步,就是 USB 主机和从机的通讯“协议”,
这个协议在那里实现呢?因为我们 Host 端软件已经是 Usb Hid Demonstrator,那么这边的协议就已经
固定了(其实在实际的开发中大多是主机端和从机相互沟通后,软件自行修改的),从机只需要对应
这套协议即可。
将如下代码复制,替换掉 usbd_custom_hid_if.c 文件中的同名数组。
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS [USBD_CUSTOM_HID_REPORT_DESC_SIZE]
__ALIGN_END =
{
0x06, 0xFF, 0x00, /* USAGE_PAGE (Vendor Page: 0xFF00) */
0x09, 0x01, /* USAGE (Demo Kit) */
0xa1, 0x01, /* COLLECTION (Application) */
/* 6 */
/* LED1 */
0x85, LED1_REPORT_ID, /* REPORT_ID (1) */
0x09, 0x01, /* USAGE (LED 1) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, LED1_REPORT_COUNT, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, LED1_REPORT_ID, /* REPORT_ID (1) */
0x09, 0x01, /* USAGE (LED 1) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 26 */
/* LED2 */
0x85, LED2_REPORT_ID, /* REPORT_ID 2 */
0x09, 0x02, /* USAGE (LED 2) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, LED2_REPORT_COUNT, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, LED2_REPORT_ID, /* REPORT_ID (2) */
0x09, 0x02, /* USAGE (LED 2) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 46 */
/* LED3 */
0x85, LED3_REPORT_ID, /* REPORT_ID (3) */
0x09, 0x03, /* USAGE (LED 3) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, LED3_REPORT_COUNT, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, LED3_REPORT_ID, /* REPORT_ID (3) */
0x09, 0x03, /* USAGE (LED 3) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 66 */
/* LED4 */
0x85, LED4_REPORT_ID, /* REPORT_ID 4) */
0x09, 0x04, /* USAGE (LED 4) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, LED4_REPORT_COUNT, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, LED4_REPORT_ID, /* REPORT_ID (4) */
0x09, 0x04, /* USAGE (LED 4) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 86 */
/* key Push Button */
0x85, KEY_REPORT_ID, /* REPORT_ID (5) */
0x09, 0x05, /* USAGE (Push Button) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x09, 0x05, /* USAGE (Push Button) */
0x75, 0x01, /* REPORT_SIZE (1) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x75, 0x07, /* REPORT_SIZE (7) */
0x81, 0x83, /* INPUT (Cnst,Var,Abs,Vol) */
0x85, KEY_REPORT_ID, /* REPORT_ID (2) */
0x75, 0x07, /* REPORT_SIZE (7) */
0xb1, 0x83, /* FEATURE (Cnst,Var,Abs,Vol) */
/* 114 */
/* Tamper Push Button */
0x85, TAMPER_REPORT_ID,/* REPORT_ID (6) */
0x09, 0x06, /* USAGE (Tamper Push Button) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x09, 0x06, /* USAGE (Tamper Push Button) */
0x75, 0x01, /* REPORT_SIZE (1) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x75, 0x07, /* REPORT_SIZE (7) */
0x81, 0x83, /* INPUT (Cnst,Var,Abs,Vol) */
0x85, TAMPER_REPORT_ID,/* REPORT_ID (6) */
0x75, 0x07, /* REPORT_SIZE (7) */
0xb1, 0x83, /* FEATURE (Cnst,Var,Abs,Vol) */
/* 142 */
/* ADC IN */
0x85, ADC_REPORT_ID, /* REPORT_ID */
0x09, 0x07, /* USAGE (ADC IN) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x26, 0xff, 0x00, /* LOGICAL_MAXIMUM (255) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x85, ADC_REPORT_ID, /* REPORT_ID (7) */
0x09, 0x07, /* USAGE (ADC in) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
/* 161 */
0xc0 /* END_COLLECTION */
注意:这里一定要覆盖“同名”数组,千万不要覆盖错了。
之后将如下代码复制到 usbd_custom_hid_if_if.h 中。
#define LED1_REPORT_ID 0x01
#define LED1_REPORT_COUNT 0x01
#define LED2_REPORT_ID 0x02
#define LED2_REPORT_COUNT 0x01
#define LED3_REPORT_ID 0x03
#define LED3_REPORT_COUNT 0x01
#define LED4_REPORT_ID 0x04
#define LED4_REPORT_COUNT 0x01
#define KEY_REPORT_ID 0x05
#define TAMPER_REPORT_ID 0x06
#define ADC_REPORT_ID 0x07
最后在 usbd_conf.h 文件中将 USBD_CUSTOM_HID_REPORT_DESC_SIZE 的定义值修改为 163
(默认值是 2)
#define USBD_CUSTOM_HID_REPORT_DESC_SIZE 163 //2
为什么这样修改呢? 简单说一下其中关键值的含义。这个 HID 的报文描述符其实定义了 8 个部分(条
目)的功能定义,分为 LED1,LED2,LED3,LED4,按键输入,篡改按键输入和 ADC 输入。每一个
部分基本的格式都是固定的。以 LED1 为例(其他条目可自行对照文档解析):
0x85, LED1_REPORT_ID, 含义是这个功能的 ID 号是 LED1_REPORT_ID(宏定义为 0x01)
这个 ID 号是每次报文发送的时候最先被发送出去的(USB 都是 LSB)字节,之后跟着的才是我们实际
有效的数据/指令,到底是数据还是指令,就看你的应用程序如何去解析这个数据了。
0x09, 0x01, 这个功能序号为 1,后边的序号依次递加,没什么好说的 0x15, 0x00, 这个是规定逻辑最小值为 0 0x25, 0x01, 这个是规定逻辑最大值为 1 上边的这两条语句规定了跟在报文 ID 后边的数据范围,最大值是 1,最小值是 0.(因为我们的 LED 也 就只有灭和亮两种状态) 0x75, 0x08, 这个是报文的大小为 8,只要别写错就行了 0x95, LED1_REPORT_COUNT, 这个是说下边有 LED1_REPORT_COUNT (宏定义为 1)个项目会被添加, 即这个功能的数量是 1 个 0xB1, 0x82, 这个是规定能够发送给从机设备的数据信息 0x91, 0x82, 这个规定了这个功能的数据方向是输出(USB 的方向都是针对主机来说的)
总结一下,通过这个报文描述符,我们就告诉了主机,在 HID 中有一个功能 ID 为 1 的功能,其方向是
从主机到从机,每次发送 1 个有效数据(前边的 ID 是都要含有的),这个数据可以是 0 或者是 1.
关于 HID 报文描述符的详细信息,您可以在下边的网址下载一篇叫做《Device Class Definition for
HID》的文档来参考。
http://www.usb.org/developers/hidpage
这样基本的程序框架就已经成功了。此时我们可以先编译一下,看看是否有任何遗漏的或者笔误。如
果编译是正确的,那么我们就可以先下载到硬件开发板上,连接到 PC 端,看看是否可以枚举出设备。
如果您前边的修改都是正确的,那么在 PC 的设备管理器中会看到如下图所示的内容。

注意:开发板上有两个一模一样的 Mini USB 接口,一个是 USB USER,另 一个是 USB ST-link,下
载代码的时候用 USB ST-Link,连接电脑运行程序的时候要用 USB USER。
此时我们的 USB 枚举就完成了,这个是 USB 通讯的关键步骤,之后的应用通讯内容都是通过
这个枚举工程来进行“规划”的。
数据发送
就类似串口通讯,我们首先做一个数据的发送工作。
在 Main.c 文件中,我们在 while(1)的主循环中增加我们的发送函数,主要就是调用发送报文的 API:
USBD_CUSTOM_HID_SendReport()
/* USER CODE BEGIN 2 */
uint8_t i=0;
sendbuffer[0]=0x07; //这个是 report ID,每次发送报文都需要以这个为开始,这样主机才能正确
//解析后边的数据含义
sendbuffer[1]=0x01; //这个是实际发送的数据,可以自由定义,只要不超过报文描述符的限制
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(100); //延迟 100ms
sendbuffer[1]++; //每次发送都将变量自加 1
USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS,sendbuffer,2);//发送报文
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
编译后下载到 MCU 内,连接上位机软件即可看到如下所示的进度条在不断的增长。

这个就是我们上传到的数据在上位机的图形显示,你也可以看 Input/output transfer 里的数据变化。

这样看起来是不是更像是串口调试助手了?嘿嘿 本来机制就差不多的。
数据接收
MCU 的 USB 数据是如何接收的呢?是不调用一个类似于串口接收的 API 呢?
不是的!USB 的数据接收都是在中断中完成的,在新建的工程中,我们在函数
CUSTOM_HID_OutEvent_FS 内增加如下代码。
static int8_t CUSTOM_HID_OutEvent_FS (uint8_t event_idx, uint8_t state)
{
/* USER CODE BEGIN 6 */ switch(event_idx)
{
case 1: /* LED3 */
(state == 1) ? HAL_GPIO_WritePin(LD3_GPIO_Port,LD3_Pin,GPIO_PIN_SET) :
HAL_GPIO_WritePin(LD3_GPIO_Port,LD3_Pin,GPIO_PIN_RESET);
break;
case 2: /* LED4 */
(state == 1) ? HAL_GPIO_WritePin(LD4_GPIO_Port,LD4_Pin,GPIO_PIN_SET) :
HAL_GPIO_WritePin(LD4_GPIO_Port,LD4_Pin,GPIO_PIN_RESET);
break;
case 3: /* LED5 */
(state == 1) ? HAL_GPIO_WritePin(LD5_GPIO_Port,LD5_Pin,GPIO_PIN_SET) :
HAL_GPIO_WritePin(LD5_GPIO_Port,LD5_Pin,GPIO_PIN_RESET);
break;
case 4: /* LED6 */
(state == 1) ? HAL_GPIO_WritePin(LD6_GPIO_Port,LD6_Pin,GPIO_PIN_SET) :
HAL_GPIO_WritePin(LD6_GPIO_Port,LD6_Pin,GPIO_PIN_RESET);
break;
default:
break;
}
return (0);
/* USER CODE END 6 */
}
编译之后下载到 MCU 内,通过 USB USER 连接到 PC 端,打开 Usb Hid Demonstrator,我们可以通
过勾选右下角的图形界面来实现控制开发板上的 LED 电量或者关闭。

当然,这个是通过图像化的界面来进行控制,你也可以通过 Input/output transfer 中的写入对话框来完
成这个操作。注意,写入的第一个字节是 ID,表示你想控制的是哪个 LED;第二个字节是 0 或者是 1,表示你想让这个 LDE 的状态变成灭还是亮。

总结:
本范例程序是为了快速实现 USB 从机设备与主句设备双向通讯目的,其初始化代码是用STM32CubeMX 来生成的,大大降低了工程师开发 USB 设备的难度(尤其是是入门阶段的难度)。从这个工程的基础上,工程师可以比较方便的建立好框架工程并,对其中的代码进行研究,进而移植或增加自己的应用代码。