目录
1. 串口通讯协议简介
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface) 是指数据一位一位地顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信。串行接口、并行接口是按照数据传输方式来划分的,串行接口是一大类接口。USB、RS232、SATA、PS/2、RS485等等,这些都属于串行接口,也就是说串口,串口是一大类接口,包括这些但不仅仅限于这些。
串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输出调试信息。
在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设;STM32 标准库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的
特性,确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、
解包标准。简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英
文来交流。
下面我们分别对串口通讯协议的物理层及协议层进行讲解。
1.1 物理层
串口通讯的物理层有很多标准及变种,我们主要讲解 RS-232 标准,RS-232 标准主要规定了信号的用途、通讯接口以及信号的电平标准。使用 RS-232 标准的串口设备间常见的通讯结构见图串口通讯结构图:
在上面的通讯方式中,两个通讯设备的 "DB9 接口" 之间通过串口信号线建立起连接,串口信号线中使用 “RS-232 标准” 传输数据信号。由于 RS-232 电平标准的信号不能直接被控制器直接识
别,所以这些信号会经过一个“电平转换芯片” 转换成控制器能识别的 “TTL 标准” 的电平信号,才能实现通讯。
1.1.1 电平标准
根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准、RS-232 标准、RS485 标准、CAN 总线 标准。
1)TTL 电平
通讯标准 | 电平标准(发送端) |
5V TTL | 逻辑 1:2.4V-5V 逻辑 0:0V-0.5V |
硬件框图,TTL 用于两个 MCU 之间的通讯:
0 和 1 表示如下图:
2)RS-232 电平
通讯标准 | 电平标准(发送端) |
RS-232 | 逻辑 1:-3V - -15V 逻辑 0:+3V - +15V |
硬件框图,RS-232 用于 MCU 与 PC 机之间的通信:
0 和 1 表示如下图:
因为控制器一般使用 TTL 电平标准,所以常常会使用电平转换芯片 MA3232 芯片对 TTL 及 RS-232 电平的信号进行互相转换。
3)RS-485 电平
通讯标准 | 电平标准(发送端) | |
RS-232 | 逻辑 1:+2V - +6V 逻辑 0:-6V - -2V | 这里的电平指AB两线间的电压差 |
硬件框图如下:
0 和 1 表示如下图:
4)CAN 总线电平
通讯标准 | 电平标准(发送端) | |
CAN | 逻辑 1:-1.5V - 0V 逻辑 0:+1.5V - +3V | 这里的电平指CAN_High、CAN_Low 两线间的电压差 |
硬件框图如下:
0 和 1 表示如下图:
总结:
从单片机软件编程的角度来讲,RS-232、RS-485 最终结果都是转换为 TTL 电平方式与单片机进行通信(CAN 收发器把差分信号转化为 TTL -> CAN 控制器(MCU))。其目的都是在硬件上实现提高通信质量,提高抗干扰能力。其实,这些电平标准就是在硬件上 信号的传输方式,是用 RS-232 标准,还是用 RS-485 标准,还是用 CAN 总线电平标准,且每种标准的硬件接口可能是不一样的,信号的电压是不一样的。
1.1.2 USB 和 串口 的区分
COM口 ( cluster communication port ) 即 串行通讯端口,简称串口。微机上的串口通常是9针,也有25针的接口,最大速率115200bps。一般在台式计算机的机箱后边后 COM 口,也称DB9接口。但现在笔记本电脑上基本没有了。
USB,是英文 Universal Serial Bus(通用串行总线)的缩写,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在PC领域的接口技术。
USB | 串口 | |
通信方式 | 使用复杂的通信协议,支持并行和串行通信,可同时连接多个设备 | 使用简单的串行通信协议,一次只能连接一个设备 |
传输速率 | 传输速度远快于串口,可达数百兆字节每秒,具体速度取决于 USB 的版本 | 通常传输速率较慢,以波特率(比如115200波特)表示 |
接口形状与连接 | 具有标准化的接口形状,易于插拔,支持即插即用功能 | 接口形状各异,就比如台式计算机中一般会有 RS-232 标准的COM口(也称DB9接口),还有其他接口等 |
用途与应用领域 | 广泛应用于外设连接,如鼠标、键盘、打印机等 | 主要用于工业控制、嵌入式设备和一些旧式硬件的连接 |
常见的疑惑:
a. USB 和 串口 是否可以互相转换?
是的,可以使用转换适配器(采用 CH340 模块 )将 USB 转换为串口,反之亦然。
b. 为什么现代计算机上越来越少见串口?
因为 USB 接口更灵活,传输速度更快,已逐渐取代串口在许多应用中的地位。
c. USB 和 串口 在数据传输中有什么安全性区别?
USB 可能提供更多的安全性,但具体取决于设备和配置。串口通常用于封闭系统,安全性也可能足够。
d. 哪些场景下应优先选择 串口 而不是 USB ?
在工业控制和嵌入式系统等需要简单、可靠连接的场景下,串口可能是更好的选择。
e. USB 的不同版本与串口相比有何优势?
USB 的不同版本提供了不同的传输速率和功能,通常在速度和灵活性方面优于串口。
f. USB 和 串口 之间的联系
所以,当我们的单片机与电脑进行通讯时,将单片机的串口与电脑的 USB 相接是需要 转换模块(CH340)的,CH340 是一个USB 总线的转接芯片,实现USB 转串口、USB 转IrDA 红外或者USB 转打印口。在串口方式下,CH340 提供常用的 MODEM 联络信号,用于为计算机扩展异步串口,或者将普通的串口设备直接升级到USB 总线。电脑端安装 CH340 驱动,就可以看到与从机通信的 COM 口。因为我们现在的电脑上,已经不存在串口(这里说的串口指的是 RS232 标准的串口),所以我们一般使用 USB 转串口芯片,目的只有一个,把电脑的 USB 口映射为串口用。
USB 转 串口 电路:
1.1.3 RS-232 信号线
在最初的应用中,RS-232 串口标准常用于计算机、路由与调制调解器 (MODEN,俗称“猫”) 之间的通讯,在这种通讯系统中,设备被分为 数据终端设备 DTE(计算机、路由) 和 数据通讯设备
DCE(调制调解器)。我们以这种通讯模型讲解它们的信号线连接方式及各个信号线的作用。在旧式的台式计算机中一般会有 RS-232 标准的 COM 口 (也称 DB9 接口),见图电脑主板上的COM 口及串口线。
其中接线口以针式引出信号线的称为公头,以孔式引出信号线的称为母头。在计算机中一般引出公头接口,而在调制调解器设备中引出的一般为母头,使用上图中的串口线即可把它与计算机连接起来。通讯时,串口线中传输的信号就是使用前面讲解的 RS-232 标准调制的。
在这种应用场合下,DB9 接口中的公头及母头的各个引脚的标准信号线接法见图 DB9 标准的公头及母头接法 及表 DB9 信号线说明。
上表中的是计算机端的 DB9 公头标准接法,由于两个通讯设备之间的收发信号 (RXD 与TXD)应交叉相连,所以调制调解器端的 DB9 母头的收发信号接法一般与公头的相反,两个设备之间连接时,只要使用 “直通型” 的串口线连接起来即可,见图计算机与调制调解器的信号线连接。
串口线中的 RTS、CTS、DSR、DTR 及 DCD 信号,使用逻辑 1 表示信号有效,逻辑 0 表示信号无效。例如,当计算机端控制 DTR 信号线表示为逻辑 1 时,它是为了告知远端的调制调解器,本机已准备好接收数据,0 则表示还没准备就绪。
在目前的其它工业控制使用的串口通讯中,一般只使用 RXD、TXD 以及 GND 三条信号线,直接传输数据信号,而 RTS、CTS、DSR、DTR 及 DCD 信号都被裁剪掉了。
1.2 协议层
串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由 启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成见图串口数据包的基本组成。
1)波特率
串口异步通讯,异步通讯中由于没有时钟信号 (如前面讲解的 DB9 接口中是没有时钟信号的),所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码,图串口数据包的基本组成 中用虚线分开的每一格就是代表一个码元。常见的波特率为 4800、9600、115200 等。波特率的单位bps,指的是每秒可以发送多少比特位,115200bps,表示每秒可以发送115200个比特位。
2)通讯的起始和停止信号
串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的 起始信号由一个逻辑 0 的数据位表示,而数据包的 停止信号可由 0.5、1、1.5 或 2 个逻辑 1 的数据位表示,只要双方约定一致即可。
3)有效数据
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为 5、6、7 或 8 位长。一般都是8位,一个字节的长度。
4)数据校验
在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验 (odd)、偶校验
(even)、0 校验 (space)、1 校验 (mark) 以及无校验 (noparity)。
奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为01101001,此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是 8 位的有效数据加上 1 位的校验位总共 9 位。
偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如一个 8 位长的有效数据11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。
0 校验是不管有效数据中的内容是什么,校验位总为“0”。1 校验是校验位总为“1”。
2. 单片机常用的通信接口概况介绍
2.1 常用的通讯接口
名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
USART | TX(数据发送引脚)、RX(数据接收引脚) | 全双工 | 异步 | 单端 | 点对点 |
I2C | SCL(时钟引脚)、SDA(数据引脚) | 半双工 | 同步 | 单端 | 多设备 |
SPI | SCLK(时钟)、MOSI、MISO、CS | 全双工 | 同步 | 单端 | 多设备 |
CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 |
USB | DP(D+)、DM(D_) | 半双工 | 异步 | 差分 | 点对点 |
引脚说明:
- MOSI:主机输出数据引脚;
- MISO:主机输入数据引脚;
- CS:片选,用于指定通信的对象;
- CAN、USB 的两个引脚表示的是 差分数据;
半/全双工:
- 全双工:指通信双方能够同时进行双向通信;
- 半双工:指通信双方在同一时刻只能有一方在传输数据,另一方在接收数据;
时钟:
- 同步:设备之间用同一根时钟线;
- 异步:需要自己定义时钟,或者如USART定义波特率;
单端/差分:
- 单端:他们的引脚的高低电平都是对 GND 的电压差,所以双方需要共地;
- 差分:信号的传输是靠两条线的电压差来区分逻辑位的;
2.2 串口通讯中什么是异步通讯?什么是同步通讯?
串口通讯中,异步通讯和同步通讯是两种不同的数据传输方式,它们在数据帧传输时的时间同步方式上有所不同:
①异步通讯(Asynchronous Communication):
- 异步通讯是一种不要求发送端和接收端时钟同步的通讯方式。
- 数据帧在传输时包含起始位(Start Bit)、数据位、校验位(可选)、停止位(Stop Bit)。
- 发送端和接收端都约定好一个特定的波特率(波特率表示每秒传输的比特数),通常是常见的波特率之一,例如 9600 bps(bits per second)。
- 发送端和接收端在传输数据前都要遵循相同的波特率,数据位数,校验方式和停止位数。然后发送端按照这些设置构建数据帧,并在每个数据帧之间插入停止位,以确保接收端能够正确解析数据。
②同步通讯(Synchronous Communication):
- 同步通讯是一种需要发送端和接收端时钟同步的通讯方式。
- 数据帧通常没有明确的起始位和停止位,而是按照时钟信号连续传输。
- 通信双方必须共享一个共同的时钟源,以确保数据的传输和接收在时钟的同步下进行。
- 同步通讯通常用于高速数据传输,如以太网通信等。
总的来说,异步通讯是串口通讯中更常见的方式,因为它不要求发送端和接收端的时钟严格同步,使其在各种不同的硬件和应用中都能使用。同步通讯则需要更严格的时钟同步,通常用于特定高速数据传输的场合。选择哪种通讯方式通常取决于应用的要求和硬件支持。
3. STM32 的 UART/USART
3.1 UART/USART 简介
串行通信一般是以帧格式传输数据,即一帧一帧的传输,每帧包含起始信号、数据信息、停止信号,可能还有校验信息。UART 和 USART 都是用于串行通信的通信接口,它们之间有一些共同之处,但也有一些关键的区别:
1)通用异步收发器 UART (Universal Asynchronous Receiver and Transmitter)
- UART 是一种通用异步收发器/发射器,它是一种硬件接口,通常用于串行通信。
- UART 通信是异步的,这意味着数据帧之间没有共享时钟信号。通信双方必须事先协商好通信参数,如波特率、数据位、停止位和奇偶校验等,以确保数据正确传输。
- UART 可以用于单向通信或双向通信,因为它包含了一个接收器和一个发射器。
2)通用同步/异步收发器 USART (Universal Synchronous Asynchronous Receiver and Transmitter)
- USART 是一种通用同步/异步收发器/发射器,也是一种串行通信接口。
- USART 可以支持异步通信(与 UART 类似)和同步通信。这意味着 USART 具有一个可选的同步模式,其中通信双方共享一个时钟信号,从而提供了更可靠的同步通信。
- USART 通常具有更多的功能和灵活性,可以支持不仅仅是异步通信,还可以适应更多的通信需求。
UART是在 USART 基础上裁剪掉了同步通信功能,只有异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是 UART。或者把 USART 当 UART 来用。
USART 的优点:
USART 满足外部设备对工业标准 NRZ 异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率,使得它的应用更加广泛。USART 支持同步单向通信和半双工单线通
信;还支持局域互连网络 LIN、智能卡 (SmartCard) 协议与 lrDA(红外线数据协会) SIR ENDEC 规
范。
USART 支持使用 DMA,可实现高速数据通信,有关 DMA 具体应用将在 DMA 章节作具体讲解。USART 在 STM32 应用最多莫过于 “打印”程序信息,一般在硬件设计时都会预留一个 USART
通信接口连接电脑,用于在调试程序是可以把一些调试信息“打印”在电脑端的串口调试助手工具上,从而了解程序运行是否正确、如果出错哪具体哪里出错等等。
3.2 硬件电路
- 简单的双向串口通信有 2 根通信线(发送端 TX 和接收端 RX)
- TX 与 RX 要交叉连接
- 当只需单向的数据传输时,可以只接一根通信线
- 当电平标准不一致,需加电平转换芯片
3.3 电平标准
电平标准是数据 1 和数据 0 的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
① TTL电平:+3.3v/5v 表示 逻辑 1,0v 表示 逻辑 0
② RS232电平:-3v~-15v 表示逻辑 1,+3v~+15v 表示 逻辑 0
③ RS485电平:两线压差+2v~+6v 表示逻辑 1,-2v~-6v 表示 逻辑 0(差分信号)
3.4 串口参数
波特率:串口通信的速率(通信双方约定速率)
起始位:标志一个数据帧的开始,串口数据帧的起始位固定为低电平
数据位:数据帧的有效载荷,1 为高电平,0 为低电平,低位先行
检验位:用于数据验证,根据数据位计算得来
停止位:用于数据帧间隔,串口数据帧的停止位固定为高电平
3.5 串口时序
在示波器上查看串口的数据传输过程:
Tx 引脚输出定时翻转的高低电平
Rx 引脚定时读取引脚的高低电平
3.6 USART 外设简介
USART 通用同步/异步收发器。
USART/UART 是 STM32 内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从 Tx 引脚发送出去,也可自动接收 Rx 引脚的数据帧时序,拼接为一个字节数据存在数据寄存器里。
自带波特率发生器(相当于分频器)
可配置数据位长度(8/9),停止位长度(0.5-2)
可选检验位(无校验/奇校验/偶校验)
支持同步模式(clk 时钟输出),硬件流控制(防止接收端 处理慢而导致数据丢失的问题)
3.7 USART 功能框图
1)功能引脚
TX | 发送数据输出引脚; |
RX | 接收数据输入引脚; |
SW_RX | 数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚; |
nRTS | 请求以发送 (Request To Send),n 表示低电平有效。如果使能 RTS 流控制,当 USART 接收器准备好接收新数据时就会将 nRTS 变成低电平;当接收寄存器已满时,nRTS 将被设置为高电平。该引脚只适用于硬件流控制; |
nCTS | 清除以发送 (Clear To Send),n 表示低电平有效。如果使能 CTS 流控制,发送器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制; |
SCLK | 发送器时钟输出引脚。这个引脚仅适用于同步模式。 |
USART 引脚在 STM32F103ZET6 芯片具体分布见表 :
STM32F103ZET6 系统控制器有三个 USART 和两个 UART(STM32F103C8T6只有三个USART)其中 USART1 时钟来源于 APB2 总线时钟,其最大频率为 72MHz,其他四个的时钟来源于 APB1 总线时钟,其最大频率为 36MHz。UART 只是异步传输功能,所以没有 SCLK、nCTS 和 nRTS 功能引脚。
2)数据寄存器
USART 数据寄存器 (USART_DR) 只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART控制寄存器 1(USART_CR1) 的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9位数据字长,我们一般使用 8 位数据字长。
USART_DR 包含了已发送的数据或者接收到的数据。USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR 读取数据会自动提取 RDR 数据。TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。
USART 支持 DMA 传输,可以实现高速数据传输,具体 DMA 使用将在 DMA 章节讲解。
3)控制器
USART 有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等等。使用
USART 之前需要向 USART_CR1 寄存器的 UE 位置 1 使能 USART,UE 位用来开启供给给串口的时钟。发送或者接收数据字长可选 8 位或 9 位,由 USART_CR1 的 M 位控制。
发送器
当 USART_CR1 寄存器的发送使能位 TE 置 1 时,启动数据发送,发送移位寄存器的数据会在 TX 引脚输出,低位在前,高位在后。如果是同步模式 SCLK 也输出时钟信号。
一个字符帧发送需要三个部分:起始位 + 数据帧 + 停止位。起始位是一个位周期的低电平,位周期就是每一位占用的时间;数据帧就是我们要发送的 8 位或 9 位数据,数据是从最低位开始传输的;停止位是一定时间周期的高电平。
停止位时间长短是可以通过 USART 控制寄存器 2(USART_CR2) 的 STOP[1:0] 位控制,可选 0.5个、1 个、1.5 个和 2 个停止位。默认使用 1 个停止位。2 个停止位适用于正常 USART 模式、单线模式和调制解调器模式。0.5 个和 1.5 个停止位用于智能卡模式。当选择 8 位字长,使用 1 个停止位时,具体发送字符时序图见图字符发送时序图。
当发送使能位 TE 置 1 之后,发送器开始会先发送一个空闲帧 (一个数据帧长度的高电平),接下来就可以往 USART_DR 寄存器写入要发送的数据。在写入最后一个数据后,需要等待 USART 状态寄存器 (USART_SR) 的 TC 位为 1,表示数据传输完成,如果 USART_CR1 寄存器的 TCIE 位置1,将产生中断。
在发送数据时,编程的时候有几个比较重要的标志位我们来总结下。
名称 | 描述 |
TE | 发送使能 |
TXE | 发送寄存器为空,发送单个字节的时候使用 |
TC | 发送完成,发送多个字节数据的时候使用 |
TXIE | 发送完成中断使能 |
接收器
如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,使得接收器在 RX 线开始搜索起始位。在确定到起始位后就根据 RX 线电平状态把数据存放在接收移位寄存器内。接收完成
后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置 1,同时如果
USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。
在接收数据时,编程的时候有几个比较重要的标志位我们来总结下。
名称 | 描述 |
RE | 接收使能 |
RXNE | 读数据寄存器非空 |
RXNEIE | 接收完成中断使能 |
4)小数波特率生成
波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,单位为波特。比特率指单位时间内传输的比特数,单位 bit/s(bps)。对于 USART 波特率与比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。
USART 的发送器和接收器使用相同的波特率。计算公式如下:
其中,fPLCK 为 USART 时钟,USARTDIV 是一个存放在波特率寄存器 (USART_BRR) 的一个无符号定点数。其中 DIV_Mantissa[11:0] 位定义 USARTDIV 的整数部分,DIV_Fraction[3:0] 位定义USARTDIV 的小数部分。
例如:DIV_Mantissa=24(0x18),DIV_Fraction=10(0x0A),此时 USART_BRR 值为 0x18A;那么 USARTDIV 的小数位 10/16=0.625;整数位 24,最终 USARTDIV 的值为 24.625。
如果知道 USARTDIV 值为 27.68,那么 DIV_Fraction=16*0.68=10.88,最接近的正整数为 11,所
以 DIV_Fraction[3:0] 为 0xB;DIV_Mantissa= 整数 (27.68)=27,即为 0x1B。
波特率的常用值有 2400、9600、19200、115200。下面以实例讲解如何设定寄存器值得到波特率的值。
我们知道 USART1 使用 APB2 总线时钟,最高可达 72MHz,其他 USART 的最高频率为 36MHz。我们选取 USART1 作为实例讲解,即 fPLCK=72MHz。为得到 115200bps 的波率,此时:
解得 USARTDIV=39.0625,可算得 DIV_Fraction=0.0625*16=1=0x01,DIV_Mantissa=39=0x27,即应该设置 USART_BRR 的值为 0x271。
这里我们直接调用库函数,设置结构体参数,输入我们想要的波特率数值即可,无需像以上这样复杂计算。
3.8 USART 基本结构
3.9 USART 初始化结构体详解
标准库函数对每个外设都建立了一个初始化结构体,比如 USART_InitTypeDef,结构体成员用于设置外设工作参数,并由外设初始化配置函数,比如 USART_Init() 调用,这些设定参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。
初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f10x_usart.h 文件中,初始化库函数定义在 stm32f10x_usart.c 文件中,编程时我们可以结合这两个文件内注释使用。
USART 初始化结构体
typedef struct
{
uint32_t USART_BaudRate; // 波特率
uint16_t USART_WordLength; // 字长
uint16_t USART_StopBits; // 停止位
uint16_t USART_Parity; // 校验位
uint16_t USART_Mode; // USART 模式
uint16_t USART_HardwareFlowControl; // 硬件流控制
} USART_InitTypeDef;
USART_BaudRate | 波特率设置。一般设置为 2400、9600、19200、115200。标准库函数会根据设定值计算得到 USARTDIV 值,从而设置USART_BRR寄存器值。 |
USART_WordLength | 数据帧字长,可选 8 位或 9 位。它设定 USART_CR1 寄存器的 M 位的值。如果没有使能奇偶校验控制,一般使用 8 数据位;如果使能了奇偶校验则一般设置为 9 数据位。 |
USART_StopBits | 停止位设置,可选 0.5 个、1 个、1.5个和2个停止位,它设定 USART_CR2 寄存器的 STOP[1:0] 位的值,一般我们选择 1 个停止位。 |
USART_Parity | 奇偶校验控制选择,可选 USART_Parity_No(无校验)、USART_Parity_Even(偶 校验) 以及 USART_Parity_Odd(奇校验),它设定 USART_CR1 寄存器的 PCE 位和 PS 位的值。 |
USART_Mode | USART 模式选择,有 USART_Mode_Rx 和 USART_Mode_Tx,允许使用逻辑或运算选择两个,它设定 USART_CR1 寄存器的 RE 位和 TE 位。 |
USART_HardwareFlowControl | 硬件流控制选择,只有在硬件流控制模式才有效,可选有 使能RTS、使能CTS、同时使能 RTS 和 CTS、不使能硬件流 |
USART 时钟初始化结构体
typedef struct
{
uint16_t USART_Clock; // 时钟使能控制
uint16_t USART_CPOL; // 时钟极性
uint16_t USART_CPHA; // 时钟相位
uint16_t USART_LastBit; // 最尾位时钟脉冲
} USART_ClockInitTypeDef;
USART_Clock | 同步模式下SCLK引脚上时钟输出使能控制,可选禁止时钟输出 (USART_Clock_Disable) 或开启时钟输出 (USART_Clock_Enable);如果使用同步模式发送,一般都需要开启时钟。它设定 USART_CR2 寄存器的 CLKEN 位的值。 |
USART_WordLength | 数据帧字长,可选 8 位或 9 位。它设定 USART_CR1 寄存器的 M 位的值。如果没有使能奇偶校验控制,一般使用 8 数据位;如果使能了奇偶校验则一般设置为 9 数据位。 |
USART_StopBits | 停止位设置,可选 0.5 个、1 个、1.5个和2个停止位,它设定 USART_CR2 寄存器的 STOP[1:0] 位的值,一般我们选择 1 个停止位。 |
USART_Parity | 奇偶校验控制选择,可选 USART_Parity_No(无校验)、USART_Parity_Even(偶 校验) 以及 USART_Parity_Odd(奇校验),它设定 USART_CR1 寄存器的 PCE 位和 PS 位的值。 |
USART_Mode | USART 模式选择,有 USART_Mode_Rx 和 USART_Mode_Tx,允许使用逻辑或运算选择两个,它设定 USART_CR1 寄存器的 RE 位和 TE 位。 |
USART_HardwareFlowControl | 硬件流控制选择,只有在硬件流控制模式才有效,可选有 使能RTS、使能CTS、同时使能 RTS 和 CTS、不使能硬件流 |
一般我们用的最多的就是 USART 的异步通讯,同步通讯,利用时钟的实验和项目不多。
4. USART1 接发通信实验
USART 只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留 USART 接口来实现与其他模块或者控制器进行数据传输,比如 GSM 模块,WIFI 模块、蓝牙模块等等。在硬件设计时,注意还需要一根“共地线”。
我们经常使用 USART 来实现控制器与电脑之间的数据传输。这使得我们调试程序非常方便,比如我们可以把一些变量的值、函数的返回值、寄存器标志位等等通过 USART 发送到串口调试助手,这样我们可以非常清楚程序的运行状态,当我们正式发布程序时再把这些调试信息去除即可。
我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工作。
首先,我们来编写一个程序实现开发板与电脑通信,在开发板上电时通过 USART 发送一串字符串给电脑,然后开发板进入中断接收等待状态,如果电脑有发送数据过来,开发板就会产生中断,我们在中断服务函数接收数据,并马上把数据返回发送给电脑。
4.1 硬件设计
为利用 USART 实现开发板与电脑通信,需要用到一个 USB 转 USART 的 IC,我们选择 CH340G 芯片来实现这个功能,CH340G 是一个 USB 总线的转接芯片,实现 USB 转 USART、USB 转 lrDA红外或者 USB 转打印机接口,我们使用其 USB 转 USART 功能。具体电路设计见图USB 转串口硬件设计 _。
我们将 CH340G的 TXD引脚与 USART1 的 RX 引脚连接,CH340G 的 RXD 引脚与 USART1 的TX 引脚连接。CH340G 芯片集成在开发板上,其地线 (GND) 已与控制器的 GND 连通。
①第一种
第二种
还有第三种,根上边的 1.1.2 中的电路原理图是一样的。都可以实现将 USB 转为 USART。
4.2 软件设计
参考正点原子的程序设计,所有的注释说明都在代码里体现了。
main.c
void main(void)
{
u16 t;
u16 len;
u16 times = 0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
usart1_init(115200); //串口初始化为115200
led_init(); //LED端口初始化
key_init(); //初始化与按键连接的硬件接口
//printf函数和UsartPrintf函数的用法
// printf("你个老六\r\n");
delay_ms(50);
UsartPrintf(USART1, "温度:%d.%d 湿度:%d.%d\r\n", 26, 26, 56, 56);//放在这里打印出错,是因为printf函数和UsartPrintf函数不能连着放在一起
while (1)
{
if (USART_RX_STA1 & 0x8000) //括号里表示的是SAT的最高位bit15,所以这里判断的是STA的最高位
{
len = USART_RX_STA1 & 0x3fff; //得到此次接收到的数据长度
printf("\r\n您发送的消息为:\r\n\r\n");
for (t = 0; t < len; t++)
{
USART_SendData(USART1, USART1_RX_BUF[t]);//向串口1发送数据
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET); //等待发送结束
}
printf("\r\n\r\n");//插入换行
USART_RX_STA1 = 0;
}
else//以下是一些提示信息
{
times++;
if (times % 5000 == 0)
{
printf("\r\n精英STM32开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n");
}
if (times % 200 == 0)
{
printf("请输入数据,以回车键结束\r\n");
// UsartPrintf(USART1, "温度:%d.%d 湿度:%d.%d\r\n", 26, 26, 56, 56); // 这里两个函数不能放在一起
}
if (times % 30 == 0)LED1_G = !LED1_G; //闪烁LED,提示系统正在运行.
delay_ms(10);
}
}
}
usart.h
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "sys.h"
#define USART_REC_LEN 200 //定义最大接收字节数 200
#define EN_USART1_RX 1 //使能(1)/禁止(0)串口1接收
extern u8 USART1_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern u16 USART_RX_STA1; //串口1接收状态标记
//如果想串口中断接收,请不要注释以下宏定义
void usart1_init(u32 bound);
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num);
void Usart_SendString( USART_TypeDef * pUSARTx, char *str);
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch);
void UsartPrintf(USART_TypeDef *USARTx, char *fmt,...);
#endif
usart.c
#include "sys.h"
#include "usart.h"
#include "stdio.h"
#include "string.h"
#include "stdarg.h"
#include "stm32f10x_tim.h"
/*---------------------------------printf函数配置-----------------------------------*/
/* 使用microLib的方法,记得要在魔术棒哪里进行勾选 */
//重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)//串口printf发送函数,若要改为其他串口直接修改USART1、2,然而这里修改串口又是可以的
{
while ((USART1->SR & 0X40) == 0); //循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(USART1);
}
/*另一种使用microLib的方法*/
/*
int fputc(int ch, FILE *f)
{
USART_SendData(USART2, (uint8_t) ch);
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET) {}
return ch;
}
int GetKey (void) {
while (!(USART2->SR & USART_FLAG_RXNE));
return ((int)(USART2->DR & 0x1FF));
}
*/
/*------------------------------------------------------------------------------------*/
/*--------------------------------------串口1配置-------------------------------------*/
#if EN_USART1_RX //如果使能了接收,若要修改串口,这里的宏定义必须也要修改
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART1_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节,这是个外部可调用的变量,需要在头文件中声明外部变量
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA1 = 0x0000; //接收状态标记,这是个外部可调用的变量,需要在头文件中声明外部变量
void usart1_init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* 开启USART1,GPIOA的时钟 只有USART1挂载在APB2上,其他的串口都在APB1上*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
/* 复位串口1 */
USART_DeInit(USART1);
/*--------------------------GPIO端配置-----------------------------*/
//USART1_TX GPIOA.9 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;// 这个是错误示例,必须使用 GPIO_Mode_AF_PP
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART1_RX GPIOA.10 浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*--------------------------串口中断优先级配置-----------------------------*/
//USART1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ; //抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
/*--------------------------串口的工作参数配置-----------------------------*/
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口1接收中断
USART_Cmd(USART1, ENABLE); //使能串口1
}
/**
* @brief USART1中断服务函数
* @param 无
* @retval 无
*/
void USART1_IRQHandler(void)//串口1接收中断服务程序
{
u8 Res;
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res = USART_ReceiveData(USART1); //读取接收到的数据
if ((USART_RX_STA1 & 0x8000) == 0) //判断USART_RX_STA的bit15,该位为0表示接收未完成
{
//用USART_RX_STA的bit14判断是否接收到回车符,如果接收到了回车符,再判断是否接收到了换行符
if (USART_RX_STA1 & 0x4000)
{
if (Res != 0x0a)
{
USART_RX_STA1 = 0; //通过接收变量Res是否接收到换行符(\n)0x0A来对USART_RX_STA的bit15进行操作
}
else
{
USART_RX_STA1 |= 0x8000; //若接收到回车符紧接着又接收到换行符,表明这一组数据接收完成了
}
}
else//如果USART_RX_STA的bit14还没被写1,表示还没收到回车符(\r)0x0D
{
if (Res == 0x0d) //电脑中换行符(\n)0x0A,回车符(\r)0x0D,空格符0x20。16进制中不区分大小写字母
{
USART_RX_STA1 |= 0x4000; //如果Res接收到回车符(\r)0x0D,把bit14写1
}
else
{
USART1_RX_BUF[USART_RX_STA1 & 0X3FFF] = Res ; //在还没有接收到回车符(\r)0x0D之前,把接收到的数据放到数组里,数组从0位开始算
USART_RX_STA1++;//放一个,USART_RX_STA数据加一,因为USART_RX_STA的0-13位是记录数据个数的。这里是从0开始算的,所以要USART_REC_LEN要减一,
if (USART_RX_STA1 > (USART_REC_LEN - 1)) //200个数据,但USART_RX_STA是从0算起的
{
USART_RX_STA1 = 0; //如果接收到数据超过200个,则接收数据错误,重新开始接收
printf("\r\n您的输入错误\r\n\r\n");
}
}
}
}
}
}
#endif
其他串口配置亦是如此。这里在 usart.c 中还写了其他的可调用函数:
发送一个字符函数:
/***************** 发送一个字符 **********************/
void Usart_SendByte(USART_TypeDef *pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx, ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
发送8位的数组:
/****************** 发送8位的数组 ************************/
void Usart_SendArray(USART_TypeDef *pUSARTx, uint8_t *array, uint16_t num)
{
uint8_t i;
for (i = 0; i < num; i++)
{
/* 发送一个字节数据到USART */
Usart_SendByte(pUSARTx, array[i]);
}
/* 等待发送完成 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}
发送字符串:
/***************** 发送字符串 **********************/
void Usart_SendString(USART_TypeDef *pUSARTx, char *str)
{
unsigned int k = 0;
do
{
Usart_SendByte(pUSARTx, *(str + k));
k++;
}
while (*(str + k) != '\0');
/* 等待发送完成 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET)
{}
}
发送一个16位数:
/***************** 发送一个16位数 **********************/
void Usart_SendHalfWord(USART_TypeDef *pUSARTx, uint16_t ch)
{
uint8_t temp_h, temp_l;
/* 取出高八位 */
temp_h = (ch & 0XFF00) >> 8;
/* 取出低八位 */
temp_l = ch & 0XFF;
/* 发送高八位 */
USART_SendData(pUSARTx, temp_h);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
/* 发送低八位 */
USART_SendData(pUSARTx, temp_l);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
格式化打印:
/*
************************************************************
* 函数名称: UsartPrintf
*
* 函数功能: 格式化打印
*
* 入口参数: USARTx:串口组
* fmt:不定长参
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
// 移植该函数的时候一定要加上头文件:#include "stdarg.h",va函数在头文件stdarg.h中,要使用其中的参数,必须包含此头文件#include "stdarg.h"
void UsartPrintf(USART_TypeDef *USARTx, char *fmt, ...)
{
unsigned char UsartPrintfBuf[296];
va_list ap;
unsigned char *pStr = UsartPrintfBuf;
va_start(ap, fmt);
vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); //格式化
va_end(ap);
while (*pStr != 0)
{
USART_SendData(USARTx, *pStr++);
while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);
}
}