STM32CubeMX教程9 USART/UART 异步通信
1、准备材料
开发板(正点原子stm32f407探索者开发板V2.4)
ST-LINK/V2驱动
STM32CubeMX软件(Version 6.10.0)
keil µVision5 IDE(MDK-Arm)
CH340G Windows系统驱动程序(CH341SER.EXE)
XCOM V2.6串口助手
逻辑分析仪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.0V3.3V(通常VDD电源电压的大约70-100%),低电平0的电压范围为0V0.3V;USART通信中一般需要设置波特率、数据字长、校验位和停止位四个参数,如下图所示位串行数据发送时序图
波特率:由于本实验的串口工作在异步通信模式,因此需要规定一个特定的传输速率,这样收发双方都以该速率解析发送的内容,才能不出错的进行通信,常见波特率9600/115200等,当然也可自定义波特率
数据字长:可选8/9位,即一帧数据中传输的数据位数,由于一字节为8位,因此该参数默认为8位
校验位:可选无/奇/偶校验
停止位:可选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的设备管理器页面,端口栏目下会出现对应串口芯片识别成功的端口号,如下图所示
串口通信中数据传输一般可以分为阻塞式数据传输和非阻塞式数据传输两种,而阻塞模式也即轮询模式,在此模式下,串口发送或者接收数据都会产生阻塞,单片机只能一直等待接收/发送完成或者达到设定的超时时间;非阻塞模式是使用中断或者DMA的方式来传输数据,顾名思义,不会产生阻塞现象,发送/接收数据的同时单片机还可以处理其他任务。本文不涉及DMA,因此非阻塞模式仅仅介绍使用中断的传输方式
3.1、CubeMX相关配置
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.1小节配置RCC和SYS
3.1.1、时钟树配置
系统时钟树配置与上一实验一致,均设置为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、生成代码
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节配置Project Manager
单击页面右上角GENERATE CODE生成工程
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(UART_HandleTypeDef huart)、HAL_UART_RxCpltCallback(UART_HandleTypeDef huart)两个函数,第一个函数为串口中断发送完毕回调函数,每次使用HAL_UART_Transmit_IT传输数据传输完之后就会进入该函数;第二个为串口中断接收完毕回调函数,使用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、烧录验证
5.1、具体步骤
“启动USART1异步通信模式 -> 配置串口相关参数 -> 使能USART1全局中断 -> 在usart.c中重新实现①串口结束传输中断回调函数HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)②串口接收完毕中断回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) -> 添加串口空闲回调函数on_UART_IDLE -> 实现命令解析函数CMD_PROCESS -> 添加本实验控制代码(具体代码请看上述3.2小节)”
5.2、实验现象
烧录程序,开发板上电,此时按键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;"的命令格式发送数据,那么程序接收到的命令将错乱,导致不能正常解析命令
参考资料
更多内容请浏览 OSnotes的CSDN博客