文章目录
- 4.1 USB 学习指南
- 4.1 USB 学习指南
- 4.2 USB 系统硬件框架和软件框架
- 4.2.1 实验现象
- 4.2.2 硬件框架
- 4.2.3 软件框架
- 4.3 软件工程师眼里的 USB 电气信号
- 4.3.1 USB 设备状态切换图
- 4.3.2 硬件线路
- 4.3.3 电子信号
- 4.3.4 低速/全速信号电平
- 4.3.5 高速信号电平
- 4.3.6 设备连接与断开
- 1. 连接
- 2. 断开
- 4.3.7 复位
- 4.3.8 设备速率识别
- 1. 低速/全速
- 2. 高速
- 4.3.9 数据信号
- 1. 低速/全速的 SOP 和 EOP
- 2. 高速的 SOP
- 3. NRZI 与位填充
- 4.4 USB 协议层数据格式
- 4.4.1 硬件拓扑结构
- 4.4.2 协议层
- 4.4.3 字节/位传输顺序
- 4.4.4 SYNC 域
- 4.4.5包格式
- 1. PID 域
- 2. 令牌包(Token)
- 3. 数据包
- 4. 握手包
- 4.4.6 传输细节
- 1. 传输(Transfer)和事务(Transaction)
- 2. 过程(stage)和阶段(phase)
- 3. 批量传输
- 4.中断传输
- 5.实时传输
- 6. 控制传输
- 4.4.7 使用工具体验数据格式
- 4.5 USB 描述符
- 4.5.1
- 1. USB 设备状态切换图
- 4.5.2 标准设备请求
- 1.SETUP事务的数据格式
- 2. 标准设备请求
- 3. 设备/配置/接口/端点
- 4.5.3 描述符
- 1. 设备描述符
- 2. 配置描述符
- 3. 接口描述符
- 4. 端点描述符
- 5.示例
- 4.5.4 设备枚举过程示例
- 4.6 USBX 组件
- 4.6.1 Azure RTOS 介绍
- 4.6.2 USBX 层次
- 4.6.3 USBX 的基本配置
- 4.7 移植 USBX 实现虚拟串口
- 4.7.1 配置 USB
- 4.7.2 添加 USBX 代码
- 1. 复制代码
- 2. 添加进工程
- 4.7.3 添加 USBX APP 代码
- 4.7.4 修改 usb.c
- 4.7.5 创建 USBX 任务
- 4.7.6 设置 MDK-ARM 工程
- 4.7.7 添加使用串口的代码
- 4.7.8 上机实验
- 4.8 虚拟串口源码分析与改造
- 4.8.1 描述符的设置
- 4.8.2 数据收发函数
- 4.8.3 使用 FreeRTOS 改造代码
4.1 USB 学习指南
阅读源码时,经常碰到如下术语:
- HCD(Host Controller Driver)
- DCD(Device Controller Driver)
- PCD(Low layer USB Peripheral Control Driver)
- CDC(Communication Device Class)
4.1 USB 学习指南
USB 本身是一个很庞大、复杂的体系, 本课程的重点在于工业互联, USB 是其中的一个 小小知识点。本章课程的目的在于:能理解 USB 的一些概念,能使用 USB 传输数据。 4.2~4.5 节, 介绍 USB 概念;4.6~4.7 节,移植 USBX 实现 USB 串口功能。
参考资料:
- 《圈圈教你玩 USB》
- 官网:https://www.usb.org/documents
- 我们从官网下载后放在网盘如下目录:
- ST 官方资料:
https://wiki.stmicroelectronics.cn/stm32mcu/wiki/Introduction_to_USBX过 USB 设备驱动,直接通过 USB 控制器驱动访问 USB 设备。
4.2 USB 系统硬件框架和软件框架
4.2.1 实验现象
现象: 把 USB 设备比如 Android 手机接到 PC
-
右下角弹出"发现 android phone"
-
跳出一个对话框, 提示你安装驱动程序
-
问 1:USB 设备插到电脑上去, 接触到的对方设备是什么?
答 1:是 USB 控制器,是 USB 控制器内嵌的 root hub -
问 2. 既然还没有"驱动程序",为何能知道是"android phone"?
答 2. windows 里已经有了 USB 的总线驱动程序, 接入 USB 设备后, 是"总线驱动程序" 知道你是"android phone"、提示你安装的是"设备驱动程序"。USB 总线驱动程序负责:识 别 USB 设备, 给 USB 设备找到对应的驱动程序。
-
问 3. 为什么一接入 USB 设备, PC 机就能发现它?
答 3. PC 的 USB 口内部,D-和 D+接有 15K 的下拉电阻, 未接 USB 设备时为低电平。USB 设备的 USB 口内部, D-或 D+接有 1.5K 的上拉电阻;它一接入 PC,就会把 PC USB 口的 D-或 D+拉高,从硬件的角度通知 PC 有新设备接入。
-
问 4. USB 设备种类非常多,为什么一接入电脑, 就能识别出来它的种类?
答 4. PC 和 USB 设备都得遵守一些规范。比如: USB 设备接入电脑后, PC 机会发出"你 是什么"?USB 设备就必须回答"我是 xxx", 并且回答的格式是固定的。USB 总线驱动程序会 发出某些命令想获取设备信息(描述符),USB 设备必须返回"描述符"给 PC。
-
问 5. PC 机上接有非常多的 USB 设备, 怎么分辨它们?
答 5. 每一个 USB 设备接入 PC 时, USB 总线驱动程序都会给它分配一个编号。 PC 机想 访问某个 USB 设备时,发出的命令都含有对应的编号(地址)。
-
问 6. USB 设备刚接入 PC 时, 还没有编号; 那么 PC 怎么把"分配的编号"告诉它?
答 6. 新接入的 USB 设备的默认编号是 0,在未分配新编号前, PC 使用 0 编号和它通 信。
-
4.2.2 硬件框架
在 USB 系统中, 有 2 个硬件概念:
-
USB Host:它跟处理器相连,处理器通过 USB Host 跟各类 USB 设备通信。 USB Host 中 集成有一个 root hub
-
USB Device:这分为两类设备
- Hub:用来扩展 USB 接口
- Function:就是普通的 USB 设备,比如 U 盘、声卡等
4.2.3 软件框架
APP 可以通过 USB 设备驱动程序访问 USB 设备,也可以绕过 USB 设备驱动,直接通过 USB 控制器驱动访问 USB 设备。
4.3 软件工程师眼里的 USB 电气信号
参考资料:
-
《圈圈教你玩 USB》
-
简书 jianshu_kevin@126.com 的文章
- https://www.jianshu.com/p/3afc1eb5bd32
- https://www.jianshu.com/p/cf8e7df5ff09
- USB 协议(三):https://www.jianshu.com/p/2a6e22194cd3
-
官网:https://www.usb.org/documents
-
《usb_20.pdf》的《chapter5 7 Electrical》
-
USB 的 NRZI 信号格式: https://zhuanlan.zhihu.com/p/460018993
-
USB2.0 包 Packet 的组成: https://www.usbzh.com/article/detail-459.html
4.3.1 USB 设备状态切换图
USB 2.0 协议支持 3 种速率: 低速(Low Speed,1.5Mbps)、全速(Full Speed, 12Mbps)、 高速(High Speed, 480Mbps)。
USB Hub、USB 设备, 也分为低速、全速、高速三种类型。 一个 USB 设备, 可能兼容低 速、全速, 可能兼容全速、高速, 但是不会同时兼容低速、高速。
4.3.2 硬件线路
下图是兼容高速模式的 USB 收发器电路图:
USB 连接涉及 Hub Port 和 USB 设备,硬件连接如下:
4.3.3 电子信号
USB 连接线有 4 条: 5V、D+、D-、GND。数据线 D+、D-,只能表示 4 种状态。 USB 协议 中,很巧妙地使用这两条线路实现了空闲(Idle)、开始(SOP)、传输数据(Data)、结束(EOP) 等功能。
4.3.4 低速/全速信号电平
4.3.5 高速信号电平
4.3.6 设备连接与断开
1. 连接
Hub 端口的 D+、D-都有 15K 的下拉电阻,平时为低电平。全速设备内部的 D+有 1.5K 的 上拉电阻, 低速设备内部的 D-有 1.5K 的上拉电阻,连接到 Hub 后会导致 Hub 的 D+或 D-电 平变化,Hub 根据变化的引脚分辨接进来的是全速设备还是低速设备。
高速设备一开始也是作为全速设备被识别的。
全速设备、高速设备连接时, D+引脚的电平由低变高:
低速设备连接时,D-引脚的电平由低变高:
2. 断开
对于低速、全速设备,接到 Hub 时导致 D-或 D+引脚变为高电平, 断开设备后, D-或 D+ 引脚变为低电平:
对于高速设备,它先作为全速设备被识别出来,然后再被识别为高速设备。工作于高 速模式时, D+的上拉电阻是断开的,所以对于工作于高速模式的 USB 设备, 无法通过 D+的 引脚电平变化监测到它已经断开。
工作于高速模式的设备, D+、D-两边有 45 欧姆的下拉电阻,用来消除反射信号:
当断开高速设备后, Hub 发出信号,得到的反射信号无法衰减, Hub 监测到这些信号后 就知道高速设备已经断开,内部电路图如下:
4.3.7 复位
从状态切换图上看,一个 USB 设备连接后,它将会被供电, 然后被复位。当软件出错 时,我们也可以发出复位信号重新驱动设备。
那么, USB Hub 端口或 USB 控制器端口如何发出复位信号? 发出 SE0 信号,并维持至少 10ms。
USB 设备看到 Reset 信号后,需要准备接收"SetAddress()“请求; 如果它不能回应这个 请求, 就是"不能识别的设备”。
4.3.8 设备速率识别
1. 低速/全速
Hub 端口的 D+、D-都有 15K 的下拉电阻,平时为低电平。全速设备内部的 D+有 1.5K 的 上拉电阻, 低速设备内部的 D-有 1.5K 的上拉电阻,连接到 Hub 后会导致 Hub 的 D+或 D-电
平变化,Hub 根据变化的引脚分辨接进来的是全速设备还是低速设备。
2. 高速
高速设备必定兼容全速模式, 所以高速设备内部 D+也有 1.5K 的上拉电阻, 只不过这个 电阻是可以断开的: 工作于高速模式时要断开它。
高速设备首先作为全速设备被识别出来,然后 Hub 如何确定它是否支持高速模式? Hub 端口如何监测一个新插入的 USB 设备能否工作于高速模式? 流程如下:
- 对于低速设备,Hub 端口不会监测它能否工作于高速模式。低速设备不能兼容高速模式。
- Hub 端口发出 SE0 信号,这就是复位信号
- USB 设备监测到 SE0 信号后,会发出"a high-speed detection handshake"信号表示自 己能支持高速模式, 这可以细分为一下 3 种情景:
- 如果 USB 设备原来处于"suspend"状态,它检测到 SE0 信号后, 就发出"a high- speed detection handshake"信号。
- 如果 USB 设备原来处于"non-suspend"状态,并且处于全速模式, 它检测到 SE0 信 号后, 就发出"a high-speed detection handshake"信号。这个情景,就是一个设备刚插 到 Hub 端口时的情况,它一开始工作于全速模式。
- 如果 USB 设备原来处于"non-suspend"状态, 并且处于高速模式,它会切换回到全 速模式(重新连接 D+的上拉电阻),然后发出"a high-speed detection handshake"信号。
“a high-speed detection handshake"信号,就是"高速设备监测握手信号”,既然是 握手信号, 自然是有来有回:
- USB 设备维持 D+的上拉电阻,发出"Chirp K "信号, 表示自己能支持高速模式
- 如果 Hub 没监测到"Chirp K "信号, 它就知道这个设备不支持高速模式
- 如果 Hub 监测到"Chirp K “信号后, 如果 Hub 能支持高速模式, 就发出一系列的"Chirp K”、"Chirp J"信号,这是用来通知 USB 设备: Hub 也能支持高速模式。发出一系列的 “Chirp K”、"Chirp J"信号后,Hub 继续维持 SE0 信号直到 10ms。
- USB 设备发出"Chirp K “信号后,就等待 Hub 回应一系列的"Chirp K”、"Chirp J"信号
- 收到一系列的"Chirp K"、"Chirp J"信号: USB 设备端口 D+的上拉电阻,使能高速模式
- 没有收到一系列的"Chirp K"、"Chirp J"信号: USB 设备转入全速模式
4.3.9 数据信号
1. 低速/全速的 SOP 和 EOP
SOP:Start Of Packet,Hub 驱动 D+、D-这两条线路从 Idle 状态变为 K 状态。 SOP 中 的 K 状态就是 SYNC 信号的第 1 位数据, SYNC 格式为 3 对 KJ 外加 2 个 K。
EOP:End Of Packet,由数据的发送方发出EOP,数据发送方驱动D+、D-这两条线路, 先设为 SE0 状态并维持 2 位时间, 再设置为 J 状态并维持 1 位时间, 最后 D+、D-变为高阻 状态, 这时由线路的上下拉电阻使得总线进入 Idle 状态。
2. 高速的 SOP
高速的 EOP 比较复杂,作为软件开发人员无需掌握。
高速模式中,Ide 状态为:D+、D-接地。SOP 格式为: 从 Idle 状态切换为 K 状态。 SOP 中的 K 状态就是 SYNC 信号的第 1 位数据。
高速模式中的 SYNC 格式为:KJKJKJKJ KJKJKJKJ KJKJKJKJ KJKJKJKK,即 15 对 KJ,外 加 2 个 K。
3. NRZI 与位填充
参考文章:USB 的 NRZI 信号格式, https://zhuanlan.zhihu.com/p/460018993
NRZI:Non Return Zero Inverted Code,反向不归零编码。 NRZI 的编码方位为:对于 数据 0,波形翻转;对于数据 1,波形不变。
使用 NRZI,发送端可以很巧妙地把"时钟频率"告诉接收端: 只要传输连续的数据 0 即 可。在下图中, 低速/全速协议中"Sync Pattern"的原始数据是"00000001",接收端从前面 的 7 个 0 波形就可以算出"时钟频率"。
使用 NRZI 时, 如果传输的数据总是"1",会导致波形维持不变。如果电平长时间维持 不变, 比如传输 100 位 1 时, 如果接收方稍有偏差,就可能认为接收到了 99 位 1、101 位 1。而 USB 中采用了 Bit-Stuffing 位填充处理,即在连续发送 6 个 1 后面会插入 1 个 0,强 制翻转发送信号,从而让接收方调整频率,同步接收。而接收方在接收时只要接收到连续 的 6 个 1 后,直接将后面的 0 删除即可恢复数据的原貌。
NRZI 数据格式如上图所示。
sidebar_position: 5
4.4 USB 协议层数据格式
参考资料:
-
《圈圈教你玩 USB》
-
简书 jianshu_kevin@126.com 的文章
- https://www.jianshu.com/p/3afc1eb5bd32
- https://www.jianshu.com/p/cf8e7df5ff09
- USB 协议(三):https://www.jianshu.com/p/2a6e22194cd3
-
官网:https://www.usb.org/documents
- 《usb_20.pdf》的《chapter5 8 Protocol Layer》
- USB 的 NRZI 信号格式: https://zhuanlan.zhihu.com/p/460018993
- USB2.0 包 Packet 的组成: https://www.usbzh.com/article/detail-459.html
4.4.1 硬件拓扑结构
compound device :多个设备组合起来,通过 HUB 跟 Host 相连
composite device :一个物理设备有多个逻辑设备(multiple interfaces)
在软件开发过程中, 我们可以忽略 Hub 的存在,硬件拓扑图简化如下:
一个物理设备里面可能有多个逻辑设备, Hos 可以外接多个逻辑设备, 硬件拓扑图如 下:
4.4.2 协议层
要理解协议层、理解数据如何传输,带着这几个问题去看文档、看视频:
- 如何寻址设备?
- 如何表示数据方向(读、还是写)
- 如何确认结果?
提前罗列出答案:
- USB 系统是一个 Host 对应多个设备, 要传输数据首先要通知设备:
- 发出 IN 令牌包: 表示想读数据,里面含有设备地址
- 发出 OUT 令牌包:表示想写数据, 里面含有设备地址
- 数据阶段:
- Host 想读数据: 前面发出 IN 令牌包后, 现在读取数据包
- Host 想发出数据:前面发出 OUT 令牌包后, 现在发出数据包
- 结果如何?有握手包
- Host 想读数据, 设备可能未就绪, 就会回应 NAK 包
- Host 想写数据, 它发出数据后,设备正确接收了, 就回复 ACK 包
4.4.3 字节/位传输顺序
先传输最低位(LSB)。在后续文档中,描述数据时按照传输顺序从左到右列出来
4.4.4 SYNC 域
Host 发出 SOP 信号后, 就会发出 SYNC 信号:它是一系列的、最大传输频率的脉冲,接 收方使用它来同步数据。对于低速/全速设备, SYNC信号是8位数据(从做到右是00000001); 对于高速设备, SYNC信号是32位数据(从左到右是00000000000000000000000000000001)。 使用 NRZI 编码时,前面每个"0"都对应一个跳变。
在很多文档里, 把 SOP 和 SYNC 统一称为"SYNC",它的意思是"SYNC"中含有"SOP"。
4.4.5包格式
USB 总线上传输的数据以包为单位。 USB 包里含有哪些内容(“域”)?
- SOP:用来表示包的起始
- SYNC:用来同步时钟
- PID:表示包的类型
- 地址:在 USB 硬件体系中, 一个 Host 对应多个 Logical Device,那么 Host 发出的包, 如何确定发给谁?
- 发给所有设备:包里不含有设备地址
- 发给某个设备:包里含有设备地址、端点号
- 帧号、数据等跟 PID 相关的内容
- CRC 校验码
发起一次完整的传输, 可能涉及多个包。那么,第 1 个包里含有设备地址、端点号, 后续的包就没必要包含设备地址、端点号。
1. PID 域
注意: 所有的 USB 文档提到的"输入"、“输出”,都是基于 Host 的角度, "输出"表示从 Host 输出到设备,"输入"表示 Host 从设备得到数据。
有哪些 USB 包? 根据包数据里的 PID 的 bit1, bit0 可以分为 4 类:
- 令牌包(Token):01B
- 数据包(Data):11B
- 握手包(Handshake):10B
- 特殊包(Special):00B
PID 有 4 位,使用 bit1,bit0 确定分类, 使用 bit3,bit2 进一步细分。如下表(来自 《圈圈教你玩 USB》)所示:
在 USB 包中,PID 域使用 8 位来表示,格式如下:
前 4 位表示 PID,后 4 位是对应位的取反。接收方发现后 4 位不是前 4 位的取反的话, 就认为发生了错误。
2. 令牌包(Token)
令牌类 的 PID ,起 "通知作用 " ,通知谁 ?SOF 令牌包被用来通 知所有设 备, OUT/IN/SETUP 令牌包被用来通知某个设备。
对于 OUT、IN、SETUP 令牌包, 它们都是要通知到具体的设备, 格式如下:
USB 设备的地址有 7 位,格式如下:
USB 设备的端点号有 4 位, 格式如下:
对于 SOF 包,英文名为"Start-of-Frame marker and frame number"。对于 USB 全速 设备, Host 每 1ms 产生一个帧; 对于高速设备, 每 125us 产生一个微帧, 1 帧里有 8 个微 帧。 Host 会对当前帧号进行累加计数, 在每帧或每微帧开始时, 通过 SOF 令牌包发送帧号。 对于高速设备, 每 1 毫秒里有 8 个微帧,这 8 个微帧的帧号是一样的, 每 125us 发送一个 SOF 令牌包。
SOF 令牌包格式如下:
3. 数据包
Host 使用 OUT、IN、SETUP 来通知设备:我要传输数据了。数据通过"数据包"进行传 输。
数据包也有 4 种类型:DATA0、DATA1、DATA2、MDATA。其中 DATA2、MDATA 在高速设备 中使用。对软件开发人员来说,我们暂时仅需了解 DATA0、DATA1。
为什么要引入 DATA0、DATA1 这些不同类型的数据包? 为了纠错。
Host 和设备都会维护自己的数据包切换机制,当数据包成功发送或者接收时,数据包 类型切换。当检测到对方使用的数据包类型不对时,USB 系统认为发生了错误。
比如:
- Host 发送 DATA0 给设备,设备返回 ACK 表示成功接收, 设备期待下一个数据是 DATA1
- 但是 Host 没有接收到 ACK,Host 认为数据没有发送成功,Host 继续使用 DATA0 发送上 一次的数据
- 设备再次接收到 DATA0 数据包, 它就知道:哦,这是重传的数据包
数据包格式如下:
对于全速设备, 数据包中的数据做大是 1023 字节;对于全速设备, 数据包中的数据做 大是 1024 字节。
4. 握手包
握手包有 4 类: ACK、NAK、STALL、NYET
- ACK:数据接收方用来回复发送方,表示正确接收到了数据并且有足够的空间保存数据。
- NAK:Host 发送数据给设备时, 设备可以回应 NAK 表示"我还没准备好,没办法接收数据"; Host 想读取设备的数据时, 设备可以回复 NAK 表示"我没有数据给你"。
- STALL:表示发生了错误,比如设备无法执行这个请求(不支持该断点等待)、断点已经挂起。设备返回 STALL 后,需要主机进行干预才能接触 STALL 状态。
- NYET:仅适用于高速设备。 Host 可以发出 PING 包用来确认设备有数据,设备可以回应 NYET 表示"还没呢"。Hub 也可以回应 NYET 表示低速/全速传输还没完结。
4.4.6 传输细节
1. 传输(Transfer)和事务(Transaction)
USB 传输的基本单位是包(Packet),包的类型由PID 表示。 一个单纯的包,是无法传输 完整的数据。
为什么?比如想输出数据,可以发出 OUT 令牌包, OUT 令牌包可以指定目的地。但是数 据如何传输呢? 还需要发出 DATA0 或 DATA1 数据包。设备收到数据后, 还要回复一个 ACK 握手包。
所以,完整的数据传输, 需要涉及多个包:令牌包、数据包、握手包。这个完整的数 据传输过程,被称为事务(Transaction)。
有些事务需要握手包,有些事务不需要握手包,有些事务可以传输很大的数据,有些 事务只能传输小量数据。
-
有四类事务:
- 批量事务:用来传输大量的数据,数据的正确性有保证,时效没有保证。
- 中断事务:用来传输周期性的、小量的数据, 数据的正确性和时效都有保证。 ③ 实时事务:用来传输实时数据, 数据的正确性没有保证,时效有保证。
- 建立事务:跟批量事务类似,只不过令牌包是 SETUP 令牌包。
-
有四类传输(Transfer):
- 批量传输:就是使用批量事务实现数据传输, 比如 U 盘。
- 中断传输:就是使用中断事务实现数据传输, 比如鼠标。
- 实时传输:就是使用实时事务实现数据传输, 比如摄像头。
- 控制传输:由建立事务、批量事务组成,所有的 USB 设备都必须支持控制传输, 用于" 识别/枚举"
-
暂时记住这个关系:
- BIT 组成域(Field)
- 域组成包(Packet)
- 包组成事务(Transaction)
- 事务组成传输(Transfer)
2. 过程(stage)和阶段(phase)
事务由多个包组成, 比如 Host 要发送数据给设备,这就会涉及很多个包:
- Host 发出 OUT 令牌包, 表示要发数据给哪个设备
- Host 发出 DATA0 数据包
- 设备收到数据后, 回应 ACK 包
这个完整的事务涉及 3 个包(Packet),分为 3 个阶段(Phase):
- 令牌阶段(Token phase):由令牌包实现
- 数据阶段(Data phase):由数据包实现
- 握手阶段(Handshake phase):由握手包实现
事务由包组成, 这些包分别处于 3 个阶段(phase):令牌阶段,数据阶段, 握手阶段。
对于批量传输、中断传输、实时传输,它们分别由一个事务组成,不再细分为若干个 过程。
但是控制传输由多个事务组成,这些事务分别处于 3 个过程: 建立过程(stage)、数据 过程(stage)、状态过程(stage)。
总结起来就是:
- 控制传输由多个过程(stage)组成, 每个过程由一个事务来实现
- 每个事务由多个阶段(phase)组成, 每个阶段有一个包来实现
3. 批量传输
批量传输用批量事务来实现,用于传输大量的数据, 数据的正确性有保证, 时效没有 保证。
批量事务由 3 个阶段(phase)组成: 令牌阶段、数据阶段、握手阶段。每个阶段都是一 个完整的包,含有 SOP、SYNC、PID、EOP。
下图中各个矩形框就对应一个完整的包。
《圈圈教你玩 USB》中有详细的示例:
4.中断传输
中断传输用中断事务来实现,用于传输小量的、周期性的数据,数据的正确性和时效 都有保证。
中断事务由 3 个阶段(phase)组成: 令牌阶段、数据阶段、握手阶段。每个阶段都是一 个完整的包,含有 SOP、SYNC、PID、EOP。
下图中各个矩形框就对应一个完整的包。
中断事务跟批量事务非常类似,Host 使用它来周期性地读数据、写数据。
以鼠标为例,我们需要及时获得鼠标的数据, 不及时的话你会感觉鼠标很迟钝。但是 USB 协议中并没有中断功能,它使用"周期性的读、写"来实现及时性。具体过程如下:
-
Host 每隔 n 毫秒发出一个 IN 令牌包
-
鼠标有数据的话,发出 DATA0 或 DATA1 数据包给 Host;鼠标没有数据的话,发出 NAK 给 Host。
中断事务的优先级比批量事务更高,它要求实时性,而批量事务不要求实时性。
5.实时传输
实时传输用实时事务来实现, 用于传输实时数据, 对数据的正确性没有要求。
实时事务由 2 个阶段(phase)组成: 令牌阶段、数据阶段。每个阶段都是一个完整的包, 含有 SOP、SYNC、PID、EOP。
实时事务不需要握手阶段,一个示例的场景是:为了传输摄像头的实时数据,偶尔的 数据错误是可以忍受的,大不了出现短暂的花屏。如果为了解决花屏而重传数据, 那就会
导致后续画面被推迟,实时性无法得到保证。
下图中各个矩形框就对应一个完整的包。
实时事务跟中断事务非常类似,Host 也会周期性的发起实时事务,主要区别在于:
-
实时事务不要求准确性,没有握手阶段
-
实时事务传输的数据量比较大, 中断事务传输的数据量比较小
6. 控制传输
在使用批量传输时, 使用 IN 令牌包或 OUT 令牌包表示数据传输方向。
控制传输的令牌包永远是 SETUP,怎么分辨是读数据, 还是写数据? 发出 SETUP 令牌包 后,还要发出 DATA0 数据包,根据数据的内容来确定后续是读数据,还是写数据。这个过 程称为"建立事务"(SETUP Transaction)
但是控制传输由多个事务组成,这些事务分别处于 3 个过程: 建立过程(stage)、数据 过程(stage)、状态过程(stage)。
-
建立过程(stage),使用 SETUP 事务:Host 发出 SETUP 令牌包、DATA0 数据包、得到 ACK 握手包
-
数据过程(stage),使用批量事务:
- 对于输出:Host 发出 OUT 令牌包,发出 DATA0、DATA1 数据包、得到 ACK 握手包
- 对于输入:Host 发出 IN 令牌包,读到 DATA0、DATA1 数据包、发出 ACK 握手包 ③ 状态过程(stage),使用批量事务:
- 对于输出:Host 发出 IN 令牌包,读到 DATA1 数据包,发出 ACK 握手包 b. 对于输入:Host 发出 OUT 令牌包,发出 DATA1 数据包,等待 ACK 握手包
上图中的每一个方框,都是一个完整的事务, 含有: Token Packet、Data Packet、 Handshake Packet。
4.4.7 使用工具体验数据格式
LeCroy(力科)成立于 1964 年, 是一家专业生产示波器厂家。旗下生产有数字示波器、 SDA 系列数字示波器、混合信号示波器、模块化仪器、任意波形发生器。
官网是:https://teledynelecroy.com/,似乎无法注册新用户,无法下载软件。 可以在搜索引擎里搜"usbprotocolsuite"。
安装"usbprotocolsuite"后, 可以在文档目录里找打很多示程序(后缀名为 usb):
使用"usbprotocolsuite"打开这些文件,即可体验 USB 数据传输:
4.5 USB 描述符
4.5.1
1. USB 设备状态切换图
4.5.2 标准设备请求
1.SETUP事务的数据格式
Host 使用控制传输来识别设备、设置设备地址、启动设备的某些特性, 对于控制传输, 它首先发出"setup 事务",如下:
在"setup 事务"中,
-
SETUP 令牌包:用来通知设备, “要开始传输了”
-
DATA0 数据包:它含有固定的格式, 用来告诉设备"是读还是写"、“读什么”、“写什么”
Host 通过 DATA0 数据包发送 8 字节数据给设备,它的格式如下图所示:
2. 标准设备请求
控制传输的建立事务中, 可以使用下列格式的数据:
上表中各个"宏"取值如下:
3. 设备/配置/接口/端点
在 SETUP 事务的数据里, 表示了要访问的是什么: Device?Interface?Endpoint?
对于一个USB 设备, 它可以多种配置(Configuration)。比如4G 上网卡就有 2 种配置: U 盘、上网卡。第 1 次把 4G 上网卡插入电脑时,它是一个 U 盘,可以按照里面的程序。装 好程序后, 把它再次插入电脑,它就是一个上网卡。驱动程序可以选择让它工作于哪种配 置,同一时间只能有一种配置。大多数的 USB 设备只有一种配置。
一个配置下,可以有多个接口(Interface),接口等同于功能(Function)。比如 USB 耳 机有两个接口(功能):声音收发、按键控制。
一个接口, 可能有多个设置(Setting),比如默认设置下它使用较低的带宽, 可以选择 其他设置以使用更高带宽。
一个接口, 由一个或多个端点(Endpoint)组成。端点 0 属于整个设备的, 端点 0 是双 向的。接口还可以有其他端点, 这些端点是单向的, 要么是批量(Bulk)端点、要么是中断 (Interrupt)端点、要么是同步(Isochronous)端点。
4.5.3 描述符
怎么描述设备、配置、接口、端点?使用描述符(Descriptors),有设备描述符、配置 描述符、接口描述符、端点描述符。所谓描述符,就是一些格式化的数据, 用来描述信息。
一个 USB 设备:
-
只有一个设备描述符:用来表示设备的 ID、它有多少个配置、它的端点 0一次最大能传 输多少字节数据
-
可能有多个配置描述符:用来表示它有多少个接口、供电方式、最大电流
-
一个配置描述符下面,可能有多个接口描述符:用来表示它是哪类接口、有几个设置 (Setting)、有几个端点
-
一个接口描述符符下面,可能有多个端点描述符: 用来表示端点号、方向(IN/OUT)、类 型(批量/中断/同步)
还有一些字符串描述符(String descriptors),它用可读的文字来描述设备,是可选 的。
1. 设备描述符
2. 配置描述符
3. 接口描述符
4. 端点描述符
5.示例
在 Ubuntu 中可以执行 lsusb -v查看 USB 设备的描述符信息:
book@100ask:~$ sudo lsusb -v
[sudo] password for book:
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 9 Hub
bDeviceSubClass 0 Unused
bDeviceProtocol 0 Full speed (or root) hub
bMaxPacketSize0 64
idVendor 0x1d6b Linux Foundation
idProduct 0x0002 2.0 root hub
bcdDevice 5.04
iManufacturer 3 Linux 5.4.0-124-generic ehci_hcd
iProduct 2 EHCI Host Controller
iSerial 1 0000:02:03.0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 25
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xe0
Self Powered
Remote Wakeup
MaxPower 0mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 9 Hub
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0 Full speed (or root) hub
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0004 1x 4 bytes
bInterval 12
Hub Descriptor:
bLength 9
bDescriptorType 41
nNbrPorts 6
wHubCharacteristic 0x000a
No power switching (usb 1.0)
Per-port overcurrent protection
bPwrOn2PwrGood
bHubContrCurrent DeviceRemovable PortPwrCtrlMask
Hub Port Status:
Port 1: 0000.0100 Port 2: 0000.0100 Port 3: 0000.0100 Port 4: 0000.0100 Port 5: 0000.0100
Port 6: 0000.0100
10 * 2 milli seconds
0 milli Ampere
0x00
0xff
power
power
power
power
power
power
Device Status: 0x0001
Self Powered
Bus 002 Device 003: ID 0e0f:0002 VMware, Inc. Virtual USB Hub
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 9 Hub
bDeviceSubClass 0 Unused
bDeviceProtocol 0 Full speed (or root) hub
bMaxPacketSize0 8
idVendor 0x0e0f VMware, Inc.
idProduct 0x0002 Virtual USB Hub
bcdDevice 1.00
iManufacturer 1 VMware, Inc.
iProduct 2 VMware Virtual USB Hub
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 25
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 1 VMware, Inc.
bmAttributes 0xe0
Self Powered
Remote Wakeup
MaxPower 0mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 9 Hub
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0 Full speed (or root) hub
iInterface 1 VMware, Inc.
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0001 1x 1 bytes
bInterval 255
Hub Descriptor:
bLength 9
bDescriptorType 41
nNbrPorts 7
wHubCharacteristic 0x0009
Per-port power switching
Per-port overcurrent protection
bPwrOn2PwrGood
bHubContrCurrent DeviceRemovable PortPwrCtrlMask
Hub Port Status:
Port 1: 0000.0100 Port 2: 0000.0100 Port 3: 0000.0100 Port 4: 0000.0100 Port 5: 0000.0100 Port 6: 0000.0100
Port 7: 0000.0100
50 * 2 milli seconds
100 milli Ampere
0x00
0xfe
power
power
power
power
power
power
power
Device Status: 0x2909
Self Powered
Bus 002 Device 002: ID 0e0f:0003 VMware, Inc. Virtual Mouse
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 0 (Defined at Interface level)
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x0e0f VMware, Inc.
idProduct 0x0003 Virtual Mouse
bcdDevice 1.03
iManufacturer 1 VMware
iProduct 2 VMware Virtual USB Mouse
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 34
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 1 VMware
bmAttributes 0xc0
Self Powered
MaxPower 0mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 2 Mouse
iInterface 1 VMware
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.10
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 46
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 1
Device Status: 0x0001
Self Powered
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 9 Hub
bDeviceSubClass 0 Unused
bDeviceProtocol 0 Full speed (or root) hub
bMaxPacketSize0 64
idVendor 0x1d6b Linux Foundation
idProduct 0x0001 1.1 root hub
bcdDevice 5.04
iManufacturer 3 Linux 5.4.0-124-generic uhci_hcd
iProduct 2 UHCI Host Controller
iSerial 1 0000:02:00.0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 25
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xe0
Self Powered
Remote Wakeup
MaxPower 0mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 9 Hub
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0 Full speed (or root) hub
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0002 1x 2 bytes
bInterval 255
Hub Descriptor:
bLength 9
bDescriptorType 41
nNbrPorts 2
wHubCharacteristic 0x000a
No power switching (usb 1.0)
Per-port overcurrent protection
bPwrOn2PwrGood 1 * 2 milli seconds
bHubContrCurrent 0 milli Ampere
DeviceRemovable 0x00
PortPwrCtrlMask 0xff
Hub Port Status:
Port 1: 0000.0103 power enable connect
Port 2: 0000.0107 power suspend enable connect
Device Status: 0x0001
Self Powered
4.5.4 设备枚举过程示例
使用"usbprotocolsuite"打开,可以看到设备的枚举过程:
- 使用控制传输,读取设备信息(设备描述符):第一次读取时, 它只需要得到 8 字节数据, 因为第 8 个数据表示端点 0 能传输的最大数据长度。
- Host 分配地址给设备, 然后把新地址发给设备:
- 使用新地址, 重新读取设备描述符, 设备描述符长度是 18:
- 读取配置描述符: 它传入的长度是 255,想一次性把当前配置描述符、它下面的接口描 述符、端点描述符全部读出来
- 读取字符描述符
4.6 USBX 组件
4.6.1 Azure RTOS 介绍
Azure RTOS 平台是运行时解决方案的集合,包括 Azure RTOS ThreadX、Azure RTOS NetX 和 NetX Duo、Azure RTOS FileX、Azure RTOS GUIX 和 Azure RTOS USBX。
Azure RTOS ThreadX 是专用于深度嵌入式应用程序的高级实时操作系统 (RTOS)。 Azure RTOS ThreadX 具有多种优势,其中包括高级调度设施、消息传递、中断管理和消息 服务。 Azure RTOS ThreadX 具有许多高级功能, 其中包括 picokernel 体系结构、抢占 式阈值调度、事件链和一系列丰富的系统服务。
USBX 是 Azure®RTOS USB 主机和 USB 设备嵌入式堆栈。它与 ThreadX 紧密耦合。在某些 类中, 它需要 FileX 和 NetX Duo 堆栈。它允许使用具有多种配置的 USB 设备、复合设备和 USB OTG 进行操作。它支持 USB 电源管理。
USBX 为 USB 主机和 USB 设备堆栈提供了大量的 USB 类。 一旦低级驱动程序能够响应 USBX 请求, 模块化架构就可以更容易地移植到不同的 USB 硬件 IP 上。
所有 STM32 USB IP(主机、设备、 OTG、高速和全速) 均由 USBX 通过通用 STM32 HAL 驱动程序 API 透明支持。
4.6.2 USBX 层次
参考资料:
https://wiki.stmicroelectronics.cn/stm32mcu/wiki/Introduction_to_USBX
USBX 分为三层, 如下图所示:
- 控制器层:最底层,USB 设备控制器的驱动程序,通常是 HAL 库
- stack layer:实现 USB 设备的基本操作,比如描述符的操作、使用 endpoint 进行数据 传输
- Class layer:实现各类 USB 设备的操作,比如 HID 设备、音频设备、虚拟串口,给 APP 提供接口
在 STM32 的固件中, 可以看到 USBX 目录,比如:
移植 Controller layer、stack layer、Class layer 并不复杂, 重点在于 2 点:
- 怎么初始化硬件以确保 Controller layer 可以正常运行
- 怎么编写 APP:提供设备信息、传输数据
4.6.3 USBX 的基本配置
USBX 依赖于 Azure®RTOS ThreadX,但是也可以单独使用 USBX,这需要配置。通常在 “ux_user.h”里进行配置,配置项如下:
- 使用单独模式或 RTOS 模式:
/* Defined, this macro will enable the standalone mode of usbx. */
#define UX_STANDALONE
当没有定义“UX_STANDALONE”时就是使用 RTOS 模式, 可以使用 ThreadX 提供的互斥 量函数实现阻塞式读写(“blocking”), 比如对于 USB 虚拟串口, 可以使用如下函数:
UINT _ux_device_class_cdc_acm_read(UX_SLAVE_CLASS_CDC_ACM *cdc_acm, UCHAR *buffer,
ULONG requested_length, ULONG *actual_length);
UINT _ux_device_class_cdc_acm_write(UX_SLAVE_CLASS_CDC_ACM *cdc_acm, UCHAR *buffer, ULONG requested_length, ULONG *actual_length);
这 2 个函数发起数据传输,在传输过程中线程阻塞,传输完成后线程被唤醒。
当定义“UX_STANDALONE”时就是使用单独模式, 不能再使用上面的阻塞函数,而要使 用非阻塞的函数(non-blocke):
UINT _ux_device_class_cdc_acm_read_run(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,
UCHAR *buffer, ULONG requested_length, ULONG *actual_length);
UINT _ux_device_class_cdc_acm_write_run(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,
UCHAR *buffer, ULONG requested_length, ULONG *actual_length);
它们只是发起传输,然后就即刻返回。需要提供回调函数,在回调函数里分辨数据是 否传输完成。
- 非阻塞模式:
/* Defined, this macro disables CDC ACM non-blocking transmission support. */ //#define UX_DEVICE_CLASS_CDC_ACM_TRANSMISSION_DISABLE
定义 UX_DEVICE_CLASS_CDC_ACM_TRANSMISSION_DISABLE 是,就禁止了“非阻塞模式”, 这时只能使用基于 RTOS 的阻塞函数。
换句话说, 要使用单独模式的非阻塞函数, 就不能定义这个配置项。
- USB HOST/Device 模式
/* Defined, this value will only enable the host side of usbx. */
/* #define UX_HOST_SIDE_ONLY */
/* Defined, this value will only enable the device side of usbx. */
#define UX_DEVICE_SIDE_ONLY
本课程定义“UX_DEVICE_SIDE_ONLY”, 仅作为 USB Device。
4.7 移植 USBX 实现虚拟串口
本节程序源码为“3_程序源码01_视频配套的源码 4-7_移植 USBX 实现虚拟串口 uart_usb.7z”,在上一节代码 uart_rtos.7z 的基础上修改得来。
移植 Controller layer、stack layer、Class layer 并不复杂, 重点在于 2 点:
- 怎么初始化硬件以确保 Controller layer 可以正常运行
- 怎么编写 APP:提供设备信息、传输数据
4.7.1 配置 USB
4.7.2 添加 USBX 代码
1. 复制代码
找到固件库,如下:
把 usbx 整个目录复制到工程“MiddlewaresThird_Party”目录下, 如下:
2. 添加进工程
需要添加 USBX 的 3 层源码。
先仿照下图添加“Class layer”源码,添加含有“ux_device_class_cdc_acm ”前缀 的 C 文件:
再仿照下图添加“stack layer”源码,可以从文件名的前面看出它们的作用, 比如 “ ux_device_stack ”表示这是 stack 源码,“ ux_utility ”表示这 是 辅助 函数 , “ux_system”表示是这是系统函数:
最后仿照下图添加“Controller layer”, 添加“ux_dcd_stm32”前缀的 C 文件:
4.7.3 添加 USBX APP 代码
参考工程:
-
GIT 仓库:https://github.com/STMicroelectronics/STM32CubeH5.git,
-
工程路径:
STM32CubeH5\Projects\NUCLEO-H563ZI\Applications\USBX\Ux_Device_HID_CDC_ACM
在网盘资料中, 找到如下目录:
把 app 文件夹复制到工程的“MiddlewaresThird_Partyusbx”目录下, 如下图所示:
各个文件的作用为:
-
ux_user.h:配置 USBX
-
ux_stm32_config.h:里面含有配置项, 表示 STM32 支持多少个 endpoint
-
ux_device_descriptors.c/h:USB 虚拟串口的描述符信息
-
ux_device_cdc_acm.c:USB 串口的 Activate/DeActivate 函数
-
app_usbx_device.c:调用 stack layer 函数, 模拟 USB 串口
在工程里添加上述文件, 如下图所示:
4.7.4 修改 usb.c
使用 STM32CubeMX 配置 usb 后生成的 usb.c 里,只是初始化了 USB 控制器,并未启动 它,也没有跟 USBX 建立联系, 需要修改代码。
代码如下:
23 /* USER CODE BEGIN 0 */
24 #include "ux_port.h"
25 #include "ux_device_descriptors.h"
26 #include "ux_dcd_stm32.h"
27 /* USER CODE END 0 */
/* 省略 */
33 void MX_USB_PCD_Init(void)
34 {
35
36 /* USER CODE BEGIN USB_Init 0 */
37 UINT MX_USBX_Device_Init(void);
38 MX_USBX_Device_Init();
39
40 /* USER CODE END USB_Init 0 */
41
42 /* USER CODE BEGIN USB_Init 1 */
43
44 /* USER CODE END USB_Init 1 */
45 hpcd_USB_DRD_FS.Instance = USB_DRD_FS;
46 hpcd_USB_DRD_FS.Init.dev_endpoints = 8;
47 hpcd_USB_DRD_FS.Init.speed = USBD_FS_SPEED;
48 hpcd_USB_DRD_FS.Init.phy_itface = PCD_PHY_EMBEDDED;
49 hpcd_USB_DRD_FS.Init.Sof_enable = DISABLE;
50 hpcd_USB_DRD_FS.Init.low_power_enable = DISABLE;
51 hpcd_USB_DRD_FS.Init.lpm_enable = DISABLE;
52 hpcd_USB_DRD_FS.Init.battery_charging_enable = DISABLE;
53 hpcd_USB_DRD_FS.Init.vbus_sensing_enable = DISABLE;
54 hpcd_USB_DRD_FS.Init.bulk_doublebuffer_enable = DISABLE;
55 hpcd_USB_DRD_FS.Init.iso_singlebuffer_enable = DISABLE;
56 if (HAL_PCD_Init(&hpcd_USB_DRD_FS) != HAL_OK)
57 {
58 Error_Handler();
59 }
60 /* USER CODE BEGIN USB_Init 2 */
61
62 HAL_PWREx_EnableVddUSB();
63 HAL_PWREx_EnableUSBVoltageDetector();
64
65 HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, 0x00, PCD_SNG_BUF, 0x14);
66 HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, 0x80, PCD_SNG_BUF, 0x54);
67 HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPINCMD_ADDR, PCD_SNG_BUF, 0x94); 68 HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPOUT_ADDR, PCD_SNG_BUF, 0xD4);
69 HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPIN_ADDR, PCD_SNG_BUF, 0x114); 70 ux_dcd_stm32_initialize((ULONG)USB_DRD_FS, (ULONG)&hpcd_USB_DRD_FS);
71
72 HAL_PCD_Start(&hpcd_USB_DRD_FS);
73
74 /* USER CODE END USB_Init 2 */
75 }
第 38 行:调用 USBX 的函数, 添加 USB 串口的支持。
第 62~63 行:使能 USB 控制器的电源。
第 65 69 行:设置 endpoint 的“Packet Buffer Memory”,这个概念可以参考:
http://www.51hei.com/bbs/dpj-40953-1.html。
第 70 行 : 把 STM32 USB 控 制 器 的 句 柄 , 传 给 USBX 系 统 ,
“usbx_stm32_device_controllers”的代码会使用这个句柄来操作硬件。 第 72 行:启动 USB 控制器。
4.7.5 创建 USBX 任务
使 用 单 独 模 式 (STANDALONE ) 时 , 需 要 创 建 一 个 任 务 , 不 断 运 行 “_ux_system_tasks_run ”函数。以下代码是在 FreeRTOS 的默认任务里运行和这个函数:
26 /* USER CODE BEGIN Includes */
27 #include "stdio.h"
28 #include "draw.h"
29 #include "ux_api.h"
30 /* USER CODE END Includes */
/* 省略 */
195 /* USER CODE END Header_StartDefaultTask */
196 void StartDefaultTask(void *argument)
197 {
198 /* USER CODE BEGIN defaultTask */
199 /* Infinite loop */
200 for(;;)
201 {
202 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
203 vTaskDelay(500);
204
205 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);
206 vTaskDelay(500);
207 ux_system_tasks_run();
208 }
209 /* USER CODE END defaultTask */
210 }
第 29 行,包含 USBX 的头文件。
第 207 行, 调用 USBX 的系统函数。
4.7.6 设置 MDK-ARM 工程
如下图配置:
- 添加宏开关: UX_INCLUDE_USER_DEFINE_FILE(图中标号 2)
- 添加头文件目录(图中标号 5)
4.7.7 添加使用串口的代码
在“CoreSrcapp_freertos.c”里添加 USB 串口的发送测试代码:
26 /* USER CODE BEGIN Includes */
27 #include "stdio.h"
28 #include "draw.h"
29 #include "ux_api.h"
30 /* USER CODE END Includes */
/* 省略 */
69 static void SPILCDTaskFunction( void *pvParameters )
70 {
71 char buf[100];
72 int cnt = 0;
73
74 while (1)
75 {
76 sprintf(buf, "USB Serial Send Test : %d\r\n", cnt++);
77 //Draw_String(0, 0, buf, 0x0000ff00, 0);
78
79 int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);
80 ux_device_cdc_acm_send((uint8_t *)buf, strlen(buf), 1000);
81 vTaskDelay(1000);
82 }
83 }
第 29 行:包含头文件。
第 79~80 行:使用 USB 串口发送数据。
在“MiddlewaresThird_Partyusbxappux_device_cdc_acm.c”中,有如下代码:
111 static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length)
112 {
113 int Draw_String(uint32_t x, uint32_t y, char *str, uint32_t front_color, uint32_t
back_color);
114 if (status == UX_SUCCESS)
115 {
116 data_pointer[length] = '\0';
117 Draw_String(0, 0, (char *)data_pointer, 0x0000ff00, 0);
118 }
119 return 0;
120 }
当 USB 串口收到数据后, ux_device_class_cdc_acm_read_callback 函数被调用。 第 117 行把接收到的数据在 LCD 上显示处来。
4.7.8 上机实验
烧写运行程序后,接上 USB 线,在电脑上可以识别出 USB 串口,查看设备管理器,可 以看到如下设备:
使用串口工具打开这个串口, 可以连续不断接收到数据,如下所示:
在串口工具上发送数据时,在板子的 LCD 上会有显示。
4.8 虚拟串口源码分析与改造
本节程序源码为“3_程序源码\01_视频配套的源码\ 4-8_虚拟串口源码分析与改造 \uart_usb_freertos.7z”,在上一节代码 uart_usb.7z 的基础上修改得来。
4.8.1 描述符的设置
在“Middlewares\Third_Party\usbx\app\ux_device_descriptors.c”有设备描述符、 配置描述符、接口描述符、端点描述符的定义。
比如, 设备描述符在如下代码中设置:
配置描述符在如下代码中设置:
4.8.2 数据收发函数
涉及文件为:demo\Middlewares\Third_Party\usbx\app\ux_device_cdc_acm.c。 开发板通过 USB 串口发出数据时, 使用以下函数:
/* 启动发送 */
UINT ux_device_class_cdc_acm_write_with_callback(UX_SLAVE_CLASS_CDC_ACM *cdc_acm, UCHAR *buffer, ULONG requested_length);
/* 发送完毕的回调函数 */
static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length);
我们将会实现如下函数,它使用“ux_device_class_cdc_acm_write_with_callback ” 来启动发送,然后等待“ux_device_class_cdc_acm_write_callback”唤醒:
int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);
开发板接收到 USB 串口数据时,以下回调函数被调用:
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length);
我们可以改造这个函数, 把接收到的数据写入队列。
4.8.3 使用 FreeRTOS 改造代码
对于发送, 实现以下函数:启动发送之后阻塞,等待回调函数唤醒或超时。
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length);
对于接收, 实现以下函数:把接收到的数据写入队列。
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length);
然后提供这个函数:
int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout);