前言
最近学习了USB2.0的一些知识,这里就当笔记,回顾记录一下,参考的资料是B站的刘凯老师STM32 培训教程和龙虎老师的USB应用精粹分析,如果有哪些错误的理解,也希望大佬们指点一二,谢谢!
回到Data_Setup0()函数,如果主机发出“获取描述符” GET_DESCRIPTOR的请求。
那么我们就要判断是 这个 Type_Recipient,他是一个宏定义,取出了USBbmRequestType的低七位,其中包含了请求种类与接收方。
如果是标准请求(STANDARD_REQUEST 0x00)且接收方是设备(DEVICE_RECIPIENT0x00)
接下来需要进一步判断USBwValue1是获取设备,配置还是字符串描述符,然后将相应的函数指针赋给CopyRoutine
获取设备描述符的函数实际是uint8_t *Joystick_GetDeviceDescriptor(uint16_t Length)
其中实际执行的是Standard_GetDescriptorData(Length, &Device_Descriptor);
第一个形参是长度(length)
我们第一次调用Standard_GetDescriptorData(Length, &Device_Descriptor)函数时,
Length传参的是0,
第二次Length传参 传的是发送数据包的字节长度(在DataStagein函数中),
第二个形参是 传的是Device_Descriptor的首地址
Device_Descriptor 其实就是 ONE_DESCRIPTOR这个结构体 它包含了描述符数据数组的首地址与长度信息,
我们的Standard_GetDescriptorData函数就是根据这两个参数(Length,&Device_Descriptor),来确定“将要发送给主机的描述数组首地址与字节长度”
在Standard_GetDescriptorData函数中,首先就是把Usb_wOffset(数据偏移地址)取出来,这个在第一次执行CopyRoutine函数指针之前已经设置为0了,由于第一次传入的长度为0,那么就是将Usb_wLength设置为Descriptor_Size,也就是设备描述符的长度,
之后就是进入DataStageIn()函数中,因为这次长度不为0,那我们就不进如if语句中,将发送数据的偏移地址进行了调整,也就是描述符的数据首地址进行偏移。
Usb_wOffset 这个变量是调整下次需要发送数据的偏移地址,Length就是要传输的长度
我们再回到Data_Setup()函数,根据总线枚举过程,接下来是获取配置描述符
相应的回调函数是这个 uint8_t *Joystick_GetConfigDescriptor(uint16_t Length)
他和配置描述符差不多就不多说了。
接下来就是获取字符串描述符请求,相应的回调函数为
uint8_t *Joystick_GetStringDescriptor(uint16_t Length)
我们从以下的程序中能发现定义的字符串描述符有4个,
主机究竟想要获取哪一个呢,这就的由数据中的wValue字段来决定,wValue字段的高字节代表描述符的类型,而低字节就是表示具体的索引值(仅仅对配置与字符串描述符有效),
索引值被定义在String_Descriptor数组中,也就是说wValue0不会大于三,所以在这个原厂固件中的比较索引值不应该是4而不是3。
以上就是主机获取到了所需要的所有描述符信息,接下来就要给设备分配一个地址,相应的请求代码是SET_ADDRESS,这个请求是没有数据时期的,所以我们来到NoData_Setup0()这个函数
第一步先判断设备地址是否有效(不能大于127),然后就是wIndex都为0,同时判断Current_Configuration不为0(因为配置完成后Current_Configuration是一个不为0的值)不然就会认为请求是错误的,就会将控制管道状态设置为STALLED直接返回。
在NoData_Setup0()这个函数中,我们没有确切的分配地址,但是在确定接收的“设置地址”请求有效之后,设备会将控制管道状态设置为WAIT_STATUS_IN,而分配地址的具体操作就是在状态时期操作的,也就是在 In0_Process()函数中被执行了,其中将主机发送过来的设备地址(wValue0)传入SetDeviceAddress()函数中这个函数做了两件事情,一个是将主机发送的设备地址保存在USB_DADDR寄存器的低7位中,并且将EF位置1(如果此位为0,USB控制器将停止工作,不响应任何USB总线通信)。第二就是通过for语句把设备所有使用过的端点寄存器都写入端点号(USB_RPOR的最低4位写0,USB_RP1R的最低4位写1,依次内推)
在这个官方例程中我们使用了两个端点,Device_Table结构体变量中包含了有使用的端点数量和配置值,如果要增加更多的端点,就要修改EP_NUM值,官方固件库的SetDeviceAddress()函数有个限制:必须使用连续的端点号,
但是官方固件库里给用户提供了一次自定义改变端点号的机会,就是SetDeviceAddress()函数使用以后还有个User_SetDeviceAddress()函数,它实际执行的是
void Joystick_SetDeviceAddress (void)函数,这里面只是将bDeviceState设置为已设置好已编址状态,我们可以在这里加其他操作。
分配好地址后,主机就是会给设备选择一个配置,相应的请求代码是SET_CONFIGURATION
我们这个官方例程是游戏操作杆,只有一个配置,所以主机发送的配置值应该是1,将这个
应用程序将其保存在指针 pInformation指向的结构体 DEVICE_INFO中的 Current_Configuration
接下来执行回调函数User_SetConfiguration,实际上就是void Joystick_SetConfiguration(void)
将设备设置为已配置状态
对于游戏操纵杆设备,选择完配置后我们还要获取报告描述符,在Data_Setup0()函数中
调用Class_Data_Setup这个函数指针,实际是Joystick_Data_Setup();
Joystick_Data_Setup()该函数执行过程和处理标准请求类似,也是通过最终调用Standard_GetDescriptorData()这个函数来解决的
还有几个标准请求
一个就是Data_Setup0()函数中“获取状态(GET_STATUS)”请求的。
获取状态可以针对设备,接口,端点。这主要取决于bmRequestType的值。如果请求对象为设备,那么wIndex就应该为0,如果请求的对象是接口或者端点,那么wIndex字段应该是相应的接口号或端点号,相应的格式,就如下所示
“获取状态”请求最终返回的数据都是2字节,根据针对的请求对象,返回的状态数据也是不同的。
如下图所示:
从上面两个图,可以看到,主机发送“获取状态”请求中,wIndex字段的高字节与wValue字段总是为0,wLength总是为2(表示两个字节)
所以呀,我们在这个Data_Setup0(void)函数中,当判断当前代码为GET_STATUS时,首先就会据此进一步判断是否为“获取状态”请求。如果确实是获取状态请求,就紧接着判断是获取设备,接口还是端点状态。如果是获取设备状态请求,则wIndex总是为0.如果是获取接口状态请求,则通过调用Class_Get_Interface_Setting(Joystick_Get_Interface_Setting函数)确认接口号是否正确。
如果是获取端点状态请求,则取出wIndex低字节的端点号,,然后根据传输方向来判断USB控制器中是否已经对该端点进行了正常初始化,无论端点的传输方向如何,最终都会将Standard_GetStatus函数指针赋给CopyRoutine,第一次就是传入0,设置发送数据长度为2字节,
而在 DataStageIn(); 还会执行第二次,然后根据需求返回StatusInfo这个全局变量,也就是需要返回的状态数据
//获取状态标准请求
uint8_t *Standard_GetStatus(uint16_t Length)
首先我们判断是不是设备,如果是的话,我们就应该返回“支持唤醒与供电模式与否”的状态数据,所以我们首先取出Current_Feature,这个变量在Joystick_Reset函数中已经设置为了配置描述符数据Joystick_ConfigDescriptor索引值为7的数据,他保存的恰好就是“设备是否支持远程唤醒与自供电模式”那个字节(bmAttributes字段),所以接下来根据Current_Feature来逐步设置数据,最后返回
如果是获取状态请求针对接口,则返回的数据总是0,所以直接返回已经被清零的StatusInfo就好了
如果是获取状态请求针对端点号,则它返回端点的停止(Halt)状态,为此首先从USBwIndex0获取主机的请求是哪个端点状态,如果状态是STALL,则将StatusInfo0的最低位置置1,需要返回的数据就创建完成了。
除了设置配置与设备地址外,我们基本都是讲如何获取信息的请求,其实也还可以设置其他请求,只不过我们对我们设备而言,并不是必须的,除了下面我框框的那两个
其他都被宏定义成了如下这个,要想使用注释掉宏定义,找到对应的回调函数修改就好了。
前面还说到,当我们流管道返回STALL握手包的时候,说明现在端点有问题,应该使用“清除特征CLEAR_FEATURE”请求重启端点,它是在NoData_Setup0()函数中执行的,与处理设置特征的请求类似。
如果接收方是设备的话,就直接清除D5位(远程唤醒支持位)返回,如果接收方是端点,就需要判断wValue与wIndex字段的值是否符合规范定义要求,即wValue字段必须是ENDPOINT_STALL
对应如下的ENDPOINT_HALT,且wIndex的高字节必须为0,
接下来就是判断端点是否有效,如果设备没有被配置,或端点状态为禁止,或者端点号大于设备使用端点号 则请求不支持,
如果是输入端点则直接调用ClearDTOG_TX函数重置发送数据时序切换位,然后将该端点的状态设置为VALID,即清除了STALL状态。如果是输出端点,则同样判断是否处于STALL状态,
然后如果是控制端点0则设置接收数据字节长度为最大数据包长度,并设置接收状态有效,如果是非0端点,则执行ClearDTOG_RX函数,重置了接收数据时序切换位,并同样设置接收状态有效,最后执行一下函数指针User_ClearFeature(实际是Joystick_ClearFeature函数)才返回