搜索
bottom↓
回复: 0

《STM32MP1 M4裸机HAL库开发指南》 第二十章 串口通信实验

[复制链接]

出0入234汤圆

发表于 2022-11-4 10:02:56 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-11-4 17:11 编辑

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png

第二十章 串口通信实验


        本章节我们来学习STM32MP1的串口使用方法,并通过串口发送和接收数据。
        本章将分为如下几个小节:
        20.1、串口简介;
        20.2、STM32MP1串口简介;
        20.3、HAL库中串口相关的API;
        20.4、串口中断接收回显实验;
        20.5、编译和测试;
        
20.1 串口简介

在学习串口前,我们先来了解一下数据通信的一些基础概念。
20.1.1 数据通信的基础概念
在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们最常见的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。
1. 数据通信方式
按数据通信方式分类,可分为串行通信和并行通信两种。串行和并行的对比如下图所示:
第二十章 串口通信实验411.png
图20.1.1. 1数据传输方式

        串行通信的基本特征是数据逐位顺序依次传输,最少只需一根传输线即可完成,所需传输线少成本低,但每次只能传送1bit的信号,比如1Byte的信号需要8次才能发完,传输速率低。串行通讯抗干扰能力强,可用于远距离传输,传输距离可以从几米到几千米。
并行通信是数据各位可以通过多条线同时传输,一次传送8bit、16bit、32bit甚至更高的位数,相应地就需要8根、16根、32根甚至更多的信号线,优点是传输速率高,缺点就是线多成本就高了,抗干扰能力差,因而适用于短距离、高速率的通信。
2. 数据传输方向
根据数据传输方向,通信又可分为全双工、半双工和单工通信,它们的比较如下图所示:
第二十章 串口通信实验747.png
图20.1.1. 2数据传输方式

        单工是指数据传输仅能沿一个方向,不能实现反方向传输;
        半双工是指数据传输可以沿着两个方向,但是需要分时进行;
        全双工是指数据可以同时进行双向传输;
        这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信(分时进行),而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。
3. 数据同步方式
根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:
第二十章 串口通信实验998.png
图20.1.1. 3数据同步方式

同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。其优点是:可以实现高速率、大容量的数据传输,以及点对多点传输。缺点是:要求发生时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。
异步通信不需要时钟信号,而是在传输的数据信号中加入起始位和停止位等一些同步信号,以使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。其优点是:没有时钟信号硬件简单,双方时钟可允许一定误差。缺点是:通信速率较低,只适用点对点传输。
4. 通信速率
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率(Rb):每秒钟传输的信息量,即每秒钟传输的二进制位数,通常用Rb表示,单位为bit/s(即比特每秒),因而又称为比特率。
传码率(RB):每秒钟传输的码元个数,通常用RB表示,单位为Bd或Baud(即波特每秒),因而又称为波特率。
        比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们看看波特率。波特率被传输的是码元,码元是信息被调制后的概念,一个码元可以被表示成多个二进制的比特信息。它们的关系可以用公式Rb=RBlog2M表示,M表示M进制码元。
在二进制系统中,波特率在数值上和比特率相等,但是其意义上不同。例如:以每秒50个二进制位数的速率传输时,传信率为50bit/s,传码率也为50Bd。这是在无调制的情况下,比特率和波特率的数值相等。代入公式:Rb=RBlog2M=RB,M=2。
如果码元是在十六进制系统的话,即使用调制技术的情况下,代入公式:Rb=RBlog2M=4RB,M=16。比如波特率为100Bd,在二进制系统中,比特率为100 bit/s;那么在四进制系统中,比特率为400 bit/s,即1个十六进制码元表示4个二进制数,可见一个码元可以表示多个比特。
        注:比特率的单位也常用bps来表示,表示每秒传输的比特数(bit per second)。波特率的单位是波特(Baud)
20.1.2 串口通信接口标准
常用的串行通信接口标准中,有RS-232、RS-422和RS-485接口。
1. 接口
(1)RS-232
电脑机箱后面一般会有2个9芯或者25芯的接口,也分别叫DB9和DB25连接器,它们是符合RS-232C标准的串行接口,可用于连接MODEM设备或者工业仪器仪表等,实现设备与电脑相互通迅。我们先来看看RS232接口,下图是DB9针的连接头,左边公头的1脚接的母头的1脚,其它脚也按数字对应相连。
第二十章 串口通信实验2122.png
图20.1.2. 1 RS232接口

RS-232C定义了数据终端设备(DTE)和数据通信设备(DCE)之间串行二进制数据交换接口技术标准,我们以DB9连接器为例,此标准接口的主要引脚定义如下表:
lQLPJxbXPsr3wdDMt80B_rAYF6fui8M_2gNhuWRCQPsA_510_183.png
表20.1.2. 1 RS232接口引脚信号

使用RS-232,对于一般双工通信,仅需要一条发送线、一条接收线和一条地线就可以实现,这3根线构成共地传输,容易产生共模干扰,抗噪声抗干扰性弱,所以使用RS-232最大通信距离为一般为15m。RS-232只允许一对一通信,以两个计算机通信为例,如果是近距离通信,两个接口可以直连:
第二十章 串口通信实验2998.png
图20.1.2. 2两台设备近距离通信

如果是长距离通信,则需要通过数据通信设备,如调制解调器来实现。调制解调器是实现数字信号和模拟信号相互转换的设备。当计算机通过电话线连入互联网时,发送方的计算机发出的数字信号,通过调制解调器转换成模拟信号在电话网上传输;接收方的计算机通过调制解调器将传输过来的模拟信号转换成数字信号。通过调制解调器,两台电脑实现通信。
第二十章 串口通信实验3206.png
图20.1.2. 3两台设备远距离通信

(2)RS-422
RS-422采用4线(2根发送、2根接收,如加上地线就5根)、差分方式传输,通过两对双绞线可以实现全双工工作收发。RS-422是一种单机发送、多机接收的双向、平衡传输,即总线上只允许一个主设备,可以有多个从设备,从设备之间不能通信。RS-422输出驱动器为双端平衡驱动器,具有电压放大的作用,且差分电路具有较强的抗干扰能力,所以RS-422的传输距离较长,可达几十米到上千米。
(3)RS-485
RS-485是RS-422的变形,RS-485使用的是一对双绞线,可实现半双工工作,不过,随着技术的发展,RS-485已经有4线方式的了,4线的通信和RS-422一样具有实现多点、双向通信的能力。RS-485采用平衡发送和差分接收的方式,抗噪声干扰性好,传输距离可以达几十米到上千米。
第二十章 串口通信实验3701.png
图20.1.2. 4 RS485通信

下表中列出RS-232、RS-422和RS-485的对比:
lQLPJxbXPuI2rinMvc0Ce7C6Icw6p34u7gNhuYo7gJEA_635_189.png
表20.1.2. 2 RS-232、RS-422和RS-485通信对比

2. 电气标准
        在电子设计中,要保证不同设备的信号之间的通讯,就会涉及到通信协议以及电平标准,电平标准有很多,常见的电平标准有TTL、RS232、RS485、COMS和LVDS等。TT(Transistor-Transistor Logic),即晶体管逻辑集成电路,TTL电平是数字电路的一种通用接口电平标准,TTL电平信号直接与集成电路连接,下面我们以一张表格来区分串行通信的电平标准。
lQLPJxbXPuxYpKhVzQLQsFkex0hsrC03A2G5miaAEAA_720_85.png
lQLPJxbXPver01rNARrNAoOwCX-AJ8wkePQDYbmtDgD7AA_643_282.png
表20.1.2. 3电气标准

20.1.3 串口通信协议简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和按位接收字节,一个字节(Byte)有8位,串口必须把这8位依次发送出去才可以完成一个字节的发送,比起按字节发送的并行通信速率要慢,效率较低,但是所需的线少,成本低,通信较稳定。
第二十章 串口通信实验5020.png
图20.1.3. 1串行通信和并行通信

串口通信协议是指规定了数据包的内容,双方需要约定一致的数据包格式才能正常收发数据。在串口通信中,常用的协议包括RS-232、RS-422和RS-485等。
随着科技的发展,RS-232在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB转串口了。现在的很多电脑已经没有DB9接口了,取而代之的是 USB 接口,所以就催生出了很多 USB转串口TTL 芯片,比如 CH340、PL2303 等。通过这些芯片就可以实现串口 TTL 转 USB。STM32的串口输出的是TTL电平信号,如果需要RS-232标准的信号可使用MAX3232芯片换成232电平以后,再通过一根USB转串口线与PC机的串口连接来实现通信。
第二十章 串口通信实验5379.png
图20.1.3. 2 USB转串口线

正点原子STM32MP157开发板就是用CH340C芯片来完成串口UART和电脑之间的通信的,开发板引出Type-C接口,硬件连接上非常方便,只需要一根Type-C 线就可以。理论上,USB2.0版本,Type-C接口最大传输速率为480Mbps,而USB3.0以上版本可以达到5Gbps或者更高的传输速率。如下图位号为USB_TTL的接口(Type-C接口)是我们后面串口实验要用到的接口。
第二十章 串口通信实验5624.png
图20.1.3. 3正点原子STM32MP157开发板USB_TTL接口

下面我们来学习串口通信协议,这里主要学习串口通信的协议层。
串口通信的数据包由发送设备的TXD接口传输到接收设备的RXD接口。在串口通信的协议层中,规定了数据包的内容,它由启始位、主体数据位、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如下图所示。
第二十章 串口通信实验5831.png
图20.1.3. 4串口通信协议数据帧格式

串口通信协议数据包组成可以分为波特率和数据帧格式两部分。
1. 波特率
串口通信分为同步通信(UART)和异步通信(USART),本章主要讲解的是串口异步通信(USART),异步通信是不需要时钟信号的,但是需要约定两个设备的波特率一致才可以正常通信。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同,常见的波特率是4800、9600、115200等。
根据波特率可以计算出每帧内部数据位各位之间的时间间隔(不是帧与帧之间的时间间隔),1s除以波特率可得时间,例如波特率为115200的时间间隔为1s /115200(波特率) = 8.68us。如果是9600波特率,则时间间隔约为104.2us。
2. 数据帧格式
数据帧格式需要我们提前约定好,串口通信的数据帧包括起始位、停止位、有效数据位以及校验位。我们来看看这些数据位:
起始位和停止位
串口通信的一个数据帧是从起始位开始,直到停止位,通信的双方要约定一致。
数据帧中的起始位是由一个逻辑0 的数据位表示,当发出一个逻辑 0 信号,表示传输字符的开始。
数据帧的停止位是数据的结束标志,它可以用 0.5、1、1.5或 2个逻辑1的数据位表示(高电平)。
有效数据位
数据帧的起始位之后,接着就是数据位,也称有效数据位,这就是我们真正需要的数据,有效数据位可以是5、6、7或者8个位长构成一个字符,通常采用ASCII码表示,有效数据位从最低位开始传送,低位(LSB)在前,高位(MSB)在后。
校验位
校验位可以认为是一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验方法有:奇检验、偶检验、0检验、1检验以及无检验。下面分别介绍一下:
奇校验是指有效数据位和校验位中是“1”的个数为奇数,比如一个 8 位长的有效数据为:10101001,总共有 4 个“1”,为达到奇校验效果,校验位应设置为“1”,最后传输的数据是8位的有效数据加上 1 位的校验位总共 9位。
偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,为达到偶校验效果,所以偶校验位为“0”。
0 校验是指不管有效数据中的内容是什么,校验位总为“0”。
1 校验是校验位总为“1”。
无校验是指数据帧中不包含校验位。
我们一般是使用无检验位的情况。
空闲位
这里还注意到,在数据帧与帧之间有空闲信号,我们也称之为空闲位,空闲位处于逻辑 1 状态(高电平),表示当前线路上没有数据在传送,空闲位的长度是不定的。如下图是异步串口通信中,帧与帧之间的空闲间隔是随机的。
第二十章 串口通信实验7005.png
图20.1.3. 5空闲信号是不定长的

