《圈圈教你玩USB》 第三章 USB鼠标的实现——看书笔记( 2 )

3.5 USB标准请求

标准请求的用途:
    主要用于设备的枚举过程。
标准请求的内容:
    8字节的数据,包含了数据过程所需要传输的数据传输方向、长度、数据类型等信息。
    正式由于8字节标准请求的原因,USB协议规定,端点0的最大包长至少为8字节。即任何USB设备都能够(且必须要)接收8字节的标准请求。    
标准请求的传输过程:
    在控制传输的建立过程通过默认控制端点0发出的。

3.5.1 USB标准设备请求的数据结构

1) USB协议规定的标准设备请求的数据结构
偏移量/字节 大小/字节 取值 描述
0 bmRequestType 1 位图 请求的特征:
 D7:数据传输方向
  0 = 主机到设备(输出)
  1 = 设备到主机(输入)
 D6~5:请求的类型
  0 = 标准
  1 = 类
  2 = 厂商
  3 = 保留
 D4~0:请求的接收者
  0 = 设备
  1 = 接口
  2 = 端点
  3 = 其他
  4~31 = 保留
1 bRequest 1 数值 请求代码
2 wValue 2 数值 该域的意义由具体的请求决定
4 wIndex 2 索引或偏移量 该域的意义由具体的请求决定
6 wLength 2 字节数 数据过程(如果有)所需要传输的字节数
本节只介绍USB协议的标准请求,即bmRequestType的D6~5位为00的请求。
2) USB协议定义了11个标准请求(bRequest),其 名字 请求代码 如下表所示:
序号 bRequest Value
1 GET_STATUS 0
2 CLEAR_FEATURE 1
3 SET_FEATURE 3
4 SET_ADDRESS 5
5 GET_DESCRIPTOR 6
6 SET_DESCRIPTOR 7
7 GET_CONFIGURATION 8
8 SET_CONFIGURATION 9
9 GET_INTERFACE 10
10 SET_INTERFACE 11
11 SYNCH_FRAME 12
3)各种标准请求的结构及需要传输的数据
不同的请求对于其接收者、wValue和wIndex,其各字段的意义是不一样的。
下表是各种标准请求的结构和数据过程需要传输的数据。
其中第一列有的有多个,主要是最低5位不同,即表示接收者不同。
有的请求只能发送到设备,而有的请求可以发送到设备、接口和端点。
常用的请求为:GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION。
序号 bmRequestType bRequest wValue wIndex wLength 数据过程
1 0000 0000b CLEAR_FEATURE 特征选择 0 0 没有
2 0000 0001b 接口号
3 0000 0010b 端点号
4 1000 0000b GET_CONFIGURATION 0 0 1 配置值
5 1000 0000b GET_DESCRIPTOR 描述符类型和索引 0或语言ID 描述符长度 描述符
6 1000 0001b GET_INTERFACE 0 接口号 1 备用接口号
7 1000 0000b GET_STATUS 0 0 2 设备、
接口或
端点状态
8 1000 0001b 接口号
9 1000 0010b 端点号
10 0000 0000b SET_ADDRESS 设备地址 0 0 没有
11 0000 0000b SET_CONFIGURATION 配置值 0 0 没有
12 0000 0000b SET_DESCRIPTOR 描述符类型和索引 0或者语言ID 描述符的长度 描述符
13 0000 0000b SET_FEATURE 特性选择 0 0 没有
14 0000 0001b 接口号
15 0000 0010b 端点号
16 0000 0001b SET_INTERFACE 备用接口号 接口号 0 没有
17 1000 0010b SYNCH_FRAME 0 端点号 2 帧号

3.5.2 GET_DESCRIPTOR请求

    GET_DESCRIPOTOR(获取描述符)请求是在枚举过程中用的最多的一个请求。主机通过发送获取描述符
请求读取设备的各种描述符,从而可以获知设备类型、端点情况等众多重要信息。
    获取描述符的接收者只能是设备,从bmRequestType的Bit7可以看出,它是请求数据输入的。
    bRequest的值为0x06(GET_DESCRIPTOR)。
   GET_DESCRIPTOR请求的结构和需要传输的数据如下表所示:
bmRequestType bRequest wValue wIndex wLength 数据过程
1000 0000b(0x80) GET_DESCRIPTOR(0x06) 描述符类型和索引 0或语言ID 描述符的长度 描述符
   注:wValue、wIndex、wLength这三个域都是两字节的,在USB协议中使用小端结构,即低字节在先。
    wValue域的第一字节(低字节)表示的是索引号,用来选择同一种描述符(例如字符串描述符和配置描述符)中具体的某个描述符。
    wValue域的第二字节表示描述符的类型编号。
描述符类型 编号
设备描述符 1
配置描述符 2
字符串描述符 3
接口描述符 4
端点描述符 5
    wIndex域只在获取字符串描述符中有用,表示字符串的语言ID号,获取除字符串描述符的其他描述符时,wIndex的值为0。
    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请求的结构:
bmRequestType bRequest wValue wIndex wLength 数据过程
0000 0000b(0x00) SET_ADDRESS(0x05) 设备地址 0x0000 0x0000 没有

3.5.4 SET_CONFIGURATION请求

    如何设置配置:
        设置配置的请求和设置地址请求类似,区别在于wValue域的意义。
在设置地址请求中,wValue第一字节(低字节)为设备地址;
    在设置配置请求中,wValue第一字节为配置的值。 当该值与某配置描述符中的配置编号一致时,表示
选中该配置。该值通常为1,因为大多数USB设备只有一种配置,配置编号为1;如果该值为0,则让设备进
入设置地址状态。
        设备只有在收到非0的配置值之后,才能启用它的非0端点。
    SET_CONFIGURATION请求的结构:
bmRequestType bRequest wValue wIndex wLength 数据过程
0000 0000b(0x00) SET_CONFIGURATION(0x09) 配置值 0x0000 0x0000 没有

3.6 设备描述符的实现

    已知每个设备都必须有且只有一个设备描述符,它的结构如下表所示。
