STM32 之 USB 虚拟串口

        在现代个人电脑的USB是几乎所有外设的标准通信端口。然而许多工业应用软件仍然使用经典的串口(UART)。USB虚拟串口提供了绕过这个问题的一个简单的解决方案。

        为了让USB被视为一个COM端口,USB设备必须根据通信设备类(CDC)规范来实现两个接口:

1.抽象控制模型通信,在端点中有1个中断:在我们的实现中,这个接口在描述符中声明,但是相关的端点(端点2)不被使用。

2.抽象控制模型数据,具有1个bulk in端点和一个bulk out 端点,这个接口在实际中由端点1(in)和端点3(out)表示。端点1用于通过USB将从UART接收到的数据发送到PC。端点3用于接收来自PC的数据并通过UART发送。

为了实现虚拟COM端口,该设备支持以下类特定请求:

● SET_CONTROL_LINE_STATE:

RS-232信号用于告诉设备数据终端设备现在是存在的.该请求总是在Virtual_Com_Port_NoData_Setup() 函数中返回一个USB_SUCCESS(usb_prop.c中 )。

● SET_COMM_FEATURE:

控制特定通信功能的设置。这个请求总是在Virtual_Com_Port_NoData_Setup() 函数中返回一个USB_SUCCESS(usb_prop.c中 )。

● SET_LINE_CODING:

发送设备的配置。它包括波特率、停止位、奇偶校验和字符位数。所接收的数据存储在一个特定的数据结构中,称为“linecoding”,用于更新UART参数。

● GET_LINE_CODING:

此命令要求获取设备当前波特率、停止位、奇偶校验和字符位数。该设备用存储在“linecoding”结构中的数据做出响应。

 

硬件配置接口

在虚拟COM端口中的硬件配置接口(hw _ config.c)管理以下程序:

●配置的系统和外设(USB和USART)时钟和中断

●USART默认值初始化

●用通过SET_LINE_CODING命令收到的参数配置串口

●把收到的数据发送到PC,通过USB串口通讯

●发送收到的数据通过USB串口通讯

 

1、USB标准规范类型

    USB规范大致可分为以下三类:

1)、标准规范

    标准规范为最基础的规范,主要有USB1.0、USB1.1、USB2.0和USB3.0等等。

2)、USB设备类规范

    设备类规范主要是针对于具体的USB设备而推出的类规范,有Mass Storage、Audio Device、Video Device等等设备相关规范。

3)、USB HOST控制器规范

    主要有OHC和UHC等

1、标准规范

     USB是通用串行总线的英文缩写,是Intel公司开发的总线架构,使得在计算机上添加串行设备非常容易。只须将设备插入计算机的USB端口中,系统会自动识别和配置。根据时代发展,USB接口标准经历了一代USB、第二代USB 2.0和第三代USB 3.0。

    USB规格第一次是于1995年,由Intel、IBM、Compaq、Microsoft、NEC、Digital、North Telecom等七家公司组成的USBIF(USB Implement Forum)共同提出,USBIF于1996年1月正式提出USB1.0规格,频宽为1.5Mbps.不过因为当时支持USB的周边装置少的可怜,所以主机板商不太把USB Port直接设计在主机板上。

    USB2.0技术规范是有由Compaq、Hewlett Packard、Intel、Lucent、Microsoft、NEC、Philips共同制定、发布的,规范把外设数据传输速度提高到了480Mbps,是USB 1.1设备的40倍!2000年制定的USB 2.0标准是真正的USB 2.0,被称为USB 2.0的高速(High-speed)版本,理论传输速度为480 Mbps.

    USB 3.0是最新的USB规范,该规范由英特尔等公司发起,USB3.0的最大传输带宽高达5.0Gbps(640MB/s),USB3.0 引入全双工数据传输。5根线路中2根用来发送数据,另2根用来接收数据,还有1根是地线。也就是说,USB 3.0可以同步全速地进行读写操作。

USB版本

最大传输速率

速率称号

最大输出电流

推出时间

USB1.0

1.5Mbps(192KB/s)

低速(Low-Speed)

5V/500mA

1996年1月

USB1.1

12Mbps(1.5MB/s)

全速(Full-Speed)

5V/500mA

1998年9月

USB2.0

480Mbps(60MB/s)

高速(High-Speed)

5V/500mA

2000年4月

USB3.0

5Gbps(500MB/s)

超高速(Super-Speed)

5V/900mA

2008年11月

USB 3.1

10Gbps(1280MB/s)

超高速+(Super-speed+)

20V/5A

2013年12月

这是从时间维度上来看的,但是每一代USB接口针对不同的设备又细分出来具体的型号,如USB Type A/B/C/Mini/Micro。下面是USB2.0和USB3.0标准下的各类接口示意图:

一、USB系统的结构

    USB系统是由三个逻辑层组成:功能层、USB设备层和USB总线接口层。并且每一层都是由主机和USB设备不同的功能模块组成,如下图所示:

1、功能层(接口)

    功能层是由客户软件和设备方的功能单元组成,其能够实现USB设备传输的特定功能。通过功能层可直观地理解USB传输的数据内容。其中,客户软件通过USB系统软件来与USB设备进行通信。功能单元对于客户软件,可视为接口的集合。

 

2、USB设备层 (端点)

    USB设备层是由USB系统软件和USB设备的USB逻辑设备组成,其实现主机和USB设备之间传输的具体配置。USB逻辑设备对于USB系统软件,可视为端点的集合。

 

3、USB总线接口层

    USB总线接口层是由主机的USB主控制器和设备的USB总线接口组成。其实现主机和USB设备实际的数据传输。

 

4、主机部分

    USB主机部分由客户软件、USB系统软件和USB总线接口组成。

