如何建立一个自定义的HID工程呢?下面就来讲讲。
首先先介绍下工程的架构,工程的总体架构下图所示,按照下图架构建工程:
分析下工程布局,首先是APP,这个组里存放着主文件mian.c,管理所有中断服务程序stm3210x_it.c,及其管理外设库头文件的stm32f10x_conf.h。BSP这个组里存放着BSP.c,外设的洗衣初始化都在这个函数中定义,比如说串口的配置,LED灯的配置,系统时钟的配置,各类NVIC的中断配置。在这个文件中,会定义一个BSP_Init()函数,所有配置的都在这个函数中调用,例如:
void BSP_Init(void)
{
RCC_Configuration();
Set_USBClock();
USB_Init();
USART1_Configuration(115200);
LED_Configuration();
NVIC_Configuration();
USB_Interrupts_Config();
}
而这个BSP_Init()函数在main中调用,这样就使主函数简洁漂亮了。至于CMSIS这个组则是关于Cotex-M3内核的相关文件看,如core_cm3.c和system_stm32f10x.c。StartUp这个组放置系统的启动文件,不同系类的处理器使用不同的启动文件,这里有必要了解:
- startup_stm32f10x_ld_vl.s: for STM32 Low density Value line devices
- startup_stm32f10x_ld.s: for STM32 Low density devices
- startup_stm32f10x_md_vl.s: for STM32 Medium density Value line devices
- startup_stm32f10x_md.s: for STM32 Medium density devices
- startup_stm32f10x_hd.s: for STM32 High density devices
- startup_stm32f10x_xl.s: for STM32 XL density devices
- startup_stm32f10x_cl.s: for STM32 Connectivity line devices
cl:互联型产品,stm32f105/107系列
vl:超值型产品,stm32f100系列
xl:超高密度产品,stm32f101/103系列
ld:低密度产品,FLASH小于64K
md:中等密度产品,FLASH=64 or 128
hd:高密度产品,FLASH大于128
我的这个工程选择高密度型的:
startup_stm32f10x_hd.s。USB_User文件组放着USB控制与应用相关的文件,在之前的文章每个文件都详细介绍过。接着是USB-FS-Device_Driver这个组放着USB的驱动,在之前的文章页已经讲述过。最后一个组是STM32F10x_StdPeriph_Driver,它里面存放着外设库文件的驱动代码,很多人为了省事,会把所有的C文件都添加进来,我不建议这么做,还是根据需要添加对应的文件,就拿我们的这个CustomHID工程,我们用到了引脚GPIO、时钟的配置,串口的配置,所以只要添加这几个对应的C库文件就可以了。
上面的各个文件大部分可以网上下载的。
接下去就讲述如何实现CustomHID功能的。
首先,最重要的文件当然是usb_desc.c这个文件了。这个文件存放着各种描述符,比如说设备描述符、配置描述符等,下面就一一介绍。
设备描述符符的定义如下:
/* USB标准设备描述符*/
const uint8_t CustomHID_DeviceDescriptor[CUSTOMHID_SIZ_DEVICE_DESC] =
{
0x12, /*bLength:长度,设备描述符的长度为18字节*/
USB_DEVICE_DESCRIPTOR_TYPE, /*bDescriptorType:类型,设备描述符的编号是0x01*/
0x00, /*bcdUSB:所使用的USB版本为2.0*/
0x02,
0x00, /*bDeviceClass:设备所使用的类代码*/
0x00, /*bDeviceSubClass:设备所使用的子类代码*/
0x00, /*bDeviceProtocol:设备所使用的协议*/
0x40, /*bMaxPacketSize:最大包长度为64字节*/
0x34, /*idVendor:厂商ID为0x1234*/
0x12,
0x10, /*idProduct:产品ID为0x1010*/
0x10,
0x00, /*bcdDevice:设备的版本号为2.00*/
0x02,
1, /*iManufacturer:厂商字符串的索引*/
2, /*iProduct:产品字符串的索引*/
3, /*iSerialNumber:设备的序列号字符串索引*/
0x01 /*bNumConfiguration:设备有1种配置*/
}; /* CustomHID设备描述符 */
设备描述符的数组的长度一般为9个字节,该描述符定义了USB协议代号、厂商ID(VID),产品ID(PID)、设备的版本号、以及厂商产品序列号描述符的索引。在USB枚举阶段,USB设备需要通过端口0向USB主机发送设备描述符。
配置描述符集合里有着丰富的USB设备的信息,如用了几个接口,用了几个端点,USB设备做什么用等。代码如下:
/* USB配置描述符集合(配置、接口、端点、类、厂商)(Configuration, Interface, Endpoint, Class, Vendor */
const uint8_t CustomHID_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] =
{
0x09, /*bLength:长度,设备字符串的长度为9字节*/
USB_CONFIGURATION_DESCRIPTOR_TYPE, /*bDescriptorType:类型,配置描述符的类型编号为0x2*/
CUSTOMHID_SIZ_CONFIG_DESC, /*wTotalLength:配置描述符的总长度为41字节*/
0x00,
0x01, /*bNumInterfaces:配置所支持的接口数量1个*/
0x01, /*bConfigurationValue:该配置的值*/
0x00, /*iConfiguration:该配置的字符串的索引值,该值为0表示没有字符串*/
0xC0, /* bmAttributes:设备的一些特性,0xc0表示自供电,不支持远程唤醒
D7:保留必须为1,D6:是否自供电,D5:是否支持远程唤醒,D4~D0:保留设置为0*/
// 0x32, /*从总线上获得的最大电流为100mA */
0x96, /*MaxPower:设备需要从总线上获取多少电流,单位为2mA,0x96表示300mA*/
/************** HID接口描述符****************/
0x09, /*bLength:长度,接口描述符的长度为9字节 */
USB_INTERFACE_DESCRIPTOR_TYPE,/* bDescriptorType:接口描述符的类型为0x4 */
0x00, /*bInterfaceNumber:该接口的编号*/
0x00, /*bAlternateSetting:该接口的备用编号 */
0x02, /*bNumEndpoints:该接口所使用的端点数*/
0x03, /*bInterfaceClass该接口所使用的类为HID*/
0x00, /*bInterfaceSubClass:该接口所用的子类 1=BOOT, 0=no boot */
0x00, /*nInterfaceProtocol :该接口使用的协议0=none, 1=keyboard, 2=mouse */
0, /*iInterface: 该接口字符串的索引 */
/*****************HID描述符 ********************/
0x09, /*bLength: HID描述符的长度为9字节 */
HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID的描述符类型为0x21 */
0x10, /*bcdHID: HID协议的版本为1.1 */
0x01,
0x00, /*bCountryCode: 国家代号 */
0x01, /*bNumDescriptors: 下级描述符的数量*/
0x22, /*bDescriptorType:下级描述符的类型*/
CUSTOMHID_SIZ_REPORT_DESC,/* wItemLength: 下一集描述符的长度*/
0x00,
/********************输入端点描述符******************/
0x07, /* bLength: 端点描述符的长度为7字节*/
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: 端点描述符的类型为0x21*/
0x82, /* bEndpointAddress: 该端点(输入)的地址,D7:0(OUT),1(IN),D6~D4:保留,D3~D0:端点号*/
0x03, /* bmAttributes: 端点的属性为为中断端点.
D0~D1表示传输类型:0(控制传输),1(等时传输),2(批量传输),3(中断传输)
非等时传输端点:D2~D7:保留为0
等时传输端点:
D2~D3表示同步的类型:0(无同步),1(异步),2(适配),3(同步)
D4~D5表示用途:0(数据端点),1(反馈端点),2(暗含反馈的数据端点),3(保留),D6~D7:保留,*/
0x40, /* wMaxPacketSize: 该端点支持的最大包长度为64字节*/
0x00,
0x02, /* bInterval: 轮询间隔(2 ms) */
/********************输出端点描述符******************/
0x07, /* 端点描述符的长度为7字节 */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: 端点描述符的类型为0x21*/
0x01, /* bEndpointAddress: 该端点(输出)的地址,D7:0(OUT),1(IN),D6~D4:保留,D3~D0:端点号*/
0x03, /* bmAttributes: 端点的属性为为中断端点 */
0x40, /* wMaxPacketSize: 该端点支持的最大包长度为64字节 */
0x00,
0x02, /* bInterval: 轮询间隔(2 ms) */
};
从上面的代码中可以看出,USB设备使用了1个接口、两个端点:一个中断传输输入端点,端点号为2;一个中断传输的输出端点,端点号为1、每个端点能通讯的最大数据包长度为64字节、USB的功能自定义等。配置描述符是在USB主机发送GET_CONFIGURATION请求时,USB设备发送的。
还有一个很重要的当然报告描述符了:
/* HID的报告描述符*/
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
{
/*short Item D7~D4:bTag;D3~D2:bType;D1~D0:bSize
**bTag —主条目 1000:输入(Input) 1001:输出(Output) 1011:特性(Feature) 1010:集合(Collection) 1100:关集合(End Collection)
** 全局条目 0000:用途页(Usage Page) 0001:逻辑最小值(Logical Minimum) 0010:逻辑最大值(Logical Maximum) 0011:物理最小值(Physical Minimum)
** 0100:物理最大值(Physical Maximum) 0101:单元指数(Unit Exponet) 0110:单元(Unit) 0111:数据域大小(Report Size)
** 1000:报告ID(Report ID) 1001:数据域数量(Report Count) 1010:压栈(Push) 1011:出栈(Pop) 1100~1111:保留(Reserved)
** 局部条目 0000:用途(Usage) 0001:用途最小值(Usage Minimum) 0010:用途最大值(Usage Maximum) 0011:标识符索引(Designator Index)
** 0100:标识符最小值(Designator Minimum) 0101:标识符最大值(Designator Maximum) 0111:字符串索引(String Index) 1000:字符串最小值(String Minimum)
** 1001:字符串最大值(String Maximum) 1010:分隔符(Delimiter) 其他:保留(Reserved)
**bType—00:主条目(main) 01:全局条目(globle) 10:局部条目(local) 11:保留(reserved)
**bSize—00:0字节 01:1字节 10:2字节 11:4字节*/
//0x05:0000 01 01 这是个全局条目,用途页为ST页
0x05, 0x8c, /* USAGE_PAGE (ST Page) */
//0x09:0000 10 01 这是个局部变量,用途为Demo Kit
0x09, 0x01, /* USAGE (Demo Kit) */
//0xa1:1010 00 01 这是一个主条目,集合为应用集合
0xa1, 0x01, /* COLLECTION (Application) */
/* 输入报告*/
//0x09:0000 10 01 这是个局部条目,用途为厂商ID
0x09,0x03, // USAGE ID - Vendor defined
//0x15:0001 01 01 这是个全局条目,逻辑最小值为0
0x15,0x00, // LOGICAL_MINIMUM (0)
//0x26:0010 01 10 这是个全局条目,逻辑最大值为255
0x26,0x00, 0xFF, // LOGICAL_MAXIMUM (255)
//0x75:0111 01 01 这是个全局条目,报告大小为8位
0x75,0x08, // REPORT_SIZE (8bit)
//0x95:1001 01 01 这是个全局条目,报告数量为64
0x95,0x40, // REPORT_COUNT (64Byte)
//0x81:1000 00 01 这是个主条目,做输入,Data表示这些数据可变,Var表示这些徐居于是独立的变量,Abs表示绝对值
0x81,0x02, // INPUT (Data,Var,Abs)
/*输出报告*/
//0x09:0000 10 01 这是个局部条目,用途为厂商ID
0x09,0x04, // USAGE ID - Vendor defined
//0x15:0001 01 01 这是个全局条目,逻辑最小值为0
0x15,0x00, // LOGICAL_MINIMUM (0)
//0x26:0010 01 10 这是个全局条目,逻辑最大值为255
0x26,0x00,0xFF, // LOGICAL_MAXIMUM (255)
//0x75:0111 01 01 这是个全局条目,报告大小为8位
0x75,0x08, // REPORT_SIZE (8bit)
//0x95:1001 01 01 这是个全局条目,报告数量为64
0x95,0x40, // REPORT_COUNT (64Byte)
//0x91:1001 00 01 这是个全局条目,做输出,Data表示这些数据可变,Var表示这些徐居于是独立的变量,Abs表示绝对值
0x91,0x02, // OUTPUT (Data,Var,Abs)
0xc0 /* END_COLLECTION */
};
关于配置描述符也是至关重要的,它规定了USB通讯的长度,具体格式。据上面的报告描述符说:定义了64*8bit的数据域作为输入,属性是Data、Var、Abs,也就是说USB设备想USB主机每次发送64字节的数据包,每个数据的值(0~255之间)可以用户自定义;还定义了64*8bit的数据域作为输出,属性是Data、Var、Abs,也就是说USB主机箱USB设备每次发送64字节的数据包,每个数据的值(0~255)由USB主机自己定义。
接下的一些说明描述符代码如下,就不详细介绍了:
/* 语言ID描述符 */
const uint8_t CustomHID_StringLangID[CUSTOMHID_SIZ_STRING_LANGID] =
{
CUSTOMHID_SIZ_STRING_LANGID, /*bLength:本描述符的长度为4字节*/
USB_STRING_DESCRIPTOR_TYPE, /*bDescriptorType:字符串描述符的类型为0x03*/
0x09, /*bString:语言ID为0x0409,表示美式英语*/
0x04
}; /* LangID = 0x0409: U.S. English*/
/*厂商字符串描述符*/
const uint8_t CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR] =
{
CUSTOMHID_SIZ_STRING_VENDOR, /*bLength:厂商字符串描述符的长度*/
USB_STRING_DESCRIPTOR_TYPE, /*bDescriptorType:字符串描述符的类型为0x03*/
‘M’, 0, ‘y’, 0, ‘U’, 0,‘S’, 0,‘B’, 0, ‘_’, 0, ‘H’, 0,‘I’,0,‘D’,0 /*自定义*/
};
/*产品的字符串描述符*/
const uint8_t CustomHID_StringProduct[CUSTOMHID_SIZ_STRING_PRODUCT] =
{
CUSTOMHID_SIZ_STRING_PRODUCT, /* bLength:产品的字符串描述符*/
USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType:字符串描述符的类型为0x03*/
‘B’, 0, ‘y’, 0, ’ ‘, 0, ‘v’, 0, ‘i’, 0, ‘e’, 0,‘w’,0,‘t’,0,‘o’,0,‘o’,0,‘l’,0/*自定义*/
};
/*产品序列号的字符串描述符*/
uint8_t CustomHID_StringSerial[CUSTOMHID_SIZ_STRING_SERIAL] =
{
CUSTOMHID_SIZ_STRING_SERIAL, /* bLength:产品序列号*/
USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType:字符串描述符的类型为0x03*/
‘x’, 0, ‘x’, 0, ‘x’, 0,‘x’, 0,‘x’, 0, ‘x’, 0, ‘x’, 0 /*自定义*/
};
接下去需要改动的的是usb_prop.c这个文件里的内容。这个文件大部分不需要膝盖,只要修改下CustomHID_Reset()这个函数(名字不定相同)。这个函数的定义如下:
/*******************************************************************************
* Function Name : CustomHID_Reset.
* Description : CustomHID Mouse reset routine.复位
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void CustomHID_Reset(void)
{
/* Set CustomHID_DEVICE as not configured */
pInformation->Current_Configuration = 0; //设置当前的配置为0,表示没有配置过
pInformation->Current_Interface = 0;//默认的接口
/* Current Feature initialization */
pInformation->Current_Feature = CustomHID_ConfigDescriptor[7];//当前的属性,bmAttributes:设备的一些特性,0xc0表示自供电,不支持远程唤醒
#ifdef STM32F10X_CL
/* EP0 is already configured in DFU_Init() by USB_SIL_Init() function */
/* Init EP1 IN snd EP1 OUT as Interrupt endpoint */
OTG_DEV_EP_Init(EP1_IN, OTG_DEV_EP_TYPE_INT, EP1_SIZE);
OTG_DEV_EP_Init(EP1_OUT, OTG_DEV_EP_TYPE_INT, EP1_SIZE);
#else
SetBTABLE(BTABLE_ADDRESS);
/*————————————————————————–*/
/* Initialize Endpoint 0 */
SetEPType(ENDP0, EP_CONTROL); //设置端点1为控制端点
SetEPTxStatus(ENDP0, EP_TX_STALL); //设置端点0发送延时
SetEPRxAddr(ENDP0, ENDP0_RXADDR); //设置端点0的接收缓冲区地址
SetEPTxAddr(ENDP0, ENDP0_TXADDR); //设置端点0的发送缓冲区地址
Clear_Status_Out(ENDP0); //清除端点0的状态
SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);//设置端点0的接收的计数
SetEPRxValid(ENDP0); //使能接收状态
/* Initialize Endpoint 1 */
SetEPType(ENDP1, EP_INTERRUPT); //设置端点1为中断控制端点
SetEPRxAddr(ENDP1, ENDP1_RXADDR); //设置端点1的接收缓冲地址
SetEPRxCount(ENDP1, REPORT_COUNT); //设置端点1的接收计数
SetEPRxStatus(ENDP1, EP_RX_VALID); //设置端点1接收有效
//SetEPTxStatus(ENDP1, EP_TX_DIS);
/* Initialize Endpoint 2 */
SetEPType(ENDP2, EP_INTERRUPT); //设置端点2为中断控制端点
SetEPTxAddr(ENDP2, ENDP2_TXADDR); //设置端点2的接收缓冲地址
SetEPTxCount(ENDP2, REPORT_COUNT); //设置端点2的接收计数
SetEPTxStatus(ENDP2, EP_TX_NAK); //设置端点2为接收不响应
/*————————————————————————–*/
bDeviceState = ATTACHED; //设置设备状态为 ATTACHED状态
/* Set this device to response on default address */
SetDeviceAddress(0); //设置设备为默认地址
#endif /* STM32F10X_CL */
bDeviceState = ATTACHED;
}
在两根”/*——*/“中间的代码是最重要的,我们根据配置描述可知,端点1作为输出端点,端点2作为输入端点,所以在Reset函数中需要对端点进行初始化:端点0在USB枚举阶段作为通讯的端点,要配置成控制端点,收发有效;端点1配置成中断传输端点,端点接收有效,发送无效;端点2配置成中断传输端点,端点发送有效。
我们在之前关于数据收发流程中说到,数据接收的流程:
USB_LP_CAN1_RX0_IRQHandler—>USB_Istr—->CTR_LP—>EPx_OUT_Callback。所以我们这里首先需要定义USB中断服务程序
USB_LP_CAN1_RX0_IRQHandler,再还要编写端点接收回调函数:
EPx_OUT_Callback函数。
在hw_config.c中编写USB中断配置函数:
void USB_Interrupts_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 2 bit for pre-emption priority, 2 bits for subpriority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
#ifdef STM32F10X_CL
/* Enable the USB Interrupts */
NVIC_InitStructure.NVIC_IRQChannel = OTG_FS_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* Enable the USB Wake-up interrupt */
NVIC_InitStructure.NVIC_IRQChannel = OTG_FS_WKUP_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_Init(&NVIC_InitStructure);
#else
/* Enable the USB interrupt */
NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; //设置USB中断服务程序
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
#endif /* STM32F10X_CL */
}
我们这里只配置
USB_LP_CAN1_RX0_IRQn这个中断,接着在stm32f10x_it.c中编写
USB_LP_CAN1_RX0_IRQHandler()函数:
void USB_LP_CAN1_RX0_IRQHandler(void)
{
USB_Istr();
}
这个中断服务程序只调用USB_Istr()函数。经过这个函数的处理,最终程序会执行到
EPx_OUT_Callback函数,由于我们使用端点1作为接受数据的端点,所以我在usb_endp.c中编写
EP1_OUT_Callback()函数:
/*******************************************************************************
* Function Name : EP1_OUT_Callback.
* Description : 端点1输出回调函数
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void EP1_OUT_Callback(void)
{
#ifndef STM32F10X_CL
PMAToUserBufferCopy(USB_Receive_Buffer, ENDP1_RXADDR, REPORT_COUNT); //PMA缓冲区接收到的数据拷贝到用户自定义缓冲区USB_Receive_Buffer中
SetEPRxStatus(ENDP1, EP_RX_VALID); //设置端点的接收状态为有效,因为端点接收到数据后会端点状态自动设置成停止状态
USB_Received_Flag=1; //设置接收到数据标志位
#else
USB_SIL_Read(EP1_OUT,USB_Receive_Buffer); //读取输出端点的数据到USB_Receive_Buffer中
USB_Received_Flag=1; //收到数据的标志
#endif
}
这个函数起始很简单,从端点缓冲区中读取数据,保存在
USB_Receive_Buffer[]数组中,到时候,可以直接拿这个数组进行数据操作了。
我们还有编写一个端点发送函数:USB_SendData()。我在自己建的usb_io.c文件中,编写该函数:
/*******************************************************************************
* Function Name : EP1_IN_Callback.
* Description : USB向主机发送数据
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
uint32_t USB_SendData(uint8_t *data,uint32_t dataNum)
{
#ifndef STM32F10X_CL
//将数据通过USB发送出去
UserToPMABufferCopy(data, ENDP2_TXADDR, dataNum); //拷贝dataNum个数据到PMA中
SetEPTxCount(ENDP2, REPORT_COUNT); //从端点2发送64字节数据
SetEPTxValid(ENDP2); //使能端点2的发送状态
#else
USB_SIL_Write(EP2_IN, data, dataNum);
#endif
return dataNum;
}
依旧很简单,只要来那个要发送的数据包写入端点的缓冲区中,再使能下短短,数据就看可以发送出去了。
最后,我们来编写我们的main函数:
/********************************************************
函数:main()
描述:程序入口地址
参数:无
返回:无
********************************************************/
int main(void)
{
uint8_t data[64];
uint32_t i=0,ret=0;
BSP_Init();
printf(” |===============================================|\r\n”);
printf(” USB CustomHID 程序开始 \r\n”);
printf(“|===============================================|\r\n”);
while(1)
{
if(USB_Received_Flag)
{
USB_Received_Flag=0;
ret = USB_GetData(data,sizeof(data)); //读取数据
printf(“usb get data %d byte data\r\n”,ret); //答应接收到的字节数
for(i=0;i<ret;i++){
printf(“0x%02X “,data[i]); //答应接收到的数据
}
printf(“\n\r”);
USB_SendData(data,sizeof(data)); //发送接收到的数据
}
}
}
主函数的意思是,USB设备接收到什么数据,就向USB主机发送什么数据,并且在串口打印出收到的数据。
至此,CustomHID程序差不多了。万事俱备只欠东风了:我们还需要一个上位机的HID程序。可惜啊,这里上传不了,只能截个图让大家看看现象了。
再看看BUS Hound捕获到的数据,可以看到发送的数据和就收到的数据一样: