STM32CubeMX教程9 USART/UART 异步通信
读者可访问 GitHub - lc-guo/STM32CubeMX-Series-Tutorial 获取原始工程代码
1、准备材料
STM32CubeMX软件(Version 6.10.0)
keil µVision5 IDE(MDK-Arm)
逻辑分析仪nanoDLA
2、实验目标
使用STM32CubeMX软件配置STM32F407开发板USART1与PC进行异步通信(阻塞传输方式、中断传输方式),具体为 使用WK_UP按键触发串口输出,每按下一次WK_UP按键就以中断方式发送一次数据,并在串口传输完成中断回调函数中输出提示信息和翻转RED_LED灯的状态,同时使用串口中断接收回调函数完成对用户发来的命令解析,发送命令“#1;”则点亮GREEN_LED,发送命令“#0;”则熄灭GREEN_LED。
3、实验流程
3.0、前提知识
USART为通用同步异步收发器,是一种串行通信接口,类似的通信协议还有USB、RS232和RS485等,他们之间电平不同因此不可以直接通信,但是可以通过转换芯片进行逻辑电平的相互转换,从而实现在不同的串行通信方式下的信息传输
对于STM32F4系列来说,USART的高电平1表示的电压范围为2.0V~3.3V(通常VDD电源电压的大约70-100%),低电平0的电压范围为0V~0.3V;USART通信中一般需要设置波特率、数据字长、校验位和停止位四个参数,如下图所示位串行数据发送时序图
Baud Rate (波特率):由于本实验的串口工作在异步通信模式,因此需要规定一个特定的传输速率,这样收发双方都以该速率解析发送的内容,才能不出错的进行通信,常见波特率9600/115200等,当然也可自定义波特率
Word Length (数据字长):可选8/9位,即一帧数据中传输的数据位数,由于一字节为8位,因此该参数默认为8位
Parity (校验位):可选无/奇/偶校验
Stop Bits (停止位):可选1/2个停止位,一般选择1个停止位
设置波特率为115200,8位字长,无校验位,1个停止位,利用单片机串口发送“Reset\r\n”信息,然后利用逻辑分析仪对TX引脚电平进行捕获,如下图所示为TX引脚捕获电平波形图
STM32F407ZGT6一共有6组串口,包括4组通用同步/异步收发器USART1、2、3、6和2组通用异步收发器UART4、5
通用异步收发器可以工作在异步通信、单线半双工、多处理器通信、红外和局域互连网络(LIN)等模式
通用同步/异步收发器除可以工作在上述模式外还具有同步通信和智能卡等工作模式,本文只介绍这6组串口的异步通信模式(最常用的模式),其他模式均不涉及,如下图示为USART1可选工作模式列表
单片机的串口并不能直接和电脑的USB端口通信,因而需要在单片机和电脑之间利用串口芯片搭建沟通的桥梁,常用的串口芯片有CH34XX和CP210X,对于串口芯片一般需要安装驱动程序,请自行查看开发板串口所示用的串口芯片,然后下载对应驱动程序
一般来说能够实现电脑和单片机正常串口通信需要满足“电脑USB接口 ⇔ 开发板USB接口 ⇔ 串口芯片 ⇔ 单片机串口RX/TX引脚”的物理连接 (注释1),当其他的一切均正常使用USB线连接电脑与开发板,在Windows的设备管理器页面,端口栏目下会出现对应串口芯片识别成功的端口号,如下图所示
如下图所示为正点原子stm32f407探索者开发板V2.4开发板上使用串口的硬件原理图,开发板上的USB接口经过串口芯片CH340G然后利用跳线帽将TXD/RXD与单片机上的PA9/10连接了起来,读者在开发时应注意检查开发板上USART1的两个引脚是否利用跳线帽与TXD/RXD进行了短接
串口通信中数据传输一般可以分为阻塞式数据传输和非阻塞式数据传输两种,而阻塞模式也即轮询模式,在此模式下,串口发送或者接收数据都会产生阻塞,单片机只能一直等待接收/发送完成或者达到设定的超时时间;非阻塞模式是使用中断或者DMA的方式来传输数据,顾名思义,不会产生阻塞现象,发送/接收数据的同时单片机还可以处理其他任务。本文不涉及DMA,因此非阻塞模式仅仅介绍使用中断的传输方式
3.1、CubeMX相关配置
3.1.0、工程基本配置
打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU,(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立”
3.1.1、时钟树配置
系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示
3.1.2、外设参数配置
在Pinout & Configuration页面左边功能分类栏目Connectivity中单击其中USART1
页面中间USART1 Mode and Configuration中将串口模式设置为异步通信工作模式,无硬件流控制
然后在Configuration页面中设置USART1的相关参数,主要有波特率、字长、奇偶校验位、停止位、数据方向和过采样率6个参数,一般采用默认即可,但要确保接收端设置与发送端一致
其他5个串口在异步通信模式下与USART1一致,唯一区别在于RX/TX引脚不同,具体参数解释可以阅读本实验“3.0、前提知识”小节
具体设置如下图所示
3.1.3、外设中断配置
在页面左边功能分类栏目中单击System Core/NVIC,勾选USART1全局中断,并设置合适的中断优先级
如果在串口中断中会使用到HAL库的延时函数,注意不要与滴答定时器优先级一致 (注释2)
具体设置如下图所示
3.2、生成代码
3.2.0、配置Project Manager页面
单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示
详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节
3.2.1、外设初始化函数调用流程
在工程代码主函数main()中调用MX_USART1_UART_Init()函数对串口1相关参数进行了配置
在该MX_USART1_UART_Init()函数中调用了HAL_UART_Init()函数对串口1进行了初始化
在该初始化HAL_UART_Init()函数中又调用了HAL_UART_MspInit()函数对串口1时钟,中断,引脚复用做了相关配置
如下图所示为具体的USART1初始化调用流程
此时我们就可以让串口工作在阻塞模式下,通过如下所示的两个函数阻塞式的发送或接收数据
/*串口阻塞发送数据*/
HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
/*串口阻塞接收数据*/
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
3.2.2、外设中断函数调用流程
勾选USART1全局中断后,在工程文件stm32f4xx_it.c中生成了USART1全局中断服务函数USART1_IRQHandler()
该函数调用了HAL库的串口统一中断处理函数HAL_UART_IRQHandler(),在该函数中通过一系列的判断,最终根据不同的串口事件调用不同的回调函数
当串口以中断方式发送完成数据时会调用串口完成中断传输回调函数HAL_UART_TxCpltCallback()
当串口以中断方式接收完成数据时会调用串口中断接收完毕回调函数HAL_UART_RxCpltCallback()
如下图所示为具体的USART1串口Tx传输完成中断调用流程
同理,感兴趣的可以自己找一找中断接收完毕回调函数HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)的调用流程
读者只需记住经过上述3.1.2和3.1.3所做的配置生成的代码,重新实现以下两个函数即可
-
HAL_UART_TxCpltCallback():串口中断发送完毕回调函数,使用HAL_UART_Transmit_IT函数传输数据完毕之后就会进入该函数
-
HAL_UART_RxCpltCallback():串口中断接收完毕回调函数,使用HAL_UART_Receive_IT接收数据时,一旦数据接收完毕之后就会进入该函数
3.2.3、添加其他必要代码
需要提到一点是,使用中断的方式接收指定长度数据时,一旦接收一次完毕,第二次不会自动启动接收,此时需要用户手动调用以中断方式接收串口数据的函数HAL_UART_Receive_IT。而一个串口往往有三种状态,要么在发送数据,要么在接收数据,要么在偷懒处于空闲状态,因此在空闲状态时重新启动中断串口接收是比较正确的选择,这里就需要我们自己设置一个串口的空闲中断回调函数on_UART_IDLE,当接受完一次数据后,将空闲中断使能,在空闲的时候进入空闲中断回调函数,处理刚刚接收到的数据并重新启动串口中断接收
接下来我们来实现串口的空闲中断回调函数,将其放在串口1的中断服务函数中,这样串口1的任何中断都会调用该函数,然后在usart.c中实现该函数,在该函数中首先判断是否是空闲中断,如果不判断则任何关于串口1的中断都会执行空闲中断回调函数函数体内容,然后清除空闲中断标志及禁用空闲中断,保证空闲中断回调函数只在串口接收中断完成后才能被触发,接着对串口接收到的数据进行处理,具体处理函数为CMD_PROCESS函数,最后重新启动串口中断接收,具体函数代码如下图所示
串口完成中断传输回调函数和中断接收完毕回调函数重新实现在usart.c中,每次接收完数据都会进入中断接收完毕回调函数,在该回调函数中启动了空闲中断,此时才可以执行空闲中断函数体内的代码,也就是处理命令、重新启动串口中断接收,值得提醒的是在串口完成中断传输回调函数中使用的串口输出是阻塞的方式输出信息的,不可以使用中断的方式输出提示信息,否则将无限套娃,具体代码如下图所示
源代码如下
/*串口结束传输中断*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
printf("Into HAL_UART_TxCpltCallback Function\r\n");
HAL_GPIO_TogglePin(RED_LED_GPIO_Port,RED_LED_Pin);
}
/*串口接收完成中断*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
//接收到固定长度数据后使能UART_IT_IDLE中断,在UART_IT_IDLE中断里再次接收
//接收完成标志
rxCompleted=SET;
//复制接收到的数据到缓冲区
for(uint16_t i=0;i<RX_CMD_LEN;i++)
proBuffer[i] = rxBuffer[i];
//接收到数据后才开启IDLE中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
}
}
/*串口空闲回调函数*/
void on_UART_IDLE(UART_HandleTypeDef *huart)
{
//判断IDLE中断是否被开启
if(__HAL_UART_GET_IT_SOURCE(huart, UART_IT_IDLE) == RESET)
return;
//清除IDLE标志
__HAL_UART_CLEAR_IDLEFLAG(huart);
//禁止IDLE中断
__HAL_UART_DISABLE_IT(huart, UART_IT_IDLE);
//接收完成
if(rxCompleted)
{
//上传接收到的指令
printf("Receive CMD is %s\r\n",proBuffer);
//处理指令
CMD_PROCESS();
//再次接收
rxCompleted = RESET;
//再次启动串口接收
HAL_UART_Receive_IT(huart, rxBuffer, RX_CMD_LEN);
}
}
/*接收命令处理函数*/
void CMD_PROCESS(void)
{
//非法的命令格式
if(proBuffer[0] != '#' && proBuffer[2] != ';')
{
printf("Unlawful Orders\r\n");
return;
}
//解析命令
uint8_t CMD = proBuffer[1]-0x30;
//控制GREEN_LED
if(CMD == 1)
{
HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_RESET);
printf("GREEN_LED ON\r\n");
}
else if(CMD == 0)
{
HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_SET);
printf("GREEN_LED OFF\r\n");
}
}
最后我们在主函数中以中断方式启动串口接收,然后编写WK_UP按键响应函数,每按下一次按键以中断方式发送一次数据,具体的代码如下图所示
上述代码中的一些变量均定义/声明在了usart.c/usart.h中,具体源代码如下
/*usart.c中定义的变量*/
uint8_t rxBuffer[3]="#0;"; //数据接收缓冲区
uint8_t proBuffer[3]="#1;"; //数据处理缓冲区
uint8_t rxCompleted=RESET; //数据接收完成标志
/*usart.h中声明的变量*/
#define RX_CMD_LEN 3 //数据接收长度
extern uint8_t rxBuffer[]; //外部声明
void on_UART_IDLE(UART_HandleTypeDef *huart); //函数声明
void CMD_PROCESS(void); //函数声明
/*main()函数按键WK_UP控制代码*/
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
HAL_UART_Transmit_IT(&huart1, (uint8_t *)"Key WK_UP Pressed!\r\n", 20);
while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
}
}
4、常用函数
/*串口阻塞接收数据*/
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
/*串口阻塞发送数据*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
/*串口中断接收数据*/
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
/*串口中断发送数据*/
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
/*串口中断接收数据完毕回调函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
/*串口中断发送数据完毕回调函数*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
5、烧录验证
烧录程序,开发板上电,此时按键WK_UP被按下,串口会同时输出信息,输出完毕后进入串口结束传输中断回调函数,输出提示信息并将RED_LED状态翻转
PC发送 "#1;" 给MCU,串口输出接收到的信息,然后解析命令,打开GREEN_LED
PC发送 "#0;" 给MCU,串口输出接收到的信息,然后解析命令,熄灭GREEN_LED
按键WK_UP又被按下,串口输出信息,输出完毕后进入串口结束传输中断回调函数,输出提示信息并将RED_LED状态翻转 (注释3),如下图所示为串口的详细输出信息
6、串口printf重定向
用户阻塞式的发送一条数据时使用的HAL_UART_Transmit函数需要指定发送数据的字节数,非常的不方便,因此简单使用串口传输数据时有必要将其重定向到我们熟悉的printf函数,以下为具体步骤
首先需要在工程设置页面勾选“Use MicroLIB”,如下图所示
然后在工程main.c文件中加入printf函数所需的头文件“#include <stdio.h> ”,并在主函数上方添加重定向函数,如下图所示,红框中的串口实例可以替换成任何正常的串口实例
源代码如下
#include "stdio.h"
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
之后在工程任何文件处均可使用printf函数用作串口函数阻塞输出从而替代HAL_UART_Transmit函数(在其它文件使用记得添加头文件#include <stdio.h>)
7、注释详解
注释1:如果你觉得自己的一切配置都没有问题,但是串口就是没有任何字符输出,可以用串口模块尝试开发板上其他的串口引脚,因为有时候开发板的某一个串口引脚可能被其他外设使用,物理上造成了冲突,无法用软件解决,比如笔者之前使用的STM32F407G-DISC1开发板其USART1就不能正常使用
注释2:如果设置串口中断优先级与系统滴答定时器优先级一致,那么在串口中断服务函数中使用HAL库的延时函数HAL_Delay的话,系统滴答定时器不能抢占串口中断,因此会出现程序卡死在HAL_Delay函数的情况
注释3:注意笔者此实验只是简单介绍每个功能的使用方法,这里的代码其实是有BUG的,如果用户不按照"#1;"/"#0;"的命令格式发送数据,而是只发送1个字符,比如"q",然后再按照"#1;"/"#0;"的命令格式发送数据,那么程序接收到的命令将错乱,导致不能正常解析命令
参考资料
补充:STM32 USART 接收任意长度数据
思路:
USART 以中断方式每次只接收1个字节数据然后保存到缓冲区中,当没有数据可接收的时候进入空闲中断中置位接收完毕标志位,表示接收到一帧数据,然后在其他地方处理接收到的这一帧数据
/*main.c*/
//接收缓冲区
unsigned char RxBuffer1[100];
//每次接收的一个字节数据
unsigned char RxByte1;
//记录收到的这帧数据长度
unsigned int RxCount1;
//这一阵数据接收完毕标志位
unsigned char RxFlag1;
/*main()*/
//以中断方式接收一字节数据
HAL_UART_Receive_IT(&huart1,&RxByte1,1);
//同时使能串口空闲中断
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
while(1)
{
//一帧数据接收完毕了
if(RxFlag1 == 1)
{
printf("Receive:%s\r\n", RxBuffer1);
//清空接收缓冲区
memset(RxBuffer1,0,sizeof(RxBuffer1));
RxFlag1 = 0;
RxCount1 = 0;
}
}
/*串口接收中断服务函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断是由哪个串口触发的中断
if(huart->Instance == USART1)
{
//将接收到的数据放进RxBuffer1缓冲区中
RxBuffer1[RxCount1] = RxByte1;
//RxBuffer1下标累加
RxCount1++;
//处理完毕还需要重新打开接收中断,否则不会接收下一个数据
HAL_UART_Receive_IT(&huart1,&RxByte1,1);
}
}
/*USART1中断统一处理函数*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/*
判断是空闲中断标志位,要注意的是这个语句不要写到HAL_UART_RxCpltCallback函数中,
因为HAL_UART_RxCpltCallback只是接收回调函数,不是接收的情况下是不会调用的,如果
写到HAL_UART_RxCpltCallback中永远也处理不了空闲中断
*/
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
//清除空闲中断标志
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
//接收标志置1,表示收到了一帧数据
RxFlag1 = 1;
}
/* USER CODE END USART1_IRQn 1 */
}/*main.c*/
//接收缓冲区
unsigned char RxBuffer1[100];
//每次接收的一个字节数据
unsigned char RxByte1;
//记录收到的这帧数据长度
unsigned int RxCount1;
//这一阵数据接收完毕标志位
unsigned char RxFlag1;
/*main()*/
//以中断方式接收一字节数据
HAL_UART_Receive_IT(&huart1,&RxByte1,1);
//同时使能串口空闲中断
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
while(1)
{
//一帧数据接收完毕了
if(RxFlag1 == 1)
{
printf("Receive:%s\r\n", RxBuffer1);
//清空接收缓冲区
memset(RxBuffer1,0,sizeof(RxBuffer1));
RxFlag1 = 0;
RxCount1 = 0;
}
}
/*串口接收中断服务函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断是由哪个串口触发的中断
if(huart->Instance == USART1)
{
//将接收到的数据放进RxBuffer1缓冲区中
RxBuffer1[RxCount1] = RxByte1;
//RxBuffer1下标累加
RxCount1++;
//处理完毕还需要重新打开接收中断,否则不会接收下一个数据
HAL_UART_Receive_IT(&huart1,&RxByte1,1);
}
}
/*USART1中断统一处理函数*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/*
判断是空闲中断标志位,要注意的是这个语句不要写到HAL_UART_RxCpltCallback函数中,
因为HAL_UART_RxCpltCallback只是接收回调函数,不是接收的情况下是不会调用的,如果
写到HAL_UART_RxCpltCallback中永远也处理不了空闲中断
*/
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
//清除空闲中断标志
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
//接收标志置1,表示收到了一帧数据
RxFlag1 = 1;
}
/* USER CODE END USART1_IRQn 1 */
}
参考 STM32 HAL库多串口任意长度接收的方法(无起始和结束标志,不使用DMA)