4.1 客户软件

    客户软件负责和USB设备的功能单元进行通信,以实现特定的功能。客户软件不能直接与USB设备相连接,必须通过USB系统软件和USB总线接口才能实现连接。客户软件包括USB设备驱动程序和界面应用程序两部分。

 

4.2 USB系统软件

    USB系统软件负责和USB逻辑设备进行配置通信,并管理客户软件启动的数据。一般包括USB总线驱动程序、USB主控制驱动程序和非USB主机软件三个部分,这部分会由系统提供。

 

4.3 USB总线接

    USB总线接口包括主控制器和跟集线器两部分。其中,主控制器是负责完成主机和USB设备间的数据实际传输。根集线器是为USB系统连接起点。

 

5、设备部分

    USB设备部分由三个功能模块组成,分别是USB总线接口、USB逻辑设备和功能单元。

    功能单元看作是一个接口的集合;USB 逻辑设备被USB系统软件看作一个端点的集合;USB总线接口是USB设备中的串行接口引擎(SIE)。

 

    当客户程序通过USB管道发送或接收数据时,它首先调用Win32 APl,调用最终将使功能驱动程序收到一个IRP。而驱动程序的工作就是把客户的请求引导到有正确端点的管道上。它把请求提交到总线驱动程序,总线驱动程序再把请求分解成多个事务,然后这些事务被送往总线。总线上的信息流以每毫秒一帧数据的形式流动。总线驱动程序必须安排好多个事务以使它们能被装入同一帧中。

 

二、USB的拓扑结构

    USB是一种主从结构的系统。主机叫Host,从机叫做Device(也叫设备)。通常所说的主机具有一个或者多个USB主控制器(host  controller)和根集线器(root hub)。主控制器主要负责数据处理,而跟集线器则提供一个连接主控制器与设备之间的接口和通路。

    通常情况下,PC机上有多个主控器和多个USB口,一个主控器下有一根集线器,一根集线器通常具有一个或者几个USB接口。通常集线器可以通过USB集线器来扩展USB接口,只是带宽是不能够拓宽的,因为带宽是共享一个USB主控器的。

    了解上面的信息后,再来看USB的拓扑结构如下图:

USB的拓扑结构看起来是一个金字塔形的结构,具体构成如下:

    塔顶是USB的主控器和根集线器,下面接USB集线器,USB集线器将一个USB口扩展为多个USB口,多个USB口同样可以通过USB集线器扩展出更多的接口。不过USB口并不能通过USB集线器无止境的扩展,他是有限制的,例如在USB2.0规定它最多扩展6次。

    理论上一个USB主控器最多可接127个设备,这是因为协议规定每个USB设备具有一个7bit地址(取值范围为0~127,地址用于给主机识别是哪个设备,其中0地址值得注意,是给刚接入未初始化的设备使用的)。

 

三、USB的设备架构

    设备架构认为设备是由一些配置、接口和端点构成的。其中配置和接口是USB功能的抽象,实际的数据传输由端点来完成。其对应关系如下图所示:

1.设备

    设备代表USB设备,它由一个或多个配置组成。设备描述符用于说明设备的总体信息,并指出其所包含配置的个数。

 

2.配置 

    在使用USB设备前,必须为其选择一个合适的配置。如USB设备的低功耗模式和高功耗模式分别对应一个配置。配置描述符用于说明USB设备中各个配置的特性。

 

3.接口 

    一个配置可以包含一个或多个接口。接口是一个端点的集合。接口描述符用于说明USB设备中各个接口的特性。 

 

4.端点 

    端点是USB设备中的实际物理单元,USB数据传输就是在主机和USB设备各个端点之间进行的。

    

5、管道

    管道,是主机软件(数据缓冲区)和USB设备的各个端点之间的数据传输链接,它是两者之间通信流的抽象。然而,实际的数据传输是由USB总线接口层来完成的。管道和USB设备中的端点一一对应,并且各个管道的数据传输是相互独立的。

    管道有两种类型:流管道和消息管道。其中最为重要的消息管道是“缺省控制管道”,这个管道在设备开始上电后就存在了,它用于提供设备的配置与状态等信息。主机与设备之间的联络就是通过消息管道实现的。

 

四、USB枚举与通信的具体过程

1、USB接头

    下图是一个USB接头的结构图:

    由图可以看出,标准的USB链接线使用4芯电缆:5V电源线(Vbus)\差分数据线负(D-)、差分数据线正(D+)和地线(GND)。

    在hub端,数据线D+和D-都有一个阻值在14.25k到24.8k的下拉电阻Rpd,而在设备端,D+(全速,高速)和D-(低速)上有一个1.5k的上拉电阻Rpu。当设备插入到hub端口时,有上拉电阻的一根数据线被拉高到幅值的90%的电压(大致是3V)。hub检测到它的一根数据线是高电平,就认为是有设备插入,并能根据是D+还是D-被拉高来判断到底是什么设备(全速/低速)插入端口。

 

2、USB通信过程

    主机和USB设备可以相互传输数据,其具体过程如下(以主机箱设备传输为例):

step1:客户软件首先将要传输的数据放入缓冲区,同时向USB总线驱动程序发出IRPS,请求数据传输。(客户软件)step2:USB总线驱动接收到程序接收请求,并对数据进行处理,转化为具有USB格式的事务处理。(USB系统软件)

step3:USB主控制器驱动程序将这些事务处理建立成事务列表,同时要求不能超过USB带宽。(USB系统软件)

step4:USB主控制器读取到事务列表并将事务转化为信息包,发送到USB总线上。(USB总线接口)

step5:USB设备收到这些信息后,SIE(USB总线接口是USB设备中的串行接口引擎)将其解包后放入指定端点的接收缓冲区内,由芯片固件对其进行处理。

 

