STM32
直播中

孔朱磊

7年用户 911经验值
私信 关注
[问答]

基于CubeMX HAL库的STM32串口发送接收配置过程分享

基于CubeMX HAL库的STM32串口发送接收配置过程分享

回帖(1)

卞轮辉

2021-12-10 14:39:19
/***********************************************
  描述:基于CubeMX+HAL库的STM32串口发送、接收配置大全,详细内容可查看下方目录。
  功能:各种常用的配置大全,可以方便的挑选合适的配置快速开发
  平台:STM32F723-DISCO,除F7特有的自适应波特率外,其余配置对各种含有串口的STM32单片机均适用
  作者:Miss_若星
  时间:2019/10/20
  工程:博客不包含代码工程,所作更改都是基于前面的步骤
  说明:因作者水平有限,不能保证100%的正确性,以下内容仅供大家参考使用,有不当之处还请指出。
  **************************************************/
   目录
  一、串口发送
  1.1、普通发送模式
  1.1.1、模式配置:
  1.1.2、中断配置:
  1.1.3、生成代码,打开工程,在主函数的循环前面添加测试语句,输出字符串:
  1.1.4、编译下载,打开串口调试助手,复位:
  1.2、使用自定义printf函数
  1.2.1、编写函数用于发送字符串
  1.2.2、在主函数中调用:
  1.2.3、测试结果:
  1.3、使用标准的printf函数
  1.3.1、在usart.h文件中包含头文件:
  1.3.2、在用户代码区添加输出重定向代码:
  1.3.3、主函数中调用:
  1.3.4、测试结果同样是可以使用
  1.4、半主机模式与C库
  1.4.1、半主机模式
  1.4.2、微库
  1.4.3、标准库,禁用半主机模式,添加重定向
  1.4.4、另外的调试方法
  1.5、DMA发送模式
  1.5.1、配置串口发送DMA
  1.5.2、在NVIC菜单下配置他的中断优先级为5和6
  1.5.3、生成代码,打开工程,在主函数中添加代码:
  1.5.4、编译下载,复位:
  1.6、DMA方式使用printf函数
  1.6.1、在usart.c文件中包含头文件:
  1.6.2、在用户代码区定义一个新的函数DMA_printf:
  1.6.3、在头文件中声明函数,在主函数中调用。
  1.6.4、下载验证:
  二、串口接收
  2.1、轮询接收模式
  2.1.1、在CubeMX中配置
  2.1.2、生成代码
  2.2、中断接收模式
  2.2.1、打开串口的全局中断:
  2.2.2、在NVIC选项里修改它的优先级:
  2.2.3、生成代码
  2.2.4、编译下载
  2.2.5、收发速度测试
  2.3、DMA接收模式
  2.3.1、配置CubeMX为DMA接收,方式选择Normal
  2.3.2、设置DMA接收中断优先级为4,打开串口接收全局中断:
  2.3.3、中断回调函数配置如下所示:
  2.3.4、测试10个数据时没有数据丢失现象:
  2.3.5、修改中断回调函数只保存,不发送
  2.3.6、可以发现最终实现的效果还是很理想的
  2.3.7、修改DMA为连续工作方式
  2.3.8、则接收回调函数可以写成如下内容,不需要每次都重新打开DMA。
  2.3.9、测试文件传输结果如下,没有数据丢失现象:
  三、自动波特率
  3.1.1、在CubeMX上配置为自动波特率
  3.1.2、在主函数中添加一句显示当前波特率的程序:
  3.1.3、编译下载
  3.1.4、使用串口助手测试
  3.1.5、按下按键可以看到数据成功返回:
  3.1.6、同样的方法,使用其他波特率也同样适用。
   
   
  
  
  
  一、串口发送

  1.1、普通发送模式

  1.1.1、模式配置:

  
  

  

  
  

  • 数据长度:为包含了奇偶校验位的数据长度,上图设置为8,奇偶校验位为0.所以数据位宽就是8.
  • 过采样:见下图
  • 单个采样点:见下图


  

  

  
  

  • Overrun数据溢出检测:见下图:


  

  

  
  

  • 接收错误时禁止DMA:详细解释参考下图:


  

  

  
  1.1.2、中断配置:

  
  

  

  
  1.1.3、生成代码,打开工程,在主函数的循环前面添加测试语句,输出字符串:

  
  HAL_UART_Transmit( &huart6 , (uint8_t *)"hello DISCOrn" , sizeof("hello DISCOrn"), 0xFFFF);  1.1.4、编译下载,打开串口调试助手,复位:

  
  

  

  1.2、使用自定义printf函数

  1.2.1、编写函数用于发送字符串

  
  /*  used for a string send by usart */ void My_String_Printf( uint8_t* String ) {  while( *String != '' ){   HAL_UART_Transmit( &huart6 , (uint8_t *)(String++), 1, 0xFFFF);  } }  1.2.2、在主函数中调用:

  
  My_String_Printf("hello DISCOrn");  1.2.3、测试结果:

  
  

  

  
  
  1.3、使用标准的printf函数

  1.3.1、在usart.h文件中包含头文件:

  
  #include "stdio.h"  1.3.2、在用户代码区添加输出重定向代码:

  

