USB枚举过程

USB枚举过程剖析


1. 主机集线器检测到新设备(热插拔检测)

  • USB主机端,集线器端口的DATA+和 DATA-差分线上都有下拉15K 的电阻。
  • 从机端,USB 设备端(鼠标键盘等),都有 1.5K 的上拉电阻。当 USB 接口空置的时候,集线器上检测到的 DATA+和 DATA-都是低电平,当 USB 设备插入的时候,就会被拉高,这个时候是产生低电平到高电平的变化。

USB主机集线器监视着每个端口的信号电压,当用USB线将PC和设备接通后,设备的上拉电阻使信号线的电位升高,因此被主机集线器检测到。反之,设备拔出,产生高电平到低电平的变化,集线器检测到设备拔出。

2. 主机发送Get_Status请求

每个集线器用中断传输来报告在集线器上的事件。当主机知道了这个事件,它给集线器发送一个Get_Status请求来了解更多的消息,返回的消息告诉主机一个设备是什么时候连接的。

3. 主机发送Set_Feature请求,集线器重启端口

当主机知道有一个新的设备时,主机给集线器发送一个Set_Feature请求,请求集线器来重启端口。集线器使得设备的USB数据线处于重启(RESET)状态至少10ms。 在重启device之前,host和hub通信,确认device存在。

4. 集线器在设备和主机之间建立一个信号通路

主机发送一个Get_Status请求来验证设备是否激起重启状态。返回的数据有一位表示设备仍然处于重启状态。当集线器释放了重启状态,设备就处于默认状态了,设备已经准备好通过Endpoint 0 的默认流程响应控制传输,即设备现在使用默认地址0x0与主机通信。
Endpoint 是指的端点,在 USB 通信中,我们知道是主从通信,通信的双方主体是主机端设备的端点和 USB 设备。由于一个 USB 口,可能不止一个端点,在 USB协议中规定,USB 接口中必须有端点 0,也就是 Endpoint 0。其中还有设备描述符、配置描述符、接口描述符、端点描述符四个部分。现在我们只需要知道,主机和从机通信是通过端点来进行,而端点 0 是任何一个 USB 设备都有的。

5. 集线器检测设备速度

集线器通过测定哪根信号线(D+或D-)在空闲时有更高的电压来检测设备是低速设备还是全速设备。(全速和高速设备D+有上拉电阻,低速设备D-有上拉电阻)。
这部分是由具体的 USB 设备生产商(鼠标键盘)设计决定的,一般高速设备都是兼容低速设备的,而且完全由集线器来检测。

前五个步中,主要工作是检测到 USB 设备、向主机报告在某个时间点有一个 USB 设备接入、集线器重启接入 USB的端口、集线器在主控制器和 USB 设备之间建立一个信号通路,最后集线器检测设备速度。

经过前面 5 个步骤,主控制器和 USB 设备之间建立了一个信号通路,而且主控制获取了设备是高速设备还是低速设备的信息。

在所有的 USB 设备中(不管是鼠标、键盘、蓝牙,还是网卡、U 盘等等),设备内部都有一个存储器,用于保存设备的信息。例如:生产厂商、产品的 ID 等等。存储器中还包含了一些具体的通信协议,在设备驱动中,需要根据USB 设备中的信息来进行配置初始化工作。
主机从 USB 设备中获取的信息,在 USB 协议中是有规定的,其中通过设备描述符、配置描述符、接口描述符、端点描述符来组织。

在 PC 上,我们接入一个 USB 设备之后,有时它会在右下角提醒,“发现新的 USB 设备XXX,已经可以使用”或者“发现新的 USB 设备 XXX,驱动无法加载”。我们可以判断出,USB 设备在接入之前,驱动是没有安装到内核中的,只有在 USB 设备被检测到,而且内核中有该设备对应的驱动,USB 设备的驱动才会被加载。

以下需要USB的firmware进行干预 。

6. 获取最大数据包长度

PC 向address 0发送USB协议规定的Get_Device_Descriptor命令,以取得缺省控制管道所支持的最大数据包长度,并在有限的时间内等待USB设备的响应。该长度包含在设备描述符的bMaxPacketSize0字段中,其地址偏移量为7,所以这时主机只需读取该描述符的前8个字节。注意,主机一次只能枚举一个USB设备,所以同一时刻只能有一个USB设备使用缺省地址0。 获取最大数据包长度是后面通信的基础。

以下操作雷同,不同操作系统设定时延是不一样的,比如说win2k大概是几毫秒,如果没有反应就再发送一次命令,重复三次。

7. 主机分配一个新的地址给设备

主机通过发送一个Set_Address请求来分配一个唯一的地址给设备。设备读取这个请求,返回一个确认,并保存新的地址。从此开始所有通信都使用这个新地址。

8. 主机重新发送Get_Device_Descriptor命令,读取完整设备描述符

主机向新地址重新发送Get_Device_Descriptor命令,此次读取其设备描述符的全部字段,以了解该设备的总体信息,如VID,PID。

9. 主机发送Get_Device_Configuration命令,获取完整配置信息

主机向设备循环发送Get_Device_Configuration命令,要求USB设备回答,以读取全部配置信息。

10. 主机发送Get_Device_String命令,获得描述字符集(unicode)

描述字符集包括了产商、产品描述、型号等信息。

11. 主机展示新设备信息

此时主机将会弹出窗口,展示发现新设备的信息,产商、产品描述、型号等。

12. PC判断能否提供该类USB的驱动

根据Device_Descriptor和Device_Configuration应答,PC判断是否能够提供USB的Driver,一般能提供几大类的设备,如游戏操作杆、存储、打印机、扫描仪等,操作就在后台运行。这一步中,加载的驱动就是设备驱动,也就是我们需要关注的驱动。

