1)实验平台:正点原子新起点V2开发板
2)平台购买地址:/item.htm?id=609758951113
2)全套实验源码+手册+视频下载地址:/thread-300792-1-1.html
3)对正点原子FPGA感兴趣的同学可以加群讨论:994244016
4)关注正点原子公众号,获取最新资料更新
第十四章 串口通信实验
本章节我们来学习STM32MP1的串口使用方法,并通过串口发送和接收数据。本章将分为如下几个小节:14.1、串口简介;14.2、STM32MP1串口简介;14.3、HAL库中串口相关的API;14.4、串口中断接收回显实验;14.5、章节小结;
14.1 串口简介
在学习串口前,我们先来了解一下数据通信的一些基础概念。
14.1.1 数据通信的基础概念
在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们最常见的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。
数据通信方式
按数据通信方式分类,可分为串行通信和并行通信两种。串行和并行的对比如下图所示:
图14.1.1. 1数据传输方式
串行通信的基本特征是数据逐位顺序依次传输,最少只需一根传输线即可完成,所需传输线少成本低,但每次只能传送1bit的信号,比如1Byte的信号需要8次才能发完,传输速率低。串行通讯抗干扰能力强,可用于远距离传输,传输距离可以从几米到几千米。
并行通信是数据各位可以通过多条线同时传输,一次传送8bit、16bit、32bit甚至更高的位数,相应地就需要8根、16根、32根甚至更多的信号线,优点是传输速率高,缺点就是线多成本就高了,抗干扰能力差,因而适用于短距离、高速率的通信。
2. 数据传输方向
根据数据传输方向,通信又可分为全双工、半双工和单工通信,它们的比较如下图所示:
图14.1.1. 2数据传输方式
单工是指数据传输仅能沿一个方向,不能实现反方向传输;
半双工是指数据传输可以沿着两个方向,但是需要分时进行;
全双工是指数据可以同时进行双向传输;
这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信(分时进行),而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。
3. 数据同步方式
根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:
图14.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)
14.1.2 串口通信接口标准
常用的串行通信接口标准中,有RS-232、RS-422和RS-485接口。
接口
(1)RS-232
电脑机箱后面一般会有2个9芯或者25芯的接口,也分别叫DB9和DB25连接器,它们是符合RS-232C标准的串行接口,可用于连接MODEM设备或者工业仪器仪表等,实现设备与电脑相互通迅。我们先来看看RS232接口,下图是DB9针的连接头,左边公头的1脚接的母头的1脚,其它脚也按数字对应相连。
图14.1.2. 1 RS232接口
RS-232C定义了数据终端设备(DTE)和数据通信设备(DCE)之间串行二进制数据交换接口技术标准,我们以DB9连接器为例,此标准接口的主要引脚定义如下表:
表14.1.2. 1 RS232接口引脚信号
使用RS-232,对于一般双工通信,仅需要一条发送线、一条接收线和一条地线就可以实现,这3根线构成共地传输,容易产生共模干扰,抗噪声抗干扰性弱,所以使用RS-232最大通信距离为一般为15m。RS-232只允许一对一通信,以两个计算机通信为例,如果是近距离通信,两个接口可以直连:
图14.1.2. 2两台设备近距离通信
如果是长距离通信,则需要通过数据通信设备,如调制解调器来实现。调制解调器是实现数字信号和模拟信号相互转换的设备。当计算机通过电话线连入互联网时,发送方的计算机发出的数字信号,通过调制解调器转换成模拟信号在电话网上传输;接收方的计算机通过调制解调器将传输过来的模拟信号转换成数字信号。通过调制解调器,两台电脑实现通信。
图14.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采用平衡发送和差分接收的方式,抗噪声干扰性好,传输距离可以达几十米到上千米。
图14.1.2. 4 RS485通信
下表中列出RS-232、RS-422和RS-485的对比:
表14.1.2. 2 RS-232、RS-422和RS-485通信对比
2. 电气标准
在电子设计中,要保证不同设备的信号之间的通讯,就会涉及到通信协议以及电平标准,电平标准有很多,常见的电平标准有TTL、RS232、RS485、COMS和LVDS等。TT(Transistor-Transistor Logic),即晶体管逻辑集成电路,TTL电平是数字电路的一种通用接口电平标准,TTL电平信号直接与集成电路连接,下面我们以一张表格来区分串行通信的电平标准。
表14.1.2. 3电气标准
14.1.3 串口通信协议简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和按位接收字节,一个字节(Byte)有8位,串口必须把这8位依次发送出去才可以完成一个字节的发送,比起按字节发送的并行通信速率要慢,效率较低,但是所需的线少,成本低,通信较稳定。
图14.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机的串口连接来实现通信。
图14.1.3. 2 USB转串口线
正点原子STM32MP157开发板就是用CH340C芯片来完成串口UART和电脑之间的通信的,开发板引出Type-C接口,硬件连接上非常方便,只需要一根Type-C 线就可以。理论上,USB2.0版本,Type-C接口最大传输速率为480Mbps,而USB3.0以上版本可以达到5Gbps或者更高的传输速率。如下图位号为USB_TTL的接口(Type-C接口)是我们后面串口实验要用到的接口。
图14.1.3. 3正点原子STM32MP157开发板USB_TTL接口
下面我们来学习串口通信协议,这里主要学习串口通信的协议层。
串口通信的数据包由发送设备的TXD接口传输到接收设备的RXD接口。在串口通信的协议层中,规定了数据包的内容,它由启始位、主体数据位、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如下图所示。
图14.1.3. 4串口通信协议数据帧格式
串口通信协议数据包组成可以分为波特率和数据帧格式两部分。
波特率
串口通信分为同步通信(UART)和异步通信(USART),本章主要讲解的是串口异步通信(USART),异步通信是不需要时钟信号的,但是需要约定两个设备的波特率一致才可以正常通信。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同,常见的波特率是4800、9600、115200等。
根据波特率可以计算出每帧内部数据位各位之间的时间间隔(不是帧与帧之间的时间间隔),1s除以波特率可得时间,例如波特率为115200的时间间隔为1s /115200(波特率) = 8.68us。如果是9600波特率,则时间间隔约为104.2us。数据帧格式
数据帧格式需要我们提前约定好,串口通信的数据帧包括起始位、停止位、有效数据位以及校验位。我们来看看这些数据位:
起始位和停止位
串口通信的一个数据帧是从起始位开始,直到停止位,通信的双方要约定一致。
数据帧中的起始位是由一个逻辑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 状态(高电平),表示当前线路上没有数据在传送,空闲位的长度是不定的。如下图是异步串口通信中,帧与帧之间的空闲间隔是随机的。
图14.1.3. 5空闲信号是不定长的
14.2 STM32MP1串口简介
14.2.1 STM32MP157串口资源
串口功能
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的功能对比表。
图14.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标注的不是很正确)
图14.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总线上。下图是参考手册中串口数据的部分截图。
图14.2.1. 3 STM32MP157的串口时钟来源
14.2.2 USART寄存器
STM32MP1的串口和其它STM32串口一样,只要你开启了串口时钟,并设置相应IO口的模式,然后配置波特率、数据位长度、奇偶校验位等信息,就可以使用了。下面,我们就简单介绍下几个与串口基本配置直接相关的寄存器。
串口时钟使能寄存器
串口作为STM32MP157的一个外设,其时钟由APB1外设时钟使能寄存器RCC_MC_APB1ENSETR控制,我们后面实验中用到的串口4是在RCC_MC_APB1ENSETR寄存器的第16位,只要对该位写1即可使能串口4的时钟。在后面的实验中,通过在STM32CubeMX上配置,系统会自动为我们完成这步操作。
图14.2.2. 1 APB1外设使能MCU设置寄存器
2. 串口波特率设置寄存器
图14.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模式下的情况来介绍。
图14.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)时,才可以写此位。
第9位,PCE为奇偶校验控制使能位。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寄存器中,我们来看看这两个寄存器
图14.2.2. 4数据发送与接收寄存器
USART_RDR的其它位保留,只有第08位可读,其包含接收到的数据字符,具体多少位,由前面介绍的M[1:0]决定。类似的,USART_TDR的第08位包含要传输的数据字符。当我们需要发送数据的时候,往USART_TDR寄存器写入想要发送的数据,就可以通过串口发送出去了。而当有串口数据接收到,需要读取出来的时候,则必须读取USART_RDR寄存器的置。
我们前面有说过,在有效数据位中,一般是低位(LSB)在前,高位(MSB)在后,在启用奇偶校验的情况下进行接收时,在MSB位中读取的值为接收的奇偶校验位,什么意思呢?即当使能校验位(USART_CR1中PCE位被置1)进行发送时,写到MSB的值会被后来的校验位取代,当使能校验位进行接收时,读到的MSB位是接收到的校验位。对于启用奇偶校验的情况,USART的数据帧格式如下表所示:
表14.2.2. 1串口数据帧格式表
例如M[1:0]=00,PCE=1,停止位为1位的时候,数据有8位,不过第7位的值被校验位改写了,第7位是校验位了。数据位为7位和9位的情况也是类似。
图14.2.2. 5 MSB位会被后来的校验位改写
5. 串口状态寄存器
串口状态通过状态寄存器USART_ISR读取,其各位描述如图所示:
图14.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的框图。
14.2.3 USART框图
下面我们先来学习USART框图,通过USART框图引出USART相关的知识点,对之后的编程也会有一个清晰的思路。
图14.2.3. 1 USART框图
为了方便大家理解,我们把整个框图用几根红线分成几个部分来介绍。
时钟与波特率
框图中①区域主要功能就是为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模式)的情况下,波特率通过以下公式得出:
在8倍过采样的情况下,波特率通过以下公式得出:
以上公式中,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,那么可得:
计算出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 寄存器中写入要发送的数据。为每个要在单缓冲区模式下发送的数据重复这一步骤:
—禁止 FIFO 模式时,向 USART_TDR 写入数据会将 TXE 标志清零;
—使能 FIFO 模式时,向 USART_TDR 写入数据会为 TXFIFO 增添一个数据。当 TXFNF标志置 1 时,会对 USART_TDR 执行写操作。该标志会保持置 1,直到 TXFIFO已满。
⑧将最后一个数据写入 USART_TDR 寄存器后,等待 TC =1(表示传送已完成)。也是分为两种情况:
—禁止 FIFO 模式时,这表示最后一个帧的发送已完成;
—使能 FIFO 模式时,这表示 TXFIFO 和移位寄存器均为空。
图14.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”,则表示起始位有效。如下图所示:
图14.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以使能接收器开始搜索起始位。
14.2.4 GPIO引脚复用功能
芯片的外设有很多,而芯片的引脚资源却是有限的,如何在有限的引脚里拓展出更多的功能呢,这里就涉及到了复用。复用,就是说,一个引脚除了可以当做普通的IO口功能以外,还可以与一些外设关联起来,作为第二功能或者第三功能甚至更多的功能使用,不过在这些功能当中,在同一个时刻中只能选用其中一种功能来用,这样就不会导致冲突。
复用功能寄存器
前面我们是有介绍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寄存器共同决定。
图14.2.4. 1 AFRL寄存器
2. 复用功能映射表
IO口并不是想复用什么功能都可以,是有规定的,每个 IO 引脚的复用可以通过查阅数据手册的复用功能映射表来确定,例如我们需要配置PG11复用为UART4_TX,即多路复用器选择为复用功能AF6,则配置寄存器GPIOG_AFRL的AFR11的第12至第15位为0110。虽然可以在STM32CubeMX插件上可以使用图形界面来配置IO口的复用功能,但是它是怎么配置的我们还是要知道的。
图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。具体的场景应用会在我们后面的实验中有所体现。
14.3 HAL库中串口相关的API
为了更好地使用HAL库来实现串口功能,下面我们来介绍HAL库中和本实验相关的结构体和API函数。
14.3.1 结构体和句柄
我们先来看看和串口相关的结构体和句柄。我们在介绍HAL库的时候有提到句柄(Handle),句柄在HAL库中是一个指针,指针指向地址,在调用API函数的时候,可以利用句柄来说明要操作哪些资源。在stm32mp1xx_hal_uart.h中有定义句柄和结构体。
USART_TypeDef结构体
通过第5.2.3小节总线架构分析,我们知道UART4挂在APB1总线上,UART4的地址范围是0x40010000 ~ 0x400103FF,即UART4的基地址是0x40010000。我们前面也说过(第6.3.2小节),通过寄存器的基地址和偏移地址就可以访问一个寄存器,而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;
UART_HandleTypeDef句柄
UART_HandleTypeDef句柄是基于以上结构体以及HAL库的函数封装后的,其定义如下:
/* UART句柄结构定义 */typedef struct __UART_HandleTypeDef{USART_TypeDef *Instance; /* UART寄存器基地址 */UART_InitTypeDefInit; /* 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_ENABLEDMDMA_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_tErrorCode;/* 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模式下:
如果过采样为8:波特率寄存器[3] = 0
(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 没有做初始状态。
14.3.2 HAL库中的API函数
STM32MP1串口有3种通信方式:轮询、中断、DMA,其中,轮询方式为阻塞模式,DMA和中断方式为非阻塞模式。阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前的线程会被挂起,调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。在前面的按键实验中,一个采用了按键轮询的方式,一个是采用了中断的方式,前者也称为阻塞,后者称为非阻塞。
stm32mp1xx_hal_uart.c文件下有很多和USART相关的库函数,这里我们只介绍几个重要的函数,DMA相关的函数这里就不介绍了。
初始化USART模式
●函数功能:根据UART_InitTypeDef中指定的参数初始化USART模式,并初始化关联的句柄。
●函数参数:UART_InitTypeDef结构体指针变量,可以指定要使用的串口。(前面已经分析了串口句柄)
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
具体的函数定义,我们这里就不列出来了。
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)以查询的方式发送/接收函数
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) /串口轮询模式接收,使用超时管理机/以中断方式发送/接收函数
●函数功能:以中断的方式发送/接收指定字节
●函数参数:
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_tpData, uint16_t Size) /串口中断模式发送 */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_tpData, uint16_t Size) /串口中断模式接收 */中断请求函数
●函数功能:串口中断请求函数
●函数参数:huart,UART_HandleTypeDef 类型结构体指针变量,可以指定要使用的串口;
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
此函数是串口中断请求函数,如果前面使用了轮询(阻塞)的方式发送和接收数据,那么此函数就会被串口中断服务函数调用来完成中断的功能。
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)回调函数
在非阻止模式下提供了一组传输完成回调函数,这些回调函数都是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)
传输中断函数
以下是一些中止正在进行的发送/接收传输函数(中断模式和阻塞模式)。
/* 中止正在进行的传输(阻塞模式) */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);
14.4 串口中断接收回显实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 7-1 UART4。
14.4.1 硬件设计
例程功能
通过串口中断将发送出的数据打印出来。这里我们使用开发板上的UART4来实现。硬件资源
本实验中我们会用到开发板底板的USB_TTL接口,此接口的引脚资源如下表所示,我们还需要一根配套的Type-C线来实现电脑和开发板进行串口通信。
UART4_TX UART4_RX
PG11 PB2
图14.4.1. 1硬件资源表原理图
USB_TTL接口硬件部分的原理图如下图所示,其中UART4_TX发送端接的PG11,UART4_RX接收端接的PB2,实验中我们就配置这两个引脚。
图14.4.1. 2 UART4两个引脚
14.4.2 软件设计
程序流程图
图14.4.2. 1程序流程图
2. 生成初始化代码
我们新建一个工程UART,再STM32CubeMX插件上配置如下:
(1)引脚配置
配置PG11复用为UART4_TX,配置PB2复用为UART4_RX,如下图:
图14.4.2. 2复用为UART4
如上图,配置好复用以后,接下来配置IO口模式,进入System Core GPIOUART中,PG11和PB2均为上拉模式,且分别定义一个User Lable宏(宏可以定义或者不定义,这里是为了后面方便查看代码)UART4_TX和UART4_RX。
图14.4.2. 3设置User Lable宏
(3)UART4配置
我们用到UART4,进入System Core Connectivity UART4中,UART4通信模式配置为异步通信,Hardware Flow Control(硬件流控制)就不用配置了,我们这里用不到。在Parameter Settings(参数设置)处可以看到系统已经自动为我们配置好了默认的参数:
波特率为115200Bit/s;字长为8位;无校验位;1位停止位;数据方向为发和收;16倍过采样;Clock Prescaler(时钟预分频器)分频值为1。
图14.4.2. 4 UART4参数配置
上面的参数我们采用默认的配置,如果需要修改某个参数,鼠标选中要修改的选项后手动进行配置即可:
图14.4.2. 5可以手动配置参数
(2)NVIC配置
实验中会用到串口接收中断,所以要配置NVIC。我们先在NVIC Settings 处开启UART4全局中断,然后再返回到NVIC处配置中断的抢占优先级和子优先级。
如下图,使能UART4全局中断,我们注意到此时抢占优先级和子优先级默认都是0。其他的配置我们用不到,就不需要配置了,例如DMA Settings是配置DMA模式的,我们这里使用的是中断模式,不用DMA。
图14.4.2. 6开启串口全局中断使能
如下图,在NVIC处配置优先级,我们配置优先级分组为2,抢占优先级和子优先级均为3。当然也可以配置为其它的优先等级,不过要记住的是,如果中断中用到HAL_Delay函数的话,对应的中断优先级不能比SysTick的优先级高(SysTick的抢占优先级和子优先级默认为0,属于最高级别优先级),这点我们在前面的外部中断实验章节有详细分析过。
图14.4.2. 7设置优先级
Code generation的配置就保持默认,如下图:
图14.4.2. 8 Code generation的配置
(3)时钟配置
这里,我们默认采用外部晶振。在System CoreRCC处选择打开HSE,并选择Crystal/Ceramic Resonator 晶体/陶瓷谐振器选项。
图14.4.2. 9开启HSE
然后,配置UART4的时钟来自于APB1,且配置最大为104.5MHz。
MCU子系统的时钟最大只能为209MHz,如下图,手动输入209以后按下回车键,STM32CubeMX插件会自动计算分频和倍频系数,然后在APB1DIV、APB2DIV、APB3DIV处输入2,因为APB1、APB2、APB3时钟最大值只能为104.5MHz,所以我们要手动设置分频值。设置好后,如下图,UART4的时钟频率为1045.MHz。STM32CubeMX会根据此频率以及默认的波特率115200来计算USARTDIV的值(也就是USART_BRR的值,计算出十进制值为907)。
图14.4.2. 10动态配置时钟
(4)配置生成独立的.c和.h头文件,如下图:
图14.4.2. 11配置生成独立的文件
以上的配置检查无误后,按下键盘的“Ctrl+S”组合键保存配置并生成工程:
图14.4.2. 12生成工程
3. 添加用户代码
这里,我们先按照步骤添加用户代码,后面再来分析整个实验代码的实现过程。
生成的工程中,有usart.c和usart.h文件,我们在usart.h文件中添加如下代码:
uint8_t RxBuffer;然后在usart.c文件后面的USER CODE BEGIN 1和USER CODE END 1之间添加如下代码:/* USER CODE BEGIN 1 */void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle){HAL_UART_Transmit(&huart4,&RxBuffer,1,0);HAL_UART_Receive_IT(&huart4,&RxBuffer,1);}/* USER CODE END 1 */
在main.c文件中添加如下标红的代码,添加的代码位置在成对出现的BEGIN和END之间,可以打开实验的工程来查看代码添加的位置(后面我们分析工程的时候会列出完整的代码)。
图14.4.2. 13添加的代码位置
/* USER CODE BEGIN 2 *//* printf("请输入字符,并按下回车键结束\r\n"); */HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* USER CODE END 2 *//* USER CODE BEGIN 3 */printf("\nPlease enter characters and press enter to end\r\n");HAL_Delay(6000);/* USER CODE END 3 *//* USER CODE BEGIN 4 */#ifdef __GNUC__#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)#else#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)#endifPUTCHAR_PROTOTYPE{/* 本实验使用的是UART4,如果使用的是其它串口,则将UART4改为对应的串口即可 */while ((UART4->ISR & 0X40) == 0);UART4->TDR = (uint8_t) ch;return ch;}/* USER CODE END 4 */
14.4.3 工程代码分析
我们来分析本工程的代码,理解串口是如何实现发送数据和接收数据的。
gpio.c文件
gpio.c文件的代码比较简单,主要是开启对应GPIO的时钟,这里注意,HSE的两个引脚PH0-OSC_IN和PH1-OSC_OUT挂在GPIOH上,所以也要开启GPIOH的时钟。
#include "gpio.h"void MX_GPIO_Init(void){/* 开启GPIOH时钟,因为HSE的两个引脚PH0-OSC_IN和PH1-OSC_OUT挂在GPIOH上 */ __HAL_RCC_GPIOH_CLK_ENABLE();__HAL_RCC_GPIOG_CLK_ENABLE();/* 开启GPIOG时钟 */__HAL_RCC_GPIOB_CLK_ENABLE();/* 开启GPIOB时钟 */}
stm32mp1xx_hal_msp.c文件
stm32mp1xx_hal_msp.c文件我们前面也是分析过,用户可以在此文件中编写回调函数,这里主要就是开启HSEM时钟,HSEM用于管理一些共享资源的访问权限和同步,保护GPIO和外部中断EXTI配置免受并发访问。同时设置中断优先级分组为2,此优先级是用户在STM32CubeMx上设置的。关于优先级分组初始化,我们在外部中断实验有详细分析过。
#include "main.h"void HAL_MspInit(void){__HAL_RCC_HSEM_CLK_ENABLE();/* 开启HSEM时钟,表示使能HSEM *//* 设置中断优先级分组为2 */HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);}
stm32mp1xx_it.c文件
stm32mp1xx_it.c文件我们在外部中断实验章节也有详细分析过,这里就不贴出所有代码了。我们主要分析第19~22行,在前面,我们在STM32CubeMX上有开启了UART4全局中断,系统会自动生成中断服务函数UART4_IRQHandler,UART4_IRQHandler函数调用了函数HAL_UART_IRQHandler,我们来看看此函数做了些什么工作。
1 #include "main.h"2 #include "stm32mp1xx_it.h"3 4 /************************************************/5 /* 省略掉部分 Cortex-M4处理器中断和异常处理程序 */6 /************************************************/7 void SysTick_Handler(void)8 {9 /* 此功能处理系统滴答计时器每隔1ms产生一次中断 */10 HAL_IncTick();11 }12 /*****************************************************************/13 /* STM32MP1xx外围设备中断处理程序 */14 /* 在此处添加所用外围设备的中断处理程序 */15 /* 有关可用的外设中断处理程序名称,请参阅启动文件(startup_stm32mp1xx.s)*/16 /*****************************************************************/1718 /* 该函数处理UART4全局中断 */19 void UART4_IRQHandler(void)20 {21 HAL_UART_IRQHandler(&huart4);22 }
HAL_UART_IRQHandler函数在文件stm32mp1xx_hal_uart.c中,我们省去部分代码,如下:
1 void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)2 {3uint32_t isrflags = READ_REG(huart->Instance->ISR);4uint32_t cr1its= READ_REG(huart->Instance->CR1);5uint32_t cr3its= READ_REG(huart->Instance->CR3);6 7uint32_t errorflags;8uint32_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模式接收器 */15if (((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 {21huart->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 {32if (huart->TxISR != NULL)33{34 huart->TxISR(huart);35}36return;37 }3839 /* UART模式发送器(发送结束)*/40 if (((isrflags & USART_ISR_TC) != 0U) && ((cr1its & USART_CR1_TCIE) != 0U))41 {42UART_EndTransmit_IT(huart);43return;44 }45 /****** 此处省略掉部分代码 ******/46 }
查看第20~25行之间的代码,通过检查几个标志位看看是否有错误发生,这几个位对应USART_ISR寄存器的某些位,在stm32mp157dxx_cm4.h文件中有定义。
表14.5. 1USART_ISR寄存器的某些位
如果没有错误发生,则再检查USART_ISR寄存器RXFNE位、USART_CR1寄存器的RXNE位、USART_CR3寄存器的RXFTIE位是否都为0,如果都不为0,表示已经准备好要接收的数据,并开启了中断:
当USART_RDR 移位寄存器的内容已传输到 USART_RDR 寄存器时,RXNE 位由硬件置1 ,表示USART_RDR 移位寄存器的内容已传输到 USART_RDR 寄存器,此时可以对USART_RDR 寄存器执行读取操作。
表14.5. 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模式 */8if ((huart->FifoMode == UART_FIFOMODE_ENABLE) && (Size >= huart->NbRxDataToProcess))9{10/* 根据数据字长设置Rx ISR功能指针 */11if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))12{13 huart->RxISR = UART_RxISR_16BIT_FIFOEN; /* 字长为9位 */14}15else16{17 huart->RxISR = UART_RxISR_8BIT_FIFOEN; /* 字长为7或者8位*/18}19 __HAL_UNLOCK(huart);20SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);21SET_BIT(huart->Instance->CR3, USART_CR3_RXFTIE);22} 23 else24 {25/* 根据数据字长设置Rx ISR功能指针 */26if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))27{28 huart->RxISR = UART_RxISR_16BIT; /* 字长为9位*/29}30else31{32 huart->RxISR = UART_RxISR_8BIT; /* 字长为7或者8位 */33}3435__HAL_UNLOCK(huart);3637/* 使能UART奇偶校验错误中断和数据寄存器非空中断 */38SET_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 {9uhdata = (uint16_t) READ_REG(huart->Instance->RDR);10 *huart->pRxBuffPtr = (uint8_t)(uhdata & (uint8_t)uhMask);11 huart->pRxBuffPtr++;12 huart->RxXferCount--;1314 if (huart->RxXferCount == 0U)15 {16/* 关闭UART奇偶校验错误中断和RXNE中断 */17CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE_RXFNEIE | \USART_CR1_PEIE));1819/* 关闭UART错误中断:(帧错误,噪声错误,溢出错误)*/20CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);2122/* Rx进程完成,将huart-> RxState还原为Ready */23huart->RxState = HAL_UART_STATE_READY;2425/* 清除RxISR函数指针 */26huart->RxISR = NULL;2728 #if (USE_HAL_UART_REGISTER_CALLBACKS == 1)29/* 调用注册的Rx完成回调函数 */30huart->RxCpltCallback(huart);31 #else32/* 调用旧版弱Rx完成回调函数*/33HAL_UART_RxCpltCallback(huart);34 #endif 35 }36 }37 else38 {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等函数,而打开的这些中断函数才是真正的串口中断接收处理函数。
usart.c文件
我们在usart.h文件中声明一个变量RxBuffer,此变量用于设置串口发送和接收缓冲区的大小。
uint8_t RxBuffer;
usart.c文件是UART4的初始化代码,因代码比较多,我们这里分成几部分列出。
usart.c文件代码1
1 #include "usart.h"2 3 UART_HandleTypeDef huart4; /* UART句柄 */4 /**5 * @brief 串口UART4初始化函数6 * @param 无7 * @note注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.8 * 这里的UART4的时钟源在SystemClock_Config()函数中已经设置好了.9 * @retval 无10 */11 void MX_UART4_Init(void)12 {13 huart4.Instance = UART4; /* USART4 */14 huart4.Init.BaudRate = 115200; /* 波特率115200 */15 huart4.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */16 huart4.Init.StopBits = UART_STOPBITS_1; /* 1个停止位 */17 huart4.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */18 huart4.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */19 huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */20 huart4.Init.OverSampling = UART_OVERSAMPLING_16;/* 16倍过采样 */21 /* 指定选择3个样本 */22 huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;23 /* 指定用于分频UART时钟源的预分频器值为1 */24 huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1;25 /* 不初始化UART的高级功能 */26 huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;27 /* HAL_UART_Init()会使能UART4 */28 if (HAL_UART_Init(&huart4) != HAL_OK)29 {30Error_Handler();31 }32 if (HAL_UARTEx_SetTxFifoThreshold(&huart4, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)33 {34Error_Handler();35 }36 if (HAL_UARTEx_SetRxFifoThreshold(&huart4, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)37 {38Error_Handler();39 }40 if (HAL_UARTEx_DisableFifoMode(&huart4) != HAL_OK)41 {42Error_Handler();43 }44 45 }
如上第一部分代码:第4行,初始化串口句柄huart4,后面通过此句柄来完成串口的初始化。剩下的部分代码,初始化UART4的重要参数:波特率为115200,字长8位,1个停止位,无奇偶校验位、收发模式、无硬件流、16倍过采样。接下来我们看看第二部分代码:
usart.c文件代码2
46 /**47 * @brief UART底层初始化函数48 * @param huart: UART句柄类型指针49 * @note 此函数会被HAL_UART_Init()调用50 * 完成时钟使能、引脚配置、中断优先级配置51 * @retval无52 */53 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)54 {55 56 GPIO_InitTypeDef GPIO_InitStruct = {0};57 RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};58 if(uartHandle->Instance==UART4)59 {60 if(IS_ENGINEERING_BOOT_MODE())61 {62 /* 指定UART24时钟源WEI PCLK1(APB1)设置UART4时钟源=APB1=104.5MHz */63PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_UART24;64PeriphClkInit.Uart24ClockSelection = RCC_UART24CLKSOURCE_PCLK1;65if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)66{67 Error_Handler();68}69 70 }71 72/* UART4 时钟使能 */73__HAL_RCC_UART4_CLK_ENABLE();74 /* RX和TX引脚时钟使能时钟使能 */75__HAL_RCC_GPIOG_CLK_ENABLE();76__HAL_RCC_GPIOB_CLK_ENABLE();77/**UART4 GPIO Configuration78PG11------> UART4_TX79PB2------> UART4_RX80*/81GPIO_InitStruct.Pin = UART4_TX_Pin;/* TX引脚 */82GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;/* 复用推挽输出 */83GPIO_InitStruct.Pull = GPIO_PULLUP;/* 上拉 */84GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;/* 高速 */85GPIO_InitStruct.Alternate = GPIO_AF6_UART4;/* 复用为UART4 */86/* 初始化发送引脚 */87HAL_GPIO_Init(UART4_TX_GPIO_Port, &GPIO_InitStruct);88 89GPIO_InitStruct.Pin = UART4_RX_Pin;/* RX引脚 */90GPIO_InitStruct.Mode = GPIO_MODE_AF;/* 复用为UART4 */91GPIO_InitStruct.Pull = GPIO_PULLUP;/* 上拉 */92GPIO_InitStruct.Alternate = GPIO_AF8_UART4;/* 初始化接收引脚 */93/* 初始化接收引脚 */94HAL_GPIO_Init(UART4_RX_GPIO_Port, &GPIO_InitStruct);95 96/* UART4 中断优先级初始化 */97HAL_NVIC_SetPriority(UART4_IRQn, 3, 3);/* 抢占优先级3,子优先级3 */98HAL_NVIC_EnableIRQ(UART4_IRQn); /*使能UART4中断号为UART4_IRQn=52 */99 }100 }
以上代码主要是使能UART4时钟、配置UART4两个引脚的工作模式和设置中断优先级。第53~76行,MX_UART4_Init函数用于开启UART4和两个引脚(TX和RX)的时钟,且时钟来源于APB1,设置最大为104.5MHz。当然也可以设置为其它的频率值,需要在STM32CubeMX上手动配置。第81行~94行,配置UART的两个引脚的模式以及复用功能。两个引脚均复用为UART4,开启上拉。第97行,设置UART4的抢占优先级和子优先级均为3。第98行,使能UART4中断。这里注意,UART4的中断号是固定的,在文件stm32mp157dxx_cm4.h中有定义,在前面的外部中断实验中我们有分析过中断号、中断向量表和中断服务函数的对应关系,如果不清除这些关系的,还可以回头看看中断服务函数章节的章节小结部分。接下来是USART的去初始化的代码(也就是反初始化),即关闭UART4时钟,不初始化两个引脚,关闭中断。
usart.c文件代码3
101102 void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)103 {104105 if(uartHandle->Instance==UART4)106 {107/* 关闭UART4时钟 */108__HAL_RCC_UART4_CLK_DISABLE();109110/**UART4 GPIO Configuration111PG11------> UART4_TX112PB2------> UART4_RX113*/114HAL_GPIO_DeInit(UART4_TX_GPIO_Port, UART4_TX_Pin);115116HAL_GPIO_DeInit(UART4_RX_GPIO_Port, UART4_RX_Pin);117118/* UART4 中断去初始化 */119HAL_NVIC_DisableIRQ(UART4_IRQn);120 }121 }我们来看看最后部分的代码,这段代码是我们前面添加上去的:usart.c文件代码4122123 /* USER CODE BEGIN 1 */124 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)125 {126 HAL_UART_Transmit(&huart4,&RxBuffer,1,0);/* 以查询方式发送字符 */127 HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* 重新打开串口中断 */128 }129 /* USER CODE END 1 */
在没有编写回调函数的时候,串口只能完成一次发送和接收,前面我们分析了,串口在完成一次中断接收以后,就关闭串口中断了,如果想要实现字符的循环发送和接收的话,可以在回调函数里添加数据发送函数HAL_UART_Transmit和函数HAL_UART_Receive_IT来实现,这样,只要串口有数据在发送,就可以接收数据。这里说明上面的重要参数:第126行,参数一指定的串口是huart4;参数二,RxBuffer是一个发送缓存区的地址,电脑发送过来的数据会堆放在这个发送缓冲区里;参数三是指定发送缓存区域的大小,这里是1个字节,发送缓冲区满(达到1个字节)以后,串口就发送数据,这里注意,回车也表示1个字节,如果按下回车,串口就会发送数据(因为回车是一个字节,发送缓冲最大设置为1个字节);参数四是指溢出时间,时间为0,溢出时间是通过Systick来计时的,我们前面也多次说过。第127行,参数一,使用的串口为huart4;参数二,RxBuffer是接收缓存区的地址,huart4接收数据的时候会把它们放在这个区域中;参数三是指定接收缓存区域的大小为1个字节,也就是当接收缓冲区达到1个字节字符以后(表示接收结束),就会发生接收完成中断,也就调用一次回调函数进行相应处理,注意的是,接收到的回车也是一个字节,所以电脑只发送回车的话,会打印空行,后面的实验中我们会看到现象。
main.c文件
main.c函数的大部分代码我们都已经熟悉了,例如系统时钟配置部分,在前面的系统时钟实验部分我们已经分析过,这里就不再讲解了。我们来看看main.c文件的代码:
1 #include "main.h"2 #include "usart.h"3 #include "gpio.h"4 void SystemClock_Config(void);5 6 int main(void)7 {8HAL_Init(); /* 初始化HAL库 */9 10 if(IS_ENGINEERING_BOOT_MODE())11 {12/* 配置系统时钟 */13SystemClock_Config();14 }15 16 MX_GPIO_Init();/* 初始化已经配置的GPIO */17 MX_UART4_Init(); /* 初始化UART4 */18 19 /* USER CODE BEGIN 2 */20 /* printf("请输入字符,并按下回车键结束\r\n"); */21 HAL_UART_Receive_IT(&huart4,&RxBuffer,1);22 /* USER CODE END 2 */23 24 while (1)25 {26/* USER CODE END WHILE */27/* USER CODE BEGIN 3 */28 printf("\nPlease enter characters and press enter to end\r\n");29 HAL_Delay(6000);30 }31 /* USER CODE END 3 */32 }33 /**34 * @brief M4主频时钟设置函数,也就是设置PLL335 * @param plln: PLL3倍频系数(PLL倍频), 取值范围: 25~200.36 * @param pllm: PLL3预分频系数(进PLL之前的分频), 取值范围: 1~64.37 * @param pllp: PLL3的p分频系数(PLL之后的分频), 分频后作为系统时钟, 取值范围: 1~128.(且必须是2的倍数)38 * @param pllq: PLL3的q分频系数(PLL之后的分频), 取值范围: 1~128.39 * @note40 * MP157使用HSE时钟的时候,默认最高配置为:41 * CPU频率(mcu_ck) = MLHCLK = PLL3P / 1 = 209Mhz42 * hclk = MLHCLK = 209Mhz43 * AHB1/2/3/4 = hclk = 209Mhz44 * APB1/2/3 = MLHCLK / 2 = 104.5Mhz45 * @retval无;46 */47 void SystemClock_Config(void)48 {49 /* 定义RCC_OscInitStruct、RCC_ClkInitStruct结构体变量,并初始化为0 */50 RCC_OscInitTypeDef RCC_OscInitStruct = {0};51 RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};52 53 /*给RCC_OscInitTypeDef结构中的成员变量赋值完成初始化RCC振荡器 */54 RCC_OscInitStruct.OscillatorType = \ RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_LSI \55 |RCC_OSCILLATORTYPE_HSE;56 RCC_OscInitStruct.HSEState = RCC_HSE_ON;/* 打开HSE */57 RCC_OscInitStruct.HSIState = RCC_HSI_ON;/* 打开HSI */58 RCC_OscInitStruct.HSICalibrationValue = 16;/* 校准HSI值 */59 RCC_OscInitStruct.HSIDivValue = RCC_HSI_DIV1;/* 设置HSI分频值为1 */60 RCC_OscInitStruct.LSIState = RCC_LSI_ON;/* 开启LSI */61 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;/* 没有PLL的状态 */62 RCC_OscInitStruct.PLL2.PLLState = RCC_PLL_NONE;/* 没有PLL2的状态 */63 RCC_OscInitStruct.PLL3.PLLState = RCC_PLL_ON;/* 开启PLL3 */64 /* PLL3输入时钟源为HSE */ 65 RCC_OscInitStruct.PLL3.PLLSource = RCC_PLL3SOURCE_HSE;66 /*67 * 配置锁相环PLL3的分频和倍频参数,也就是:68 * DIVM3=2,DIVN3=52,DIVP3=3,DIVQ3=2,DIVR3=2,FRACV=204869 * 则PLL3的pll3_p_ck输出频率为:70 * pll3_p_ck=(hse_ck*(DIVN3+FRACV/2^13 ))/(DIVM3*DIVP3)=209MHz71 */72 RCC_OscInitStruct.PLL3.PLLM = 2;/* DIVM3=2 */73 RCC_OscInitStruct.PLL3.PLLN = 52;/* DIVN3=52 */74 RCC_OscInitStruct.PLL3.PLLP = 3;/* DIVP3=3 */75 RCC_OscInitStruct.PLL3.PLLQ = 2;/* DIVQ3=2 */76 RCC_OscInitStruct.PLL3.PLLR = 2;/* DIVR3=2 */77 RCC_OscInitStruct.PLL3.PLLRGE = RCC_PLL3IFRANGE_1;78 RCC_OscInitStruct.PLL3.PLLFRACV = 2048;/* FRACV=2048 */79 RCC_OscInitStruct.PLL3.PLLMODE = RCC_PLL_FRACTIONAL;/* 分数模式 */80 RCC_OscInitStruct.PLL4.PLLState = RCC_PLL_NONE;/* PLL4没有状态 */81 /* 调用的HAL_RCC_OscConfig函数用于判断 HSE、HSI、LSI、LSE 和82* PLL(PLL1、PLL2、PLL3和PLL4)是否配置完成,配置完成则返回HAL_OK。83* 如果没有配置完成,发生错误的话就会进入Error_Handler函数(空循环)。84*/85 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)86 {87Error_Handler();88 }89 /*90* 给RCC_ClkInitStruct结构体成员赋值来配置RCC时钟,也就是:91* 配置AXI的时钟源和分频器分频值(也就是配置ACLK);92* 配置MCU的时钟源和分频器分频值;93* 配置APB1~APB5的分频值(也就是配置PCLK1~5)。94*/95 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_ACLK96 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK297 |RCC_CLOCKTYPE_PCLK3|RCC_CLOCKTYPE_PCLK498 |RCC_CLOCKTYPE_PCLK5;99/* 配置AXI时钟源为HSI */ 100 RCC_ClkInitStruct.AXISSInit.AXI_Clock = RCC_AXISSOURCE_HSI;101 /* 配置AXI分频器为1分频=ACLK=64MHz */ 102 RCC_ClkInitStruct.AXISSInit.AXI_Div = RCC_AXI_DIV1;103 /* 配置MCU时钟源来自PLL3=209MHz */104 RCC_ClkInitStruct.MCUInit.MCU_Clock = RCC_MCUSSOURCE_PLL3;105 /* 配置MCU分频器为1分频=MCU=209MHz */106 RCC_ClkInitStruct.MCUInit.MCU_Div = RCC_MCU_DIV1;107 /* 配置APB4分频器为1分频=PCLK4=64MHz*/108 RCC_ClkInitStruct.APB4_Div = RCC_APB4_DIV1;109 /* 配置APB5分频器为2分频=PCLK5=64MHz */110 RCC_ClkInitStruct.APB5_Div = RCC_APB5_DIV1;111 /* 配置APB1分频器为2分频=PCLK1=104.5MHz */112 RCC_ClkInitStruct.APB1_Div = RCC_APB1_DIV2;113 /* 配置APB2分频器为2分频=PCLK2=104.5MHz */114 RCC_ClkInitStruct.APB2_Div = RCC_APB2_DIV2;115 /* 配置APB3分频器为2分频=PCLK3=104.5MHz */116 RCC_ClkInitStruct.APB3_Div = RCC_APB3_DIV2;117 /*118 * 调用HAL_RCC_ClockConfig函数,根据RCC_ClkInitStruct中119 * 指定的参数初始化MPU,AXI,AHB和APB总线时钟,如果初始化120 * 不成功,则进入Error_Handler空循环函数。121 */122 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct) != HAL_OK)123 {124Error_Handler();125 }126 /* 设置RTC时钟的HSE分频因子 */127 __HAL_RCC_RTC_HSEDIV(1);128 }129130 /* USER CODE BEGIN 4 */131 #ifdef __GNUC__132 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)133 #else134 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)135 #endif136 PUTCHAR_PROTOTYPE137 {138 /* 本实验使用的是UART4,如果使用的是其它串口,则将UART4改为对应的串口即可 */139while ((UART4->ISR & 0X40) == 0); /* 等待上一个字符发送完成 */140UART4->TDR = (uint8_t) ch; /* 将要发送的字符 ch 写入到TDR寄存器 */141return ch;/* 返回要发送的字符 */142 }143 /* USER CODE END 4 */144145 /* 发生错误时执行此函数 */146 void Error_Handler(void)147 {148 149 }150151 #ifdef USE_FULL_ASSERT152 /* 断言相关函数 */153 void assert_failed(uint8_t *file, uint32_t line)154 {155 156 }157 #endif
第21行的代码是我们前面手动添加的,调用HAL_UART_Receive_IT(&huart4,&RxBuffer,1)函数开启接收中断来完成一次串口接收。参数一指定串口huart4,参数二RxBuffer是串口接收缓冲区,参数三是1,表示缓冲区最大为1字节,也就是缓冲区达到1字节的时候就表示接收结束,然后进入回调函数HAL_UART_RxCpltCallback中,回调函数我们前面已经重新定义了,在前面的usart.c文件中我们已经分析了回调函数。通过回调函数,我们就可以实现字符的循环发送和接收。这里提一下,第21行的代码也可以直接放到while循环中执行。第28和29行,通过printf输出流和HAL_Delay函数每隔6s的时间打印一句话。打印的字符尽量写成英文格式,因为STM32CubIDE上显示中文会出现乱码,其对中文兼容的不是很好,这个也是软件的一个bug。直接通过printf函数的话,STM32是不能够输出字符的,要在嵌入式中使用此函数的话,需要通过重映射的方式,将printf函数重映射到STM32串口的寄存器上才可以,也就是我们手动添加的第131~142行的代码。首先,要使用printf函数的话,工程中要调用stdio.h文件,实际上工程里已经包含了stdio.h文件了。其次,printf函数实质上是操作__io_putchar或者fputc函数的,只需要改写或者重定向这两个函数即可。第132和134行,将宏PUTCHAR_PROTOTYPE定义为__io_putchar和fputc函数,然后再重定义PUTCHAR_PROTOTYPE函数。第139~141行是PUTCHAR_PROTOTYPE函数的内容,通过查看USART_ISR寄存器的标志位,判断上一个字符发送完成以后,再将要发送的字符(也就是printf后面的字符)写入到TDR寄存器,然后通过串口打印出来。printf函数使用很灵活,大家可以多尝试。例如,上面的PUTCHAR_PROTOTYPE函数是直接操作的寄存器来实现的,我们也可以改用HAL库来实现(两种方式本质一样):
PUTCHAR_PROTOTYPE{/* 注意第一个参数是&husart4 */HAL_UART_Transmit(&huart4 , (uint8_t *)&ch, 1, HAL_MAX_DELAY);return ch;}
还记的我们前面第八章节实验中说的syscalls.c文件吗,我们也可以利用此文件中的_write函数来实现,_write函数有预留__io_putchar接口给我们使用,其代码如下:
extern int __io_putchar(int ch) __attribute__((weak));extern int __io_getchar(void) __attribute__((weak));__attribute__((weak)) int _write(int file, char *ptr, int len){int DataIdx;for (DataIdx = 0; DataIdx < len; DataIdx++){__io_putchar(*ptr++);}return len;}所以,我们前面第131~142的代码也可以用如下代码代替:int __io_putchar(int ch){/* 具体哪个串口可以更改USART1为其它串口 */while ((UART4->ISR & 0X40) == 0); /* 等待上一个字符发送完成*/UART4->TDR = (uint8_t) ch; /* 将要发送的字符 ch 写入到DR寄存器 */return ch;}
14.4.4 编译和测试
以上代码添加完毕以后,编译工程无报错以后,用Type-C线接在开发板的USB_TTL接口上,线的一端接在电脑的USB口上,按照前面的步骤连接好ST-Link,同时注意开发板上的JP11处的跳线帽是否已经接好,如果跳线帽没接,那么UART4则无法正常通信,拨码开关拨成001,即MCU启动模式,进入Debug模式。
图14.4.2. 14开发板连接方式
双击开发板光盘A-基础资料\3、软件下的串口软件XCOM V2.0.exe将其打开:
图14.4.2. 15打开XCOM V2.0
打开XCOM V2.0以后,选择Type-C接口对应的串口(笔者的是com61),设置波特率为115200,停止位为1,数据位为8,无奇偶校验位,即保持和前面配置工程的时候一样的参数配置,在串口操作处选择打开串口(打开串口以后显示的字眼是关闭串口):
图14.4.2. 16设置打开XCOM V2.0的参数
进入Debug以后,点击运行按钮,可以看到串口软件每隔6s的时间打印一句话:Please enter characters and press enter to end。
图14.4.2. 17串口输出printf的信息
我们在数据输入框中输入我们想要发送的字符,例如输入“好好学习,天天向上”,输入好以后鼠标点击发送,然后看到串口软件上显示我们发送的字符,串口中断接收回显实验验证完成:
图14.4.2. 18串口接收回显
14.5 串口中断发送实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 7-2 UART5_TX。
前面我们使用UART4的发送端TX来发送数据,并使用接收端RX接收发送出的数据,然后可以通过串口中断打印出来,实验中我们使用HAL_UART_Transmit以查询方式发送字符,并使用HAL_UART_Receive_IT重新打开串口中断,因为串口在接收完成后就自动关闭中断了,所以要想连续发送和接收,我们就在串口接收完成回调函数中做文章,添加如下代码:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle){HAL_UART_Transmit(&huart4,&RxBuffer,1,0); /* 以查询方式发送字符 */HAL_UART_Receive_IT(&huart4,&RxBuffer,1); /* 重新打开串口中断 */}
本节实验,我们来使用串口的发送中断来发送一串字符,上一章节的实验也可以测试本章的实验。
14.5.1 硬件设计
例程功能
有时候,可能我们只需要串口的发送功能,实验调试的时候可以配置好串口发送端,然后使串口发送端发送数据,上位机来接收数据。本实验将UART5的发送端的引脚TX接入到一个带有USB接口的串口模块的RX引脚中,然后通过串口中断发送数据,而串口模块接收数据,并将接收到的数据在串口终端软件中打印出来。本实验需要USB转TTL 串口(CH340)模块,例如正点原子的USB转TTL串口模块(CH340):
图14.5.1. 1 USB转TTL串口模块(CH340)
或者手上有三合一USB串口转换器以及正点原子高速DAP仿真器都是可以的,这些模块都带了USB转TTL 串口(CH340)功能:
图14.5.1. 2 DAP仿真器
2. 硬件资源
本实验中我们会用到开发板底板的UART5的发送引脚TX来发送数据:UART5_TX—PB6。
3. 原理图
开发板的JP1排针引出了PB6引脚:
图14.5.1. 3 UART5发送端引脚
从数据手册可以看出,PB6引脚可以复用为UART5_TX,我们就使用该引脚来发送数据:
图14.5.1. 4 PB6可以复用为UART5_TX
14.5.2 软件设计
新建和配置工程
(1)配置引脚
新建工程UART5,然后配置PB6复用为UART5_TX:
图14.5.2. 1配置PB6复用为UART5_TX
配置UART5_TX后,UART5_RX也会自动配置了:
图14.5.2. 2UART5_RX自动配置
(2)配置UART5参数
UART5的参数我们选择默认配置:波特率为115200Bit/s;字长为8位;无校验位;1位停止位;数据方向为发和收;16倍过采样;Clock Prescaler(时钟预分频器)分频值为1。
图14.5.2. 3UART5参数配置
(3)配置GPIO
如下,配置UART5_TX为上拉和高速模式,其它就保持默认:
图14.5.2. 4UART5_RX引脚配置为上拉高速模式
(4)配置NVIC
本实验我们要使用串口发送中断,所以要配置NVIC,这里注意的是,如果串口中断程序(例如回调函数)中使用HAL_Delay延时函数(优先级默认为0)的话,要注意串口中断的优先级要比HAL_Delay的优先级要低,否则会出现程序卡死的情况,这点我们在上一章外部中断实验章节有详细介绍到。先开启串口全局中断,此时中断优先级默认为0:
图14.5.2. 5开启全局中断
如果串口中断程序中要使用HAL_Delay延时函数,或者其他中断程序(中断的嵌套)则在NVIC处配置中断优先级,这里我们配置串口中断优先级分组为2,抢占优先级和子优先级都是3,关于优先级怎么分配,我们在上一章的实验中有讲解过:
图14.5.2. 6配置中断优先级
(5)配置时钟
串口时钟最大可以是105MHz,我们就按照上一章节的配置方法来配置,先开启HSE,再手动配置时钟:
图14.5.2. 7配置时钟树
(6)配置生成独立的.c和.h头文件:
图14.5.2. 8配置生成独立的文件
(7)生成初始化代码
按下“Ctrl+S”保存配置,生成初始化代码:
图14.5.2. 9生成工程
2. 添加用户代码
(1)修改main.c文件
main.c文件代码如下,串口启动后,第一次发送的字符是“”。
/* USER CODE BEGIN 2/
/进入main.c文件时第一次发送的字符串/
uint8_t Senbuff[] ={“\r\n”};
/使用串口中断发送Senbuff里的字符串 */
HAL_UART_Transmit_IT(&huart5,(uint8_t)Senbuff, sizeof(Senbuff));
/USER CODE END 2 */
(2)修改usart.c文件
usart.c文件的代码如下,串口第一次发送完成后会调用回调函数HAL_UART_TxCpltCallback,同时将串口中断关闭,如果想实现第二次或者更多次发送,可以在回调函数中再次执行发送,即再次执行HAL_UART_Transmit_IT函数,我们选择发送字符串ALIENTEK:
/* USER CODE BEGIN 1 */uint8_t RxBuffer[] ={"ALIENTEK\r\n"};/* 要发送的字符串 *//*void delay_short(volatile unsigned int n){while(n--){}}void delay(volatile unsigned int n){while(n--){delay_short(0x7fff);}}*//* 串口发送完成中断回调函数 */void HAL_UART_TxCpltCallback(UART_HandleTypeDef *UartHandle){HAL_Delay(3000); /* 延时3s,每隔3s发送一次 *//* 启动UART5串口发送中断 */HAL_UART_Transmit_IT(&huart5,(uint8_t *)RxBuffer, sizeof(RxBuffer));}/* USER CODE END 1 */
以上代码每隔3s发送一次,回调函数中使用了HAL_Delay延时函数,所以前面我们配置串口的中断优先级比HAL_Delay的优先级低。当然,也可以使用其它空循环来达到延时的效果,如上面注释部分的函数delay_short。
14.5.3 编译和测试
保存修改,编译工程无报错后进行测试。将开发板的PB6用杜邦线接到USB转TTL 串口模块(CH340)的RX端,将开发板引出的地线接到USB转TTL 串口模块(CH340)的GND端:
图14.5.3. 1硬件接线
调试结果如下,可以看到串口接收到数据并打印出来了,第一条字符串是,后续的字符串是ALIENTEK:
图14.5.3. 2测试结果
14.6 章节小结
14.6.1 程序设计总结
本小节我们对前面的串口中断接收回显实验程序实现的过程做一个简单的总结:
1)STM32CubeIDE对串口的初始化操作已经为我们做好了,并使能了UART4中断;
2)我们先指定大小固定的发送缓存区和接收缓存区;
3)电脑发送过来的数据会堆放在发送缓存区里,当达到用户指定的大小以后,TX就利用HAL_UART_Transmit函数发送接收缓存区的数据;
4)串口接收缓存区接收到TX发送过来的数据,当达到用户指定的接收缓存区大小以后,就开始调用函数HAL_UART_Receive_IT,此函数会根据有无FIFO以及字长是多少位等条件来调用对应的中断处理函数UART_RxISR_8BIT;
5)串口接收RX进入中断处理函数UART_RxISR_8BIT以后,发送完数据就自动将串口中断关闭了,然后调用回调函数HAL_UART_RxCpltCallback执行后续的操作;
6)回调函数本来是一个弱定义、无实际内容的函数,我们手动编写了回调函数,可以实现再次发送字符并重新打开串口中断,以实现继续接收下一次发送过来的字符。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{
HAL_UART_Transmit(&huart4,&RxBuffer,1,0);
HAL_UART_Receive_IT(&huart4,&RxBuffer,1);
}
这里说明一下函数HAL_UART_Receive_IT和HAL_UART_Receive的差别:HAL_UART_Receive函数没有回调函数,使用轮询(阻塞模式)的方式接收数据,其规定了一个接收超时机制,如果函数接收超时就返回。HAL_UART_Receive_IT函数并不接收数据,只是打开对应的串口中断,数据接收是在串口中断中进行的。在一个工程中不能同时使用这两个函数,如果同时使用这两个函数,那么HAL_UART_Receive不能进入阻塞模式,此函数就形同虚设了。
图14.5.1. 5几个文件的关系
14.6.2 串口中断过程
对于中断的过程,可以按照前面外部中断实验章节的来分析。
stm32mp1xx_hal_msp.c文件有配置中断优先级分组为2。
void HAL_MspInit(void){__HAL_RCC_HSEM_CLK_ENABLE();HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);}
usart.c文件中有配置中断的优先级并开启UART4中断,其中UART4_IRQn是中断号,中断号为52,这是在stm32mp157dxx_cm4.h文件中定义好了的。
HAL_NVIC_SetPriority(UART4_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(UART4_IRQn);
stm32mp1xx_it.c文件中有中断服务函数。UART4的中断服务函数UART4_IRQHandler会调用中断响应函数HAL_UART_IRQHandler,这里注意的是,所有的串口中断都会调用HAL_UART_IRQHandler函数来响应中断,具体是哪一个串口,参数里会指定,如上面的代码,参数是句柄huart4,所以会处理UART4的中断。HAL_UART_IRQHandler函数会判断是什么中断,当判断是接收中断时,会调用HAL_UART_Receive_IT函数,接下来的操作就是我们前面介绍的部分了。
void UART4_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart4);
}
CPU根据中断号判断是UART4中断以后,CPU会根据中断号找到启动文件中的中断服务程序的入口地址,根据此地址跳到中断服务函数。如果用户没有定义中断服务函数,发生中断以后则执行启动文件中预定义的空函数,如果用户有正确定义中断服务函数,发生中断后则执行用户定义的中断服务函数。如果有中断嵌套,要注意中断的优先级,例如串口中断回调函数中使用HAL_Delay延时函数时,串口中断的优先级不能比HAL_Delay的优先级高。
本篇实验我们只是涉及到了M4内核的中断控制器NVIC,到了A7内核使用的是GIC,中断的内容很庞大,中断的应用非常广,理解并学好中断对后续的实验以及项目的开发很有帮助。关于中断的处理流程我们就介绍到这里,在以后的实验中,遇见中断,大家就想到中断号、中断向量表和中断服务函数的对应关系就明白了,在以后的实验中我们不再花更多的篇幅去讲解这部分内容。
图14.5.2 1中断的处理过程
如果觉得《【正点原子FPGA连载】第十四章 串口通信实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1》对你有帮助,请点赞、收藏,并留下你的观点哦!