3.5 USB标准请求
标准请求的用途:
主要用于设备的枚举过程。
标准请求的内容:
8字节的数据,包含了数据过程所需要传输的数据传输方向、长度、数据类型等信息。
正式由于8字节标准请求的原因,USB协议规定,端点0的最大包长至少为8字节。即任何USB设备都能够(且必须要)接收8字节的标准请求。
标准请求的传输过程:
在控制传输的建立过程通过默认控制端点0发出的。
3.5.1 USB标准设备请求的数据结构
1) USB协议规定的标准设备请求的数据结构
2)
USB协议定义了11个标准请求(bRequest),其
名字
和
请求代码
如下表所示:
3)各种标准请求的结构及需要传输的数据
不同的请求对于其接收者、wValue和wIndex,其各字段的意义是不一样的。
下表是各种标准请求的结构和数据过程需要传输的数据。
其中第一列有的有多个,主要是最低5位不同,即表示接收者不同。
有的请求只能发送到设备,而有的请求可以发送到设备、接口和端点。
常用的请求为:GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION。
3.5.2 GET_DESCRIPTOR请求
GET_DESCRIPOTOR(获取描述符)请求是在枚举过程中用的最多的一个请求。主机通过发送获取描述符
请求读取设备的各种描述符,从而可以获知设备类型、端点情况等众多重要信息。
获取描述符的接收者只能是设备,从bmRequestType的Bit7可以看出,它是请求数据输入的。
bRequest的值为0x06(GET_DESCRIPTOR)。
GET_DESCRIPTOR请求的结构和需要传输的数据如下表所示:
wValue域的第一字节(低字节)表示的是索引号,用来选择同一种描述符(例如字符串描述符和配置描述符)中具体的某个描述符。
wValue域的第二字节表示描述符的类型编号。
wLength域为请求设备返回数据的字节数,设备实际返回的字节数可以比该域指定的字节数少。
对于全速和低速模式,获取描述符的标准请求只有三种:获取设备描述符、获取配置描述符和获取字符串描述符。
另外的接口描述符和端点描述符是跟随配置描述符一并返回的,不能单独请求返回(如果单独返回,主机无
法确认它们属于哪个配置)。
前面的调试信息中,收到的数据为
0x80 0x06 0x00 0x01 0x00 0x00 0x40 0x00,对照标准请求的结构可
知,它是一个请求设备描述符的标准请求,请求的数据长度为64字节。
3.5.3 SET_ADDRESS请求
设置地址请求的作用:
主机指定设备使用指定的地址。
如何设置地址:
指定的地址包含在8字节数据中的wValue字段中。
在设备使用默认地址0时,先根据需要发送设备描述符,之后主机发送设置地址请求。
设置地址请求没有数据过程,故wLength值为0。wIndex也用不到,为0。
设备收到设置地址请求后,直接进入状态过程,等待主机读取0长状态数据包。
主机成功读取到状态数据包(用ACK响应设备)后,设备将启用新的地址。这以后的传输中,主机就将
使用新的地址与设备进行通信。
SET_ADDRESS请求的结构:
3.5.4 SET_CONFIGURATION请求
如何设置配置:
设置配置的请求和设置地址请求类似,区别在于wValue域的意义。
在设置地址请求中,wValue第一字节(低字节)为设备地址;
在设置配置请求中,wValue第一字节为配置的值。
当该值与某配置描述符中的配置编号一致时,表示
选中该配置。该值通常为1,因为大多数USB设备只有一种配置,配置编号为1;如果该值为0,则让设备进
入设置地址状态。
设备只有在收到非0的配置值之后,才能启用它的非0端点。
SET_CONFIGURATION请求的结构:
3.6 设备描述符的实现
已知每个设备都必须有且只有一个设备描述符,它的结构如下表所示。
USB鼠标实例的设备描述符:
//USB设备描述符的定义
code uint8 DeviceDescriptor[0x12]= //设备描述符为18字节
{
0x12,
//bLength字段。设备描述符的长度为18(0x12)字节0x01,
//bDescriptorType字段。设备描述符的编号为0x010x10,
//bcdUSB字段。这里设置版本为USB1.1,即0x0110。
由于是小端结构,所以低字节在先,即0x10,0x01。0x01,
0x00,
//bDeviceClass字段。我们不在设备描述符中定义设备类,
而在接口描述符中定义设备类,所以该字段的值为0。0x00,
//bDeviceSubClass字段。bDeviceClass字段为0时,该字段也为0。
0x00,
//bDeviceProtocol字段。bDeviceClass字段为0时,该字段也为0。
0x10,
//bMaxPacketSize0字段。PDIUSBD12的端点0大小的16字节。
0x88,
//idVender字段。厂商ID号,我们这里取0x8888,仅供实验用。
实际产品不能随便使用厂商ID号,必须跟USB协会申请厂商ID号。注意小端模式,低字节在先。0x88,
0x01,
//idProduct字段。产品ID号,由于是第一个实验,我们这里取0x0001。
注意小端模式,低字节应该在前。0x00,
0x00,
//bcdDevice字段。我们这个USB鼠标刚开始做,就叫它1.0版吧,即0x0100。
小端模式,低字节在先。0x01,
0x01,
//iManufacturer字段。厂商字符串的索引值,为了方便记忆和管理,
字符串索引就从1开始吧。
0x02,
//iProduct字段。产品字符串的索引值。刚刚用了1,这里就取2吧。
注意字符串索引值不要使用相同的值。
0x03,
//iSerialNumber字段。设备的序列号字符串索引值。
这里取3就可以了。
0x01
//bNumConfigurations字段。该设备所具有的配置数。
我们只需要一种配置就行了,因此该值设置为1。};
3.7设备描述符的返回
1. 如何返回设备描述符
通过控制输入端点0返回。
1)返回设备描述符的大致流程:
在端点0的输出中断处理函数中,
先对接收到的建立过程的数据进行判断,
如果是获取设备描述符的请求,
则将设备描述符的内容写入端点0输入缓冲区中,并使能端点发送。
当主机在下一次发送IN令牌包后,D12将会自动将端点0输入缓冲区中的数据返回给主机,这样就实
现了获取设备描述符的请求。
2)如何将数据写入端点缓冲区:
① 选择端点;
② 写数据到端点缓冲区
Write Buffer命令
,命令代码是0xF0。
写入的数据结构:1(0x00)+1(len)+n(data)。
③ 将端点的发送缓冲区中的数据设置为有效,数据才在主机发送IN令牌包后发送出去。
Validate Buffer命令,命令代码为0xFA。
3)将数据写入缓冲区的代码:
- //函数功能:将数据写入端点缓冲区函数。
//入口参数:Endp:端点号;Len:需要发送的长度;Buf:保存数据的缓冲区。
//返 回:Len的值。
uint8 D12WriteEndpointBuffer(uint8 Endp,uint8 Len,uint8 * Buf)
{
uint8 i;
D12SelectEndpoint(Endp); //选择端点
D12WriteCommand(D12_WRITE_BUFFER); //写Write Buffer命令
D12WriteByte(0); //该字节必须写0
D12WriteByte(Len); //写需要发送数据的长度
#ifdef DEBUG1 //如果定义了DEBUG1,则需要显示调试信息
Prints("写端点");
PrintLongInt(Endp/2); //端点号。由于D12特殊的端点组织形式,
//这里的0和1分别表示端点0的输出和输入;
//而2、3分别表示端点1的输出和输入;
//3、4分别表示端点2的输出和输入。
//因此要除以2才显示对应的端点。
Prints("缓冲区");
PrintLongInt(Len); //写入的字节数
Prints("字节。\r\n");
#endif
D12SetPortOut(); //将数据口设置为输出状态(注意这里为空宏,移植时可能有用)
for(i=0;i<Len;i++)
{
//这里不直接调用写一字节的函数,而直接在这里模拟时序,可以节省时间
D12ClrWr(); //WR置低
D12SetData(*(Buf+i)); //将数据放到数据线上
D12SetWr(); //WR置高,完成一字节写
#ifdef DEBUG1
PrintHex(*(Buf+i)); //如果需要显示调试信息,则显示发送的数据
if((i+1)%16==0)Prints("\r\n"); //每16字节换行一次
#endif
}
#ifdef DEBUG1
if((Len%16)!=0)Prints("\r\n"); //换行
#endif
D12SetPortIn(); //数据口切换到输入状态
D12ValidateBuffer(); //使端点数据有效
return Len; //返回Len
}
4)使能端点缓冲区的代码
//函数功能:使能发送端点缓冲区数据有效的函数。
//备 注:只有使用该函数使能发送端点数据有效之后,数据才能发送出去。
********************************************************************/
void D12ValidateBuffer(void)
{
D12WriteCommand(D12_VALIDATE_BUFFER);
}
5
)返回设备描述符的详细流程:
分析建立过程数据包的内容,以确定何时返回设备描述符。
分析数据包的内容主要使用if和switch语句来散转,对不同的请求做不同的处理。
在分析数据前,先申请一些变量,用来保存这些标准请求的不同字段。另外,还需要一个用来保存发送位
置的指针变量和一个保存剩余字节数的变量,以及一个是否需要返回0长度数据包的标志。
变量定义代码如下:
idata uint8 Buffer[16]; //读端点0用的缓冲区
//USB设备请求的各字段
uint8 bmRequestType;
uint8 bRequest;
uint16 wValue;
uint16 wIndex;
uint16 wLength;
uint8 * pSendData;//当前发送数据的位置
uint16 SendLength;//需要发送数据的长度
uint8 NeedZeroPacket;
//是否需要发送0数据包的标志。在USB控制传输的数据过程中,当返回的数据包字节数少于最大包长时,会认为数据过程结束。
//当请求的字节数比实际需要返回的字节数长,而实际返回的字节数又刚好是端点0大小的整数倍时,就需要返回一个0长度的
//数据包来结束数据过程。因此这里增加一个标志,供程序决定是否需要返回一个0长度的数据包。
接着,将从缓冲区中读出的数据分别添加到设备请求的各字段中
//将缓冲数据填到设备请求的各字段中
bmRequestType=Buffer[0];
bRequest=Buffer[1];
wValue=Buffer[2]+(((uint16)Buffer[3])<<8);
wIndex=Buffer[4]+(((uint16)Buffer[5])<<8);
wLength=Buffer[6]+(((uint16)Buffer[7])<<8);
具体散转顺序:
首先,判断
bmRequestType的Bit7,如果是1,则说明是输入请求;如果是0,则说明是输出请求。
再判断
Bit6~5,如果是00,则说明是标准请求;如果是01,则说明是类请求;如果是10,则说明是厂商请求。
再写上所有的标准请求情况,如果是获取描述符的请求,则对bRequest进行散转,看具体是请求什么描述符;
如果是获取字符串描述符,则还需要对wValue值的低字节散转,看是哪个字符串描述符。
散转的代码实现:
//判断具体的请求,根据不同的请求进行相关操作。如果D7位为1,则说明是输入请求。
//还需要对接收者进行散转,因为不同的请求接收者不同。接收者在bmRequestType的Bit4~0中定义。
//此处为了简化操作,省略了对接收者的判断。例如获取描述符的请求,只根据描述符的类型来区别。
//下面的代码判断具体的请求,并根据不同的请求进行相关操作,如果D7位为1,则说明是输入请求
if((bmRequestType&0x80)==0x80)
{
//根据bmRequestType的D6~5位散转,D6~5位表示请求的类型
//0为标准请求,1为类请求,2为厂商请求。
switch((bmRequestType>>5)&0x03)
{
case 0: //标准请求
#ifdef DEBUG0
Prints("USB标准输入请求:");
#endif
//USB协议定义了几个标准输入请求,我们实现这些标准请求即可
//请求的代码在bRequest中,对不同的请求代码进行散转
switch(bRequest)
{
case GET_CONFIGURATION: //获取配置
#ifdef DEBUG0
Prints("获取配置。\r\n");
#endif
break;
case GET_DESCRIPTOR: //获取描述符
#ifdef DEBUG0
Prints("获取描述符——");
#endif
/*对具体描述符的散转和描述符的返回*/
break;
case GET_INTERFACE: //获取接口
#ifdef DEBUG0
Prints("获取接口。\r\n");
#endif
break;
case GET_STATUS: //获取状态
#ifdef DEBUG0
Prints("获取状态。\r\n");
#endif
break;
case SYNCH_FRAME: //同步帧
#ifdef DEBUG0
Prints("同步帧。\r\n");
#endif
break;
default: //未定义的标准请求
#ifdef DEBUG0
Prints("错误:未定义的标准输入请求。\r\n");
#endif
break;
}
break;
case 1: //类请求
#ifdef DEBUG0
Prints("USB类输入请求:\r\n");
#endif
break;
case 2: //厂商请求
#ifdef DEBUG0
Prints("USB厂商输入请求:\r\n");
#endif
break;
default: //未定义的请求。这里只显示一个报错信息。
#ifdef DEBUG0
Prints("错误:未定义的输入请求。\r\n");
#endif
break;
}
}
//否则说明是输出请求
else //if(bmRequestType&0x80==0x80)之else
{
//根据bmRequestType的D6~5位散转,D6~5位表示请求的类型
//0为标准请求,1为类请求,2为厂商请求。
switch((bmRequestType>>5)&0x03)
{
case 0: //标准请求
#ifdef DEBUG0
Prints("USB标准输出请求:");
#endif
//USB协议定义了几个标准输出请求,我们实现这些标准请求即可
//请求的代码在bRequest中,对不同的请求代码进行散转
switch(bRequest)
{
case CLEAR_FEATURE: //清除特性
#ifdef DEBUG0
Prints("清除特性。\r\n");
#endif
break;
case SET_ADDRESS: //设置地址
#ifdef DEBUG0
Prints("设置地址。地址为:");
PrintHex(wValue&0xFF); //显示所设置的地址
Prints("\r\n");
#endif
/*设置地址的具体操作*/
break;
case SET_CONFIGURATION: //设置配置
#ifdef DEBUG0
Prints("设置配置。\r\n");
#endif
break;
case SET_DESCRIPTOR: //设置描述符
#ifdef DEBUG0
Prints("设置描述符。\r\n");
#endif
break;
case SET_FEATURE: //设置特性
#ifdef DEBUG0
Prints("设置特性。\r\n");
#endif
break;
case SET_INTERFACE: //设置接口
#ifdef DEBUG0
Prints("设置接口。\r\n");
#endif
break;
default: //未定义的标准请求
#ifdef DEBUG0
Prints("错误:未定义的标准输出请求。\r\n");
#endif
break;
}
break;
case 1: //类请求
#ifdef DEBUG0
Prints("USB类输出请求:");
#endif
break;
case 2: //厂商请求
#ifdef DEBUG0
Prints("USB厂商输出请求:\r\n");
#endif
break;
default: //未定义的请求。这里只显示一个报错信息。
#ifdef DEBUG0
Prints("错误:未定义的输出请求。\r\n");
#endif
break;
}
}
具体描述符的散转和数据返回的代码实现:
//在获取描述符的处理中,增加对具体描述符散转的处理。描述符的类型保存在wValue中的高字节中。
//由于暂时只实现了设备描述符,所以这里仅有对设备描述符的处理。
case GET_DESCRIPTOR: //获取描述符
#ifdef DEBUG0
Prints("获取描述符——");
#endif
//对描述符类型进行散转,对于全速设备,
标准请求只支持发送到设备的设备、配置、字符串三种描述符switch((wValue>>8)&0xFF)
{
case DEVICE_DESCRIPTOR: //设备描述符
#ifdef DEBUG0
Prints("设备描述符。\r\n");
#endif
pSendData=DeviceDescriptor; //需要发送的数据
//判断请求的字节数是否比实际需要发送的字节数多
//这里请求的是设备描述符,因此数据长度就是
//DeviceDescriptor[0]。如果请求的比实际的长,
//那么只返回实际长度的数据
if(wLength>DeviceDescriptor[0])
{
SendLength=DeviceDescriptor[0];
if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
{
NeedZeroPacket=1; //需要返回0长度的数据包
}
}
else
{
SendLength=wLength;
}
//将数据通过EP0返回
UsbEp0SendData();
break;
case CONFIGURATION_DESCRIPTOR: //配置描述符
#ifdef DEBUG0
Prints("配置描述符。\r\n");
#endif
break;
case STRING_DESCRIPTOR: //字符串描述符
#ifdef DEBUG0
Prints("字符串描述符");
#endif
break;
default: //其它描述符
#ifdef DEBUG0
Prints("其他描述符,描述符代码:");
PrintHex((wValue>>8)&0xFF);
Prints("\r\n");
#endif
break;
}
break;
具体将数据发送给PC的函数:
在上面的代码中,用到了UsbEp0SendData()函数。其功能是根据需要发送数据的长度和位置,将数据写
入端点0去。以后需要往端点0写数据时,先设置好需要发送数据的位置和长度,再调用这个函数即可。
- //函数功能:根据pData和SendLength将数据发送到端点0的函数。
void UsbEp0SendData(void)
{
//将数据写到端点中去准备发送
//写之前要先判断一下需要发送的数据是否比端点0
//最大长度大,如果超过端点大小,则一次只能发送
//最大包长的数据。端点0的最大包长在DeviceDescriptor[7]
if(SendLength>DeviceDescriptor[7])
{
//按最大包长度发送
D12WriteEndpointBuffer(1,DeviceDescriptor[7],pSendData);
//发送后剩余字节数减少最大包长
SendLength-=DeviceDescriptor[7];
//发送一次后指针位置要调整
pSendData+= DeviceDescriptor[7];
}
else
{
if(SendLength!=0)
{
//不够最大包长,可以直接发送
D12WriteEndpointBuffer(1,SendLength,pSendData);
//发送完毕后,SendLength长度变为0
SendLength=0;
}
else //如果要发送的数据包长度为0
{
if(NeedZeroPacket==1) //如果需要发送0长度数据
{
D12WriteEndpointBuffer(1,0,pSendData); //发送0长度数据包
NeedZeroPacket=0; //清需要发送0长度数据包标志
}
}
}
}
怎么处理要发送的数据比端点0还多的情况
因为函数UsbEp0SendData()一次只能发送最大包长的数据,所以当要发送的字节数比端点0还多时,需要
在端点0输入中断函数UsbEp0In()中处理。
当前面的数据成功发送后,端点0输入中断机会产生。因此只要在端点0输入中断中发送剩下的数据即可。
在端点0输入中断函数UsbEp0In()中处理。
当前面的数据成功发送后,端点0输入中断机会产生。因此只要在端点0输入中断中发送剩下的数据即可。
记得还要调用D12ReadEndpointLastStatus()清楚端点0输入中断。
注:前面提到过,主机第一次获取设备描述符时,只会读一个数据包的设备描述符,但此处设备描述符长
度比端点0的大小要大,因而就会发送两次数据包。此时不会出问题,因为D12在收到下一个SETUP包之
后,会自动将端点0输入缓冲区的数据置为无效,即第二次加进去的数据实际上并没有起作用。所以,这里
不对端点0的输入做特殊处理。
端点0输入中断处理函数如下所示:
//函数功能:端点0输入中断处理函数。
void UsbEp0In(void)
{
#ifdef DEBUG0
Prints("USB端点0输入中断。\r\n");
#endif
//读最后发送状态,这将清除端点0的中断标志位
D12ReadEndpointLastStatus(1);
//发送剩余的字节数
UsbEp0SendData();
}
6)分析调试信息
将程序下载到开发板中,可以看到端点0的写入操作。如果主机正确收到了设备描述符,应该接着对总线复位一次,然后发送
设置地址的标准请求。
实际调试信息如下图所示:
从返回的调试信息看出:
① 确实已经成功返回了设备描述符
② 主机发出了设置地址的标准请求。设置地址值存在wValue的低字节,即上图中倒数第二行的0x04。
③ 接下来就要对设置地址这个标准请求进行处理了。
3.8 设置地址请求的处理
1. 设置地址请求的流程及如何确定设置成功
每个USB设备都有一个唯一的设备地址,这个地址是主机在设置地址请求时分配给设备的。
设备在收到设置地址请求后,
应该返回一个0长度的状态数据包(因为设置地址请求是没有数据过程的),
然后等待主机确认这个数据包(即ACK应答设备)。
设备在正确接收到状态数据包的ACK之后,开始使用新的设备地址。
2. D12芯片如何设置地址
D12芯片提供了一条设置地址的命令:Set Address/Enable命令,代码是0xD0。
设置地址命令后跟一字节数据写入操作,该字节的Bit7用来控制设备是否使能,只有当Bit7设置为1时,
D12的普通端点才能通过使能端点(Set Endpoint Enable)命令启用。Bit6~Bit0是设备的7位地址,应该将
接收到的标准请求中的地址写入Bit6~0中。代码实现如下:
//函数功能:设置地址函数。
//入口参数:Addr:要设置的地址值。
void D12SetAddress(uint8 Addr)
{
D12WriteCommand(D12_SET_ADDRESS_ENABLE); //写设置地址命令
D12WriteByte(0x80 | Addr); //写一字节数据:使能及地址
}
3. 何时设置地址
在上面的端点0输出中断中添加设置地址的处理。
注意,在D12芯片中,设置地址后要等待主机返回ACK之后新地址才生效。也就是说,在主机返回0长度
的状态数据包之前写D12的地址寄存器,当主机取走状态数据包并用ACK应答时,D12才自动启用新设置的
地址。这之前使用的都是原来的 地址(虽然已经写了地址寄存器)。
代码实现如下:
//在上面的case SET_ADDRESS:中调试信息后面添加如下代码:
- D12SetAddress(wValue&0xFF); //wValue中的低字节是设置的地址值
SendLength=0;
//设置地址没有数据过程,直接进入到状态过程,返回一个0长度的数据包NeedZeroPacket=1;
UsbEp0SendData();
//将数据通过EP0返回
4. 分析调试信息
编译下载上面的代码后,显示的调试信息如下:
1)此时显示分配的地址和前面的0x04不一样,变成0x14了。这是因为不同时刻主机管理分配的地址
号不同,只要分配的是一个唯一的设备地址即可。
2)地址设置好之后,调试信息显示又接收到了主机获取设备描述符的请求,这说明设置地址操作已
经成功了。因为这是主机已经使用新地址来发送请求了,如果不成功,主机会检测到超时,从而复位总
线。
这次获取
设备
描述符将会完整第获取。
3)之后,主机接着发送获取配置描述符的请求,请求长度为9字节。
标准配置描述符的长度就是9字节的,通常主机第一次先获取9字节长度的配置描述符(即标准配置
描述符),然后根据配置描述符中的配置集合长度,再次获取配置描述符。第二次获取配置描述符时,会
将配置描述符、接口描述符、类特殊描述符(如果有)、端点描述符等一起读回。因此接下来就是实现配置
描述符和接口描述符、类特殊描述符、端点描述符这个大集合——配置描述符集合。