用框图表示如下:

3、USB枚举通信具体过程

     枚举就是从设备读取一些信息,知道设备是什么样的设备,如何进行通信,这样主机就可以根据这些信息来加载合适的驱动程序。调试USB设备,很重要的一点就是USB的枚举过程,只要枚举成功了,那么就已经成功大半了。

 

枚举通信过程具体如下:

step1:检测电压变化,报告主机

    首先,USB设备上电后,一直监测USB设备接口电平变化HUB检测到有电压变化,将利用自己的中断端点将信息反馈给主控制器有设备连接。

 

Step2:主机了解连接设备

    主机在知道有设备接入后会发送一个Get_Port_Status请求(request)给hub以了解此次状态改变的确切含义。

 

Step3:Hub检测所插入的设备是高速还是低速

    hub通过检测USB总线空闲(Idle)时差分线的高低电压来判断所连接设备的速度类型,当host发来Get_Port_Status请求时,hub就可以将此设备的速度类型信息回复给host。USB 2.0规范要求速度检测要先于复位(Reset)操作。

 

Step4:hub复位设备

    主机一旦得知新设备已连上以后,它至少等待100ms以使得插入操作的完成以及设备电源稳定工作。然后主机控制器就向hub发出一个 Set_Port_Feature请求让hub复位其管理的端口(刚才设备插上的端口)。hub通过驱动数据线到复位状态(D+和D-全为低电平 ),并持续至少10ms。当然,hub不会把这样的复位信号发送给其他已有设备连接的端口,所以其他连在该hub上的设备自然看不到复位信号,不受影响。

 

Step5: Host检测所连接的全速设备是否是支持高速模式

    因为根据USB 2.0协议,高速(High Speed)设备在初始时是默认全速(Full Speed )状态运行,所以对于一个支持USB 2.0的高速hub,当它发现它的端口连接的是一个全速设备时,会进行高速检测,看看目前这个设备是否还支持高速传输,如果是,那就切到高速信号模式,否则就一直在全速状态下工作。

    同样的,从设备的角度来看,如果是一个高速设备,在刚连接bub或上电时只能用全速信号模式运行(根据USB 2.0协议,高速设备必须向下兼容USB 1.1的全速模式)。随后hub会进行高速检测,之后这个设备才会切换到高速模式下工作。假如所连接的hub不支持USB 2.0,即不是高速hub,不能进行高速检测,设备将一直以全速工作。

 

Step6:Hub建立设备和主机之间的信息通道

    主机不停地向hub发送Get_Port_Status请求,以查询设备是否复位成功。Hub返回的报告信息中有专门的一位用来标志设备的复位状态。

       当hub撤销了复位信号,设备就处于默认/空闲状态(Default state),准备接收主机发来的请求。设备和主机之间的通信通过控制传输,默认地址0,端点号0进行。此时,设备能从总线上得到的最大电流是100mA。(所有的USB设备在总线复位后其地址都为0,这样主机就可以跟那些刚刚插入的设备通过地址0通信。)

 

Step7:主机发送Get_Descriptor请求获取默认管道的最大包长度

     默认管道(Default Pipe)在设备一端来看就是端点0。主机此时发送的请求是默认地址0,端点0,虽然所有未分配地址的设备都是通过地址0来获取主机发来的请求,但由于枚举过程不是多个设备并行处理,而是一次枚举一个设备的方式进行,所以不会发生多个设备同时响应主机发来的请求。

      设备描述符的第8字节代表设备端点0的最大包大小。虽然说设备所返回的设备描述符(Device Descriptor)长度只有18字节,但系统也不在乎,此时,描述符的长度信息对它来说是最重要的,其他的瞄一眼就过了。当完成第一次的控制传输后,也就是完成控制传输的状态阶段,系统会要求hub对设备进行再一次的复位操作(USB规范里面可没这要求)。再次复位的目的是使设备进入一个确定的状态。

 

Step8:主机给设备分配一个地址

     主机控制器通过Set_Address请求向设备分配一个唯一的地址。在完成这次传输之后,设备进入地址状态(Address state),之后就启用新地址继续与主机通信。这个地址对于设备来说是终生制的,设备在,地址在;设备消失(被拔出,复位,系统重启),地址被收回。同一个设备当再次被枚举后得到的地址不一定是上次那个了。

 

Step9:主机获取设备的信息

    主机发送 Get_Descriptor请求到新地址读取设备描述符,这次主机发送Get_Descriptor请求可算是诚心,它会认真解析设备描述符的内容。设备描述符内信息包括端点0的最大包长度,设备所支持的配置(Configuration)个数,设备类型,VID(Vendor ID,由USB-IF分配), PID(Product ID,由厂商自己定制)等信息。

    之后主机发送Get_Descriptor请求,读取配置描述符(Configuration Descriptor),字符串等,逐一了解设备更详细的信息。事实上,对于配置描述符的标准请求中,有时wLength一项会大于实际配置描述符的长度(9字节),比如255。这样的效果便是:主机发送了一个Get_Descriptor_Configuration 的请求,设备会把接口描述符,端点描述符等后续描述符一并回给主机,主机则根据描述符头部的标志判断送上来的具体是何种描述符。

      接下来,主机就会获取配置描述符。配置描述符总共为9字节。主机在获取到配置描述符后,根据里面的配置集合总长度,再获取配置集合。配置集合包括配置描述符,接口描述符,端点描符等等。

     如果有字符串描述符的话,还要获取字符串描述符。另外HID设备还有HID描述符等。

 