序号 偏移量/字节 大小/字节 说明 备注
1 0 bLength 1 该描述符的长度(18字节)  
2 1 bDescriptorType 1 描述符的类型(设备描述符为0x01)  
3 2 bcdUSB 2 本设备所使用的USB协议版本 它使用的是BCD码,例如USB2.0就是0x0200,而USB1.1就是0x0110。因为使用的是小端结构,所以实际传输时USB2.0的bcdUSB拆成两字节0x00和0x02,而USB1.1的bcdUSB拆成两个字节0x10和0x01。
4 4 bDeviceClass 1 类代码 大多数标准的USB设备类,该字段通常为0,而在接口描述符中的bInterfaceClass中指定接口所实现的功能。
当bDeviceClass为0时,下面的bDeviceSubClass也为0。
如果bDeviceClass为0xFF,表示是厂商自定义的设备类。
5 5 bDeviceSubClass 1 子类代码 当类代码不是0和0xFF时,子类代码有USB协议规定。
当类代码为0时,此处必须也为0.
6 6 bDeviceProtocol 1 设备所使用的协议 当该字段为0时,表示设备不使用类所定义的协议;
当该字段为0xFF时,表示设备使用厂商自定义的协议。
本字段必须结合类代码和子类代码一起使用才有意义,因此,当类代码为0时,bDeviceProtocol也要为0.
7 7 bMaxPackeSize0 1 端点0最大包长 可以取值:8、16、32、64
8 8 idVender 2 厂商ID 主机通常靠VID、PID和产品序列号来安装和加载驱动,如果使用了别人的ID号,可能导致驱动程序安装或加载错误,从而导致设备无法工作。注意小端结构。
9 10 idProduct 2 产品ID  
10 12 bcdDevice 2 设备版本号 当同一个产品升级后(例如修改了固件增加了新功能),可以通过修改设备版本号来区别
11 14 iManufacturer 1 描述厂商的字符串索引 当该值为0时,表示没有厂商字符串。
主机在获取设备描述符时,会将索引值放在wValue的第一字节中,用来选择不同的字符串。
12 15 iProduct 1 描述产品的字符串的索引 当该值为0时,表示没有产品字符串。
当第一次插入某个USB设备时,会在windows的右下角弹出一个对话框,显示发现新硬件,并且会显示该设备的名称。这里显示的信息就是从产品字符串中获取的。
13 16 iSerialNumber 1 产品序列号字符串索引 当该值为0时,表示没有序列号字符串。
用于区分多个VID和PID相同的设备,防止设备无法正确识别。
14 17 bNumConfigurations 1 可能的配置数 每种配置都有一个配置描述符,主机通过发送设置配置来选择某种配置。大部分USB设备只有一个配置,即该字段的值为1。
    USB鼠标实例的设备描述符:
   
   
  1. //USB设备描述符的定义
  2. code uint8 DeviceDescriptor[0x12]= //设备描述符为18字节
  3. {
  4. 0x12,    //bLength字段。设备描述符的长度为18(0x12)字节
  5. 0x01,    //bDescriptorType字段。设备描述符的编号为0x01
  6. 0x10,    //bcdUSB字段。这里设置版本为USB1.1,即0x0110。由于是小端结构,所以低字节在先,即0x10,0x01。
  7. 0x01,
  8. 0x00,    //bDeviceClass字段。我们不在设备描述符中定义设备类,而在接口描述符中定义设备类,所以该字段的值为0。
  9. 0x00,    //bDeviceSubClass字段。bDeviceClass字段为0时,该字段也为0。
  10. 0x00,    //bDeviceProtocol字段。bDeviceClass字段为0时,该字段也为0。
  11. 0x10,    //bMaxPacketSize0字段。PDIUSBD12的端点0大小的16字节。
  12. 0x88,    //idVender字段。厂商ID号,我们这里取0x8888,仅供实验用。实际产品不能随便使用厂商ID号,必须跟USB协会申请厂商ID号。注意小端模式,低字节在先。
  13. 0x88,
  14. 0x01,    //idProduct字段。产品ID号,由于是第一个实验,我们这里取0x0001。注意小端模式,低字节应该在前。
  15. 0x00,
  16. 0x00,    //bcdDevice字段。我们这个USB鼠标刚开始做,就叫它1.0版吧,即0x0100。小端模式,低字节在先。
  17. 0x01,
  18. 0x01,    //iManufacturer字段。厂商字符串的索引值,为了方便记忆和管理,字符串索引就从1开始吧。
  19.  
  20. 0x02,    //iProduct字段。产品字符串的索引值。刚刚用了1,这里就取2吧。注意字符串索引值不要使用相同的值。
  21. 0x03,    //iSerialNumber字段。设备的序列号字符串索引值。这里取3就可以了。
  22.  
  23. 0x01     //bNumConfigurations字段。该设备所具有的配置数。我们只需要一种配置就行了,因此该值设置为1。
  24. };

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)将数据写入缓冲区的代码:
   
   
  1. //函数功能:将数据写入端点缓冲区函数。
  2. //入口参数:Endp:端点号;Len:需要发送的长度;Buf:保存数据的缓冲区。
  3. //返 回:Len的值。
  4. uint8 D12WriteEndpointBuffer(uint8 Endp,uint8 Len,uint8 * Buf)
  5. {
  6. uint8 i;
  7. D12SelectEndpoint(Endp);             //选择端点
  8. D12WriteCommand(D12_WRITE_BUFFER);   //写Write Buffer命令
  9. D12WriteByte(0);                     //该字节必须写0
  10. D12WriteByte(Len);                  //写需要发送数据的长度
  11. #ifdef DEBUG1                         //如果定义了DEBUG1,则需要显示调试信息
  12. Prints("写端点");
  13. PrintLongInt(Endp/2);                 //端点号。由于D12特殊的端点组织形式,
  14.                  //这里的0和1分别表示端点0的输出和输入;
  15.                 //而2、3分别表示端点1的输出和输入;
  16.                 //3、4分别表示端点2的输出和输入。
  17.                 //因此要除以2才显示对应的端点。
  18. Prints("缓冲区");
  19. PrintLongInt(Len);                 //写入的字节数
  20. Prints("字节。\r\n");
  21. #endif
  22. D12SetPortOut();                     //将数据口设置为输出状态(注意这里为空宏,移植时可能有用)
  23. for(i=0;i<Len;i++)
  24. {
  25. //这里不直接调用写一字节的函数,而直接在这里模拟时序,可以节省时间
  26. D12ClrWr();                         //WR置低
  27. D12SetData(*(Buf+i));               //将数据放到数据线上
  28. D12SetWr();                        //WR置高,完成一字节写
  29. #ifdef DEBUG1
  30. PrintHex(*(Buf+i));                //如果需要显示调试信息,则显示发送的数据
  31. if((i+1)%16==0)Prints("\r\n");      //每16字节换行一次
  32. #endif
  33. }
  34. #ifdef DEBUG1
  35. if((Len%16)!=0)Prints("\r\n");       //换行
  36. #endif
  37. D12SetPortIn();                      //数据口切换到输入状态
  38. D12ValidateBuffer();                 //使端点数据有效
  39. return Len;                          //返回Len
  40. }
     4)使能端点缓冲区的代码
   
   
  1. //函数功能:使能发送端点缓冲区数据有效的函数。
  2. //备 注:只有使用该函数使能发送端点数据有效之后,数据才能发送出去。
  3. ********************************************************************/
  4. void D12ValidateBuffer(void)
  5. {
  6. D12WriteCommand(D12_VALIDATE_BUFFER);
  7. }
    5 )返回设备描述符的详细流程:
    分析建立过程数据包的内容,以确定何时返回设备描述符。
    分析数据包的内容主要使用if和switch语句来散转,对不同的请求做不同的处理。

    在分析数据前,先申请一些变量,用来保存这些标准请求的不同字段。另外,还需要一个用来保存发送位
