15. USART—串口通讯¶
本章参考资料:《dm00327659-stm32mp157-advanced-armbased-32bit-mpus-stmicroelectronics》 参考手册USART章节。
学习本章时,配合《dm00327659-stm32mp157-advanced-armbased-32bit-mpus-stmicroelectronics》USART章节一起阅读, 效果会更佳。
特别说明,本书内容是以STM32MP15x系列控制器资源讲解。
15.1. 串口通讯协议简介¶
串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单便捷, 因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输出调试信息。
在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设; STM32HAL库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也以分层的方式来理解, 最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的特性, 确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。 简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。
下面我们分别对串口通讯协议的物理层及协议层进行讲解。
15.1.1. 物理层¶
串口通讯的物理层有很多标准及变种,我们主要讲解RS-232标准 ,RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。
使用RS-232标准的串口设备间常见的通讯结构见 图14_1。
图 14‑1 串口通讯结构图
在上面的通讯方式中,两个通讯设备的“DB9接口”之间通过串口信号线建立起连接, 串口信号线中使用“RS-232标准”传输数据信号。由于RS-232电平标准的信号不能直接被控制器直接识别, 所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL标准”的电平信号,才能实现通讯。
15.1.1.1. 电平标准¶
根据通讯使用的电平标准不同,串口通讯可分为TTL标准及RS-232标准,见表14‑1。
表 14‑1 TTL电平标准与RS232电平标准
通讯标准 |
电平标准(发送端) |
---|---|
5V TTL |
逻辑1:2.4V-5V 逻辑0:0~0.5V |
RS-232 |
逻辑1:-15V~-3V 逻辑0:+3V~+15V |
我们知道常见的电子电路中常使用TTL的电平标准,理想状态下,使用5V表示二进制逻辑1,使用0V表示逻辑0; 而为了增加串口通讯的远距离传输及抗干扰能力, 它使用-15V表示逻辑1,+15V表示逻辑0。使用RS232与TTL电平校准表示同一个信号时的对比见 图14_2。
图 14‑2 RS-232与TTL电平标准下表示同一个信号
因为控制器一般使用TTL电平标准,所以常常会使用MA3232芯片对TTL及RS-232电平的信号进行互相转换。
15.1.1.2. RS-232信号线¶
在最初的应用中,RS-232串口标准常用于计算机、路由与调制调解器(MODEN,俗称“猫”)之间的通讯 ,在这种通讯系统中,设备被分为数据终端设备DTE(计算机、路由)和数据通讯设备DCE(调制调解器)。 我们以这种通讯模型讲解它们的信号线连接方式及各个信号线的作用。
在旧式的台式计算机中一般会有RS-232标准的COM口(也称DB9接口),见 图14_3。
图 14‑3 电脑主板上的COM口及串口线
其中接线口以针式引出信号线的称为公头,以孔式引出信号线的称为母头。在计算机中一般引出公头接口, 而在调制调解器设备中引出的一般为母头,使用上图中的串口线即可把它与计算机连接起来。 通讯时,串口线中传输的信号就是使用前面讲解的RS-232标准调制的。
在这种应用场合下,DB9接口中的公头及母头的各个引脚的标准信号线接法见 图14_4 及 表14_2。
图 14‑4 DB9标准的公头及母头接法
表 14‑2 DB9信号线说明(公头,为方便理解,可把DTE理解为计算机,DCE理解为调制调解器)
序号 |
名称 |
符号 |
数据方向 |
说明 |
---|---|---|---|---|
1 |
载波检测 |
DCD |
DTEDCE |
Data Carrier Detect,数据载波 检测,用于DTE告知对 方,本机是否收到对方的 载波信号 |
2 |
接收数据 |
RXD |
DTEDCE |
Receive Data,数据接收信号 ,即输入 。 |
3 |
发送数据 |
TXD |
DTEDCE |
Transmit Data,数据发送信号 ,即输出。两个设备之间 的TXD与RXD应交叉 相连 |
4 |
数据终端 (DTE) 就绪 |
DTR |
DTEDCE |
Data Terminal Ready,数据终端就 绪,用于DTE向对方告 知本机是否已准备好 |
5 |
信号地 |
GND |
地线,两个通讯设备之间 的地电位可能不一样,这 会影响收发双方的电平信 号,所以两个串口设备之 间必须要使用地线连接, 即共地。 |
|
6 |
数据设备(DCE)就绪 |
DSR |
DTEDCE |
Data Set Ready,数据发送就 绪,用于DCE告知对方 本机是否处于待命状态 |
7 |
请求发送 |
RTS |
DTEDCE |
Request To Send,请求发送, DTE 请求 DCE 本设备向DCE端发送数 据 |
8 |
允许发送 |
CTS |
DTEDCE |
Clear To Send,允许发送,D CE回应对方的RTS发 送请求,告知对方是否可 以发送数据 |
9 |
响铃指示 |
RI |
DTEDCE |
Ring Indicator,响 铃指示,表示DCE端与 线路已接通 |
上表中的是计算机端的DB9公头标准接法,由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连, 所以调制调解器端的DB9母头的收发信号接法一般与公头的相反,两个设备之间连接时, 只要使用“直通型”的串口线连接起来即可,见 图14_5。
图 14‑5 计算机与调制调解器的信号线连接
串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑1表示信号有效,逻辑0表示信号无效。 例如,当计算机端控制DTR信号线表示为逻辑1时,它是为了告知远端的调制调解器, 本机已准备好接收数据,0则表示还没准备就绪。
在目前的其它工业控制使用的串口通讯中,一般只使用RXD、TXD以及GND三条信号线, 直接传输数据信号,而RTS、CTS、DSR、DTR及DCD信号都被裁剪掉了。
15.1.2. 协议层¶
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口。在串口通讯的协议层中,规定了数据包的内容, 它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成见 图14_6。
图 14‑6 串口数据包的基本组成
15.1.2.1. 波特率¶
本章中主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号(如前面讲解的DB9接口中是没有时钟信号的), 所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码, 图14_6 中用虚线分开的每一格就是代表一个码元。常见的波特率为4800、9600、115200等。
15.1.2.2. 通讯的起始和停止信号¶
串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0的数据位表示, 而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
15.1.2.3. 有效数据¶
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为5、6、7或8位长。
15.1.2.4. 数据校验¶
在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差, 可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)以及无校验(noparity), 它们介绍如下:
奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个8位长的有效数据为:01101001,此时总共有4个“1”,为达到奇校验效果, 校验位为“1”,最后传输的数据将是8位的有效数据加上1位的校验位总共9位。
偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为4个, 所以偶校验位为“0”。
0校验是不管有效数据中的内容是什么,校验位总为“0”,1校验是校验位总为“1”。
在无校验的情况下,数据包中不包含校验位。
15.2. STM32的USART简介¶
STM32芯片具有多个USART外设用于串口通讯,它是 Universal Synchronous Asynchronous Receiver and Transmitter的缩写, 即通用同步异步收发器可以灵活地与外部设备进行全双工数据交换。有别于USART,它还有具有UART外设( Universal Asynchronous Receiver and Transmitter),它是在USART基础上裁剪掉了同步通信功能, 只有异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是UART。
USART满足外部设备对工业标准NRZ异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率, 使得它的应用更加广泛。USART支持同步单向通信和半双工单线通信;还支持局域互连网络LIN、 智能卡(SmartCard)协议与lrDA(红外线数据协会) SIR ENDEC规范。
USART支持使用DMA,可实现高速数据通信,有关DMA具体应用将在DMA章节作具体讲解。
USART在STM32应用最多莫过于“打印”程序信息,一般在硬件设计时都会预留一个USART通信接口连接电脑, 用于在调试程序是可以把一些调试信息“打印”在电脑端的串口调试助手工具上, 从而了解程序运行是否正确、指出运行出错位置等等。
STM32的USART输出的是TTL电平信号,若需要RS-232标准的信号可使用MAX3232芯片进行转换。
15.3. USART功能框图¶
STM32的USART的功能框图包含了USART最核心内容,掌握了功能框图,对USART就有一个整体的把握, 在编程时就思路就非常清晰。USART功能框图见 图14_7。
图 14‑7 USART功能框图
15.3.1. 功能引脚¶
TX:发送数据输出引脚。
RX:接收数据输入引脚。
RTS:请求以发送(Request To Send),n表示低电平有效。如果使能RTS流控制,当USART接收器准备好接收新数据时就会将nRTS变成低电平; 当接收寄存器已满时,nRTS将被设置为高电平。该引脚只适用于硬件流控制。
DE:“驱动器使能”用于激活外部收发器的发送模式,在 RS485 硬件控制模式下需要这个引脚,DE 和 nRTS 共用同一个引脚。
CTS:清除以发送(Clear To Send),n表示低电平有效。如果使能CTS流控制,发送器在发送下一帧数据之前会检测nCTS引脚, 如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。 该引脚只适用于硬件流控制。
CK:发送器时钟输出引脚。这个引脚仅适用于同步模式。
STM32MP157系统控制器有四个USART和四个UART,其中USART1的时钟来源于APB5时钟,其最大频率为133MHz, 其他七个的时钟来源于APB1时钟,其最大频率为104.5MHz。并且USART1外设是属于A7内核专享的,其他的串口 可以选择分配A7还是M4。
UART只是异步传输功能,所以没有CK、CTS和RTS功能引脚。
15.3.2. 数据寄存器¶
USART数据寄存器包含了两个寄存器,一个专门用于发送的可写USART_TDR, 一个专门用于接收的可读USART_RDR。 这两个寄存器只有低9位有效,并且第9位数据是否有效要取决于USART控制寄存器1(USART_CR1)的M位设置, 当M位为0时表示8位数据字长,当M位为1表示9位数据字长,我们一般使用8位数据字长。
USART_TDR和USART_RDR都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把USART_TDR内容转移到发送移位寄存器, 然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到USART_RDR。
USART支持DMA传输,可以实现高速数据传输,具体DMA使用将在DMA章节讲解。
15.3.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个停止位时,具体发送字符时序图见 图14_8。
图 14‑8 字符发送时序图
当发送使能位TE置1之后,发送器开始会先发送一个空闲帧(一个数据帧长度的高电平),接下来就可以往USART_TDR寄存器写入要发送的数据。 在写入最后一个数据后,需要等待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 |
发送完成中断使能 |
为得到一个信号真实情况,需要用一个比这个信号频率高的采样信号去检测,称为过采样, 这个采样信号的频率大小决定最后得到源信号准确度,一般频率越高得到的准确度越高, 但为了得到越高频率采样信号越也困难,运算和功耗等等也会增加,所以一般选择合适就好。
接收器可配置为不同过采样技术,以实现从噪声中提取有效的数据。USART_CR1寄存器的OVER8位用来选择不同的采样采样方法, 如果OVER8位设置为1采用8倍过采样,即用8个采样信号采样一位数据;如果OVER8位设置为0采用16倍过采样,即用16个采样信号采样一位数据。
USART的起始位检测需要用到特定序列。如果在RX线识别到该特定序列就认为是检测到了起始位。 起始位检测对使用16倍或8倍过采样的序列都是一样的。该特定序列为:1110X0X0X0000,其中X表示电平任意,1或0皆可。
8倍过采样速度更快,最高速度可达fPCLK/8,fPCLK为USART时钟,采样过程见下图。 使用第4、5、6次脉冲的值决定该位的电平状态。
16倍过采样速度虽然没有8倍过采样那么快,但得到的数据更加精准, 采样过程见下图。使用第8、9、10次脉冲的值决定该位的电平状态。
15.3.4. 波特率生成¶
波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,单位为波特。比特率指单位时间内传输的比特数, 单位bit/s(bps)。对于USART波特率与比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。
USART的发送器和接收器使用相同的波特率。计算公式如下:
其中,usart_ker_ckpres 为USART时钟;USARTDIV为USART_CR1寄存器的OVER8位对应的值, USARTDIV是一个存放在波特率寄存器(USART_BRR)的一个无符号定点数。16 倍或 8 倍过采样时, USARTDIV 必须大于或等于 16d。
当需要计算波特率代入公式计算即可。 波特率的常用值有2400、9600、19200、115200,除非使用寄存器编程,不然很少需要我们自己手动计算波特率。
15.3.5. 校验控制¶
STM32MP157系列控制器USART支持奇偶校验。当使用校验位时,串口传输的长度将是8位的数据帧加上1位的校验位总共9位, 此时USART_CR1寄存器的M位需要设置为1,即9数据位。将USART_CR1寄存器的PCE位置1就可以启动奇偶校验控制, 奇偶校验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收数据时自动验证校验位。 接收数据时如果出现奇偶校验位验证失败,会见USART_ISR寄存器的PE位置1,并可以产生奇偶校验中断。
使能了奇偶校验控制后,每个字符帧的格式将变成:起始位+数据帧+校验位+停止位。
15.3.6. 中断控制¶
USART有多个中断请求事件,具体见下表。
表 14‑4 USART中断请求
中断事件 |
事件标志 |
使能控制位 |
发送数据寄存器为空 |
TXE |
TXEIE |
CTS标志 |
CTS |
CTSIE |
发送完成 |
TC |
TCIE |
准备好读取接收到的数据 |
RXNE |
RXNEIE |
检测到上溢错误 |
ORE |
RXNEIE |
检测到空闲线路 |
IDLE |
IDLEIE |
奇偶校验错误 |
PE |
PEIE |
断路标志 |
LBD |
LBDIE |
多缓冲通信中的噪声标志、上溢错误和帧错误 |
NF/ORE/FE |
EIE |
15.4. USART收发通信实验¶
USART只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留USART接口来实现与其他模块或者控制器进行数据传输, 比如GSM模块,WIFI模块、蓝牙模块等等。在硬件设计时,注意还需要一根“共地线”。
我们经常使用USART来实现控制器与电脑之间的数据传输。这使得我们调试程序非常方便,比如我们可以把一些变量的值、函数的返回值、 寄存器标志位等等通过USART发送到串口调试助手,这样我们可以非常清楚程序的运行状态,当我们正式发布程序时再把这些调试信息去除即可。
我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给控制器, 控制器程序根据接收到的数据进行下一步工作。
首先,我们来编写一个程序实现开发板与电脑通信,在开发板上电时通过USART发送一串字符串给电脑,然后开发板进入中断接收等待状态, 如果电脑有发送数据过来,开发板就会产生中断,我们在中断服务函数接收数据,并马上把数据返回发送给电脑。
15.4.1. 硬件设计¶
为利用USART实现开发板与电脑通信,需要用到一个USB转USART的IC,我们选择CH340G芯片来实现这个功能, CH340G是一个USB总线的转接芯片,实现USB转USART、USB转IrDA红外或者USB转打印机接口, 我们使用其USB转USART功能。具体电路设计见 图14_12。
我们将CH340G的TXD引脚通过74LVC1G157再通过跳帽J16与UART4的RX引脚连接,CH340G的RXD引脚通过74LVC1G157 再跳帽J15与UART4的TX引脚连接。CH340G芯片集成在开发板上,其地线(GND)已与控制器的GND连通。如果我们想使用串口3, 那么就可以把开发板中的默认连接的跳帽拔掉,然后用杜邦线连接USART3与CH340G的通信接口就可实现通信, 这个就是用了跳帽的好处,不会把CH340G的通信引脚固定死。
图 14‑12 USB转串口硬件设计
在STM32MP157中,除了USART1是属于A7内核专属的,其他7个串口都能选择分配给M4还是A7。 由于UART4已经默认作为Linux信息输出,这里使用USART3作为本次串口实验。需要我们将J15 J16跳线帽拔掉后, 使用杜邦线将RXD_1连接到UART3_TX,将TXD_1连接到UART3_RX上,如下图所示。
先在底板的原理图上找UART3部分内容。
再从核心板上确定SART3所使用的引脚,可知USART3_TX和USART3_RX分别使用的是PB10以及PB12引脚上。
15.4.2. 软件设计¶
在Connectivity中选择需要配置的串口,在本节实验使用的是USART3。选择分配给Cortex-M4内核,并在模式中 选择Asynchronous异步模式。在Parameter Settings中可对串口进行配置,默认的设置为:波特率115200, 8位数据位,1位停止位,无奇偶校验位。这也是在串口中最常用的配置。可根据需要自行修改串口参数,除了这些 基本的参数配置之外还有一些更为高级的配置参数,本章节只讲解最常用的配置。
开启USART中断的方式很简单,只需要在NVIC中,将USART3的中断使能勾选即可。
开发板上的USART3_TX和USART3_RX引脚分别使用的是PB10及PB12, 配置完串口参数之后,需要选择USART3所在的引脚设置为串口模式。
将PB10引脚设置为USART3_TX模式
将PB12引脚设置为USART3_RX模式
在之前的步骤中我们已经将USART3分配给了Cortex-M4,此处便不需要重新配置引脚给M4内核了。
启用HSE时钟
将设置Cortex-M4的时钟为209Mhz
同时也可以选择调整USART3的时钟
到这里USART3的初始化设置已经完成,生成工程代码即可。 在我们进入代码中,看下USART3具体是怎么被初始化以及工作的之前,先了解下 USART初始化相关的结构体。
15.4.2.1. UART_HandleTypeDef结构体¶
串口使用UART_HandleTypeDef结构体来描述一个串口设备,部分成员如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | typedef struct {
USART_TypeDef *Instance; /*!< 串口外设基地址 */
UART_InitTypeDef Init; /*!< 串口初始化结构体 */
UART_AdvFeatureInitTypeDef AdvancedInit; /*!< 串口高级功能配置结构体 */
uint8_t *pTxBuffPtr; /*!< 发送数据存放的地址 */
uint16_t TxXferSize; /*!< 发送数据的大小 */
__IO uint16_t TxXferCount; /*!< 发送数据的个数 */
uint8_t *pRxBuffPtr; /*!< 存放数据的地址 */
uint16_t RxXferSize; /*!< 接受数据的大小 */
__IO uint16_t RxXferCount; /*!< 接受数据的个数 */
uint16_t Mask; /*!< 串口接受寄存器掩码 */
DMA_HandleTypeDef *hdmatx; /*!< 串口发送使能DMA的参数配置结构体 */
DMA_HandleTypeDef *hdmarx; /*!< 串口接受使能DMA的参数配置结构体 */
HAL_LockTypeDef Lock; /*!< 锁资源 */
__IO HAL_UART_StateTypeDef gState; /*!< 串口发送状态结构体以及 */
__IO HAL_UART_StateTypeDef RxState; /*!< 串口接受状态结构体 */
__IO uint32_t ErrorCode; /*!< 串口操作错误信息*/
void (*RxISR)(struct __UART_HandleTypeDef *huart); /*!< 接收中断处理函数上的函数指针 */
void (*TxISR)(struct __UART_HandleTypeDef *huart); /*!< 发送中断处理函数上的函数指针 */
} UART_HandleTypeDef;
|
1. Instance指针: 用于指向用户使用的串口寄存器基地址;
2. Init串口初始化结构体: 用于配置串口的通讯参数,如波特率、串口数据位数、停止位等等。详细参数说明,请看下面初始化结构体的分析;
3. AdvancedInit串口高级功能配置结构: 用于配置串口的高级功能,如自动波特率,MSB先行等等功能。本章节暂时用不到,所以不详细进行讲解;
4. pTxBuffPtr, TxXferSize,TxXferCount:分别是需要发送数据的地址指针,发送数据的大小以及需要发送的数据个数;
5. pRxBuffPtr, RxXferSize,RxXferCount:分别是指向存放数据的地址指针,接受数据的大小,接受数据的个数;
6. Mask: 串口接受寄存器的掩码,用于存放数据的校验位,与初始化结构体中的Parity参数有关;
7. hdmatx, hdmarx结构体:配置串口发送接受数据的DMA具体参数;
8. Lock:串口对象资源锁,该结构体主要负责分配锁资源,可选择HAL_UNLOCKED或者是HAL_LOCKED两个参数。如果gState的值等于HAL_UART_STATE_RESET, 则认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit函数来对串口的GPIO和时钟进行初始化,这部分的代码用于实现串口底层配置的功能。
9. gState,RxState:分别是串口的发送状态、工作状态的结构体和串口接受状态的结构体。HAL_UART_StateTypeDef是一个枚举类型, 列出串口在工作过程中的状态值,有些值只适用于gState,如HAL_UART_STATE_BUSY;
ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。
RxISR:函数指针,通常用于处理串口接收中断
TxISR:函数指针,通常用于处理串口发送中断
15.4.2.2. UART_InitTypeDef结构体¶
该结构体用于配置串口的通讯方式,内嵌于UART_HandleTypeDef结构体中,定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct {
uint32_t BaudRate; //波特率
uint32_t WordLength; //字长
uint32_t StopBits; //停止位
uint32_t Parity; //校验位
uint32_t Mode; //UART模式
uint32_t HwFlowCtl; //硬件流控制
uint32_t OverSampling; // 过采样设置,8倍或者16倍
uint32_t OneBitSampling; //采样值的位数
uint32_t ClockPrescaler; //时钟分频因子
} UART_InitTypeDef;
|
1. BaudRate:波特率设置。 一般设置为2400、9600、19200、115200。标准库函数会根据设定值计算得到USARTDIV值,见公式 20‑1,并设置USART_BRR寄存器值。
2. WordLength:数据帧字长, 可选8位或9位。它设定USART_CR1寄存器的M位的值。如果没有使能奇偶校验控制,一般使用8数据位;如果使能了奇偶校验则一般设置为9数据位。
3. StopBits:停止位设置, 可选0.5个、1个、1.5个和2个停止位,它设定USART_CR2寄存器的STOP[1:0]位的值,一般我们选择1个停止位。
4. Parity:奇偶校验控制选择, 可选USART_Parity_No(无校验)、USART_Parity_Even(偶校验)以及USART_Parity_Odd(奇校验),它设定USART_CR1寄存器的PCE位和PS位的值。
5. Mode:USART模式选择, 有USART_Mode_Rx和USART_Mode_Tx,允许使用逻辑或运算选择两个,它设定USART_CR1寄存器的RE位和TE位。
6. HwFlowCtl: 硬件流控制选择,只有在硬件流控制模式下才有效,可选有:使能RTS、使能CTS、同时使能RTS和CTS、不使能硬件流。
7. OverSampling : 过采样选择,选择8倍过采样或者16过采样。
8. OneBitSampling: 一个采样位方法使能。可选择三个采样位方法或者一个采样位方法。
9. ClockPrescaler: 串口时钟分频因子。默认选择不分频。
15.4.2.3. MX_USART3_UART_Init串口初始化函数¶
在了解了上面介绍的两个结构体之后,就可以进入MX_USART3_UART_Init串口初始化函数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | UART_HandleTypeDef huart3; //声明一个UART_HandleTypeDef类型结构体
void MX_USART3_UART_Init(void)
{
huart3.Instance = USART3; //选择串口3
huart3.Init.BaudRate = 115200; //波特率为115200
huart3.Init.WordLength = UART_WORDLENGTH_8B; //8位数据位
huart3.Init.StopBits = UART_STOPBITS_1; //1位停止位
huart3.Init.Parity = UART_PARITY_NONE; //无校验位
huart3.Init.Mode = UART_MODE_TX_RX; //接收发送模式
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE; //无硬件流控制
huart3.Init.OverSampling = UART_OVERSAMPLING_16; //采样设置
huart3.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; //采样值的位数
huart3.Init.ClockPrescaler = UART_PRESCALER_DIV1; //时钟分频系数为1
huart3.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; //UART高级特性的初始化参数
if (HAL_UART_Init(&huart3) != HAL_OK) //初始化usart3
{
Error_Handler();
}
//设置发送FIFO长度
if (HAL_UARTEx_SetTxFifoThreshold(&huart3, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
//设置接收FIFO长度
if (HAL_UARTEx_SetRxFifoThreshold(&huart3, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
//不是能FIFO
if (HAL_UARTEx_DisableFifoMode(&huart3) != HAL_OK)
{
Error_Handler();
}
}
|
在MX_USART3_UART_Init函数中的内容也是相对简单,对USART3进行一些基本的配置,并调用了HAL_UART_Init去初始化huart3。 HAL_UART_Init函数的功能跟我们直接讲解过得HAL_GPIO_Init函数是差不多的,都是对一些底层的寄存器进行初始化,有兴趣的读者 可按照我们直接讲解HAL_GPIO_Init函数的思路去跟踪下串口的寄存器是怎么初始化的。还有一点需要注意的是在HAL_UART_Init中 调用了HAL_UART_MspInit函数,对GPIO引脚以及时钟进行了初始化,HAL_UART_MspInit函数同样也是STM32CubeIDE为我们生成的。 详细代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
if(uartHandle->Instance==USART3)
{
if(IS_ENGINEERING_BOOT_MODE())
{
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_UART35;
PeriphClkInit.Uart35ClockSelection = RCC_UART35CLKSOURCE_PCLK1;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
__HAL_RCC_USART3_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Alternate = GPIO_AF8_USART3;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_NVIC_SetPriority(USART3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART3_IRQn);
}
|
在HAL_UART_MspInit函数的功能可分为设置串口时钟、引脚功能以及中断等。 相对串口的配置更为底层。
15.4.2.4. 串口的收发函数¶
在HAL库的收发函数中,经常使用的有以下两组收发函数
1 2 3 | //阻塞式串口收发函数
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
参数
huart : UART_HandleTypeDef类型结构体指针,包含着串口的基地址。
pData : 发送(或接收)数据的首地址。
Size : 发送(或接收)数据的大小。
Timeout : 函数超时时间。
返回值
成功: HAL_OK
失败: 当串口被占用时返回 HAL_BUSY ,超过Timeout时返回 HAL_TIMEOUT ,当pData或者Size参数错误时返回 HAL_ERROR 。
在调用这两个串口收发函数时,该函数会以阻塞的方式进行发送,当在超时时间结束时还没发送完成则退出该发送函数。
1 2 3 | //中断收发函数
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
|
参数
huart : UART_HandleTypeDef类型结构体指针,包含着串口的基地址。
pData : 发送(或接收)数据的首地址。
Size : 发送(或接收)数据的大小。
返回值
成功: HAL_OK
失败: 当串口被占用时返回 HAL_BUSY ,当pData或者Size参数错误时返回 HAL_ERROR 。
在使用中断的串口收发函数时,主要完成两种功能。
使能相对应的串口接收或发送中断。
对UART_HandleTypeDef中的RxISR函数指针或TxISR函数指针进行赋值。
完成中断收发之后关闭接收或发送中断使能,将UART_HandleTypeDef中的RxISR函数指针或TxISR函数指针 赋为NULL;
15.4.2.5. 中断服务函数¶
在了解串口是如何初始化以及串口收发函数接口之后,具体看下STM32CubeIDE中生成的中断服务函数为我们做了哪些工作, 我们是否在此基础上需要做一些其他的内容。
stm32mp1xx_it.c文件中保存着各种中断函数入口,其中USART3的中断服务函数也是在该文件, USART3_IRQHandler函数内容如下所示,
1 2 3 4 | void USART3_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart3);
}
|
在USART3的中断服务函数中调用了HAL_UART_IRQHandler函数, HAL_UART_IRQHandler函数多达两百多行,这里我们只关心接收及发送中断, 精简版HAL_UART_IRQHandler函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
/*传输错误中断处理代码省略*/
/*UART唤醒停止模式中断发生代码省略*/
/*发送FIFO为空中断代码省略*/
/*接收FIFO为满中断代码省略*/
/*接收中断*/
if (((isrflags & USART_ISR_RXNE_RXFNE) != 0U)
&& (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U)
|| ((cr3its & USART_CR3_RXFTIE) != 0U)))
{
if (huart->RxISR != NULL)
{
huart->RxISR(huart);
}
return;
}
/*发送中断*/
if (((isrflags & USART_ISR_TXE_TXFNF) != 0U)
&& (((cr1its & USART_CR1_TXEIE_TXFNFIE) != 0U)
|| ((cr3its & USART_CR3_TXFTIE) != 0U)))
{
if (huart->TxISR != NULL)
{
huart->TxISR(huart);
}
return;
}
}
|
通过HAL_UART_IRQHandler函数的内容,我们知道当接收中断产生时,huart->RxISR函数指针如果不为空,则将会被调用, 同样当发送中断产生时,huart->TxISR函数指针不为空,则会被调用。
15.4.2.6. 回调函数¶
处理中断服务函数之外,HAL还为我们提供了一系列的回调函数。在特定串口完成特定的功能时执行。
HAL为我们提供了丰富的串口回调函数,常用的回调函数有以下几种。
1 2 3 4 5 | void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);//已发送一半
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//已接收一半
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//错误处理回调函数
|
例如当串口发送完成之后会调用HAL_UART_TxCpltCallback回调函数(需要使用中断的串口收发函数),
当我们只需要使用定长的数据收发时,只需要使用HAL库为我们提供串口收发函数即可。 当我们需要不定长的数据接收时,可通过使用回调对数据进行处理。 例如在HAL_UART_Receive_IT设置发送数据长度设置为1,在HAL_UART_RxCpltCallback函数中, 再次调用HAL_UART_Receive_IT进行数据接收。
15.4.2.7. 添加文件及代码¶
在本工程中将添加bsp_debug_usart.h以及bsp_debug_usart.c两个文件,添加串口相关操作的代码。如回调函数, 另外的串口发送接收函数等。
bsp_debug_usart.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #ifndef BSP_DEBUG_USART_H_
#define BSP_DEBUG_USART_H_
#include "main.h"
#include "string.h"
extern UART_HandleTypeDef huart3;
#define RX_MAX 255 //串口接收的最大长度
#define UartHandle huart3
extern uint8_t rx_buf[RX_MAX]; //接收数据数组
extern uint8_t rx_flag; //当一帧数据接收完成之后置1
void Usart_Config(void); //配置函数
void Usart_SendString(uint8_t *str); //发送字符串
#endif /* BSP_DEBUG_USART_H_ */
|
第4-5行,包含了main.h及string.h头文件,在我们自己添加的头文件中,只要包含了main.h即可使用各种HAL库相关的内容。 string.h文件,则包含了字符串相关处理的内容。
第7行,huart3句柄定义在usart.c文件中,需要使用extern才能引用外部变量。
第9-10行,定义串口接收的最大长度以及串口宏。
第12-13行,声明rx_buf数组以及rx_flag变量能够被其他文件引用。
第15-16行,提供了两个对外的接口函数,一个用于配置串口,一个用于发送字符。
bsp_debug_usart.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | #include "./usart/bsp_debug_usart.h"
uint8_t rx_buf[RX_MAX]; //串口接收数据存储
uint8_t rx_flag = 0; //当一帧数据接收完成之后置1
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //接收完成回调函数
{
static uint8_t *ucTemp = rx_buf;
//接收到字符串结束符时,并重新接收
if( *ucTemp == '\n')
{
ucTemp++;
*ucTemp = '\0'; //添加字符串结束符
ucTemp = rx_buf;
HAL_UART_Receive_IT(&UartHandle, ucTemp, 1);
rx_flag = 1;
}
else
{
ucTemp++;
HAL_UART_Receive_IT(&UartHandle, ucTemp, 1);
}
}
void Usart_Config(void) //配置函数
{
//启用串口接收
HAL_UART_Receive_IT(&UartHandle, rx_buf, 1);
}
//串口发送函数
void Usart_SendString(uint8_t *str)
{
unsigned int k=0;
while (*(str + k)!='\0')
{
HAL_UART_Transmit( &UartHandle,(uint8_t *)(str + k) ,1,HAL_MAX_DELAY);
k++;
}
}
|
第3-4行,声明了rx_buf串口接收数组,用于存储串口接收到的数据。rx_flag变量用于存储接收完成标志。
第28-32行,串口配置函数,启用串口的中断接收, 此处我们将接收到的字符大小设置为1个字节。第一个接收到的数据将被存储到rx_buf的首地址中。
第6-26行,HAL_UART_RxCpltCallback串口接收回调函数,当串口接收完成之后会调用该函数。 在函数中定义了一个局部静态变量指针ucTemp,指向rx_buf的地址。当接收的字符为’n’(回车)时, 表示一帧数据传输结束。在接收到的数据末尾添加’0’字符串结束符。并将ucTemp重新指向rx_buf 的首地址,重新开始接收数据。并将接收标志位rx_flag设置为1; 当接收到的数据不为’n’(回车)时,ucTemp变量自加指向rx_buf的下一个字符,接收新数据。
第35-42行,定义了一个新的串口发送函数,用于发送字符串。内部逻辑很简单,使用单个字符发送, 直到遇到字符串结束符’0’停止发送。
此处的串口接收回调函数适合在对于串口接收数据量不是很大的场景,当需要使用大量的串口数据收发时推荐 自己重新编写整个中断服务函数或者使用DMA的方式进行串口数据的收发,关于DMA的使用将在下节内容中介绍。
重定向printf函数
在C语言中我们最常用的标准输入函数非printf莫属,为了能够使我们也能够在STM32MP157上使用printf函数, 我们需要对__io_putchar函数进行重定向。同样在bsp_debug_usart.c文件上添加以下重定向函数
1 2 3 4 5 6 | //重定义printf函数
int __io_putchar(int ch)
{
HAL_UART_Transmit(&UartHandle, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
|
如果我们想要打印浮点数据类型,还需要在设置上做以下操作
选择CM4工程,点击鼠标右键,并选择最后一个Pronperties
勾选以下两个选项
现在即可像在PC电脑一样在板子上方便得使用printf打印函数了。
main_task.c
在main_task中实现应用功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void Main_Config(void) //配置函数
{
//启动串口接收函数
Usart_Config();
}
void Main_Task(void) //主要的任务函数
{
if(rx_flag == 1)
{
printf((char*)rx_buf);
//HAL_UART_Transmit_IT(&UartHandle, rx_buf, strlen((char *)rx_buf));
rx_flag = 0;
}
}
|
在Main_Config函数中调用了Usart_Config串口接收函数。
在Main_Task函数中,判断rx_flag是否为1, 当rx_flag标志位为1即说明当前收到了一帧数据,并将收到的数据打印出来。
15.4.2.8. 主函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | int main(void)
{
HAL_Init();
if(IS_ENGINEERING_BOOT_MODE())
{
SystemClock_Config();
}
MX_GPIO_Init();
MX_USART3_UART_Init();
Main_Config();
printf("--------野火stm32mp157 Cortex-M4内核串口实验----------\n");
while (1)
{
Main_Task();
}
}
|
main函数的内容基本上保持不变, 在while循环前添加Main_Config()函数,并打印 “——–野火stm32mp157 Cortex-M4内核串口实验———-”, 最后在while循环中添加Main_Task()任务的主体函数。
15.4.3. 下载验证¶
打开野火调试助手(其他串口助手也可以),介绍如下
根据开发板上设置的串口参数设置串口助手,端口号以自己电脑上为准。如果中文显示乱码,请将编码设置为UTF-8模式。 勾选左下角的发送新行会在每次发送数据后自动发送回车。
将编译好的程序编译好之后下载到开发板中,当板子开始运行时,会打印以下第一条信息,当我们发送一条信息时, 板子上回显相同的信息,如下图所示。