STM32 HAL库串口使用笔记
Posted 三明治开发社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32 HAL库串口使用笔记相关的知识,希望对你有一定的参考价值。
简介:本文通过介绍串口收发工程以及串口相关函数,来让大家了解STM32 HAL库串口的基础使用方法。
一.串口收发工程讲解
1.使用STM32CubeMX新建一个工程
2.如下图选择自己现有的STM32开发板进行开发:
3.配置时钟源
如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC;
如果使用默认内部时钟(HSI),这一步可以略过;
下面工程我使用的是内部时钟,因此这一步是不需要的。
4.配置串口(配置自己需要的串口)
配置串口2作为调试打印日志口。
USART2配置如下:
配置串口1作为和其它设备通信的接口。
USART1配置如下:(串口1配置为全局中断)
5.配置时钟树
STM32L4的最高主频到80M,所以配置PLL,最后HCLK=80Mhz即可:
6.生产工程设置
7.代码生成设置
最后设置生成独立的初始化文件:
8.生成代码
点击GENERATE CODE即可生成MDK-V5工程如下:
9.用KEIL5打开生成的代码,然后开始添加自己要实现的功能函数。
二.串口相关函数介绍
1.USART初始化
串口的数据类型定义:
(1)初始化结构体
- BaudRate:波特率设置。一般设置为2400、9600、19200、115200。HAL 库函数会根据设定值计算得到UARTDIV 值,见公式。
- WordLength:数据帧字长,可选8 位或9 位。如果没有使能奇偶校验控制,一般使用8位;如果使能了奇偶校验则一般设置为9位。
- StopBits:停止位设置,可选0.5 个、1 个、1.5 个和2 个停止位,一般我们选择1 个停止位。
- Parity : 奇偶校验控制选择, 可选UART_PARITY_NONE ( 无校验) 、UART_PARITY_EVEN (偶校验)以及UART_PARITY_ODD (奇校验)。
- Mode:UART 模式选择,有UART_MODE_RX 、UART_MODE_TX 和UART_MODE_TX_RX。
- HwFlowCtl:硬件流控可以控制数据传输的进程,防止数据丢失,该功能主要在收发双方传输速度不匹配的时候使用。一般不使能。
- OverSampling:设置8倍过采样是否使能
串口初始化过程:
2.轮询方式串口通信
(1)接口函数
代码如下(示例):
Transmit函数:该函数连续发送数据,发送过程中通过判断TXE标志来发送下一个数据,通过判断TC标志来结束数据的发送;如果在等待时间内没有完成发送,则不再发送,返回超时标志 Receive函数:该函数连续接收数据,接收过程中通过判断RXNE标志来接收新的数据;如果在超时时间内没有完成接收,则不再接收数据,返回超时标志
3.固定长度收发示例
从PC上发送固定数量字符到开发板,开发板收到后原样发回PC,通过串口助手观测发送。
(1) 使用MX完成串口外设初始化配置 (如上“使用STM32CubeMX新建一个工程”方法进行操作,参考串口2的配置)
-
选择异步模式,无硬件流控
-
在参数设置中,设置初始化结构体中的通信参数:使用115200-8-n-1模式,即波特率设置为115200,采用8位数据位,无校验,1位停止位; 使能接收和发 送,采用16倍过采样。
-
在GPIO选项卡中,可使用默认的引脚,也可选择其他引脚。
-
配置完成后生成工程代码
打开工程代码编译的时候可能会报如下所示的错误,这是因为没有加载启动文件导致的。
在生成的工程代码目录下找到如下所示的文件夹,找到“startup_stm32l476xx.s”这个文件,添加到KEIL工程中即可。
(2)使用MDK完成用户代码编写
(3)使用串口助手调试
串口参数设置与开发板一致
发送任意2个字符,观察串口助手的接收区和发送区
4.串口重定向示列
利用串口实现printf函数向串口发送数据,scanf从串口读取数据。
-
在C语言中,printf函数是将数据格式化输出到屏幕,scanf函数是从键盘格式化输入数据;
-
在嵌入式系统中,一般采用串口进行数据的输入和输出;
-
重定向是指用户改写 C语言的库函数,当链接器检查到用户编写了与 C库函数同名的函数时,将优先使用户编写的函数,从而实现对库函数的修改;
-
printf函数内部通过调用 fputc 函数来实现数据输出,scanf函数内部通过调用 fgetc 函数来实现数据输入,因此用户需要改写这两个函数实现串口重定向。
-
这边简单介绍一下,printf函数定义,scanf函数用户可以参考设计一下。
****串口轮询方式通信demo下载链接
5.中断方式串口通信
中断方式不必等待数据的传输过程,只需要在每字节数据收发完成后,由中断标志位触发中断,在中断服务程序中放入新的一字节数据或者读取接收到的一字节数据。在传输数据量较大,且通信波特率较高(大于38400)时,如果采用中断方式,每收发一个字节的数据,CPU都会被打断,造成CPU无法处理其他事务。因此在批量数据传输,通信波特率较高时,可采用DMA方式。
串口中断处理过程
1.接口函数
-
发送过程:每发送一个数据进入一次中断,在中断中根据发送数据的个数来判断数据是否发送完成
-
接收过程:每接收一个数据进入一次中断,在中断中根据接收数据的个数来判断数据是否接收完成
代码如下(示例):
- Transmit函数:函数开始将使能串口发送中断,完成指定数量的数据发送后,将会关闭发送中断;因此用户采用中断方式连续发送数据时,需要重复调用此函数,以便重新开启发送中断;当指定数量的数据发送完成后,将调用发送中断回调函数HAL_UART_TxCpltCallback进行后续处理,如连续发送,可在最后再次调用发送函数
- Receive函数:函数开始将使能串口接收中断,完成指定数量的数据接收后,将会关闭接收中断;因此用户采用中断方式连续接收数据时,需要重复调用此函数以便重新开启接收中断;当指定数量的数据接收完成后,将调用接收中断回调函数HAL_UART_RxCpltCallback进行后续处理,如连续接收,可在最后再次调用接收函数
代码如下(示例):
-
串口通用句柄函数:函数内部首先判断中断类型,并清除对应的中断标志,最后调用回调函数完成对应的中断处理
-
回调函数:函数内部根据串口句柄实例判断是哪一个串口产生的中断,由用户根据具体的处理任务进行代码实现
-
宏函数:根据名字很清晰的就能看出功能,使能中断,查询标志,清除标志
2.固定长度收发示例
采用前后台编程模式。前台程序为中断服务程序,一旦数据接收完成,则设置一个标志位为1;后台程序为while(1)的死循环,在循环中不断检测标志位是否为1。如果为1,表明数据接收完成,并存放在接收缓冲区中。然后进行后续处理:先清除标志位,再把接收的数据原样发回。
(1) 使用MX完成串口外设初始化配置 (如上“使用STM32CubeMX新建一个工程”方法进行操作,参考串口1配置)
-
选择异步模式,无硬件流控
-
在参数设置中,设置初始化结构体中的通信参数:使用115200-8-n-1模式,即波特率设置为115200,采用8位数据位,无校验,1位停止位;使能接收和发送,采用16倍过采样
-
在GPIO选项卡中,可使用默认的引脚,也可选择其他引脚
-
使能使用的串口的全局中断,中断优先级使用默认值
-
配置完成后生成工程代码
打开工程代码编译的时候可能会报错误,这是因为没有加载启动文件导致的。如上进行加载一下启动文件即可。
(2)使用MDK完成用户代码编写
(3)使用串口助手调试
串口参数设置与开发板一致
发送任意2个字符,观察串口助手的接收区和发送区
(4)同时对串口1和串口2进行配置
(1)main.c中,调用了串口初始化
(2)usart.c中,进行了串口1和串口2进行了详细参数配置。(串口初始化函数赋值了串口的参数)
(3)HAL_UART_MspInit函数相当于底层的初始化,配置引脚、并开启中断。
(4)这里要注意的是在函数初始化函数中或者主程序中要开启中断。
(5)串口通信demo下载链接
采用STM32CubeMX编程环境开发的demo,可以直接生成自己所用编程环境(如KEIL或者IAR等)的代码,在此demo基础上实现自己的功能代码。
(5)库文件stm32f1xx_hal_uart.c内到底做了哪些事情
(1)初始化
-
usart.c中,MX_USART1_UART_Init 调用了库的HAL_UART_Init,将结构体传递进该函数中
-
HAL_UART_Init做了哪些事情
调用MspInit-->修改状态忙-->配置寄存器-->清楚标志位
(2) 先理解HAL_UART_Receive函数
判断是否忙-->锁住-->标记接收忙-->获取tick计数
-->*赋值RxXferCount有多少数据要接收-->每次从RDR内获取一个Byte存在pData指向的空间*
(3) HAL_UART_Receive_IT只是配置了一下参数,并没有做任何处理
存储在pData指向位置、空间大小RxXferSize 、接收计数RxXferCount ; 接收状态忙;使能接收中断
那么当有数据来的时候,就需要依靠中断函数来处理了。
(4)再看看中断函数在做什么
stm32L4xx_it.c内有定义USART1_IRQHandler,只调用了HAL_UART_IRQHandler函数,下面是
HAL_UART_IRQHandler具体内容
-
做了三件事,首先判断是由什么中断响应的,其次有错误则处理,最后响应要调用的接收或者发送
-
注意区别 UART_Receive_IT 和 HAL_UART_Receive_IT,HAL_UART_Receive_IT是用户调用的需要接收多少数据存在何处
-
UART_RxISR_8BIT和UART_RxISR_16BIT是中断调用的有数据收到该如何处理
(5) UART_RxISR_8BIT和UART_RxISR_16BIT是真正在接收数据的函数,但在最后会关闭中断
-
如果是接收状态忙,则从RDR中读取1Byte数据。
-
如果接收计数归零,则使中断失效,并调用回调函数(用户定义则调用用户的,否则调用系统的)
(6)小结
-
HAL_UART_Receive_IT和HAL_UART_Receive的区别就是:中断接收是有数据到了才去读;直接接收是直接读取,如果超时就返回
-
HAL_UART_Receive_IT配置后,有数据来,计数会在调用中断函数之后自动减1。只有到计数为0时,才会关闭中断并调用回调函数。至此有数据来不再调用中断函数,因为中断已经失效。
-
HAL_UART_Receive_IT在计数未至0之前,应该可以读取之前接收到的数据,但这样做应该比较危险。
-
使用 HAL_UART_Receive_IT(&huart1,(uint8_t *)aRxBuffer, 1); 即Size设置为1,只接收1Byte数据,在每次中断结束后重新配置来使能中断。
6.串口使用过程中可能出现的问题点及解决办法
(1)hal库接收中断的分析
ST的hal库,这个库相较于之前的标准库,优缺点兼具,hal库封装了更多底层的细节,我们可以很轻易的实现我们需要的功能,但是由于封装了太多的细节,导致一旦出问题,比较难发现问题,内部调用比较复杂。
上面已经介绍了串口中断的配置了,这里就不在详细叙述了。
使用接收中断的方式是:在主程序串口初始化后面或者串口初始化函数最后面开启接收中断,开启的函数如下:
HAL_UART_Receive_IT(&huart1,(uint8_t*)aRxBuffer, 1);//该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量,
然后直接调用总的回调函数就可以了(这个函数的名称是定义好的):
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)//判断,如果是串口1
{
//在这里写入自己要实现功能的代码
}
}
使用回调函数的原理:MCU接收到数据产生中断,进入中断函数:
//串口中断服务程序
void USART1_IRQHandler(void)
{
u32 timeout=0;
HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数
/*
省略一部分代码
*/
}
中断进入了HAL_UART_IRQHandler(&UART1_Handler)这个函数,这个函数是一个总的中断处理函数,很多串口的中断处理最后都汇集到这个地方来处理。
如下所示:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
/*
省略一部分代码
*/
UART_Receive_IT(huart);
/*
省略一部分代码
*/
}
UART_Receive_IT(huart)这个函数的实现如下所示:
static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
/*
省略一部分代码
*/
HAL_UART_RxCpltCallback(huart);
/*
省略一部分代码
*/
}
最后会调用到 HAL_UART_RxCpltCallback(huart)这个函数,我们在这个函数内部实现自己的功能即可。
(2)效率分析
由上面的分析,我们可以知道,hal库的中断接收虽然很方便,但是是非常没有效率的,绕了一个大弯才能处理中断接收的数据。尤其对于使用的单片机是STM32F0,频率只有48M;STM32G071的芯片,频率只有64M。这样的中断处理显然不能接受。
借鉴于标准库,想直接在串口中断函数(void USART1_IRQHandler(void))上直接处理函数,而不用在转一个圈跑到回调函数
( HAL_UART_RxCpltCallback(huart))上处理。
(3)问题分析
进中断出不来:
判断和清除中断标志忘记清除,加上清除标志即可。
void USART1_IRQHandler(void)
{
/* UART in mode Receiver ---------------------------------------------------*/
if((__HAL_UART_GET_IT(&huart1, UART_IT_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE) != RESET))
{
/*
需要处理的代码
*/
/* Clear RXNE interrupt flag */
__HAL_UART_SEND_REQ(&huart1, UART_RXDATA_FLUSH_REQUEST); //清除接收数据非空中断标志
}
}
进中断MCU死机:
这个问题是由于在中断处理了大量的任务导致的死机。不要在中断干太多的任务,可以在中断完成一些标志,或者处理一下接收的数据(可以转存到别的数据,但记住不要用hal库的函数,可能会卡)然后再回到主函数处理。比如:
void USART1_IRQHandler(void)
{
/* UART in mode Receiver ---------------------------------------------------*/
if((__HAL_UART_GET_IT(&huart1, UART_IT_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE) != RESET))
{
flag=1; //中断标志
RxBuffer = huart1.Instance->RDR; //将接收到的数据保存在全局变量
/* Clear RXNE interrupt flag */
__HAL_UART_SEND_REQ(&huart1, UART_RXDATA_FLUSH_REQUEST); //清除接收数据非空中断标志
}
}
以上是关于STM32 HAL库串口使用笔记的主要内容,如果未能解决你的问题,请参考以下文章
STM32CubeMX-HAL库开发笔记-基于Proteus仿真