13. 主机发送Set_Configuration(x)命令,请求为设备选择一个配置

加载了USB设备驱动以后,主机发送Set_Configuration(x)命令请求为该设备选择一个合适的配置(x代表非0的配置值)。如果配置成功,USB设备进入“配置”状态,并可以和客户软件进行数据传输。

此时,常规的USB完成了其必须进行的配置和连接工作。查看注册表,能够发现相应的项目已经添加完毕,至此设备应当可以开始使用。不过,USB协议还提供了一些用户可选的协议,设备如果不应答,也不会出错,但是会影响到系统的功能。


STM32的USB枚举过程介绍引用

枚举前的工作

根据STM32的USB库做移植,介绍枚举过程,SetSystem函数是一些初始化化设置。

  • 首先系统执行USB中断设置:USB_Interrupts_Config(); //中断向量表设置
  • 然后执行USB时钟设置:Set_USBClock(); // USB使用48M时钟,
  • 然后执行USB初始化设置:USB_Init(); // 初始化主要进行结构体及函数指针
void USB_Init(void)
{
    pInformation = &Device_Info;//当前的连接状态与信息,关于Device_Info的信息,自己go to 去查看
    pInformation->ControlState = 2;//当前的控制状态设置为IN_DATA
    pProperty = &Device_Property;//设备本身支持的属性和方法
    pUser_Standard_Requests =&User_Standard_Requests;//主机请求的实现方法
    /* Initialize devices one by one */
    pProperty->Init();//回调设备的初始化例程
}

这里面还调用了pProperty->Init();函数进行初始化,它的函数主体是Joystick_init();函数程序执行到Joystick_init();函数,首先Get_SerialNum();获取STM32内部唯一标识码。然后赋给Joystick_StringSerial。
接下来执行PowerOn();函数,这个函数首先把D+的上拉电阻上电,这样电脑就可以检测到设备了。(集线器报告设备连接状态,并收到主机指令后,会复位 USB总线,这需要一定的时间(这段时间内设备应该准备好处理复位指令)。但是现在设备初始化程序将继续往下进行,因为它还没有使能复位中断)
接着执行语句

<span style="font-size:10px;">  /*** CNTR_PWDN = 0 ***/
wRegVal = CNTR_FRES;
_SetCNTR(wRegVal);</span>

这两句话的含义实际上是使能了USB模块电源,因为上电复位时,CNTR寄存器的断电控制位PNWN位是1,模块是断电的,语句将强制复位USB模块,直至

wInterrupt_Mask = 0;
_SetCNTR(wInterrupt_Mask);

语句清除复位信号,然后终止复位操作,这个过程中因为复位中断允许位CNTR_RESETM没有被使能,所以,不会触发复位中断,而是间接使PDWN=0,模块开始工作。

wInterrupt_Mask =CNTR_RESETM | CNTR_SUSPM| CNTR_WKUPM;
_SetCNTR(wInterrupt_Mask);

执行语句后,复位中断,挂起中断,唤醒中断被允许,而此时集线器多半已经开始复位端口了,或者说稍微有限延迟,设备固件还能继续初始化一些部件,但已经不会影响整个工作流程了。那么实际上程序确实直接进入了复位中断。

程序直接进入了USB_LP_CAN1_RX0_IRQHandler的中断口,执行 USB_Istr();,通过匹配发现中断源为复位中断,程序运行到复位中断部分,开始执行复位程序(这里提一下,判断发生复位中断后,首先清中断位。在其它几个中断源也是先清中断位,但是CTR_LP();函数之前却没有清中断位是为什么呢?在STM32参考手册中是这样说的:端点在成功完成一次传输后, CTR位会被硬件置起,如果USB_CNTR上的相应位也被设置的话,就会产生中断。与端点相关的中断标志和USB_CNTR寄存器的CTRM位无关。这两个中断标志位将一直保持有效,直到应用程序清除了USB_EpnR寄存器中的相关中断挂起位(CTR位是个只读位)。)

Device_Property.Reset();
这个函数指针,指向的函数是Joystick_Reset。

voidJoystick_Reset(void)
{ 
  /* Set Joystick_DEVICE as not configured */
  pInformation->Current_Configuration = 0;
  pInformation->Current_Interface = 0;/*thedefault Interface*/
  
  /* Current Feature initialization */
  pInformation->Current_Feature =Joystick_ConfigDescriptor[7];//供电方式,总线供电,自供电
  SetBTABLE(BTABLE_ADDRESS);//设置包缓冲区地址
  /* Initialize Endpoint 0 */
  SetEPType(ENDP0, EP_CONTROL);//端点0为控制端点
  SetEPTxStatus(ENDP0, EP_TX_STALL);//端点状态为发送无效,也就是主机IN令牌包来的时候,回送一个STALL
  SetEPRxAddr(ENDP0, ENDP0_RXADDR);//设置端点0的描述符表,包括接收缓冲区地址,最大允许接收的字节数、发送缓冲区地址三个量。
  SetEPTxAddr(ENDP0, ENDP0_TXADDR);//发送缓冲区地址
  Clear_Status_Out(ENDP0);///清除EP_KIND的STATUS_OUT位,如果改位被设置, 在控制模式下只对0字节数据包相应。其它的都返回STALL。主要用于控制传输的状态过程
  SetEPRxCount(ENDP0,Device_Property.MaxPacketSize);///接收缓冲区支持64个字节
  SetEPRxValid(ENDP0);//使能端点0的接收,因为很快就要接收SETUP令牌包了
 
  /* Initialize Endpoint 1 */
  SetEPType(ENDP1, EP_INTERRUPT);//端点1设为中断端点
  SetEPTxAddr(ENDP1, ENDP1_TXADDR);//设置发送缓冲区地址
  SetEPTxCount(ENDP1, 4);//每次发送4个字节
  SetEPRxStatus(ENDP1, EP_RX_DIS);///接收禁止,只发送 Mouse 信息,而不从主机接收
  SetEPTxStatus(ENDP1, EP_TX_NAK);///现在发送端点还不允许发送数据
 
  /* Set this device to response on defaultaddress */
  SetDeviceAddress(0);//址默认为 0.
  bDeviceState = ATTACHED;//接状态改为已经连接,默认地址状态
}

