首页|资源下载
登录|注册

您现在的位置是:首页 > 技术阅读 >  主机:你是谁? 设备:你好,我是 USB

主机:你是谁? 设备:你好,我是 USB

时间:2022-09-29

来源:公众号【鱼鹰谈单片机】

作者:鱼鹰Osprey

ID   :emOsprey


本篇笔记是 16 年写的,写的很糙,在这里你会看到很多专业名词,可能会一脸懵逼,但还是先发出来给大家看看,没有基础的不用细看,大概了解即可,后面的笔记将把这些内容揉碎了再呈现给大家。而且当时看的例程是 HID 鼠标的,比较复杂,内核代码也没有使用状态机,比较难以理解,但是鱼鹰接下来的例程用了状态机,并且鱼鹰画了整个状态机的运行流程图,将更加清晰易懂,所以 HID 的例程暂时就不再提供给大家,后面系列完结了将以 USB 资料大礼包 的形式送给大家。


现在和电脑通信基本上是采用USB 进行通信,所以我们有必要学习USB 协议。而USB协议中最重要的一种传输就是控制传输
串口通信在设置串口波特率、连上线后是没有数据传输的,只有在电脑或设备发送数据时,线上才会有数据传输,而USB 设备不同,在设备插入电脑时,电脑就开始和设备进行交流了,就像人们刚见面就打招呼似的。但是USB 主机可不是仅仅问你(USB设备)有没有吃饭什么的,而是全面的了解你的信息(必要信息)。
为了获得你的信息,就必须有一个通道,还有一个双方支持的通信格式才行。所以就有了控制端点。控制端点是一个具有双向通信能力的默认端点(也叫缺省端点),是一个USB 设备必须支持的端点。初期的枚举过程都在该端点上完成。枚举过程是一个USB 设备必须经历的一个过程。通过枚举,让主机了解设备的一些基本信息。而枚举过程的实现就是通过控制传输实现的。我们通过枚举过程来了解控制传输。
首先确定控制传输是四大传输之一,也是最基本的传输,所有的USB 设备都必须支持。它是可靠的传输,具有最少两个个阶段,建立阶段(Setup Stage)、状态阶段(Data Stage),还有一个数据阶段(Status Stage)是可能会有的。建立阶段是由建立传输事务(Setup Transaction)实现的,状态阶段和数据阶段是由数据输出(Data Out Transaction)或输入事务(Data IN Transaction)实现的(如果数据太多,将会采用多次数据输入或输出事务)。如下图,本文将主要讲这图中的内容,一定要明白它们之间的关系。
   各种可能的控制传输过程
