(请保留-> 作者: 罗冰 https://blog.csdn.net/luobing4365)
在介绍完所有背景知识后,终于可以进入实质的嵌入式编程了。
本篇拟使用YIE002开发板,制作一个USB HID设备,支持三种通信方式,以对应UEFI开发探索73和74所讨论的三种上位机通信。
这个系列的博客,主要还是偏向UEFI编程的探索,对于嵌入式的编程,不想讨论过多。对于YIE002的嵌入式开发,请移步我另外一个专栏“嵌入式开发”,在其中我开了一个新坑,用来探索YIE002的编程。
这款开发板的主芯片是STM32F103C8T6,是我出差最常带的版型。本篇所写的代码,适用于所有F1系列作为主芯片的开发板,比如正点原子的战舰开发板。
从意法的官方资料可知,STM32 MCU有如下USB IP:
官方也提供了不同的USB库,以适应开发需求。比如针对USB IP和USB+ IP,提供的库如图1所示。
图1 USB(+) IP对应的USB库
因此,对F103系的MCU而言,可以选择Legacy library进行开发,也可以使用Cube library进行开发。
本篇的开发,是基于Legacy library的示例工程Custom_HID改造的。使用Cube library开发的方式,可以博客中参考嵌入式开发的专栏。
我平常开发嵌入式产品,主要使用的是MDK Keil工具。Legacy library(STSW-STM32121)中提供的示例工程,支持各种编译工具。当然,也带来很多我不需要的冗余代码。另外,由于共用了外围设备库和评估板的各种文件,Legacy library组织了很好的代码结构,但对我而言是累赘,总不能每次都在库的文件夹下进行修改吧。
因此,我对示例工程Custom_HID进行了调整,删除了很多不需要的代码,重新组织了代码结构。
如图2所示,是调整过的代码结构。
图2 调整过的代码结构
另一个需要注意的地方,是头文件的路径,如图3所示。
图3 头文件包含路径
另外,在选择设备的时候,选择STM32F103C8,编译针对YIE002-STM32型开发板的生成文件。
其他的细节,请以本篇博客提供的工程,对照原始的Custom_HID工程,即可了解。
修改代码的过程主要包括:
详细过程如下。
描述符主要在文件usb_desc.c中修改。配置描述符比较简单,主要将厂商ID和产品ID修改为自己需要的值。主要的修改在配置描述符中,需要注意的是,端点描述符和接口描述符与配置描述符在同一数组内。
示例1 配置描述符
const uint8_t CustomHID_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] = { ……//前略 /******************** Descriptor of Custom HID endpoints ******************/ /* 27 */ 0x07, /* bLength: Endpoint Descriptor size */ USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */ 0x81, /* bEndpointAddress: Endpoint Address (IN) */ 0x03, /* bmAttributes: Interrupt endpoint */ 0x40, /* wMaxPacketSize: 64 Bytes max */ 0x00, 0x20, /* bInterval: Polling Interval (32 ms) */ /* 34 */ 0x07, /* bLength: Endpoint Descriptor size */ USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */ /* Endpoint descriptor type */ 0x01, /* bEndpointAddress: */ /* Endpoint Address (OUT) */ 0x03, /* bmAttributes: Interrupt endpoint */ 0x40, /* wMaxPacketSize: 64 Bytes max */ 0x00, 0x20, /* bInterval: Polling Interval (20 ms) */ }
从示例1可以看出,通信用的端点号为1,可以输入输出。包括缺省的端点0在内,使用了两个端点号,因此在接口描述符中,bNumEndpoints必须为2(示例1中没有列出)。
而报表描述符,设置了16字节的通道,如示例2所示。
示例2 报表描述符
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x00, // USAGE (0) 0xa1, 0x01, // COLLECTION (Application) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0xff, // LOGICAL_MAXIMUM (255) 0x19, 0x01, // USAGE_MINIMUM (1) 0x29, 0x10, // USAGE_MAXIMUM (16) 0x95, 0x10, // REPORT_COUNT (16) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x19, 0x01, // USAGE_MINIMUM (1) 0x29, 0x10, // USAGE_MAXIMUM (16) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x19, 0x01, // USAGE_MINIMUM (1) 0x29, 0x10, // USAGE_MAXIMUM (16) 0xB1, 0x02, // Feature(Data, Variable, Absolute) 0xc0 // END_COLLECTION }; /* CustomHID_ReportDescriptor */
从中可以看出,构建的通道中,Input报告、Output报告和Feature报告均为16字节长。
在源文件usb_prop.c中,修改端点的通信能力:
SetEPTxCount(ENDP1, 0x40); //robin: 修改为64字节大小 SetEPRxCount(ENDP1, 0x40); //robin: 修改为64字节大小
修改通信函数,在源文件usb_endp.c中进行修改,代码如下:
uint8_t Receive_Buffer[0xff]; void EP1_OUT_Callback(void) { // BitAction Led_State; uint32_t DataLength = 0; /* Read received data (2 bytes) */ DataLength=USB_SIL_Read(EP1_OUT, Receive_Buffer); //读取端点得到的数据 SetEPRxStatus(ENDP1, EP_RX_VALID); if (Receive_Buffer[0] == 0xA0)//将第二个字节改为1返回,表示是采用端点发送的方式 { Receive_Buffer[1]=0x1; } USB_SIL_Write(EP1_IN,Receive_Buffer,DataLength); SetEPTxStatus(ENDP1,EP_TX_VALID); }
其他代码不用动,并将usb_prop.c下的 void CustomHID_Status_In(void)函数中的内容全部注释掉。
至此,就完成了这种通信方式的编写。
为了支持这两种通信方式,需要实现Set_Report和Get_Report类命令。这两个类命令,有三种报告使用,包括Input报告、Output报告和Feature报告。其中,Input报告和Output报告作为输入输出通信,对应上层的函数为HidD_GetInputReport()和HidD_SetOutputReport();Feature报告对应上层的函数为HidD_GetFeature()和HidD_SetFeature()。
这三种报告都是使用Set_Report 和Get_Report命令来传输数据的,只是通过类命令中的wValue来区分报告类型:1为Input报告、2为Output报告、3为Feature报告。
需要注意的是,USB通信中,都是站在主机的角度来看通信过程的,所有命令也都是由主机发起的。Set_Report对设备而言,是接收数据;Get_Report对设备而言,是发送数据。
修改过程如下:
uint8_t Report_Buf[16]; //Robin: 报告长度为16,见报告描述符 uint8_t Report_InOut_Flag=0; //Robin: Input报告和Output报告标志 uint8_t Report_Feature_Flag=0;//Robin: Feature报告标志
...... /*** GET_PROTOCOL, GET_REPORT, SET_REPORT ***/ else if ( (Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT)) ) { switch( RequestNo ) { case GET_PROTOCOL: CopyRoutine = CustomHID_GetProtocolValue; break; case SET_REPORT: CopyRoutine = CustomHID_SetReport_Feature; Request = SET_REPORT; break; //robin add for get_report case GET_REPORT: if((Report_InOut_Flag==0)&&(Report_Feature_Flag==0)) return USB_NOT_READY; //Robin: Inform the host that the data is not ready CopyRoutine = CustomHID_GetReport_Feature; Request = GET_REPORT; break; default: break; } } if (CopyRoutine == NULL) { return USB_UNSUPPORT; } pInformation->Ctrl_Info.CopyData = CopyRoutine; pInformation->Ctrl_Info.Usb_wOffset = 0; (*CopyRoutine)(0); return USB_SUCCESS; }
也即增加了Get_Report和Set_Report的处理函数。
/******************************************************************************* * Function Name : CustomHID_SetReport_Feature * Description : Set Feature request handling * Input : Length. * Output : None. * Return : Buffer *******************************************************************************/ uint8_t *CustomHID_SetReport_Feature(uint16_t Length) { if(pInformation->USBwValues.bw.bb1 == OUT_REPORT) Report_InOut_Flag=1; else if(pInformation->USBwValues.bw.bb1 == FEATURE_REPORT) Report_Feature_Flag=1; if (Length == 0) { pInformation->Ctrl_Info.Usb_wLength = 16;//2; //robin return NULL; } else { // return Report_Buf; return &Report_Buf[pInformation->Ctrl_Info.Usb_wOffset]; } } /************************************************************************* * Function Name : CustomHID_GetReport_Feature * Description : Set Feature request handling * Input : Length. * Output : None. * Return : Buffer *************************************************************************/ uint8_t *CustomHID_GetReport_Feature(uint16_t Length) { if(pInformation->USBwValues.bw.bb1 == IN_REPORT) Report_InOut_Flag=0; else if(pInformation->USBwValues.bw.bb1 == FEATURE_REPORT) Report_Feature_Flag=0; if (Length == 0) //此处报告需要发送的长度 { pInformation->Ctrl_Info.Usb_wLength = 16;//2; //robin return NULL; // return (uint8_t *)16; } else //此处返回需要处理的数据 { if(pInformation->USBwValues.bw.bb1 == FEATURE_REPORT) { if(Report_Buf[0] == 0xA0) Report_Buf[1]=0x3; } else { if(Report_Buf[0] == 0xA0) Report_Buf[1]=0x2; } return Report_Buf; } }
从中很容易看出,针对不同的报告,函数进行了不同的处理。为了方便上位机测试,简单地将收到的数据,进行了局部修改,然后返回。
将固件代码编译,下载到YIE002开发板中,使用上位机工具进行测试,如图4所示。
图4 使用UsbHID工具测试HID设备
根据HID设备的固件代码,不同的通信方式,在第一个字节为0xA0的时候,返回的第二个字节分别为0x1、0x2和0x3。从图3中可以看出,是按照代码设定的逻辑在正常工作的。
至此,我们完成了USB HID设备的编写。下一篇开始,将在UEFI环境下对HID设备进行访问。
Gitee地址:https://gitee.com/luobing4365/uefi-exolorer
项目代码位于:/84 YIE2HID下