20.2 STM32MP1串口简介
20.2.1 STM32MP157串口资源

1. 串口功能
STM32MP157的串口资源相当丰富,功能也相当强劲:支持8/16倍过采样、支持自动波特率检测、支持Modbus通信、支持同步单线通信和半双工单线通讯、支持LIN(局域互连网络)、支持FIFO模式、支持调制解调器操作、智能卡协议和IrDA SIR ENDEC规范、具有DMA等等。支持的功能有很多,我们后面的实验主要是使用串口接收中断。
STM32MP1的串口分为两种:USART(即通用同步收发器)和UART(即通用异步收发器),其中USART有USART1/2/3/6,而UART有UART4/5/7/8。UART是在 USART基础上裁剪掉了同步通信功能,只剩下异步通信功能。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用串口通信基本都是异步通信。如下表是USART和UART的功能对比表。
lQLPJxbXPwhcTWvNAS7NAcuwu3Hqn81gfGkDYbnIeACRAA_459_302.png
图20.2.1. 1STM32MP157的USART和UART的功能对比

2. 串口时钟
STM32MP1的每一路串口的时钟均可以来自于HSE、CSI和HSI、PLL4Q。而USART1时钟还可以来源于APB5和PLL3Q,USART2/3以及UART4/5/7/8的时钟还可以来自于APB1,USART6的时钟还可以来自于APB2,如下时钟树的部分截图所示(图中USART和UART标注的不是很正确)
第二十章 串口通信实验7967.png
图20.2.1. 2 STM32MP157的串口时钟来源

USART1只供Cortex-A7使用,而其余的串口Cortex-A7和Cortex-M4均可以使用,其中USART1的频率最大为133MHz,其余的6个串口的时钟最大为104.5MHz。其中,UART2/3/4/5/7/8挂在APB1总线上,UART6挂在APB2总线上,UART1挂在APB5总线上。下图是参考手册中串口数据的部分截图。
第二十章 串口通信实验8196.png
图20.2.1. 3 STM32MP157的串口时钟来源

20.2.2 USART寄存器
STM32MP1的串口和其它STM32串口一样,只要你开启了串口时钟,并设置相应IO口的模式,然后配置波特率、数据位长度、奇偶校验位等信息,就可以使用了。下面,我们就简单介绍下几个与串口基本配置直接相关的寄存器。
1. 串口时钟使能寄存器
串口作为STM32MP157的一个外设,其时钟由APB1外设时钟使能寄存器RCC_MC_APB1ENSETR控制,我们后面实验中用到的串口4是在RCC_MC_APB1ENSETR寄存器的第16位,只要对该位写1即可使能串口4的时钟。在后面的实验中,通过在STM32CubeMX上配置,系统会自动为我们完成这步操作。
第二十章 串口通信实验8548.png
图20.2.2. 1 APB1外设使能MCU设置寄存器

2. 串口波特率设置寄存器
第二十章 串口通信实验8619.png
图20.2.2. 2串口波特率设置寄存器

串口波特率由USART_BRR寄存器来设置,它属于可读可写寄存器,仅低0~15位有效,其它位保留,仅当禁用USART(UE = 0)时才能写入该寄存器,在自动波特率检测模式下,该位由硬件自动更新。前面我们也说了,USART_BRR寄存器的第4~15位,BRR[15:4] = USARTDIV[15:4],剩下的第0~3位分情况:
当OVER8 = 0,BRR[3:0] = USARTDIV[3:0]。
当OVER8 = 1,BRR[2:0] = USARTDIV[3:0],右移1位。(BRR [3]必须保持清除状态,即为0)。
3. 串口控制寄存器
参考手册中将串口控制寄存器USART_CR1分为两种情况来介绍,一种是在启用FIFO模式下,另一种是在禁用FIFO模式下。这里我们以启用FIFO模式下的情况来介绍。
第二十章 串口通信实验9030.png
图20.2.2. 3串口控制寄存器

串口控制寄存器32位可设,大部分位可以由软件置1或者清除,我们这里仅分析常用的几位,其它位需要的时候可以再根据参考手册来分析。
第0位,UE为USART使能位。将UE位置1表示使能串口,清0表示禁止USART输出,并进入低功耗模式。
第2位,RE接收器使能位。0:禁止接收器; 1:使能接收器并开始搜索起始位。
第3位,TE表示发送使能位。 0:禁止发送器; 1:使能发送器。
第4位,IDLE 中断使能。0:禁止中断;1:当 USART_ISR 寄存器中的 IDLE=“1”时,生成 USART 中断。
第5位,RXNEIE/RXFNEIE接收数据寄存器非空/RXFIFO 非空中断使能。0:禁止接收器;1:当 USART_ISR 寄存器中的 ORE=“1”或 RXNE/RXFNE=“1”时,生成 USART 中断。
第6位,TCIE表示传输完成中断使能位。0:禁止中断;1:当 USART_ISR 寄存器中的 TC=“1”时,生成 USART 中断。
第7位,TXFNFIE表示发送数据寄存器为空/TXFIFO 未满中断使能,0:禁止中断;1:每当USART_ISR寄存器中的TXFNF = 1时,都会产生USART中断。
第8位,PEIE属于PE中断使能。0:表示禁止中断;1:当 USART_ISR 寄存器中的 PE=“1”时,生成 USART 中断
第9位,PS为校验位选择位。0:偶校验;1:寄校验。仅当禁用USART(UE=0)时,才可以写此位。
第10位,PCE表示校验启用位。0:禁用奇偶校验控制,1:启用奇偶校验控制。仅当禁用USART(UE = 0)时才能写入该位。这里,如果启用了奇偶校验位的话,将计算出的奇偶校验位插入到MSB位置(如果M = 1,则为第9位;如果M = 0,则为第8位),并在接收到的数据上检查奇偶校验。
第12和第28位,M0、M1为字长设置位,M[1:0]=00,表示1个起始位,8个数据位,n个停止位;M[1:0]=01,表示1个起始位,9个数据位,n个停止位;M [1:0]  ='10'表示1个起始位,7个数据位,n停止位;该位只能在禁止USART(UE = 0)时才可以写入,这里,n个停止位,n的个数,由USART_CR2的[13:12]位控制。
第15位,OVER8为过采样模式设置位。0:16倍过采样;1:8倍过采样。
第29位,FIFOEN为FIFO 模式使能位,0:禁止 FIFO 模式;1:使能 FIFO 模式。只有在禁止 USART(UE=“0”)时才能写入此位。
第30位,TXFIFO 为空时中断使能位。0:禁止中断;1:当 USART_ISR 寄存器中的 TXFE=“1”时,生成 USART 中断。
第31位,RXFIFO 变满时中断使能位。0:禁止中断;1:当 USART_ISR 寄存器中的 RXFF=“1”时,生成 USART 中断。
4. 数据发送与接收寄存器
STM32MP157的串口数据发送和数据接收由两个不同的寄存器组成,发送的数据在USART_TDR寄存器中,接收的数据在USART_RDR寄存器中,我们来看看这两个寄存器:
第二十章 串口通信实验10389.png
图20.2.2. 4数据发送与接收寄存器

        USART_RDR的其它位保留,只有第0~8位可读,其包含接收到的数据字符,具体多少位,由前面介绍的M[1:0]决定。类似的,USART_TDR的第0~8位包含要传输的数据字符。当我们需要发送数据的时候,往USART_TDR寄存器写入想要发送的数据,就可以通过串口发送出去了。而当有串口数据接收到,需要读取出来的时候,则必须读取USART_RDR寄存器的置。
我们前面有说过,在有效数据位中,一般是低位(LSB)在前,高位(MSB)在后,在启用奇偶校验的情况下进行接收时,在MSB位中读取的值为接收的奇偶校验位,什么意思呢?即当使能校验位(USART_CR1中PCE位被置1)进行发送时,写到MSB的值会被后来的校验位取代,当使能校验位进行接收时,读到的MSB位是接收到的校验位。对于启用奇偶校验的情况,USART的数据帧格式如下表所示:
lQLPJxbXPxTgoah-zQIDsI5uYgLAIu4wA2G53NuAEAA_515_126.png
表20.2.2. 1串口数据帧格式表

例如M[1:0]=00,PCE=1,停止位为1位的时候,数据有8位,不过第7位的值被校验位改写了,第7位是校验位了。数据位为7位和9位的情况也是类似。
第二十章 串口通信实验11122.png
图20.2.2. 5 MSB位会被后来的校验位改写

5. 串口状态寄存器
串口状态通过状态寄存器USART_ISR读取,其各位描述如图所示:
第二十章 串口通信实验11222.png
图20.2.2. 6串口状态寄存器

注:
        可以在启用FIFO模式和禁用FIFO模式中使用同一寄存器。其中,如果启用FIFO /智能卡模式,则X = 2,如果启用了FIFO并且禁用了智能卡模式,则X = 0。
