
串口通信实验 本章我们将学习STM32H7的串口,教大家如何使用STM32H7的串口来发送和接收数据。本章将实现如下功能:STM32H7通过串口和上位机的对话,STM32H7在收到上位机发过来的字符串后,原原本本的返回给上位机。0 V& { w- R/ E # E- }& I z0 O9 W 17.1串口简介 学习串口前,我们先来了解一下数据通信的一些基础概念。 17.1.1 数据通信的基础概念- v. K; R: x N8 _. G4 H! ~* W 在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们常见的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。 1.数据通信方式 按数据通信方式分类,可分为串行通信和并行通信两种。串行和并行的对比如下图所示: ![]() 图17.1.1.1 数据传输方式 串行通信的基本特征是数据逐位顺序依次传输,优点是传输线少成本低,抗干扰能力强可用于远距离传输,缺点就是传输速率低。5 _- y" {7 n' t: r8 F8 P 而并行通信是数据各位可以通过多条线同时传输,优点是传输速率高,缺点就是线多成本就高了,抗干扰能力差因而适用于短距离、高速率的通信。 2. 数据传输方向 根据数据传输方向,通信又可分为全双工、半双工和单工通信。全双工、半双工和单工通信的比较如下图所示: & B: x" ^) D) f: I3 m [6 H ![]() 图17.1.1.2 数据传输方式 单工是指数据传输仅能沿一个方向,不能实现反方向传输。 半双工是指数据传输可以沿着两个方向,但是需要分时进行。( a" r, `3 |; q1 O 全双工是指数据可以同时进行双向传输。, Z6 W J+ R# M% J 这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信,而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。 3. 数据同步方式 根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示: , D4 \' Z* A+ d$ u7 o% D ![]() 图17.1.1.3 数据同步方式& N4 e; M0 R1 N# d, G; {5 b$ S 同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。优点:可以实现高速率、大容量的数据传输,以及点对多点传输。缺点:要求发送时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。 异步通信不需要时钟信号,而是在数据信号中加入开始位和停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。优点:没有时钟信号硬件简单,双方时钟可允许一定误差。缺点:通信速率较低,只适用点对点传输。% |1 f% \1 J2 P% a! \ 4. 通信速率" [9 a% n/ ~& j1 m0 {' Q 在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。2 \+ V/ n/ b# D( r5 x! e 传信率:每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为bit/s(即比特每秒),因而又称为比特率。, \7 f6 O$ H8 ~0 f 传码率:每秒钟传输的码元个数,单位为Baud(即波特每秒),因而又称为波特率。 比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们来看看波特率,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定bit的数据信息量。举个例子,在TTL电平标准的通信中,用0V表示逻辑0,5V表示逻辑1,这时候这个码元就可以表示两种状态。如果电平信号0V、2V、4V和6V分别表示二进制数00、01、10、11,这时候每一个码元就可以表示四种状态。 由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。' E) i- S5 f0 V+ F% \% w 比特率和波特率的关系可以用以下式子表示:! F5 V' s- l0 |) P0 M 比特率 = 波特率 * log2M 其中M表示码元承载的信息量。我们也可以理解M为码元的进制数。 举个例子:波特率为100 Baud,即每秒传输100个码元,如果码元采用十六进制编码(即M=2,代入上述式子),那么这时候的比特率就是400 bit/s。如果码元采用二进制编码(即M=2,代入上述式子),那么这时候的比特率就是100 bit/s。 可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了。. P; K9 u* W' [0 L9 |9 a 17.1.2 串口通信协议简介 串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422和RS-485等。 随着科技的发展,RS-232在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB转串口取代了RS-232串口。我们只需要在电路中添加一个USB转串口芯片,就可以实现USB通信协议和标准UART串行通信协议的转换,而我们开发板上的USB转串口芯片是CH340C这个芯片。关于USB转串口芯片的原理图请看17.2小节。% g$ L; h }1 h) E# X' Q% Y3 X 下面我们来学习串口通信协议,这里主要学习串口通信的协议层。3 B. [: S5 m3 P/ O: n5 {8 X 串口通信的数据包由发送设备的TXD接口传输到接收设备的RXD接口。在串口通信的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如图 17.1.2.1所示。 ![]() 图17.1.2.1 串口通信协议数据帧格式3 {. s& u- u( A5 v2 Z 串口通信协议数据包组成可以分为波特率和数据帧格式两部分。 1.波特率+ i5 d" y) ^0 t6 b2 [# u- }. M 本章主要讲解的是串口异步通信,异步通信是不需要时钟信号的,但是这里需要我们约定好两个设备的波特率。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同,我们常见的波特率是4800、9600、115200等。: Y# V3 U/ g& ] 2.数据帧格式1 W: s/ e" r$ [0 G( k5 H, d 数据帧格式需要我们提前约定好,串口通信的数据帧包括起始位、停止位、有效数据位以及校验位。 起始位和停止位5 a6 B, j7 @2 J/ ]* i4 I, N 串口通信的一个数据帧是从起始位开始,直到停止位。数据帧中的起始位是由一个逻辑0 的数据位表示,而数据帧的停止位可以是 0.5、1、1.5或 2个逻辑1的数据位表示,只要双方约定一致即可。 有效数据位 数据帧的起始位之后,就接着是数据位,也称有效数据位,这就是我们真正需要的数据,有效数据位通常会被约定为5、6、7或者8个位长。有效数据位是低位(LSB)在前,高位(MSB)在后 校验位 校验位可以认为是一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验方法有:奇检验、偶检验、0检验、1检验以及无检验。下面分别介绍一下:' w; d7 p& C. k ` 奇校验是指有效数据为和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为:10101001,总共有 4 个“1”,为达到奇校验效果,校验位设置为“1”,最后传输的数据是8位的有效数据加上 1 位的校验位总共 9位。, x7 n5 C3 v& f5 ] c0 |5 k 偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。 0校验是指不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。 无校验是指数据帧中不包含校验位。% @8 y9 s5 L' i1 @3 ?5 f 我们一般是使用无校验的情况。 4 w' H' y8 z& } 17.1.3 STM32H7的串口简介 STM32H750的串口资源相当丰富,功能也相当强劲。STM32H750VBT6最多可提供9路串口(8路普通串口和1路低功耗串口),支持8/16倍过采样、支持自动波特率检测、支持Modbus通信、支持同步单线通信和半双工单线通讯、支持LIN、支持调制解调器操作、智能卡协议和IrDA SIR ENDEC规范、具有DMA等。) y4 \' `4 D+ p" P7 J+ W STM32H7的8路普通串口分为两种:USART(即通用同步异步收发器)和UART(即通用异步收发器)。UART是在 USART基础上裁剪掉了同步通信功能,只剩异步通信功能。简单区分同步和异步就看通信时需不需要对外提供时钟输出,我们平时用串口通信基本都是异步通信。 STM32H7有四个USART和四个UART,其中USART1和USART6的时钟源可以来于 APB2/PLL2_Q/PLL3_Q/HSI/CSI/LSE 时钟,其最大频率为120MHz,其他六个串口的时钟源可以来于APB1/PLL2_Q/PLL3_Q/HSI/CSI/LSE时钟,其最大频率为120MHz。 STM32的串口输出的是TTL电平信号,如果需要RS-232标准的信号可使用MAX3232芯片进行转换,而本实验我们是通过USB转串口芯片来与电脑的上位机进行通信。 17.1.3.1 USART框图7 R: k( s& T7 F0 [ 下面先来学习USART框图,通过USART框图引出USART的相关知识,从而有了一个很好的整体掌握,对之后的编程也会有一个清晰的思路。9 P7 a1 s7 v ?# \ ![]() : Z ? U0 I- i: W, Q 图17.1.3.1.1 USART框图 E' X7 p& T1 |4 G 为了方便大家理解,我们把整个框图分成几个部分来介绍。 ①时钟与波特率& x; g- U3 K* \, p 这部分的主要功能就是为USART提供时钟以及配置波特率。* d& i2 q3 y/ `; h. u0 ]" j/ ~( E1 G 在USART框图中,可以看到有两个时钟域,usart_pclk 时钟域和usart_ker_ck 内核时钟域。usart_pclk是外设总线时钟,需要访问 USART 寄存器时,该信号必须有效。usart_ker_ck是USART时钟源,独立于 usart_pclk,由RCC提供。因此,即使usart_ker_ck 时钟停止,也可以连续对 USART 寄存器进行读/写操作。) w4 R9 u# A: c6 r: H' a 波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),波特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。波特率的计算公式分为16倍过采样和8倍过采样: 在16倍过采样的情况下,波特率通过以下公式得出: 在8倍过采样的情况下,波特率通过以下公式得出:9 ]; z0 Q2 {, b( v/ O% F ! J; c' \/ I- Y: g( j usart_ker_ckpres为USART的工作时钟,在usart_ker_ck不分频的情况下,时钟频率最高为120MHz。USARTDIV 是一个无符号定点数,我们可以通过上述的式子得到,因为一般我们知道的是baud和usart_ker_ck的时钟。得到 USARTDIV 的值后,下一步就是要把该值设置到波特率寄存器USART1->BRR中,以完成波特率的设置。波特率的设置还得根据OVER8的值来确定格式。 OVER8是USART_CR1寄存器的位15,当OVER8 = 0时(即16 倍过采样),BRR = USARTDIV;当OVER8 = 1时,BRR[2:0] = USARTDIV[3:0],这里右移 1 位,BRR[3]必须保持清零,BRR[15:4] = USARTDIV[15:4]。无论是16倍采样还是8倍过采样,USARTDIV都必须大于或等于 0d16。- o2 L* {2 S6 e8 B7 M0 k- Q 下面举个例子说明:, x- b6 L- |% r+ b 当在16倍过采样时需要得到 115200 的波特率,usart_ker_ckpre = 120MHZ,那么可得: 得到USARTDIV = 1042,可解得 BRR = USARTDIV = 1042d = 0412h,那么需要设置 USART_BRR 的值为 0x412。这里的USARTDIV是有余数的,我们用四舍五入进行取整,这样会导致波特率会有所偏差,而这样的小误差是可以被允许的。8倍过采样计算方法类似。 ②收发数据 USART双向通信需要的两个引脚: TX:发送数据输出引脚。7 |8 m* E/ M2 k5 o2 K. v5 {7 f7 i RX:接收数据输入引脚。 USART_TDR是USART发送数据寄存器,要发送什么数据,往这个寄存器里写即可,低9位有效。( Z! i$ D0 F# l( F/ R USART_RDR是USART接收数据寄存器,要接收什么数据,读这个寄存器里写即可,低9位有效。 USART_TDR和USART_RDR的第9位是否有效,通过USART 控制寄存器 1(USART_CR1)的 M位(M0:位 12,M1:位 28)设置: 7 位字符长度:M[1:0] =“10” 8 位字符长度:M[1:0] =“00”$ G$ N. S! _; R+ n 9 位字符长度:M[1:0] =“01” 我们基本都是使用 8位数据字长。2 C" Y( m( j. m9 M ③控制寄存器 我们可以通过控制寄存器控制USART数据的发送、数据接收、各种通信模式的设置、中断、DMA 模式还有唤醒单元等。具体在后面讲解USART寄存器的时候细讲。 ④DMA和中断功能 USART支持DMA传输,可以实现高速数据传输,具体我们会在DMA实验中为大家讲解。在 USART 通信过程中,中断可由不同事件生成,同时支持 USART 模块生成唤醒中断。常用的中断比如:发送数据寄存器为空、发送 FIFO 未满、发送完成、接收 FIFO 非空、接收 FIFO 已满等。6 q& U' v. D. N' N$ T ⑤USART信号引脚+ V s3 {" T- e! i1 T0 L3 M$ m/ c 在 RS232 硬件流控制模式下需要以下两个引脚: CTS(清除以发送):发送器在发送下一帧数据之前会检测 CTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。: b3 Y# D0 l8 | RTS(请求以发送):如果为低电平,则该信号用于指示 USART 已准备好接收数据。 在 RS485 硬件控制模式下需要下面这个引脚:" j# e( }: ^4 A9 O DE(驱动器使能):该信号用于激活外部收发器的发送模式。7 N8 ?8 [- P# r6 |( F, Z0 {$ E 在同步主/从模式和智能卡模式下需要以下引脚: CK:该引脚在同步主模式和智能卡模式下用作时钟输出,在同步从模式下用作时钟输入。 NSS:该引脚在同步从模式下用作从器件选择输入。 这些引脚我们暂时都没有用到,就给大家简单提一下。' g; Y& }2 s; `! n% e; r- }8 y 17.1.3.2 USART寄存器) v3 l1 E L0 |0 }) z" X8 n STM32H750的串口使用起来还是蛮简单的,只要你开启了串口时钟,并设置相应IO口的模式,然后配置一下波特率,数据位长度,奇偶校验位等信息,就可以使用了。下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。/ o- s. ~7 f E (1)串口时钟使能( [7 |0 H3 v8 h+ z7 k/ b 串口作为STM32H750的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口1是在APB2ENR寄存器的第4位。注意:除了串口1和串口6的时钟使能在APB2ENR寄存器,其他串口的时钟使能位都在APB1LENR和APB4ENR寄存器。: B$ }& ?$ m- U3 u (2)串口波特率设置% m5 F+ w. }# v0 |) S( e! K- n 每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。波特率设置介绍过,请回顾。8 g5 P" n8 ^' X9 x% }0 r) r& Y2 \ (3)串口控制% y3 S; o; ?+ r6 r8 z F. s STM32H750的每个串口都有3个控制寄存器USART_CR1~3,串口的很多配置都是通过这3个寄存器来设置的。USART_CR1寄存器的描述如图17.1.3.2.1所示:/ f% x( K* K/ o% U# T9 I4 B3 N1 T: u, } ( A& y+ @, _9 V7 x4 R' e ![]() 图17.1.3.2.1 USART_CR1寄存器 该寄存器我们只介绍本节需要用到的一些位:M[1:0]位(位28和12),用于设置字长,我们一般设置为:00表示1个起始位,8个数据位,n个停止位(n的个数,由USART_CR2的[13:12]位控制)。OVER8为过采样模式设置位,我们一般设置位0,即16倍过采样已获得更好的容错性;UE为串口使能位,通过该位置1,以使能串口;PCE为校验使能位,设置为0,则禁止校验,否则使能校验;PS为校验位选择位,设置为0则为偶校验,否则为奇校验;TXEIE为发送缓冲区空中断使能位,设置该位为1,当USART_ISR中的TXE位为1时,将产生串口中断;TCIE为发送完成中断使能位,设置该位为1,当USART_ISR中的TC位为1时,将产生串口中断;RXNEIE为接收缓冲区非空中断使能,设置该位为1,当USART_ISR中的ORE或者RXNE位为1时,将产生串口中断;TE为发送使能位,设置为1,将开启串口的发送功能;RE为接收使能位,用法同TE。 其他位的设置,大家可以参考《STM32H7xx参考手册_V7(英文版).pdf》 。; Z" J, f( t, n( j9 d2 V* A \ (4)数据发送与接收 与STM32F1和F4不同,STM32H7的串口发送和接收由两个不同的寄存器组成。发送数据是USART_TDR寄存器,接收数据是USART_RDR寄存器,USART_TDR寄存器描述如图17.1.3.2.2所示: ![]() 3 P8 l c8 N( _/ G$ I4 t. i( T' p 图17.1.3.2.2 USART_TDR寄存器8 |4 x, H( c% v9 [) K& G 3 K1 z7 r( l/ o 可以看出,USART_TDR虽然是一个32位寄存器,但是只用了低9位(DR[8:0]),其他都是保留,TDR[8:0]为串口数据,具体多少位,由前面介绍的M[1:0]决定(一般是8位数据)。/ Z) z2 Y- o" O+ u 当我们需要发送数据的时候,往USART_TDR寄存器写入你想要发送的数据,就可以通过串口发送出去了。而当有串口数据接收到,需要读取出来的时候,我们则必须读取USART_RDR寄存器,USART_RDR寄存器各位描述同USART_TDR是完全一样的,只是一个用来接收,一个用来发送。! o8 f& j2 W, i, Z4 h 当使能校验位(USART_CR1中PCE位被置位)进行发送时,写到MSB的值(根据数据的长度不同,MSB是第7位或者第8位)会被后来的校验位取代。: `5 ]1 i. F; l" o5 @& Y4 Q 当使能校验位进行接收时,读到的MSB位是接收到的校验位。 (5)串口状态/ |3 F0 l3 |& N* l" z6 S+ y 串口状态通过状态寄存器USART_ISR读取。USART_ISR的各位描述如图17.1.3.2.3所示:& O. ?3 }3 ~+ p- s1 S: H $ ^0 f( t% p& t; s' d$ q ![]() ' Z. ?9 k7 x. e- k0 ]9 v 图17.1.3.2.3 USART_ISR寄存器5 s6 h2 h6 }- f |( d, a D2 M5 a 这里我们关注一下两个位,第5、6位RXNE和TC。 RXNE(读数据寄存器非空),当该位被置1的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_RDR,通过读USART_RDR可以将该位清零,也可以向该位写0,直接清除。 TC(发送完成),当该位被置位的时候,表示USART_TDR内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式: 1)读USART_ISR,写USART_TDR。 2)向ICR寄存器的TCCF位写1。! y& ^9 f" d3 r3 @7 m$ u 通过以上一些寄存器的操作再加上IO口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32H7xx参考手册_V7(英文版).pdf》通用同步异步收发器这一章。 17.1.4 GPIO引脚复用功能- B! w3 |5 V4 c) G 我们知道芯片有许多外设,而引脚的资源是很有限的,为了解决这个问题,方法就是引脚复用,这样使得引脚除了作为普通的IO口之外,还会与一些外设关联起来,作为第二功能使用,而且一个引脚不单单只有一种复用功能,而是拥有多个第二功能,但是一次只允许一个外设的复用功能,以确保共用同一个 IO 引脚的外设之间不会产生冲突。 下面我们把之前没讲解的GPIO复用功能寄存器讲解一下。 GPIO复用功能寄存器(AFRH 和 AFRL) 复用功能寄存器有2个,都是32位有效的寄存器,分高位(AFRH)和低位(AFRL)。复用器采用16路复用功能输入AF0~AF15,通过GPIOx_AFRL(引脚 0~7)、GPIOx_AFRH(引脚 8~15)寄存器对复用功能输入进行配置,每四位控制1路复用。9 y0 c9 F1 G4 V b- N r * N# f$ ?* G1 c6 S& P3 U ![]() 图17.1.4.1 AFRL寄存器 AFRL寄存器配置引脚 0~7复用功能,AFRH寄存器配置引脚 8~15复用功能。 IO口并不是想复用什么功能都可以,是有规定的,每个 IO 引脚的复用可以通过查阅数据手册《STM32H750VBT6.pdf》,我们看看Table 8. Port A alternate functions,其他的端口请自行查阅。- C$ N6 U) v' n: G& u9 |) k ![]() & B/ e4 Z2 `5 x, y 表17.1.4.1 Port A引脚复用 我们圈出来PA9和PA10对应AF7这列,因为我们的串口1就是用到这两个IO口,配置复用功能,使得PA9用做串口1的发送引脚TX,PA10则用做串口1的接收引脚RX。! w. D/ ?1 F4 y; d# E 我们还需要学会在HAL库中寻找这些复用的宏定义,在stm32h7xx_hal_gpio_ex.h文件中可以找到。从表17.1.4.1我们知道PA9和PA10都是在复用器的AF7这一路中,所以我们在HAL库中就找AF7的宏定义,其定义如下:# ?3 g y' g* V3 E7 P5 d) t ' Z7 i9 r( v R" R5 R3 E& O
细心的朋友可以看出,这些宏定义的值都是一样的,即都是(uint8_t)0x07,而宏名只是为了区分是哪个外设而已。因为我们的外设是串口1,所以就很容易选择到我们复用的功能要用到的宏定义,就是GPIO_AF7_USART1。具体的场景应用请看我们的串口1的初始化源码。0 s1 [3 F* e# U0 _& M- T 17.2 硬件设计. } b& k/ h, s5 h' G8 P& L 1.例程功能3 n1 n' J7 I6 {7 Q y3 d* ^# ^ LED0闪烁,提示程序在运行。 STM32通过串口1和上位机对话,STM32在收到上位机发过来的字符串(以回车换行结束)后,会返回给上位机。同时每隔一定时间,通过串口1输出一段信息到电脑。 2.硬件资源' i$ N7 ~4 H; B7 w% ? 1)RGB灯; |# R4 J# K- s" q RED :LED0 - PB4( G0 y0 c! Z. s7 B, b 2)串口1 (PA9/PA10连接在板载USB转串口芯片CH340C上面) ,需要跳线帽连接。 3.原理图 USB转串口硬件部分的原理图,如图17.2.1所示:: T. p2 M% q" n$ v0 R; U5 r* l 0 f# y( }; _- H$ | ![]() $ y& ~, T% H9 P$ z1 ` 图17.2.1 USB转串口原理图$ I' m% B% f; `( F1 ~ 这里需要注意的是:上图中的红色的接线引脚和蓝色的接线引脚需要用短路帽连接起来,否则串口的信号线跟USB接口是没有连接的。这里我们要把P5的RXD和PA9用跳线帽连接,以及TXD和PA10也用跳线帽连接。如图17.2.2所示: ( j) [, @( `2 |5 D. O) H4 y- P5 y ![]() - b+ m& `" W! j) N 图17.2.2 短路帽连接 0 f3 N3 P- n( w" D( R 17.3 程序设计! P0 \0 [1 F$ A6 c4 J 17.3.1 USART的HAL库驱动! W$ o* [+ C, z( s HAL库中关于串口的驱动程序比较多,我们主要先来学习本章需要用到的,其余的后续用到再讲解。因为我们现在只是用到异步收发器功能,所以我们现在只需要stm32h7xx_hal_uart.c文件(及其头文件)的驱动代码,stm32h7xx_hal_usart.c是通用同步异步收发器,暂时没有用到,可以暂时不看。用到一个外设第一个函数就应该是其初始化函数。) c8 J0 E. [0 s/ I! G 1.HAL_UART_Init函数6 g6 l, h+ |+ j* q" x6 u9 k# F- Q& x 要使用一个外设首先要对它进行初始化,所以先看串口的初始化函数,其声明如下: HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);( M( S# G! T1 ~8 S* x7 q 函数描述:+ x1 n- v0 m8 Q0 u! h 用于初始化异步模式的收发器。 函数形参:5 v3 A) r ?! t, E5 R 形参1是串口的句柄,UART_HandleTypeDef结构体类型,其定义如下:! d+ Z1 h \. v9 E. b- O0 K
1)Instance:指向UART 寄存器基地址。实际上这个基地址HAL库已经定义好了,可以选择范围:USART1~ USART3、USART6、UART4、UART5、UART7、UART8。 2)Init:UART初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。下面我们再详细讲解这个结构体。4 H2 j" @$ o4 F* }1 H/ O 3)AdvancedInit:用于配置高级功能,如自动波特率,MSB 先行等。 4)pTxBuffPtr,TxXferSize,TxXferCount:分别是指向发送数据缓冲区的指针,发送数据的大小,发送数据的个数。3 k% }' e8 h' ~+ e& g, V 5)pRxBuffPtr,RxXferSize,RxXferCount:分别是指向接收数据缓冲区的指针,接受数据的大小,接收数据的个数; 6)Mask:UART数据接收寄存器的掩码,用于存放数据的校验位。 7)hdmatx,hdmarx:配置串口发送接收数据的 DMA具体参数。3 x# ~2 y" C- q3 H0 p 8)Lock:对资源操作增加操作锁保护,可选HAL_UNLOCKED或者HAL_LOCKED两个参数。如果gState的值等于HAL_UART_STATE_RESET,则认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit函数来对串口的GPIO和时钟进行初始化。) t) }7 j8 f& r 9)gState,RxState:分别是UART的发送状态、工作状态的结构体和UART接受状态的结构体。HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于gState,如 HAL_UART_STATE_BUSY。2 e! B- q' K; @$ l/ |: b 10)ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。 下面,我们来了解UART_InitTypeDef 这个结构体类型,该结构体用于配置UART的各个通信参数,包括波特率,停止位等,具体说明如下:/ d4 l/ ^9 y, k" x/ B- m- s( Y/ c; H& j
1)BaudRate:波特率设置。一般设置为 2400、9600、19200、115200。 2)WordLength:数据帧字长,可选 8 位或 9 位。这里我们设置为8位字长数据格式。% M9 ]# Z4 [" n- K3 o, n# ? 3)StopBits:停止位设置,可选0.5个、1个、1.5个和2个停止位,一般我们选择1个停止位。0 i. {4 o. ^; T2 D3 {1 O& g# p 4)Parity:奇偶校验控制选择,我们设定为无奇偶校验位。 5)Mode:UART模式选择,可以设置为只收模式,只发模式,或者收发模式。这里我们设置为全双工收发模式。/ w7 x: N. W) w) ? 6)HwFlowCtl:硬件流控制选择,我们设置为无硬件流控制。 7)OverSampling:过采样选择,选择8倍过采样或者16过采样,一般选择16过采样。) b# J& i# K4 d+ r0 H/ ?% ^ 8)OneBitSampling:一个采样位方法使能,0:三个采样位方法;1:一个采样位方法。 A ^# ~; n/ u* J i. B. } 9)Prescaler:时钟分频系数,默认选择不分频。 10)FIFOMode:FIFO模式的使能或失能。0 ?) R" b8 ?7 ~; M) N/ Y$ k 11)TXFIFOThreshold:发送 FIFO 的阈值。当达到设定的阈值时,将数据发送给TX移位寄存器。阈值的值可以为容量 1/8,1/4,1/2,3/4,7/8,满。 12)RXFIFOThreshold:接收FIFO的阈值。当达到设定的阈值时,将数据给接收寄存器。阈值的值可以为容量 1/8,1/4,1/2,3/4,7/8,满。" m; l" F. r$ \! s 函数返回值:/ o4 G. D# F& c! M7 _; e4 @1 Y HAL_StatusTypeDef枚举类型的值,有4个,分别是HAL_OK表示成功,HAL_ERROR表示错误,HAL_BUSY表示忙碌,HAL_TIMEOUT超时。后续遇到该结构体也是一样的。$ Z9 `# L# h! |7 w- U, Q* F 2. HAL_UART_Receive_IT函数 HAL_UART_Receive_IT函数是开启串口接收中断函数。其声明如下: HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,3 X+ c* p5 u& F! H* d7 y uint8_t *pData, uint16_t Size);2 D4 C/ l7 Y* _8 L5 a8 m 函数描述:6 G( f' }. G$ H' g) u 用于开启以中断的方式接收指定字节。数据接收在中断处理函数里面实现。 函数形参: o5 n% Z9 G( R1 l, U( f 形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。* q ^! t4 q$ ] 形参2是要接收的数据地址。 形参3是要接收的数据大小,以字节为单位。 函数返回值: HAL_StatusTypeDef枚举类型的值。 3. HAL_UART_IRQHandler函数 HAL_UART_IRQHandler函数是HAL库中断处理公共函数。其声明如下: void HAL_UART_IRQHandler(UART_HandleTypeDef *huart); d4 o6 ^: h$ `% S 函数描述:$ J& y. n; n- L9 B 该函数是HAL库中断处理公共函数,在串口中断服务函数中被调用。% N; V- [) [/ Y6 |" i( ]" D 函数形参: o5 i: @9 ?- r" Z 形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。 函数返回值:无 注意事项: 该函数是HAL库已经定义好,用户一般不能随意修改。,如果用户要在中断中实现自己的逻辑代码,可以直接在函数HAL_UART_IRQHandler 的前面或者后面添加新代码,也可以直接在 HAL_UART_IRQHandler 调用的各种回调函数里面执行,这些回调都是弱定义的,方便用户直接在其它文件里面重定义。串口回调函数主要有下面几个:, X7 F% @; L6 c. |' G/ s1 r 6 k0 V% S0 {+ R, c3 v
本实验我们用到的是接收回调函数HAL_UART_RxCpltCallback,就是在接收回调函数里面编写我们的接收逻辑代码,具体请参考实验源码。 串口通信配置步骤 1.串口参数初始化(波特率、字长、奇偶校验等),并使能串口。) \4 n a- J' j HAL库通过调用串口初始化函数HAL_UART_Init完成对串口参数初始化,详见例程源码。. r3 s* Z" B, v, @3 P 注意:该函数会调用:HAL_UART_MspInit函数来完成对串口底层的初始化,包括:串口及GPIO时钟使能、GPIO模式设置、中断设置等。' z& h8 { r7 I& k3 H3 t- } 2)使能串口和GPIO口时钟 本实验用到USART1串口,使用PA9和PA10作为串口的TX和RX脚,因此需要先使能USART1和GPIOA时钟。参考代码如下: __HAL_RCC_USART1_CLK_ENABLE(); /* 使能USART1时钟 /# U4 F4 r2 |4 r9 Y# x9 Y$ k __HAL_RCC_GPIOA_CLK_ENABLE(); / 使能GPIOA时钟 */ 3)GPIO模式设置(速度、上下拉、复用功能等) GPIO模式设置通过调用HAL_GPIO_Init函数实现,详见本例程源码。 4)开启串口相关中断,配置串口中断优先级" a$ q1 D) a3 w- V9 | 本实验我们使用串口中断来接收数据。我们使用HAL_UART_Receive_IT函数开启串口中断接收,并设置接收buffer及其长度。通过HAL_NVIC_EnableIRQ函数使能串口中断,通过HAL_NVIC_SetPriority函数设置中断优先级。 5)编写中断服务函数 串口1中断服务函数为:USART1_IRQHandler,当发生中断的时候,程序就会执行中断服务函数。HAL库为了使用方便,提供了一个串口中断通用处理函数HAL_UART_IRQHandler,该函数在串口接收完数据后,又会调用回调函数HAL_UART_RxCpltCallback ,用于给用户处理串口接收到的数据。 因此我们需要在HAL_UART_RxCpltCallback函数实现数据接收处理,详见本例程源码。# K1 u! C' z* S! E$ U 6)串口数据接收和发送2 G7 s' b& T v- \9 f# a# I 最后我们可以通过读写USART_DR寄存器,完成串口数据的接收和发送,HAL库也给我们提供了:HAL_UART_Receive和HAL_UART_Transmit两个函数用于串口数据的接收和发送。7 I- x! J* W) Z6 ?$ R: Y 大家可以根据实际情况选择使用那种方式来收发串口数据。 17.3.2 程序流程图) u6 N7 o. \: X ![]() 图17.3.2.1 串口通信实验程序流程图/ S' L2 |. a* e2 t0 @+ l 17.3.3 程序解析 1.串口1驱动代码& ^- B3 Q4 W- o8 C) b 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。串口1(USART1)驱动源码包括两个文件:usart.c和usart.h。 下面我们先解析usart.h的程序。 串口1引脚定义 由硬件设计小节,我们知道PA9和PA10分别被复用为串口1的发送和接收引脚,我们做了下面的引脚定义。 /* 串口1的GPIO */
PA9和PA10都在复用器的AF7这路信号线中,所以都复用为GPIO_AF7_USART1,前面有提过。USART1_IRQn也就是我们中断向量表的37号中断,USART1_IRQHandler是串口1的中断服务函数。每个串口都有自己的中断函数,但是我们最终都是通过回调函数去实现逻辑代码,当然我们亦可在中断函数里实现逻辑代码。+ u+ L$ J$ X$ F$ U6 |) p) @, g 另外我们还定义了三个宏,具体如下:
可以看到USART_REC_LEN表示最大接收字节数,这里定义的是200个字节,后续如果有需求要发送更大的数据包,可以改大这个值,这里不改太大,是避免浪费太多内存。USART_EN_RX则是用于使能串口1的接收数据。RXBUFFERSIZE是缓冲大小。' [/ ]. E2 w+ H) j4 E& o 下面我们再解析usart.c的程序,先看串口1的初始化函数,其定义如下:
g_uart1_handle 是结构体UART_HandleTypeDef类型的全局变量,UART_HandleTypeDef结构体成员的含义请回到前面回顾。波特率我们直接赋值给g_uart1_handle.Init.BaudRate这个成员,可以看出很方便。需要注意的是,最后一行代码调用函数HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量。 上面的初始化函数只是串口初始化的其中一部分,我们还有一部分初始化需要HAL_UART_MspInit函数去完成。HAL_UART_MspInit是HAL库定义的弱定义函数,这里我们做重定义以实现我们的初始化需求。HAL_UART_MspInit函数在HAL_UART_Init函数中会被调用,其定义如下:) E! w. Q6 Z2 V0 F ! N9 q+ p' ~+ Z3 p) R
怎么来理解这个函数呢?该函数主要实现底层的初始化,事实上这个函数的代码还可以直接放到usart_init函数里面,但是HAL库为了代码的功能分层初始化,定义这个函数方便用户使用。所以我们也按照HAL库的这个结构来初始化外设。这个函数首先是调用if(huart->Instance == USART1)判断是要初始化那个串口,因为每个串口初始化都会调用HAL_UART_MspInit这个函数,所以需要判断是哪个串口要初始化才做相应的处理。只能说HAL库这样的结构机制有好处,自然也有坏处。 首先就是使能串口以及PA9和PA10的时钟,PA9和PA10需要用做复用功能,复用功能模式有两个选择:GPIO_MODE_AF_PP推挽式复用和GPIO_MODE_AF_OD开漏式复用,我们选择推挽式复用。选择了推挽式复用,我们还需要指定复用到哪个外设,通过赋值给Alternate这个结构体成员。然后就是调用HAL_GPIO_Init函数进行IO口的初始化。 最后因为我们用到串口中断,所以还需要中断相关的配置。HAL_NVIC_EnableIRQ函数使能串口1复用通道。HAL_NVIC_SetPriority函数配置串口中断的抢占优先级以及响应优先级。' s+ }- [2 R. ~4 J" O# ?% F3 o 串口初始化由上述两个函数完成,下面就该讲到串口中断服务函数了,其定义如下:7 y! w! C0 ~$ J8 C- O/ e" c
从代码逻辑可以看出,在中断服务函数内部通过调用HAL_UART_GetState函数获取串口状态,计数处理时间是否超时,然后完成一次传输后,调用UART_Receive_IT函数重新开启中断。UART_Receive_IT函数的作用就是把每次中断接收到的字符保存在串口句柄的缓存指针pRxBuffPtr中,同时每次接收一个字符,其计数器RxXferCount减1,直到接收完成RxXferSize个字符,RxXferCount设置为0,同时调用接收回调函数HAL_UART_RxCpltCallback进行处理。 下面列出串口接收中断的一般流程,如图17.3.3.1所示: 1 j( {) r) u$ e4 v ![]() 图17.3.3.1 串口接收中断执行流程图# z0 T4 ]! Q, E# t, Q% u 串口接收回调函数定义如下:/ a9 W5 C) [7 K/ I- ^
因为我们设置了串口句柄成员变量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所示: g_usart_rx_sta bit15 bit14 bit13~0 接收完成标志 接收到0X0D标志 接收到的有效字节个数; L& y7 V1 {# N/ D8 m! z 表17.3.3.1 接收状态寄存器位定义表 设计思路如下:: L3 n6 w, m+ p4 S2 t7 c$ A 当接收到从电脑发过来的数据,把接收到的数据保存在数组g_usart_rx_buf中,同时在接收状态寄存器(g_usart_rx_sta)中计数接收到的有效数据个数,当收到回车(回车的表示由2个字节组成:0X0D和0X0A)的第一个字节0X0D时,计数器将不再增加,等待0X0A的到来,而如果0X0A没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到0X0A,则标记g_usart_rx_sta的第15位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到0X0D,那么在接收数据超过USART_REC_LEN的时候,则会丢弃前面的数据,重新接收。; m, B- u! Z4 @3 V 学到这里大家会发现,HAL库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用HAL_UART_IRQHandler函数,而是直接编写自己的中断服务函数。串口实验我们之所以遵循HAL库写法, 是为了让大家对HAL库有一个更清晰的理解。 2. main.c代码 在main.c里面编写如下代码:
我们主要看无限循环里面的逻辑:首先判断全局变量g_usart_rx_sta的最高位是否为1,如果为1的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口,在上位机显示。这里比较重点的两条语句是:第一条是调用HAL串口发送函数HAL_UART_Transmit来发送一段字符到串口。第二条是我们发送一个字节之后之后,要检测这个数据是否已经被发送完成了。如果全局变量g_usart_rx_sta的最高位为0,则执行一段时间往上位机发送提示字符,以及让LED0每隔一段时间翻转,提示系统正在运行。+ t% M5 {, L Z % G2 q+ n1 a9 C 17.4 下载验证 在下载好程序后,可以看到板子上的LED0开始闪烁,说明程序已经在跑了。串口调试助手,我们用XCOM V2.7,该软件在光盘有提供,且无需安装,直接可以运行,但是需要你的电脑安装有.NET Framework 4.0(WIN自带了)或以上版本的环境才可以。 接着我们打开XCOM V2.7,设置串口为开发板的USB转串口(CH340虚拟串口,得根据你自己的电脑选择,我的电脑是COM8,另外,请注意:波特率是115200)。因为我们在程序上面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符,这里XCOM提供的发送方法是通过勾选发送新行实现,只要勾选了这个选项,每次发送数据后,XCOM都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发送区输入你想要发送的文字,然后单击发送,可以看到如图17.4.1所示信息:$ o+ \, ?* |+ o3 z- n' p9 t( ~ ![]() 可以看到,我们发送的消息被发送回来了。大家可以试试,如果不发送回车(取消发送新行),在输入内容之后,直接按发送是什么结果,大家测试一下吧。 ————————————————0 O: H$ t4 i' o9 k 版权声明:正点原子, r3 ~$ X4 v8 T: g & r+ V% y# V2 R, h1 T5 \* F1 j7 K 3 b. x, m/ Y& G6 p& p F |
【2025·STM32峰会】GUI解决方案实训分享4-使用MVP架构从硬件外设读取数据并显示到图形界面、从图形界面发送指令控制硬件外设
【2025·STM32峰会】GUI解决方案实训分享3-搭建空白TouchGFX例程并实现简单的功能(含硬件部分的串口打印)
【2025·STM32峰会】GUI解决方案实训分享2-编译运行TouchGFX咖啡机例程(含桌面仿真)
【2025·STM32峰会】+TouchGFX实现动态进度显示以及界面切换
【2025·STM32峰会】+使用TouchGFX快速创建GUI
【2025·STM32峰会】GUI解决方案实训分享1-对LVGL咖啡机例程的牛刀小试以及问题排查
实战经验 | 关于STM32H7使用LL库生成ADC代码工作异常问题说明
实战经验 | 关于STM32H745的MC SDK电机控制工程问题的解决办法
【H745I GUI】1.GreenHouse 灯光控制
【Wio Lite AI视觉开发套件】+移植TensorFlow Lite