(SETUP(0)由三个包组成:SETUP Packet 、DATA0 Packet、ACK Packet;
OUT(1)由三个包组成:OUT Packet 、DATA1 Packet、ACK Packet;
IN(1)由三个包组成:IN Packet 、DATA0 Packet、ACK Packet 。这里的 SETUP 、DATA0 、ACK 、OUT 、IN是PID)
一个事务又是由各种包组成的。包又分为四大类包:Token(令牌)Packet、Data(数据) Packet、 Handshake(握手)Packet、特殊 Packet。每类包中又分为各种具体的包(由PID(Packet ID)确定该包属于哪一种包)。需要注意的是包是 USB 传输最小的传输单位、所有数据都必须进行打包才可以进行传输,包格式如下。
(灰色区域表示每个包都有,彩色表示可能有)
因为有各种不同的包,各种包的具体格式也不尽相同。所以我们只分析我们需要的包。为了简化内容,让我们能够将注意力集中在我们需要的东西上,我们都以包来进行说明,包里面具体的其他内容,将选择性的了解,对于包,我们也只对需要的包进行说明。
令牌包:SETUP包、OUT包、IN包。(主机发送)
主机最开始会发出的包,标志一次传输事务的开始,里面有PID(标志这个是什么包,SETUP 包中的PID就是SETUP)、地址(包括设备地址,端点地址)、CRC(对地址信息进行校验)。在程序中我们可以知道是什么类型的包。
数据包:DATA0包、DATA1包。(主机设备都可以发送)
里面有PID、数据、CRC(对数据信息进行校验)。这是用户真正要关心的数据。里面的数据我们是可以进行查看的。我们枚举过程处理的就是这些数据。
握手包:STALL包、NAK包、ACK包(主机只能发送ACK包)
里面只有PID。
ACK包:表示正确接收数据,并且有足够的空间来容纳数据,主机和设备都可以用ACK确认,而NAK,STALL,NYET只有设备能够返回,主机不能使用(因为请求都是主机发送,当主机发送请求了,就说明已经准备好了,自然不应该有这些回答)。
NAK 包:表示没有数据需要返回,或者数据接收正确但是没有足够的空间来容纳数据,当主机收到NAK是,知道设备没有准备好,主机会在合适的时机进行重新传输。
STALL包:表示设备无法执行这个请求,或者端点已经被挂起,它表示一种错误的状态。设备返回STALL
后需要主机进行干预才能解除这种STALL状态。
注意:返回NAK并不代表数据出错,只是说明设备暂时没有数据传输或暂时没有能力接收数据.当主机或设备检测到数据出错时(如CRC校验出错,PID校验出错,位填充出错等 )将什么也不返回。这时等待握手包的一方就会因为收不到握手包而等待超时。
现在有了这些基础知识,我们为了更清楚认识控制传输,就通过讲解 usb_core.h、 usb_core.c 文件(本文必须结合这两个文件)来了解枚举过程,进而认识控制传输。
之前也说过,枚举过程是通过控制传输实现的,那么现在就来看看当你把一个USB 设备插入主机时,它的枚举过程是怎样的。
首先,弄清楚一点,这两个文件处理的就是枚举过程,也是整个USB程序中最最核心的部分,所有的USB 相关的文件都是以它为基础。它定义了各种结构体,各种联合体。而其他文件就是根据这个结构体、联合体进行具体的定义,并且写出具体的实现的方法。弄明白这两个文件是重中之重。
因为控制传输比较复杂,和上一个状态有很大关系。所以定义了一个枚举类型CONTROL_STATE确定程序当前的各种状态。还有结构体ENDPOINT_INFO、DEVICE_INFO、DEVICE_PROP,还有一个特殊的结构体USER_STANDARD_REQUESTS等各种类型。
CONTROL_STATE:确定当前的端点的传输状态
ENDPOINT_INFO:端点传输数据的信息,包括要发送或接收的剩余数据长度,已经发送或接收的数据偏移,该端点的最大包长(决定了该端点一次处理的数据的能力),要发送或接收数据的地址。(数据长度和地址由具体的函数提供,这个结构体有一个函数指针指向具体实现函数。也就是说要接收或发送数据的大小和地址是由具体的函数实现的)。
DEVICE_INFO:包含了设备的状态信息,控制端点的传输状态(CONTROL_STATE控制传输状态),控制端点的信息(ENDPOINT_INFO的具体实现),还有标准请求的数据信息。
DEVICE_PROP:这里面包含了具体的实现函数。如具体设备的硬件相关的函数实现
void (*Init)(void);,复位信号发过来时的处理函数void (*Reset)(void);,控制传输中的状态阶段的处理函数void (*Process_Status_IN)(void); void (*Process_Status_OUT)(void);,发送接收函数的具体实现函数,就是之前的ENDPOINT_INFO中需要的具体实现函数。等等。
USER_STANDARD_REQUESTS:这个结构体就比较特殊了,因为这个结构体的函数是用来给用户使用的。比如主机发送了什么信息过来,我们把数据发出去了,那么我们还要告知用户(我们的程序),我们做了这些操作才行,但是这些操作函数有的可以省去的,而不会影响正常通信,虽然不会影响正常通信,但是对设备本身还是有可能有影响的。
现在我们来具体看看主机与设备是怎么通信的,所以看它的使用函数void CTR_LP(void)(usb_int.c文件),USB 通信是在中断函数中的。
在我们插上USB设备时,枚举过程数据(串口接收到的数据):
SETUP:80 6 1 0 40 发送数据:112 200 0 4000 483 5710 200 201 103 IN:OUT:SETUP:0 5 100 0 0 INSETUP:80 6 1 0 12 发送数据:112 200 0 4000 483 5710 200 201 103 IN:OUT:SETUP:80 6 2 0 ff 发送数据:209 22 101 e000 932 4 100 103 2 2109 100 100 4a22 700 8105 403 2000 IN:OUT:SETUP:80 6 303 904 ff 发送数据:31a 38 ff ff ff 33 41 37 31 21 63 15 57 IN:OUT:SETUP:80 6 3 0 ff 发送数据:304 409 IN:OUT:SETUP:80 6 203 904 ff 发送数据:322 41 4c 49 45 4e 54 45 4b 20 89e6 63a7 55 53 42 9f20 6807 IN:OUT:SETUP:80 6 6 0 aSETUP:80 6 1 0 12 发送数据:112 200 0 4000 483 5710 200 201 103 IN:OUT:SETUP:80 6 2 0 9 发送数据:209 22 101 e000 932 IN:OUT:SETUP:80 6 2 0 22 发送数据:209 22 101 e000 932 4 100 103 2 2109 100 100 4a22 700 8105 403 2000 IN:OUT:SETUP:0 9 100 0 0 INSETUP:21 a 0 0 0
注意:因为用的是 printf 函数发送的,所以有些是省去了,有些高字节和低字节可能换了,所以分析时要注意。
我们现在主要分析传输过程,而不是具体数据的处理,所以不要太关心具体数据。
我们假设前期工作都做好了,USB 能通信了,既然如此,程序必定会进入usb_int.c文件中的void CTR_LP(void)函数。当第一次进入此函数时,就说明一次传输事务成功完成。从包的角度来看就是主机发送一个SETUP包,再发一个DATA包,设备确认数据的正确性(从PID和CRC确定)然后发送ACK包。设备硬件将该端点自动设置为NAK(此时如果主机发送包过来,一律以NAK回应,表示忙着呢,还没有准备好),使设备有足够的时间处理DATA包中的数据。在这之后程序才运行到了CTR_LP()函数里。
程序每运行到这里一次,意味着主机发送的设备地址和端点号正确,也意味着一次传输事务的结束。这一点要牢记。
现在我们看着设备接收到的数据再理一理过程。(以下内容必须结合附带工程进行验证)
SETUP:80 6 1 0 40 发送数据:112 200 0 4000 483 5710 200 201 103 IN:OUT:
设备的一次传输事务结束了(从SETUP:看出)(第一次进入CTR_LP()),先判断这次传输事务是什么类型传输事务(STM32中有一个标志位可以进行判断),然后发现是建立令牌包,那么就是建立传输事务,现在就转到建立传输事务程序中。
在该程序中,先把数据包中的数据拷贝出来再说。
拷完了,同时设置当前的设备状态为SETTING_UP,表示开始处理一次建立事务,然后发现这是一个有数据阶段的建立传输事务而且是设备发送数据给主机(怎么发现的就看具体的标准请求格式吧,现在只说结果)。
好吧,看看要什么数据,发现要设备描述符,长度是64字节。那行,只是我的设备描述符只有18字节,怎么办,不管,就发18字节过去。
发送完这18字节后程序第二次进入了CTR_LP()函数(从IN:看出),刚刚设备把数据发送出去了,这次进入相关处理函数也就没什么事干了,就等着主机发送状态数据过来呢,所以设置设备状态为WAIT_STATUS_OUT,表示正在等待主机发送状态数据(之后发送端点状态设为STALL,表示如果主机发送IN令牌包将以STALL握手包进行回应)。
然后第三次进入CTR_LP()函数(从OUT:看出),因为是OUT包,所以是一次数据输出事务的完成(没有写错,就是输出,这是对于主机来说),这时设备就知道这应该是状态阶段的数据了,经过一系列的判断,发现确实是(之后发送接收端点都设置为STALL)。
到这里一个完整的控制传输过程算是完成了。但是一个枚举过程是由很多这样的控制传输过程构成的。
SETUP:0 5 100 0 0 IN:
第二次建立传输事务又来了(从SETUP:看出),也标志着一次控制传输的开始(建立阶段)。看看有没有数据要发送或输入,嗯,好像没有,而且是设置地址的(这里有地址信息,就是1),那行,开始设置地址吧?
那可不不行,USB 协议规定,设置地址的工作必须在设备发送状态数据(状态阶段没有具体的数据,也就是说数据区为0字节)到主机,主机接收到后发送ACK握手包后被设备接收到才能进行设置设备的地址(为什么这么规定,可能是防止中间出现问题吧)。
所以在程序中还真没进行设置地址的工作,而是只是设置设备当前的状态为WAIT_STATUS_IN,什么意思,就是表示设备正在等待发送状态数据呢(为什么要等,而不是直接发送,这是因为USB 是有主从关系的,主机没叫你发送数据,你是不能发送数据的,你只能把数据放在缓存区中,并且设置发送端点有效。当设备收到IN令牌包时,它就自动由硬件发送出去了)。
当主机成功收到状态阶段的数据时就会发送ACK握手包。然后设备接收到了握手包,这时也就意味着一次数据输入事务(状态阶段)的结束,此时,程序再次运行到CTR_LP()这里(从IN:看出),并进入到相关处理函数,这时相关处理函数就会设置地址了,此后发送和接收的端点再次变为STALL。
上面这些都是正常的控制传输,如果不正常(不正常不代表数据出错,而是请求不合理)的呢?我们跳过正常的,直接看下面这一次控制传输。
SETUP:80 6 6 0 a
看这条请求代码是6(后面那个),要求发送10个字节数据给主机,但是我的设备没有这种代码为6的描述符,怎么办。USB 协议规定有些请求是必须支持的,但是有些请求却不一定要支持。这个描述符没有应该怎么处理呢。
分析程序,最终你会发现控制状态设置为STALLED,也就是说在主机下一个IN令牌包(在此之前主机可能已经发送了多次IN令牌包过来,只是设备都以NAK包进行回应)来的时候,设备会返回一个STALL包,当主机收到这个握手包之后,就知道设备不支持该请求,它就会放弃继续发送IN令牌包,而是发送SETUP包,因为设备控制端点处于STALL状态时,必须主机进行干预才能帮助设备解除这种状态。
由于此次控制传输是设备不支持的请求,所以就没有数据阶段,同时也没有状态阶段,既然不需要发送或接收数据,也就不会在建立传输事务来之前再次进入程序了。但是如果发送的请求为输出数据,那么程序就会直接接收数据了。
说了这么多,下面进行一些总结:
  1. 程序运行到CTR_LP()里面,就说明一次事务传输的完成,不管是建立事务还是数据输入输出事务。注意是已经完成,而不是将要完成。
  2. 数据如果出错了,只会有错误中断产生,而不会产生传输完成中断,也就不会进入CTR_LP()服务程序中。
  3. 注意分析每一次事务传输的完成将使当前端点的发送接收状态产生何种影响,又会对之后的事务传输产生何种影响。
  4. 每一次控制传输的结束,都会使端点发送和接收状态都设为STALL。
  5. 控制传输在请求不支持情况下没有状态阶段的数据。
  6. 建立阶段也由三个包组成,SETUP令牌包 、DATA0数据包(只能是DATA0)、ACK握手包。也是一次建立事务。
  7. 数据阶段也由三个包组成,OUT/IN令牌包 、DATA0/1数据包、ACK握手包。也是一次数据输出或输入事务。在建立传输中有可能有多次该传输事务。
  8. 状态阶段也由三个包组成,OUT/IN令牌包 、DATA1数据包、ACK握手包。也是一次数据输出或输入事务。但是它没有用户所使用的数据。
  9. OUT、IN是对于主机来说的。OUT就是主机向设备发送数据。
10. 注意根据实际数据进行分析。
 注意:如果使用USB协议分析软件,你会发现软件的捕获数据不全,所以要根据设备串口发送的数据进行分析


推荐阅读:
嵌入式系统优先级详解
KEIL 调试经验总结
线程CPU使用率到底该如何计算?
许久以后,你会感谢自己写的异常处理代码
终极串口接收方式,极致效率
延时功能进化论(合集)
如何写一个健壮且高效的串口接收程序?
打了多年的单片机调试断点到底应该怎么设置?| 颠覆认知


微信公众号「鱼鹰谈单片机

每周一更单片机知识

长按后前往图中包含的公众号关注