置的指针变量和一个保存剩余字节数的变量,以及一个是否需要返回0长度数据包的标志。
    变量定义代码如下:
   
   
  1. idata uint8 Buffer[16]; //读端点0用的缓冲区
  2. //USB设备请求的各字段
  3. uint8 bmRequestType;
  4. uint8 bRequest;
  5. uint16 wValue;
  6. uint16 wIndex;
  7. uint16 wLength;
  8. uint8 * pSendData;//当前发送数据的位置
  9. uint16 SendLength;//需要发送数据的长度
  10. uint8 NeedZeroPacket;
  11. //是否需要发送0数据包的标志。在USB控制传输的数据过程中,当返回的数据包字节数少于最大包长时,会认为数据过程结束。
  12. //当请求的字节数比实际需要返回的字节数长,而实际返回的字节数又刚好是端点0大小的整数倍时,就需要返回一个0长度的
  13. //数据包来结束数据过程。因此这里增加一个标志,供程序决定是否需要返回一个0长度的数据包。
    接着,将从缓冲区中读出的数据分别添加到设备请求的各字段中
    
    
  1. //将缓冲数据填到设备请求的各字段中
  2. bmRequestType=Buffer[0];
  3. bRequest=Buffer[1];
  4. wValue=Buffer[2]+(((uint16)Buffer[3])<<8);
  5. wIndex=Buffer[4]+(((uint16)Buffer[5])<<8);
  6. wLength=Buffer[6]+(((uint16)Buffer[7])<<8);
    具体散转顺序:
    首先,判断 bmRequestType的Bit7,如果是1,则说明是输入请求;如果是0,则说明是输出请求。
    再判断 Bit6~5,如果是00,则说明是标准请求;如果是01,则说明是类请求;如果是10,则说明是厂商请求。
    再写上所有的标准请求情况,如果是获取描述符的请求,则对bRequest进行散转,看具体是请求什么描述符;
    如果是获取字符串描述符,则还需要对wValue值的低字节散转,看是哪个字符串描述符。
    散转的代码实现:
    //判断具体的请求,根据不同的请求进行相关操作。如果D7位为1,则说明是输入请求。
    //还需要对接收者进行散转,因为不同的请求接收者不同。接收者在bmRequestType的Bit4~0中定义。
    //此处为了简化操作,省略了对接收者的判断。例如获取描述符的请求,只根据描述符的类型来区别。
   
   
  1. //下面的代码判断具体的请求,并根据不同的请求进行相关操作,如果D7位为1,则说明是输入请求
  2. if((bmRequestType&0x80)==0x80)
  3. {
  4. //根据bmRequestType的D6~5位散转,D6~5位表示请求的类型
  5. //0为标准请求,1为类请求,2为厂商请求。
  6. switch((bmRequestType>>5)&0x03)
  7. {
  8. case 0: //标准请求
  9. #ifdef DEBUG0
  10. Prints("USB标准输入请求:");
  11. #endif
  12. //USB协议定义了几个标准输入请求,我们实现这些标准请求即可
  13. //请求的代码在bRequest中,对不同的请求代码进行散转
  14. switch(bRequest)
  15. {
  16. case GET_CONFIGURATION: //获取配置
  17. #ifdef DEBUG0
  18. Prints("获取配置。\r\n");
  19. #endif
  20. break;
  21. case GET_DESCRIPTOR: //获取描述符
  22. #ifdef DEBUG0
  23. Prints("获取描述符——");
  24. #endif
  25.      /*对具体描述符的散转和描述符的返回*/
  26. break;
  27. case GET_INTERFACE: //获取接口
  28. #ifdef DEBUG0
  29. Prints("获取接口。\r\n");
  30. #endif
  31. break;
  32. case GET_STATUS: //获取状态
  33. #ifdef DEBUG0
  34. Prints("获取状态。\r\n");
  35. #endif
  36. break;
  37. case SYNCH_FRAME: //同步帧
  38. #ifdef DEBUG0
  39. Prints("同步帧。\r\n");
  40. #endif
  41. break;
  42. default: //未定义的标准请求
  43. #ifdef DEBUG0
  44. Prints("错误:未定义的标准输入请求。\r\n");
  45. #endif
  46. break;
  47. }
  48. break;
  49. case 1: //类请求
  50. #ifdef DEBUG0
  51. Prints("USB类输入请求:\r\n");
  52. #endif
  53. break;
  54. case 2: //厂商请求
  55. #ifdef DEBUG0
  56. Prints("USB厂商输入请求:\r\n");
  57. #endif
  58. break;
  59. default: //未定义的请求。这里只显示一个报错信息。
  60. #ifdef DEBUG0
  61. Prints("错误:未定义的输入请求。\r\n");
  62. #endif
  63. break;
  64. }
  65. }
  66. //否则说明是输出请求
  67. else //if(bmRequestType&0x80==0x80)之else
  68. {
  69. //根据bmRequestType的D6~5位散转,D6~5位表示请求的类型
  70. //0为标准请求,1为类请求,2为厂商请求。
  71. switch((bmRequestType>>5)&0x03)
  72. {
  73. case 0: //标准请求
  74. #ifdef DEBUG0
  75. Prints("USB标准输出请求:");
  76. #endif
  77. //USB协议定义了几个标准输出请求,我们实现这些标准请求即可
  78. //请求的代码在bRequest中,对不同的请求代码进行散转
  79. switch(bRequest)
  80. {
  81. case CLEAR_FEATURE: //清除特性
  82. #ifdef DEBUG0
  83. Prints("清除特性。\r\n");
  84. #endif
  85. break;
  86. case SET_ADDRESS: //设置地址
  87. #ifdef DEBUG0
  88. Prints("设置地址。地址为:");
  89. PrintHex(wValue&0xFF); //显示所设置的地址
  90. Prints("\r\n");
  91. #endif
  92.      /*设置地址的具体操作*/
  93. break;
  94. case SET_CONFIGURATION: //设置配置
  95. #ifdef DEBUG0
  96. Prints("设置配置。\r\n");
  97. #endif
  98. break;
  99. case SET_DESCRIPTOR: //设置描述符
  100. #ifdef DEBUG0
  101. Prints("设置描述符。\r\n");
  102. #endif
  103. break;
  104. case SET_FEATURE: //设置特性
  105. #ifdef DEBUG0
  106. Prints("设置特性。\r\n");
  107. #endif
  108. break;
  109. case SET_INTERFACE: //设置接口
  110. #ifdef DEBUG0
  111. Prints("设置接口。\r\n");
  112. #endif
  113. break;
  114. default: //未定义的标准请求
  115. #ifdef DEBUG0
  116. Prints("错误:未定义的标准输出请求。\r\n");
  117. #endif
  118. break;
  119. }
  120. break;
  121. case 1: //类请求
  122. #ifdef DEBUG0
  123. Prints("USB类输出请求:");
  124. #endif
  125. break;
  126. case 2: //厂商请求
  127. #ifdef DEBUG0
  128. Prints("USB厂商输出请求:\r\n");
  129. #endif
  130. break;
  131. default: //未定义的请求。这里只显示一个报错信息。
  132. #ifdef DEBUG0
  133. Prints("错误:未定义的输出请求。\r\n");
  134. #endif
  135. break;
  136. }
  137. }
    具体描述符的散转和数据返回的代码实现:
    //在获取描述符的处理中,增加对具体描述符散转的处理。描述符的类型保存在wValue中的高字节中。
    //由于暂时只实现了设备描述符,所以这里仅有对设备描述符的处理。
   
   
  1. case GET_DESCRIPTOR: //获取描述符
  2. #ifdef DEBUG0
  3. Prints("获取描述符——");
  4. #endif
  5. //对描述符类型进行散转,对于全速设备,标准请求只支持发送到设备的设备、配置、字符串三种描述符
  6. switch((wValue>>8)&0xFF)
  7. {
  8. case DEVICE_DESCRIPTOR: //设备描述符
  9. #ifdef DEBUG0
  10. Prints("设备描述符。\r\n");
  11. #endif
  12. pSendData=DeviceDescriptor; //需要发送的数据
  13. //判断请求的字节数是否比实际需要发送的字节数多
  14. //这里请求的是设备描述符,因此数据长度就是
  15. //DeviceDescriptor[0]。如果请求的比实际的长,
  16. //那么只返回实际长度的数据
  17. if(wLength>DeviceDescriptor[0])
  18. {
  19. SendLength=DeviceDescriptor[0];
  20. if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
  21. {
  22. NeedZeroPacket=1; //需要返回0长度的数据包
  23. }
  24. }
  25. else
  26. {
  27. SendLength=wLength;
  28. }
  29. //将数据通过EP0返回
  30. UsbEp0SendData();
  31. break;
  32. case CONFIGURATION_DESCRIPTOR: //配置描述符
  33. #ifdef DEBUG0
  34. Prints("配置描述符。\r\n");
  35. #endif
  36. break;
  37. case STRING_DESCRIPTOR: //字符串描述符
  38. #ifdef DEBUG0
  39. Prints("字符串描述符");
  40. #endif
  41. break;
  42. default: //其它描述符
  43. #ifdef DEBUG0
  44. Prints("其他描述符,描述符代码:");
  45. PrintHex((wValue>>8)&0xFF);
  46. Prints("\r\n");
  47. #endif
  48. break;
  49. }
  50. break;
    具体将数据发送给PC的函数:
    在上面的代码中,用到了UsbEp0SendData()函数。其功能是根据需要发送数据的长度和位置,将数据写
入端点0去。以后需要往端点0写数据时,先设置好需要发送数据的位置和长度,再调用这个函数即可。
   
   
  1. //函数功能:根据pData和SendLength将数据发送到端点0的函数。
  2. void UsbEp0SendData(void)
  3. {
  4. //将数据写到端点中去准备发送
  5. //写之前要先判断一下需要发送的数据是否比端点0
  6. //最大长度大,如果超过端点大小,则一次只能发送
  7. //最大包长的数据。端点0的最大包长在DeviceDescriptor[7]
  8. if(SendLength>DeviceDescriptor[7])
  9. {
  10. //按最大包长度发送
  11. D12WriteEndpointBuffer(1,DeviceDescriptor[7],pSendData);
  12. //发送后剩余字节数减少最大包长
  13. SendLength-=DeviceDescriptor[7];
  14. //发送一次后指针位置要调整
  15. pSendData+= DeviceDescriptor[7];
  16. }
  17. else
  18. {
  19. if(SendLength!=0)
  20. {
  21. //不够最大包长,可以直接发送
  22. D12WriteEndpointBuffer(1,SendLength,pSendData);
  23. //发送完毕后,SendLength长度变为0
  24. SendLength=0;
  25. }
  26. else //如果要发送的数据包长度为0
  27. {
  28. if(NeedZeroPacket==1) //如果需要发送0长度数据
  29. {
  30. D12WriteEndpointBuffer(1,0,pSendData); //发送0长度数据包
  31. NeedZeroPacket=0; //清需要发送0长度数据包标志
  32. }
  33. }
  34. }
  35. }
    怎么处理要发送的数据比端点0还多的情况
    因为函数UsbEp0SendData()一次只能发送最大包长的数据,所以当要发送的字节数比端点0还多时,需要
在端点0输入中断函数UsbEp0In()中处理。
    当前面的数据成功发送后,端点0输入中断机会产生。因此只要在端点0输入中断中发送剩下的数据即可。
记得还要调用D12ReadEndpointLastStatus()清楚端点0输入中断。
    注:前面提到过,主机第一次获取设备描述符时,只会读一个数据包的设备描述符,但此处设备描述符长
度比端点0的大小要大,因而就会发送两次数据包。此时不会出问题,因为D12在收到下一个SETUP包之
后,会自动将端点0输入缓冲区的数据置为无效,即第二次加进去的数据实际上并没有起作用。所以,这里
不对端点0的输入做特殊处理。
    端点0输入中断处理函数如下所示:
   
   
  1. //函数功能:端点0输入中断处理函数。
  2. void UsbEp0In(void)
  3. {
  4. #ifdef DEBUG0
  5. Prints("USB端点0输入中断。\r\n");
  6. #endif
  7. //读最后发送状态,这将清除端点0的中断标志位
  8. D12ReadEndpointLastStatus(1);
  9. //发送剩余的字节数
  10. UsbEp0SendData();
  11. }
     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中。代码实现如下:
   
   
  1. //函数功能:设置地址函数。
  2. //入口参数:Addr:要设置的地址值。
  3. void D12SetAddress(uint8 Addr)
  4. {
  5. D12WriteCommand(D12_SET_ADDRESS_ENABLE); //写设置地址命令
  6. D12WriteByte(0x80 | Addr); //写一字节数据:使能及地址
  7. }

