河南键盘价格联盟

USB HID 通信 超详细

唐智嵌入式学习整理2018-06-24 08:45:29

1.IN端点数据数据发送

a.      端点控制寄存器,此寄存器可以判断当前的端点是否可以写入数据,写入数据后将相应位设置后就可以告诉主机,数据写入完成,主机可以读取,主机读取完成后,硬件自动置位,代表主机读取完成,MCU可以继续写入数据。

b.      发送IN端点数据

发送IN端点数据,要判断两个寄存器。要向主机发送数据,肯定有IN包数据先发送到USB设备,设备接收到IN包数据后,才开始响应数据。因此,除了判断1中描述端点是否可写寄存器后,还要判断是否有IN包收到,寄存器为TKIR,Token 寄存器,记录了不同端点是否有对应的TOKEN数据收到,如INSETUPOUT

c.      芯片发送数据到控制端点代码:

 

 

void usb_send_data_cpu(UINT8 const*InData,UINT32 len,UINT8 ex_index)

{

         BYTEi = 0;

         if(gucSendStall== 1)

         {

                   gucSendStall= 0;

                   return;

         }

         if(len== 0)

         {

                   EP0CSR|= EPCLR;

                   EPCSR  = EP0INRDY;// data send ok ,usb host canread.if 1 ,mcu can write data

                   return;

         }

         EP0CSR|= EPCLR;

         while(len>= CTRL_FIFOLEN)

         {

             //代表有IN 包数据

                   while(((TKIR& EP0INTK) == 0) || (EPCSR & EP0INRDY))

                   {

                            if(EPCSR& (EP0OUTRDY | EP0SETUPRDY)) != 0)

                            {

                                     EP0CSR|= EPCLR;

                                     EPCSR= EP0OUTRDY;//数据已经读取,可以进行下发下一包数据

                                     //  控制传输IN 包阶段,不会处理OUT包数据。

                                     //但是如果是SETUP包数据,则退出后进行处理。有中断会继续进入。

                                     return;

                            }

                            if(USBIR& (URES | SUSPEND | RESUME))

                            {

                                     EP0CSR|= EPCLR;

                                     EPCSR= EP0OUTRDY;

                                     return;

                            }                          

                   }

                   EP0CSR|= EPCLR;//指针从头开始写入数据

                   for(i= 0; i < CTRL_FIFOLEN;i++)

                   {

                            EP0IFIFO= *(InData++)

                   }

                   EPCSR= EP0INRDY;

                   TKIR= EP0INTK;//发送完成一个IN包数据,主机还会再次下发IN TOKEN来获取数据

                   len-= CTRL_FIFOLEN

 

 

                  

         }

 

         while(((TKIR& EP0INTK) == 0) || (EPCSR & EP0INRDY))

                   {

                            if(EPCSR& (EP0OUTRDY | EP0SETUPRDY)) != 0)

                            {

                                     EP0CSR|= EPCLR;

                                     EPCSR= EP0OUTRDY;//数据已经读取,可以进行下发下一包数据

                                     //  控制传输IN 包阶段,不会处理OUT包数据。

                                     //但是如果是SETUP包数据,则退出后进行处理。有中断会继续进入。

                                     return;

                            }

                            if(USBIR& (URES | SUSPEND | RESUME))

                            {

                                     EP0CSR|= EPCLR;

                                     EPCSR= EP0OUTRDY;//re enum ,becase the default value of OUT,should clear to 0,

                                     //thesetup default value is 0.

                                     return;

                            }                          

                   }

                   EP0CSR|= EPCLR;//指针从头开始写入数据

                   //maybe a empty packet ,but it is need.

                   for(i= 0; i < CTRL_FIFOLEN;i++)

                   {

                            EP0IFIFO= *(InData++)

                   }

                   EPCSR= EP0INRDY;

                   TKIR= EP0INTK;//发送完成一个IN包数据,主机还会再次下发IN TOKEN来获取数据

         //statusphase 0 size data packet  

   while(EPCSR & EP0OUTRDY == 0)

         {

                   if(USBIR& (URES | SUSPEND | RESUME))

                   {

                            EP0CSR|= EPCLR;

                            EPCSR= EP0OUTRDY;// notice the default value of the epcsr

                            return;

                   }       

         }

         EP0CSR|= EPCLR;

         EPCSR= EP0OUTRDY; //设备会发送ACKthe host shoult send next packet.

 

        

}

2.HID通信

对于HID通信,HID并没有像CCID协议那样有一套自己的协议,因为其是以报告描述符的格式上传数据的,主机会按照报告描述符的格式进行解析数据。但是实际作为数据通信时,并不理会报告描述符,认为其都是数据,为了拆包和组包(链包)的方便,则自己在传输层定义一个协议头,协议头为4个字节。主机下发的数据大于一个报文数据的长度时就要拆成多个包进行发送,每次发送都会携带协议头,协议头会记录总数据长度和当前包号,是否为最后一包等信息,上送时原理一样。

处理逻辑:

代码中将报告描述符的报告数据长度定义为64字节,这个长度其实是考虑到端点buf的大小定义的。但是代码为了做到通用性和兼容性,代码中也考虑了端点buf大小小于64字节和大于64字节时的情况。,具体分析见代码。

 

实际工作过程中,host都会按照报告描述符指定的长度去发送和接收数据,必定会64字节发送一个数据包,如果数据太大则会拆成多个64字节的数据包去发送数据。每一个64字节的数据到都会携带一个4字节的协议都指定总长度、当前包号、当前包的长度等信息。

如果实际端点描述符小于报告描述指定的长度64字节,则一个报告数据的长度要分多次接收,而等于大于报告描述符指定数据长度的端点buf则接收一次就可以了。所以实际工作过程中要判断当前的报告数据是否能一次性接收完成,不能接收完成则要多次接收直到当前报告数据接收完成。但是对协议头的判断每一个报告数据只判断一次就OK,最状态要判断准确。

下面的函数head是接收的报告数据的第一个报数据包,body函数是接收的是当前报告数据的剩余部分数据。





 

发送数据的原理和接收时的原理一样,见下面代码:

 

 

 



 

USBhid也可以发送WTX,只是这个WTX协议也自定义的命令。

 

 

 

2.详细枚举流程

  1. 设备插入到主机后,先会进行USB模块的初始化,主要为SUB时钟、门控、总线、中断寄存器、状态寄存器等配置,要看具体的芯片手册。

  2. 由于上电后主机检测到如果有设备插入到主机,则主机会对设备产生复位,因此要复位中断使能。但复位函数要做最简单的配置,一般只最状态变量进行初始化即可。

  3. 复位中断完成后就是主机要进行枚举,先发送setup数据,这时采用控制端点,因此应先使能控制端点的SETUP包功能。整个程序的处理逻辑都是枚举过程中SETUP包的处理是采用中断的方式,因此要处理SETUP包的中断处理函数。但是对于INOUT包的处理则是采用查询的功能。

  4. 枚举完成后就采用了中断端点的接收和发送,也是采用查询功能进行完成的。

  5. 对于USB设备会对D+信号线的拉高进行配置,只有主机检测到拉高才会认为有设备插入。

  6. USB初始化函数代码如图1:主要为USB模块初始化,时钟配置、端点配置、中断初始化、中断处理函数设置等操作。USB初始化完成后,应调用D+信号线上拉操作,代码如图2。上拉完成后,主机就可以检测到设备了,这时应该产生复位中断。中断处理函数如图3.4是复位中断处理函数,很简单只是修改枚举状态。


1.USB模块初始化


2.D+信号线上拉配置。

 


3.USB 服务中断函数。

 


4:复位中断处理函数

  1. 复位完成后就是枚举流程了,枚举流程都一样,只是不同类型的设备,描述符不一样。

  2. 主机会先发送SETUP包,获取设备描述符:SETUP包的组成是:SETUP包、DATA包、ACK应答;然后是IN/OUT包,可选的DATA包,状态包(0长度的数据包)。图5SETUP包的建立SETUP传输阶段,指明获取的设备描述符类型,图6SETUP数据,图7是设备的ACK应答。

5.setup建立传输

 

6.setup数据

 

 

 

 


7.设备的ACK应答

 

设备插入到主机后,主机检测到有设备插入,则会对设备进行复位,这时第一次复位(也许之前还有多次复位,但是都无所谓)。复位完成后,主机发送获取设备描述符的setup包。

主机获取数据时会先发送IN包数据,设备检测到IN包数据后开始发送响应数据。设备响应描述符期间,如果数据还没准备好,就先发送NAK,就是对应的寄存器位进行设置。

如图8就是NAK应答。

8 NAK应答

9是准好的响应数据,主机会不停的发送IN包数据,设备响应数据,主机ACK应答。然后主机会发送握手包,就是一个空包数据数据,先OUT包、空包数据、设备ACK应答,见图10

9.设备描述符应答



10.握手包

控制端点的buf大小只有8个字节,所以设备应答一次只能传输8个字节大小的数据。但是主机不会再次发送IN包,来获取后续数据,因为默认使用的端点0地址。主机获取第一包设备描述符之后知道端点buf的信息(后续接收数据就可以知道一个端点接收的最大数据长度),就会对设备再次复位(第二次复位),复位之后是对设备进行设置地址操作。后续都是通过新设置的地址进行通信。然后再重新开始获取完整的设备描述符。这些在bushold上是看不到的。图11是设置地址阶段。

 


11,设置地址SETUP包,当然后面还有一个设备的ACK应答。

对于设置地址,要看不同的芯片处理不一样,有的芯片需要将地址设置到响应的地址寄存器才能生效,有的是芯片硬件本身就能处理,不需要软件处理。

9.设置地址阶段,没有数据,因此设备直接响应握手包即可。如图12。设备发送握手包,主机会先发送IN包来获取数据,设备发送空包数据,需要软件来处理。

12 设备发送握手包。

  1. 设置完地址阶段完成后,主机就开始重新获取设备描述符,由于控制端点buf大小为8字节,主机一次获取不完,因此要发送3IN包,来获取设备描述符。如图13.这时的bushold也能抓取到数据。


13.获取设备描述符

设备描述符响应:



13.设备描述符响应

后续是一个OUT包的状态包,如图14

14.OUT包握手包状态包

下面数据是bushold抓到的数据。

  1. 设备描述符解析,标准描述符的各个字段含义解析见文章《USB设备标准描述符解析》。

CTL   80 06 00 01  00 00 12 00         GET DESCRIPTOR 主机发送获取设备描述符,长度为12字节,最后的长度两个字节要翻转,也就是大小端格式的处理。

IN    12 01 10 01  00 00 00 08  80 17 12 03 01 00 01 02  设备返回给主机的设备描述符。具体含义,如图15.

15.hid设备描述符各字段含义

实际代码中定义如下:

const uchar code DeviceDescriptorHid[] =

{

0x12,//本描述符的长度,包括自身

0x01,//设备描述符标志为1

0x10,0x01, //usb bcd

0x00,//设备类标志,这个按照规范定义即可

0x00,//

0x00,// 这两个都定义为0,说明其功能由接口描述符来指明。

USB_EP0_MAX_PACKET_SIZE,//控制端点的buf大小

USB_VID % 0x100,USB_VID /0x100,//指明VIDPID,这个要注意大小端格式,实际定义为1780,0312.实际开发过程中就自己先自定义即可。

USB_PID_HID %0x100,USB_PID_HID / 0x100,

0x01,0x00,//bcd device

0x01,//厂商字符串索引为1,字符串索引从1开始

0x02,//产品字符串索引为1,字符串索引从1开始

 

0x00,//设备字符串索引,0表示没有字符

0x01,// the number ofconfigurations


};

 

  1. SETUP包的处理

接收到一个SETUP后,设备会产生中段进入到中断处理程序,中断处理程序会判断是否为端点中断,是否为SETUP包产生的中断,见图3。如果是则进入到SETUP包处理函数中进行处理,如图16171819

 


16. Setup包的处理软件结构


17.setup包命令结构体定义

18.setup函数执行

18.获取描述符函数执行

19.查找对应的描述符返回数据

20.内部自己定义的实际描述符数据

 


  1. 获取配置描述符,主机先发送获取配置描述符,然后根据配置描述符指定的配置描述符集合的长度去获取配置描述符集合。设备返回配置描述符还是配置描述符集合是根据主机获取描述符指定的长度来返回的,如果只获取配置描述符则指定的长度就是9,而获取配置描述符集合则指定的长度是0xFF。如图21,获取长度就是0xff,设备会返回描述符集合。但是在实际枚举过程中,不同的主机可能放的不一样,但对设备而言只要实现了全部的配置描述符,根据获取的长度,要返回小于等于主机获取的长度。如图22,先获取配置描述符,图23是获取配置描述符集合。


21.获取配置描述符集合

 


22.获取配置描述符

23.获取配置描述符集合

配置描述符的应答,设备是根据请求长度返回数据的,返回的数据小于等于主机请求的长度。






24.配置描述符返回数据。配置描述符应答见图24,下面是配置描述的具体含义解析。配置描述符集合包含了配置描述符(图25)、接口描述符(图26)、HID描述符(图27)、端口描述符(28)。

 

25.配置描述

 

26.接口描述符

 

 

const uchar codeConfigDescriptorHid[] =