这个复位的过程我们用一句话总结就是端点初始化。至此RESET中断处理完成,因为我们也没有设置RESET_CALLBACK,所以也不会有RESET_Callback();,不过它本身也是个空函数。在执行完复位函数后PC指针又回到main函数中来,Joystick_init函数往下执行, USB_SIL_Init();函数中使能所有常用中断,当开复位中断后,程序进入中断,那么复位中断执行完成以后,真正的枚举的过程(获取描述符)并没有开始。而是有一段空余的时间,这段时间内,程序执行了上面的提到的USB_SIL_Init();直至USB_Init();函数结束。那么真正的USB枚举过程都是在中断中完成。

通过我调试发现,在完成复位中断后,主机好像并没有发送获取描述符的命令,在这个过程中,系统两次进入挂起中断,然后触发唤醒中断,然后复位中断。我个人的理解是,这段时间因为stm32没有收到sof,所有触发挂起中断,当设备发送获取设备描述符命令时,触发唤醒中断,在唤醒中断中,系统设置恢复正常工作。至于最后的复位请求,我表示我也不知道了。。。(这里需要说一下,在移植的挂起中断处理程序中,针对stm32部分也做了挂起处理,所以,只保留了USB挂起,而对整个stm32的挂起被屏蔽掉了。)(此时pInformation->ControlState=IN_DATA, bDeviceState=UNCONNECTED)

枚举过程

枚举过程就是USB设备与主机通讯获取设备的一系列描述符的过程。
在这里插入图片描述
据说这是圈圈大神整理的枚举过程图,接下来我们来一步一步的看枚举的过程。整个枚举的过程以我实际调试的结果为准。

1、获取设备描述符

在这helai描述
注:packet包是实际传输时的数据包。

首先是建立连接阶段,在经过漫长的等待以后,主机发出对地址0、端点0发出SETUP令牌包和数据包,然后设备返回ACK包。点0寄存器的CTR_RX被置位为1, ISTR的CTR置位为1, DIR=1, EP_ID=0,表示端点0接收到主机来的请求数据。此时设备已经ACK主机,将触发正确传输完成中断。程序进入CTR_LP();中断,我们来详细看一下

void CTR_LP(void)
{
  __IO uint16_t wEPVal = 0;
       
  /* stay in loop while pending interrupts */
  while (((wIstr = _GetISTR()) & ISTR_CTR)!= 0)
  {
    /* extract highest priority endpoint number*/
   EPindex = (uint8_t)(wIstr & ISTR_EP_ID);//判断读取端点号
    if (EPindex == 0)
    {    
             SaveRState = _GetENDPOINT(ENDP0);//USB端点0寄存器  USB_EP0R中的数据
             SaveTState = SaveRState & EPTX_STAT;//获取发送状态码
             SaveRState &=  EPRX_STAT; 
 
             _SetEPRxTxStatus(ENDP0,EP_RX_NAK,EP_TX_NAK);//将TXRX的状态都改为NAK
 
      if ((wIstr & ISTR_DIR) == 0)//判断是IN包
      {
 
        _ClearEP_CTR_TX(ENDP0);
        In0_Process();
           _SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
                      return;
      }
     else//判断是OUT包或SETUP包
      {
        wEPVal = _GetENDPOINT(ENDP0);
       
       if ((wEPVal &EP_SETUP) != 0)//判断是SETUP包
        {
         Setup0_Process();
         _SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
          return;
        }
        else if ((wEPVal & EP_CTR_RX) !=0)//判断是OUT包
        {
          _ClearEP_CTR_RX(ENDP0);
          Out0_Process();  
 _SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
          return;
        }
      }
    }/* if(EPindex == 0) */
    else//判断是非0端点包
    {
    }/*if(EPindex == 0) else */
  }/* while(...) */
}

其实以上这些判断在实际的USB协议中,都在令牌包中都有体现,但是STM32的USB硬件都帮我们分析了,这些分析的结果以寄存器的形式显示可以看到程序跳转执行Setup0_Process();我们跟踪过来继续看它的跟踪过程

uint8_tSetup0_Process(void)
{
 
  union
  {
    uint8_t* b;
    uint16_t* w;
  } pBuf;
  uint16_t offset = 1;
   
  pBuf.b = PMAAddr + (uint8_t*)(_GetEPRxAddr(ENDP0) * 2); /* *2 for 32 bits addr 要读取数据的地址*/
 
  if (pInformation->ControlState!= PAUSE)
  {
    pInformation->USBbmRequestType =*pBuf.b++; /* bmRequestType */
    pInformation->USBbRequest = *pBuf.b++;/* bRequest */
    pBuf.w += offset;  /* word not accessed because of 32 bitsaddressing */
    pInformation->USBwValue =ByteSwap(*pBuf.w++); /* wValue */
    pBuf.w += offset;  /* word not accessed because of 32 bitsaddressing */
    pInformation->USBwIndex  = ByteSwap(*pBuf.w++); /* wIndex */
    pBuf.w += offset;  /* word not accessed because of 32 bitsaddressing */
    pInformation->USBwLength = *pBuf.w; /*wLength */
  }//将USB标准设备请求赋给对应的结构体
 
 
  pInformation->ControlState = SETTING_UP;
  if (pInformation->USBwLength == 0)
  {
    /* Setup with no data stage */
    NoData_Setup0();
  }
 <span style="color:#ff0000;">else</span>
  {
    /* Setup with data stage */
   Data_Setup0();
  }
  return Post0_Process();
}