#ifdef __GNUC__
  /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
     set to 'Yes') calls __io_putchar() */
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
        HAL_UART_Transmit( &huart6 , (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}
1.3.3、主函数中调用:

  
  printf("hello DISCOrn");  1.3.4、测试结果同样是可以使用

  
  

  

  1.4、半主机模式与C库

  在嵌入式系统中,通过串口打印log是非常重要的调试手段,但是直接调用底层驱动打印信息非常不方便,在c语言中一般使用printf打印基本的显示信息,而默认printf的结果不会通过串口发送,解决方法主要有两种:
  1.4.1、半主机模式

  半主机是用于 ARM 目标的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机。 例如,使用此机制可以启用 C 库中的函数,如 printf() 和 scanf(),来使用主机的屏幕和键盘,而不是在目标系统上配备屏幕和键盘。
  简单的来说,半主机模式就是通过仿真器实现开发板在电脑上的输入和输出。和半主机模式功能相同的是ITM调试机制。ITM是ARM在推出semihosting之后推出的新一代调试机制。这两种机制的运行均需要仿真器,否则无法运行。
  1.4.2、微库

  使用微库的话,不会使用半主机模式.
  microlib 是缺省 C 库的备选库。 它用于必须在极少量内存环境下运行的深层嵌入式应用程序。 这些应用程序不在操作系统中运行。
  1.4.3、标准库,禁用半主机模式,添加重定向

  

#pragma import(__use_no_semihosting)            //不使用半主机模式
         
//标准库需要的支持函数                 
struct __FILE
{
        int handle;
};

FILE __stdout;      
//定义_sys_exit()以避免使用半主机模式   
_sys_exit(int x)
{
        x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{        
        USART_SendData(USART1,ch);
        while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);  
        return ch;
}   
    1.4.4、另外的调试方法

  


  • Jlink自带的RTT:速度更快,也不依赖编译工具,只要有JLink即可。
  • JScope:图形化显示数据调试。


  1.5、DMA发送模式

  1.5.1、配置串口发送DMA

  
  

  

  模式选择:
  

  • Normal正常模式,DMA发送一次就停止发送;
  • Circular循环模式,会一直发送数据;

FIFO:
  

  • DMA每个数据流都有一个独立的4字FIFO,阈值级别可由软件配置为1/4、1/2、3/4或满。可以用于字、半字、字节之间的转换。如下图所示:


  

  

  

  • 打开DMA通道后,会自动开启DMA的一个中断,此处应该将串口全局中断也打开:


  

  

  1.5.2、在NVIC菜单下配置他的中断优先级为5和6

  
  

  

  注:我使用的优先级分组为4:
  
  

  

  1.5.3、生成代码,打开工程,在主函数中添加代码:

  
  HAL_UART_Transmit_DMA( &huart6, (uint8_t *)"hello DISCO by DMArn", sizeof("hello DISCO by DMArn") );  1.5.4、编译下载,复位:

  
  

  

  但是如果不打开串口中断的话,程序只能发送一次数据,之后便无法将串口数据发送出来,调试看到husart中的gState位没有被复位,一直不是READY状态:
  
  

  

  
1.6、DMA方式使用printf函数
在嵌入式系统中,printf是一个相对比较占资源的函数,调试过程中如果过多地使用printf甚至会影响程序本来的结果。


stm32具有DMA,可以把程序运行过程中的调试信息通过DMA的方式打印出来,无疑提高了程序运行的效率。要通过DMA方式把串口的内容格式化打印出来,重定向是不可能的了,唯一办法是自己实现一个printf函数。




1.6.1、在usart.c文件中包含头文件:


#include "stdarg.h"
1.6.2、在用户代码区定义一个新的函数DMA_printf:
其中使用了C语言的标准库函数 ,参考资料:


https://www.runoob.com/cprogramming/c-standard-library-stdarg-h.html




uint8_t DMA_PRINTF_BUFF[100];
void DMA_printf(const char *format, ...)
{
        uint32_t length;
    va_list args;
               
    va_start(args, format);
    length = vsnprintf((char *)DMA_PRINTF_BUFF, sizeof(DMA_PRINTF_BUFF), (char *)format, args);
    va_end(args);

    HAL_UART_Transmit_DMA(&huart6, (uint8_t *)DMA_PRINTF_BUFF, length);
}
1.6.3、在头文件中声明函数,在主函数中调用。
使用方法和标准库的printf函数一样。




void DMA_printf(const char *format, ...);

DMA_printf("hello DISCO by DMArn");


   1.6.4、下载验证:

  
  

  

  
  二、串口接收

  2.1、轮询接收模式

  2.1.1、在CubeMX中配置

  
  

  

  2.1.2、生成代码

  在主函数的主循环中添加测试代码,最后一个输入参数1为超时时间,单位为ms,表示1
  ms内如果没有收到数据,函数就退出,返回超时。
  
  if( HAL_OK == HAL_UART_Receive( &huart6, &pData, 1, 1) )         DMA_printf("Receive Data:%crn",pData);  这种轮询的方法非常占用CPU,一般不使用。
  
  2.2、中断接收模式

  2.2.1、打开串口的全局中断:

  
  

  

  2.2.2、在NVIC选项里修改它的优先级:

  
  

  

  2.2.3、生成代码

  在usart.c中添加接收完成回调函数:
  

uint8_t pData = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                DMA_printf("Receive Data:%crn",pData);
                HAL_UART_Receive_IT( &huart6, &pData, 1);
        }
}
文件中声明外部变量:
  
  extern uint8_t pData;  主函数开头设置初始化:
  
  HAL_UART_Receive_IT( &huart6, &pData, 1);  2.2.4、编译下载

  
  

  

  2.2.5、收发速度测试

  


  • 将接收回调函数配置成收到数据立刻发送的状态:



void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                HAL_UART_Transmit( &huart6, &pData, 1, 20);
                HAL_UART_Receive_IT( &huart6, &pData, 1);
        }
}



  • 设置串口助手一次发送10个字节,可以看到串口返回来的数据也是10个字节,没有数据丢失,设置自动发送时间200ms,数据同样没有丢失。


  

  

  

  • 当一次发送180个字节的时候,就会出现数据丢失的现象:


  

  

  

  • 考虑出现此问题的原因可能是因为接收速度不够,或者是因为串口发送数据的速度不够,所以修改代码如下,测试是否是因为串口发送数据太慢限制了收发性能,设置接收回调函数中只对数据进行保存,不发送,在主函数中判断按键是否按下,按下了就将收到数据发送出去。



void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                UART6_RXD_BUFF[UART6_RXD_COUNT] = pData;
                UART6_RXD_COUNT++;
                HAL_UART_Receive_IT( &huart6, &pData, 1);
        }
}



  • 同时在主循环中添加如下的扫描按键的程序



if( BSP_PB_GetState( BUTTON_USER ) == GPIO_PIN_SET )
{
        HAL_Delay(500);
        BSP_LED_Toggle(LED_RED);
        for(i=0;i         {
                HAL_UART_Transmit( &huart6 , (uint8_t *)&UART6_RXD_BUFF , 1, 0xFFFF);
                UART6_RXD_BUFF = UART6_RXD_BUFF;
        }
        UART6_RXD_COUNT=0;
}


  • 测试,可以看到,数据收发的数量是一致的,说明使用中断方式接收串口数据的速度是挺快的,只要在回调函数中不运行过多的代码,就可以保证数据完整传输。


  

  

  

  • 附带测试一个文件传输,以此程序的main.c为例,将其发送到单片机。可以看到收发数据是一致的。


  

  

  
  
  2.3、DMA接收模式

  2.3.1、配置CubeMX为DMA接收,方式选择Normal

  (第2.3.7步为循环模式):
  
  

  

  2.3.2、设置DMA接收中断优先级为4,打开串口接收全局中断:

  
  

  

  2.3.3、中断回调函数配置如下所示:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                HAL_UART_Transmit( &huart6 , (uint8_t *)&pData , 1, 0xFFFF);
                HAL_UART_Receive_DMA( &huart6, &pData, 1);
        }
}
2.3.4、测试10个数据时没有数据丢失现象:

  
  

  

  但一次发送50个数据时,便出现了严重的数据丢失现象:
  
  

  

  2.3.5、修改中断回调函数只保存,不发送

  

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                UART6_RXD_BUFF[UART6_RXD_COUNT] = pData;
                UART6_RXD_COUNT++;
                //HAL_UART_Transmit( &huart6 , (uint8_t *)&pData , 1, 0xFFFF);
                HAL_UART_Receive_DMA( &huart6, &pData, 1);
        }
}
2.3.6、可以发现最终实现的效果还是很理想的

  
  

  

  接收文件数据也是可行的:
  
  

  

  2.3.7、修改DMA为连续工作方式

  
  

  

  2.3.8、则接收回调函数可以写成如下内容,不需要每次都重新打开DMA。

  
  

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if( huart == &huart6 )
        {
                UART6_RXD_BUFF[UART6_RXD_COUNT] = TEMP_BUFF;
                UART6_RXD_COUNT++;
        }
}
2.3.9、测试文件传输结果如下,没有数据丢失现象:

  
  

  

  
  三、自动波特率

  3.1.1、在CubeMX上配置为自动波特率

  使用模式三,0x55帧确定当前通信波特率。
  
  

  

  生成代码后发现初始化函数中的波特率有一个预设值为:115200
  
  

  

  3.1.2、在主函数中添加一句显示当前波特率的程序:

  
  GUI_DispDecAt ( 108000000/(huart6.Instance->BRR)  , 0, 20, 7);  因为USART6是挂载在APB2总线上的,所以它的工作时钟为108MHz
  
  

  

  从官方的参考手册中可以得知:USARTDIV 是一个存放在 USARTx_BRR 寄存器中的无符号定点数。即为分频值,按照给出的公式,可以计算出当前的波特率。
  
  

  

  3.1.3、编译下载

  可以看到屏幕上显示115138,十分接近115200的默认波特率。
  
  

  

  3.1.4、使用串口助手测试

  在波特率为256000的情况下,发送一串以字母U为开头的字符串,可以看到显示屏上的数字变成了257142。
  
  

  

  3.1.5、按下按键可以看到数据成功返回:

  
  

  

  3.1.6、同样的方法,使用其他波特率也同样适用。

  缺点:配置成模式3,在每次上电之后都需要让对方先发送一个0x55的字符,才可以确定波特率,之后一直保持此波特率不变,否则串口不会正常工作。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分