{

         0x09,//配置描述符长度

         0x02,//配置描述符类型

         (9 + (9 + 9 + 7 + 7)),//配置描述符集合的总长度

          0x00,      //------- configration descriptor----

          0x01, //接口数量

          0x01, 配置值从1开始,详细介绍见USB描述符文章

          0x01,//对应字符串描述符

          0x80,采用总线供电

          0x32,//获取电流值为100ma

          //---------------------------

          //-------interface ------------

          0x09,//接口描述符长度

          0x04,//接口描述符类型

          0x00,//接口序号

          0x00,//接口配置值,见USB描述符文章

          0x02,//本接口使用的端点个数

          0x03,//指定本接口的功能为HID功能,具体见USBHID规范

          0x00,//no boot

          0x00, //Protocol code 没有

          0x00,//index of string

          //---------------------------

27 HID描述符

          //-------HID-------

          0x09, //指定描述符长度

          0x21,//HID 标准请求类型0x21

          0x10,//bcd

          0x01,//bcd

          0x00,//国家码,见USB规范

          0x01,//hid设备类序号,见规范

          0x22,//报告描述符类型

          sizeof(HidReportDescriptor),//报告描述符长度

          0x00,//长度低字节

          

          //------------------

28. 端点描述符

         //---------------endpoint -------

         0x07,//端点描述符长度

         0x05,//端点描述符类型

         0x80|USB_HID_IN_ENDPOINT,//IN端点

         0x03,//interrupt endpoint //中断端点

         USB_HID_IN_BUFFERSIZE,//端点buf大小

         0x00,//buf.低字节

         0x03,//主机查询中断端点的间隔时间

//下面也是端点描述符,只是配置为OUT端点。

         0x07,

         0x05,

         0x00|USB_HID_OUT_ENDPOINT,

         0x03,

         USB_HID_OUT_BUFFERSIZE,

         0x00,

         0x03,

         //------------------------------

};

#endif

  1. 获取字符串描述符,获取字符串描述符是先获取语言ID,详细描述见USB描述符文章,如图29wValue的高字节指明是字符串还是语言ID(值为0),语言ID应答如图30

29.获取字符串描述符语言ID

30.字符语言ID应答

下面是定义的语言ID描述符。

uchar codeLanguageString[] =

{

         0x04,0x03,

         0x09,0x04,

};

然后是获取字符串描述符,见图31

31.获取字符串描述符

字符串描述符应答,如图32

        

32.字符串描述符应答。

下面是定义的字符串描述符。

const uchar codeManufactureString[] =

{

         0x12,

         0x03,

         'T',0x00,

         'e',0x00,

         'n',0x00,

         'd',0x00,

         'y',0x00,

         'r',0x00,

         '0',0x00,

         'n',0x00,

};

 

const uchar codeProductString[] =

{

         0x1e,0x03,

         'T',0x00,

         'e',0x00,

         'n',0x00,

         'd',0x00,

         'y',0x00,

         'r',0x00,

         'o',0x00,

         'n',0x00,

         ' ',0x00,

         'T',0x00,

         'o',0x00,

         'k',0x00,

         'e',0x00,

         'n',0x00,

};

14.之后主机会发送设置配置描述符,主机设置配置描述符之后,设备要配置寄存器,这样其它端点才有效。见图33.这样就枚举结束,后面就是HID协议的通信操作了,主机会发送一个SET IDLE的指令,设备响应为空即可,见图3435即可。




33.设置配置描述符

34.setidle

35.IDLE应答空数据

15.主机获取报告描述符:见图36,37.

36.获取报告描述符

37.报告描述符应答

报告描述符:

const uchar codeHidReportDescriptor[] =

{

         0x06,0xA0,0xFF, //usage page (vendordefined)

         0x09,0xA2,  //usage (vendor defined because usage page isvendor defined,this is any value)

         0xa1,0x01,  //Collection Application

         0x15,0x80,//logic mininum

         0x25,0x7F,//logic maxinum

         0x75,0x08,//report size

         0x95,REPORT_LEN,//reprot count

         0x09,0xA5,//usage (vendor defined)

         0x82,0x02,0x01,//input variable

         0x09,0xA6,

         0x92,0x02,0x01,//output variable

         0xc0 //end collection

};

详见报告描述符文章。

  1. 再后续就是HID通信了,主机会发送OUT包数据来发送数据走的是中断输出端点,IN端点用来设备发送数据到主机,见图3839.HID通信要自己定义通信协议。

 


38.out包数据

39.IN包数据