因为这个标准设备请求是来要求设备返回设备描述符的,需要返回数据,所以程序会跳转到Data_Setup0();执行在这个函数里分析要返回的数据。

voidData_Setup0(void)
{
  uint32_t Request_No =pInformation->USBbRequest;//获取请求代码
 
  /*GET DESCRIPTOR*/
  if (<spanstyle="color:#ff0000;">Request_No ==GET_DESCRIPTOR</span>)//判断为获取设备描述符
  {
    if(<span style="color:#ff0000;">Type_Recipient ==(STANDARD_REQUEST | DEVICE_RECIPIENT)</span>)//标准请求
    {
      uint8_t wValue1 =pInformation->USBwValue1;
      if (<spanstyle="color:#ff0000;">wValue1 ==DEVICE_DESCRIPTOR</span>)//判断是获取设备描述符
      {
        <spanstyle="color:#ff0000;">CopyRoutine =pProperty->GetDeviceDescriptor;//复制数据到CopyRoutine</span>
      }
    }
  }
 
  if (<spanstyle="color:#ff0000;">CopyRoutine</span>)//有数据将一些信息写入控制结构体
  {
    pInformation->Ctrl_Info.Usb_wOffset =wOffset;
    pInformation->Ctrl_Info.CopyData =CopyRoutine;
    /* sb in the original the cast to word wasdirectly */
    /* now the cast is made step by step */
    (*CopyRoutine)(0);//这个函数这里调用的目的只是设置了 pInformation中需要写入的描述符的长度
 
    Result = USB_SUCCESS;
  }
 
  if (<spanstyle="color:#ff0000;">ValBit(pInformation->USBbmRequestType,7)</span>)//判断是从设备到主机
  {
    /* Device ==> Host */
    __IO uint32_t wLength =pInformation->USBwLength;
    
    /* Restrict the data length to be the onehost asks for */
    if (pInformation->Ctrl_Info.Usb_wLength> wLength)
    {//设备描述符长度为18
      pInformation->Ctrl_Info.Usb_wLength =wLength;
    }  
    pInformation->Ctrl_Info.PacketSize =pProperty->MaxPacketSize;   
    <spanstyle="color:#ff0000;">DataStageIn();</span>//完成描述符的输出准备
  }
  return;
}

最后的重点落在 DataStageIn();函数上,我们来看一下

voidDataStageIn(void)
{
  ENDPOINT_INFO *pEPinfo =&pInformation->Ctrl_Info;
  uint32_t save_wLength =pEPinfo->Usb_wLength;//还需要发送多少字节
  uint32_t ControlState =pInformation->ControlState;
  DataBuffer= (*pEPinfo->CopyData)(Length);//取得要发送的数据设备描述符
  UserToPMABufferCopy(DataBuffer,GetEPTxAddr(ENDP0), Length);将设备描述符复制到用户的发送缓冲区
  SetEPTxCount(ENDP0, Length);//设置发送字节数目
 
  pEPinfo->Usb_wLength -= Length;//等于0
  pEPinfo->Usb_wOffset += Length;//偏移到18
  vSetEPTxStatus(EP_TX_VALID);///使能端点发送,只要主机的 IN令牌包一来,SIE就会将描述符返回给主机
  USB_StatusOut();/* 这个实际上是使接收也有效,主机可取消 IN
Expect_Status_Out:
    pInformation->ControlState =ControlState;
}

这一次的CTR中断就执行完了,就等着,主机再发IN包把数据读回去。

接下来就是可选的数据传输阶段。主机首先发一个 IN令牌包,由于端点0发送有效, SIE将数据返回主机。主机方返回一个 ACK后,主机发送数据的 CTR标志置位, DIR=0, EP_ID=0,表明主机正确收到了用户发过去的描述符。固件程序由此进入IN中断。所以接下来进入用 In0_Process()进行处理。但是此时设备描述符返回成功了。同样的在用In0_Process()函数中,其目的只是期待主机的0状态字节输出了。

uint8_tIn0_Process(void)
{
  uint32_t ControlState =pInformation->ControlState;
   
  if ((ControlState == IN_DATA) || (<spanstyle="color:#ff0000;">ControlState == LAST_IN_DATA</span>))
  {
    <span style="color:#ff0000;">DataStageIn();//此次调用后,当前状态变成WAIT_STATUS_OUT,表明设备等待状态过程,主机输出0字节</span>
    /* ControlState may be changed outside thefunction */
    ControlState =pInformation->ControlState;
  }
  else if (ControlState == WAIT_STATUS_IN)
  {
  } 
  else
  {
    ControlState = STALLED;
  }
  pInformation->ControlState = ControlState;
  return Post0_Process();
}
    我们进入DataStageIn();函数


voidDataStageIn(void)
{
  ENDPOINT_INFO *pEPinfo =&pInformation->Ctrl_Info;
  uint32_t save_wLength = pEPinfo->Usb_wLength;
  uint32_t ControlState =pInformation->ControlState;
 
  uint8_t *DataBuffer;
  uint32_t Length;
    
  if ((save_wLength == 0) && (<spanstyle="color:#ff0000;">ControlState == LAST_IN_DATA</span>))
  {
    if(Data_Mul_MaxPacketSize == TRUE)
    {
      /* No more data to send and empty packet*/
      Send0LengthData();
      ControlState = LAST_IN_DATA;
      Data_Mul_MaxPacketSize = FALSE;
    }
   <spanstyle="color:#ff0000;"> else </span>
    {
      /* No more data to send so STALL the TXStatus*/
      ControlState = WAIT_STATUS_OUT;
      vSetEPTxStatus(EP_TX_STALL);
    }   
    <spanstyle="color:#ff0000;">goto Expect_Status_Out;</span>
  }
  Length = pEPinfo->PacketSize;
  ControlState = (save_wLength <= Length) ?LAST_IN_DATA : IN_DATA;
 
  if (Length > save_wLength)
  {
    Length = save_wLength;
  }
 
  DataBuffer = (*pEPinfo->CopyData)(Length);
 
  UserToPMABufferCopy(DataBuffer,GetEPTxAddr(ENDP0), Length);
 
  SetEPTxCount(ENDP0, Length);
  pEPinfo->Usb_wLength -= Length;
  pEPinfo->Usb_wOffset += Length;
  vSetEPTxStatus(EP_TX_VALID);
  USB_StatusOut();/* Expect the host to abortthe data IN stage */
 
Expect_Status_Out:
    pInformation->ControlState =ControlState;  
}

主机收到18个字节的描述符后,进入状态事务过程,此过程的主句发送令牌包为OUT,和0字节的数据包,设备需要回一个ACK。中断处理程序会进入Out0_Process()。由于此时状态为WAIT_STATUS_OUT,所以执行以下这段

<spanstyle="font-size:10px;">uint8_t Out0_Process(void)
{
  uint32_t ControlState =pInformation->ControlState;
 
  if ((ControlState == IN_DATA) ||(ControlState == LAST_IN_DATA))
  {
  }
  else if ((ControlState == OUT_DATA) ||(ControlState == LAST_OUT_DATA))
  {
  }
 
  else if (<spanstyle="color:#ff0000;">ControlState ==WAIT_STATUS_OUT</span>)
  {
    (*pProperty->Process_Status_OUT)();//空函数什么也没干
   <spanstyle="color:#ff0000;"> ControlState = STALLED;</span>
  }
  /* Unexpect state, STALL the endpoint */
  else
  {
    ControlState = STALLED;
  }
  pInformation->ControlState = ControlState;
 
  return Post0_Process();
}</span>

2、主机发送复位命令

获取设备描述符后,主机再一次复位设备。设备又进入初始状态

3、设置地址

在这里插入图片描述

  1. 重新从复位状态开始
    在第一次获取设备描述符后,程序使端点 0 的发送和接收都无效,状态也设置为STALLED,所以主机先发一个复位,使得端点 0接收有效。 虽然说在 NAK和STALL状态下,端点仍然可以响应和接收 SETUP包。
  2. 设置地址的建立阶段
    主机先发一个 SETUP令牌包,设备端 EP0的 SETUP标志置位。然后主机发了一个OUT 包,共 8个字节,里面包含设置地址的要求(USB标准设备请求)。设备在检验数据后,发一个 ACK握手包。同时 CTR_RX 置位,CTR置位。数据已经保存到 RxADDR 所指向的缓冲区。此时 USB产生数据接收中断。由于 CTR_RX和 SETUP同时置位, 终端处理程序调用 Setup0_Process(),所做的工作仍然是先填充 pInformation结构, 获取请求特征码、 请求代码和数据长度。由于设置地址不会携带数据,所以接下来调用 NoData_Setup0()。执行以下代码:
void NoData_Setup0(void)
{
 RESULT Result = USB_UNSUPPORT;
 uint32_t RequestNo = pInformation->USBbRequest;
 uint32_t ControlState;
   
 if (Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
 {
    if (RequestNo == SET_CONFIGURATION)
    {
    }
 
    /*SET ADDRESS*/
    else if (RequestNo== SET_ADDRESS)
    {
      if ((pInformation->USBwValue0 > 127)|| (pInformation->USBwValue1 != 0)
          || (pInformation->USBwIndex != 0)
          ||(pInformation->Current_Configuration != 0))
        /* Device Address should be 127 orless*/
      {
        ControlState = STALLED;
        goto exit_NoData_Setup0;
      }
     else
      {
       Result =USB_SUCCESS;//说明设置地址没有做任何工作
      }
    }
    /*SET FEATURE for Device*/
    else if (RequestNo == SET_FEATURE)
    {
    }
    /*Clear FEATURE for Device */
    else if (RequestNo == CLEAR_FEATURE)
    {
    }
 }
 
 /* Interface Request*/
 else if (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
 {
 }
 
 /* EndPoint Request*/
 else if (Type_Recipient == (STANDARD_REQUEST | ENDPOINT_RECIPIENT))
 {
 }
 else
 {
    Result = USB_UNSUPPORT;
 }
 if (Result != USB_SUCCESS)
 { 
 }
 
 if (Result != USB_SUCCESS)
 {
 }
 ControlState = WAIT_STATUS_IN;/* After nodata stage SETUP */
 USB_StatusIn();
exit_NoData_Setup0:
  pInformation->ControlState = ControlState;
  return;
}

USB_StatusIn(); //这句话是一个关键,它是一个宏,实际是准备好发送 0 字节的状态数据包。 因为地址设置没有数据过程, 建立阶段后直接进入状态阶段,主机发 IN令牌包,设备返回 0 字节数据包,主机再 ACK。

它对应的宏是这样的:
#define USB_StatusIn() Send0LengthData() //准备发送 0 字节数据
#define Send0LengthData() { _SetEPTxCount(ENDP0, 0);
vSetEPTxStatus(EP_TX_VALID); \ //设置发送有效,发送字节数为 0}

  1. 设置地址的状态阶段:
    而前面把状态设置为 WAIT_STATUS_IN是给 IN令牌包的处理提供指示。因为建立阶段结束以后, 主机接着发一个 IN 令牌包, 设备返回 0 字节数据包,主机返回ACK包,进入中断。本次中断由 IN0_Process()函数来处理,追踪进入,它执行以下代码:
uint8_t In0_Process(void)
{
 uint32_t ControlState = pInformation->ControlState;
   
 if ((ControlState == IN_DATA) || (ControlState == LAST_IN_DATA))
 {
 }
 
 else if (ControlState == WAIT_STATUS_IN)
 {
    if ((pInformation->USBbRequest== SET_ADDRESS) &&
        (Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT)))
    {     
     SetDeviceAddress(pInformation->USBwValue0);
     pUser_Standard_Requests->User_SetDeviceAddress();//这个函数就一个赋值语句, bDeviceState = ADDRESSED。
    }
    (*pProperty->Process_Status_IN)();
    ControlState = STALLED;
 }
 
 else
 {
    ControlState = STALLED;
 }
 
  pInformation->ControlState = ControlState;
  return Post0_Process();
}

执行设置地址操作、 采用新地址后, 把设备的状态改为 STALLED。 而在处理的出口中调用 Post0_Process()函数,这个所做的工作是:

SetEPRxCount(ENDP0,Device_Property.MaxPacketSize);//将端点 0 的缓冲区大小设置为 64字节
if (pInformation->ControlState== STALLED)
{
 vSetEPRxStatus(EP_RX_STALL);
 vSetEPTxStatus(EP_TX_STALL);
}//将端点 0 的发送和接收都设置为: STALL,这种状态下只接受 SETUP 令牌包

4、从新地址获取设备描述符

因为过程类似

1)上一阶段末尾的状态

端点 0的发送和接收都设置为: STALL,只接收 SETUP令牌包。

2)建立阶段:主机发令牌包、数据包、设备回应ACK

产生数据接收中断,且端点 0 的SETUP 置位,调用 Setup0_Process() 函数进行处理。在Setup0_Process()中,因为主机发送了请求数据 8 个字节。由调用Data_Setup0()函数进行处理。 首先是获取设备描述符的长度,描述符的起始地址,传送的最大字节数,根据这些参数确定本次能够传输的字节数,然后调用DataStageIn()函数进行实际的数据传输操作,设备描述符必须在本次中断中就写入发送缓冲区,因为很快就要进入数据阶段了。在函数处理的最后:

vSetEPTxStatus(EP_TX_VALID);
USB_StatusOut();/* 本来期待 IN令牌包,但用户可以取消数据阶段,一般不会用到 */
3)数据阶段:主机发IN包,设备返回数据,主机发ACK

本次操作会产生数据发送完成中断, 由 In0_Process(void)来处理中断, 它也调用DataStageIn()函数来进行处理。如果数据已经发送完:

ControlState = WAIT_STATUS_OUT;
vSetEPTxStatus(EP_TX_STALL);//转入状态阶段。

有可能的话:

Send0LengthData();
ControlState = LAST_IN_DATA;
Data_Mul_MaxPacketSize = FALSE;//这一次发送 0 个字节,状态转为最后输入阶段。

否则,继续准备数据,调整剩余字节数、发送指针位置,等待主机的下一个 IN令牌包。

4)状态阶段:主机发OUT包、 0字节包,设备发送ACK

数据发送完成中断, 调用 Out0_Process(void)函数进行处理,由于在数据阶段的末尾已经设置设备状态为: WAIT_STATUS_OUT, 所以处理函数基本上没有做什么事,就退出了。并将状态设为 STALLED

5、获取配置描述符

在这里插入图片描述
以后的过程大概类似就不做详细说明了。只做几点说明。按照网上的说法这次其实只是获取9字节的配置描述符,然后通过获取配置描述符的第二个字节(从0开始算)得知整个描述符集合(配置描述符、接口描述符、HID描述符(最后两个字节为下级描述符的长度(报告描述符))、端点描述符)共有多少个字节。然后获取整个描述符的集合。但是我在自己调试的时候,发现USB直接获取了整个描述符集合的数据。不知到为啥?

6、获取字符串描述符。

通过实际调试发现,获取三次字符串描述符,分别是,主机获取字符串序号描述符、主机获取字符串ID描述符、主机获取字符串产品描述符。

7、主机再次获取设备描述符和配置描述符集合

在这里插入图片描述
在这一步骤,主机进行了三次USB标准设备请求,分别为获取设备描述符、获取配置描述符、获取配置描述符集合。

8、主机设置配置

在这里插入图片描述
建立阶段:主机发 SETUP包、发请求数据包( DATA0 包)、用户发 ACK。
进入 CTR中断,用户调用 Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用NoData_Setup0()函数进行处理。判断为设置配置后, 调用Standard_SetInterface()函数将设备状态结构体的当前配置改为主机数据中的配置参数。同时调用用户的设置配置函数,将设备状态改为“ configured” 。退出时,将控制传输状态改为:ControlState= WAIT_STATUS_IN,进入状态阶段。

设备期待主机的IN令牌包,返回状态数据。状态阶段:主机发IN令牌、 设备返回0Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用 NoData_Setup0()函数进行处理。设置空闲时一个类特殊请求, 其特征码为0x21, 2表示类请求而不是标准请求,1表示接收对象是接口而不是设备。USB的底层并不支持类特殊请求,它将调用上层函数提供的函数:

if (Result !=USB_SUCCESS)
{
  Result =(*pProperty->Class_NoData_Setup)(RequestNo); //这里就是调用用户提供的类特殊请求的处理函数。 结果发现用户       提供的类特殊请求(针对无数据情况)只支持SET_PROTOCOL。针对有数据情况只支持: GET_PROTOCOL。
  if ((Type_Recipient==(CLASS_REQUEST |INTERFACE_RECIPIENT))&& (RequestNo == SET_PROTOCOL)
  {
    return Joystick_SetProtocol();
  }
}

9、主机获取报告描述符

建立阶段:主机发SETUP包、发请求数据包( DATA0包)、用户ACK。进入CTR中断,获取描述符是一个标准请求,但是报告描述符并不是需要通用实现的,所以在底层函数中没有实现。跟踪Setup0_Process(void)——进入Data_Setup(void)函数,它是这么处理的:

void Data_Setup0(void)
{
 uint8_t *(*CopyRoutine)(uint16_t);
 RESULT Result;
 uint32_t Request_No = pInformation->USBbRequest;
 
 uint32_t Related_Endpoint, Reserved;
 uint32_t wOffset, Status;
 
 CopyRoutine = NULL;
 wOffset = 0;
 
 /*GET DESCRIPTOR*/
 if (Request_No == GET_DESCRIPTOR)
 {
    if (Type_Recipient== (STANDARD_REQUEST | DEVICE_RECIPIENT))
    {//在这里面并没有获得有用的信息因为它并不是一个标准设备请求
      uint8_t wValue1 =pInformation->USBwValue1;
      if (wValue1 == DEVICE_DESCRIPTOR)
      {
        CopyRoutine =pProperty->GetDeviceDescriptor;
      }
     else if (wValue1 ==CONFIG_DESCRIPTOR)
      {
        CopyRoutine =pProperty->GetConfigDescriptor;
      }
      else if (wValue1 == STRING_DESCRIPTOR)
      {
        CopyRoutine =pProperty->GetStringDescriptor;
      } /* End of GET_DESCRIPTOR */
    }
 }
 
 /*GET STATUS*/
 else if ((Request_No == GET_STATUS) &&(pInformation->USBwValue == 0)
           &&(pInformation->USBwLength == 0x0002)
           &&(pInformation->USBwIndex1 == 0))
 {
    /* GET STATUS for Device*/
    if ((Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
        && (pInformation->USBwIndex== 0))
    {
      CopyRoutine = Standard_GetStatus;
    }
 
    /* GET STATUS for Interface*/
    else if (Type_Recipient ==(STANDARD_REQUEST | INTERFACE_RECIPIENT))
    {
      if(((*pProperty->Class_Get_Interface_Setting)(pInformation->USBwIndex0, 0)== USB_SUCCESS)
          &&(pInformation->Current_Configuration != 0))
      {
        CopyRoutine = Standard_GetStatus;
      }
    }
 
    /* GET STATUS for EndPoint*/
    else if (Type_Recipient ==(STANDARD_REQUEST | ENDPOINT_RECIPIENT))
    {
      Related_Endpoint =(pInformation->USBwIndex0 & 0x0f);
      Reserved = pInformation->USBwIndex0& 0x70;
 
      if (ValBit(pInformation->USBwIndex0,7))
      {
        /*Get Status of endpoint & stallthe request if the related_ENdpoint
        is Disabled*/
        Status =_GetEPTxStatus(Related_Endpoint);
      }
      else
      {
        Status =_GetEPRxStatus(Related_Endpoint);
      }
 
      if ((Related_Endpoint <Device_Table.Total_Endpoint) && (Reserved == 0)
          && (Status != 0))
      {
        CopyRoutine = Standard_GetStatus;
      }
    }
 
 }
 
 /*GET CONFIGURATION*/
 else if (Request_No == GET_CONFIGURATION)
 {
    if (Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
    {
      CopyRoutine = Standard_GetConfiguration;
    }
 }
 /*GET INTERFACE*/
 else if (Request_No == GET_INTERFACE)
 {
    if ((Type_Recipient == (STANDARD_REQUEST |INTERFACE_RECIPIENT))
        && (pInformation->Current_Configuration!= 0) && (pInformation->USBwValue == 0)
        && (pInformation->USBwIndex1== 0) && (pInformation->USBwLength == 0x0001)
        &&((*pProperty->Class_Get_Interface_Setting)(pInformation->USBwIndex0, 0)== USB_SUCCESS))
    {      
      CopyRoutine = Standard_GetInterface;
    }
 
 }
 
 if (CopyRoutine)
 {
    pInformation->Ctrl_Info.Usb_wOffset =wOffset;
    pInformation->Ctrl_Info.CopyData =CopyRoutine;
    /* sb in the original the cast to word wasdirectly */
    /* now the cast is made step by step */
    (*CopyRoutine)(0);///这个函数这里调用的目的只是设置了 pInformation中需要写入的描述符的长度
    Result = USB_SUCCESS;
 }
 else//最终通过调用用火的类特殊实现来获取报告描述符
 {
   Result =(*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
    if (Result == USB_NOT_READY)
    {
      pInformation->ControlState = PAUSE;
      return;
    }
 }
 
 if (pInformation->Ctrl_Info.Usb_wLength == 0xFFFF)
 {
    /* Data is not ready, wait it */
    pInformation->ControlState = PAUSE;
    return;
 }
 if ((Result == USB_UNSUPPORT) || (pInformation->Ctrl_Info.Usb_wLength== 0))
 {
    /* Unsupported request */
    pInformation->ControlState = STALLED;
    return;
 }
 
 
 if (ValBit(pInformation->USBbmRequestType, 7))//设备到主机
 {
    /* Device ==> Host */
    __IO uint32_t wLength =pInformation->USBwLength;
    
    /* Restrict the data length to be the onehost asks for */
    if (pInformation->Ctrl_Info.Usb_wLength> wLength)
    {
      pInformation->Ctrl_Info.Usb_wLength =wLength;
    }
   
    else if(pInformation->Ctrl_Info.Usb_wLength < pInformation->USBwLength)
    {
      if(pInformation->Ctrl_Info.Usb_wLength < pProperty->MaxPacketSize)
      {
        Data_Mul_MaxPacketSize = FALSE;
      }
      else if((pInformation->Ctrl_Info.Usb_wLength % pProperty->MaxPacketSize) == 0)
      {
        Data_Mul_MaxPacketSize = TRUE;
      }
    }  
 
    pInformation->Ctrl_Info.PacketSize =pProperty->MaxPacketSize;
   
    DataStageIn();
 }
 else
 {
    pInformation->ControlState = OUT_DATA;
   vSetEPRxStatus(EP_RX_VALID); /*enable for next data reception */
 }
 
 return;
}

可见核心函数只支持设备描述符、配置描述符以及字符串描述符。 最终该函数将调用:
Result= (*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
调用用户的类特殊实现来获取报告描述符,它的函数本体是Joystick_Data_Setup函数,同时 HID 类描述符也是通过这种方式取得的,我们来看一下

RESULTJoystick_Data_Setup(uint8_t RequestNo)
{
 uint8_t *(*CopyRoutine)(uint16_t);
 
 CopyRoutine = NULL;
 if ((RequestNo == GET_DESCRIPTOR)
      && (Type_Recipient ==(STANDARD_REQUEST | INTERFACE_RECIPIENT))
      && (pInformation->USBwIndex0== 0))
 {
    if (pInformation->USBwValue1== REPORT_DESCRIPTOR)
    {
     CopyRoutine = Joystick_GetReportDescriptor;
    }
    else if (pInformation->USBwValue1 ==HID_DESCRIPTOR_TYPE)
    {
      CopyRoutine = Joystick_GetHIDDescriptor;
   }
 
 } /* End of GET_DESCRIPTOR */
 
 /*** GET_PROTOCOL ***/
 else if ((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT))
           && RequestNo ==GET_PROTOCOL)
 {
    CopyRoutine = Joystick_GetProtocolValue;
 }
 if (CopyRoutine == NULL)
 {
    return USB_UNSUPPORT;
 }
  pInformation->Ctrl_Info.CopyData =CopyRoutine;
  pInformation->Ctrl_Info.Usb_wOffset = 0;
 (*CopyRoutine)(0);
 return USB_SUCCESS;
}

最后通过DataStageIn做发送准备。主机发送IN令牌包时,数据就被发送出去了

下面贴一下我自己调试时,发送的一些串口的数据。

usb触摸鼠标。
RESET中断。
SUSP中断。
SUSP中断。
WKUP中断。
RESET中断。
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x40//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
RESET中断。
标准请求:
0x0 0x5 0x2 0x0 0x0 0x0 0x0 0x0//主机设置地址
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x12//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00xff//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32 0x9 0x4 0x0 0x0 0x1 0x3 0x1 0x2 0x0 0x9 0x21 0x10 0x1 0x0 0x1 0x22 0x4a0x0 0x7 0x5 0x81 0x3 0x4 0x0 0x20//设备发送配置描述符
标准请求:
0x80 0x6 0x3 0x3 0x9 0x4 0x00xff//主机获取字符串序号描述符
发送数据:
0x1a 0x3 0x34 0x0 0xff 0x0 0xd40x0 0x5 0x0 0x33 0x0 0x46 0x0 0x34 0x0 0x30 0x0 0x35 0x0 0x83 0x0 0x12 0x0 0x430x0//设备发送字符串序号描述符
标准请求:
0x80 0x6 0x0 0x3 0x0 0x0 0x00xff//主机获取字符串ID描述符
发送数据:
0x4 0x3 0x9 0x4 //设备发送字符串ID描述符
标准请求:
0x80 0x6 0x2 0x3 0x9 0x4 0x00xff//主机获取字符串产品描述符
发送数据:
0x1e 0x3 0x53 0x0 0x54 0x0 0x4d0x0 0x33 0x0 0x32 0x0 0x20 0x0 0x4a 0x0 0x6f 0x0 0x79 0x0 0x73 0x0 0x74 0x00x69 0x0 0x63 0x0 0x6b 0x0//设备发送字符串产品描述符
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x12//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00x9//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32//设备发送配置描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00x22//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32 0x9 0x4 0x0 0x0 0x1 0x3 0x1 0x2 0x0 0x9 0x21 0x10 0x1 0x0 0x1 0x22 0x4a0x0 0x7 0x5 0x81 0x3 0x4 0x0 0x20 //设备发送配置描述符
标准请求:
0x0 0x9 0x1 0x0 0x0 0x0 0x00x0///主机设置配置
标准请求:
0x21 0xa 0x0 0x0 0x0 0x0 0x00x0//HID设置SET IDLE
标准请求:
0x81 0x6 0x0 0x22 0x0 0x0 0x00x8a//主机获取报告描述符
发送数据:
0x5 0x1 0x9 0x2 0xa1 0x1 0x9 0x10xa1 0x0 0x5 0x9 0x19 0x1 0x29 0x3 0x15 0x0 0x25 0x1 0x95 0x3 0x75 0x1 0x81 0x20x95
 
0x1 0x75 0x5 0x81 0x1 0x5 0x1 0x90x30 0x9 0x31 0x9 0x38 0x15 0x81 0x25 0x7f 0x75 0x8 0x95 0x3 0x81 0x6 0xc0 0x90x3c
 
0x5 0xff 0x9 0x1 0x15 0x0 0x250x1 0x75 0x1 0x95//设备发送报告描述符
发送数据:
0x2 0xb1 0x22 0x75 0x6 0x95 0x10xb1 0x1 0xc0//设备发送报告描述符 

然后又用Bus Hound抓了一下USB的包数据,如下所示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
看捕捉的数据,也是发现有不理解的地方,我们看USB主机与28通信的数据(42-47),是对USB设置进行枚举的过程,但是在与28通信的过程,并没有对于字符串描述符的获取,后来仔细查看,原来字符串描述符是在7端口获取(36-40),而且还在28端口进行数据通信之前。

  • 7
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值