3.9 配置描述符集合的结构
每个USB设备都至少有一个配置描述符,设备描述符中规定了该设备有多少种配置,每种配置都有一个描述符。
本USB实例中,只有一个配置描述符。
3.9.1 配置描述符的结构
USB协议规定的标准配置描述符的结构:
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
备注 |
1 |
0 |
bLength |
1 |
该描述符的长度(9字节) |
标准USB配置描述符的长度为9字节 |
2 |
1 |
bDescriptorType |
1 |
描述符的类型(配置描述符为0x02) |
|
3 |
2 |
wTotalLength |
2 |
配置描述符集合总长度 |
包括配置、接口、类特殊(如果有)和端点描述符,低字节在先 |
4 |
4 |
bNumInterfaces |
1 |
该配置支持的接口数 |
通常,功能单一的设备只有一个接口(例如鼠标),而复合设备则支持多个接口(例如音频设备) |
5 |
5 |
bConfigurationValue |
1 |
该配置的值 |
通常,一个USB设备可以支持多个配置,bConfigurationValue就是每个配置的标识。设置配置请求时会发送一个配置值,如果某个配置的bConfiguratonValue值与它相匹配,就表示该配置被激活,为当前配置。 |
6 |
6 |
iConfiguration |
1 |
描述该配置的字符串的索引值 |
如果该值为0,表示没有字符串 |
7 |
7 |
bmAttributes |
1 |
该配置的属性 |
Bit7:保留,必须设置为1; Bit6:表示供电方式,为1时,表示设备自供电;为0时,表示总线供电; Bit5:表示十分支持远程唤醒,为1时,表示支持; Bit4~0:保留,设置为0。 |
8 |
8 |
bMaxPower |
1 |
设备所需要从总线获取的最大电流(单位是2mA) |
例如,如果需要200mA的最大电流,则该字节的值为100。 |
3.9.2 接口描述符的结构
USB协议规定的标准接口描述符的结构(注:接口描述符不能单独返回,必须附着在配置描述符后一起返回):
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
备注 |
1 |
0 |
bLength |
1 |
该描述符的长度(9字节) |
标准USB接口 描述符的长度为9字节 |
2 |
1 |
bDescriptorType |
1 |
描述符的类型(接口描述符为0x04) |
|
3 |
2 |
bInterfaceNumber |
1 |
该接口的编号(从0开始) |
当一个配置具有多个接口时,每个接口的编号都不相同。从0开始一次递增对一个配置的接口进行编号。 |
4 |
3 |
bAlternateSetting |
1 |
该接口的备用编号 |
编号规则与bInterfaceNumber一样,很少使用,设置为0 |
5 |
4 |
bNumEndpoints |
1 |
该接口所使用的端点数 |
不包括0端点。如果该字段为0,说明没有非0端点,只使用默认的控制端点。 |
6 |
5 |
bInterfaceClass |
1 |
该接口所使用的类 |
和设备描述符中的意义类似。 通常在接口中定义设备的功能,而在设备描述符中将类、子类和协议字段的值设置为0。 |
7 |
6 |
bInterfaceSubClass |
1 |
该接口所使用的子类 |
8 |
7 |
bInterfaceProtocol |
1 |
该接口所使用的协议 |
9 |
8 |
iInterface |
1 |
描述该接口的字符串索引 |
如果该值为0,则表示没有字符串 |
3.9.3 端点描述符的结构
USB协议规定的标准端点描述符的结构(注:端点描述符不能单独返回,必须附着在配置描述符后一起返回):
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
备注 |
1 |
0 |
bLength |
1 |
该描述符的长度(7字节) |
标准USB端点描述符的长度为7字节 |
2 |
1 |
bDescriptorType |
1 |
描述符的类型(端点描述符为0x05) |
|
3 |
2 |
bEndpointAddress |
1 |
该端点的地址(端点传输方向+端点号) |
Bit7表示该端点传输方向,1为输入;0为输出。 Bit3~0为端点号;Bit6~4保留,为0. |
4 |
3 |
bmAttributes |
1 |
该端点的属性 |
Bit1~0为端点的传输类型,0位控制传输;1位等时传输;2为批量传输;3为中断传输。 如果该端点为非等时传输,则Bit7~2为保留值,设为0; 如果该端点为等时传输,则 Bit3~2表示同步的类型,0为无同步;1为异步;2为适配;3为同步。 Bit5~4表示用途,0为数据端点;1为反馈端点;2为暗含反馈的数据端点;3为保留值。 Bit7~6保留。 |
5 |
4 |
wMaxPackeSize |
2 |
该端点支持的最大包长度 |
对于全速模式和低速模式,Bit10~0表示端点的最大包长,其他位保留为0。对于高速模式,Bit12~11为每个帧附加的传输次数,具体参看USB2.0协议。 |
6 |
6 |
bInterval |
1 |
端点的查询时间 |
对于中断端点,表示查询的帧间隔数。 对于等时传输和高速模式的中断、批量传输,该字段的意义情参看USB2.0协议。 |
3.9.4 HID描述符的结构
USB鼠标是属于USB HID类的。查看USB HID类的官方文档,HID类的涉笔在配置描述符中还需要一个HID
描述符。它是一个类描述符,应该跟在接口描述符后面。
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
备注 |
1 |
0 |
bLength |
1 |
该描述符的长度 |
是该描述符的总长度。其大小和该描述符中下级描述符的个数有关。 例如,只有一个下级描述符时,总长度为1+1+2+1+1+1+2=9字节。 |
2 |
1 |
bDescriptorType |
1 |
描述符类型(HID描述符为0x21) |
|
3 |
2 |
bcdHID |
2 |
HID协议的版本 |
这里参看的是USB HID1.1协议,故此处为0x0110 |
4 |
4 |
bCountyCode |
1 |
国家代码 |
通常键盘是美式键盘,代码为33,即0x21 |
5 |
5 |
bNumDescriptors |
1 |
下级描述符的数量 |
该值至少为1,即至少要有一个报告描述符。 下级描述符可以是报告描述符或物理描述符。 |
6 |
6 |
bDescriptorType |
1 |
下级描述符的类型 |
报告描述符的编号为0x22,物理描述符的编号为0x23 |
7 |
7 |
wDescriptorLength |
2 |
下级描述符的长度 |
当有多个下级描述符时,bDescriptorType和bDescriptorLength交替重复下去。 |
8 |
9 |
bDescriptorType |
1 |
下级描述符的类型(可选) |
|
9 |
10 |
wDescriptorLength |
2 |
下级描述符的长度(可选) |
|
|
|
... |
... |
...(可选) |
|
3.10 配置描述符集合的实现和返回
通过前面的分析知道了配置描述符的集合的结构,下面用代码来实现一个配置描述符的集合,并在主机的
获取配置描述符请求中返回。
1)配置描述符集合:
//USB配置描述符集合的定义
//配置描述符总长度为9+9+9+7字节
code uint8 ConfigurationDescriptor[9+9+9+7]=
{
/***************配置描述符***********************/
//bLength字段。配置描述符的长度为9字节。
0x09,
//bDescriptorType字段。配置描述符编号为0x02。
0x02,
//wTotalLength字段。配置描述符集合的总长度,
//包括配置描述符本身、接口描述符、类描述符、端点描述符等。
sizeof(ConfigurationDescriptor)&0xFF, //低字节
(sizeof(ConfigurationDescriptor)>>8)&0xFF, //高字节
//bNumInterfaces字段。该配置包含的接口数,只有一个接口。
0x01,
//bConfiguration字段。该配置的值为1。
0x01,
//iConfigurationz字段,该配置的字符串索引。这里没有,为0。
0x00,
//bmAttributes字段,该设备的属性。由于我们的板子是总线供电的,
//并且我们不想实现远程唤醒的功能,所以该字段的值为0x80。
0x80,
//bMaxPower字段,该设备需要的最大电流量。由于我们的板子
//需要的电流不到100mA,因此我们这里设置为100mA。由于每单位
//电流为2mA,所以这里设置为50(0x32)。
0x32,
/*******************接口描述符*********************/
//bLength字段。接口描述符的长度为9字节。
0x09,
//bDescriptorType字段。接口描述符的编号为0x04。
0x04,
//bInterfaceNumber字段。该接口的编号,第一个接口,编号为0。
0x00,
//bAlternateSetting字段。该接口的备用编号,为0。
0x00,
//bNumEndpoints字段。非0端点的数目。由于USB鼠标只需要一个
//中断输入端点,因此该值为1。
0x01,
//bInterfaceClass字段。该接口所使用的类。USB鼠标是HID类,
//HID类的编码为0x03。
0x03,
//bInterfaceSubClass字段。该接口所使用的子类。在HID1.1协议中,
//只规定了一种子类:支持BIOS引导启动的子类。
//USB键盘、鼠标属于该子类,子类代码为0x01。
0x01,
//bInterfaceProtocol字段。如果子类为支持引导启动的子类,
//则协议可选择鼠标和键盘。键盘代码为0x01,鼠标代码为0x02。
0x02,
//iConfiguration字段。该接口的字符串索引值。这里没有,为0。
0x00,
/******************HID描述符************************/
//bLength字段。本HID描述符下只有一个下级描述符。所以长度为9字节。
0x09,
//bDescriptorType字段。HID描述符的编号为0x21。
0x21,
//bcdHID字段。本协议使用的HID1.1协议。注意低字节在先。
0x10,
0x01,
//bCountyCode字段。设备适用的国家代码,这里选择为美国,代码0x21。
0x21,
//bNumDescriptors字段。下级描述符的数目。我们只有一个报告描述符。
0x01,
//bDescritporType字段。下级描述符的类型,为报告描述符,编号为0x22。
0x22,
//bDescriptorLength字段。下级描述符的长度。下级描述符为报告描述符。
sizeof(ReportDescriptor)&0xFF,
(sizeof(ReportDescriptor)>>8)&0xFF,
/**********************端点描述符***********************/
//bLength字段。端点描述符长度为7字节。
0x07,
//bDescriptorType字段。端点描述符编号为0x05。
0x05,
//bEndpointAddress字段。端点的地址。我们使用D12的输入端点1。
//D7位表示数据方向,输入端点D7为1。所以输入端点1的地址为0x81。
0x81,
//bmAttributes字段。D1~D0为端点传输类型选择。
//该端点为中断端点。中断端点的编号为3。其它位保留为0。
0x03,
//wMaxPacketSize字段。该端点的最大包长。端点1的最大包长为16字节。
//注意低字节在先。
0x10,
0x00,
//bInterval字段。端点查询的时间,我们设置为10个帧时间,即10ms。
0x0A
};
2) 编造报告描述符
程序中需要知道报告描述符的大小,但是现在还未构造报告描述符,下面先构造一个假的报告描述符充一下数。
让程序先跑起来。
//USB报告描述符
code uint8 ReportDescriport[]=
{
0x00
};
3)对配置描述符返回的代码实现
代码位置:
在端点0输出中断处理函数
void UsbEp0Out()
的
获取配置描述符请求中
。
case CONFIGURATION_DESCRIPTOR: //配置描述符
#ifdef DEBUG0
Prints("配置描述符。\r\n");
#endif
pSendData=ConfigurationDescriptor; //需要发送的数据为配置描述符
//判断请求的字节数是否比实际需要发送的字节数多
//这里请求的是配置描述符集合,因此数据长度就是
//ConfigurationDescriptor[3]*256+ConfigurationDescriptor[2]。
//如果请求的比实际的长,那么只返回实际长度的数据
SendLength=ConfigurationDescriptor[3];
SendLength=SendLength*256+ConfigurationDescriptor[2];
if(wLength>SendLength)
{
if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
{
NeedZeroPacket=1; //需要返回0长度的数据包
}
}
else
{
SendLength=wLength;
}
//将数据通过EP0返回
UsbEp0SendData();
break;
4)分析调试信息
编译代码,下载运行,串口调试信息如下:
分析得知:
① 程序返回了9字节的配置描述符,其中0x22, 0x00表示描述符结合的总长度为34字节。
② 主机接着发送了获取字符串描述符的请求,且请求索引值为0。
这个请求不是请求索引为0的字符串,因为没有哪个字符串可以使用索引值0。这个请求是请求语言ID的。
接下来就要实现返回语言ID以及各字符串的请求了。
3.11 字符串和语言ID请求的实现
1)获取字符串和语言ID的原理
在USB协议中,字符串描述符时可选的。当某个描述符中的字符串索引值为非0时,就表示它具有那个字符
串描述符,注意,索引值不能重复。
在设备描述符中,申请了3个非0的索引值,分别是厂商、产品字符串以及
产品序列号,其索引值分贝为1,2,3。
USB主机使用获取字符串描述符和索引值来获取对应的字符串。当索引值为0时,表示获取语言ID。语言ID是
一个描述该设备支持的语言种类的数组,每个ID号占2字节。
语言ID描述符结构:
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
1 |
0 |
bLength |
1 |
描述符的长度 |
2 |
1 |
bDescriptorType |
1 |
描述符类型(字符串为0x03) |
3 |
2 |
wLANGID[0] |
2 |
语言ID号0 |
4 |
... |
... |
... |
... |
3+n |
2*n+2 |
wLANGID[n] |
2 |
语言ID号n |
语言ID,这里只使用美式英语一种,即0x0409.
字符串描述符结构:
序号 |
偏移量/字节 |
域 |
大小/字节 |
说明 |
1 |
0 |
bLength |
1 |
描述符的长度 |
2 |
1 |
bDescriptorType |
1 |
描述符类型(字符串为0x03) |
3 |
2 |
bString |
N |
UNICODE编码的字符串 |
字符串描述符中的bString字段是使用UNICODE编码的字符串。UNICODE用2字节来表示一个字符,
如果是英文字符,则直接在ASCII码前补1字节的0扩充为2字节的UNICODE码。通常在程序中直接用
双引号引起来的字符串使用的是GB码,它跟UNICODE编码没有什么规律。
2)如何通过代码获取字符串和语言ID
如何编写语言ID和字符串
为了方便使用,圈圈用Java脚本写了专门生成字符串描述符的小工具,可以直接在里面输入需要的字符串,然后
点击“ 转换 ”按钮,就可以生成所需要的字符串描述符了。
下面是语言ID和厂商字符串、产品字符串和产品序列号的定义:
/************************语言ID的定义********************/
code uint8 LanguageId[4]=
{
0x04, //本描述符的长度
0x03, //字符串描述符
//0x0409为美式英语的ID
0x09,
0x04
};
-
//厂商字符串“电脑圈圈的USB专区 Http://group.ednchina.com/93/”的Unicode编码
//8位小端格式
code uint8 ManufacturerStringDescriptor[82]={
82, //该描述符的长度为82字节
0x03, //字符串描述符的类型编码为0x03
0x35, 0x75, //电
0x11, 0x81, //脑
0x08, 0x57, //圈
0x08, 0x57, //圈
0x84, 0x76, //的
0x55, 0x00, //U
0x53, 0x00, //S
0x42, 0x00, //B
0x13, 0x4e, //专
0x3a, 0x53, //区
0x20, 0x00, //
0x48, 0x00, //H
0x74, 0x00, //t
0x74, 0x00, //t
0x70, 0x00, //p
0x3a, 0x00, //:
0x2f, 0x00, ///
0x2f, 0x00, ///
0x67, 0x00, //g
0x72, 0x00, //r
0x6f, 0x00, //o
0x75, 0x00, //u
0x70, 0x00, //p
0x2e, 0x00, //.
0x65, 0x00, //e
0x64, 0x00, //d
0x6e, 0x00, //n
0x63, 0x00, //c
0x68, 0x00, //h
0x69, 0x00, //i
0x6e, 0x00, //n
0x61, 0x00, //a
0x2e, 0x00, //.
0x63, 0x00, //c
0x6f, 0x00, //o
0x6d, 0x00, //m
0x2f, 0x00, ///
0x39, 0x00, //9
0x33, 0x00, //3
0x2f, 0x00 ///
};
//字符串“《圈圈教你玩USB》之USB鼠标”的Unicode编码
//8位小端格式
code uint8 ProductStringDescriptor[34]={
34, //该描述符的长度为34字节
0x03, //字符串描述符的类型编码为0x03
0x0a, 0x30, //《
0x08, 0x57, //圈
0x08, 0x57, //圈
0x59, 0x65, //教
0x60, 0x4f, //你
0xa9, 0x73, //玩
0x55, 0x00, //U
0x53, 0x00, //S
0x42, 0x00, //B
0x0b, 0x30, //》
0x4b, 0x4e, //之
0x55, 0x00, //U
0x53, 0x00, //S
0x42, 0x00, //B
0x20, 0x9f, //鼠
0x07, 0x68 //标
};
//字符串“2008-07-07”的Unicode编码
//8位小端格式
code uint8 SerialNumberStringDescriptor[22]={
22, //该描述符的长度为22字节
0x03, //字符串描述符的类型编码为0x03
0x32, 0x00, //2
0x30, 0x00, //0
0x30, 0x00, //0
0x38, 0x00, //8
0x2d, 0x00, //-
0x30, 0x00, //0
0x37, 0x00, //7
0x2d, 0x00, //-
0x30, 0x00, //0
0x37, 0x00 //7
};
如何返回语言ID和字符串
再回到端点0输出中断处理的函数
void UsbEp0Out()中,增加对获取描述符的相关处理。主要是对wValue的低字
节进行散转,以判断返回哪个具体的字符串描述符。
代码如下:
case STRING_DESCRIPTOR: //字符串描述符
#ifdef DEBUG0
Prints("字符串描述符");
#endif
switch(wValue&0xFF) //根据wValue的低字节(索引值)散转
{
case 0: //获取语言ID
#ifdef DEBUG0
Prints("(语言ID)。\r\n");
#endif
pSendData=LanguageId;
SendLength=LanguageId[0];
break;
case 1: //厂商字符串的索引值为1,所以这里为厂商字符串
#ifdef DEBUG0
Prints("(厂商描述)。\r\n");
#endif
pSendData=ManufacturerStringDescriptor;
SendLength=ManufacturerStringDescriptor[0];
break;
case 2: //产品字符串的索引值为2,所以这里为产品字符串
#ifdef DEBUG0
Prints("(产品描述)。\r\n");
#endif
pSendData=ProductStringDescriptor;
SendLength=ProductStringDescriptor[0];
break;
case 3: //产品序列号的索引值为3,所以这里为序列号
#ifdef DEBUG0
Prints("(产品序列号)。\r\n");
#endif
pSendData=SerialNumberStringDescriptor;
SendLength=SerialNumberStringDescriptor[0];
break;
default :
#ifdef DEBUG0
Prints("(未知的索引值)。\r\n");
#endif
//对于未知索引值的请求,返回一个0长度的包
SendLength=0;
NeedZeroPacket=1;
break;
}
//判断请求的字节数是否比实际需要发送的字节数多
//如果请求的比实际的长,那么只返回实际长度的数据
if(wLength>SendLength)
{
if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
{
NeedZeroPacket=1; //需要返回0长度的数据包
}
}
else
{
SendLength=wLength;
}
//将数据通过EP0返回
UsbEp0SendData();
break;
3)分析调试信息
将程序编译下载后,串口显示的调试信息如下:
由于调试信息太长,这里就不给出了。
② 主机在获取语言ID后,接着获取了产品序列号。
③ 然后再一次获取配置描述符,这次使用的长度为0xFF,所以返回了全部的配置描述符集合。
④ 然后再次获取语言ID和产品序列号字符串。
⑤ 接着又获取了设备描述符和配置描述符集合,最后发送了设置配置的请求。
3.12 设置配置请求的实现
1)设置配置请求的原理
比较简单,它是一个输出请求,只要根据所请求的配置值,使能相应的端点即可。
由于我们的鼠标只有一个配置,所以连配置值都可以忽略,直接使能端点,然后返回一个0长度的状态数据包即可。
注:只有收到非0的配置值后才可以使能非0端点,否则禁止用非0端点。
2)如何在D12中设置配置请求
使能D12的非0端点,要用到D12的SetEndpointEable命令,代码是0xD8,后面跟1字节数据写入。该字节的最低
位D0为1时,使能端点。其他位保留,为0。该命令只有在设置地址命令时使能了设备(D7为1)后才能起作用。这一步
前面已经做过了。
使能端点的函数如下:
//函数功能:使能端点函数。
//入口参数:Enable: 是否使能。0值为不使能,非0值为使能。
void D12SetEndpointEnable(uint8 Enable)
{
D12WriteCommand(D12_SET_ENDPOINT_ENABLE);
if(Enable!=0)
{
D12WriteByte(0x01); //D0为1使能端点
}
else
{
D12WriteByte(0x00); //不使能端点
}
}
3)何时何地设置配置
在端点0输出中断处理函数
void UsbEp0Out()
的设置配置中,增加使能端点的代码以及返回0长度状态数据包等处理。
case SET_CONFIGURATION: //设置配置
#ifdef DEBUG0
Prints("设置配置。\r\n");
#endif
//使能非0端点。非0端点只有在设置为非0的配置后才能使能。
//wValue的低字节为配置的值,如果该值为非0,才能使能非0端点。
//保存当前配置值
ConfigValue=wValue&0xFF;
D12SetEndpointEnable(ConfigValue);
//返回一个0长度的状态数据包
SendLength=0;
NeedZeroPacket=1;
//将数据通过EP0返回
UsbEp0SendData();
break;
4)分析调试信息
编译下载,返回如下调试信息:
① 主机发送了一个bRequest为0x0A的类输出请求。
② 查看HID协议文档,0x0A的请求是Set_Idle请求。这个请求告诉设备,在没有新事件发生时,不要从中断
端点返回数据。
对于我们的USB鼠标来说,收到这个请求可以什么都不做,直接返回一个0长度的状态数据包即可。
5)何时何地回应类输出请求
在端点0输出中断处理函数
void UsbEp0Out()
的
类输出请求中,返回一个0长度的状态数据包
case 1: //类请求
#ifdef DEBUG0
Prints("USB类输出请求:");
#endif
switch(bRequest)
{
case SET_IDLE:
#ifdef DEBUG0
Prints("设置空闲。\r\n");
#endif
//只需要返回一个0长度的数据包即可
SendLength=0;
NeedZeroPacket=1;
//将数据通过EP0返回
UsbEp0SendData();
break;
default:
#ifdef DEBUG0
Prints("未知请求。\r\n");
#endif
break;
}
break;
6)分析调试信息
编译下载,返回的调试信息如下:
① 主机发送了一个获取描述符的请求,接受者是接口(bmRequestType的D4~D0位值为1),请求的描述符代码为0x22。
② 0x22是HID协议中定义的报告描述符。
③ 为了简化代码,此处没有判断请求的接收者,仅靠描述符代码来区分。
④ 这里请求的长度怎么是0x0041呢?暂时不知道,只要她请求的长度比实际的报告描述符大就够了,因为程序不
会返回过多的描述符数据。
下面将实现这个报告描述符。
3.13 报告描述符的结构及实现
1)什么是报告
USB HID设备是通过报告(report)来传送数据的,报告有输入和输出之分。
输入报告是USB设备发送给主机的,例如:USB鼠标将鼠标移动和鼠标点击等信息返回给计算机,键盘将按键数据返回给计算机等。
输出报告是主机发送给USB设备的,例如:键盘上的数字键盘锁定灯和大写字母锁定灯的控制等。
报告里面包含的是所要传送的数据,数量为整数字节,被划分成一个个域。
2)怎么传送报告
输入报告是通过中断
输入端点返回的;
输出报告分不同情况,当没有中断输出端点时,可以
通过控制端点0发送;当有中断输
出端点时,通过中断输出端点发出。
当然,不管设备是否具有中断输出端点(中断输入端点是必须要的),主机都可以通过获取报告和设置
报告的请求从端点0来获取或发送报告。
3)什么是报告描述符,有什么作用,怎么传送
报告描述符(report descriptor),是用来描述一个报告的结构以及该报告里面的数据是用来干什么用的。
通过报告描述符,USB主机可以分析出报告里面的数据所表示的意思。
报告描述符与普通描述符一样,都是通过控制输入端点0来返回,主机使用获取报告描述符请求来获取报告描述符。注意这个请求是发送到接口的,而不是到设备的。
4)报告描述符的结构是怎么样的
一个报告描述符可以描述多个报告,不同的报告通过报告ID来识别。报告ID放在报告的最前面,即第一个字节。
当报告描述符中没有规定报告ID时,报告中没有ID字段,开始就是数据。
报告描述符与前面遇到的描述符结构不同,没有描述符长度和描述符类型等信息,而是由一个个条目(item)组
成的。通常,在写报告描述符时,一个条目占据一行,这样看起来清晰一些。
HID协议中规定了两种条目:短条目和长条目。长条目很少用,这里只介绍短条目的结构。
短条目由1字节的前缀+可选的数据字节组成。可选的数据字节可以为0字节、1字节、2字节或者4字节。实际
使用的条目,大部分是只有1字节可选数据的,少数会使用0字节或2字节数据。
条目前缀结构如下图所示:
Bit1~0为bSize,用来表示后面所跟数据的字节数,0为0字节,1为1字节,2为2字节,3为4字节;
Bit3~0为bType,表示条目的类型,0为主(main)条目,1为全局(global)条目,2为局部(local)条目,3为保留值;
Bit7~
4为bTag,表示该条目的功能,具体参看HID协议和HID用途表。
主条目、全局条目和局部条目的对比:
序号 |
条目分类 |
作用 |
内部分类 |
作用 |
备注 |
1 |
主条目 |
用来定义或者分组报告的数据域 |
输入(Input) |
将输入报告划分为不同的数据域,以及指定该域的属性 |
后面跟的第一字节数据每个位的数据表示一种属性,例如: Bit0表示该数据域是变量还是常量; Bit1表示是数组还是单一变量; Bit2表示是相对值还是绝对值等。 |
输出(Output) |
|
特性(Feature) |
|
集合(Collection) |
|
|
关集合(End Collection) |
|
|
2 |
全局条目 |
用来选择用途页,定义数据域的长度、数量、报告ID等。 全局条目出现后对接下来的所有主条目都有效,除非遇到另一个全局条目来改变它。 |
用途页(Usage Page) |
指定设备的功能,相当于HID的子集 |
|
逻辑最小值(Logical Minimum) |
描述数据域的取值范围 |
|
逻辑最大值(Logical Maximum) |
|
物理最小值(Physical Minimum) |
|
|
物理最大值(Physical Maximum) |
|
|
数据域大小(Report Size) |
描述某个数据域有多少个位 |
|
数据域数量(Report Count) |
描述这样的数据域有多少个 |
|
报告ID(Report ID) |
|
|
3 |
局部条目 |
用来定义控制的特性 只在局部有效,遇到一个主条目后,它的效用就结束了。 |
用途(Usage) |
指定个别报表的功能,相当于Usage Page的子集 |
|
用途最小值(Usage Minimum) |
|
|
用途最大值(Usage Maximum) |
|
|
5)如何生成报告描述符
可以对照协议来设计报告描述符,也可以使用USB官方网站提供的HID描述符工具来生成;还可以用现成的
报告描述符修改,例如,在HID协议以及用图表文档中,就有很多现成的例子。
6)USB鼠标的报告描述符代码实现
//USB报告描述符的定义
code uint8 ReportDescriptor[]=
{
//每行开始的第一字节为该条目的前缀,前缀的格式为:
//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)
//这是一个局部条目。说明用途为指针集合
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表示
//这些数据域是独立的,每个域表示一个意思。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)
//这是一个全局条目,选择用途页为普通桌面Generic Desktop Page(0x01)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
//这是一个局部条目,说明用途为X轴
0x09, 0x30, // USAGE (X)
//这是一个局部条目,说明用途为Y轴
0x09, 0x31, // USAGE (Y)
//这是一个局部条目,说明用途为滚轮
0x09, 0x38, // USAGE (Wheel)
//下面两个为全局条目,说明返回的逻辑最小和最大值。
//因为鼠标指针移动时,通常是用相对值来表示的,
//相对值的意思就是,当指针移动时,只发送移动量。
//往右移动时,X值为正;往下移动时,Y值为正。
//对于滚轮,当滚轮往上滚时,值为正。
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
//这是一个全局条目,说明数据域的长度为8bit。
0x75, 0x08, // REPORT_SIZE (8)
//这是一个全局条目,说明数据域的个数为3个。
0x95, 0x03, // REPORT_COUNT (3)
//这是一个主条目。它说明这三个8bit的数据域是输入用的,
//属性为:Data,Var,Rel。Data说明数据是可以变的,Var说明
//这些数据域是独立的,即第一个8bit表示X轴,第二个8bit表示
//Y轴,第三个8bit表示滚轮。Rel表示这些值是相对值。
0x81, 0x06, // INPUT (Data,Var,Rel)
//下面这两个主条目用来关闭前面的集合用。
//我们开了两个集合,所以要关两次。bSize为0,所以后面没数据。
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
};
//通过上面的报告描述符的定义,我们知道返回的输入报告具有4字节。
//第一字节的低3位用来表示按键是否按下的,高5位为常数0,无用。
//第二字节表示X轴改的变量,第三字节表示Y轴的改变量,第四字节表示
//滚轮的改变量。我们在中断端点1中应该要按照上面的格式返回实际的
//鼠标数据。
通过上面的报告描述符定义可知,返回的输入报告具有4字节。
第一字节的低3位用来表示按键是否按下,
高5位为常数0,无用;
第二字节表示X轴的该变量;
第三字节表示Y轴的该变量;
第四字节表示滚轮的改变量。
在中断输入端点1中按照上面的格式返回实际的鼠标数据。
7)如何返回报告描述符
到获得描述符的处理中,增加对获取报告描述符请求的处理代码。
在
端点0输出中断处理函数
void UsbEp0Out()
的
获取描述符的switch散转代码中,增加以下代码:
case REPORT_DESCRIPTOR: //报告描述符
#ifdef DEBUG0
Prints("报告描述符。\r\n");
#endif
pSendData=ReportDescriptor; //需要发送的数据为报告描述符
SendLength=sizeof(ReportDescriptor); //需要返回的数据长度
//判断请求的字节数是否比实际需要发送的字节数多
//如果请求的比实际的长,那么只返回实际长度的数据
if(wLength>SendLength)
{
if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
{
NeedZeroPacket=1; //需要返回0长度的数据包
}
}
else
{
SendLength=wLength;
}
//将数据通过EP0返回
UsbEp0SendData();
break;
8)分析调试信息
编译下载,可以看到下面的调试信息:
① 设备成功返回了0x34(16+16+16+4)个字节的报告描述符。接下来就没动静了,主机不再发送请求,
枚举过程完成!
② 接下来主机应该一直查询端点1,以读取鼠标返回的报告,这里看不到动作,是因为端点1还没有成功发送数据。
下图是设备管理器中的鼠标信息:
接下来的工作是扫描鼠标的动作,然后将数据通过端点1返回。
3.14 报告的返回
通过前面报告描述符的定义可知,要返回4字节的报告数据。
1)何时返回报告
报告也不是随时都能返回的,只有在设置非0配置之后,
才能将数据写到端点1中返回。怎么知道已经
进行设置配置了呢?
可以增加一个变量ConfigValue,初始化为0。在收到设置配置请求后,将配置值赋
给它,如果是非0配置,可以返回报告数据了。
2)如何返回报告(返回报告的原理)
发送到端点1去之前,需要检查端点1是否处于忙状态,即里面是否还有数据未发送出去。为此增加一个
Ep1InIsBusy的标志,来决定端点1输入缓冲是否空闲。如果空闲,则可以发送数据,将数据写入缓冲区后
设置Ep1InIsBusy为真(即端点忙)。
然后,在端点1输入中断处理中(此时数据已经发送完毕),将Ep1InIsBusy设置为假(即端点已空闲)。
端点1输入中断的处理函数:
//函数功能:端点0输入中断处理函数。
void UsbEp0In(void)
{
#ifdef DEBUG0
Prints("USB端点0输入中断。\r\n");
#endif
//读最后发送状态,这将清除端点0的中断标志位
D12ReadEndpointLastStatus(1);
//发送剩余的字节数
UsbEp0SendData();
}
在设备
复位处理中,将Ep1InIsBusy设置为假,即设备复位后,端点1处于空闲状态。
总线复位处理的函数:
//函数功能:总线复位中断处理函数。
void UsbBusReset(void)
{
#ifdef DEBUG0
Prints("USB总线复位。\r\n");
#endif
Ep1InIsBusy=0; //复位后端点1输入缓冲区空闲。
}
3)返回报告的代码实现
在main函数的主循环中(判断是否有中断发生后),增加判断是否返回报告的代码,如下所示:
if(ConfigValue!=0) //如果已经设置为非0的配置,则可以返回报告数据
{
LEDs=~KeyPress; //利用板上8个LED显示按键状态,按下时亮
if(!Ep1InIsBusy) //如果端点1输入没有处于忙状态,则可以发送数据
{
KeyCanChange=0; //禁止按键扫描
if(KeyUp||KeyDown||KeyPress) //如果有按键事件发生
{
SendReport(); //则返回报告
}
KeyCanChange=1; //允许按键扫描
}
}
4)按键和返回报告的对应关系
上面的SendReport()函数根据当前的按键情况返回报告。
本开发板有8个按键,功能能为:KEY1为光标左移,KEY2为光标右移,KEY3为光标上移,KEY4
为光标下移,KEY5为滚轮下滚,KEY6为滚轮上滚,KEY7为鼠标左键,KEY8为鼠标右键。
光标左移时,X轴为负,右移时,X轴为正;
光标下移时,Y轴为正,上移时,Y轴为负。
滚轮下滚时,为负,上滚时,为正。
左键或右键按下时,对应的位为1。
该函数中先判断是否需要返回报告,只有当光标或滚轮滚动(KEY1
~6按住)或者左、右键(KEY7~8)状态
改变时,才需要返回报告。
对于移动和滚动,每次返回为1个单位,如果觉得移动速度慢,可以增大该值,以提高灵敏度。
使用完KeyUp和KeyDown后,记得清零它们。
最终发送的报告函数代码实现:
//函数功能:根据按键情况返回报告的函数。
void SendReport(void)
{
//需要返回的4字节报告的缓冲
//Buf[0]的D0就是左键,D1就是右键,D2就是中键(这里没有)
//Buf[1]为X轴,Buf[2]为Y轴,Buf[3]为滚轮
uint8 Buf[4]={0,0,0,0};
//我们不需要KEY1~KEY6按键改变的信息,所以先将它们清0
KeyUp &=~(KEY1|KEY2|KEY3|KEY4|KEY5|KEY6);
KeyDown &=~(KEY1|KEY2|KEY3|KEY4|KEY5|KEY6);
//如果有按键按住,并且不是KEY7、KEY8(左、右键)
//或者KEY7、KEY8任何一个键有变动的话,则需要返回报告
if((KeyPress&(~(KEY7|KEY8)))||KeyUp||KeyDown)
{
if(KeyPress & KEY1) //如果KEY1按住,则光标需要左移,即X轴为负值。
{
Buf[1]=-1; //这里一次往左移动一个单位。
}
if(KeyPress & KEY2) //如果KEY2按住,则光标需要右移,即X轴为正值。
{
Buf[1]=1; //这里一次往右移动一个单位。
}
if(KeyPress & KEY3) //如果KEY3按住,则光标需要上移,即Y轴为负值。
{
Buf[2]=-1; //这里一次往上移动一个单位。
}
if(KeyPress & KEY4) //如果KEY4按住,则光标需要下移,即Y轴为正值。
{
Buf[2]=1; //这里一次往下移动一个单位。
}
if(KeyPress & KEY5) //如果KEY5按住,则滚轮下滚,即滚轮值为负。
{
Buf[3]=-1; //这里一次往下滚动一个单位。
}
if(KeyPress & KEY6) //如果KEY6按住,则滚轮上滚,既滚轮值为正
{
Buf[3]=1; //这里一次往上滚动一个单位。
}
if(KeyPress & KEY7) //鼠标左键
{
Buf[0]|=0x01; //D0为鼠标左键
}
if(KeyPress & KEY8) //鼠标右键
{
Buf[0]|=0x02; //D1为鼠标右键
}
//报告准备好了,通过端点1返回,长度为4字节。
D12WriteEndpointBuffer(3,4,Buf);
Ep1InIsBusy=1; //设置端点忙标志。
}
//记得清除KeyUp和KeyDown
KeyUp=0;
KeyDown=0;
}
到这里,这个USB实例算是完成了,虽然距离实际产品还有一段距离,但实现了鼠标基本功能。
程序中还有其他的一些标准请求、类请求等,还没有相关代码处理。例如,增加请求中接收者的
判断,增加对其他请求的响应等。对于其他请求,很少用到,所以不管是输入请求还是输出请求,只
要简单地返回0长度数据包即可。
当调试完毕,不需要调试信息时,通过config.h中的宏定义来屏蔽调试信息。去掉调试信息后,鼠
标移动速度会快很多。
按下KEY1后,返回的数据:
3.16本章小结
本章一步步介绍了:
USB鼠标实例的实现,
对D12芯片命令的使用,
USB标准请求的格式,
USB各种描述符和描述符的返回等。