usb驱动开发之USB协议枚举过程详解

linux 驱动 同时被 2 个专栏收录
10 篇文章 1 订阅
2 篇文章 0 订阅

当usb插入之后,主机是如何识别usb的,并且对它做了什么?这个过程专业术语就叫枚举。本文把usb的枚举过程通过文字、程序和图形三种形式描述出来,并形成对照。

 枚举过程之文字描述

         •主机集线器监视着每个端口的信号电压,当有新设备接入时便可觉察。(集线器端口的两根信号线的每一根都有15kΩ的下拉电阻,而每一个设备在D+都有一个1.5kΩ的上拉电阻。当用USB线将PC和设备接通后,设备的上拉电阻使信号线的电位升高,因此被主机集线器检测到。)

   • 连接了设备的 HUB在 HOST 查询其状态改变端点时返回对应的 bitmap,告知HOST 某个PORT状态发生了改变。 
   • 主机向 HUB查询该PORT的状态,得知有设备连接,并知道了该设备的基本特性。  
   • 主机等待(至少 100mS)设备上电稳定,然后向 HUB 发送请求,复位并使能该PORT。 
   • HUB执行PORT复位操作,复位完成后该PORT就使能了。现在设备进入到defalut状态,可以从Vbus获取不超过 100mA 的电流。主机可以通过 0地址与其通讯。 
   1 主机通过0地址向该设备发送get_device_descriptor标准请求,获取设备的描述符。目的是取得却缺省控制管道所支持的最大数据包长度,该长度包含在设备描述符的bMaxPacketSize0字段中,其地址偏移量为7,主机读取64字节,但实际不一定能读到,因为这时候还不知道一次能读取的最大长度,但是肯定能读到前8个字节,因为可能的值为(8,16,32,64).
   • 主机再次向 HUB发送请求,复位该PORT。 
   2 主机通过标准请求 set_address给设备分配地址。 
   3 主机通过新地址向设备发送 get_device_descriptor标准请求,获取设备的描述符。 
   4 主机通过新地址向设备发送其他 get_configuration请求,获取设备的配置描述符。

   5 根据配置描述符的wTotalLength字段(地址偏移为2,总共两个字节,即偏移地址3表示高8位,偏移地址2表示低8位),表示该配置描述符及其包含的接口描述符、端点描述符和供应商描述等的总长度。英文原文:Total length of data returned for this configuration.Includesthe combined length of all descriptors (configuration, interface,endpoint,and class- or vendor-specific)returned for this configuration. 再次发送get_configuration请求,获取数据长度为wTotalLength。

   6 根据配置信息,主机选择合适配置,通过 set_configuration请求对设备而进行配置。这时设备方可正常使用。

二 枚举过程之程序描述

这是uboot中usb_hub.c和usb.c中精简之后的代码,可以大致看出整个过程。文字描述中的1-6都有对应的函数,前面几项描述由于uboot和linux内核的实现稍有差别,就不列出来了,只简单描述,读者自己可以查看源码。uboot是通过在命令行usb start,调用到do_usb()->usb_init()->usb_scan_devices()->usb_new_device(dev)->usb_hub_probe()->usb_hub_configure()->usb_hub_port_connect_change()。也就是说是插入u盘之后,手动输入命令,使用了一个查询过程。函数实现简单、清晰,易入手。

  内核源码也是在hub.c中,usb_hub_init()->kthread_run(hub_thread,NULL, "khubd")-> hub_thread-> hub_events()->hub_port_connect_change()。当有usb设备插入时,会唤醒hub_thread线程,从而调用hub_events执行,检测端口状态变化。

void usb_hub_port_connect_change(struct usb_device *dev, int port)
{
          /* 获取端口状态变化信息*/
          usb_get_port_status(dev, port + 1,portsts) ;
 
          /* Clear the connection change status 清除端口变化 */
          usb_clear_port_feature(dev, port + 1,USB_PORT_FEAT_C_CONNECTION);
 
        /*等待至少100ms,等待插入设备稳定*/
          mdelay(200);
 
          /* Reset the port 对设备的第一次操作,复位设备;对设备的第一次操作一定是复位。先复位,后获取
      *注意hub端口和usb设备是不同的操作,前面usb_get_port_status是对hub 端口的操作。*/
          hub_port_reset(dev, port,&portstatus) ;
         /*等待设备复位完成*/
          mdelay(200);
 
           /*好戏来了*/
          usb_new_device(usb);
         
}
int usb_new_device(struct usb_device *dev)
{
        /*包大小先初始化一个值64*/
        dev->maxpacketsize = PACKET_SIZE_64;
        /*1 获取设备描述符,读取长度64*/
        err = usb_get_descriptor(dev,USB_DT_DEVICE, 0, desc, 64);
        /*获取最大包长度*/
        dev->descriptor.bMaxPacketSize0 =desc->bMaxPacketSize0;
 
        /* reset the port for the second time第二次复位设备 */
        err = hub_port_reset(dev->parent,port, &portstatus);
 
 
        dev->epmaxpacketin[0] =dev->descriptor.bMaxPacketSize0;
        dev->epmaxpacketout[0] =dev->descriptor.bMaxPacketSize0;
 
        switch(dev->descriptor.bMaxPacketSize0) {
        case 8:
               dev->maxpacketsize  = PACKET_SIZE_8;
               break;
        case 16:
               dev->maxpacketsize =PACKET_SIZE_16;
               break;
        case 32:
               dev->maxpacketsize =PACKET_SIZE_32;
               break;
        case 64:
               dev->maxpacketsize =PACKET_SIZE_64;
               break;
        }
        dev->devnum = addr;
 
        /*2 设置设备地址*/
        err = usb_set_address(dev); /* setaddress */
 
        mdelay(10);    /* Let the SET_ADDRESS settle */
 
        tmp = sizeof(dev->descriptor);
 
        /*3 用新设置的地址获取设备描述符*/
        err = usb_get_descriptor(dev,USB_DT_DEVICE, 0, tmpbuf, sizeof(dev->descriptor));
 
        memcpy(&dev->descriptor, tmpbuf,sizeof(dev->descriptor));
 
        /* only support for one config for now*/
        /*4 和 5封装成了一个函数 获取配置描述符*/
        usb_get_configuration_no(dev, tmpbuf,0);
        usb_parse_config(dev, tmpbuf, 0);
        usb_set_maxpacket(dev);
        /* we set the default configuration here*/
       /*6 配置设备*/
        usb_set_configuration(dev,dev->config.desc.bConfigurationValue);
 
        return 0;
}
int usb_get_configuration_no(struct usb_device *dev, unsigned char *buffer, int cfgno)
{
	struct usb_configuration_descriptor *config;
	config = (struct usb_configuration_descriptor *)&buffer[0];
	/*4 获取配置描述符,仅配置描述使长度为9*/
	result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno, buffer, 9);
	/*当前配置下的描述符总长度*/
	tmp = le16_to_cpu(config->wTotalLength);
	/*5 再一次获取当前配置下的全部配置、接口、端点信息*/
	result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno, buffer, tmp);
	return result;
}

三  枚举过程之图形描述

枚举的总过程,看过下面的分解过程,再回头看这个总图就非常容易了。


1. 获取设备描述符GET_DESCRIPTOR

      总线复位及向默认地址0发送GET_DESCRIPTOR指令包,请求设备描述。第一获取描述符要先复位设备,然后等待至少100ms(100ms可以满足大多数设备),这里实际只等待了43ms。如图一所示:


                                                                                    图一

1)Index[4 - 5]:表示USB插入总线复位
2)Index[7 - 8]:表示主机向默认地址发送GET_DESCRIPTOR指令包,详细信息也抓出来了,如图所示:

                                                                  
3)Index[15 - 17]:表示设备向主机发送设备描述数据Index[16]
4)Index[18 - 19]:表示主机完成GET_DESCRIPTOR指令后,给设备发送一个空应答;
 
数据是由二进制数字串构成的,首先数字串构成域(有七种:同步域(SYNC)、标识域(PID)、地址域(ADDR)、端点域(ENDP)、数据域(DATA)、帧号域(FRAM)、校验域(CRC)),域再构成包(比如握手包:格式如下  SYNC+PID,ACK属于PID的一种),包再构成事务(IN、OUT、SETUP,每一种事务都由令牌包、数据包、握手包三个阶段构成),事务最后构成传输(中断传输、并行传输、批量传输和控制传输)。关于usb包结构,请看下一篇文章,这里先知道是这么回事就行。
这里先解析一下设置事务的值:80 06 00 01 00 00 40 00
/* device request (setup) */
struct devrequest {
unsigned char requesttype;
unsigned char request;
unsigned shortvalue;
unsigned shortindex;
unsigned shortlength;
} __attribute__ ((packed));
requesttype=0x80,request=0x06,value=0x0100,index=0x0000,length=0x0040;
根据usb2.0协议9.4.3节描述,获取描述符时requesttype=0x80,request=0x06,这个是协议规定的。
value=0x0100,高字节表示描述符类型,01表示设备,02表示配置;低字节表示索引。比如设备有多个配置,那需要读取不同配置的时候就通过低字节。或者一个配置下有多个接口,通过索引选择不同的接口。
index=0x0000,这个参数如果为0,则不关心;如果为非零,则表示Langurage ID,每一位都有对应的意义。
length=0x0040,表示请求的数据包长度为64.
所以本次设置事务的目的明确了,获取设备描述符,长度为64字节。
输入事务,是usb设备对请求进行回应,传输了16个字节的数据,为什么是传输了16个字节而不是64字节,看看偏移地址7 bMaxPacketSize0=0x10,即该设备的最大包传输大小为16字节,如果超过16字节,需要多次传输。实际设备描述符大小为18,可以看第三步的图,传输完16字节之后,主机再次发送令牌环,设备把剩下的2字节传输完成。这里我们只要获得 bMaxPacketSize0值就可以了,所以接下来直接对其进行了复位操作,否则设备还在等待传输剩余的2个字节呢。
struct usb_device_descriptor {
u8 bLength;
u8 bDescriptorType;
u16 bcdUSB;
u8 bDeviceClass;
u8 bDeviceSubClass;
u8 bDeviceProtocol;
u8 bMaxPacketSize0;
u16 idVendor;
u16 idProduct;
u16 bcdDevice;
u8 iManufacturer;
u8 iProduct;
u8 iSerialNumber;
u8 bNumConfigurations;
} __attribute__ ((packed));
输出事务:因为获取描述符之前把设备包大小的初始值设为了64,所以输入事务之后,就认为传输完成,主机发送了一个输出事务,响应设备,已经收到数据。

2. 设置地址SET_ADDRESS

     再次复位总线及向设备发送SET_ADDRESS指令包,设置设备地址。如图二所示:
 
                                                                                     图二
1)Index[22 - 23]:表示再次总线复位,该复位自动完成,不是手工插拔USB完成
2)Index[25 - 27]:表示主机向默认地址发送SET_ADDRESS指令包,详细信息如图所示:
                                           
设置地址为1

3)Index[29 - 31]:表示设备完成SET_ADDRESS指令后,给主机发送一个空应答;


设置地址为0x0002,由于我是从网上找的图,所以两幅图地址设置的不一样,这里注意一下就行。

3.请求设备描述符GET_DESCRIPTOR

    向第二步设定的设备地址发送GET_DESCRIPTOR指令包,请求设备描述。如图三所示:
 
                                                                                      图三
1)Index[33 - 35]:表示主机向地址01发送GET_DESCRIPTOR指令包,详细信息如图
                                                               
2)Index[41 - 43]:表示设备向主机发送设备描述数据Index[42]
3)Index[45 - 47]:表示设备向主机发送设备描述数据Index[46]
4)Index[48 - 50]:表示主机完成GET_DESCRIPTOR指令后,给设备发送一个空应答
   

4. 请求配置描述符GET_DESCRIPTOR

向第二步设定的设备地址发送GET_DESCRIPTOR指令包,请求配置描述。如图四所示:
 
                                                                                  图四
1)Index[52 - 54]:表示主机向地址01发送GET_DESCRIPTOR指令包,详细信息如图所示
                                                       
2)Index[57 - 59]:表示设备向主机发送配置描述数据Index[58]
3)Index[60 - 62]:表示主机完成GET_DESCRIPTOR指令后,给设备发送一个空应答;
 
解析输入事件获取的配置信息:
struct usb_configuration_descriptor {
u8 bLength;                      /*09,描述符长度为9*/
u8 bDescriptorType;/* 0x2, 表示配置描述符*/
u16 wTotalLength;          /*0x002E,表示当前配置下的各种描述信息总长度为46*/
u8 bNumInterfaces;        /*0x01,当前配置下共一个接口*/
u8 bConfigurationValue; /*0x01,当前配置索引,当设置某一配置时,给SetConfiguration(x)传递的参数*/
u8 iConfiguration;/*0x00,字符串描述索引*/
u8 bmAttributes;/*0x60,属性信息*/
u8 bMaxPower;/*0x01当前配置最大消耗电流*/
} __attribute__ ((packed));

5. 读取完整设备描述及配置描述

再次读取配置描述符,读取长度46字节。
 
    09 02 2E 00 01 01 00 60 01 和第四步获得的一样
    09 04 00 00 04 00 00 00 00 描述符长度也是09,04表示是一个接口描述符
    07 05 81 03 08 00 c8 描述符长度为07,05表示端点描述符
    07 05 01 03 08 00 c8
    07 05 82 02 40 00 00
    07 05 02 02 40 00 00
接口描述符详解:
struct usb_interface_descriptor {
u8 bLength;                     /*0x09,描述符长度*/
u8 bDescriptorType;/* 0x04 ,表示接口描述符*/
u8 bInterfaceNumber;    /*0,接口号为0*/
u8 bAlternateSetting;    /*0,接口的可选设置*/
u8 bNumEndpoints;     /*04,当前接口共4个端点*/
u8 bInterfaceClass;       /*0*/
u8 bInterfaceSubClass; /*0*/
u8 bInterfaceProtocol;  /*0*/
u8 iInterface;                /*0*/
} ;
struct usb_endpoint_descriptor {
u8 bLength;                     /*07,描述符长度*/
u8 bDescriptorType;/* 0x5 ,表端点描述符*/
u8 bEndpointAddress; /*0x81,bit[7] =1,表示输入端点,0表示输出端点;bit[6:4],保留;bit[3:0]端点号,为1*/
u8 bmAttributes; /*0x03,bit[1:0]=11,表传输类型为中断传输*/
u16 wMaxPacketSize; /*0x0008,当前端点最大发送和接收包大小*/
u8 bInterval; /*0xc8,查询时发送数据的间隔时间*/
} ;

6 配置设备SET_CONFIGURATION

       向第二步设定的设备地址发送SET_CONFIGURATION指令包,设置配置描述。如图六所示:
 
                                                                     图六
1)Index[139 - 141]:表示主机向地址01发送SET_CONFIGURATION指令包,详细信息如下:
                                            
2)Index[143 - 145]:表示设备完成SET_CONFIGURATION指令后,给主机发送一个空应答
至此,枚举过程结束,设备可通过驱动与主机通信了。
 
/* device request (setup) */
struct devrequest {
unsigned char requesttype;  /*0x00,表请求设置*/
unsigned char request;        /*0x09,表示设置配置*/
unsigned shortvalue;   /*0x0001,使用配置,必须匹配从配置描述符都过来的bConfigurationValue*/
unsigned shortindex;   /*0x0000,协议规定设为0*/
unsigned shortlength;  /*0x0000,协议规定设为0*/
} __attribute__ ((packed));

四 总结

下面举几个例子来说明USB的通讯过程:
1:主机想要向设备传送一串数据。 过程如下:
(1) 主机向从机发送 “令牌包”,令牌包的类型为输出包,表示主机要向从机发送数据了。
(2) 主机向从机发送完令牌以后,USB处理器件根据发送的令牌,会将中断状态寄存器标志置位,从机CPU通过查询USB处理器件的中断状态寄存器,对主机的令牌包进行响应
(3)从机判别出中断类型,于是,准备从主机接收数据。
(4) 从机准备好了,于是主机开始发送“数据包” 这时,USB处理器件会自动将从主发送过来的数据放如它的内部缓冲区内,接收完这个数据包后,从机向主机发送“应答包” 
这就是一个完整的通讯过程。
由以上可以看出,USB若是想要传送数据,那么主机必须先发一个 IN 或OUT的令牌包,然后发送DATA0,或DATA1数据包。
简单的用现实生活中的事件进行描述:  老板想让员工去做一件事情,老板 先会发出命令,告诉要做什么事情,员工准备好以后呢,老板再把做这件事情的经费发放给员工,当员工把发放的经费清点以后,发现数目正确,他会给老板一个回应信息,告诉老板,钱已经收到了,而且数目正确。
老板想让员工做的事:  对应USB通讯里的令牌包。
老板想要发放的经费:  对应USB通讯里的数据包。
员工给老板的回应:    对应USB通讯里的握手包。
这里尤其需要注意一个问题就是:
USB主机向设备发送令牌包的时候,接收令牌是有USB器件来完成的,而不是有从机CPU来完成的,如主机发送一个如下的令牌:
SYNC+PID+ADDR+ENDP+CRC5
USB器件回根据PID的类型来判断是哪种类型的令牌 根据ADDR的值来判断是否是和自己通讯,根据ENDP的值来判断是和哪个端点进行通讯,根据校验来判断,数据传送是否无误。根据以上的令牌包信息,USB器件会将其内部的中断状态寄存器相应的位置位,从机CPU可以查询这个中断状态寄存器来进行相应的操作。





  • 5
    点赞
  • 0
    评论
  • 23
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

ToureYaya

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值