|
LPC1788—USB学习
第四章;USB设备枚举
第一篇;分析设备请求数据结构包
在上一章说到,把主机发过来的数据接收到。现在就根据USB标准设备请求数据结构来分析,接收到的数据是要让设备干嘛!接收到的数据是,0x80 0x06 0x00 0x01 0x00 0x00 0x0040。注意;数据传输为小端格式,低字节在前高字节在后。后面的0x0040是使用一个16位无符号短整型接收,所以低字节在后高字节在前。
第一个域是bmRequestType这个域占用了一个字节,0x80转换为二进制为“1000 0000”,在USB标准设备请求数据结构中,在这个字节中每一位都有不同的作用如下:
D7:数据传输方向
0—主机到设备
1—设备到主机
D5~6:请求类型
0—标准
1—类
2—厂商
3—保留
D0~4:请求连接者
0—设备
1—接口
2—端点
3—其他
4~31—保留
可以看出来主机所要的数据方向是设备到主机,请求类型是标准类型,请求连接者是设备。
第二个域是bRequest这个域占用了一个字节,这个就是标准请求域,USB协议定义了11个标准请求。如表4—1—1:
表4—1—1 标准请求码
bRequest 数值 描述
GET_STATUS 0 获得状态
CLEAR_FEATURE 1 清除特征
SET_FEATURE 3 设置特征
SET_ADDRESS 5 设置地址
GET_DESCRIPTOR 6 获取描述符
SET_DESCRIPTOR 7 设置描述符
GET_CONFIGURATION 8 获取配置
SET_CONFIGURATION 9 设置配置
GET_INTERFACE 10 获取接口
SET_INTERFACE 11 设置接口
SYNCH_FRAME 12 同步帧
主机发的这个SETUP包bRequest域是0x06,就是要获取描述符。
后面的第三第四个域是wValue和wIndex域它们各占用两个字节,它们的作用和前面bRequest域的请求码有关,请求码不同wValue和wIndex域代表的功能也不同。表4—1—2就是几个常用传给设备的标准请求结构。
表4—1—2
bmRequestType bRequest wValue wIndex wLength 返回给主机的数据
10000000B GET_DESCRIPTOR (0x06) 描述符类型和索引 0或语言ID 描述符长度 返回给主机描述符
00000000B SET_ADDRESS (0x05) 设备地址 0 0 不会回给主机数据
00000000B SET_CONFIGURATION (0x09) 配置值 0 0 不会回给主机数据
从前面的输出包分析可以知道是一个获取描述符的请求,而wValue和wIndex域分别是0x00 0x01 和 0x00 0x00。wValue域是1,wIndex域是0。
bRequest域指明请求号0x06,GET_DESCRIPTOR 的请求号为0x06。
wValue域的高字节表示所要读取描述符类型的值;低字节表示描述符索引的值,只对配置描述符和字符串描述符有效,对于其他描述符该字节为0。表4—1—3是就个常用的描述符类型编号。
表4—1—3
描述符类型 编号
设备描述符(DEVICE_DESCRIPTOR_TYPE) 1
配置描述符(CONFIGURATION_DESCRIPTOR_TYPE) 2
字符串描述符(STRING_DESCRIPTOR_TYPE) 3
接口描述符(INTERFACE_DESCRIPTOR_TYPE ) 4
端点描述符(ENDPOINT_DESCRIPTOR_TYPE) 5
wIndex域指表示符串描述符的语言ID,对于其他描述符该字段为0。
wLength域表示这个请求所返回的描述符长度是多少(字节)。如果wLength大于描述符实际长度,就返回实际描述符的长度;如果wLength小于实际描述符长度,就返回描述符前wLength个长度的字节数据。
接收到的0x80 0x06 0x00 0x01 0x00 0x00 0x0040这包数据按照上面的分析,主机是要向设备请求设备描述符,请求的长度是0x0040也就是64字节。
第二篇;设备描述符
设备描述符是每个设备都必须具有的,而且必须只有一个设备描述符。因为设备就只有一个,所以设备描述符也就一个了。不会平白无故飞一个设备出来。接下来就来分析设备描述符的结构,表4—2—1就是设备描述符该有的东西。
域 大小(字节) 说明
bLength 1 设备描述符长度(18字节)
bDescriptorType 1 描述符类型
bcdUSB 2 该设备使用的USB协议版本(BCD码)
bDeviceClass 1 类代码
bDeviceSubClass 1 子类代码
bDeviceProtocol 1 设备所使用的协议
bMaxPacketSize0 1 端点0最大长度
idVendor 2 厂商ID
idProduct 2 产品ID
bcdDevice 2 设备版本号
iManufacturer 1 供应商字符串描述符索引值
iProduct 1 产品字符串描述符索引值
iSerialNumber 1 设备序列号字符串描述符索引值
bNumConfigurations 1 所支持的配置数
现在就结合代码来分析设备描述符的数据。下面就是设备描述符,我们用一个数组来封装描述符。
/****************************设备描述符*******************************/
const uint8_t USB_DeviceDescriptor[0x12] = {
0x12, /* bLength 大小1字节。表示描述符的长度(字节),
设备描述符的长度是18字节,十六进制“0x12”。*/
0x01, /* bDescriptorType 大小1字节。表示描述符的类型,
大部分都是表4—1—3的那些东东了。主机请求的是
设备描述符那就该是“0x01”。*/
0x10,0x01, /* bcdUSB 大小2字节。表示这个设备使用的USB协
议版本。也就是我们常说的“傻逼2.0”或者“傻逼1.1”
等版本号。它们用BCD码表示,例如USB2.0协议就是
0x0200,USB1.1协议就是0x0110。数据传输是小端格式,
所以USB2.0就是0x00 0x20,USB1.1就是0x10 0x01。*/
0x00, /* bDeviceClass 大小1字节。设备类码是有USB协会规
定的,表示的是接口所能实现的功能。当此域为0时下面
的子类也必须为0,当为0xFF表示的是厂商自定义设备类。*/
0x00, /* bDeviceSubClass 大小1字节。子类代码这个码值的
意思是根据设备类码来看。如设备类码为零,这字段也要零
如设备类码为0xFF,此域的所有 值保留。*/
0x00, /* bDeviceProtocol 大小1字节。表示协议码 这些码的值视
设备码和子类代码的值而定。 当该字段为0是,表示设备
不使用类所定义的协议,当该FFH字段的值为是,表示使用
设备厂商自定义的协议 。*/
0x10, /* bMaxPacketSize0 大小1字节。是端点0的最大包长度,
0x10是16 */
0x88,0x88, /* idVendor 大小2字节。表示厂商ID号。这个ID号是由
USB协会分配,可以跟USB协会申请一个厂商ID号,但
是是要给钱的,这里我们用0x8888来做为学习用。*/
0x01,0x00, /* idProduct 大小2个字节。表示产品ID。这个ID比较
自由这里用0x0001来表示*/
0x00,0x01, /* bcdDevice 大小2个字节。表示设备版本号*/
0x01, /* iManufacturer 大小1个字节。表示厂商字符串的索引值。*/
0x02, /* iProduct 大小1个字节。表示产品字符串索引值。*/
0x03, /* iSerialNumber 大小1个字节。表示设备序列号字符串
索引值。*/
0x01 /* bNumConfigurations 大小1个字节。表示设备所具有的配,
一种配置就可以了,所以这个值取0x01*/
};
第三篇;返回设备描述符
有了描述符现在就该返回描述符。在上一章中用了一个void USB_EndPoint0 (uint32_t Event)函数打印端点0接收到的数据,现在就要利用这个函数返回描述符了代码如下:
/**********************************************************************
函数功能;USB端点0函数
函 数 名;USB_EndPoint0
函数参数;Event; 需要端点0执行的事件。
函数返回;无
作 者: 旺宝电子科技有限公司
***********************************************************************/
void USB_EndPoint0 (uint32_t Event)
{
switch (Event) {
case USB_EVT_SETUP:
USB_ReadEP (0, (uint8_t *)&SetupPacket);
EP0Data.Count = SetupPacket.wLength;
#if PRINTF
_DBH(SetupPacket.bmRequestType.B);
_DBG_(" ");
_DBH(SetupPacket.bRequest);
_DBG_(" ");
_DBH(SetupPacket.wValue.WB.L);
_DBG_(" ");
_DBH(SetupPacket.wValue.WB.H);
_DBG_(" ");
_DBH(SetupPacket.wIndex.WB.L);
_DBG_(" ");
_DBH(SetupPacket.wIndex.WB.H);
_DBG_(" ");
_DBH16(SetupPacket.wLength);
_DBG_(" ");
_DBG_("\r\n");
#endif
switch (SetupPacket.bmRequestType.BM.Type){
case REQUEST_STANDARD: //标准
switch (SetupPacket.bRequest) { //请求代码
case REQUEST_SET_ADDRESS: //设置地址
#if PRINTF
_DBG_("设置地址\r\n");
#endif
break;
case REQUEST_GET_DESCRIPTOR: //获取描述符
#if PRINTF
_DBG_("获取描述符\r\n");
#endif
if (!USB_ReqGetDescriptor()){
#if PRINTF
_DBG_("写入描述符错误\r\n");
#endif
}
USB_DataInStage(); //将数据写入到端点缓冲区中。
break;
case REQUEST_SET_CONFIGURATION: //设置配置
#if PRINTF
_DBG_("设置配置\r\n");
#endif
break;
}
break; //USB标准请求结束
}
break;
case USB_EVT_OUT:
break;
case USB_EVT_IN :
if (SetupPacket.bmRequestType.BM.Dir == REQUEST_DEVICE_TO_HOST){
USB_DataInStage(); //发送数据
}
else {
if(DeviceAddress & 0x80) { /*判断DeviceAddress变量里
是否有地址。*/
DeviceAddress &= 0x7F;
USB_SetAddress(DeviceAddress); //设置地址
}
}
break;
}
}
在void USB_EndPoint0 (uint32_t Event)函数里用分支语句来判断定义的标准请求宏
#define REQUEST_SET_ADDRESS 5 //设置地址
#define REQUEST_GET_DESCRIPTOR 6 //获取描述符
#define REQUEST_SET_CONFIGURATION 9 //设置设备配置
在相应的分支里添加相应的代码,在当前我们分析出来的请求是获取描述,描述符类型是设备描述符,所以在获取描述符分支里添加了一个判断获取描述符类型的函数USB_ReqGetDescriptor ();函数源码如下:
/**********************************************************************
函数功能;判断USB请求描述符类型,
并写入到USB端点数据结
构体全局变量EP0Data中。
函 数 名;USB_ReqGetDescriptor
函数参数;无
函数返回;返回(TRUE)写入成功。
返回(FALSE)写入失败。
作 者: 旺宝电子科技有限公司
***********************************************************************/
__inline uint32_t USB_ReqGetDescriptor (void)
{
uint8_t *pD;
uint32_t len, n;
switch (SetupPacket.bmRequestType.BM.Recipient) {
case REQUEST_TO_DEVICE: //设备
switch (SetupPacket.wValue.WB.H) {
case DEVICE_DESCRIPTOR_TYPE:
#if PRINTF
_DBG_("\r\n写入设备描述符\r\n");
#endif
EP0Data.pData = (uint8_t *)USB_DeviceDescriptor; //设备描述符首地址
len = sizeof(USB_DeviceDescriptor); //描述符大小
break;
case CONFIGURATION_DESCRIPTOR_TYPE:
#if PRINTF
_DBG_("\r\n写入配置描述符\r\n");
#endif
///////////////////添加写入配置描述符代码///////////////////
break;
case STRING_DESCRIPTOR_TYPE:
#if PRINTF
_DBG_("\r\n字符串描述符\r\n");
#endif
////////////////////添加写入字符串描述符代码///////////////////
break;
default:
#if PRINTF
_DBG_("\r\n未知字符串描述符\r\n");
#endif
return (FALSE);
}
break;
case REQUEST_TO_INTERFACE: //接口
switch (SetupPacket.wValue.WB.H) {
case HID_REPORT_DESCRIPTOR_TYPE: //报告描述符
#if PRINTF
_DBG_("\r\n写入报告描述符\r\n");
#endif
////////////////////////////////添加写入报告描述符代码///////////////////
return (FALSE);
default:
#if PRINTF
_DBG_("\r\n未知接口描述符\r\n");
#endif
return (FALSE);
}
break;
default:
#if PRINTF
_DBG_("\r\n未知接口描述符\r\n");
#endif
return (FALSE);
}
if (EP0Data.Count > len) { /*如果索要描述符大于实际描述符就赋予实际描述符大小
如果索要描述符小于实际描述符就赋予索要描述符大小*/
EP0Data.Count = len;
}
return (TRUE);
}
在这个函数里用到一个结构体变量EP0Data原型如下:
typedef struct _USB_EP_DATA {
uint8_t *pData;
uint16_t Count;
} USB_EP_DATA;
用这个结构体定义的全局变量EP0Data。这个函数的主要功能就是把需要的描述符赋给这个全局变量。如果描述符赋值成功USB_EndPoint0函数就会调用USB_DataInStage()函数把描述符数据写入端点0。函数原型如下:
/**********************************************************************
函数功能;USB请求数据输入阶段函数
函 数 名;USB_DataInStage
函数参数;无
函数返回;无
作 者: 旺宝电子科技有限公司
***********************************************************************/
void USB_DataInStage (void)
{
uint32_t cnt;
if (EP0Data.Count > USB_MAX_PACKET0) { /*所要发送的包如果大于端点缓冲区就 写入端点缓冲区大小的数据如果小于端 点缓冲就写入所有描述符大小的数据*/
cnt = USB_MAX_PACKET0;
} else {
cnt = EP0Data.Count;
}
cnt = USB_WriteEP(0x80, EP0Data.pData, cnt); //数据写入端点0
EP0Data.pData += cnt;
EP0Data.Count -= cnt;
}
这里面还调用了一个函数就是USB_WriteEP这个函数就是向指定端点写入数据,细心的可就会看到,明明是向端点0写数据,而参数却是0x80。这里先不管先看USB_WriteEP函数原型。代码如下:
/**********************************************************************
函数功能;写USB端点数据函数
函 数 名;USB_WriteEP
函数参数;EPNum;要写的端点。
*pData;指向缓冲区的指针。
cnt;写入的字节数。
函数返回;写入的字节数。
作 者:旺宝电子科技有限公司
***********************************************************************/
uint32_t USB_WriteEP(uint32_t EPNum, uint8_t *pData, uint32_t cnt)
{
uint32_t n;
LPC_USB->Ctrl = ((EPNum & 0x0F) << 2) | CTRL_WR_EN;
LPC_USB->TxPLen = cnt;
for (n = 0; n < (cnt + 3) / 4; n++){
LPC_USB->TxData = *((__packed uint32_t *)pData);
_DBH32(*((__packed uint32_t *)pData));
_DBG_(" ");
pData += 4;
}
_DBG_("\r\n");
LPC_USB->Ctrl = 0; //禁止向端点读写数据
WrCmdEP(EPNum, CMD_VALID_BUF); //确认缓冲区
return (cnt);
}
现在USB_WriteEP函数里又调用了一个WrCmdEP函数
/**********************************************************************
函数功能;向端点写命令函数
函 数 名;WrCmdEP
函数参数;EPNum: 端点号
cmd;命令
函数返回;无
作 者: 旺宝电子科技有限公司
***********************************************************************/
void WrCmdEP(uint32_t EPNum, uint32_t cmd)
{
uint32_t val;
val = (EPNum & 0x0F) << 1;
if (EPNum & 0x80) { //如果端点参数EPNum为0x80说明是输入端点
val += 1;
}
LPC_USB->DevIntClr = CCEMTY_INT;
LPC_USB->CmdCode = CMD_SEL_EP(val); //选择端点准备向该端点写命令
while ((LPC_USB->DevIntSt & CCEMTY_INT) == 0);
LPC_USB->DevIntClr = CCEMTY_INT;
LPC_USB->CmdCode = cmd; //写入命令
while ((LPC_USB->DevIntSt & CCEMTY_INT) == 0);
}
0x80就是在确认缓冲区的时候来判断物理输出端点。好了现在下载程序运行看串口调试助手输出的数据。如图4—3—1:
图4—3—1
现在程序跑到了设置地址的地方。
case REQUEST_SET_ADDRESS: //设置地址
#if PRINTF
_DBG_("设置地址\r\n");
#endif
break;
现在在设置地址的地方没有代码,所以主机就会连续的向设备索要设备描述符,然后在来设置地址。反复几次主机就会然设备挂起!而不管它。
现在就来说添加设置地址的代码。之前我们在复位设备时就讲了一个设置地址的函数USB_SetAddress(uint32_t adr);函数就是设置地址的函数,但是在这里面要连续设置两次地址才能正常,这个我也不太明白是啥原因,如果有那位大神知道的可以讨论一下。代码如下。
/**********************************************************************
函数功能;设置USB地址函数
函 数 名;USB_SetAddress
函数参数;adr;要写入的地址
函数返回;无
作 者: 旺宝电子科技有限公司
***********************************************************************/
void USB_SetAddress(uint32_t adr)
{
WrCmdDat(CMD_SET_ADDR, DAT_WR_BYTE(0x80 | adr)); //(1<<7)的意思是使能0—6位的地址。
WrCmdDat(CMD_SET_ADDR, DAT_WR_BYTE(0x80 | adr));
#if PRINTF
_DBG_("地址");
_DBH(adr);
_DBG_("\r\n");
#endif
}
在设置地址前先要把输出包的地址提取出来,主机发来的地址就在wValue域的低字节中。只需要把wValue域的低字节提出来赋值给一个全局变量,然后再发一个0个数据的输入包,让主机知道地址已经成功接收到。这时设备就会产生一个主机到设备的输入中断启用设备地址。以下代码就是提取地址的源代码:
/**********************************************************************
函数功能;设置USB设备地址
函 数 名;USB_ReqSetAddress
函数参数;无
函数返回;无
作 者: 旺宝电子科技有限公司
***********************************************************************/
__inline uint32_t USB_ReqSetAddress (void)
{
switch (SetupPacket.bmRequestType.BM.Recipient){
case REQUEST_TO_DEVICE:
DeviceAddress = 0x80 | SetupPacket.wValue.WB.L; /*或上0x80的目的是在主 机到设备的输入中断里判断 DeviceAddress里有地址*/
break;
default:
return (FALSE);
}
return (TRUE);
}
下载程序运行正常就会出现主机在次获取设备描述符,这次获取是用设置好了的地址获取的设备描述符,而且获取的描述符长度也变成了0x0012也就是现在的设备描述符长度十进制18字节。现象如图4—3—2:
图4—3—2
在之前已经写好了返回给主机设备描述符的程序,所以程序还会继续运行把设备描述符在次返回给主机如图4—3—3:
图4—3—3
运行到这里主机又输出了一个索要配置描述的SETUP数据包。
第四篇;配置描述符
/***************************** USB配置描述符 *************************/
const uint8_t USB_ConfigDescriptor[0x09+0x09+0x09+0x07] = {
0x09, /* bLength 大小1字节。表示配置符的长度。*/
0x02, /* bDescriptorType 大小2字节。表示描述符的类型。
配置描述符的类型编码为0x02*/
sizeof(USB_ConfigDescriptor) & 0xff, (sizeof(USB_ConfigDescriptor)>>8) & 0xff, /* wTotalLength 大小2字节。表示整个 配置描述符长度,有配置描述符、类特殊 描述符和端点描述符。*/
0x01, /* bNumInterfaces 大小1字节。表示这个配置所支持的接口数量。
现在用到的接口数量就一个,所以是0x01。*/
0x01, /* bConfigurationValue 大小1字节。表示配置值。USB支持多少种配置, bConfigurationValue就是每个配置的标志。在配置请求时会发送配置值,
发送的配置值与bConfigurationValue相匹配,那么该配置就会被激活为当
前配置。*/
0x00, /* iConfiguration 大小1字节。表示描述该配置的字符串索引值。*/
0x80, /* bmAttributes 大小1字节。描述设备的一些特性。第7位必须为1。
第6位表示供电方式为1是自供电,为0是总线供电。第5位表示是
否支持远程唤醒。第4—0位保留为0。*/
0x32, /* bMaxPower 大小1字节。表示设备从总线上获取的最大电流,单位
以2mA为单位。例如100mA 就是2*50,50转换为十六进制为0x32。*/
/*******************************接口描述符**************************************************/
0x09, /* bLength 大小1字节。表示接口描述符的长度。*/
0x04, /* bDescriptorType 大小1字节。表示描述符类型。接口描述符类型编码
为0x04。*/
0x00, /* bInterfaceNumber 大小1字节。表示接口编号。当配置具有多个接口时, 每个接口编号都不一样。都是从0递增对第一个编号配置的接口进行编号。 */
0x00, /* bAlternateSetting大小1字节。表示接口的备用编号。很少会用到。它的规 律和bInterfaceNumber一样。*/
0x01, /* bNumEndpoints 大小1字节。表示该接口使用的端点数(不包括0端点)。 如果为0表示没有非0端点,只使用默认的控制端点。*/
0x03, /* bInterfaceClass 大小1字节。表示接口类。USB鼠标是HID类,编码为 0x03。 */
0x01, /* bInterfaceSubClass 大小1字节。表示接口使用的子类。HID1.1只规定了 了一种子类:支持BIOS引导启动的子类。USB鼠标、键盘属于该之类,代 码为0x01.*/
0x02, /* bInterfaceProtocol 大小1字节。表示子类支持引导启动的子类,协议可选 择鼠标和键盘,鼠标码为0x02,键盘码为0x01。*/
0x00, /*iConfiguration 大小1字节。表示接口字符串索引值。没有就为0。*/
/*******************************HID描述符****************************************************/
0x09, /* bLength 大小1字节。表示HID描述符长度 */
0x21, /* bDescriptorType 大小1字节。表示描述符编号。HID的编号为0x21。 */
0x10,0x01, /*bcdHID 大小2字节。表示HID协议号,这里是HID1.1协议,为0x10,0x01 (小端格式低字节在前高字节在后)*/
0x21, /*bCountyCode 大小1字节。表示设备所适用国家。0x21是美国*/
0x01, /*bNumDescriptors 大小1字节。表示下级描述符的数目。这里就只有一个报 告描述符。为0x01。*/
0x22, /*bDescriptorsType 大小1字节。表示下级描述符的类型。报告描述符编号为 0x22*/
0x34,0x00, /*bDescriptorsLength 大小2字节。表示下级报告描述符的长度*/
/******************************端点描述符****************************************************/
0x07, /* bLength 大小1字节。表示端点描述符长度*/
0x05, /* bDescriptorType 大小1字节。表示描述符的类型。端点描述符类型为码 为0x05。*/
0x81, /* bEndpointAddress 大小1字节。表示端点的地址。最高位第7位为端点 传输方向,1为输入。0为输出。第6—4位保留,值为0。第3—0位为端点 号*/
0x03, /* bmAttributes 大小1字节。表示端点属性。最低位第1—0位表示端点传 输类型,0为控制传输,1为等时传输,2为批量传输,3为中断传输。如果 端点为非等时传输的端点,第7—2位就为保留值0。端点类型为等时传输时, 第3—2位表示同步的类型,0为无同步,1为异步,2为适配,3为同步。第 5—4位表示端点用途,0为数据端点,1为反馈端点,2为暗含反馈的数据端 点,3为保留。最高位第7—6位保留。现在是用的中断传输编号为3,其他 保留为0。*/
0x10,0x00, /* wMaxPacketSize 大小2字节。表示端点最大包长度。设为16字节。*/
0x0a, /* bInterval 大小1字节。表示端点的查询时间。这里设为10ms*/
};
有了配置描述符现在就应该输入给主机配置描述符了,这个过程和输入设备描述符一样在__inline uint32_t USB_ReqGetDescriptor (void)请求描述符函数里的配置描述符类型分支里添加写入配置描述符的代码。代码如下:
case CONFIGURATION_DESCRIPTOR_TYPE: //配置描述符
#if PRINTF
_DBG_("\r\n写入配置描述符\r\n");
#endif
EP0Data.pData = (uint8_t *)USB_ConfigDescriptor;
len = sizeof(USB_ConfigDescriptor);
break;
编译运行查看串口调试助手输出现象如图4—3—4:
图4—3—4
从串口打印的数据可以看出索要了9字节的配置描述符,而打印了12个字节多了09H 04H 00H(十六进制),那是应为在写入端点函数是按照字写入的也就是四字节写入,如果写两次就会少一个字节,所以不能少,而多出的三个字节可以不用管它。在写如的配置描述符里可以看到有02H 22H 两个十六进制数,这两个就是表示的是描述符的总长度34个字节。现在主机又输出了获取描述符的请求,这次是获取字符串描述符请求,请求的字符串索引号为0,而我们在设备描述符里定义的索引号没有索引号为0的字符串描述符。主机在索取索引号为0的字符串的目的是索取语言ID用的。接下来就说字符串描述符的事!
第四篇;字符串描述符
在USB协议里字符串描述符是可选的。在设备描述符里的字符串索引号不为0时,就表示它具有那个字符串描述符,要注意的是索引号不能重复。我们在设备描述符里申请了3个字符串描述符索引号分别是01H代表厂商字符串索引号、02H代表产品字符串索引号、03H代表设备序列号字符串索引号。主机就通过索引号来获取相应的字符串描述符。字符串索引号为0时,表示获取语言ID。语言ID是用来描述设备支持的语言种类。每个ID号占用两个字节。在字符串描述符中的bString域是用UNICODE编码的字符串。一下就是各个字符串描述符的代码。
/**************************************字符串描述符*******************************************/
/*语言ID*/
const uint8_t languageID[4] = {
0x04, /* bLength 大小1字节。表示描述符长度*/
0x03, /* bDescriptorType 大小1字节。表示描述符类型。字符串描述符的类型 为0x03。*/
0x09, 0x04 /* wLANGID 大小2字节。表示语言ID号。0x0409表示美式英语。*/
};
/*厂商字符串*/
const uint8_t ManufacturStringDescriptor[13*2 + 2] = {
(13*2 + 2), /* bLength 大小1字节。表示描述符长度 */
0x03, /* bDescriptorType 大小1字节。表示描述符类型。字符串描述符的类型 为0x03*/
'N',0, /*bString 大小2字节。这个字段用UNICODE编码的*/
'X',0,
'P',0,
' ',0,
'S',0,
'E',0,
'M',0,
'I',0,
'C',0,
'O',0,
'N',0,
'D',0,
' ',0
};
/*产品字符串*/
const uint8_t ProductDescriptor[18*2 + 2] = {
(18*2 + 2), /* bLength ( 17 Char + Type + lenght) */
0x03, /* bDescriptorType */
' ',0,
0xFA,0x65, //旺
0x9D,0x5B, //宝
0x35,0x75, //电
0x50,0x5B, //子
' ',0,
'L',0,
'P',0,
'C',0,
'1',0,
'7',0,
'8',0,
'8',0,
' ',0,
'H',0,
'I',0,
'D',0,
' ',0
};
/*产品序列号字符串*/
const uint8_t SerialDescriptor[12*2 + 2] = {
(12*2 + 2), /* bLength (12 Char + Type + lenght) */
0x03, /* bDescriptorType */
'D',0,
'E',0,
'M',0,
'O',0,
'0',0,
'0',0,
'0',0,
'0',0,
'0',0,
'0',0,
'0',0,
'0',0
};
有了字符串描述符现在就是输入字符串描述符了。前面讲过主机是通过索引号来分别区分索要的字符串描述符,那么在__inline uint32_t USB_ReqGetDescriptor (void)函数的case STRING_DESCRIPTOR_TYPE:分支代码中也要进行相应的判断。代码如下:
case STRING_DESCRIPTOR_TYPE: //字符串描述符
#if PRINTF
_DBG_("\r\n字符串描述符\r\n");
#endif
switch (SetupPacket.wValue.WB.L){
case 0:
EP0Data.pData = (uint8_t *)languageID; /*语言ID*/
len = sizeof(languageID);
break;
case 1:
EP0Data.pData = (uint8_t *)ManufacturStringDescriptor; /*厂商字符串*/
len = sizeof(ManufacturStringDescriptor);
break;
case 2:
EP0Data.pData = (uint8_t *)ProductDescriptor; /*产品字符串*/
len = sizeof(ProductDescriptor);
break;
case 3:
EP0Data.pData = (uint8_t *)SerialDescriptor; /*产品序列号字 符串*/
len = sizeof(SerialDescriptor);
break;
default:
#if PRINTF
_DBG_("\r\未知字符串描述符\r\n");
#endif
break;
}
break;
现在编译程序下载到开发板上实验现象如图4—4—1:
图4—4—1
如果不出意外会连续经过获取配置描述符,字符串描述符,设备描述符,几次获取后就会接收到一个设置配置的SETUP包。
第五篇;设置配置
设置配置就按照端点描述符的说明,去配置相应的端点:如下代码:
void USB_ConfigEP(uint32_t EPNum, uint32_t MaxPacketSize)
{
uint32_t val;
val = (EPNum & 0x0F) << 1;
if (EPNum & 0x80) {
val += 1;
}
LPC_USB->ReEp |= 0x00000080; //使能物理端点
LPC_USB->EpInd = 0x00000003; //端点索引寄存器,也是物理端点的编号(0—31)
LPC_USB->MaxPSize = MaxPacketSize; //端点最大包长度
LPC_USB->ReEp |= 0x00000040;//(1 << val); //使能物理端点
LPC_USB->EpInd = 0x00000002; //端点索引寄存器,也是物理端点的编号(0—31)
LPC_USB->MaxPSize = MaxPacketSize; //端点最大包长度
while ((LPC_USB->DevIntSt & 1<<8) == 0); //等待端点被使能
LPC_USB->DevIntClr = 1<<8; //清除DevIntSt寄存器端点使能中断
WrCmdDat(CMD_SET_EP_STAT(val), DAT_WR_BYTE(0)); //设置端点状态
}
第六篇;报告描述符
USB HID 设备是通过报告来传输数据的,报告分输入报告和输出报告。
输入报告是USB设备发送给主机的,如USB鼠标移动和鼠标点击信息输入给主机,键盘把按键信息输入给主机。
输出报告是主机发送给USB设备的,如USB键盘上的数字小键盘锁定灯和大小写指示灯等等。
而报告描述符,是用来描述一个报告的结构和报告里面的数据用来干嘛的,主机通过报告描述符,判断出报告里的数据所表示的含义。报告描述符和其他描述符一样都是通过控制输入端点0传输给主机的。一个报告描述符可以描述多个报告,不同的报告用ID号来区分。
报告描述符和其他描述不一样,它没有表示描述符长度和描述符类型的域。而是由一个个条目组成。条目分为长条目和短条目,这里只说短条目。
短条目由1字节的前缀和可选的数据组成,可选的数据大小从0、1、2、4字节不等。表4—1—1是一个短条目的结构。
D7—D4位 D3—D2位 D1—D0位
表示条目的功能具体查看HID协议 表示条目类型:
0—表示主条目
1—表示全局条目
2—表示局部条目
3—保留值 表示条目后面的数据字节:
0—表示0字节
1—表示1字节
2—表示2字节
3—表示4字节
主条目共5个分别是Input(输入)、Output(输出)、Feature(特性)、Collection(集合)、EndCollection(关集合)。
/*HID报告描述符*/
const uint8_t HID_ReportDescriptor[26*2] = {
0x05, 0x01, // 用途页(generic desktop)
0x09, 0x02, // 用法索引(mouse)
0xA1, 0x01, // 集合开始(collection,application)
0x09, 0x01, // 用法索引(指针)
0xA1, 0x00, // 集合开始
0x05, 0x09, // 用法页
0x19, 0x01, // 用法最小值
0x29, 0x03, // 用法最大值
0x15, 0x00, // 逻辑最小值
0x25, 0x01, // 逻辑最大值
0x95, 0x03, // 报告计数(3):三键
0x75, 0x01, // 报告大小(1):按键值占用1Bit
0x81, 0x02, // 输入(2)
0x95, 0x01, // 报告计数(1)
0x75, 0x05, // 报告大小(5)
0x81, 0x01, // 输入(1)
0x05, 0x01, // 使用页(通用桌面)
0x09, 0x30, // 用法(X)
0x09, 0x31, // 用法(Y)
0x09, 0x38, // 用法(Z)
0x15, 0x81, // 逻辑最小值(129)
0x25, 0x7F, // 逻辑最大值(127)
0x75, 0x08, // 报告大小(8):每轴的数值占用一个字节
0x95, 0x03, // 报告计数(3):三轴
0x81, 0x06, // 输入(6)
0xC0, // 集合结束
0xC0 // 集合结束
};
现在在__inline uint32_t USB_ReqGetDescriptor (void)函数里添加返回报告描述符的代码,代码如下:
case REQUEST_TO_INTERFACE: //接口
switch (SetupPacket.wValue.WB.H) {
case HID_REPORT_DESCRIPTOR_TYPE: //报告描述符
#if PRINTF
_DBG_("\r\n写入报告描述符\r\n");
#endif
EP0Data.pData = (uint8_t *)HID_ReportDescriptor;
len = sizeof(HID_ReportDescriptor);
break;
default:
return (FALSE);
}
编译下载到开发板上看看现象。
在设备管理器里就看到了有两个鼠标和两个USB输入设备!
第四章会说到USB设备的枚举过程。结合代码和数据手册可以更好的理解。不足之处还请多多指点。此帖会不断更新,以实现一个USB HID枚举的整个过程。附件有实现本章的代码!
开发环境:集成开发环境µVision4 IDE版本4.60.0.0。
主机系统:Microsoft Windows XP。
开发平台:旺宝悍马1788开发板。
|
|