Step10: 主机给设备挂载驱动(复合设备除外)

    主机通过解析描述符后对设备有了足够的了解,会选择一个最合适的驱动给设备。  然后tell the world(announce_device)说明设备已经找到了,最后调用设备模型提供的接口device_add将设备添加到 usb 总线的设备列表里,然后 usb总线会遍历驱动列表里的每个驱动,调用自己的 match(usb_device_match) 函数看它们和你的设备或接口是否匹配,匹配的话调用device_bind_driver函数,现在就将控制权交到设备驱动了。   

     对于复合设备,通常应该是不同的接口(Interface)配置给不同的驱动,因此,需要等到当设备被配置并把接口使能后才可以把驱动挂载上去。

 

Step11:设备驱动选择一个配置

    驱动(注意,这里是驱动,之后的事情都是有驱动来接管负责与设备的通信)根据前面设备回复的信息,发送Set_Configuration请求来正式确定选择设备的哪个配置(Configuration)作为工作配置(对于大多数设备来说,一般只有一个配置被定义)。至此,设备处于配置状态(Configured),当然,设备也应该使能它的各个接口(Interface)。

    对于复合设备,主机会在这个时候根据设备接口信息,给它们挂载驱动。

一、端点概念。

    端点(Endpoint),是主机与设备之间通讯数据的接收或来源。主机与设备之间通信时最终会总用于设备上的各个端点,它是主机与设备间通信流的一个逻辑终端。一系列相互独立的端点在一起构成了USB逻辑设备,在系统结构中,位于下方红色方框内:

二、端点的分类

    每个USB设备都有一个唯一的设备地址,设备地址是设备连接上主机时由主机分配的,主机主要依靠这个设备地址对USB设备进行访问。但是在设备内部地址会被分的更细,设备会分出一些端点来,每个端点在设备都会有唯一的端点号,这个端点号是设计设备时给定的。如端点0,端点1等。一个设备最多可以包含16个端点,每个端点的地址为0-15。(网上也有说几十个的,有待考究)    

    其中每个端点地址对应一个方向。例如端点3-IN,端点3-OUT,这两个含义完全不同。但是需要注意其中的一个特殊端点--端点0,每个USB设备必须要有一个端点0,其作用为对设备枚举和对设备进行一些基本的控制功能,端点0也被称为控制端点。并且它与其他的端点还有一个不同之处在于端点0的数据传输方向是双向的,即端点0既可以给主机发送数据,也可以接收主机发送过来的数据,而其它端点均为单向。

    虽然有16个端点,但通常我们只用到3个,如下:

     1)、EP0:做传输配置和控制信息;

     2)、EP1:做数据输入IN_EP;

     3)、EP2:做数据输出OUT_EP。

 

注意:除了端点0,其余的端点在设备配置之前不能与主机通信,只有向主机报告这些端点的特性并被确认后才能被激活。

 

三、端点的特性

    一个端点的特性决定了它与客户软件进行传送的类型。一个端点具有以下一些特性:

   ·端点的总线访问频率要求

   ·端点的总线延迟要求

   ·端点的带宽要求

   ·端点的端点号

   ·对错误处理的要求

   ·端点能接收或发送的包的最大长度

   ·端点的传送类型

   ·端点与主机的数据传送方向

 

四、端点描述符

    USB设备中端点描述符描述了端点信息,端点描述符格式如下:

typedef struct _USB_ENDPOINT_DESCRIPTOR_

{

    BYTE        bLength,

    BYTE        bDescriptorType,

    BYTE        bEndpointAddress,

    BYTE        bmAttributes,

    WORD      wMaxPacketSize,

    BYTE        bInterval

}USB_ENDPOINT_DESCRIPTOR;

各变量具体释义如下:

bLength : 描述符大小.固定为0x07.

bDescriptorType : 接口描述符类型.固定为0x05.

bEndpointType : USB设备的端点地址.Bit7,方向,对于控制端点可以忽略,1/0:IN/OUT.Bit6-4,保留.BIt3-0:端点号.

bmAttributes : 端点属性.Bit7-2,保留.BIt1-0:00控制,01同步,02批量,03中断.

wMaxPacketSize : 本端点接收或发送的最大信息包大小.

bInterval : 轮训数据传送端点的时间间隔.对于批量传送和控制传送的端点忽略.对于同步传送的端点,必须为1,对于中断传送的端点,范围为1-255。

 

五、端点与管道

1、管道的概念

    管道是主机软件(数据缓存区),和USB设备各各端点之间的数据传输连接,他是两者之间通信流的抽象(实际上数据传输是USB总线接口完成)。管道与USB设备中的端点逐个对应,并且各个管道的数据传输是相互独立的。

 

2、管道的格式分类

    管带的通信格式分为两种,一种为流,另一种为消息,这两种通信格式不同且互斥。

1)、“流”指不具有USB定义格式的数据流,流通道中的数据是流的形式,也就是该数据内容不具有USB要求的结构。数据从流通道一端流进的顺序与它们从流通道另一端流出时的顺序是一样的(先进先出),并且流通道中的通信流总是单向的。

2)、“消息”指具有某种USB定义格式的数据流。消息通道与端点的关系同流通道与端点的关系是不同的。首先,主机向USB设备发出一个请求;接着,就是数据的传送;最后,是一个状态阶段(这部分即一次命令请求的过程)。为了能够容纳请求/数据/状态的变化,消息通道要求数据有一个格式,此格式保证了命令能够被可靠地传送和确认。消息通道允许双方向的信息流。

 

六、端点的传输类型

    一个具体的端点只能属于四个传输模式下中的一种。数据传输类型分为四种分别是:控制传输、批量传输、同步传输和中断传输。一般情况下,通常把工作在什么模式下的端点就叫什么端点,例如:控制端点、批量端点、同步端点和中断端点。

    端点0,是设备的默认控制端点,在设备上电后就存在并可以使用,在Set Config之前所有的传输都是通过端点0传输的。

