通过 STM32CubeMX 生成 HID 双向通讯工程

judy的头像
judy 发布于:周一, 07/03/2017 - 16:18 ,关键词:

前言

客户在做 USB 通讯的时候,基本的需求就是发送某些数据到 USB host 端,同时接收一些数据从 USB Host 端,那么如何快速的建立一个工程并验证数据是否正确呢?下边我们就结合 STM32F072 的评估板(其他的 STM32xx 系列的实现方式都是类似的)来快速实现一个简单的数据收发实验。

问题分析

USB Host 软件

PC 端软件使用 ST 免费提供的 Usb Hid Demonstrator。这个软件可以在 ST 官网上免费下载到。连接
地址:STSW-STM32084,此软件调用的是 windows 标准的 HID 类驱动,所以无需安装任何驱动程序及可运行。

通过 STM32CubeMX 生成 HID 双向通讯工程

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

其次,在 Pinout 选项中,开打 USB 的 device 功能。
通过 STM32CubeMX 生成 HID 双向通讯工程

并在 Middleware 中选择开启 class for IP 中的 custom Human Interface Device(HID)。
通过 STM32CubeMX 生成 HID 双向通讯工程

点击“保存”后直接生成工程。我们这里以生成 IAR 工程为例,项目名叫做 HID
通过 STM32CubeMX 生成 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 的设备管理器中会看到如下图所示的内容。

通过 STM32CubeMX 生成 HID 双向通讯工程

注意:开发板上有两个一模一样的 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 内,连接上位机软件即可看到如下所示的进度条在不断的增长。

通过 STM32CubeMX 生成 HID 双向通讯工程

这个就是我们上传到的数据在上位机的图形显示,你也可以看 Input/output transfer 里的数据变化。
通过 STM32CubeMX 生成 HID 双向通讯工程

这样看起来是不是更像是串口调试助手了?嘿嘿 本来机制就差不多的。

数据接收

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 电量或者关闭。

通过 STM32CubeMX 生成 HID 双向通讯工程

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

总结:

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

围观 1167