今天我们学习STM32CubeMX串口的操作,以及HAL库串口的配置,我们会详细的讲解各个模块的使用和具体功能,并且基于HAL库实现Printf函数功能重定向,UART中断接收,本章不讲解串口的原理,在之前的51单片机里面就有一章讲过串口通信的原理了,感兴趣的可以去前面看看。
一、原理简介
STM32串口简介
USART-通用同步异步收发器(Universal Synchronous Asynchronous Receiver and Transmitter)是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。有别于 USART 还有一个UART(Universal Asynchronous Receiver and Transmitter),它是在 USART 基础上裁剪掉了同步通信功能(时钟同步),只有异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是 UART。
1.物理层
1)RS232标准
很多单片机内部例如我们所用的STM32,以及一些传感器一般都是TTL电平。
RS232是一种串行数据传输形式,称其为串行连接,最经典的标志就是 9 针孔的 DB9 电缆RS232电压表示逻辑 1 ,0的范围大极大的增强了容错率,主要用于工业设备直接通信。
两个通讯设备的“DB9 接口”之间通过串口信号线建立起连接,串口信号线中使用“RS-232 标准”传输数据信号。由于 RS-232 电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL 标准”的电平信号,才能实现通讯。
2)USB转串口(重点)
至于为什么是重点因为这是我实验用的方式重点介绍:
USB转串口:主要用于设备(STM32)与电脑通信
原理图:一定要搞懂下面这张图
2.协议层
串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致(一样的起始位 数据 校验位 停止位)才能正常收发数据
1)通讯的起始和停止信号
串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑 0 的数据位表示,而数据包的停止信号可由 0.5、1、1.5 或 2 个逻辑 1 的数据位表示
1个停止位:停止位位数的默认值。
2个停止位:可用于常规USART模式、单线模式以及调制解调器模式。
0.5个停止位:在智能卡模式下接收数据时使用。
1.5个停止位:在智能卡模式下发送和接收数据时使用。
2)有效数据
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为 5、6、7 或 8 位长
3)数据校验
偶校验:校验位使得一帧中的7或8个LSB数据以及校验位中’1’的个数为偶数。
例如:数据=00110101,有4个’1’,如果选择偶校验(在USART_CR1中的PS=0),校验位将是’0’,最后数据检验如果数据有偶数个1则数据传输没有出错(但不是绝对的,如果同时两个数据为发送错误(0变成1)则还是偶数个1)
-
奇校验:此校验位使得一帧中的7或8个LSB数据以及校验位中’1’的个数为奇数。
例如:数据=00110101,有4个’1’,如果选择奇校验(在USART_CR1中的PS=1),校验位将是’1’,最后数据检验如果数据有奇数个1则数据传输没有出错,但同样不是绝对的(同时两个1变成0)
二、寄存器讲解
1.控制寄存器
传输模式:如果USART_CR1的PCE位被置位,如果奇偶校验失败USART_SR寄存器中的PE标志被置’1’,并且如果USART_CR1寄存器的PEIE在被预先设置的话,中断产生(我们可以在相应的中断服务函数中,写处理校验失败的代码)。
2.状态寄存器
3.USART 功能框图(超级重要)
1)功能引脚:
2)数据寄存器(重点)
下面这张图也非常重要理解理解!!
3)控制单元(重点)
<1>发送器
发送器根据M位的状态发送8位或9位的数据字。当发送使能位(TE)被设置时,发送移位寄存器中的数据在TX脚上输出,相应的时钟脉冲在CK脚上输出。
一个字符帧发送需要三个部分:起始位+数据帧(可能有校验位)+停止位。每个字符(一个数据帧)之前都有一个低电平的起始位,之后跟着的停止位,其数目可配置,数据帧就是我们要发送的 8 位或 9 位数据,数据是从最低位开始传输的,停止位是一定时间周期的高电平。
配置步骤:
1.通过在USART_CR1寄存器上置位UE位来激活USART
2.编程USART_CR1的M位来定义字长。
3.在USART_CR2中编程停止位的位数。
4.如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器,关于DMA下期再详细讲解。
5.利用USART_BRR寄存器选择要求的波特率。
发送和接收由一共用的波特率发生器驱动,当发送器和接收器的使能位分别置位时,分别为其产生时钟。
6.设置USART_CR1中的TE位,发送一个空闲帧帧(一个数据帧长度的高电平)
作为第一次数据发送。
7.把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
8.在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束(移位寄存器中的数据全部发送完毕)
。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。
深入理解TXE位与TC位
清零TXE位总是通过对数据寄存器的写操作(CPU 或 DMA)来完成的,当TXE位已经被硬件置1它表明:
● 数据已经从TDR移送到移位寄存器,数据发送已经开始(发送移位寄存器正在一位一位向外传输数据)
● TDR寄存器被清空
● 下一个数据可以被写进USART_DR寄存器而不会覆盖先前的数据如果TXEIE位被设置,此标志将产生一个中断。
如果此时USART正在发送数据(发送移位寄存器正在一位一位向外传输数据),对USART_DR寄存器的写操作把数据存进TDR寄存器,并在当前传输结束时把该数据复制进移位寄存器,也就是说移位寄存器里面的数据并不会被覆盖,所以我觉得只要你发送一帧数据等待TXE置1,就算是发送多帧数据时最后也不用等待TC=1。
如果此时USART没有在发送数据,处于空闲状态,对USART_DR寄存器的写操作直接把数据放进移位寄存器,数据传输开始,TXE位立即被置起。
当一帧发送完成时(停止位发送后)并且设置了TXE位,TC位被置起,如果USART_CR1寄存器中的TCIE位被置起时,则会产生中断
使用下列软件过程清除TC位:
1.读一次USART_SR寄存器;
2.写一次USART_DR寄存器。
TC位也可以通过软件对它写’0’来清除。此清零方式只推荐在多缓冲器通信模式下使用
<2>接收器
如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,使得接收器在 RX 线开始搜索起始位。在确定到起始位后就根据 RX 线电平状态把数据存放在接收移位寄存器内。接收完成后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置1,同时如果 USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。
当一字符被接收到时.
- RXNE位被置1。它表明移位寄存器的内容被转移到RDR。换句话说,数据已经被接收并且可以被读出。
- 如果RXNEIE位被设置,产生中断。
- 在多缓冲器通信时,RXNE在每个字节接收后被置起,并由DMA对数据寄存器的读操作而清零。
- 在单缓冲器模式里,由软件读USART_DR寄存器完成对RXNE位清除,RXNE标志也可以通过对它写0来清除。RXNE位必须在下一字符接收结束前(接收移位寄存器接收满)被清零(要将数据读出),以避免溢出错误(移位寄存器的数据会被覆盖)。
溢出错误
如果RXNE还没有被复位(还没有读出DR寄存器的数据),又接收到一个字符,则发生溢出错误,数据只有当RXNE位被清零后才能从移位寄存器转移到RDR寄存器。RXNE标记是接收到每个字节后被置位的。如果下一个数据已被收到或先前DMA请求还没被服务时,RXNE标志仍是1,溢出错误产生。
当溢出错误产生时:
- ORE位被置位。
- RDR内容将不会丢失。读USART_DR寄存器仍能得到先前的数据。
- 移位寄存器中以前的内容将被覆盖。随后接收到的数据都将丢失。
- 如果RXNEIE位被设置或EIE和DMAR位都被设置,中断产生。
- 顺序执行对USART_SR和USART_DR寄存器的读操作,可复位ORE位
USART相关中断:
三、工程创建
1.设置RCC,开启外部高速晶振。
2.设置串口
- 1点击USATR1
- 2设置MODE为异步通信(Asynchronous)
- 3基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能
- 4. NVIC Seting使能接收中断。
- 5.设置时钟
- 1选择外部时钟HSE 8MHz
- 2PLL锁相环倍频72倍
- 3系统时钟来源选择为PLL
- 4设置APB1分频器为 /2
3.工程的设置
然后就可以点击工程的创建。
三、HAL函数讲解。
1.UART结构体定义
UART_HandleTypeDef huart1;
UART的名称定义,这个结构体中存放了UART所有用到的功能,后面的别名就是我们所用的uart串口的别名,默认为huart1,可以自行修改。
打开这个结构体:
2.串口的发送/接收函数:
- HAL_UART_Transmit();串口发送数据,使用超时管理机制
- HAL_UART_Receive();串口接收数据,使用超时管理机制
- HAL_UART_Transmit_IT();串口中断模式发送
- HAL_UART_Receive_IT();串口中断模式接收
- HAL_UART_Transmit_DMA();串口DMA模式发送
- HAL_UART_Transmit_DMA();串口DMA模式接收
这里的函数参数基本上一样,挑两个来讲解一下。
(1)串口发送数据函数:
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
功能:串口发送指定长度的数据。如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)。
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
- *pData 需要发送的数据
- Size 发送的字节数
- Timeout 最大发送时间,发送数据超过该时间退出发送
举例: HAL_UART_Transmit(&huart1, (uint8_t *)ZZX, 3, 0xffff); //串口发送三个字节数据,最大传输时间0xffff。
(2)串口中断接收数据函数:
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口中断接收,以中断方式接收指定长度数据。
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
- *pData 接收到的数据存放地址
- Size 接收的字节数
大致过程是:设置数据存放位置,接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。再然后,串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断。(只触发一次中断)
举例: HAL_UART_Receive_IT(&huart1,(uint8_t *)&value,1); //中断接收一个字符,存储到value中
3.串口中断函数:
- HAL_UART_IRQHandler(UART_HandleTypeDef *huart); //串口中断处理函数
- HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //串口发送中断回调函数
- HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); //串口发送一半中断回调函数(用的较少)
- HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //串口接收中断回调函数
- HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//串口接收一半回调函数(用的较少)
- HAL_UART_ErrorCallback();串口接收错误函数
(1)串口接收中断回调函数
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
功能:对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用。
如果接收数据,则会进行接收中断处理函数。
/* UART in mode Receiver ---------------------------------------------------*/
if((tmp_flag != RESET) && (tmp_it_source != RESET))
{
UART_Receive_IT(huart);
}
如果发送数据,则会进行发送中断处理函数。
/* UART in mode Transmitter ------------------------------------------------*/
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
4.串口查询函数
HAL_UART_GetState(); 判断UART的接收是否结束,或者发送数据是否忙碌
举例:
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX) //检测UART发送结束
5.USART的重定义。
重新定义printf函数
在 stm32f1xx_hal.h中包含#include <stdio.h>
#include "stm32f4xx_hal.h"
#include <stdio.h>
extern UART_HandleTypeDef huart1; //声明串口
在 stm32f1xx_hal.c 中重写fget和fput函数
/**
* 函数功能: 重定向c库函数printf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
记得打开魔术棒:
使用微库。MicoroLIB 是缺省 C 库的备选库,它对标准 C 库进行了高度优化使代码更少,占用更少资源。为使用 printf、 scanf 函数需要
在文件中包含 stdio.h 头文件
四、实验示例
实验一:串口接收中断
因为中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再调用一次中断接收函数
一、.具体流程:
1、初始化串口
2、在main中第一次调用接收中断函数
3、进入接收中断,接收完数据 进入中断回调函数
4、修改HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,
5 回调函数中要调用一次HAL_UART_Receive_IT函数,使得程序可以重新触发接收中断
二、函数流程图:
HAL_UART_Receive_IT(中断接收函数) -> USART2_IRQHandler(void)(中断服务函数) -> HAL_UART_IRQHandler(UART_HandleTypeDef *huart)(中断处理函数) ->
UART_Receive_IT(UART_HandleTypeDef *huart) (接收函数) ->
HAL_UART_RxCpltCallback(huart);(中断回调函数)
HAL_UART_RxCpltCallback函数就是用户要重写在main.c里的回调函数。
三、代码实现:
并在main.c中添加下列定义:
#include <string.h>
#define RXBUFFERSIZE 256 //最大接收字节数
char RxBuffer[RXBUFFERSIZE]; //接收数据
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
在main()主函数中,调用一次接收中断函数
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
/* USER CODE END 2 */
在main.c下方添加中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer));
HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF);
}
else
{
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存
if((RxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart1, (uint8_t *)&RxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //再开启接收中断
}
}
发送数据被正常返回。
实验二:串口接收字符串数据并且判断数据
如果要实现串口接收数据的判断,如下:
宏定义和变量定义:
const char String1[8] = "kaishi";
const char String2[11] = "chushihua";
const char String3[9] = "xianshi";
#define RXBUFFERSIZE 256 //最大接收字节数
char RxBuffer[RXBUFFERSIZE]; //接收数据
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
串口接收中断回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存
if((RxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
Uart1_Rx_Cnt = 0;
if(strstr((const char *)RxBuffer,String1) != NULL)
{
//当接收到数据和String1一样时,需要实现的写在这
}
memset(RxBuffer,0x00,sizeof(RxBuffer)); //清空数组
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //再开启接收中断
}
}