(5)STM32 USB设备开发-USB键盘
讲解视频:2、USB键盘-下_哔哩哔哩_bilibili
例程:STM32USBdevice: 基于STM32的USB设备例子程序 - Gitee.com
本篇为使用使用STM32模拟USB键盘的例程,没有知识,全是实操,按照步骤就能获得一个STM32的USB键盘。本例子是在野火F103MINI开发板上验证的,如果代码中出现一些外设的配置,可以参考野火F103MINI开发板原理图对照。
设置外部晶振,必须要使用外部晶振,因为USB控制器需要48M的系统时钟,内部晶振无法倍频出48M。
配置外部时钟
配置调试口和系统基准源
开启USB设备
中间件中设备USB设备类型
标蓝色的部分需要根据你之前有没有使用过这两VID和PID,如果使用过最好换一下,避免使用之前的驱动引起一些奇奇怪怪的问题。
我使用了freertos v2
配置外部系统时钟
配置独立C和H文件
下面就讲一下设备描述符,设备描述符就像一个身份证一样,它包含了这个USB设备的全部信息,说明了USB设备的通用信息,包含应用到全部设备和所有设备配置的信息。USB设备只有一个设备描述符。设备描述符是在设备连接时主机读取的第一个描述符。设备描述符所含的信息,被主机用来取得设备的额外内容。设备描述符提供了关于设备、设备的配置以及任何设备所归属的类的信息。
如果你对USB协议足够了解,可以手写,但是还可以使用官方提供了一个专门的工具来生成,下载地址:HID Descriptor Tool | USB-IF
界面如下:
我们通过菜单的 FILE/Open,弹出需要打开的HID报告描述符。
这里我们选择键盘的报告描述符:keybrd.hid
如需要修改某些值,可以双击选中需要修改的行,如我们双击 INPUT(Data,Var,Abs) 81 02,弹出其修改页:
如果需要添加item可以在界面左侧 HID Items 栏中是一系列的 Item,通过双击需要的 Item 添加到右侧 Report Descriptor 中。添加过程中该工具会根据不同的 Item 让你选择或者填入值。
点击 Parser Descriptor 就会显示解析的结果
如果描述符有错,会给予提示,例如把例子中的 END_COLLECTION 去掉,再进行校验就会有如下提示:
修改完成以后,点击“File -> Save As”,保存为.h格式。保存完成后打开,效果如下:
char ReportDescriptor[63] = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xc0 // END_COLLECTION
};
在上面的描述符中可以看到INPUT给电脑的是一个大小为REPORT_SIZE (8)的Ary数组的Data变量,
所以我们需要创建一个8字节的数组。
键盘发送给PC的数据每次8个字节
BYTE1 BYTE2 BYTE3 BYTE4 BYTE5 BYTE6 BYTE7 BYTE8
定义分别是:
BYTE0 --(0 = OFF,1 = ON,CONSTANT为保留位)
|--bit0: Left Control是否按下,按下为1
|--bit1: Left Shift 是否按下,按下为1
|--bit2: Left Alt 是否按下,按下为1
|--bit3: Left GUI 是否按下,按下为1
|--bit4: Right Control是否按下,按下为1
|--bit5: Right Shift 是否按下,按下为1
|--bit6: Right Alt 是否按下,按下为1
|--bit7: Right GUI 是否按下,按下为1
BYTE1 -- 为常量值,保留字节
BYTE2--BYTE7 -- 这六个为普通按键
代码需要修改如下:
在Middlewares\ST\STM32_USB_Device_Library\Class\HID\Src\usbd_hid.c文件中添加
__ALIGN_BEGIN static uint8_t HID_KEYBOARD_ReportDesc[HID_KEYBOARD_REPORT_DESC_SIZE] __ALIGN_END = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xc0 // END_COLLECTION
};
在文件Middlewares\ST\STM32_USB_Device_Library\Class\HID\Inc\usbd_hid.h中添加
#define HID_KEYBOARD_REPORT_DESC_SIZE 63U
将程序中所有HID_MOUSE_ReportDesc替换为HID_KEYBOARD_ReportDesc
将程序中所有HID_MOUSE_REPORT_DESC_SIZE替换为HID_KEYBOARD_REPORT_DESC_SIZE.
配置描述符需要修改,在文件Middlewares\ST\STM32_USB_Device_Library\Class\HID\Src\usbd_hid.c中
修改端点大小,在文件Middlewares\ST\STM32_USB_Device_Library\Class\HID\Inc\usbd_hid.h中,因为我们上边定义的input是8个字节,所以这里改为8
我是在Core\Src\freertos.c文件中添加了按键检测的代码
/* init code for USB_DEVICE */
MX_USB_DEVICE_Init();
/* USER CODE BEGIN StartDefaultTask */
uint8_t keyBoard[8] = {0};
uint8_t key1Status = 0;
uint8_t key2Status = 0;
TickType_t xLastFlashTime = osKernelGetTickCount();
/* Infinite loop */
for (;;) {
// 获取当前时间戳
TickType_t xCurrentTime = osKernelGetTickCount();
// 检查是否已经过了 1 秒(pdMS_TO_TICKS 函数将毫秒转换为系统时钟节拍)
if ((xCurrentTime - xLastFlashTime) >= pdMS_TO_TICKS(1000)) {
// 切换 LED1 的状态
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
// HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
// 更新上一次的时间戳
xLastFlashTime = xCurrentTime;
}
if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_SET) {
if (key1Status == 0) {
keyBoard[2] = 4; // 设置a按键状态
USBD_HID_SendReport(&hUsbDeviceFS, keyBoard, 8);
key1Status = 1;
}
} else {
if (key1Status == 1) {
key1Status = 0;
keyBoard[2] = 0; // 清除按键状态
USBD_HID_SendReport(&hUsbDeviceFS, keyBoard, 8);
}
}
if (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_SET) {
if (key2Status == 0) {
keyBoard[2] = 5; // 设置b按键状态
USBD_HID_SendReport(&hUsbDeviceFS, keyBoard, 8);
key2Status = 1;
}
} else {
if (key2Status == 1) {
key2Status = 0;
keyBoard[2] = 0; // 清除按键状态
USBD_HID_SendReport(&hUsbDeviceFS, keyBoard, 8);
}
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
当检测到KEY1按下的时候我们在数组2中写入4,对应下图按键Usage ID“a”字母,当检测到KEY2按下的时候我们在数组2中写入5,对应下图按键Usage ID“b”字母。
这里主要是一个发送的函数需要我们来实现,函数名称为:USBD_HID_SendReport,我们可以跳转到这个函数的定义,函数说明
/**
* @brief USBD_HID_SendReport
* Send HID Report
* @param pdev: device instance
* @param buff: pointer to report
* @retval status
*/
uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev,
uint8_t *report,
uint16_t len)
第一个参数为USB设备的枚举,第二个设备为要发送的报文信息,第三个为报文的长度,这里我们先定义一个报文的数组,之后在不断发送数据即可。
第一个参数需要从#include "usbd_def.h"头文件中引出
/* USER CODE BEGIN Variables */
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE END Variables */
引用头文件
/* USER CODE BEGIN Includes */
#include "usbd_def.h"
#include "usbd_hid.h"
/* USER CODE END Includes */
编译下载后能看到键盘设备
按下按键会发现文本中有字符输出。
键盘灯支持
我们在报告描述符中加入了一个output端点信息,就是灯信息。
我们要在代码中接收到灯信息的消息,因为我们是从鼠标工程的基础上进行修改的,而鼠标只有输入,没有输出,所以我们要增加一些输出端点的代码,内容有点多,下面一个个来。
首先我们在USB_DEVICE\Target\usbd_conf.c文件中需要修改PMA的端点映射,让PMA为输出端点分配内存
在USBD_LL_Init函数中,添加修改如下内容
/* USER CODE BEGIN EndPoint_Configuration */
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x20);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x60);
/* USER CODE END EndPoint_Configuration */
/* USER CODE BEGIN EndPoint_Configuration_HID */
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xA0);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0xE0);
/* USER CODE END EndPoint_Configuration_HID */
这里我们看到一共用到了4个端点,分别是输入端点0X80和0X81,输出端点0X00和0X01,其中0X00端点和0X80端点是供USB使用必须有的,0X81和0X01端点则是MSC设备输入输出端点。
那么一共使用了4个端点,按理来说PMA头部的端点描述大小应该是4X8=32(十六进制的0X20)字节,0X20之后的才是各个端点缓冲区。STM32的PMA一共512字节,也就是缓冲区大小一共可以到0x1FF。
输出端点0缓冲区对应0X20,输入端点的缓冲区是0X60,是因为USB全速设备的最大包是64字节(十进制的0X40),所以这里PMA的划分就是:
头部0X20字节为各个端点的描述
0X20地址开始的64字节为输出端点0的缓冲区
0X60地址开始的64字节为输入端点0的缓冲区
0XA0地址开始的64字节为输入端点1的缓冲区
0XE0地址开始的64字节为输出端点1的缓冲区
因为我们要添加一个输出端点的描述,所以我们还需要为这个输出端点准备一些宏,在Middlewares\ST\STM32_USB_Device_Library\Class\HID\Inc\usbd_hid.h文件中添加如下内容。
#define HID_EPIN_ADDR 0x81U
#define HID_EPOUT_ADDR 0x01U
#define HID_EPIN_SIZE 0x08U
#define HID_EPOUT_SIZE 0x02U
这里对应了在USBD_LL_Init函数中的端点地址0x01,还有端点大小2字节,其实我们报告描述符中只定义了一个字节,但是STM32是两字节对齐的,所以我们这里也就需要定义为2字节
我们还需要在Middlewares\ST\STM32_USB_Device_Library\Class\HID\Src\usbd_hid.c文件中修改配置描述符USBD_HID_CfgFSDesc,USBD_HID_CfgHSDesc,USBD_HID_OtherSpeedCfgDesc,因为我们需要告诉PC我们还有一个端点。
在每个变量的末尾添加如下代码,看注释应该就能知道是什么意思了。
0x07, /* bLength: Endpoint Descriptor size */
USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
HID_EPOUT_ADDR, /*bEndpointAddress: Endpoint Address (OUT)*/
0x03, /* bmAttributes: Interrupt endpoint */
HID_EPOUT_SIZE, /* wMaxPacketSize: 2 Bytes max */
0x00,
HID_HS_BINTERVAL, /* bInterval: Polling Interval */
/* 41 */
对比如下:
我们会发现添加完成后我们的变量大小改变了,由原来的34个字节增加到了41个字节,所以我们还需要修改变量大小的宏定义。
在Middlewares\ST\STM32_USB_Device_Library\Class\HID\Inc\usbd_hid.h文件中
下面我们开始添加接收函数,让我们能收到灯变化的消息
我们需要在Middlewares\ST\STM32_USB_Device_Library\Class\HID\Src\usbd_hid.c文件中定义USBD_HID_DataOut函数,并且在USBD_HID变量中添加。
在配置描述符中修改端点数量为2,这一步很重要,也是很容易遗漏的一步。
在函数USBD_HID_Init中添加端点初始化代码,并且执行一次USBD_LL_PrepareReceive函数,为接收端点准备接收变量。这里定义了一个uint8_t pbuf[5];全局变量,因为USBD_LL_PrepareReceive函数需要先调用,后从pbuf中拿到结果,拿到后还需要再次调用USBD_LL_PrepareReceive函数,后面会看到。
uint8_t pbuf[5]; // 定义接收数据全局变量
/**
* @brief USBD_HID_Init
* Initialize the HID interface
* @param pdev: device instance
* @param cfgidx: Configuration index
* @retval status
*/
static uint8_t USBD_HID_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{
/* Open EP IN */
USBD_LL_OpenEP(pdev, HID_EPIN_ADDR, USBD_EP_TYPE_INTR, HID_EPIN_SIZE);
pdev->ep_in[HID_EPIN_ADDR & 0xFU].is_used = 1U;
/* Open EP OUT */
USBD_LL_OpenEP(pdev, HID_EPOUT_ADDR, USBD_EP_TYPE_INTR, HID_EPOUT_SIZE);
pdev->ep_out[HID_EPOUT_ADDR & 0xFU].is_used = 1U;
pdev->pClassData = USBD_malloc(sizeof(USBD_HID_HandleTypeDef));
if (pdev->pClassData == NULL)
{
return USBD_FAIL;
}
else
{
USBD_LL_PrepareReceive(pdev, HID_EPOUT_ADDR , pbuf, HID_EPOUT_SIZE);
}
((USBD_HID_HandleTypeDef *)pdev->pClassData)->state = HID_IDLE;
return USBD_OK;
}
在USBD_HID_DeInit函数中添加端点关闭函数。
static uint8_t USBD_HID_DeInit(USBD_HandleTypeDef *pdev,
uint8_t cfgidx)
{
/* Close HID EPs */
USBD_LL_CloseEP(pdev, HID_EPIN_ADDR);
pdev->ep_in[HID_EPIN_ADDR & 0xFU].is_used = 0U;
USBD_LL_CloseEP(pdev, HID_EPOUT_ADDR);
pdev->ep_out[HID_EPOUT_ADDR & 0xFU].is_used = 0U;
/* FRee allocated memory */
if (pdev->pClassData != NULL)
{
USBD_free(pdev->pClassData);
pdev->pClassData = NULL;
}
return USBD_OK;
}
USBD_HID_DataOut函数实现,这里我做了个LED2的闪烁,并打印接收到的消息,从pbuf中拿到结果,拿到后还需要再次调用USBD_LL_PrepareReceive函数。
static uint8_t USBD_HID_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
printf("data out: %x %x\n", pbuf[0], pbuf[1]);
USBD_LL_PrepareReceive(pdev, HID_EPOUT_ADDR , pbuf, HID_EPOUT_SIZE);
return USBD_OK;
}
我们街道电脑上后,当按另一个键盘上的能够让灯亮的按键:Caps Lock,Num Lock, Scroll Lock时板子会收到灯消息。