1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html
本章我们将学习STM32F1的串口,教大家如何使用STM32F1的串口来发送和接收数据。本章将实现如下功能:STM32F1通过串口和上位机的对话,STM32F1在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下几个小节:
17.1 串口简介
17.2 硬件设计
17.3 程序设计
17.4 下载验证
17.1串口简介
学习串口前,我们先来了解一下数据通信的一些基础概念。
17.1.1 数据通信的基础概念
在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们最常的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。
图17.1.1.1 数据传输方式
串行通信的基本特征是数据逐位顺序依次传输,优点是传输线少、布线成本低、灵活度高等优点,一般用于近距离人机交互,特殊处理后也可以用于远距离,缺点就是传输速率低。
而并行通信是数据各位可以通过多条线同时传输,优点是传输速率高,缺点就是布线成本高,抗干扰能力差因而适用于短距离、高速率的通信。
2. 数据传输方向
根据数据传输方向,通信又可分为全双工、半双工和单工通信。全双工、半双工和单工通信的比较如下图所示:
图17.1.1.2 数据传输方式
单工是指数据传输仅能沿一个方向,不能实现反方向传输,如校园广播。
半双工是指数据传输可以沿着两个方向,但是需要分时进行,如对讲机。
全双工是指数据可以同时进行双向传输,日常的打电话属于这种情形。
这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信,而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。
3. 数据同步方式
根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:
图17.1.1.3 数据同步方式
同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。优点:可以实现高速率、大容量的数据传输,以及点对多点传输。缺点:要求发送时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。
异步通信不需要时钟信号,而是在数据信号中加入开始位和停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。优点:没有时钟信号硬件简单,双方时钟可允许一定误差。缺点:通信速率较低,只适用点对点传输。
4. 通信速率
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率:每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为bit/s(即比特每秒),因而又称为比特率。
传码率:每秒钟传输的码元个数,单位为Baud(即波特每秒),因而又称为波特率。
比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们来看看波特率,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定bit的数据信息量。举个例子,在TTL电平标准的通信中,用0V表示逻辑0,5V表示逻辑1,这时候这个码元就可以表示两种状态。如果电平信号0V、2V、4V和6V分别表示二进制数00、01、10、11,这时候每一个码元就可以表示四种状态。
由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。
比特率和波特率的关系可以用以下式子表示:
比特率 = 波特率 * log2M
其中M表示码元承载的信息量。我们也可以理解M为码元的进制数。
举个例子:波特率为100 Baud,即每秒传输100个码元,如果码元采用十六进制编码(即M=2,代入上述式子),那么这时候的比特率就是400 bit/s。如果码元采用二进制编码(即M=2,代入上述式子),那么这时候的比特率就是100 bit/s。
可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了。
17.1.2 串口通信协议简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422和RS-485等。
随着科技的发展,RS-232在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB转串口取代了RS-232串口。我们只需要在电路中添加一个USB转串口芯片,就可以实现USB通信协议和标准UART串行通信协议的转换,而我们开发板上的USB转串口芯片是CH340C这个芯片。关于USB转串口芯片的原理图请看17.2小节。
下面我们来学习串口通信协议,这里主要学习串口通信的协议层。
串口通信的数据包由发送设备的TXD接口传输到接收设备的RXD接口。在串口通信的协议层中,规定了数据包的内容,它由起始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如图17.1.2.1所示。
图17.1.2.1 串口通信协议数据帧格式
串口通信协议数据包组成可以分为波特率和数据帧格式两部分。
图17.1.3.1.1 USART框图
为了方便大家理解,我们把整个框图分成几个部分来介绍。
①USART信号引脚
TX:发送数据输出引脚
RX:接收数据输入引脚
SCLK:发送器时钟输出,适用于同步传输
SW_RX:数据接收引脚,属于内部引脚,用于智能卡模式
IrDA_RDI:IrDA模式下的数据输入
IrDA_TDO:IrDA模式下的数据输出
nRTS:发送请求,若是低电平,表示USART准备好接收数据
nCTS:清除发送,若是高电平,在当前数据传输结束时阻断下一次的数据发送
②数据寄存器
USART_DR包含了已发送或接收到的数据。由于它本身就是两个寄存器组成的,一个专门给发送用的(TDR),一个专门给接收用的(RDR),该寄存器具备读和写的功能。TDR寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR寄存器提供了输入移位寄存器和内部总线之间的并行接口。当进行数据发送操作时,往USART_DR中写入数据会自动存储在TDR内;当进行读取操作时,向USART_DR读取数据会自动提去RDR数据。
USART数据寄存器(USART_DR)低9位数据有效,其他数据位保留。USART_DR的第9位数据是否有效跟USART_CR1的M位设置有关,当M位为0表示8位数据字长;当M位为1时表示9位数据字长,一般使用8位数据字长。
当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。
③控制器
USART有专门控制发送的发送器,控制接收的接收器,还有唤醒单元、中断控制等等,具体在后面讲解USART寄存器的时候细讲。
④时钟与波特率
这部分的主要功能就是为USART提供时钟以及配置波特率。
波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),波特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。
波特率通过以下公式得出:
fck是给串口的时钟(USART2\3\3\4\5 的时钟源为PCLK1,USART1的时钟源为PCLK2),USARTDIV是一个无符号的定点数,存放在波特率寄存器(USART_BRR)的低16位,DIV_Mantissa[11:0]存放的是USARTDIV的整数部分,DIV_Fractionp[3:0]存放的是USARTDIV的小数部分。
下面举个例子说明:
当串口1设置需要得到 115200 的波特率,fck = 72MHz,那么可得:
得到USARTDIV = 39.0625,分离USARTDIV的整数部分与小数部分,整数部分为39,即0x27,那么DIV_Mantissa = 0x27;小数部分为0.0625,转化为十六进制即0.0625*16 = 1,所以DIV_Fractionp = 0x1,USART_BRR寄存器应该赋值为0x271,成功设置波特率为115200。
值得注意USARTDIV是允许有余数的,我们用四舍五入进行取整,这样会导致波特率会有所偏差,而这样的小误差是可以被允许的。
17.1.3.2 USART寄存器
使用STM32F103的USART的配置步骤在《STM32F10xxx参考手册_V10(中文版).pdf》中有列出,这里我们引用手册中的配置步骤:
图17.1.3.2.1 RCC_APB2RSTR寄存器
(3)串口波特率设置
每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。在前面的USART框图部分描述过,为了让大家更好了解波特率寄存器,下面截取USART_BRR寄存器图,如图17.1.3.2.2所示:
图17.1.3.2.2 USART_BRR寄存器
(4)串口控制
STM32F103每个串口都有3个控制寄存器USART_CR1~3,串口的很多配置都是通过这3个寄存器来设置的。USART_CR1寄存器的描述如图17.1.3.2.3所示:
图17.1.3.2.3 USART_CR1寄存器
该寄存器的高18位没有用到,低14位用于串口的功能设置。我们在这里只介绍需要用到的一些位,其他位可以参考《STM32F10xxx参考手册_V10(中文版).pdf》。UE为串口使能位,通过该位置1,使能串口。M为字长,当该位为0的时候设置串口为8个字长外加n个停止位,停止位的个数(n)是根据USART_CR2的[13:12]位设置来决定的,默认为0。PCE为校验使能位,设置为0,即禁止校验,否则使能校验。PS为校验位选择,设置为0为偶校验,否则奇校验。TXIE为发送缓冲区空中断使能位,设置该位为1,当USART_SR中的TXE位为1时,将产生串口中断。TCIE为发送完成中断使能位,设置该位为1,当USART_SR中的TC位为1时,将产生串口中断。RXNEIE为接收缓冲区非空中断使能,设置该位为1,当USART_SR中的ORE或者RXNE位为1时,将产生串口中断。TE为发送使能位,设置为1,将开启串口的发送功能。RE为接收使能位,用法同TE。
(5)数据发送与接收
STM32的发送与接收是通过数据寄存器USART_DR来实现的,这是一个双寄存器,包含了TDR和RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。在前面的USART框图已经对USART_DR有详细的介绍,大家可以自行查阅。下面看一下寄存器的各位描述如图17.1.3.2.4。
图17.1.3.2.4 USART_DR寄存器
(6)串口状态
串口状态通过状态寄存器USART_SR读取。USART_SR的各位描述如图17.1.3.2.5所示:
图17.1.3.2.5 USART_SR寄存器
这里我们关注一下两个位,第5、6位RXNE和TC。
RXNE(读数据寄存器非空),当该位被置1的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_DR,通过读USART_DR可以将该位清零,也可以向该位写0,直接清除。
TC(发送完成),当该位被置位的时候,表示USART_DR内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:
1)读USART_SR,写USART_DR。
2)直接向该位写0
通过以上一些寄存器的操作再加上IO口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32F10xxx参考手册_V10(中文版).pdf》关于通用同步异步收发器这一章的相关知识。
17.1.4 GPIO引脚复用功能
我们知道芯片有许多外设,而引脚的资源是很有限的,为了解决这个问题,方法就是引脚复用,这样使得引脚除了作为普通的IO口之外,还会与一些外设关联起来,作为第二功能使用,而且一个引脚不单单只有一种复用功能,而是拥有多个第二功能,但是一次只允许一个外设的复用功能,以确保共用同一个 IO 引脚的外设之间不会产生冲突。
下面我们把之前没讲解的复用功能寄存器AFIO讲解一下。
AFIO寄存器的作用就是复用功能I/O和调试配置的,STM32F103ZET6共有6个AFIO的寄存器,事件控制寄存器AFIO_EVCR、复用重映射和调试I/O配置寄存器AFIO_MAPR、外部中断配置寄存器AFIO_EXTICR1、外部中断配置寄存器AFIO_EXTICR2、外部中断配置寄存器AFIO_EXTICR3和外部中断配置寄存器AFIO_EXTICR4。
在对这些寄存器进行读写操作前,应先打开AFIO时钟,该时钟在RCC_APB2ENR寄存器上的位0上配置,在位0上置0表示辅助功能IO时钟关闭;在位0上置1表示辅助功能IO时钟开启。
事件控制寄存器AFIO_EVCR,用得比较少这里不作过多介绍。
AFIO_EXTICRx寄存器,在中断章节再来详细讲解,本章节不涉及这些寄存器的配置。
复用重映射和调试I/O配置寄存器AFIO_MAPR寄存器描述,如图17.1.4.1所示。
图17.1.4.1 AFIO_MAPR寄存器
在对AFIO_MAPR寄存器某些位进行写入实现引脚的重新映射,这时候,复用功能不再映射到它们原始分配上。例如AFIO_MAPR寄存器位2是对USART1的重映射,置0: 没有重映像(TX/PA9,RX/PA10); 置1: 重映像(TX/PB6,RX/PB7)。默认情况下,PA9和PA10是作为串口1的引脚使用,假如PA9和PA10被用作其他地方,但还是需要用到串口1,那么就可以在AFIO_MAPR的位2置1,把串口1的引脚重映射到PB6和PB7。这个串口初始化的过程,就有点变化,需要初始化AFIO时钟,和对AFIO_MAPR的第2位进行置1操作,其他与普通串口配置没有区别。
HAL库关于端口复用相关的代码在STM32F1xx_hal_gpio_ex.h文件中可以找到,USART1重映射操作代码如下:
/**
* @brief Enable the remapping of USART1 alternate function TX and RX.
* @note ENABLE: Remap (TX/PB6, RX/PB7)
* @retval None
*/
#define __HAL_AFIO_REMAP_USART1_ENABLE() \
AFIO_REMAP_ENABLE(AFIO_MAPR_USART1_REMAP)
/**
* @brief Disable the remapping of USART1 alternate function TX and RX.
* @note DISABLE: No remap (TX/PA9, RX/PA10)
* @retval None
*/
#define __HAL_AFIO_REMAP_USART1_DISABLE() \
AFIO_REMAP_DISABLE(AFIO_MAPR_USART1_REMAP)
以上是使用重映射的串口1的介绍。
17.2 硬件设计
图17.2.1 USB转串口原理图
这里需要注意的是:上图中的P4的RXD和PA9用跳线帽连接,以及TXD和PA10也用跳线帽连接。如图17.2.2所示:
图17.2.2 短路帽连接
17.3 程序设计
17.3.1 USART的HAL库驱动
HAL库中关于串口的驱动程序比较多,我们主要先来学习本章需要用到的,其余的后续用到再讲解。因为我们现在只是用到异步收发器功能,所以我们现在只需要STM32F1xx_hal_uart.c文件(及其头文件)的驱动代码,STM32F1xx_hal_usart.c是通用同步异步收发器,暂时没有用到,可以暂时不看。用到一个外设第一个函数就应该是其初始化函数。
typedef struct
{
USART_TypeDef *Instance; /* UART寄存器基地址 */
UART_InitTypeDef Init; /* UART通信参数 */
uint8_t *pTxBuffPtr; /* 指向 UART 发送缓冲区 */
uint16_t TxXferSize; /* UART发送数据的大小 */
__IO uint16_t TxXferCount; /* UART发送数据的个数 */
uint8_t *pRxBuffPtr; /* 指向UART接收缓冲区 */
uint16_t RxXferSize; /* UART接收数据大小 */
__IO uint16_t RxXferCount; /* UART接收数据的个数 */
DMA_HandleTypeDef *hdmatx; /* UART 发送参数设置(DMA) */
DMA_HandleTypeDef *hdmarx; /* UART 接收参数设置(DMA) */
HAL_LockTypeDef Lock; /* 锁定对象 */
__IO HAL_UART_StateTypeDef gState; /* UART发送状态结构体 */
__IO HAL_UART_StateTypeDef RxState; /* UART接收状态结构体 */
__IO uint32_t ErrorCode; /* UART操作错误信息 */
}UART_HandleTypeDef;
1)Instance:指向UART 寄存器基地址。实际上这个基地址HAL库已经定义好了,可以选择范围:USART1~ USART3、UART4、UART5。
2)Init:UART初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。下面我们再详细讲解这个结构体。
3)pTxBuffPtr,TxXferSize,TxXferCount:分别是指向发送数据缓冲区的指针,发送数据的大小,发送数据的个数。
4)pRxBuffPtr,RxXferSize,RxXferCount:分别是指向接收数据缓冲区的指针,接受数据的大小,接收数据的个数;
5)hdmatx,hdmarx:配置串口发送接收数据的 DMA具体参数。
6)Lock:对资源操作增加操作锁保护功能,可选HAL_UNLOCKED或者HAL_LOCKED两个参数。如果gState的值等于HAL_UART_STATE_RESET,则可认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit函数来对串口的GPIO和时钟进行初始化。
7)gState,RxState:分别是UART的发送状态、工作状态的结构体和UART接受状态的结构体。HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于gState,如 HAL_UART_STATE_BUSY。
8)ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。
下面,我们来了解UART_InitTypeDef 这个结构体类型,该结构体用于配置UART的各个通信参数,包括波特率,停止位等,具体说明如下:
typedef struct
{
uint32_t BaudRate; /* 波特率 */
uint32_t WordLength; /* 字长 */
uint32_t StopBits; /* 停止位 */
uint32_t Parity; /* 校验位 */
uint32_t Mode; /* UART模式 */
uint32_t HwFlowCtl; /* 硬件流设置 */
uint32_t OverSampling; /* 过采样设置 */
}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过采样。
函数返回值:
HAL_StatusTypeDef枚举类型的值,有4个,分别是HAL_OK表示成功,HAL_ERROR表示错误,HAL_BUSY表示忙碌,HAL_TIMEOUT超时。后续遇到该结构体也是一样的。
2. HAL_UART_Receive_IT函数
HAL_UART_Receive_IT函数是开启串口接收中断函数。其声明如下:
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size);
函数描述:
用于开启以中断的方式接收指定字节。数据接收在中断处理函数里面实现。
函数形参:
形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。
形参2是要接收的数据地址。
形参3是要接收的数据大小,以字节为单位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_UART_IRQHandler函数
HAL_UART_IRQHandler函数是HAL库中断处理公共函数。其声明如下:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
函数描述:
该函数是HAL库中断处理公共函数,在串口中断服务函数中被调用。
函数形参:
形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。
函数返回值:
无
注意事项:
该函数是HAL库已经定义好,用户一般不能随意修改。如果用户要在中断中实现自己的逻辑代码,可以直接在函数HAL_UART_IRQHandler 的前面或者后面添加新代码,也可以直接在 HAL_UART_IRQHandler调用的各种回调函数里面执行,这些回调都是弱定义的,方便用户直接在其它文件里面重定义。串口回调函数主要有下面几个:
__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)
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart)
本实验我们用到的是接收回调函数HAL_UART_RxCpltCallback,就是在接收回调函数里面编写我们的接收逻辑代码,具体请参考实验源码。
串口通讯配置步骤
__HAL_RCC_USART1_CLK_ENABLE(); /* 使能USART1时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能GPIOA时钟 */
3)GPIO模式设置(速度、上下拉、复用功能等)
GPIO模式设置通过调用HAL_GPIO_Init函数实现,详见本例程源码。
4)开启串口相关中断,配置串口中断优先级
本实验我们使用串口中断来接收数据。我们使用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函数实现数据接收处理,详见本例程源码。
6)串口数据接收和发送
最后我们可以通过读写USART_DR寄存器,完成串口数据的接收和发送,HAL库也给我们提供了:HAL_UART_Receive和HAL_UART_Transmit两个函数用于串口数据的接收和发送。
大家可以根据实际情况选择使用方式来收发串口数据。
17.3.2 程序流程图
图17.3.2.1 串口通信实验程序流程图
17.3.3 程序解析
#define USART_TX_GPIO_PORT GPIOA
#define USART_TX_GPIO_PIN GPIO_PIN_9
/* 发送引脚时钟使能 */
#define USART_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_RX_GPIO_PORT GPIOA
#define USART_RX_GPIO_PIN GPIO_PIN_10
/* 接收引脚时钟使能 */
#define USART_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_UX USART1
#define USART_UX_IRQn USART1_IRQn
#define USART_UX_IRQHandler USART1_IRQHandler
/* USART1 时钟使能 */
#define USART_UX_CLK_ENABLE() do{ __HAL_RCC_USART1_CLK_ENABLE(); }while(0)
USART1_IRQn也就是我们中断向量表的37号中断,USART1_IRQHandler是串口1的中断服务函数。每个串口都有自己的中断函数,但是我们最终都是通过回调函数去实现逻辑代码,当然我们亦可在中断函数里实现逻辑代码。
另外我们还定义了三个宏,具体如下:
#define USART_REC_LEN 200 /* 定义最大接收字节数 200 */
#define USART_EN_RX 1 /* 使能(1)/禁止(0)串口1接收 */
#define RXBUFFERSIZE 1 /* 缓存大小 */
可以看到USART_REC_LEN表示最大接收字节数,这里定义的是200个字节,后续如果有需求要发送更大的数据包,可以改大这个值,这里不改太大,是避免浪费太多内存。USART_EN_RX则是用于使能串口1的接收数据。RXBUFFERSIZE是缓冲大小。
下面我们再解析usart.c的程序,先看串口1的初始化函数,其定义如下:
/**
* @brief 串口X初始化函数
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @note 注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
* 这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
* @retval 无
*/
void usart_init(uint32_t baudrate)
{
uartx_handle.Instance = USART_UX; /* USART1 */
uartx_handle.Init.BaudRate = baudrate; /* 波特率 */
uartx_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
uartx_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
uartx_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
uartx_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
uartx_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&uartx_handle); /* HAL_UART_Init()会使能UART1 */
/该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量/
HAL_UART_Receive_IT(&uartx_handle, (uint8_t *)aRxbuffer, RXBUFFERSIZE);
}
uartx_handle 是结构体UART_HandleTypeDef类型的全局变量,UART_HandleTypeDef结构体成员的含义请回到前面回顾。波特率我们直接赋值给uartx_handle.Init.BaudRate这个成员,可以看出很方便。需要注意的是,最后一行代码调用函数HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量。
上面的初始化函数只是串口初始化的其中一部分,我们还有一部分初始化需要HAL_UART_MspInit函数去完成。HAL_UART_MspInit是HAL库定义的弱定义函数,这里我们做重定义以实现我们的初始化需求。HAL_UART_MspInit函数在HAL_UART_Init函数中会被调用,其定义如下:
/**
* @brief UART底层初始化函数
* @param huart: UART句柄类型指针
* @note 此函数会被HAL_UART_Init()调用
* 完成时钟使能,引脚配置,中断配置
* @retval 无
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance == USART1) /* 如果是串口1,进行串口1 MSP初始化 */
{
USART_UX_CLK_ENABLE(); /* USART1 时钟使能 */
USART_TX_GPIO_CLK_ENABLE(); /* 发送引脚时钟使能 */
USART_RX_GPIO_CLK_ENABLE(); /* 接收引脚时钟使能 */
gpio_init_struct.Pin = USART_TX_GPIO_PIN; /* TX引脚 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct); /* 初始化发送引脚 */
gpio_init_struct.Pin = USART_RX_GPIO_PIN; /* RX引脚 */
gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;
HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct); /* 初始化接收引脚 */
#if USART_EN_RX
HAL_NVIC_EnableIRQ(USART_UX_IRQn); /* 使能USART1中断通道 */
HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */
#endif
}
}
怎么来理解这个函数呢?该函数主要实现底层的初始化,事实上这个函数的代码还可以直接放到usart_init函数里面,但是HAL库为了代码的功能分层初始化,定义这个函数方便用户使用。所以我们也按照HAL库的这个结构来初始化外设。这个函数首先是调用if(huart->Instance == USART1)判断是要初始化个串口,因为每个串口初始化都会调用HAL_UART_MspInit这个函数,所以需要判断是哪个串口要初始化才做相应的处理。只能说HAL库这样的结构机制有好处,自然也有坏处。
首先就是使能串口以及PA9和PA10的时钟,PA9和PA10需要用做复用功能,复用功能模式有两个选择:GPIO_MODE_AF_PP推挽式复用和GPIO_MODE_AF_OD开漏式复用,我们选择的是推挽式复用,因为PA9是一个发送管脚,所以模式设置为复用推挽输出,而PA10是一个接收管脚,所以它的模式设置为浮空输入即可,GPIO_MODE_AF_INPUT实质就是一个GPIO_MODE_INPUT浮空输入模式。选择了推挽式复用。然后就是调用HAL_GPIO_Init函数进行IO口的初始化。
最后因为我们用到串口中断,所以还需要中断相关的配置。HAL_NVIC_EnableIRQ函数使能串口1复用通道。HAL_NVIC_SetPriority函数配置串口中断的抢占优先级以及响应优先级。
串口初始化由上述两个函数完成,下面就该讲到串口中断服务函数了,其定义如下:
/**
* @brief 串口X中断服务函数
注意,读取USARTx->SR能避免莫名其妙的错误
* @param 无
* @retval 无
*/
void USART_UX_IRQHandler(void)
{
#if SYSTEM_SUPPORT_OS /*使用OS*/
OSIntEnter();
#endif
HAL_UART_IRQHandler(&uartx_handler); /*调用HAL库中断处理公用函数*/
while (HAL_UART_Receive_IT(&uartx_handler, (uint8_t *)aRxBuffer,
RXBUFFERSIZE) != HAL_OK) /*一次处理完成之后,重新开启中断并设置RxXferCount为1*/
{
/* 如果出错会卡死在这里 */
}
#if SYSTEM_SUPPORT_OS /*使用OS*/
OSIntExit();
#endif
}
从代码逻辑可以看出,在中断服务函数内部通过调用HAL_UART_GetState函数获取串口状态,计数处理时间是否超时,然后完成一次传输后,调用UART_Receive_IT函数重新开启中断。UART_Receive_IT函数的作用就是把每次中断接收到的字符保存在串口句柄的缓存指针pRxBuffPtr中,同时每次接收一个字符,其计数器RxXferCount减1,直到接收完成RxXferSize个字符之后RxXferCount设置为0,同时调用接收回调函数HAL_UART_RxCpltCallback进行处理。HAL_MAX_DELAY最大延时时间在STM32F1xx_hal_def.h中定义。
下面列出串口接收中断的一般流程,如图17.3.3.1所示:
图17.3.3.1 串口接收中断执行流程图
串口接收回调函数定义如下:
/**
* @brief UART数据接收回调接口
数据处理在这里进行
* @param huart:串口句柄
* @retval 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART_UX) /* 如果是串口1 */
{
if((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成 */
{
if(g_usart_rx_sta & 0x4000) /* 接收到了0x0d(即回车键)*/
{
if(aRxBuffer[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键)*/
{
g_usart_rx_sta = 0; /*接收错误,重新开始 */
}
else /* 接收到的是0x0a(即换行键)*/
{
g_usart_rx_sta |= 0x8000; /* 接收完成了 */
}
}
else /* 还没收到0X0D(即回车键) */
{
if (aRxBuffer[0] == 0x0d)
g_usart_rx_sta |= 0x4000;
else
{
g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = aRxBuffer[0];
g_usart_rx_sta++;
if (g_usart_rx_sta > (USART_REC_LEN - 1))
{
g_usart_rx_sta = 0; /* 接收数据错误,重新开始接收 */
}
}
}
}
}
}
因为我们设置了串口句柄成员变量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标志 接收到的有效字节个数
表15.3.3.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库有一个更清晰的理解。
2. main.c代码
在main.c里面编写如下代码:
int main(void)
{
uint8_t len;
uint16_t times = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟为72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
while (1)
{
if (g_usart_rx_sta & 0x8000) /* 接收到了数据? */
{
len = g_usart_rx_sta & 0x3fff; /* 得到此次接收到的数据长度 */
printf("\r\n您发送的消息为:\r\n");
/*发送接收到的数据*/
HAL_UART_Transmit(&uartx_handler,(uint8_t*)g_usart_rx_buf,len,1000);
/*等待发送结束*/
while(__HAL_UART_GET_FLAG(&uartx_handler,UART_FLAG_TC)!=SET);
printf("\r\n\r\n"); /* 插入换行 */
g_usart_rx_sta = 0;
}
else
{
times++;
if (times % 5000 == 0)
{
printf("\r\n正点原子 STM32开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
if (times % 30 == 0) LED0_TOGGLE(); /* 闪烁LED,提示系统正在运行. */
delay_ms(10);
}
}
}
我们主要看无限循环里面的逻辑:首先判断全局变量g_usart_rx_sta的最高位是否为1,如果为1的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口,在上位机显示。这里比较重点的两条语句是:第一条是调用HAL串口发送函数HAL_UART_Transmit来发送一段字符到串口。第二条是我们发送一个字节之后,要检测这个数据是否已经被发送完成了。如果全局变量g_usart_rx_sta的最高位为0,则执行一段时间往上位机发送提示字符,以及让LED0每隔一段时间翻转,提示系统正在运行。
17.4 下载验证
在下载好程序后,可以看到板子上的LED0开始闪烁,说明程序已经在跑了。串口调试助手,我们用XCOM,该软件在光盘有提供,且无需安装,直接可以运行,但是需要你的电脑安装有.NET Framework 4.0(WIN自带了)或以上版本的环境才可以,该软件的详细介绍请看:http://www.openedv.com/posts/list/22994.htm 这个帖子。
接着我们打开XCOM(正点原子的串口调试助手,位于光资料盘(A盘)→6,软件资料→1,软件→串口调试助手→XCOM),设置串口为开发板的USB转串口(CH340虚拟串口,得根据你自己的电脑选择,我的电脑是COM15,另外,请注意:波特率是115200)。因为我们在程序上面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符,这里XCOM提供的发送方法是通过勾选发送新行实现,只要勾选了这个选项,每次发送数据后,XCOM都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发送区输入你想要发送的文字,然后单击发送,可以看到如图15.4.1所示信息:
图15.4.1 串口助手
可以看到,我们发送的消息被发送回来了。大家可以试试,如果不发送回车(取消发送新行),在输入内容之后,直接按发送是什么结果,大家测试一下吧。