关于USART_ISR,我们也是介绍常用的几位,其它位大家可以根据参考手册来查阅。
第0位,PE表示校验错误位。0:无奇偶校验错误;1:奇偶校验错误。当在接收器模式下发生奇偶校验错误时,此位由硬件设置,在USART_ICR寄存器的第0位PECF中写入1即可将此位软件清除。
第1位,FE表示帧错误位。0:未检测到帧错误;1:检测到帧错误或 break 字符。当检测到帧错误时,该位将由硬件置位,在USART_ICR寄存器的第1位FECF写入1即可将此位软件清除。
第4位,检测到空闲线路。0:未检测到空闲线路;1:检测到空闲线路。当检测到空闲线路时,该位由硬件置 1,如果响USART_CR1 寄存器中的IDLEIE写1,可以清除此位。
第5位,RXNE表示读取数据寄存器非空/RXFIFO 非空。0:未接收到数据;1:已准备好读取接收到的数据。当RXFIFO不为空时,硬件会将RXFNE位置1,表示已经有数据被接收到了,并且可以从USART_RDR寄存器中读取数据,当RXFIFO为空时,RXFNE的值被硬件清0,也可以软件上通过将1写入USART_RQR寄存器中的RXFRQ位来清除RXFNE标志。
第6位,TC(传输完成),当该位被置位的时候,表示USART_TDR内的数据已经被发送完成了(如果TE位被复位且没有正在进行的传输,则TC位将立即置1),如果为0,表示没有传输完成。该位也有两种软件清零方式:
1)读USART_ISR,写USART_TDR。
2)向USART_ICR寄存器的TCCF位写1。
寄存器的介绍就到此了,因为寄存器位数比较多,每位控制的功能还不一样,文档里不能把每一位都介绍一遍,而且也是记不住的,所以,我们在需要的时候再查看参考手册。
第7位,TXFNF表示发送数据寄存器为空/TXFIFO 未满。0:数据寄存器已满/发送 FIFO 已满;1:数据寄存器/发送 FIFO 未满。
第15位,ABRF表示:自动波特率标志。如果已设置自动波特率,或者自动波特率操
作未成功完成时,此位由硬件置 1。
第16位,BUSY表示忙标志。0:USART 处于空闲状态(无接收);1:正在接收。
第23位,TXFIFO 为空。0:TXFIFO 非空;1:TXFIFO 为空。
第24位,RXFIFO 已满。0:RXFIFO 未满;1:RXFIFO 已满。
第26位,RXFT是RXFIFO 阈值标志。0:接收 FIFO 未达到编程的阈值;1:接收 FIFO 已达到编程的阈值。
第27位,TXFT为TXFIFO 阈值标志。0:TXFIFO 未达到编程的阈值;1:TXFIFO 已达到编程的阈值。
关于寄存器我们就介绍到这里了,遇到寄存器配置不要慌,HAL库都为我们处理好了底层驱动,我们只是从寄存器层面去理解USART是怎样控制串口工作的,下面,我们来看看USART的框图。
20.2.3 USART框图
下面我们先来学习USART框图,通过USART框图引出USART相关的知识点,对之后的编程也会有一个清晰的思路。
D9188351-D4CA-422b-80B7-2207E70144AF.png
图20.2.3. 1 USART框图

为了方便大家理解,我们把整个框图用几根红线分成几个部分来介绍。
1. 时钟与波特率
        框图中①区域主要功能就是为USART提供时钟以及配置波特率。
时钟:
在USART框图中,可以看到有两个时钟域:usart_pclk 时钟域和usart_ker_ck 内核时钟域。        (1)usart_pclk是外设总线时钟,需要访问 USART 寄存器时,该信号必须有效。
(2)usart_ker_ck是USART时钟源,独立于 usart_pclk,由RCC提供。因此,即使usart_ker_ck 时钟停止,也可以连续对 USART 寄存器进行读/写操作。
usart_pclk和usart_ker_ck之间没有约束,usart_ker_ck可以比usart_pclk更快或更慢,唯一的限制是软件足够快地管理通信的能力。当USART在SPI从模式下工作时,它使用从外部主SPI设备提供的外部SCLK信号得出的串行接口时钟来处理数据流,usart_ker_ck时钟必须至少比CK输入上的时钟快3倍。
波特率:
波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),比特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。波特率的计算公式分为16倍过采样和8倍过采样:
在16倍过采样(或者在LIN模式)的情况下,波特率通过以下公式得出:
第二十章 串口通信实验13264.png
在8倍过采样的情况下,波特率通过以下公式得出:
第二十章 串口通信实验13290.png
以上公式中,Tx/Rx baud表示串口的波特率;usart_ker_ckpres为真正到USART的时钟频率(uart_ker_ck_pres是UART输入时钟除以预分频值后的数值),在usart_ker_ck不分频的情况下,USART1的时钟频率最大为133MHz,其余6个串口的时钟频率最大为104.5MHz。
USARTDIV是一个存放在USART_BRR寄存器中的无符号定点数,其值和USART_CR1寄存器的第15位有关,USART_CR1寄存器的第15位为OVER8,用于设置采样模式。为了从噪声中提取到一个有效目标信号,一般会采用一个频率比较高的信号去采样目标信号,如8倍过采样和16倍过采样,倍数越高,采样的次数越多,采样速度越慢,但是采样后得到的数据越准确。如8倍过采样的速度高达:usart_ker_ck_pres/8。
当OVER8值为0时,表示16倍过采样,此时BRR = USARTDIV;
当OVER8值为1时,表示8倍过采样,此时BRR[2:0] = USARTDIV[3:0],相对于OVER8值为0的情况,这里BRR右移了1位,变成BRR[2:0],此时BRR[3]必须保持清零;
不管是16倍还是8倍,BRR[15:4]都等于USARTDIV[15:4],且USARTDIV都必须大于或等于 16(10进制)。
下面举个例子说明来说明此公式:
当在16倍过采样时,如果需要得到 115200 的波特率,此时usart_ker_ckpre =104.5MHZ,那么可得:
第二十章 串口通信实验13946.png
计算出USARTDIV = 907(10进制),可解得 BRR = USARTDIV =388(16进制),那么需要设置USART_BRR 的值为 0x388。这里的USARTDIV是有余数的,我们用四舍五入进行取整,这样会导致波特率会有所偏差,而这样的小误差是可以被允许的。8倍过采样计算方法类似。
2. 收发数据
框图中②区域部分包含了USART双向通信需要的两个引脚:
TX:发送数据输出引脚。
RX:接收数据输入引脚。采用过采样技术进行数据恢复,用于区分有效输入数据和噪声。
USART_TDR是USART发送数据寄存器,要发送什么数据,就往这个寄存器里写数据即可,其低9位有效。USART_RDR是USART接收数据寄存器,包含接收到的数据字符,要接收什么数据,读这个寄存器即可,其低9位有效。
USART有一个发送 FIFO (TXFIFO) 和一个接收 FIFO (RXFIFO),所以USART可以工作在FIFO 模式下,可以通过将 USART_CR1寄存器中的 FIFOEN(第29位)置 1 来使能 FIFO 模式。RXFIFO 的默认宽度为 12 位,即最大9位数据+1位奇偶校验位+1位噪声错误+1位帧错误标志。如果使能FIFO,写入到发送数据寄存器 (USART_TDR) 中的数据会在 TXFIFO 中排队,然后再通过TX移位寄存器发送到RX移位寄存器,再由RX移位寄存器发送到RxFIFO,然后USART_RDR寄存器从RxFIFO中获得数据。
向 USART_TDR 中写入要发送的数据前,发送使能位 (TE) 先置 1以激活发送器功能,发送移位寄存器(USART_TDR)中的数据在 TX 引脚输出,默认情况下,先发送数据的最低位(LSB),相应的时钟脉冲在SCLK 引脚输出。一旦向发送数据寄存器中写数据,UART的BUSY位开始有效,当从发送移位寄存器发送最后一个字符(包括停止位)才变无效。
RX接收字符时,如果已禁止 FIFO 模式,则,RXNE位置1,表明移位寄存器的内容已传送到 RDR,也就是说,已接收到并可读取数据;如果已使能 FIFO 模式,则RXFNE位置1,这表示 RXFIFO 非空,读取 USART_RDR会返回输入到 RXFIFO 中的最早数据。
发送器可发送 7 位、8 位或 9 位的数据字,具体取决于 M 位的设置,通USART控制寄存器1(USART_CR1)的 M位(M0:位 12,M1:位 28)来设置:
7 位字符长度:M[1:0] =“10”
8 位字符长度:M[1:0] =“00”
9 位字符长度:M[1:0] =“01”
每个字符从起始位开始到可配置数量的停止位终止,停止位的数量可配置为 0.5、1、1.5 或 2位。在默认情况下,信号(TX 或 RX)在起始位工作期间处于低电平状态。在停止位工作期间处于高电平状态。如果发送期间复位TE 位会冻结波特率计数器,当前发送的数据随即丢失,使能 TE 位时,将发送空闲帧,空闲帧发送将包括停止位。
3. 控制寄存器
        框图中③区域部分,我们可以通过控制寄存器控制USART数据的发送、数据接收、各种通信模式的设置、中断、DMA 模式还有唤醒单元等。前面部分我们已经介绍了对应的寄存器。
4. DMA和IRQ中断功能
框图中④区域部分涉及两个接口:IRQ和DMA中断接口。
USART支持DMA传输,可以实现高速数据传输,具体我们会在DMA实验中为大家讲解。在 USART 通信过程中,中断可由不同事件生成,同时支持 USART 模块生成唤醒中断。常用的中断比如:发送数据寄存器为空、发送 FIFO 未满、发送完成、接收 FIFO 非空、接收 FIFO 已满等。有关所有 USART 中断请求的详细说明可以查看参考手册详细列表。
5. USART信号引脚
        框图中⑤区域部分是USART信号的引脚。
在 RS232 硬件流控制模式下需要以下两个引脚:
CTS(清除以发送):发送器在发送下一帧数据之前会检测 CTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。
RTS(请求以发送):如果为低电平,则该信号用于指示 USART 已准备好接收数据。
在 RS485 硬件控制模式下需要下面这个引脚:
DE(驱动器使能):该信号用于激活外部收发器的发送模式。
在同步主/从模式和智能卡模式下需要以下引脚:
CK:该引脚在同步主模式和智能卡模式下用作时钟输出,在同步从模式下用作时钟输入。
NSS:该引脚在同步从模式下用作从器件选择输入。
这些引脚我们暂时都没有用到,就给大家简单提一下。
        注: DE和 RTS共用同一个引脚。
6. USART工作过程
        前面我们分析了USART的框图和串口相关的寄存器,后面的开发中我们使用的是HAL库,HAL库中对这些寄存器的操作已经封装好了,这里我们从寄存器的层面大概了解USART的工作过程:
(1)USART发送器
        要发送字符,需遵循以下步骤:
        ①设置USART_CR1 中的 M 位以配置字长;
        ②设置USART_BRR 寄存器以选择所需波特率;
        ③配置USART_CR2 中的STOP[1:0]位以配置停止位数;
        ④将USART_CR1 寄存器中的 UE 位写入 1 以使能 USART;
        ⑤如果必须进行多缓冲区通信,请选择 USART_CR3 中的 DMA 使能 (DMAT);
        ⑥将 USART_CR1 中的 TE 位置 1 以使能发送器,在首次发送时发送一个空闲帧;
        ⑦在 USART_TDR 寄存器中写入要发送的数据;
        ⑧将最后一个数据写入 USART_TDR 寄存器后,等待 TC =1(表示传送已完成)。
第二十章 串口通信实验16326.png
图20.2.3. 2串口发送时TC/TXE的行为

        注:
        TXE位为1表示数据寄存器/发送 FIFO 未满,为0表示数据寄存器已满/发送 FIFO 已满;