3. 何时设置地址

    在上面的端点0输出中断中添加设置地址的处理。
    注意,在D12芯片中,设置地址后要等待主机返回ACK之后新地址才生效。也就是说,在主机返回0长度
的状态数据包之前写D12的地址寄存器,当主机取走状态数据包并用ACK应答时,D12才自动启用新设置的
地址。这之前使用的都是原来的 地址(虽然已经写了地址寄存器)。
    代码实现如下:
   
   
  1. //在上面的case SET_ADDRESS:中调试信息后面添加如下代码:
  2. D12SetAddress(wValue&0xFF); //wValue中的低字节是设置的地址值
  3. SendLength=0;                //设置地址没有数据过程,直接进入到状态过程,返回一个0长度的数据包
  4. NeedZeroPacket=1;
  5. UsbEp0SendData();            //将数据通过EP0返回

4. 分析调试信息

    编译下载上面的代码后,显示的调试信息如下:
    
     1)此时显示分配的地址和前面的0x04不一样,变成0x14了。这是因为不同时刻主机管理分配的地址
号不同,只要分配的是一个唯一的设备地址即可。
    2)地址设置好之后,调试信息显示又接收到了主机获取设备描述符的请求,这说明设置地址操作已
经成功了。因为这是主机已经使用新地址来发送请求了,如果不成功,主机会检测到超时,从而复位总
线。 这次获取 设备 描述符将会完整第获取。
    3)之后,主机接着发送获取配置描述符的请求,请求长度为9字节。
          标准配置描述符的长度就是9字节的,通常主机第一次先获取9字节长度的配置描述符(即标准配置
描述符),然后根据配置描述符中的配置集合长度,再次获取配置描述符。第二次获取配置描述符时,会
将配置描述符、接口描述符、类特殊描述符(如果有)、端点描述符等一起读回。因此接下来就是实现配置
描述符和接口描述符、类特殊描述符、端点描述符这个大集合——配置描述符集合。
第一章 USB概述及协议基础 1 1.1 USB是什么 1 1.2 USB的特点 1 1.3 USB的拓扑结构 2 1.4 USB的电气特性 5 1.5 USB的线缆以及插头、插座 5 1.6 USB的插入检测机制 7 1.7 USB的描述符及其之间的关系 9 1.8 USB设备的枚举过程 10 1.9 USB的包结构及传输过程 11 1.9.1 USB包的结构及包的分类 11 1.9.2 令牌包 13 1.9.3 数据包 14 1.9.4 握手包 14 1.9.5 特殊包 15 1.9.6 如何处理数据包 15 1.10 USB的四种传输类型 16 1.10.1 USB事务 16 1.10.2 批量传输 16 1.10.3 中断传输 18 1.10.4 等时传输(同步传输) 19 1.10.5 控制传输 20 1.10.6 端点类型与传输类型的关系 21 1.10.7 传输类型与端点支持的最大包长 21 1.11 本章小结 21 第二章 硬件系统设计 1 2.1 方案以及芯片的选定 1 2.2 D12引脚功能说明 2 2.3 D12与89S52的连接 4 2.4 串口部分电路 6 2.5 按键部分 7 2.6 指示灯部分 7 2.7 IDE接口部分 8 2.8 单片机部分 8 2.9 元件安装 8 2.10 电路调试 11 2.11 测试程序的编写和调试 12 2.11.1 建立一个工程 12 2.11.2 为工程添加源文件 14 2.11.3 KEIL工具栏及仿真介绍 15 2.11.4 按键驱动的编写 18 2.11.5 串口驱动的编写 24 2.11.6 PDIUSBD12读写函数及读ID的实现 28 2.12 本章小结 33 第三章 USB鼠标实现 1 3.1 USB鼠标工程的建立 1 3.2 USB的断开与连接 1 3.3 USB中断的处理 4 3.4 读取从主机发送到端点0的数据 6 3.5 USB标准请求 12 3.5.1 USB标准设备请求的结构 13 3.5.2 GET_DESCRIPTOR请求 15 3.5.3 SET_ADDRESS请求 16 3.5.6 SET_CONFIGURATION请求 16 3.6 设备描述符的实现 17 3.7 设备描述符的返回 20 3.8 设置地址请求的处理 30 3.9 配置描述符集合的结构 32 3.9.1 配置描述符的结构 32 3.9.2 接口描述符的结构 33 3.9.3 端点描述符的结构 33 3.9.4 HID描述符的结构 34 3.10 配置描述符集合的实现以及返回 35 3.11 字符串及语言ID请求的实现 39 3.12 设置配置请求的实现 45 3.13 报告描述符的结构及实现 48 3.14 报告的返回 54 3.15 Bus Hound工具的简介 57 3.16 本章小结 59 第四章 USB键盘的实现 1 4.1 USB键盘工程的建立 1 4.2 设备描述符的实现 1 4.4 配置描述符集合的实现 2 4.4.1 配置描述符 3 4.4.2 接口描述符 3 4.4.3 HID描述符 3 4.4.4 端点描述 3 4.5 字符串描述符 6 4.6 报告描述符 6 4.7 输入和输出报告的实现 10 4.8 USB键盘实例的测试 13 4.9 再谈USB HID的报告描述符 14 4.10 带鼠标功能的USB键盘(方法一) 16 4.11 带鼠标功能的键盘(方法二) 22 4.12 多媒体USB键盘 29 4.13 本章小结 34 第五章 用户自定义的USB HID设备 1 5.1 MyUsbHid工程的建立 1 5.2 描述符的修改 1 5.3 报告的实现 3 5.4 对用户自定义的USB HID设备的访问 5 5.5 访问HID设备时所用到的相关函数 5 5.5.1 获取HID设备的接口类GUID的函数 5 5.5.2 获取指定类的所有设备信息集合的函数 6 5.5.3 从设备信息集合中获取一个设备接口信息的函数 6 5.5.4 获取指定设备接口详细信息的函数 7 5.5.5 打开设备的函数 8 5.5.6 获取HID设备属性的函数 8 5.5.7 从设备读取数据的函数 9 5.5.8 往设备写数据的函数 9 5.5.9 通过控制端点0读取报告的函数 10 5.5.10 通过控制端点0发送报告的函数 10 5.5.11 关闭设备的函数 10 5.5.12 需要包含的库文件 11 5.6 访问USB HID设备的上位机软件的实现 11 5
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值