本帖最后由 lilijin1995 于 2022-11-12 13:41 编辑
#申请原创#
@21小跑堂
基于STM32F103实现USB Joystick并介绍USB HID协议在STM32 HAL库中实现
自序
由于作者水平有限,文档和视频中难免有出错和讲得不好的地方,欢迎各位读者和观众善意地提出意见和建议,谢谢!
第一部分、硬件概述1.1 实物概图
图1.1Gamepad实物概图
如图1.1所示Gamepad评估板配置了8个6*6轻触按键,一个摇杆(Joystick),搭载一颗WS2812B灯珠,并将UART1串口,编程接口(SWD),外接Joystick接口,microUSB接口引出;
1.2 Gamepad原理图Gamepad原理图如图1.2所示,如看不清可打开Doc目录下的PDF文档查阅
第二部分、软件工具2.1 软件概述 在 /Software 目录下是常用的工具软件: 第三部分、实战训练3.1 实例Eg1_Joystick目标是实现 Joystick:枚举成XY轴的平面坐标和8个按键的USB HID。 3.1.1硬件设计 图1.3 Joystick原理图
其中VRX1与VRY1是摇杆的电位器输出的电压信号(ADC检测);SW1则是按键,右侧H1是外接的Joystick口备用;
图1.4 KEY原理图
如图1.4是KEY原理图,我们只要配置8个GPIO作为输入去检测按键信号; 3.1.2 软件设计USB设备开发需要具备一定的USB设备开发知识;关于Usb的学习,这里推荐两个学习视频和一个学习网站: USB技术应用与开发:
CherryUSB设备协议栈教程:
USB中文网: https://www.usbzh.com/ 我们主要做USB HID开发,一般我们需要了解一些标准请求,还有HID类的请求;其中标准请求主要是主机获取设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符的过程,如果是HID,还有HID描述符的过程 ,以及报表描述符的过程; 一般的,我们配套的视频都有讲解USB设备枚举过程在代码中的实现,这里主要是基于STM32 HAL库的; 首先是初始化代码,我们通过STM32cubeMX软件去生成代码,具体配置请打开GamePad.ioc查阅,配套视频也有关键部分的讲解,这里不再赘述; 我们的工程使用的是Keil-MDK编译器,生成的工程目录如图1.5
图1.5 工程目录其中
Application/MDK-ARM 存放的是启动代码; Application/User/Core: main函数,中断Handler,MSP相关代码; Application/User/USB_DEVICE/App:* USB设备应用代码; Application/User/USB_DEVICE/Target: USB设备配置代码; Drivers/STM32F1xx_HAL_Driver: HAL库驱动代码 Drivers/CMSIS: CMSIS相关代码 Middlewares/USB_Device_Library/ USB设备库代码,对应cubemx Middleware; Customer: 这是我们自定义的代码; Doc: 存放说明文本文档;
工程目录这里只做一次介绍,后面的样例目录大同小异。 以下USB部分内容请大家务必多看几遍代码, 我们打开工程中的main函数可以看到
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_USB_DEVICE_Init();
MX_USART1_UART_Init();
Gp_ADC_Start_DMA();//启动ADC DMA并开启中断
while (1)
{
Gp_SendReport();
}
}
其中只有Gp_ADC_Start_DMA和Gp_SendReport是我们自定义的代码;其他均是STM32CUBEMX生成;由于篇幅的原因,我们主要介绍MX_USB_DEVICE_Init;
void MX_USB_DEVICE_Init(void)
{
/* Init Device Library, add supported class and start the library. */
if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
{
Error_Handler();
}
if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_HID) != USBD_OK)
{
Error_Handler();
}
if (USBD_CUSTOM_HID_RegisterInterface(&hUsbDeviceFS, &USBD_CustomHID_fops_FS) != USBD_OK)
{
Error_Handler();
}
if (USBD_Start(&hUsbDeviceFS) != USBD_OK)
{
Error_Handler();
}
}
其中USBD_Init函数中的FS_Desc数据结构里面是获取UBS设备描述符USBD_FS_DeviceDescriptor,语言ID字符串描述符USBD_FS_LangIDStrDescriptor,厂商字符串描述符USBD_FS_ManufacturerStrDescriptor,产品字符串描述符USBD_FS_ProductStrDescriptor,序列号字符串描述符USBD_FS_SerialStrDescriptor,配置字符串描述符USBD_FS_ConfigStrDescriptor,接口描述符USBD_FS_InterfaceStrDescriptor的函数指针;
上述描述符中,UBS设备描述符是必需的,其他都是字符串描述符,可选的;我们不妨打开usb描述符获取函数与usb描述符报表。
关于USB设备描述符的介绍,注释已经非常清楚;不了解的地方可以学习一下前面的推荐两个学习视频和一个学习网站; 设备描述符找到了,我们还需要的配置描述符,接口描述符,HID描述符(如果是HID设备),端点描述符; 我们继续回到MX_USB_DEVICE_Init,可以看到USBD_RegisterClass中的USBD_CUSTOM_HID,其中USBD_CUSTOM_HID_Setup是处理USB主机的一些请求过程,包括标准请求等;而USBD_CUSTOM_HID_GetHSCfgDesc(高速)和USBD_CUSTOM_HID_GetFSCfgDesc(全速)都是配置描述符;
USBD_ClassTypeDef USBD_CUSTOM_HID =
{
USBD_CUSTOM_HID_Init,
USBD_CUSTOM_HID_DeInit,
USBD_CUSTOM_HID_Setup,
NULL, /*EP0_TxSent*/
USBD_CUSTOM_HID_EP0_RxReady, /*EP0_RxReady*/ /* STATUS STAGE IN */
USBD_CUSTOM_HID_DataIn, /*DataIn*/
USBD_CUSTOM_HID_DataOut,
NULL, /*SOF */
NULL,
NULL,
USBD_CUSTOM_HID_GetHSCfgDesc,
USBD_CUSTOM_HID_GetFSCfgDesc,
USBD_CUSTOM_HID_GetOtherSpeedCfgDesc,
USBD_CUSTOM_HID_GetDeviceQualifierDesc,
};
由于我们的MCU是支持全速的,所以这里应该是USBD_CUSTOM_HID_GetFSCfgDesc,/* USB CUSTOM_HID device FS Configuration Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgFSDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
0x09, /* bLength: Configuration Descriptor size */
USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
USB_CUSTOM_HID_CONFIG_DESC_SIZ,
/* wTotalLength: Bytes returned */
0x00,
0x01, /*bNumInterfaces: 1 interface*/
0x01, /*bConfigurationValue: Configuration value*/
0x00, /*iConfiguration: Index of string descriptor describing
the configuration*/
0xC0, /*bmAttributes: bus powered */
0x32, /*MaxPower 100 mA: this current is used for detecting Vbus*/
/************** Descriptor of CUSTOM HID interface ****************/
/* 09 */
0x09, /*bLength: Interface Descriptor size*/
USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type*/
0x00, /*bInterfaceNumber: Number of Interface*/
0x00, /*bAlternateSetting: Alternate setting*/
0x02, /*bNumEndpoints*/
0x03, /*bInterfaceClass: CUSTOM_HID*/
0x00, /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
0x00, /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
0, /*iInterface: Index of string descriptor*/
/******************** Descriptor of CUSTOM_HID *************************/
/* 18 */
0x09, /*bLength: CUSTOM_HID Descriptor size*/
CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
0x11, /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
0x01,
0x00, /*bCountryCode: Hardware target country*/
0x01, /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
0x22, /*bDescriptorType*/
USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
0x00,
/******************** Descriptor of Custom HID endpoints ********************/
/* 27 */
0x07, /*bLength: Endpoint Descriptor size*/
USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/
CUSTOM_HID_EPIN_ADDR, /*bEndpointAddress: Endpoint Address (IN)*/
0x03, /*bmAttributes: Interrupt endpoint*/
CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 2 Byte max */
0x00,
CUSTOM_HID_FS_BINTERVAL, /*bInterval: Polling Interval */
/* 34 */
0x07, /* bLength: Endpoint Descriptor size */
USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
CUSTOM_HID_EPOUT_ADDR, /*bEndpointAddress: Endpoint Address (OUT)*/
0x03, /* bmAttributes: Interrupt endpoint */
CUSTOM_HID_EPOUT_SIZE, /* wMaxPacketSize: 2 Bytes max */
0x00,
CUSTOM_HID_FS_BINTERVAL, /* bInterval: Polling Interval */
/* 41 */
};
static uint8_t *USBD_CUSTOM_HID_GetFSCfgDesc(uint16_t *length)
{
*length = sizeof(USBD_CUSTOM_HID_CfgFSDesc);
return USBD_CUSTOM_HID_CfgFSDesc;
}
细心的同学可以发现,其实这个函数是获取了配置描述符,接口描述符,HID描述符,端点描述符;不过一般的,主机一般都是先请求配置描述符,然后通过配置描述符就知道了整个描述符集合的大小USB_CUSTOM_HID_CONFIG_DESC_SIZ; 现在配置描述符,接口描述符,HID描述符,端点描述符都有了,因为我们是HID设备,故而还需要报表描述符;还是回到MX_USB_DEVICE_Init,在USBD_CUSTOM_HID_RegisterInterface这个函数中的USBD_CustomHID_fops_FS结构体中的第一个成员,真是众里寻他千百度,蓦然回首,那报表描述符却在灯火阑珊处。 /** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x04, // USAGE (Joystick)
0xa1, 0x01, // COLLECTION (Application)
0xa1, 0x02, // COLLECTION (Logical)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255)
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x46, 0xff, 0x00, // PHYSICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x08, // USAGE_MAXIMUM (Button 8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x08, // REPORT_COUNT (8)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0xc0, // END_COLLECTION
0xC0 //END_COLLECTION */
};
USBD_CUSTOM_HID_ItfTypeDef USBD_CustomHID_fops_FS =
{
CUSTOM_HID_ReportDesc_FS,
CUSTOM_HID_Init_FS,
CUSTOM_HID_DeInit_FS,
CUSTOM_HID_OutEvent_FS
};
另外需要注意USBD_CUSTOM_HID_REPORT_DESC_SIZE, 这个宏是报告描述符实际数组大小,大小不对会导致枚举失败; "#define USBD_CUSTOM_HID_REPORT_DESC_SIZE 46" 以上报表描述符通过生成工具Dt2_4配置生成报告描述符,可以看出X,Y轴定义成无符号8位数,XY的描述占用2个字;按键一共有8个,每个大小描述是bit,8个bit即1个byte;因此XY坐标+8个按键=3个byte;我们需要上报3个byte的数据给主机(HOST)。 最后是应用程序的编写,在main函数中调用Gp_ADC_Start_DMA和Gp_SendReport,其中Gp_ADC_Start_DMA是启动ADC DMA 中断完成采样的,而Gp_SendReport则是解析ADC采样后的数据并上报; 我们是通过Gp_ADC_Start_DMA调用HAL_ADC_Start_DMA启动ADC DMA模式采样,需要传入hadc1句柄,AD_DATA数据缓存,AD_DATA_SIZE缓存大小,其中偶数索引是ADC通道0的采样数据,如AD_DATA[0],AD_DATA[2],反之则是通道2的数据;HAL_ADC_Start_IT是启动ADC1的全局中断,HAL_ADC_ConvCpltCallback是采样完成的中断回调函数,在stm32f1xx_it.c的ADC1_2_IRQHandler里面可遍历找到。 //启动ADC DMA
void Gp_ADC_Start_DMA(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&AD_DATA, AD_DATA_SIZE);
HAL_ADC_Start_IT(&hadc1);
}
/** ADC ISR
* Handles the values from ADC after the conversion finished
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if( adcValueReady == 0 ) {
for(i=0; i<AD_DATA_SIZE;)
{
AdXSum += AD_DATA[i];
i++;
AdYSum += AD_DATA[i];
i++;
}
adcValueReady = 1;
}
}
再来看我们的数据处理上报函数 //按键扫描
u8 key_scan(void)
{
key=0;
if((UPKEY)==0)
{
key|=BIT0;
}else{
key&=(~BIT0);
}
if((LFKEY)==0)
{
key|=BIT1;
}else{
key&=(~BIT1);
}
if((RGKEY)==0)
{
key|=BIT2;
}else{
key&=(~BIT2);
}
if((DNKEY)==0)
{
key|=BIT3;
}else{
key&=(~BIT3);
}
if((TBKEY)==0)
{
key|=BIT4;
}else{
key&=(~BIT4);
}
if((BKKEY)==0)
{
key|=BIT5;
}else{
key&=(~BIT5);
}
if((MDKEY)==0)
{
key|=BIT6;
}else{
key&=(~BIT6);
}
if((STKEY)==0)
{
key|=BIT7;
}else{
key&=(~BIT7);
}
return key;
}
/* Re-maps a number from one range to another
*
*/
int32_t map(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max){
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
//处理并上报数据
void Gp_SendReport(void)
{
u8 X=0,Y=0,Ktemp=0;
if( adcValueReady == 1 )
{
Xtemp=AdXSum/10;
AdXSum=0;
Ytemp=AdYSum/10;
AdYSum=0;
if(Xtemp>Xmax)
Xtemp=Xmax;
if(Xtemp<Xmin)
Xtemp=Xmin;
if(Ytemp>Ymax)
Ytemp=Ymax;
if(Ytemp<Ymin)
Ytemp=Ymin;
adcValueReady=0;
}
X=(uint8_t)map( Xtemp, Xmin, Xmax, 0, UINT8_MAX );
Y=(uint8_t)map( Ytemp, Ymin, Ymax, 0, UINT8_MAX );
Ktemp=key_scan();
Joystick_Report[0]=Y;
Joystick_Report[1]=X;
Joystick_Report[2]=Ktemp;
USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS,(u8*)&Joystick_Report, 3);
HAL_Delay(5);
}
key_scan是扫描按键,map是转换数据的范围,因为我们的ADC采样是12bit,取值0~4095,需要转成8bit,0~255; 最后通过USBD_CUSTOM_HID_SendReport上报数据给主机。
3.1.3 下载验证我们把固件程序下载进去可以,打开“设备与打印机”可以看到USB设备枚举成了一个Gamepad,如下图。
图1.5 Gamepad设备 右键打开游戏控制器后,点击属性得到下图所示界面
图1.6 游戏控制器 我们可以摇Joystick和按按键可以发现上图游戏控制器界面也跟着响应。 3.1.4 入门视频本期的入门视频如下:
|