USB控制传输分为以下四种:

  • 批量传输:批量传输一般用于批量的和非实时的数据传输,通俗的来说就是用于数据量大但对时间要求又不高的场合的一种传输方式,类似用于USB打印机和USB扫描仪等等。
  • 中断传输:中断传输一般用于小批量的和非连续的数据传输,通俗的来说就是用于数据量小的数据不连续的但实时性高的场合的一种传输方式,类似用于USB鼠标和USB键盘等等。
  • 等时传输:等时传输也有“同步传输”的叫法,一般用于要求数据连续、实时且数据量大的场合,其对传输延时十分敏感,类似用于USB摄像设备,USB语音设备等等。
  • 控制传输:控制传输是一种特殊的传输方式,且传输过程相对以上三种而言更复杂一些,但也十分重要。当USB设备初次连接主机时,用控制传输传送控制命令等对设备进行配置。同时设备接入主机时,需要通过控制传输去获取USB设备的描述符以及对设备进行识别,在设备的枚举过程中都是使用控制传输进行数据交换。

 

一、控制传输的结构

    一次完整控制传输可以分为三个阶段:初始设置阶段--->数据阶段(不必须)--->状态信息阶段。下面的

 

1、初始设置阶段

    初始设置阶段用于固定建立SETUP事务,标志一次控制传输的开始。初始设置阶段为一个SETUP事务,同样分为三个阶段如下:

令牌包阶段:

    主机会发送一个SETUP令牌包,如下:

相当于告诉设备,我要跟你进行通讯请你做好准备。

 

数据包阶段:

    发送DATA0数据包(注意SETUP只能使用DATA0包,8字节),让设备接收。例如发送获取设备描述符命令包:

相当于告诉设备,请将设备描述符的内容发给我。

 

握手包阶段:

    设备自动应答。

 

结合上面的过程可以用下图表示初始设置阶段。

 

2、数据阶段

    初始设置阶段中命令如果要求读/写数据,数据阶段就会在这一阶段来具体交换数据(如果没有数据交换要求则可省去该步骤,具体有SETUP事务标准请求命令决定)。在此需要做一些说明:传输控制在前言有说到控制传输的用途是获取设备信息与对设备进行配置。所以这些数据操作分为以下三类:

1)、控制读传输;

2)、控制写传输;

3)、无数据控制传输。

 

    一次控制传输必定为上面三种中的其中一种,所以数据阶段中的数据事务也是根据该规则来决定数据事务的。

    此处还应该注意的是数据阶段是由一到多个IN/OUT事务组成。这是由于有时候存在一个事务传不完的数据,所以可能存在多个连续IN/OUT事务的情况。这也就决定了,在同一次数据传输阶段中事务类型必定相同(IN/OUT事务)。

 

2.1、传输格式

    综上所述,所以从传输控制的不同类型来讲述数据阶段的格式会更好理解。

1)、控制读传输

    控制读传输时数据阶段在整个传输的格式如下图蓝框部分:

 

数据方向为:设备 —> 主机 (读取USB描述符)

    这里每个数据包是DATA0和DATA1交替出现的。需要注意的是当最后一个包刚好为允许的最大数据包大小时需要再传一个0长度的数据包,表示传输的结束。

 

控制读传输的数据过程IN事务的三个阶段如下:

令牌包阶段:

    主机会发送一个IN令牌包,触发设备产生IN包中断,如下:

数据包阶段:

    设备回复主机请求,回应数据。例如回复设备描述符命令请求:

握手包阶段:

    主机自动应答。

2)、控制写传输

    控制写传输时数据阶段在整个传输的格式如下下图蓝框部分:

数据方向为:主机 —> 设备(配置USB设备)

 

传输过程和规则基本与读取相似,不多做赘述。

 

 

3)、无数据控制

    控制传输不一定要传输很多数据,有些控制可能只是告诉设备要做一件事,这个命令包含在建立阶段的建立事务的8字节数据中即可,设备只需回复主机收到命令与否即可,所以就跳过数据阶段直接进入到状态阶段。无数据控制的格式如下图:

所以注意在无数据控制传输时,是无数据阶段的!

 

 

3、状态信息阶段

    状态信息阶段是要返回数据传输的成功与否,具体也需要看控制传输的类型。需要注意的是,状态信息的数据传输方向与数据阶段方向相反。例如,数据阶段为IN事务则状态信息阶段为OUT事务。

 

3.1、控制读传输

    在控制读传输时,该阶段则为OUT事务,其中的数据包固定为DATA1数据包。返回数据成功与否以有以下情况:

    1)、读数据成功                      主机发送OUT令牌包(ping令牌包,高速情况下),主机发送0长度数据包,设备ACK。

    2)、数据传输出错                   主机发送OUT令牌包(ping令牌包,高速情况下),主机发送0长度数据包,设备STALL。

    3)、设备忙(比如正在写数据)   主机发送OUT令牌包(ping令牌包,高速情况下),主机发送0长度数据包,设备NAK。 

 

控制读传输的状态信息阶段OUT事务的三个阶段如下(以ACK为例):

令牌包阶段:

    主机会发送一个OUT令牌包,如下:

数据包阶段:

    主机发送0字节数据包,作为状态正常信息回应:

握手包阶段:

    设备自动应答。

3.2、控制写传输

    在控制读传输时,该阶段则为IN事务,其中的数据包固定为DATA1数据包。返回数据成功与否以有以下情况:

    1)、写数据成功                      主机发送IN令牌包,主机发送0长度数据包,设备回复ACK。

    2)、数据传输出错                   主机发送IN令牌包,设备回复STALL。

    3)、设备忙(比如正在写数据)   主机发送IN令牌包,设备回复NAK。 

 