TC位为1表示传送已完成,为0表示未发送完成。
(2)USART接收器
        接收器采用过采样来判断接收到的数据是否准确。在空闲状态时,传送线为高电平状态(逻辑1),而数据的起始位是逻辑0。当接收器检测到一个从1到0的跳变沿的时候,便视为可能的起始位(可能是存在干扰引起跳变),接收器是怎么去判断接收到的数据是否有效呢?
        它采用了起始位检测序列来判断,当以16或8倍过采样时,此序列均为:1110 X 0X0X0 000,其中 X表示电平任意,可以为0或者1。如果检测到的序列不完整,则起始位检测将中止,接收器返回空闲状态,在该状态中等待下降沿。如果针对第 3 位、第 5 位和第 7 位进行首次采样时检测到这3位均为“0”;针对第 8 位、第 9 位和第 10 位进行第二次采样时仍检测到这3位均为“0”,则表示起始位有效。如下图所示:
第二十章 串口通信实验16803.png
图20.2.3. 3串口过采样时检测有效位

        USART 接收期间,首先通过 RX 引脚移出数据的最低有效位(默认配置)。
        字符接收步骤:
        ①设置USART_CR1 中的M位以配置字长;
        ②设置USART_BRR 寄存器以选择所需波特率;
        ③配置USART_CR2 中的STOP[1:0]位以配置停止位数;
        ④将USART_CR1 寄存器中的UE位写入 1 以使能 USART;
        ⑤如果将进行多缓冲区通信,请选择 USART_CR3 中的 DMA 使能 (DMAR);
        ⑥将USART_CR1的RE位置1以使能接收器开始搜索起始位。
20.2.4 GPIO引脚复用功能
        芯片的外设有很多,而芯片的引脚资源却是有限的,如何在有限的引脚里拓展出更多的功能呢,这里就涉及到了复用。复用,就是说,一个引脚除了可以当做普通的IO口功能以外,还可以与一些外设关联起来,作为第二功能或者第三功能甚至更多的功能使用,不过在这些功能当中,在同一个时刻中只能选用其中一种功能来用,这样就不会导致冲突。
1. 复用功能寄存器
        前面我们是有介绍GPIO的部分寄存器,这里,我们再来熟悉和复用功能有关的两个寄存器:
GPIOx_AFRL和GPIOx_AFRH(复用功能高位寄存器和复用功能低位寄存器)
        有两个32位的复用功能寄存器(或者称为多路复用器),AFRL和AFRH,每4个位配置一个IO口的复用功能,AFRL配置第0至第7个IO口,AFRH 配置第8至第15个IO口,总共16和IO口,每个IO口可以选择AF0至AF15中的某个复用功能(具体根据数据手册中的复用功能映射表来确定)。复位后,多路复用器选择为复用功能AF0,复位后的IO口工作模式由MODER和AFRL/H寄存器共同决定。
3D9FFC37-CCA0-4c12-94FE-E5083BEF21B6.png
图20.2.4. 1 AFRL寄存器

2. 复用功能映射表
        IO口并不是想复用什么功能都可以,是有规定的,每个 IO 引脚的复用可以通过查阅数据手册的复用功能映射表来确定,例如我们需要配置PG11复用为UART4_TX,即多路复用器选择为复用功能AF6,则配置寄存器GPIOG_AFRL的AFR11的第12至第15位为0110。当然也可以在STM32CubeMX上使用图形界面来配置IO口的复用功能。
第二十章 串口通信实验17808.png
图14.2.4. 2 Port G引脚复用

3. 复用功能选择宏定义
        我们的开发板上,USB_TTL这个接口使用的是PG11和PB12,他们复用成UART4来用了,从上表中看出PG11可以复用为UART4_TX,是在AF6这一列。在HAL库的stm32mp1xx_hal_gpio_ex.h文件中可以找到这些复用的宏定义:
/**
* @brief  AF 6选择
*/
#define GPIO_AF6_SPI3     ((uint8_t)0x06)                  /* SPI3复用功能映射  */
#define GPIO_AF6_SAI1     ((uint8_t)0x06)                  /* SAI1复用功能映射  */
#define GPIO_AF6_SAI3     ((uint8_t)0x06)                  /* SAI3复用功能映射  */
#define GPIO_AF6_SAI4     ((uint8_t)0x06)                  /* SAI4复用功能映射  */
#define GPIO_AF6_I2C4     ((uint8_t)0x06)                  /* I2C4复用功能映射  */
#define GPIO_AF6_DFSDM1   ((uint8_t)0x06)          /* DFSDM1复用功能映射 */
#define GPIO_AF6_UART4    ((uint8_t)0x06)          /* UART4复用功能映射 */
        AF6的宏定义的值都是一样的,即都是(uint8_t)0x06,这些宏名只是为了区分是当做哪个外设而已,例如我们开发板的外设是串口4,所以就很容易选择到我们要复用的功能对应的宏定义,就是GPIO_AF6_UART4。具体的场景应用会在我们后面的实验中有所体现。
20.3 HAL库中串口相关的API
        为了更好地使用HAL库来实现串口功能,下面我们来介绍HAL库中和本实验相关的结构体和API函数。
20.3.1 结构体和句柄
        我们先来看看和串口相关的结构体和句柄。我们在介绍HAL库的时候有提到句柄(Handle),句柄在HAL库中是一个指针,指针指向地址,在调用API函数的时候,可以利用句柄来说明要操作哪些资源。在stm32mp1xx_hal_uart.h中有定义句柄和结构体。
1. USART_TypeDef结构体
        UART4挂在APB1总线上,UART4的地址范围是0x40010000 ~ 0x400103FF,即UART4的基地址是0x40010000。我们前面也说过,通过寄存器的基地址和偏移地址就可以访问一个寄存器,而HAL里边使用了大量的结构体来对寄存器进行了封装,如果我们要配置某个寄存器,只需要定义一个结构体指针,然后通过指针来配置这个结构体里面的成员变量,当结构体成员配置号以后,HAL库就会根据这些配置来初始化IO。USART_TypeDef结构体定义如下:
/* UART句柄结构定义 */
typedef struct
{
  __IO uint32_t CR1;                            /* USART控制寄存器1,地址偏移量:0x00 */
  __IO uint32_t CR2;                            /* USART控制寄存器2,地址偏移量:0x04 */
  __IO uint32_t CR3;                            /* USART控制寄存器3,地址偏移量:0x08 */
  __IO uint32_t BRR;                            /* USART波特率寄存器,地址偏移量:0x0C */
  __IO uint16_t GTPR;           /* USART保护时间和预分频器寄存器,地址偏移量:0x10 */
  uint16_t  RESERVED2;                          /* 保留,0x12 */
  __IO uint32_t RTOR;                           /* USART接收器超时寄存器,地址偏移量:0x14 */
  __IO uint16_t RQR;                            /* USART请求寄存器,地址偏移量:0x18 */
  uint16_t  RESERVED3;                          /* 保留,0x1A */
  __IO uint32_t ISR;                            /* USART中断和状态寄存器,地址偏移量:0x1C */
  __IO uint32_t ICR;                            /* USART中断标志清除寄存器,地址偏移量:0x20 */
  __IO uint16_t RDR;                            /* USART接收数据寄存器,地址偏移量:0x24*/
  uint16_t  RESERVED4;                          /* 保留,0x26 */
  __IO uint16_t TDR;                            /* USART发送数据寄存器,地址偏移量:0x28 */
  uint16_t  RESERVED5;                          /* 保留,0x2A */
  __IO uint32_t PRESC;                          /* USART时钟预分频器寄存器,地址偏移量:0x2C */
  uint32_t  RESERVED6[239];          /* 预留0x30-0x3E8 */
  __IO uint32_t HWCFGR2;                  /* USART 配置2寄存器,地址偏移量:0x3EC */
  __IO uint32_t HWCFGR1;                  /* USART 配置1寄存器,地址偏移量:0x3F0 */
  __IO uint32_t VERR;                           /* USART版本寄存器,地址偏移量:0x3F4 */
  __IO uint32_t IPIDR;                          /* USART标识寄存器,地址偏移量:0x3F8 */
  __IO uint32_t SIDR;                           /* USART时钟大小识别寄存器,地址偏移量:0x3FC */
} USART_TypeDef;
        我们在前面有分析过,__IO在core_m4.h 文件中有定义,表示volatile,volatile表示强制编译器减少优化,告诉编译器必须每次去内存中取变量值。
#define   __IO    volatile
2. UART_InitTypeDef结构体
        UART_InitTypeDef结构体主要用于设置串口的波特率、数据位长度、停止位数、过采样倍数等信息,通过对结构体成员的写操作即可实现这些设置,其定义如下:
/* UART初始化结构定义 */
typedef struct
{
  uint32_t BaudRate;                   /* 该成员配置UART通信波特率 */
  uint32_t WordLength;                 /* 指定在一帧中发送或接收的数据位数 */
  uint32_t StopBits;                   /* 指定发送的停止位数 */
  uint32_t Parity;                     /* 指定奇偶校验模式 */
  uint32_t Mode;                        /* 指定启用还是禁用接收或发送模式 */                                       
  uint32_t HwFlowCtl;                  /* 指定启用还是禁用硬件流控制模式 */
  uint32_t OverSampling;                         /* 指定是否启用过采样8 */
  uint32_t OneBitSampling;             /* 指定是选择单个样本还是三个样本 */
  uint32_t ClockPrescaler;             /* 指定用于分频UART时钟源的预分频器值 */
} UART_InitTypeDef;
(1)BaudRate:设置波特率,一般设置为 2400、9600、19200、115200;
(2)WordLength:设置数据位数,可选 8 位或 9 位。后面的实验我们设置为8位字长;
(3)StopBits:设置停止位个数,可选0.5个、1个、1.5个和2个停止位,后面的实验我们选择1个停止位;
(4)Parity:设置奇偶校验位,我们设定为无奇偶校验位。
(5)Mode:设置UART模式选择,可以设置为只收模式、只发模式、或者收发模式。我们选择设置为全双工收发模式。
(6)HwFlowCtl:硬件流控制选择,我们选择为无硬件流控制。
(7)OverSampling:过采样选择,可选择8倍过采样或者16过采样,我们可以选择16过采样。
3. UART_AdvFeatureInitTypeDef结构体
        如下是UART高级功能初始化结构定义,ST新出的芯片添加了一些新特性,其中:
        自动波特率检测是指某一方可以自动检测对方传输数据时的波特率,从而自动采用与对方相同的波特率进行数据传输,而不需要人工去设置波特率。
        是否交换TX和RX引脚是否反转是指:RXD和TXD管脚互换。有时候,我们在外接串口引脚时,可能会犯低级错误,会将RXD和TXD引脚的两根线接反了,只能拆下来重新接。不过STM32MP1以及STM32H7系列的串口是支持RXD和TXD管脚互换的,通过设置SWAP位即可将RXD和TXD管脚互换。
        指定TX/RX引脚的活动电平是否反转:通常默认串口高电平为逻辑1,低电平为逻辑0,
而在STM32的USART新特性中是可以将高电平设置为逻辑0,低电平设置为逻辑1的。
        关于串口的新特性我们就介绍到这里,感兴趣的小伙伴可以自行查阅参考手册的介绍。
