你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】USB CDC 类入门培训

[复制链接]
STMCU小助手 发布时间:2022-3-3 22:19
1 前言9 H/ d8 G" v% I
本文节选自 2017 年度 USB CDC 类培训内容的整理,主要目的是以方便些没有到现场参加培训的碟粉们可以参阅学习。本文力求从理论到实践,尽量给读者一个整体了解 USB CDC 类的窗口。当然,阅读此文,还是需要基本的 USB 知识,这个请读者自行预备。
  [' x1 P7 M* D$ D) X6 }( i0 q7 K8 S2 b2 u) n" [. U- l2 G$ R# f0 c
5 D. B9 c8 e2 Y' B: L" n6 T! L7 r
2 USB CDC 类基础理论知识介绍1 T; U4 b3 o9 j) g5 U
2.1 USB CDC 类、USB2.0 标准与 PSTN 之间的关系
, V5 S  h% E. i8 N5 l
CDC(Communication Device Class)类是 USB2.0 标准下的一个子类,定义了通信相关设备的抽象集合。它与 USB2.0 标准以及其下的子类的相互关系如下图所示:
! l* D+ R6 ^' d9 C
8 T" @7 Y5 y7 O P(8JX8POO`@7AT%H[8RFPF7.png . W7 Z8 N. {' X$ Q" N3 E9 B3 f
9 Y1 a- D8 x/ D- h  s
如上图,USB2.0 标准下定义了很多子类,有音频类,CDC 类,HID,打印,大容量存储类,HUB,智能卡等等,这些在urb.org 官网上有具体的定义,这里我们主要讲的是通信类 CDC,CDC 类下面,根据具体的应用场合,又有一些子类,这里我们主要讲的是 PSTN(Public Switched Telephone Network)。从 PSTN 官方标准文档来看,PSTN 子类是一个与电信相关的子类,而这里,我们只是将它作为一个普通的通信设备使用,并没有使用到它的一些电话特性。
* P9 t% U" Z9 W: V
7 d& l: f& S) J% N3 Z/ a2.2 从一个具体的 CDC 类通信数据说起4 h) f0 y2 X* N1 }3 K" q2 ?4 j4 e
9 f) x& i0 F) g9 t% ~9 ^
Y@W2P~2GX2%~KC6WR7CFM.png 0 G5 f* a# Q' c' k! C
. J" {7 \' h5 _1 t  I
如上图,USB CDC 类的通信部分主要包含三部分:枚举过程、虚拟串口操作和数据通信。其中虚拟串口操作部分并不一定强制需要,因为若跳过这些虚拟串口的操作,实际上 USB 依然是可以通信的,这也就是为什么上图中,在操作虚拟串口之前会有两条数据通信的数据。之所以会有虚拟串口操作,主要是我们通常使用 PC 作为 Host 端,在 PC 端使用一个串口工具来与其进行通信,PC 端的对应驱动将其虚拟成一个普通串口,这样一来,可以方便 PC 端软件通过操作串口的方式来与其进行通信,但实际上,Host 端与 Device 端物理上是通过 USB 总线来进行通信的,与串口没有关系,这一虚拟化过程,起决定性作用的是对应驱动,包含如何将每一条具体的虚拟串口操作对应到实际上的 USB 操作。这里需要注意地是,Host 端与 Device端的 USB 通信速率并不受所谓的串口波特率影响,它就是标准的 USB2.0 全速(12Mbps)速度,实际速率取决于总线的实际使用率、驱动访问 USB 外设有效速率(两边)以及外部环境对通信本身造成的干扰率等等因素组成。( y! p, B  D3 O/ ]3 p/ K: Y: O! S

# P3 _' r* p& u% T% h0 g  ]( T3 ^4 s* b' G7 v$ B
2.3 CDC 类设备枚举过程
" U7 B( T7 }- `1 I, I& tCDC 类设备与其他标准 USB 设备枚举过程的并没有什么特殊的地方。在设备描述符内可以使用 DeviceClass=0x00,SubClass=0x00, Protocol=0x00 表示此类信息在接口描述符内给出;或者也可以使用 0x02,0x00,0x00;来表明该设备为CDC 类设备。或者使用 0xef, 0x02,0x01 表示当前为复合设备。7 O7 a1 I+ @- `7 Z& s1 S/ t
CDC 类设备在枚举过程中最主要的信息存储在配置描述符内:
4 y% w8 l  d: w' [% x; f+ u& ^. I, S" |! T3 x' a7 y' F
$)3QWWHKWI[PRP88L8Y4YOQ.png 5 K4 c* ]( s) Q, D

! _" w8 o5 j) T  r如上图所示,CDC 类的配置描述符一般包含两个接口(Interface 0),一个控制接口,另外一个是数据接口(Interface 1), 除此之外,还有一个虚线指向的 IAD(Interface Association Description),这个表示这个是不是可选的,得根据实际情况来确定其是否真实存在。9 C1 n" d* N8 H& o
) u8 ]% l6 E8 v8 d% _0 V5 a" P

" G; S4 w2 ^# m% B2.3 1 控制接口
( K' u  d; I5 d/ F9 q  q$ v; q% i控制接口下包含类描述符合一个端点(ie:0x82),这个端点(中断传输模式)为异步通知消息的端点,当设备端需要向 Host 端发送异步消息时,可以通错此端点来发送,但平时主机端都是通过端点 0 来向设备端发送控制消息的,比如那些虚拟串口的操作指令等等。
4 ?  o) e: Q( D+ I8 \1 n1 N除这异步通知端点外,控制接口下还包含 CDC 类相关描述符,这其中就包含 Header 描述符,Call Management 描述符,ACM 描述符以及 Union 描述符。这些功能描述符整合在一起用来描述此 USB 设备的一些功能特性,比如 AT 指令支持情况,ACM 模型下的指令集支持情况,以及还有哪些接口与此接口一起对应 Host 端的一个功能(驱动)。
3 r8 V7 `) @" _( h0 g" P在具体配置描述符内的控制接口内,功能描述符紧跟在接口描述符后,最后才是端点描述符。
! a6 Z# n* V3 X) K0 Q7 c 控制接口' w* s: G. `. h

  `) B: c# n/ O2 Q8 } JD@ZO`]W``(YK`I~YF%MBBY.png 6 ^1 V3 Y. B  V* x2 A/ o
7 `/ G& C( a4 n! l2 y. R! K
控制接口主要用来做设备管理和电话管理(可选),设备管理涉及到请求(request)和通知(notification),端点 0 一般用做请求,一般用来控制和配置设备的运行状态,而非 0 端点(0x82)一般用作异步事件通知,设备端通过此端点向主机端发送设备内部的一些事件,比如串口状态变化事件,电话状态改变等等。1 d2 t% z& Y' }/ F# _0 v1 ?
这里使用到 ACM 模型,后续将讲到这个模型,并且这里指明使用到 V250 版本的 AT 指令,这些指令是与电话相关的,但在我们这里讲的 CDC 通信实际上并不需要使用这些与电话相关的指令,它只是简单通信而已,这里指出 AT 指令也没有关系,只是实际不用它而已。) r! N, m( E% w+ Q
如上图,bNumEndpoints 表示此接口下包含的端点数,这里为 1 个,即那个异步通知端点。bInterfaceSubClass 为0x02,ACM 通信模型,bInterfaceProtocol 表示 AT 指令集的版本,虽然这里举例为 V2.50,但实际上并没有使用到任何 AT 指令,因此它放' o$ r, J4 e  d$ ?9 ?
 Header 功能描述符; E$ v8 P* G; K

$ N" r1 v# D2 [9 N% }  | Q6QSNWIB3@10BAUU}SR[YO1.png " W2 G  n- {. ~1 L, _  n
2 P, t2 z7 ~7 L1 _
Header 功能描述符表示功能描述符的开始,其他紧跟的内容就是此设备的功能描述符的内容。bcdCDC 表示的是 CDC 的版本。) g- m0 F7 I5 W6 A2 U. L3 n
 ACM 功能描述符
4 y. x7 j  t) i2 S4 t1 @# [' ]7 }! f' g1 T1 c" N  L1 g
RSH0K8}49W5E{$_@VODUAXP.png % o6 ^. R6 |8 s8 _! t

. M2 c  U$ y3 A! ]; j$ Q$ |ACM(Abstract Control Model),即抽象控制模型,PSTN 下,除了 ACM 模型还有还有 DLM(Direct Line Mode),TCM(Telephone Control Model)。
$ k+ q5 f: O7 @1 N; G7 p: }) yPSTN 定义了三种模型LM(Direct Line Mode),ACM(Abstract Control Model)和 TCM(Telephone Control Model).
" b% r" _- Y  a# C( W( k• DLM 模型下,USB 设备直接将模拟信号转化为数字信号,并放到 USB 上传输,数据接口直接使用 Audio 类传输音频数据,控制接口传输的也都是些比较原始的指令,比如脉宽设置,发送脉宽等等;. H6 j5 o: R' P7 V
• ACM 模型则可以很好的支持 AT V250 指令集,数据接口可以使用 Audio 类或 CDC DATA,控制接口传输的也是比较抽象的高层指令,比如设置、获取波特率,设置获取与通信相关的参数等等,而 AT 指令可以通过控制接口或者数据接口,这个在控制接口下的功能描述符 Call Management Descriptor 中指明。
. a1 o+ H9 b: D# _5 B* p' s+ w2 L• TCM 是指在物理上存在多个连接,可以将接口 0 和接口 1 分别对应到不同的物理连接上。
$ P0 S9 T1 \0 \+ P5 B此外,不同的通信模型对应的指令集合(控制指令)也是不同的,而上图中 bmCapliblities 为位图,内部 bit0~bit3 分别表示 4类控制指令集在此设备的支持情况。
8 S+ W! t; [0 M6 e# U/ {& i$ K0 J( l8 }8 O# u$ }
$ A( s, M" w  _- Y# ?! r" h8 X
RLE`QUV0J(AVL]@1G~VIYZB.png
. l, m- H* J/ }! `4 W8 Q8 U
" F$ ]* `  o4 A! |9 j如上表,为 ACM 模型下的指令集,但不是说,这些个指令就一定会在 ACM 模型下存在,此 USB 设备是不是支持此某个控制指令,还得看 bmCapliblities 这个参数具体对应位是否使能。
! [6 |  s' T* `& M% Y在实际的 STM32 USB 协议栈中,针对于 CDC 类,使用 LineStateCoding,GetLineCoding,SetControlState 类指令,用来读取,设置串口波特率以及串口的打开与关闭,这个具体的映射实现是通过主机端的驱动来实现;从设备端来看,当设备端收到这些来自主机端操作串口的控制指令时,这些指令具体怎么执行完全取决于设备端,也就说,所有的这些操作,比如设置波特率为 115200,对于设备端来说这个只是个通过 SetLineCoding 指令传过来的一个参数而已,具体怎么处理这个参数,取决于设备端应用程序具体怎么处理这个参数,这个有用户来处理,这个 115200 波特率与 USB 本身的波特率 12Mbps(全速)是没有关系的。9 [! U& M( I. k% E0 }$ T0 u
 Call Management 功能描述符
2 u$ g/ K& g  c/ ^5 B+ s( K* o$ p' P
{LKZQ11KBEWBB]{WYES78F8.png
; Y* s0 t7 \; y7 Y( e7 V
! q3 b5 C  u, r# rCall Management 描述的就是电话相关的东西,AT 指令集的支持情况。但在这里,我们并没有用到任何与电话相关的指令,因此 bmCapabilities 下的位图各个位都是为 0:Bit0:是否支持电话相关的指令(AT 指令集);Bit1:电话相关的指令(AT 指令集)是否经过 Comm. Class Interface; bDataInterface 表示如有电话时,电话数据内容对应的接口号。: Z+ \0 H8 {2 o
 Union 功能描述符) N0 k+ w, @+ c- B2 B# [9 C( f# L
/ z( P5 I1 w& _/ |8 K1 j+ ]. K4 k
H8}TL~W[ULPS{)A%PCOJIZK.png : J& o: S' b$ ^0 n$ }- s
- E% W& e4 G+ b. c
Union 描述符就是用来告诉主机端,哪些接口是联合在一起的,对应着一个功能,这个功能需要主机装载对应的驱动来实现,因此,功能与驱动是一对一的关系。这里 bControlInterface 值为 0,则表示接口 0 为控制接口,bSubBoardinateInterface0 值为 1,表示接口 1 为控制接口 0 的下级接口,即数据接口。在 CDC 标准中,控制接口是必须的,而数据接口是可选的,因此,数据接口为控制接口的附属。+ H6 v' W( i$ L( g$ d6 n& B6 ]- Z
2.3.2 数据接口1 H% i  e6 L! K, r
( _7 s/ C, u% L: y& H8 j5 {
I6J7EZN4G)JQ08SII~YZ3E4.png
9 t& `& {5 S" o+ A
4 J; N! y( t# {数据接口比较简单,就是数据通信的,用到两个端点 IN/OUT 0x81/0x01,为块传输类型。2 B) ?; |1 I: h! h2 d/ s7 m6 e
- B6 u/ J! |# j3 |4 r0 L) d) w. d
2.3.3 IAD(Interface Association Descriptor)5 p/ D( D6 A8 @6 S) i
! }  U7 \/ t: s) {- n
`}~4[$[WM(ER%LT4C7VZI{Q.png
7 _' |* {$ L4 x
4 ?0 n9 n* p2 J9 h0 J) g3 qUSB 刚出来的时候,一开始默认是一个接口对应一个功能,而一个功能对应着主机端的一个驱动,这在当时是 OK 的,但是后来,人们发现,需要多个接口对应一个功能的时候,比如这个 CDC,除了数据接口外还需要控制接口,这在当时是没有这方面的统一标准,于是就出了 Union 来表示多个接口对应一个功能的情况。再后来,USB 标准协会又增加了 IAD。' G7 ~$ a  C) T# b8 \
IAD 与 Union 类似,Union 是旧版本下实现多个接口对应一个功能的功能描述符,而 IAD 是 USB 协会后来针对多个接口对应一个功能的情况而扩展的,旧的主机可能只支持 Union 方式,但 IAD 并不会影响旧版本主机对设备的识别,因为旧版本主机会通过 Union 来识别哪些接口是联合在一起的,对于 IAD 则跳过忽略;而新版主机则可以通过 IAD 来识别,跳过忽略老的Union,因此两者可以完美兼容,互不影响。因而主机端可以精确地装载对应的驱动。3 L! h" C; ?9 w3 m9 g/ M
IAD 只用在设备描述符中只用了 device class code,并且指明了使用 IAD 来识别设备,比如 bDeviceClass: Miscellaneous(0xef), bDeviceSubClass: Common (0x02), bDeviceProtocol: Interface Association Descriptor (0x01)就是一个例子;0x02,0x00,0x00 是另外一个例子。
& o5 O" f+ k& R/ D' f如上图,bFirstInterface 值为 0,表示第一个接口个接口 0,默认为控制接口;bInterfaceCount 值为 2,标志此功能总共存在2 个接口,那么第二个接口就是接口 1,因为 USB2.0 IAD ECN 补充标准规定,这里提到的接口号必须是连续的,也就是说,接口 0 为第一个控制接口,那么接口 1 则为数据接口。
9 D9 a) e; `7 P% V, Y  s下面我们来个具体的 IAD 例子:. {* V- x# G- P4 E) y! Q
8 m, }" X5 |* M# M( `, x
LW54H3[[IE(W2{`23D7QCO6.png : S  r7 q( z3 p7 |7 }

: W7 Y: G, T7 R% s0 ~如上图所示,一般 IAD 存在的情况下,在设备描述符中 DeviceClass 等三个参数不再都为 0x00,图 12 中为 0xef,0x02,0x01,这个表示是复合设备,此时,可以使用 IAD 来定义多个接口联合起来对应一个 USB 驱动。从 IAD 中可以看出,bFunctionClass 参数就定义了此 IAD 表示的设备为 CDC 类设备,ACM 模型。就这样,通过 IAD 描述符,实现了与 Union 功能描述符相同的功能。
& ?) t+ z0 A8 T+ m+ M2 x6 C- e1 g4 r9 f. J" X- A* _

. Z; F# {0 ^7 c2.3.4 ACM 模型) O3 [2 |* }( o- }* A
之前我们已经在控制接口中的功能描述符中已有对 ACM(Abstract Control Mode)模型的简介,也有提到过,在 PSTN 中,除了 ACM 模式,还有 TCM,DLM 模式。这三种模式,不同的模式下包含的控制指令集是不尽相同的,有部分控制指令可能同时存在两个或三个模式下,除了控制指令,还有异步通知消息,这个在三个不同模式下也是不相同的。; `9 p- F8 @) U: |
& @5 D. a( z& H
8I8Z_97XX}_7UBW]N)YA[Y6.png
8 {* G- r0 t5 U/ U; \  R 9(3S5)SQ}L0~2X)C}0[F9.png
4 k; Z/ y3 g5 ~- n2 P
5 V6 U" b, d2 _2 c7 ]由图 14~19 可知,当设备选择了某个模型后,其控制指令集和异步通知消息也就得符合此模式下的对应集合,否则则不符合标准。这里我们主要是使用到 ACM 模式,因此,此 ACM 模式下的有 Host 端发现 Device 端的控制指令和有 Device 端向Host 端发送的异步通知消息都是固定的那么几条指令或消息,但并不是说,只要是 ACM 模式,那么就表示此模式下的所有控制指令和异步通知消息都必须支持。控制指令在设备端的控制接口描述符中的 ACM 功能描述符中的 bCapabilities 字段有按位定义 ACM 模式下的控制指令的支持情况,而异步通知消息,则完全看 device 端的应用情况是否需要,并没有在任何描述符中指出那些消息是否支持。
# {; P$ D' B9 Z$ g8 A( I在 ST 给出的 CDC 例程中,主要是使用到了 SetLineCoding 指令来设置和修改虚拟串口的波特率,使用 GetLineCoding 来获取当前波特率,使用 SetControlLineState 来打开或关闭串口,这种操作是在 Host 端 CDC 驱动来具体映射实现的,至于Device 端收到这些个控制指令该怎么处理,就是另外一回事了,Device 端也可以完全不做任何处理,有 CubeMx 自动生成的CDC 类代码就是这样,对接收到的任何控制指令到没有做任何处理,当然,如果需要的话,则按应用的需要来处理,这个完全取决于用户。, u8 U* h. C% R1 f
* I% ?% {- y! M0 _9 i- J
`U8S3(G1V{2SK`7`QCOEZ[4.png : S# \3 ]9 |9 K: x

6 B* e$ J& O7 L( q& @# f _01P[@]IN`5F}XA40%`PL~R.png % G& w0 e# o/ q/ ]( E6 F! ^' V
- G) ^; s0 `  r
3 CDC 类软件框架介绍0 U" E3 I" y( p! \" V
3.1 CDC 软件框架简介

2 }( q7 q6 f: u$ Q) j8 ~$ f6 ?4 H" U: f. i2 y2 q0 e+ p- y

. V' ~% ~: \% u, G  ^0 @ 0ROP0AWF08NW}Y5UQ_(X{FI.png
4 H; }) y4 v4 u" `6 t6 ~6 y4 W- s5 H+ z, {1 j7 {
如上图所示,黄色 USB Device Core 部分为 USB 设备库文件,属于中间件,它为 USB 协议栈的核心源文件,一般不需要修改:
, {( p$ A+ g, b8 B USB Device Core 中,Log/debug 为打印/调试开关;- g1 k; M! D/ H! X* j! w) i
 core 为 USB 设备核心;
( q3 n4 v$ v, P% p8 b2 o USB request 中定义了枚举过程中各种标准请求的处理;
& w1 O4 ~) Q7 E0 v3 z1 k6 t7 s) [5 j I/O request 为底层针对 USB 通信接口的封装。$ n# s/ q$ G' y; n* c$ E
黄色 USB Device Class 部分为 USB 类文件,也属于中间件,USB 设备库,目前 ST DEMO 中支持的类有 HID, Customer HID,CDC, MSC, DFU, Audio, ST 提供了这些类的源码框架,其他的 Class 或者是复合设备需要自己根据实际需求情况进行扩展或定制。如果用户需求只是需要一个标准类,比如 CDC 通信,那么最好就使用现成的代码,不需要做任何修改就可以实现这个CDC 类通信的功能。
" v$ t) x; ^5 x蓝色 USB Device HAL Driver 为 HAL 库部分,是对 USB 外设接口的封装,属于底层驱动,不需要修改,它分为 PCD 和 LLDriver,PCD 处于 LL Driver 之上。( W7 V7 D8 b7 y2 _* l, {) D/ J
洋红色 USB Device Configuration 为 USB 配置封装,位于 USB 底层 HAL 层驱动与中间件 USB 协议栈之间,一方面向上层(USB 设备库)提供各种操作调用接口,另一方面,向底层 USB 驱动提供各种回调接口。正是由于它的存在,使得 USB 协议栈(USB 设备库)与底层硬件完全分离,从而使 USB 设备库具有更加兼容所有 STM32 的通用性。USB Device Configuration为开放给用户的源文件,用户可以根据自己的某些特殊需要进行修改,也可以使用默认的源文件,假如没有任何特殊要求的话,我们使用默认即可。9 b' f! s% L1 m" y' O/ Z
Application 为应用层,USB Device Class 有可能将自己对应该的操作接口封装在一个操作数据结构中,由应用来具体实现这些操作,在系统初始化时,由应用将已经定义好的操作接口注册到对应的 USB 类中,比如 usbd_cdc_if, 就这样,使得应用层的应用代码与属于中间件层的 USB 协议栈分离。同时,USB 协议栈会将一些字符串描述符放到 APP 中,当 USB 初始化时将这些已经定义好的字符串通过指针初始化到 USB 协议栈中,以便后续需要时获取。
( X: [. h) t# _' X$ j# h1 X
0 Y* T( L/ A: y4 P' B3.2 工程源码文件与软件框架的对应关系
) u  t% c- \/ ]1 F, c7 l7 O% O- c6 c& K- h2 A9 ]1 M6 b
`WN8F`63IZMMAU8OLQ3%[%2.png
1 M0 P/ i, [& a0 A' e+ N2 P
. U2 j7 \: `  ]; ~1 j3.3 USBD 内核与 USBD_CDC 的关系
7 Y9 G& b4 {6 e1 x& g1 t7 N) L3.1 节中,我们已经提到过 ST 官方 Cube 库中提供的官方 USB 协议栈,主要是包含了 USBD 内核与 USB 各种类。USBD 内核一般是固定的,用户一般不需要修改,但 USBD 类,如果用户需要修改或者扩展,比如复合设备或者用户自定义设备,还有就是,ST 目前官方提供的 USB 设备类的 DEMO 程序并没有囊括所有 USB 类,因此,若用户需要实现这些官方提供DEMO 之外的 USB 类时,则用户需要根据自己的需要来定制化自己的 USB 类,那么又该如何开始呢?
4 D& g7 N% ]" M$ a: q1 B7 g我们已经知道,ST 提供的 USB 协议栈中已经有 USBD 内核,且这个内核源文件一般是不需要修改的,那么这里我们需要自定义这么一个 USB 类,那么我们首先得知道,这个我们需要自定义的 USB 类是如何与 USBD 内核打交道的?: D+ I% [; P4 P0 v
USB 协议栈将所有 USB 类都抽象成一个数据结构:USBD_ClassTypeDef,其定义如下所示:
) L3 F! W5 {7 `2 X" G! h
  1. typedef struct _Device_cb: k# w4 n3 Q5 i7 e/ f$ a, ~
  2. {6 |* m& y  T/ W6 q
  3. uint8_t (*Init) (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
    & }% r& W$ ^( y, v
  4. uint8_t (*DeInit) (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
    " t' b# d: E; G/ s
  5. /* Control Endpoints*/6 E# a' s- M& L; p% K5 i
  6. uint8_t (*Setup) (struct _USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef
    , O" l& U2 h3 E' L4 V5 X$ U
  7. *req);; j6 Q9 s! g: S8 }0 \
  8. uint8_t (*EP0_TxSent) (struct _USBD_HandleTypeDef *pdev );
    ) n6 }! ~7 r) w4 N
  9. uint8_t (*EP0_RxReady) (struct _USBD_HandleTypeDef *pdev );
    / L$ E& y  S0 O* w7 E  F9 r% L  N) t
  10. /* Class Specific Endpoints*/
    3 ?) i( T. J* D& A% d9 N
  11. uint8_t (*DataIn) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
      i* i3 I& x5 z3 K0 A
  12. uint8_t (*DataOut) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);* h4 M6 i' T7 X
  13. uint8_t (*SOF) (struct _USBD_HandleTypeDef *pdev);! J5 x  g" v) b+ W9 x
  14. uint8_t (*IsoINIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
      G7 Z: p# X) O
  15. uint8_t (*IsoOUTIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
      _* j. p8 C# Y: Q
  16. uint8_t *(*GetHSConfigDescriptor)(uint16_t *length);
    7 V# @& {5 O! p: L9 m3 i( B
  17. uint8_t *(*GetFSConfigDescriptor)(uint16_t *length);  d9 P& a6 ~7 S; {( T
  18. uint8_t *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
    ' l: X/ b1 b% R2 G; r3 q
  19. uint8_t *(*GetDeviceQualifierDescriptor)(uint16_t *length);
    ) v6 o( ]" y- q! l+ o
  20. #if (USBD_SUPPORT_USER_STRING == 1)
    7 `# ]8 R8 n" z" B; u% j
  21. uint8_t *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev ,uint8_t index,; _0 m, b' B) ~
  22. uint16_t *length);
    . g; l4 C( j, r2 `. t
  23. #endif
    1 ?/ z8 h& O$ g7 x. W  \4 J2 _
  24. ) j' t* H& @; J7 M5 U& W; F2 o
  25. } USBD_ClassTypeDef;
复制代码

# s6 Z7 ]+ W: }4 n这个结构体是一个抽象类,定义了一些虚拟函数,比如初始化,反初始化,类请求指令处理函数,端点 0 发送完成,端点 0接收处理,数据发送完成,数据接收处理,SOF 中断处理,同步传输发送未完成,同步传输接收未完成处理等等;用户在实现自己具体的 USB 类的时候需要将它实例化,USBD_ClassTypeDef 结构体是 USBD 内核提供给外部定义一个 USB 设备类的窗口,而 USB 类文件(如 usbd_cdc.c)实际就是实现这个结构体具体实例化的过程。最后将这个具体实例化的对象注册到USBD 内核的同时, USBD 内核与 USBD 类也进行了关联。
4 W- l! x0 ?) v7 H- Z  j; t2 P9 U7 U4 j6 e1 [& N/ h
4(}YI%T@QS)TXM)(~`[WL.png / w$ o( z! S1 S, S6 z+ U+ Y
: \" Z, E3 o& [
可以这么说,USBD 内核与 USBD 类之间的纽带就是 USBD_ClassType 这个结构体。0 D; \) b2 C6 q
下面我们来看看 ST 提供的 CDC DEMO 中具体 CDC 类:' v: d+ Y# S1 f2 _( u: c8 g, t
  1. USBD_ClassTypeDef USBD_CDC =
    $ t# }) s. j3 S& K- x0 Q
  2. {4 ^/ s4 z; f1 m
  3. USBD_CDC_Init, //初始化  K; r+ J4 Q+ ?0 H- t
  4. USBD_CDC_DeInit, //反初始化
    1 E2 ]% O2 Q: [3 b3 b  r- w- C2 i# I
  5. USBD_CDC_Setup, //CDC 类请求指令处理,也就是指 CDC 类控制指令的处理% s3 Z% _8 w# I$ p
  6. NULL, /* EP0_TxSent, */ //端点 0 发送完成,不需要处理5 o5 ^1 @' }$ A# @% C& J
  7. USBD_CDC_EP0_RxReady, //端点 0 接收处理,实际上当做 CDC 类控制指令来处理
    ' t0 g- l- B6 o9 t: ?$ }
  8. USBD_CDC_DataIn, //CDC 发送数据完成处理+ u% D* o# Q( s6 ?# w, e
  9. USBD_CDC_DataOut, //CDC 类接收数据处理
    4 a) a% @2 H( b# C, ~. Q* m/ P; ~  Q* Y
  10. NULL, //SOF 中断不做处理
    ( U! ?: a( v) {& v! f' J
  11. NULL, //同步传输发送未完成中断不做处理
    : {1 h4 U" k  P2 d9 M% W
  12. NULL, //同步传输接收未完成中断也不做处理
    2 E& y3 o( t$ m6 }& i& `8 \% F& a
  13. USBD_CDC_GetHSCfgDesc, //获取高速 USB 配置描述符
    " D9 d1 `' Q( T6 Y; w
  14. USBD_CDC_GetFSCfgDesc, //获取全速设备配置描述符
    # |# _1 d" H) G% h
  15. USBD_CDC_GetOtherSpeedCfgDesc,/ Z9 o* `5 E  \  j" B
  16. USBD_CDC_GetDeviceQualifierDescriptor,
    & o6 d2 T! ~' v* g6 C" x
  17. };
复制代码
0 {4 R5 \6 q3 p" S# T
这个就是具体一个 CDC 类实例化的对象,上层应用通过 USBD_RegisterClass 函数,将此对象注册到 usbd 内核 :
: p7 P0 t6 @0 p+ g
  1. USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
复制代码
1 w+ ]4 a8 U0 A; ]8 }" B
它主要在 usbd_cdc.c 源文件中实现它的各个成员函数,当然,usbd_cdc.c 源文件中,除了这些 CDC 类成员函数的具体实现之外,还包含其他一些对上层提供的接口,比如发送 USBD_CDC_TransmitPacket, USBD_CDC_RegisterInterface,上层应用通过调用 USBD_CDC_TransmitPacket 来发送数据,通过 USBD_CDC_RegisterInterface 来注册操作接口,这也是我们接下来将要讲述的内容。! y! v) y: J9 g) S% G: s, I3 ?
3 W1 s, e, y: Q- Z
3.4 USBD_CDC 与 USBD_CDC_If 的关系( _! J7 \* ~" i* c" x1 N
讲完了 USBD 内核与 USBD_CDC 的关系,接下来我们来讲下 USB_CDC 与上层应用是如何对接的。为了将 USBD_CDC 与上层应用层完全分离出来,类似 USBD 内核与 USBD_CDC 类完全分离一般,USBD_CDC 类对上层同样提供一个抽象的数据操作接口 USBD_CDC_If 结构体:. _4 t. y% z! D% H
  1. typedef struct _USBD_CDC_Itf6 E# J/ j- ~- @, U! U
  2. {) T& l2 N0 W: t2 B
  3. int8_t (* Init) (void);
    & x6 d5 ^5 ~$ i/ C0 H  a
  4. int8_t (* DeInit) (void);
    1 G1 Y- g. u+ i( G! s
  5. int8_t (* Control) (uint8_t, uint8_t * , uint16_t);//处理收到的来自 Host 端的控制指令4 P/ E/ `% |5 l6 ^! x
  6. int8_t (* Receive) (uint8_t *, uint32_t *); //处理收到的数据
    ; |) |- N. y" w" F
  7. }USBD_CDC_ItfTypeDef;
复制代码
* l* d& m# U: Z/ {9 m+ \
如上所示,如何处理来自 Host 端发送过来的控制指令和数据,完全是由应用层来决定,具体实现是应用层将此抽象的操作接口具体实例化,并注册到 USBD_CDC 类对象中:
: M$ y8 e+ U1 f) |" p P$PVPNFBRQ32(9@{O{RH9O0.png
( @# ?) \' o$ i, y5 U$ K2 q( H" {+ |) j8 Q7 {& @
如上图所示,通过引入 USBD_CDC_If 这么一个数据结构,就实现了 USBD_CDC 类与应用层的完全分离。USBD_CDC_If 的具体实例化对象如下:
9 O/ J5 r- D, L: e0 P) m
  1. USBD_CDC_ItfTypeDef USBD_Interface_fops_FS =
    ' ]5 c! V7 b5 J( f: \  C
  2. {
    2 Z' l- b. y0 }. d, Q
  3. CDC_Init_FS,5 f' `; u* r: B7 Z, G5 |
  4. CDC_DeInit_FS,5 l$ Q- f0 Y0 Y2 |$ Q: \9 R9 F
  5. CDC_Control_FS,6 z; g  G* y; C, P7 U
  6. CDC_Receive_FS8 B5 a  V% h+ N- V! |5 `: C8 Q' H
  7. };
复制代码

9 Y- {" q8 j3 _$ i! b& s7 w6 ~; m7 A源文件 usbd_cdc_if.c 就是实现这些成员函数的过程,除此之外,还包含发送接口。最后应用层通过调用USBD_CDC_RegisterInterface 函数将此操作接口注册到 USBD_CDC 类中 :
" Z/ x8 e3 x, F" ~% D
  1. USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
复制代码

$ y; ^4 k* \5 b  k3.5 应用接口* f  s! k9 y/ j0 s
 初始化 :
( D" C7 y& D8 f
  1. void MX_USB_DEVICE_Init(void)
    * U$ ?, `7 p, y" z3 N" R! T$ t
  2. {
    ( L4 J, B' z4 {+ k
  3. /* Init Device Library,Add Supported Class and Start the library*/8 B0 s" \7 j$ e, }
  4. USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
    3 H2 m0 i3 x+ d/ p) c1 F
  5. USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); //注册 USBD_CDC 类到内核" [; L' q, [, X  V9 a( e" L+ {0 b
  6. USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);//注册 USBD_CDC_If 到
    " a9 T/ ^$ Q5 `9 i
  7. CDC 类. |, `9 t- R6 B1 p: d9 W, K. g, m
  8. USBD_Start(&hUsbDeviceFS);" \, Q5 x# }: X8 o, f7 o' D/ s
  9. }
复制代码
* v+ D8 k  U; E3 a
如上图所示 :( a4 B* z& n0 v
初始化分 4 步:2 f2 S- T2 s" R/ E" `
1> 初始化 USBD 内核
0 L" s- u& O7 L& ~& p2> 给 USBD 内核注册 USBD_CDC 类9 {7 i) S* b; V: s1 L
3> 给 USBD_CDC 类注册 USBD_CDC_If 接口8 e! o1 B6 x/ ]% J% u
4> 正式启动 USBD
5 o. n# z7 z  V' r 发送数据:
+ ]5 F. G; Q4 {- F2 N2 zuint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len);( c' b' A  J. ?1 {% N
 接收回调处理:
8 d2 f. a; U, Y! J, T( Nstatic int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len);7 E; R# n1 n2 s. n3 F
 接收控制指令处理 :6 n+ f+ s4 f% X1 t2 a* K8 o
static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length);
% F; ~& S. X8 e# K. [
  X' u- D8 l0 {" _+ n" Y
* B* i5 {2 e$ h4 实践动手部分
+ v$ z5 I8 z; B" F4 j本实验的目的是让用户学会自己动手创建一个 STM32CubeMx 工程,一步一步实现通过使用 USB CDC 类与 PC 端进行串口通信。9 M# }, a8 O" D, _3 f
8 o( j, p( {0 q; p! O2 U
4.1 实验环境及 STM32F072-Discovery 板简介
3 i, B, I1 r+ V- t% j硬件准备:( l: f+ }& k0 C
 STM32F072 Discovery 板一块
0 E3 V) \9 C% }% B9 W Mini USB 线两根" A4 Y7 h8 D- x, e, D- c, P* k/ c5 @( e. X
 PC 一台% S/ n$ N* g3 T
软件准备:: Q% A  ?; Q$ o0 o" H
 IAR V6.7.0 或者以上版本
/ s" g! c7 ~/ Q# T3 I STM32CubeF0 V1.7.0
$ ~" j% M1 R) U7 ^5 w+ y: Y STM32CubeMX V4.19
. ?1 A& }0 b4 q SSCOM 串口工具9 \9 F7 B  W( ]# {! E* B
 VCP 虚拟串口驱动6 k+ d; _/ |& _' L1 _
( U' j8 N" W/ p, n# z
ZU`8_}3%P`Y_KQTVBESX3NC.png
$ s  e' v  N; K0 n0 s
; M% z  ~" ~; j& I5 x" k  w OA2TAK)OR2H{XGI7][_]SR2.png
) L6 f3 a( j! h3 T/ B
1 y0 D4 }4 T* O  H  M' W4.2 使用 STM32CubeMx 制作 CDC 工程
0 S' ]" W+ J9 W- O  S5 S8 c
5 p; v5 g: }6 h, y# `! W$ h OK2RFXG2E@_NG1HI`FRXBRX.png
7 |; M" o0 w' {! U4 r7 Y: }- T
UFDC4QUET]{WCUO@Q@[@2Z2.png
; l) \2 G& T( G; d+ a/ {. B# _& a# v% y# F
使用内部 48M 的 HSI48 RC 作为时钟源。# m, \) k* B2 `6 P

; h1 r! E9 Y9 ^0 P  x KST~4@3XA_5G(N{@XVUPL_7.png
  @; f( O1 K  L! M+ [1 [% P& u7 T. G3 k, t, u# R" G
最终生成的代码工程与 USB CDC 类软件框架的对应关系:- u" D4 v1 p- h! ~& O/ e: D
- `4 e$ Y1 w5 Y. [
`9GQ~`4{T6C@}IAT_I7%CMW.png
5 i% P# X+ Z8 e% i* j
/ T7 V" A0 f! K# X8 [) R" V" B4.3 添加测试代码6 W+ L8 h+ {: o! N) J/ }5 P/ c
为了更好的验证通信,我们需要添加点测试代码:1 \2 O% ]9 o) n( O
  1. static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)$ a8 \) J$ ?, v: i% W# }1 L0 b
  2. {
    " N# v! U7 [; ]8 L
  3. /* USER CODE BEGIN 6 */
    2 @, w- F# L# o* A9 @7 p
  4. HandleReceiveData(Buf,*Len);7 B9 g7 w& y0 {" I
  5. USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);* X/ P: l: o1 `3 H3 q; i
  6. USBD_CDC_ReceivePacket(&hUsbDeviceFS);& e9 d1 @9 ?9 R& i7 m
  7. return (USBD_OK);
    + N3 V% A9 E- M$ r& F' A9 S9 d
  8. /* USER CODE END 6 */! _( s& |0 @$ `  S: b3 z6 d. y
  9. }
复制代码

6 Q. Y  p+ }5 \3 c) R在接收回调中,我们将接收到的数据转给 HandleReceiveData 函数处理:
) V: t. x  F5 T' ^: ~8 t% T: s
  1. void HandleReceiveData(uint8_t *pData, uint32_t len)+ V% \9 b' x; {' _2 k+ w' ^" N
  2. {
    3 c" n9 V, {$ A" I! ~
  3. CDC_Transmit_FS(pData,len);
    * d1 [7 o! T. b$ l; ?* o: ]
  4. }
复制代码

0 k( d( Q" [' R/ L1 R. S- C而在 HandleReceiveData 函数中我们将收到的数据原样返回给 Host 端,这样一来,Host 端的串口工具将发送什么就将收到什么。: j0 t# W1 U2 _, z$ W6 ^
  1. uint8_t SendData[256];
    % z5 U. t7 V. p$ p
  2. static uint8_t StartFlag =0;+ \9 f9 h5 z+ a# z4 @. L- |. z0 @
  3. void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    5 M* ^" k" ^4 ?
  4. {
    ) n7 Q0 y) _4 _  Y- p% @2 Y4 e
  5. //CDC_Transmit_FS(SendData,64);* l8 k2 t' f/ W9 s
  6. if(StartFlag ==0)0 P( ^# s- o3 F+ R3 t1 N
  7. {
    6 [8 _$ o; V& W
  8. StartFlag =1;. J1 s5 n* F, }
  9. }
    " i9 e' Y7 }" K; I
  10. else  X( q% \* Y) p# Z' c
  11. {
    / C- v: b- S. ]
  12. StartFlag =0;6 B, o6 k3 C& i3 b. o1 K
  13. }
    " {! u. J' _) ?) {- O+ u! h% {; U8 T
  14. }
复制代码

) ]8 e0 }1 I! Y6 R" ]- `另一方面,我们定义了一全局变量 StartFlag,它用来标志是否循环从 Device 端向 Host 端主动发送数据,其值由外部按键控制。然后在 Main 函数内的 while(1)循环内添加如下测试代码:& V9 [; l, |0 @/ i. U: V
  1. /* USER CODE BEGIN WHILE */
    ) a% ^( x/ S' Y" }) p7 u0 h& U
  2. while (1)  N+ t$ w" Z- v" @
  3. {3 A! A6 r- ]8 C
  4. /* USER CODE END WHILE */
    0 a6 o) `% n" c) ~
  5. /* USER CODE BEGIN 3 */
      l) o" h# ~- z9 [3 \' U
  6. if(StartFlag ==1)' x1 [7 x* O6 d  I" U
  7. {/ T" A3 \. P4 V# m
  8. if(hUsbDeviceFS.dev_state ==USBD_STATE_CONFIGURED)
    ) f+ m3 g9 Z' a. z
  9. {
    5 S- R5 L" U& |2 W; D* K& s/ K
  10. CDC_Transmit_FS(SendData,60);
    5 g; o2 o7 g6 T8 U% w& z7 H
  11. }
    8 L7 d0 Q4 M) U
  12. }9 s4 A' Z. i+ o* L/ d5 ^
  13. }
    : D: O8 o& L  e/ [, d) \% C0 ~
  14. /* USER CODE END 3 */
复制代码
+ t& _- L( U8 h( g
只要 StartFlag 标志为 1,在枚举结束后则不断向 Host 端发送数据。
2 H$ K, g# ]. i+ G$ x- Z( R/ |% Q6 M" h3 s( }
4.4 验证结果; Y4 ^1 a$ S! j0 n# ~" J& ?
在编译完并将代码烧录进 MCU 后,我们首先验证 PC 端通过串口工具发送数据的情况:
$ v; N: B0 @7 k+ c8 h
( ]* e4 i( b/ Y" w, h+ i* c9 J0 X: B! M 852$)9C[OPG}`0NNN4WF]$P.png . g$ p& b% O/ ?( R! b# A$ \( a: `

. W+ W: G2 a" m! S8 T  V3 E: x如上图所示,串口工具发送 63 个字节到 Device 端后,能够接收到从 Device 端返回到的一模一样的数据,这说明发送与接收都是正常的。2 u$ ^/ X6 j( b) ^
# F0 i1 v' f% j
D1A~2Y0B]]I4EK(]`Z$TZUE.png
5 g% L$ f, N! C3 A
, ^: [. h& ~2 X; O在按下用户按键后,串口工具能够无限收到来自 Device 端的数据。0 T( z7 C! l3 v+ c: X" ^5 Y
2 I- g! P1 j: c% k
[XBEEB]A6(5]T{%PWQE4`PU.png
0 N$ V0 O9 T: |# k* e& J
* f: v  g  r6 j收发同时进行也是正常的。至此,USB CDC 设备端的收发验证均未正常。! V+ o5 y5 m- m, V* R$ L0 N" Q4 L
2 E- O8 O, s; ^# [" Q

& K6 N2 a, g: _0 x# T3 L4.5 注意事项! z  k1 U0 ?4 B9 o
 STM32CubeMx 默认生成的工程在发送 64 整数倍数据的时候 PC 端收不到,这个问题已经在之前的文章中有所描述,这里不再重复。4 ]8 Z, o- M. G7 i8 C8 p  F
 CDC device 端若无限向 PC 端发送数据,若 PC 端没有及时读走数据,导致 PC 端接收缓存爆满,此时 PC 端回复NACK,此时会导致发送返回 BUSY.
7 x+ |/ w( n* [
4 h: n& N6 b8 O* G6 u0 `; K8 U& N* f/ A/ v5 B, B8 \  m
收藏 2 评论0 发布时间:2022-3-3 22:19

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版