控制读传输的状态信息阶段IN事务过程与读类似。

 

3.3、无数据控制传输

    该阶段则为IN事务,其规则与控制写传输相似。

 

至此,一次控制传输完成,整个过程结束。

 

STM32F103 的 MCU 自带 USB 从控制器,符合 USB 规范的通信连接;PC 主机和微控制器
之间的数据传输是通过共享一专用的数据缓冲区来完成的,该数据缓冲区能被 USB 外设直接访
问。这块专用数据缓冲区的大小由所使用的端点数目和每个端点最大的数据分组大小所决定,
每个端点最大可使用 512 字节缓冲区(专用的 512 字节,和 CAN 共用),最多可用于 16 个单
向或 8 个双向端点。USB 模块同 PC 主机通信,根据 USB 规范实现令牌分组的检测,数据发送
/接收的处理,和握手分组的处理。整个传输的格式由硬件完成,其中包括 CRC 的生成和校验。
每个端点都有一个缓冲区描述块,描述该端点使用的缓冲区地址、大小和需要传输的字节
数。当 USB 模块识别出一个有效的功能/端点的令牌分组时,(如果需要传输数据并且端点已配
置)随之发生相关的数据传输。USB 模块通过一个内部的 16 位寄存器实现端口与专用缓冲区的
数据交换。在所有的数据传输完成后,如果需要,则根据传输的方向,发送或接收适当的握手
分组。在数据传输结束时,USB 模块将触发与端点相关的中断,通过读状态寄存器和/或者利用
不同的中断来处理。
USB 的中断映射单元:将可能产生中断的 USB 事件映射到三个不同的 NVIC 请求线上:
1、USB 低优先级中断(通道 20):可由所有 USB 事件触发(正确传输,USB 复位等)。固件
在处理中断前应当首先确定中断源。
2、USB 高优先级中断(通道 19):仅能由同步和双缓冲批量传输的正确传输事件触发,目
的是保证最大的传输速率。
3、USB 唤醒中断(通道 42):由 USB 挂起模式的唤醒事件触发

ST 提供的 USB 驱动库,可以在: http://www.stmcu.org/document/detail/index/id-213156 这里
下载到(STSW-STM32121)

 

ST 不但提供源码,还提供了说明文件:CD00158241.pdf(UM0424),专门讲解 USB 库怎
么使用。有了这些资料对我们了解 STM32F103 的 USB 会有不少帮助,尤其在不懂的时候,看
看 ST 的例程,会有意想不到的收获。本实验的 USB 部分就是移植 ST 的 Virtual_COM_Port 例
程相关部分而来,完成一个 USB 虚拟串口的功能。
硬件设计

53.3 软件设计
代码移植自 ST 官方例程:

有了这个官方例程做指引,我们就知道具体需要哪些文件,从而实现本例程。
首先,在工程文件夹下面,新建 USB 文件夹,
并拷贝官方 USB 驱动库相关代码到该文件夹下,即拷贝STM32_USB-FS-Device_Lib_V4.0.0Libraries 文件夹下的 STM32_USB-FS-Device_Driver 文件夹到该文件夹下面。然后,在 USB 文件夹下,新建 CONFIG 文件夹存放 Virtual COM 实现相关代码,即:STM32_USB-FS-Device_Lib_V4.0.0ProjectsVirtual_COM_Portsrc 文件夹下的部分代码:
hw_config.c、usb_desc.c、usb_endp.c、usb_istr.c、usb_prop.c 和 usb_pwr.c 等 6 个.c 文件,同时
拷贝:STM32_USB-FS-Device_Lib_V4.0.0ProjectsVirtual_COM_Portinc 文件夹下面的:
hw_config.h、platform_config.h、usb_conf.h、usb_desc.h、usb_istr.h、usb_prop.h 和 usb_pwr.h 等
7 个头文件到 CONFIG 文件夹下,最后 CONFIG 文件夹下的文件如图

之后,根据 ST 官方 Virtual_COM_Port 例程,在我们本章例程的基础上新建分组添加相关
代码,具体细节,这里就不详细介绍了,添加好之后,如图  所示: 

移植时,我们重点要修改的就是 CONFIG 文件夹下面的代码,USB_CORE 文件夹下的代
码一般不用修改。现在,我们先来简单介绍一下 USB_CORE 文件夹下的几个.c 文件。

usb_regs.c 文件,该文件主要负责 USB 控制寄存器的底层操作,里面有各种 USB 寄存器
的底层操作函数。
usb_init.c 文件,该文件里面只有一个函数:USB_Init,用于 USB 控制器的初始化,不过对
USB 控制器的初始化,是 USB_Init 调用用其他文件的函数实现的,USB_Init 只不过是把他们
连接一下罢了,这样使得代码比较规范。
usb_int.c 文件,该文件里面只有两个函数 CTR_LP 和 CTR_HP,CTR_LP 负责 USB 低优先
级中断的处理。而 CTR_HP 负责 USB 高优先级中断的处理。
usb_mem.c 文件,该文件用于处理 PMA 数据,PMA 全称为 Packet memory area,是 STM32
内部用于 USB/CAN 的专用数据缓冲区,该文件内也只有 2 个函数即: PMAToUserBufferCopy
和 UserToPMABufferCopy,分别用于将 USB 端点的数据传送给主机和主机的数据传送到 USB
端点。
usb_croe.c 文件,该文件用于处理 USB2.0 协议。
usb_sil.c 文件,该文件为 USB 端点提供简化的读写访问函数。
以上几个文件具有很强的独立性,除特殊情况,不需要用户修改,直接调用内部的函数即
可。接着我们介绍 CONFIG 文件夹里面的几个.c 文件。
hw_config.c 文件,该文件用于硬件的配置,比如初始化 USB 时钟、USB 中断、低功耗模
式处理等。
usb_desc.c 文件,该文件用于 Virtual Com 描述符的处理。
usb_endp.c 文件,该文件用于非控制传输,处理正确传输中断回调函数。
usb_pwr.c 文件,该文件用于 USB 控制器的电源管理;
usb_istr.c 文件,该文件用于处理 USB 中断。
usb_prop.c 文件,该文件用于处理所有 Virtual Com 的相关事件,包括 Virtual Com 的初始
化、复位等等操作。
另外官方例程用到 stm32_it.c 来处理 USB 相关中断,包括两个中断服务函数,第一个是:
USB_LP_CAN1_RX0_IRQHandler 函数,我们在该函数里面调用 USB_Istr 函数,用于处理 USB
发生的各种中断。另外一个就是 USBWakeUp_IRQHandler 函数,我们在该函数就做了一件事:
清除中断标志。为了方便,我们直接将 USB 中断相关代码,全部放到 hw_config.c 里面,所以,
本例程直接用不到 stm32_it.c。
USB 相关代码,就给大家介绍到这里,详细的介绍,请大家参考:CD00158241.pdf 这个文
档。
注意,以上代码,有些是经过修改了的,并非完全照搬官方例程。接着我们在工程文件里
面新建 USB_CORE 和 USB_CONFIG 分组,分别加入 USB\ STM32_USB-FS-Device_Driver\src
下面的代码和 USB\CONFIG 下面的代码,然后把 USB\STM32_USB-FS-Device_Driver\inc 和
USB\CONFIG 文件夹加入头文件包含路径。
最后修改 main.c 里面代码如下:
int main(void)
{
u16 t; u16 len;
u16 times=0; u8 usbstatus=0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2
uart_init(115200); //串口初始化为 115200
LED_Init(); //初始化与 LED 连接的硬件接口

LCD_Init(); //初始化 LCD
POINT_COLOR=RED; //设置字体为红色
LCD_ShowString(30,50,200,16,16,"WarShip STM32");
LCD_ShowString(30,70,200,16,16,"USB Virtual USART TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2015/1/28");
LCD_ShowString(30,130,200,16,16,"USB Connecting...");//提示 USB 开始连接
delay_ms(1800);
USB_Port_Set(0); //USB 先断开
delay_ms(700);
USB_Port_Set(1); //USB 再次连接
Set_USBClock();
USB_Interrupts_Config();
USB_Init();
while(1)
{
if(usbstatus!=bDeviceState)//USB 连接状态发生了改变.
{
usbstatus=bDeviceState;//记录新的状态
if(usbstatus==CONFIGURED)
{
POINT_COLOR=BLUE;
LCD_ShowString(30,130,200,16,16,"USB Connected "); //连接成功
LED1=0;//DS1 亮
}else
{
POINT_COLOR=RED;
LCD_ShowString(30,130,200,16,16,"USB disConnected "); //提示断开
LED1=1;//DS1 灭
}
}
if(USB_USART_RX_STA&0x8000)
{
len=USB_USART_RX_STA&0x3FFF;//得到此次接收到的数据长度
usb_printf("\r\n 您发送的消息为:%d\r\n\r\n",len);
for(t=0;t {
USB_USART_SendData(USB_USART_RX_BUF[t]);//按字节发送给 USB
}
usb_printf("\r\n\r\n");//插入换行
USB_USART_RX_STA=0;
}else
{

times++;
if(times%5000==0)
{
usb_printf("\r\n 战舰 STM32 开发板 USB 虚拟串口实验\r\n");
usb_printf("正点原子@ALIENTEK\r\n\r\n");
}
if(times%200==0)usb_printf("请输入数据,以回车键结束\r\n");
if(times%30==0)LED0=!LED0;//闪烁 LED,提示系统正在运行.
delay_ms(10);
}
}
}
在此部分代码用于实现我们在硬件设计部分提到的功能,USB 的配置通过三个函数完成:
USB_Interrupts_Config()、Set_USBClock()和 USB_Init(),第一个函数用于设置 USB 唤醒中断和
USB 低优先级数据处理中断,Set_USBClock 函数用于 配置 USB 时钟,也就是从 72M 的主频
得到 48M 的 USB 时钟(1.5 分频)。最后 USB_Init()函数用于初始化 USB,最主要的就是调用
了 Virtual_Com_Port_init 函数,开启了 USB 部分的电源等。这里需要特别说明的是,USB 配置
并没有对 PA11 和 PA12 这两个 IO 口进行设置,是因为,一旦开启了 USB 电源(USB_CNTR
的 PDWN 位清零)PA11 和 PA12 将不再作为其他功能使用,仅供 USB 使用,所以在开启了 USB
电源之后不论你怎么配置这两个 IO 口,都是无效的。要在此获取这两个 IO 口的配置权,则需
要关闭 USB 电源,也就是置位 USB_CNTR 的 PDWN 位,我们通过 USB_Port_Set 函数来禁止/
允许 USB 连接,在复位的时候,先禁止,再允许,这样每次我们按复位电脑都可以识别到 USB
鼠标,而不需要我们每次都拔 USB 线。USB_Port_Set 函数在 hw_config.c 里面实现,代码请参
考本例程源码。
USB 虚拟串口的数据发送,我们通过函数:USB_USART_SendData 来实现,该函数在
hw_config.c 里面实现,该函数代码如下:
//发送一个字节数据到 USB 虚拟串口
void USB_USART_SendData(u8 data)
{
uu_txfifo.buffer[uu_txfifo.writeptr]=data;
uu_txfifo.writeptr++;
if(uu_txfifo.writeptr==USB_USART_TXFIFO_SIZE)//超过 buf 大小了,归零.
{
uu_txfifo.writeptr=0;
}
}
该函数实现发送 1 个字节到虚拟串口,这里,我们用到了一个 uu_txfifo 的结构体,该结构
体是我们在 hw_config 里面定义的一个 USB 虚拟串口发送数据 FIFO 结构体,定义如下:
//定义一个 USB USART FIFO 结构体
typedef struct
{
u8 buffer[USB_USART_TXFIFO_SIZE]; //buffer
vu16 writeptr; //写指针

vu16 readptr; //读指针
}_usb_usart_fifo;
extern _usb_usart_fifo uu_txfifo; //USB 串口发送 FIFO
该结构体用于处理 USB 串口要发送的数据,所有要通过 USB 串口发送的数据,都将先存
放在该结构体的 buffer 数组(FIFO 缓存区)里面,USB_USART_TXFIFO_SIZE 定义了该数组
的大小,通过 writeptr 和 readptr 来控制 FIFO 的写入和读出,该结构体 buffer 数据的写入,是
通过 USB_USART_SendData 函数实现,而 buffer 数据的读出(然后发送到 USB)则是通过端
点 1 回调函数:EP1_IN_Callback 函数实现,该函数在 usb_endp.c 里面实现,代码如下:
void EP1_IN_Callback (void)
{
u16 USB_Tx_ptr;
u16 USB_Tx_length;
if(uu_txfifo.readptr==uu_txfifo.writeptr) return; //无任何数据要发送,直接退出
if(uu_txfifo.readptr {
USB_Tx_length=uu_txfifo.writeptr-uu_txfifo.readptr; //得到要发送的数据长度
}else //超过数组了 读指针>写指针
{
USB_Tx_length=USB_USART_TXFIFO_SIZE-uu_txfifo.readptr;//发送的数据长度
}
if(USB_Tx_length>VIRTUAL_COM_PORT_DATA_SIZE) //超过 64 字节?
{
USB_Tx_length=VIRTUAL_COM_PORT_DATA_SIZE; //此次发送数据量
}
USB_Tx_ptr=uu_txfifo.readptr; //发送起始地址
uu_txfifo.readptr+=USB_Tx_length; //读指针偏移
if(uu_txfifo.readptr>=USB_USART_TXFIFO_SIZE) //读指针归零
{
uu_txfifo.readptr=0;
}
UserToPMABufferCopy(&uu_txfifo.buffer[USB_Tx_ptr], ENDP1_TXADDR, USB_Tx_
length);
SetEPTxCount(ENDP1, USB_Tx_length);
SetEPTxValid(ENDP1);
}
这个函数由 USB 中断处理相关函数调用,将要通过 USB 发送给电脑的数据拷贝到端点 1
的发送区,然后通过 USB 发送给电脑,从而实现串口数据的发送。因为 USB 每次传输数据长
度不超过 VIRTUAL_COM_PORT_DATA_SIZE,所以 USB 发送数据长度:USB_Tx_length 的最
大值只能是 VIRTUAL_COM_PORT_DATA_SIZE。
以上,就是 USB 虚拟串口的数据发送过程,而 USB 虚拟串口数据的接收,则是通过端点
3 来实现的,端点 3 的回调函数为 EP3_OUT_Callback,该函数也是在 usb_endp.c 里面定义,代
码如下:
void EP3_OUT_Callback(void)

{
u16 USB_Rx_Cnt;
USB_Rx_Cnt = USB_SIL_Read(EP3_OUT, USB_Rx_Buffer);
//得到 USB 接收到的数据及其长度
USB_To_USART_Send_Data(USB_Rx_Buffer, USB_Rx_Cnt);
//处理数据(其实就是保存数据)
SetEPRxValid(ENDP3); //时能端点 3 的数据接收
}
该函数也是被 USB 中断处理调用,该函数通过调用 USB_To_USART_Send_Data 函数,实
现 USB 接收数据的保存,USB_To_USART_Send_Data 函数在 hw_config.c 里面实现,代码如下:
//用类似串口 1 接收数据的方法,来处理 USB 虚拟串口接收到的数据.
u8 USB_USART_RX_BUF[USB_USART_REC_LEN];
//接收缓冲,最大 USART_REC_LEN 个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到 0x0d
//bit13~0, 接收到的有效字节数目
u16 USB_USART_RX_STA=0; //接收状态标记
//处理从 USB 虚拟串口接收到的数据
//databuffer:数据缓存区
//Nb_bytes:接收到的字节数.
void USB_To_USART_Send_Data(u8* data_buffer, u8 Nb_bytes)
{
u8 i;
u8 res;
for(i=0;i {
res=data_buffer[i];
if((USB_USART_RX_STA&0x8000)==0) //接收未完成
{
if(USB_USART_RX_STA&0x4000) //接收到了 0x0d
{
if(res!=0x0a)USB_USART_RX_STA=0;//接收错误,重新开始
else USB_USART_RX_STA|=0x8000; //接收完成了
}else //还没收到 0X0D
{
if(res==0x0d)USB_USART_RX_STA|=0x4000;
else
{
USB_USART_RX_BUF[USB_USART_RX_STA&0X3FFF]=res;
USB_USART_RX_STA++;
if(USB_USART_RX_STA>(USB_USART_REC_LEN-1))
USB_USART_RX_STA=0;//接收数据错误,重新开始接收

}
}
}
}
}
该函数接收数据的方法,同第九章串口通信实验的串口中断接收数据方法完全一样,在这
里就不详细介绍了,请参考第九章相关内容即可。USB_To_USART_Send_Data 函数类似于串口
通信实验的串口中断服务函数(USART1_IRQHandler),完成 USB 虚拟串口的数据接收。

你可能感兴趣的:(USB,嵌入式,usb)