stm32cubx-usb-hid键鼠
发布版本: 1.0
文件密级: 公开资料
前言
概述
读者
本文档(本指南)主要使用于以下工程师:
软件开发工程师
产品版本 修订记录
日期 | 版本 | 作者/邮箱 | 修订说明 |
---|---|---|---|
2019-10-5 | V1.0 | wingceltis-c / wingceltis@aliyun.com | 初始版本 |
1. 简介
HID是Human Interface Device的缩写,由其名称可以了解HID设备是直接与人交互的设备,例如键盘、鼠标与游戏杆等。不过HID设备并不一定要有人机接口,只要符合HID类别规范的设备都是HID设备。HID的一大优势就是操作系统已经内置了HID设备类驱动,一般不需要安装驱动程序。通用的HID设备可以直接使用HID设备类驱动程序(hidclass.sys)和HID小驱动程序(hidusb.sys)。
2. Cube配置
-
USB HID配置
HID配置比较简单,使能USB外设,并且选择HID 设备就行了。有些开发板USB的 PD+引脚的上拉电阻是通过三极管控制的,需要单独使能GPIO来控制。
-
时钟配置
时钟配置最主要能保证USB总线的时钟是48MHz即可。
-
工程配置
USB设备需要大一点的堆栈,网上都是这样说,我也没具体研究过,可以先这样配置,等熟悉后再考虑减少堆栈大小,不然这里有问题话,还得查半天浪费时间。
-
鼠标功能验证
上面配置好后编译烧写到开发板PC端就会枚举出一个鼠标设备,没有出现枚举设备可以看下开发板的USB PD+是否有上拉电阻,有三极管控制的话控制引脚是否配置正确了。
如果PD+只有一个上拉电阻的情况下可以在系统时钟初始化后调用下面的方法模拟USB拔插让电脑识别。
USB_Port_Set(0);
HAL_Delay(1800);
USB_Port_Set(1);
HAL_Delay(1000);
//USB使能连接/断线 enabele:0 断开, 1 允许连接
void USB_Port_Set(uint8_t enable)
{
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOA); //开启GPIOA时钟
if(enable)_SetCNTR(_GetCNTR()&(~(1<<1))); //退出断电模式
else
{
_SetCNTR(_GetCNTR()|(1<<1)); //断电模式
GPIOA->CRH&=0XFFF00FFF;
GPIOA->CRH|=0X00033000;
LL_GPIO_ResetOutputPin(GPIOA, GPIO_PIN_12);
}
}
鼠标发送给PC的数据每次4个字节
BYTE1 BYTE2 BYTE3 BYTE4
定义分别是:
BYTE1 –
|–bit7: 1 表示 Y 坐标的变化量超出-256 ~ 255的范围,0表示没有溢出
|–bit6: 1 表示 X 坐标的变化量超出-256 ~ 255的范围,0表示没有溢出
|–bit5: Y 坐标变化的符号位,1表示负数,即鼠标向下移动
|–bit4: X 坐标变化的符号位,1表示负数,即鼠标向左移动
|–bit3: 恒为1
|–bit2: 1表示中键按下
|–bit1: 1表示右键按下
|–bit0: 1表示左键按下
BYTE2 – X坐标变化量,与byte的bit4组成9位符号数,负数表示向左移,正数表右移。用补码表示变化量
BYTE3 – Y坐标变化量,与byte的bit5组成9位符号数,负数表示向下移,正数表上移。用补码表示变化量
BYTE4 – 滚轮变化。
BYTE1高5位是可以不用关注的,一般这5bit 在HID描述符中都是作为填充位使用,置0即可。
按照上面的鼠标数据格式发送给电脑端验证功能是否正常。
extern USBD_HandleTypeDef hUsbDeviceFS;
void mouse_send(int8_t x, int8_t y)
{
uint8_t buf[4] = {0,x,y,0};
USBD_HID_SendReport(&hUsbDeviceFS, buf, 4);
}
在主函数调用mouse_send, 正常编译烧写成功后能看到鼠标每隔1s往屏幕右下角移动一段距离。
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
mouse_send(10,10);
HAL_Delay(1000);
/* USER CODE BEGIN 3 */
}
3. 键鼠功能
键鼠都是HID设备,所以最简单的只要修改HID描述符以及缓存大小即可。
修改usbd_hid.c文件
__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END =
{
//-------------键盘部分报告描述符----------------
//表示用途页为通用桌面设备
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
//表示用途为键盘
0x09, 0x06, // USAGE (Keyboard)
//表示应用集合,必须要以END_COLLECTION来结束它,见最后的END_COLLECTION
0xa1, 0x01, // COLLECTION (Application)
//报告ID(报告ID 0是保留的)
0x85, 0x01, //Report ID (1)
//表示用途页为按键
0x05, 0x07, // USAGE_PAGE (Keyboard)
//用途最小值,这里为左ctrl键
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
//用途最大值,这里为右GUI键,即window键
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
//逻辑最小值为0
0x15, 0x00, // LOGICAL_MINIMUM (0)
//逻辑最大值为1
0x25, 0x01, // LOGICAL_MAXIMUM (1)
//报告大小(即这个字段的宽度)为1bit,所以前面的逻辑最小值为0,逻辑最大值为1
0x75, 0x01, // REPORT_SIZE (1)
//报告的个数为8,即总共有8个bits
0x95, 0x08, // REPORT_COUNT (8)
//输入用,变量,值,绝对值。像键盘这类一般报告绝对值,
//而鼠标移动这样的则报告相对值,表示鼠标移动多少
0x81, 0x02, // INPUT (Data,Var,Abs)
//上面这这几项描述了一个输入用的字段,总共为8个bits,每个bit表示一个按键
//分别从左ctrl键到右GUI键。这8个bits刚好构成一个字节,它位于报告的第一个字节。
//它的最低位,即bit-0对应着左ctrl键,如果返回的数据该位为1,则表示左ctrl键被按下,
//否则,左ctrl键没有按下。最高位,即bit-7表示右GUI键的按下情况。中间的几个位,
//需要根据HID协议中规定的用途页表(HID Usage Tables)来确定。这里通常用来表示
//特殊键,例如ctrl,shift,del键等
//这样的数据段个数为1
0x95, 0x01, // REPORT_COUNT (1)
//每个段长度为8bits
0x75, 0x08, // REPORT_SIZE (8)
//输入用,常量,值,绝对值
0x81, 0x03, // INPUT (Cnst,Var,Abs)
//上面这8个bit是常量,设备必须返回0
//这样的数据段个数为5
0x95, 0x05, // REPORT_COUNT (5)
//每个段大小为1bit
0x75, 0x01, // REPORT_SIZE (1)
//用途是LED,即用来控制键盘上的LED用的,因此下面会说明它是输出用
0x05, 0x08, // USAGE_PAGE (LEDs)
//用途最小值是Num Lock,即数字键锁定灯
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
//用途最大值是Kana,这个是什么灯我也不清楚^_^
0x29, 0x05, // USAGE_MAXIMUM (Kana)
//如前面所说,这个字段是输出用的,用来控制LED。变量,值,绝对值。
//1表示灯亮,0表示灯灭
0x91, 0x02, // OUTPUT (Data,Var,Abs)
//这样的数据段个数为1
0x95, 0x01, // REPORT_COUNT (1)
//每个段大小为3bits
0x75, 0x03, // REPORT_SIZE (3)
//输出用,常量,值,绝对
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
//由于要按字节对齐,而前面控制LED的只用了5个bit,
//所以后面需要附加3个不用bit,设置为常量。
//报告个数为6
0x95, 0x06, // REPORT_COUNT (6)
//每个段大小为8bits
0x75, 0x08, // REPORT_SIZE (8)
//逻辑最小值0
0x15, 0x00, // LOGICAL_MINIMUM (0)
//逻辑最大值255
0x25, 0xFF, // LOGICAL_MAXIMUM (255)
//用途页为按键
0x05, 0x07, // USAGE_PAGE (Keyboard)
//使用最小值为0
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
//使用最大值为0x65
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
//输入用,变量,数组,绝对值
0x81, 0x00, // INPUT (Data,Ary,Abs)
//以上定义了6个8bit宽的数组,每个8bit(即一个字节)用来表示一个按键,所以可以同时
//有6个按键按下。没有按键按下时,全部返回0。如果按下的键太多,导致键盘扫描系统
//无法区分按键时,则全部返回0x01,即6个0x01。如果有一个键按下,则这6个字节中的第一
//个字节为相应的键值(具体的值参看HID Usage Tables),如果两个键按下,则第1、2两个
//字节分别为相应的键值,以次类推。
//关集合,跟上面的对应
0xc0 , // END_COLLECTION
//-----------------------鼠标部分报告描述符----------------------------
//每行开始的第一字节为该条目的前缀,前缀的格式为:
//D7~D4:bTag。D3~D2:bType;D1~D0:bSize。以下分别对每个条目注释。
//这是一个全局(bType为1)条目,选择用途页为普通桌面Generic Desktop Page(0x01)
//后面跟一字节数据(bSize为1),后面的字节数就不注释了,
//自己根据bSize来判断。
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
//这是一个局部(bType为2)条目,说明接下来的应用集合用途用于鼠标
0x09, 0x02, // USAGE (Mouse)
//这是一个主(bType为0)条目,开集合,后面跟的数据0x01表示
//该集合是一个应用集合。它的性质在前面由用途页和用途定义为
//普通桌面用的鼠标。
0xa1, 0x01, // COLLECTION (Application) //1byte报告ID +按键(3bit)+填充行(5bit)=1byte ; 坐标(16bits*2个)=4bytes;所以上报数据就是6bytes;
0x85,0x02, // 报告ID (2)
//这是一个局部条目。说明用途为指针集合
0x09, 0x01, // USAGE (Pointer)
//这是一个主条目,开集合,后面跟的数据0x00表示该集合是一个
//物理集合,用途由前面的局部条目定义为指针集合。
0xa1, 0x00, // COLLECTION (Physical)
//这是一个全局条目,选择用途页为按键(Button Page(0x09))
0x05, 0x09, // USAGE_PAGE (Button)
//这是一个局部条目,说明用途的最小值为1。实际上是鼠标左键。
0x19, 0x01, // USAGE_MINIMUM (Button 1)
//这是一个局部条目,说明用途的最大值为3。实际上是鼠标中键。
0x29, 0x03, // USAGE_MAXIMUM (Button 3)
//这是一个全局条目,说明返回的数据的逻辑值(就是我们返回的数据域的值啦)
//最小为0。因为我们这里用Bit来表示一个数据域,因此最小为0,最大为1。
0x15, 0x00, // LOGICAL_MINIMUM (0)
//这是一个全局条目,说明逻辑值最大为1。
0x25, 0x01, // LOGICAL_MAXIMUM (1)
//这是一个全局条目,说明数据域的数量为三个。
0x95, 0x03, // REPORT_COUNT (3)
//这是一个全局条目,说明每个数据域的长度为1个bit。
0x75, 0x01, // REPORT_SIZE (1)
//这是一个主条目,说明有3个长度为1bit的数据域(数量和长度
//由前面的两个全局条目所定义)用来做为输入,
//属性为:Data,Var,Abs。Data表示这些数据可以变动,Var表示 //Var可以任意大小数据但是最小值要从0开始 Abs只能一个字节
//这些数据域是独立的,每个域表示一个意思。Abs表示绝对值。
//这样定义的结果就是,第一个数据域bit0表示按键1(左键)是否按下,
//第二个数据域bit1表示按键2(右键)是否按下,第三个数据域bit2表示
//按键3(中键)是否按下。
0x81, 0x02, // INPUT (Data,Var,Abs)
//这是一个全局条目,说明数据域数量为1个
0x95, 0x01, // REPORT_COUNT (1)
//这是一个全局条目,说明每个数据域的长度为5bit。
0x75, 0x05, // REPORT_SIZE (5)
//这是一个主条目,输入用,由前面两个全局条目可知,长度为5bit,
//数量为1个。它的属性为常量(即返回的数据一直是0)。
//这个只是为了凑齐一个字节(前面用了3个bit)而填充的一些数据
//而已,所以它是没有实际用途的。
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95,0x03, // 这是一个全局条目,说明数据域的个数为3个。
0x75,0x08, // 这是一个全局条目,说明数据域的长度为8bit。
0x05,0x01, // 这是一个全局条目,选择用途页为普通桌面Generic Desktop Page(0x01)
0x09,0x30, // 这是一个局部条目,说明用途为X轴
0x09,0x31, // 这是一个局部条目,说明用途为Y轴
0x09,0x38, // 这是一个局部条目,说明用途为滚轴
0x15,0x81, // 这是一个全局条目,说明返回的逻辑最小为-128。
0x25,0x7f, // 这是一个全局条目,说明返回的逻辑最大为127。
0x81,0x06, // 这是一个主条目。标识上面的3个数据是绝对值。
0xc0, // 我们开了两个集合,所以要关两次。bSize为0,所以后面没数据。
0xc0 // END_COLLECTION
/****************************单点描述符结束*********************/
};
还需要修改HID_MOUSE_REPORT_DESC_SIZE宏大小, usbd_hid.h
#define HID_EPIN_SIZE 0x10
#define HID_MOUSE_REPORT_DESC_SIZE 119
前面的鼠标发送函数需要修改下,增加报告ID,这个ID是在HID描述符中配置的。
extern USBD_HandleTypeDef hUsbDeviceFS;
void mouse_send(int8_t x, int8_t y)
{ //0x02就是报告ID
uint8_t buf[5] = {0x02,x,y,0};
USBD_HID_SendReport(&hUsbDeviceFS, buf, 5);
}
USB键盘数据包含8个字节
BYTE1 – 特殊按键
|–bit0: Left Control是否按下,按下为1
|–bit1: Left Shift 是否按下,按下为1
|–bit2: Left Alt 是否按下,按下为1
|–bit3: Left GUI(Windows键) 是否按下,按下为1
|–bit4: Right Control是否按下,按下为1
|–bit5: Right Shift 是否按下,按下为1
|–bit6: Right Alt 是否按下,按下为1
|–bit7: Right GUI 是否按下,按下为1
BYTE2 – 好像必须为0,反正0没错
BYTE3-BYTE8 当前按下的普通按键键值,也就是最多六个按键
void keyboard_send(int8_t key)
{
uint8_t buf[9] = {0x01,0,0,key,0};
USBD_HID_SendReport(&hUsbDeviceFS, buf, 9);
}
因为鼠标和键盘用的是同一个EP端口,两条数据之间最好添加延迟,不然会引起数据覆盖问题。
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
mouse_send(10,10);
HAL_Delay(100);
keyboard_send(4);
HAL_Delay(1000);
mouse_send(10,10);
HAL_Delay(100);
keyboard_send(0);
HAL_Delay(1000);
/* USER CODE BEGIN 3 */
}