/* UART高级功能初始化结构定义 */
typedef struct
{
  /* 指定初始化哪些高级UART功能,可同时初始化几个高级功能 */
  uint32_t AdvFeatureInit;  
  uint32_t TxPinLevelInvert;              /* 指定TX引脚的活动电平是否反转 */
  uint32_t RxPinLevelInvert;              /* 指定RX引脚的活动电平是否反转 */
  uint32_t DataInvert;       /* 指定是否反转数据(正逻辑/正逻辑与负逻辑/反逻辑)*/
  uint32_t Swap;                                  /* 指定是否交换TX和RX引脚 */
  uint32_t OverrunDisable;                /* 指定是否禁用接收溢出检测 */                                      
  uint32_t DMADisableonRxError;           /* 指定在接收错误的情况下是否禁用DMA */                                    
  uint32_t AutoBaudRateEnable;            /* 指定是否启用自动波特率检测 */                                      
  uint32_t AutoBaudRateMode;/* 如启用了自动波特率检测,请指定如何进行速率检测 */                                    
  uint32_t MSBFirst;                              /* 指定是否首先在UART线上发送MSB */                                   
} UART_AdvFeatureInitTypeDef;
4. UART_HandleTypeDef句柄
        UART_HandleTypeDef句柄是基于以上结构体以及HAL库的函数封装后的,其定义如下:
/* UART句柄结构定义 */
typedef struct __UART_HandleTypeDef
{
  USART_TypeDef        *Instance;                                 /* UART寄存器基地址 */
  UART_InitTypeDef     Init;                                      /* UART通讯参数 */
  UART_AdvFeatureInitTypeDef AdvancedInit;     /* UART高级功能初始化参数 */
  uint8_t              *pTxBuffPtr;       /* 指向UART Tx传输缓冲区的指针 */
  uint16_t             TxXferSize;        /* UART Tx传输大小 */
  __IO uint16_t        TxXferCount;      /* UART Tx传输计数器 */
  uint8_t              *pRxBuffPtr;       /* 指向UART Rx传输缓冲区的指针 */
  uint16_t             RxXferSize;        /* UART Rx传输大小 */
  __IO uint16_t        RxXferCount;      /* UART Rx传输计数器 */
  uint16_t             Mask;                      /* UART Rx RDR寄存器掩码 */
  uint32_t             FifoMode;                  /* 指定是否正在使用FIFO模式 */                                                            
  uint16_t             NbRxDataToProcess;   /* RX ISR执行期间要处理的数据数 */
  uint16_t             NbTxDataToProcess;   /* TX ISR执行期间要处理的数据数 */
/* Rx IRQ处理程序上的函数指针 */
  void (*RxISR)(struct __UART_HandleTypeDef *huart);
/* Tx IRQ处理程序上的函数指针 */
  void (*TxISR)(struct __UART_HandleTypeDef *huart);
  DMA_HandleTypeDef     *hdmatx;              /* UART Tx DMA句柄参数 */
  DMA_HandleTypeDef     *hdmarx;              /* UART Rx DMA句柄参数 */
#ifdef HAL_MDMA_MODULE_ENABLED
  MDMA_HandleTypeDef    *hmdmatx;            /* UART Tx MDMA句柄参数 */
  MDMA_HandleTypeDef    *hmdmarx;            /* UART Rx MDMA句柄参数 */
#endif /* HAL_MDMA_MODULE_ENABLED */
  HAL_LockTypeDef       Lock;                         /* 锁定对象 */
/* 与全局句柄管理以及Tx操作有关的UART状态信息 */                                            
  __IO HAL_UART_StateTypeDef   gState;            /*与Tx操作有关的UART状态信息 */                  
  __IO HAL_UART_StateTypeDef   RxState;     /* 与Rx操作有关的UART状态信息 */                                                        
  __IO uint32_t                ErrorCode;     /* UART错误代码 */

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
  void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);
  void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);      
  void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);      
  void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);           
  void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);            
  void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);         
  void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart);
  void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);  
  void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);            
  void (* RxFifoFullCallback)(struct __UART_HandleTypeDef *huart);      
  void (* TxFifoEmptyCallback)(struct __UART_HandleTypeDef *huart);      
  void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);           
  void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);        
#endif  /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef;
        此句柄内容有很多,我们先关注几个重要的:
(1)Instance
        我们前面已经介绍了USART_TypeDef结构体,Instance是USART_TypeDef结构体指针变量,指向UART 寄存器基地址,通过此变量即可操作对应的结构体成员(寄存器)。
(2)Init
        UART_InitTypeDef结构体我们前面也有介绍到,此处声明UART_InitTypeDef结构体变量Init,可以通过Init来设置串口的波特率、数据位长度、停止位数、过采样倍数等信息。
        波特率的计算公式我们前面已经讲解了,套入前面的波特率计算公式中。
        如果过采样为16或在LIN模式下:
第二十章 串口通信实验25927.png

        如果过采样为8:
第二十章 串口通信实验25939.png

        波特率寄存器[3] = 0
第二十章 串口通信实验25959.png

(3)pTxBuffPtr
                pTxBuffPtr、TxXferSize、TxXferCount分别是指向发送数据缓冲区的指针、发送数据的大小、发送数据的个数。
(4)pRxBuffPtr
                pRxBuffPtr 、RxXferSize和RxXferCount分别是指向接收数据缓冲区的指针、接收数据的大小、接收数据的个数。
(5)Lock
                Lock是对资源操作增加操作锁保护功能,可选HAL_UNLOCKED或者HAL_LOCKED两个参数。如果gState的值等于HAL_UART_STATE_RESET,则可认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit函数来对串口的GPIO和时钟进行初始化。
(6)最后的部分是一些回调函数,如果宏USE_HAL_UART_REGISTER_CALLBACKS的值为1,表示可以使用这些回调函数。
        这里注意,结构体成员 gState 没有做初始状态。
20.3.2 HAL库中的API函数
        STM32MP1串口有3种通信方式:轮询、中断、DMA,其中,轮询方式为阻塞模式,DMA和中断方式为非阻塞模式。阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前的线程会被挂起,调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。在前面的按键输入实验和外部中断实验中,一个采用了按键轮询的方式,一个是采用了中断的方式,前者也称为阻塞,后者称为非阻塞。
        stm32mp1xx_hal_uart.c文件下有很多和USART相关的库函数,这里我们只介绍几个重要的函数,DMA相关的函数这里就不介绍了,后续DMA实验章节我们再介绍。
1. 初始化USART模式
●函数功能:根据UART_InitTypeDef中指定的参数初始化USART模式,并初始化关联的句柄。
●函数参数:UART_InitTypeDef结构体指针变量,可以指定要使用的串口。(前面已经分析了串口句柄)
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
具体的函数定义,我们这里就不列出来了。
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
2. 以查询的方式发送/接收函数
        HAL_UART_Transmit函数以轮询(阻塞模式)的方式发送/接收指定字节,HAL_UART_Receive函数以查询的方式接收指定字节。
●函数功能:以查询的方式发送指定字节
●函数参数:
huart: UART_HandleTypeDef 类型结构体指针变量,可以指定要使用的串口;
pData:要发送的数据地址;
Size:要发送的数据大小(单位:字节);
Timeout:超时,溢出时间(单位:ms),对于接收来说,串口在Timeout的时间内等待接收Size个字节。
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
此函数以查询的方式发送/接收指定字节,主要是通过操作USART_TDR寄存器来实现发送数据的,具体的函数定义,我们这里就不列出来了。
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) /* 串口轮询模式接收,使用超时管理机 */
3. 以中断方式发送/接收函数
●函数功能:以中断的方式发送/接收指定字节
●函数参数:
        huart: UART_HandleTypeDef 类型结构体指针变量,可以指定要使用的串口;
        pData:要接收/发送的数据地址;
        Size:要接收的数据大小(单位:字节);
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
        此函数以中断的方式发送指定字节,数据发送在中断请求函数HAL_UART_IRQHandler中实现。可以使用FIFO相关中断来实现,主要是通过操作USART_CR1、USART_CR3、USART_ISR和USART_TDR寄存器来实现发送数据。
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) /* 串口中断模式接收 */
4. 中断请求函数
●函数功能:串口中断请求函数
●函数参数:huart,UART_HandleTypeDef 类型结构体指针变量,可以指定要使用的串口;
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
        此函数是串口中断请求函数,如果前面使用了轮询(阻塞)的方式发送和接收数据,那么此函数就会被串口中断服务函数调用来完成中断的功能。
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
5. 回调函数
        在非阻止模式下提供了一组传输完成回调函数,这些回调函数都是weak弱定义的,可以被用户重新定义,以实现串口发送和接收的逻辑。
/* 数据完全发送完成后调用 */
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
/* 一半数据发送完成时调用 */
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
/* 一半数据接收完成时调用 */
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
/* 数据完全接受完成后调用 */
__weak void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
/* 传输出现错误时调用 */
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
/* UART中止完成时调用 */
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart)
/* UART中止完成回调函数 */
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart)
/* UART中止接收完整的回调函数 */
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart)
6. 传输中断函数
        以下是一些中止正在进行的发送/接收传输函数(中断模式和阻塞模式)。
/* 中止正在进行的传输(阻塞模式) */
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *huart);
/* 中止正在进行的传输传输(阻塞模式) */            
HAL_StatusTypeDef HAL_UART_AbortTransmit(UART_HandleTypeDef *huart);
/* 中止正在进行的接收传输(阻塞模式) */  
HAL_StatusTypeDef HAL_UART_AbortReceive(UART_HandleTypeDef *huart);
/* 中止正在进行的传输(中断模式) */   
HAL_StatusTypeDef HAL_UART_Abort_IT(UART_HandleTypeDef *huart);
/* 中止正在进行的传输(中断模式) */        
HAL_StatusTypeDef HAL_UART_AbortTransmit_IT(UART_HandleTypeDef *huart);
/* 中止正在进行的接收传输(中断模式) */
HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart);  
7. 串口中断过程分析
        发生串口中断后,会调用串口中断服务函数UART4_IRQHandler,此函数调用了串口中断请求函数HAL_UART_IRQHandler,我们来看看HAL_UART_IRQHandler函数做了些什么工作:
1   void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
2   {
3     uint32_t isrflags   = READ_REG(huart->Instance->ISR);
4     uint32_t cr1its     = READ_REG(huart->Instance->CR1);
5     uint32_t cr3its     = READ_REG(huart->Instance->CR3);
6
7     uint32_t errorflags;
8     uint32_t errorcode;
9
10    /* 如果没有错误发生 */
11    errorflags = (isrflags & (uint32_t)(USART_ISR_PE | USART_ISR_FE |                                                         USART_ISR_ORE | USART_ISR_NE | USART_ISR_RTOF));
12    if (errorflags == 0U)
13    {
14      /* UART模式接收器 */
15      if (((isrflags & USART_ISR_RXNE_RXFNE) != 0U)
16          && (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U)
17              || ((cr3its & USART_CR3_RXFTIE) != 0U)))
18      {
19        if (huart->RxISR != NULL)
20        {
21          huart->RxISR(huart);
22        }
23        return;
24      }
25    }
26    /****** 此处省略掉部分代码 ******/
27    /* UART模式发送器 */
28    if (((isrflags & USART_ISR_TXE_TXFNF) != 0U)
29        && (((cr1its & USART_CR1_TXEIE_TXFNFIE) != 0U)
30            || ((cr3its & USART_CR3_TXFTIE) != 0U)))
31    {
32      if (huart->TxISR != NULL)
33      {
34        huart->TxISR(huart);
35      }
36      return;
37    }
38
39    /* UART模式发送器(发送结束)*/
40   if (((isrflags & USART_ISR_TC) != 0U) && ((cr1its & USART_CR1_TCIE) !=                                                                                                                                 0U))
41    {
42      UART_EndTransmit_IT(huart);
43      return;
44    }
45  /****** 此处省略掉部分代码 ******/
46  }
        查看第20~25行之间的代码,通过检查几个标志位看看是否有错误发生,这几个位对应USART_ISR寄存器的某些位,在stm32mp157dxx_cm4.h文件中有定义。
lQLPJxbXU-t-QSxmzQJysD3xAj-Lq1eBA2HcAXIAHAA_626_102.png
表20.3.2.1 USART_ISR寄存器的某些位

        如果没有错误发生,则再检查USART_ISR寄存器RXFNE位、USART_CR1寄存器的RXNE位、USART_CR3寄存器的RXFTIE位是否都为0,如果都不为0,表示已经准备好要接收的数据,并开启了中断:
        当USART_RDR 移位寄存器的内容已传输到 USART_RDR 寄存器时,RXNE 位由硬件置1 ,表示USART_RDR 移位寄存器的内容已传输到 USART_RDR 寄存器,此时可以对USART_RDR 寄存器执行读取操作。
lQLPJxbXU_WgO5ZlzQLDsOoJR4Hvo-6TA2HcEXiAmwA_707_101.png
表20.3.2.2串口寄存器

        以上条件满足以后,就去判断函数指针RxISR是否非空,如果非空,就执行:
huart->RxISR(huart);
        这句话是什么意思呢?RxISR是接收端Rx中断请求处理程序上的函数指针,其定义如下,RxISR是一个指向函数的指针,(*RxISR)是一个带有参数huart返回值类型为void的函数,参数huart也是一个指针。此函数最后会映射到HAL_UART_Receive_IT函数里,也就是会执行HAL_UART_Receive_IT函数。
void (*RxISR)(struct __UART_HandleTypeDef *huart);
        我们来看看HAL_UART_Receive_IT函数:
1 HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t                                                                                                         *pData, uint16_t Size)
2 {
3   if (huart->RxState == HAL_UART_STATE_READY)/*检查接收进程是否尚未进行*/
4  {        /****** 此处省略部分代码 ******/
5                   __HAL_LOCK(huart);
6           SET_BIT(huart->Instance->CR3, USART_CR3_EIE); /*打开UART错误中断*/
7     /* 配置Rx中断处理----FIFO模式 */
8     if ((huart->FifoMode == UART_FIFOMODE_ENABLE) && (Size >=                                                                                                                         huart->NbRxDataToProcess))
9     {
10      /* 根据数据字长设置Rx ISR功能指针 */
11      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) &&                                                                                                 (huart->Init.Parity == UART_PARITY_NONE))
12      {
13        huart->RxISR = UART_RxISR_16BIT_FIFOEN; /* 字长为9位 */
14      }
15      else
16      {
17        huart->RxISR = UART_RxISR_8BIT_FIFOEN; /* 字长为7或者8位*/
18      }
19                 __HAL_UNLOCK(huart);
20                SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);
21                SET_BIT(huart->Instance->CR3, USART_CR3_RXFTIE);
22     }   
23    else
24    {
25      /* 根据数据字长设置Rx ISR功能指针 */
26      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) &&                                                                                         (huart->Init.Parity == UART_PARITY_NONE))
27      {
28        huart->RxISR = UART_RxISR_16BIT;                 /* 字长为9位*/
29      }
30      else
31      {
32        huart->RxISR = UART_RxISR_8BIT;                 /* 字长为7或者8位 */
33      }
34
35      __HAL_UNLOCK(huart);
36
37      /* 使能UART奇偶校验错误中断和数据寄存器非空中断 */
38      SET_BIT(huart->Instance->CR1, USART_CR1_PEIE |                                                                                                                                         USART_CR1_RXNEIE_RXFNEIE);
39    }
40    return HAL_OK;
41  }
42 /****** 此处省略部分代码 ******/
43}
        以上代码,我们就关注标红的部分,第23行~33行,先判断字长,然后根据字长来映射到对应的中断处理函数,我们的实验使用的不是FIFO模式,字长设置为8位,所以会映射到UART_RxISR_8BIT函数。
        第5、19和35行,看到调用了__HAL_LOCK和__HAL_UNLOCK这两个宏,在stm32mp1xx_hal_def.h文件中有定义:
  #define __HAL_LOCK(__HANDLE__) \
                             do{    \
                                   if((__HANDLE__)->Lock == HAL_LOCKED)\
                                   {   \
                                      return HAL_BUSY; \
                                    } \
                                    else \
                                    {    \
                                       (__HANDLE__)->Lock = HAL_LOCKED; \
                                    }    \
                                  }while (0U)

  #define __HAL_UNLOCK(__HANDLE__) \
                                  do{    \
                                      (__HANDLE__)->Lock = HAL_UNLOCKED;  \
                                    }while (0U)
        HAL_UNLOCKED表示0,HAL_LOCKED表示1,在文件stm32mp1xx_hal_def.h中有定义。可以看到,__HAL_LOCK其实就是判断被操作的__HANDLE__(本实验中被操作的是串口4)是否已经上锁,如果没有上锁,则执行加锁,如果已经上锁,则返回繁忙HAL_BUSY,程序就立即退出所进入的函数,函数后面的代码就不会再被执行。__HAL_UNLOCK则表示解锁。程序是怎样的设计思路呢?我们来分析一下:
        进入HAL_UART_Receive_IT函数以后,第5行,先执行__HAL_LOCK进行加锁,如果在解锁前再次调用HAL_UART_Receive_IT函数,则会返回HAL_BUSY,程序退出。第32行,通过函数指针调用UART_RxISR_8BIT来完成字符接收操作,所以第35行就调用__HAL_UNLOCK进行解锁,也就是说,必须执行接收数据操作以后,才可以再次调用HAL_UART_Receive_IT函数。
        __HAL_LOCK和__HAL_UNLOCK在DMA、定时器、串口等HAL库驱动中比较常见,我们后面的实验还会遇见它们,如果是双工通信,这两个宏要格外注意。
        我们来看看UART_RxISR_8BIT函数是怎么接收字符的:
1 static void UART_RxISR_8BIT(UART_HandleTypeDef *huart)
2 {
3   uint16_t uhMask = huart->Mask;
4   uint16_t  uhdata;
5
6   /* 检查接收过程是否正在进行*/
7   if (huart->RxState == HAL_UART_STATE_BUSY_RX)
8   {
9     uhdata = (uint16_t) READ_REG(huart->Instance->RDR);
10    *huart->pRxBuffPtr = (uint8_t)(uhdata & (uint8_t)uhMask);
11    huart->pRxBuffPtr++;
12    huart->RxXferCount--;
13
14    if (huart->RxXferCount == 0U)
15    {
16      /* 关闭UART奇偶校验错误中断和RXNE中断 */
17      CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE_RXFNEIE |  \                                                                                                                        USART_CR1_PEIE));
18
19      /* 关闭UART错误中断:(帧错误,噪声错误,溢出错误)*/
20      CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);
21
22      /* Rx进程完成,将huart-> RxState还原为Ready  */
23      huart->RxState = HAL_UART_STATE_READY;
24
25      /* 清除RxISR函数指针 */
26      huart->RxISR = NULL;
27
28 #if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
29      /* 调用注册的Rx完成回调函数 */
30      huart->RxCpltCallback(huart);
31 #else
32      /* 调用旧版弱Rx完成回调函数*/
33      HAL_UART_RxCpltCallback(huart);
34 #endif
35    }
36  }
37  else
38  {
39    /* 清除RXNE中断标志 */
40    __HAL_UART_SEND_REQ(huart, UART_RXDATA_FLUSH_REQUEST);
41  }
42 }
        以上代码,我们也是关注红色的部分,进入UART_RxISR_8BIT函数以后:
        第17和第20行表示关闭串口接收中断;
        第26行,清除RxISR函数指针;
        第33行,调用HAL_UART_RxCpltCallback(huart)回调函数以完成中断,此回调函数是弱定义的函数,需要用户重新定义一个回调函数,后面我们会自己编写。这里注意,前面第17和20行已经关闭了串口接收中断,也就是说,在一次串口中断接收的最后,即串口接收完一组数据之后就关闭了串口接收中断了,程序只能完成一次中断接收,如果要下次还要接收数据的时候,就要再次打开串口,所以可以在回调函数中再次调用HAL_UART_Receive_IT函数来开启接收中断,从而实现继续接收数据。
        我们注意到,HAL_UART_Receive_IT函数并不是用来接收串口数据的,而是根据条件打开对应的中断处理函数的,如前面标红的UART_RxISR_16BIT_FIFOEN和UART_RxISR_8BIT等函数,而打开的这些中断函数才是真正的串口中断接收处理函数。
20.4 串口中断接收回显实验
20.4.1 硬件设计

1. 例程功能
STM32MP157通过串口4和上位机对话,STM32MP157在收到上位机发过来的字符串(以回车换行结束)后,会返回给上位机,同时每隔一定时间,串口4输出一段信息到电脑,LED0闪烁,提示程序在运行。
2. 硬件资源
        本实验中我们会用到开发板底板的USB_TTL接口,此接口的引脚资源如下表所示,我们还需要一根配套的Type-C线来实现电脑和开发板进行串口通信。
lQLPJxbXU__CPH8izQFYsAuvmMSi5F8tA2HcInhA7AA_344_34.png
图20.4.1. 1硬件资源表

3. 原理图
        USB_TTL接口硬件部分的原理图如下图所示,其中UART4_TX发送端接的PG11,UART4_RX接收端接的PB2,实验中我们就配置这两个引脚。
第二十章 串口通信实验37350.png
图20.4.1. 2 UART4两个引脚

20.4.2 程序设计
        本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验9-1 串口通信实验1。
1. 程序流程图
第二十章 串口通信实验37539.png
图20.4.2. 1程序流程图

2. 新建工程
        我们可以直接在上一章节实验的工程的Drivers/SYSTEM下新建usart文件夹,然后在usart文件夹下新建usart.c和usart.h文件,并将usart.c文件关联到工程中。关于串口4相关的代码我们就添加在这个文件中。此外,工程中要关联stm32mp1xx_hal_uart.c、stm32mp1xx_hal_uart_ex.c、stm32mp1xx_hal_usart.c和stm32mp1xx_hal_usart_ex.c文件。
第二十章 串口通信实验37817.png
图20.4.2.2 新建usart.c文件

3. usart.h文件代码
由硬件设计小节,我们知道PG11和PB2分别被复用为串口4的发送和接收引脚,我们做了下面的引脚定义。
#ifndef __USART_H
#define __USART_H

#include "stdio.h"
#include "./SYSTEM/sys/sys.h"

/* 串口4的GPIO发送引脚定义和时钟使能 */
#define USART_TX_GPIO_PORT           GPIOG
#define USART_TX_GPIO_PIN            GPIO_PIN_11
#define USART_TX_GPIO_AF             GPIO_AF6_UART4
/* 发送引脚时钟使能 */
#define USART_TX_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)
/* 串口4的GPIO接收引脚定义和时钟使能 */
#define USART_RX_GPIO_PORT           GPIOB
#define USART_RX_GPIO_PIN             GPIO_PIN_2
#define USART_RX_GPIO_AF              GPIO_AF8_UART4
/* 接收引脚时钟使能 */
#define USART_RX_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)

/* 串口4的中断号和中断服务函数定义,以及使能串口4时钟 */
#define USART_UX                        UART4
#define USART_UX_IRQn                  UART4_IRQn
#define USART_UX_IRQHandler           UART4_IRQHandler
/* UART4 时钟使能 */
#define USART_UX_CLK_ENABLE()         do{ __HAL_RCC_UART4_CLK_ENABLE(); }while(0)

#define USART_REC_LEN   200               /* 定义最大接收字节数 200 */
#define USART_EN_RX     1                  /* 使能(1)/禁止(0)串口4接收 */
#define RXBUFFERSIZE    1                  /* 缓存大小 */

extern UART_HandleTypeDef g_uart4_handle; /* UART4句柄 */
/* 接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 */
extern uint8_t  g_usart_rx_buf[USART_REC_LEN];
extern uint16_t g_usart_rx_sta;                    /* 接收状态标记 */
extern uint8_t g_rx_buffer[RXBUFFERSIZE];        /* HAL库USART接收Buffer */

void usart_init(uint32_t baudrate);               /* 串口初始化函数声明 */
#endif
以上代码中,PG11和PB2分别复用为GPIO_AF6_UART4和GPIO_AF8_UART4,即复用为UART4。同时,不要忘记了使能GPIO的时钟和UART4的时钟,前面我们说过,STM32的外设在使用的时候都需要先使能时钟。UART4的中断号是UART4_IRQn,中断服务函数是UART4_IRQHandler。
最后我们还定义了3个宏, USART_REC_LEN表示最大接收字节数,这里定义的是200个字节,后续如果有需求要发送更大的数据包,可以改大这个值,这里不改太大,是避免浪费太多内存。USART_EN_RX则是用于使能串口1的接收数据。RXBUFFERSIZE是缓冲大小。
4. usart.c文件代码
(1)串口初始化函数:
UART_HandleTypeDef g_uart4_handle;    /* UART句柄 */
/**
* @brief       串口X初始化函数
* @param       baudrate: 波特率, 根据自己需要设置波特率值
* @note        注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
*               这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
* @retval      无
*/
void usart_init(uint32_t baudrate)
{
    g_uart4_handle.Instance = USART_UX;                     /* USART4 */
    g_uart4_handle.Init.BaudRate = baudrate;               /* 波特率 */
    g_uart4_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
    g_uart4_handle.Init.StopBits = UART_STOPBITS_1;       /* 一个停止位 */
    g_uart4_handle.Init.Parity = UART_PARITY_NONE;        /* 无奇偶校验位 */
    g_uart4_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
    g_uart4_handle.Init.Mode = UART_MODE_TX_RX;            /* 收发模式 */
    HAL_UART_Init(&g_uart4_handle);        /* HAL_UART_Init()会使能UART4 */
    /* 该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量 */
    HAL_UART_Receive_IT(&g_uart4_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE);
}
以上代码中,g_uart4_handle 是结构体UART_HandleTypeDef类型的全局变量,UART_HandleTypeDef结构体成员的含义请回到前面回顾。波特率我们直接赋值给g_uart4_handle.Init.BaudRate这个成员,串口波特率需要我们设置,这是很方便的,要用什么波特率,直接设置参数即可。需要注意的是,最后一行代码调用函数HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量。
以上只是设置了串口4的参数,串口相关的引脚定义以及时钟使能和中断设置等参数我们还没有设置。HAL_UART_Init函数会调用HAL_UART_MspInit函数,所以以上内容我们可以在HAL_UART_MspInit函数完成,代码如下:
1   /**
2    * @brief       UART底层初始化函数
3    * @param       huart: UART句柄类型指针
4    * @note        此函数会被HAL_UART_Init()调用
5    *              完成时钟使能,引脚配置,中断配置
6    * @retval      无
7    */
8   void HAL_UART_MspInit(UART_HandleTypeDef *huart)
9   {
10      GPIO_InitTypeDef gpio_init_struct;
11      RCC_PeriphCLKInitTypeDef rcc_periphclk_init_struct;
12      
13      if(huart->Instance == UART4)  /* 如果是串口4,进行串口4 MSP初始化 */
14      {
15         USART_UX_CLK_ENABLE();                     /* USART4时钟使能 */
16         USART_TX_GPIO_CLK_ENABLE();    /* 发送引脚时钟使能 */
17         USART_RX_GPIO_CLK_ENABLE();    /* 接收引脚时钟使能 */
18
19         gpio_init_struct.Pin = USART_TX_GPIO_PIN;                    /* 指定TX引脚 */
20         gpio_init_struct.Mode = GPIO_MODE_AF_PP;                     /* 复用推挽输出 */
21         gpio_init_struct.Pull = GPIO_PULLUP;                         /* 上拉 */
22         gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */
23         gpio_init_struct.Alternate = USART_TX_GPIO_AF;          /* 复用为UART4 */
24         HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct); /* 初始化发送引脚 */
25
26         gpio_init_struct.Pin = USART_RX_GPIO_PIN;              /* 指定RX引脚 */
27         gpio_init_struct.Alternate = USART_RX_GPIO_AF;        /* 复用为UART4 */
28         HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct); /* 初始化接收引脚 */
29         
30          /* 设置UART4时钟源=PLL4Q=74.25MHz  */
31     rcc_periphclk_init_struct.Uart24ClockSelection = RCC_UART24CLKSOURCE_PLL4;
32          HAL_RCCEx_PeriphCLKConfig(&rcc_periphclk_init_struct);
33
34  #if USART_EN_RX
35          HAL_NVIC_EnableIRQ(USART_UX_IRQn);            /* 使能USART4中断通道 */
36          HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);   /* 抢占优先级3,子优先级3 */
37  #endif
38      }
39  }
我们分析以上代码:
第10和11行,定义两个结构体变量,gpio_init_struct用于初始化GPIO,rcc_periphclk_init_struct用于初始化UART4的时钟;
第19~28行,初始化UART4的发送引脚和接收引脚;
第31行,指定UART4的时钟源来自PLL4Q。UART4的时钟源可以来自APB1总线、PLL4Q、HSE、CSI、HSI,其中PLL4Q在上一章节,我们配置的时候是74.25MHz,即我们配置UART4的时钟为74.25MHz;
第二十章 串口通信实验42953.png
图20.4.2.3 UART4时钟来源

第34~37行,使能UART4中断,并设置中断优先级和子优先级都为3,这里注意的是,中断优先级分组在HAL_Init中有设置为分组2了,上一章节我们有分析过。
(2)串口中断服务函数
        下面我们手动编写串口中断服务函数。
1   /**
2    * @brief       串口4中断服务函数
3    * @param       无
4    * @retval      无
5    */
6   void USART_UX_IRQHandler(void)
7   {
8       uint32_t timeout = 0;
9       uint32_t maxDelay = 0x1FFFF;
10     HAL_UART_IRQHandler(&g_uart4_handle); /* 调用HAL库中断处理公用函数 */
11      timeout = 0;
12      /* 等待就绪 */
13      while (HAL_UART_GetState(&g_uart4_handle) != HAL_UART_STATE_READY)
14      {
15          timeout++;                       /* 超时处理 */
16          if(timeout > maxDelay)
17          {
18              break;
19          }
20      }
21      timeout=0;                          /* 超时处理 */
22      /* 一次处理完成之后,重新开启中断并设置RxXferCount为1 */
23 while (HAL_UART_Receive_IT(&g_uart4_handle, (uint8_t *)g_rx_buffer, \                                                                                         RXBUFFERSIZE) != HAL_OK)
24      {
25          timeout++;                      /* 超时处理 */
26          if (timeout > maxDelay)
27          {
28              break;
29          }
30      }
31  }
第8行,定义的timeout用于超时处理;
第9行,定义的maxDelay用于计算为最大超时时间;
第10行,调用HAL库里的中断请求函数HAL_UART_IRQHandler,该函数用于处理所有中断相关的中断请求;
第13~20行,调用HAL_UART_GetState函数获取串口状态,计数处理时间是否超时,如果在规定的maxDelay时间内串口还未准备就绪,执行break语句,程序退出;
第23~30行,调用UART_Receive_IT函数重新开启中断,同时这里也做了超时处理机制。        这里要注意的是,我们前面有分析过,HAL_UART_Receive_IT函数并不是用来接收串口数据的,而是根据条件打开对应的中断处理函数的,而真正的串口接收处理函数是UART_RxISR_8BIT函数。此外,UART_RxISR_8BIT函数在完成一次接收后就关闭串口中断了。下面列出串口接收中断的一般流程:
第二十章 串口通信实验44409.png
图20.4.2.4 串口接收中断流程

(3)串口4接收回调函数
        UART_RxISR_8BIT函数会调用回调函数HAL_UART_RxCpltCallback,不过此回调函数是弱定义的空函数。在没有编写回调函数的时候,串口只能完成一次发送和接收,前面我们分析了,串口在完成一次中断接收以后,就关闭串口中断了。如果想要实现字符的循环发送和接收的话,我们需要在接收回调函数里做文章。下面我们来看看接收回调函数:
1   /**
2    * @brief       Rx传输回调函数
3    * @param       huart: UART4句柄类型指针
4    * @retval      无
5    */
6   void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
7   {
8       if(huart->Instance == UART4)                /* 如果是串口4 */
9       {
10          if((g_usart_rx_sta & 0x8000) == 0)     /* 接收未完成 */
11          {
12              if(g_usart_rx_sta & 0x4000)          /* 接收到了0x0d */
13              {
14                  if(g_rx_buffer[0] != 0x0a)
15                  {
16                      g_usart_rx_sta = 0;            /* 接收错误,重新开始 */
17                  }
18                  else
19                  {
20                      g_usart_rx_sta |= 0x8000;     /* 接收完成了 */
21                  }
22              }
23              else                                       /* 还没收到0X0D */
24              {
25                  if(g_rx_buffer[0] == 0x0d)
26                  {
27                      g_usart_rx_sta |= 0x4000;
28                  }
29                  else
30                  {
31                g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = g_rx_buffer[0] ;
32                g_usart_rx_sta++;
33                 if(g_usart_rx_sta > (USART_REC_LEN - 1))
34                      {
35                          g_usart_rx_sta = 0;   /* 接收数据错误,重新开始接收 */
36                      }
37                  }
38              }
39          }
40      }
41  }
因为我们设置了串口句柄成员变量RxXferSize为1,那么每当串口1接收到一个字符后触发接收完成中断,便会在中断服务函数中引导执行该回调函数。当串口接受到一个字符后,它会保存在缓存g_rx_buffer中,由于我们设置了缓存大小为1,而且RxXferSize=1,所以每次接受一个字符,会直接保存到RxXferSize[0]中,我们直接通过读取RxXferSize[0]的值就是本次接收到的字符。这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组g_usart_rx_buf ,一个接收状态寄存器g_usart_rx_sta(此寄存器其实就是一个全局变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。数组g_usart_rx_buf的大小由USART_REC_LEN定义,也就是一次接收的数据最大不能超过USART_REC_LEN个字节。g_usart_rx_sta是一个接收状态寄存器其各的定义如表17.3.3.1所示:
lQLPJxbXVAp80_g_zQI0sO9D-csEtarDA2HcNERAHAA_564_63.png
表20.4.2.1接收状态寄存器位定义表

设计思路如下:
当接收到从电脑发过来的数据,把接收到的数据保存在数组g_usart_rx_buf中,同时在接收状态寄存器(g_usart_rx_sta)中计数接收到的有效数据个数,当收到回车(回车的表示由2个字节组成:0X0D和0X0A)的第一个字节0X0D时,计数器将不再增加,等待0X0A的到来,而如果0X0A没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到0X0A,则标记g_usart_rx_sta的第15位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到0X0D,那么在接收数据超过USART_REC_LEN的时候,则会丢弃前面的数据,重新接收。
        学到这里大家会发现,HAL库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用HAL_UART_IRQHandler函数,而是直接编写自己的中断服务函数。串口实验我们之所以遵循HAL库写法, 是为了让大家对HAL库有一个更清晰的理解。
(4)printf函数支持
        直接通过printf函数的话,STM32是不能够输出字符的,要在嵌入式中使用此函数的话,需要通过重映射的方式,将printf函数重映射到STM32串口的寄存器上才可以。
        首先,要使用printf函数的话,工程中要调用stdio.h文件,我们在usart.h头文件中已经添加了此文件:#include "stdio.h"
        其次,添加printf函数重映射到STM32串口的寄存器上的代码,如下:
1   #if 1
2   #pragma import(__use_no_semihosting)
3   /* 解决HAL库使用时, 某些情况可能报错的bug */
4   int _ttywrch(int ch)
5   {
6       ch = ch;
7       return ch;
8   }
9   /* 解决HAL库使用时, 某些情况可能报错的bug */
10  struct __FILE
11  {
12      int handle;
13  };
14  /* FILE 在stdio.h中已经使用'typedef’来定义了 */
15  FILE __stdout;
16
17  /* 定义_sys_exit()以避免使用半主机模式 */
18  void _sys_exit(int x)
19  {
20      x = x;
21  }
22
23  /* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
24  int fputc(int ch, FILE *f)
25  {
26      while ((USART_UX->ISR & 0X40) == 0); /* 等待上一个字符发送完成 */
27      USART_UX->TDR = (uint8_t)ch;    /* 将要发送的字符 ch 写入到DR寄存器 */
28      return ch;                          /* 返回要发送的字符 */
29  }
30  /*
31  int fputc(int ch, FILE *f)
32  {
33      HAL_UART_Transmit(&g_uart4_handle, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
34      return ch;
35  }
36  */
37  #endif
        我们关注第24~29行,重定义了fputc函数,通过查看USART_ISR寄存器的标志位,判断上一个字符发送完成以后,再将要发送的字符(也就是printf后面的字符)写入到TDR寄存器,然后通过串口打印出来。printf函数使用很灵活,大家可以多尝试,第24~29行是使用直接操作寄存器的方式来实现,我们也可以使用第31~35行的代码,调用HAL库的API函数来实现。
5. main.c文件代码
        main.c文件代码如下:
1   #include "./SYSTEM/sys/sys.h"
2   #include "./SYSTEM/delay/delay.h"
3   #include "./BSP/LED/led.h"
4   #include "./BSP/BEEP/beep.h"
5   #include "./BSP/KEY/key.h"
6   #include "./BSP/EXTI/exti.h"
7   #include "./SYSTEM/usart/usart.h"
8   /**
9    * @brief       主函数
10   * @param       无
11   * @retval      无
12   */
13  int main(void)
14  {
15      uint8_t t;
16      uint8_t len;
17      uint16_t times = 0;
18
19      HAL_Init();     /* 初始化HAL库  */
20      
21      /* 初始化M4内核时钟 */
22      if(IS_ENGINEERING_BOOT_MODE())
23      {
24          sys_stm32_clock_init(34, 2, 2, 17, 6826);
25      }
26      usart_init(115200);    /* 串口初始化为115200 */
27      led_init();              /* 初始化LED  */
28      beep_init();             /* 初始化蜂鸣器 */
29      extix_init();            /* 初始化中断  */
30      
31      printf("\r\n正点原子 STM32MP1 开发板 串口实验\r\n");
32      while(1)
33      {
34          if (g_usart_rx_sta & 0x8000)        /* 接收到了数据 */
35          {
36                 len = g_usart_rx_sta & 0x3fff;  /* 得到此次接收到的数据长度 */
37                  printf("\r\n您发送的消息为:\r\n");
38                  for (t = 0; t < len; t++)
39                  {
40                       USART_UX->TDR = g_usart_rx_buf[t];
41                       while ((USART_UX->ISR & 0X40) == 0);  /* 等待发送结束 */
42                  }
43                  printf("\r\n\r\n"); /* 插入换行 */
44                  g_usart_rx_sta = 0;
45          }
46          else
47          {
48                  times++;
49                  if (times % 5000 == 0)
50                  {
51                      printf("\r\n正点原子 STM32MP1 开发板 串口实验\r\n");
52                      printf("正点原子@ALIENTEK\r\n\r\n\r\n");
53                  }
54                  if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
55                  /* 闪烁LED,提示系统正在运行. */
56                  if (times % 30  == 0) LED0_TOGGLE();
57                  //delay(10);
58                  HAL_Delay(10);
59          }
60      }
61  }
第34~45行,首先判断全局变量g_usart_rx_sta的最高位是否为1,如果为1的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口,在上位机显示。
第40行,将g_usart_rx_buf里的数据进行发送,该行是通过直接操作寄存器来实现的,也可以使用HAL库的API函数来实现:
HAL_UART_Transmit(&g_uart4_handle,(uint8_t*)g_usart_rx_buf,len,1000);
第41行,等待发送结束,慈航也是直接操作寄存器来实现的,我们也可以使用HAL库的API函数来实现:
while(__HAL_UART_GET_FLAG(&g_uart4_handle,UART_FLAG_TC)!=SET);  
第44行,将全局变量g_usart_rx_sta清零;
第46~59行,如果判断全局变量g_usart_rx_sta的最高位是0,则执行一段时间,然后往上位机发送提示字符,以及让LED0每隔一段时间翻转,提示系统正在运行。
20.5 编译和测试
        以上代码添加完毕以后,编译工程无报错以后,用Type-C线接在开发板的USB_TTL接口上,线的一端接在电脑的USB口上,按照前面的步骤连接好ST-Link,同时注意开发板上的JP11处的跳线帽是否已经接好,如果跳线帽没接,那么UART4则无法正常通信,拨码开关拨成001,即MCU启动模式,进入Debug模式。
第二十章 串口通信实验50825.png
图20.5.1开发板连接方式

        双击开发板光盘A-基础资料\3、软件下的串口软件XCOM V2.0.exe将其打开:
第二十章 串口通信实验50886.png
图20.5.2打开XCOM V2.0

        打开XCOM V2.0以后,选择Type-C接口对应的串口(笔者的是com61),设置波特率为115200,停止位为1,数据位为8,无奇偶校验位,即保持和前面配置工程的时候一样的参数配置,在串口操作处选择打开串口(打开串口以后显示的字眼是关闭串口):
第二十章 串口通信实验51034.png
图20.5.3设置打开XCOM V2.0的参数

        进入Debug以后,点击运行按钮,可以看到串口软件第一句打印的是:“正点原子 STM32MP1 开发板 串口实验”,然后每隔一段时间时间打印句话“请输入数据,以回车键结束”
第二十章 串口通信实验51149.png
图20.5.4串口输出printf的信息

        我们在数据输入框中输入我们想要发送的字符,例如输入“正点原子”,输入好以后鼠标点击发送,然后看到串口软件上显示我们发送的字符,串口中断接收回显实验验证完成:
第二十章 串口通信实验51253.png
图20.5.5串口接收回显

        为了加深大家对串口发送和接收的理解,以上代码是通过操作TDR寄存器来实现发送,通过观察ISR寄存器来检查是否发送已完成,同时,在中断服务函数中执行HAL_UART_Receive_IT函数来开启串口中断(每次发送完成,串口中断会关闭)。
        下面我们也可以使用另一种方式来实现,即将串口接收回调函数修改为:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  HAL_UART_Transmit(&g_uart4_handle,&RxBuffer,1,0); /* 发送 */
  HAL_UART_Receive_IT(&g_uart4_handle,&RxBuffer,1);/* 启动串口中断 */
}
        即,每次进入回调函数后会执行发送,并开启串口中断,这样一来就可以连续不断发送和接收数据了。
        这里说明一下函数HAL_UART_Receive_IT和HAL_UART_Receive的差别:HAL_UART_Receive函数没有回调函数,使用轮询(阻塞模式)的方式接收数据,其规定了一个接收超时机制,如果函数接收超时就返回。HAL_UART_Receive_IT函数并不接收数据,只是打开对应的串口中断,数据接收是在串口中断中进行的。在一个工程中不能同时使用这两个函数,如果同时使用这两个函数,那么HAL_UART_Receive不能进入阻塞模式,此函数就形同虚设了。
第二十章 串口通信实验51895.png
图20.5.6几个文件的关系

        最后,main.c函数修改为:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/EXTI/exti.h"
#include "./SYSTEM/usart/usart.h"

/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{
    HAL_Init();     /* 初始化HAL库  */
    extern uint8_t g_rx_buffer;
    /* 初始化M4内核时钟 */
    if(IS_ENGINEERING_BOOT_MODE())
    {
        sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    usart_init(115200);     /* 串口初始化为115200 */
led_init();             /* 初始化LED     */
        HAL_UART_Receive_IT(&g_uart4_handle,&g_rx_buffer,1);/* 以中断方式接收函数 */
    printf("\r\n正点原子 STM32MP1 开发板 串口实验\r\n");
    while(1)
    {
        printf("请输入数据,以回车键结束\r\n");
         HAL_Delay(6000);
    }
}
        以上代码也是可以实现串口发送和接收回显的,具体代码可以参考:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验9-1 串口通信实验2。





回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-20 10:12

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表