嵌入式开发笔记:STM32G431 USART串口通讯学习(基于HAL库)

  系列文章目录

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记01:赛事介绍与硬件平台

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记02:开发环境安装

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记03:G4时钟结构

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记04:从零开始创建工程模板并开始点灯

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记05:Systick滴答定时器

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记06:按键输入

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记07:ADC模数转换

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记08:LCD液晶屏

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记09:EEPROM

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记10:USART串口通讯


目录

  系列文章目录

前言

一、基础知识

二、串口发送程序的设计

1.程序设计步骤

 2.串口发送函数

三、串口发送printf重定向

四、串口接收程序的设计

1.程序设计步骤

2.串口接收函数

3.串口接收固定长度数据

4.串口接收带帧尾的不定长数据

总结


前言

今天讲解一下STM32G4的串口通讯模块USART,即通用同步异步收发器。相比与UART异步收发器,USART是可以同步通讯的,即接收端和发送端共用一个时钟,是有一个时钟线的。但是我们比赛和蓝桥杯板子上只用到了异步收发,所以我们只能把USART设置成Asynchronous(只需要用到两根线,收和发)。

一、基础知识

异步通讯有一个特点就是发送方和接收方共用一个波特率(如果是同步的话就是由时钟线提供时钟,主机和从机不用约定好同一个波特率)。所谓的波特率就是每秒传输的二进制位数,单位是bps(位/秒),比如说9600的波特率,那么每秒就能传输9600位的数据,考虑到每个字节传输的时候会有一个起始位start和一个停止位stop,所以一个字节需要10位的数据,那么一秒钟就能发送960个字节。

还要了解一下异步收发的通讯格式,一般异步收发是不需要clock的,如果是发送方,会有一个起始位的低电平,然后发送八个位的数据,最后是一个停止位的高电平,就是一个完整的字节的数据传输,一共10个bit。(也可以用奇偶校验,如果是奇偶校验,就是9个位,但是我们比赛一般用不到,只用头尾校验就可以了)

同步我们就不需要了解了,因为板子上并没有提供同步通讯所需的时钟线,如图所示只有两根线。

我们也可以顺便看一下串口通讯的电路。可以看到PA9连到RX端,PA10接到TX端。我们也可以看到CubeMX上面,PA9定义成USART1_TX,即串口1的发送引脚,PA10定义成USART1_TX,即串口1的接收引脚。也就是说PA10是接收DAPLink的发送,PA9是stm32的发送,发送给DAPLink的接收端。DAP仿真器接收到串口数据,通过USB接口发送给电脑。

还有一个要注意的地方,在CubeMX里面设置USART1之后,他默认生成的是PC4、PC5的口,但是硬件上并没有连接,所以我们必须把PA9、PA10手动配置一下。

二、串口发送程序的设计

1.程序设计步骤

1.1 “模板”作为CubeMX生成代码的工程。

1.2 配置USART1的PA9、PA10为串口收发管脚。

打开CubeMX,把PA9配置成USART1_TX,把PA10配置成USART1_RX,然后配置USART1的模式为异步通讯Asynchronous。

1.3 根据需求,配置USART的波特率,数据位长度、奇偶校验位、停止位和时钟。

在USART1的Configuration里面配置,其中Basic Parameters里面的设置按顺序分别是:波特率、包含奇偶校验位的字节长度、奇偶校验位、停止位。因为我们不用奇偶校验(none),所以字节长度就是8,停止位为1。

1.4 将usart.c和usart.h移植到“编程工程”。

生成文件后,打开usart.c和usart.h。然后复制到编程工程中。然后把模块添加到工程中,然后在drivers中把库函数也添加进来。(我们只需要将uart的两个添加进来就行了,因为只需要用到异步的功能)

​然后在main.h>>stm32g4xx_hal.h>>stm32g4xx_hal_conf.h里面要启用一下异步收发的模块。然后编译一下,没有问题。

然后在main.c包含#include "usart.h"。

然后在主函数调用初始化函数MX_USART1_UART_Init。

然后初始化USART1的时钟:打开模板生成的main.c可以找到时钟初始化,里面就包含USART1的时钟初始化,可以看到它把时钟选成PCLK2。

我们将其复制到编程工程的相同位置。注意这里其他外设也要初始化时钟,所以复制过来后要相与。

最后我们就可以调用HAL_UART_Transmit串口发送函数了。

我们总结一下移植过程的注意点:

  • 复制并添加工程
  • 添加异步相关的hal库函数
  • 在hal_conf.h中启动UART模块
  • 包含usart的头文件
  • 调用初始化函数
  • 时钟初始化
  •  2.串口发送函数

    HAL_UART_Transmit()有四个参数,第一个是结构体&huart,第二个是我们要发送的数据,第三个是数据大小,第四个是超时(默认50,即超过50毫秒自动跳出,防止卡在里面影响其他程序)。这我们从它的定义就可以看出来。

    HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

    例如:

    HAL_UART_Transmit(&huart1, (unsigned char *)“Hello World!\r\n”, sizeof(“Hello World!/r/n”), 50)

    其中\n是换行的意思,注意这里的斜杠方向,(unsigned char *)是强制类型转换的意思,防止warning。

    我们下载一下看一下效果。注意,关于串口通讯软件,我们更推荐用单片机组的STC-ISP这个软件,因为嵌入式提供的软件非常不好用,比赛的电脑上是会提供单片机的资源包的,所以可以使用单片机资源包里面的STC_ISP串口助手。

    我们打开通讯软件,找到扫描串口,选择COM(每个电脑不一样,看你插的哪个usb,可以在设备管理器里面看),我这里是COM3,然后配置波特率115200,没有校验位,一个停止位。

    然后点击打开串口,然后reset一下,就可以看到显示的内容了。并且每次reset都会发送一次。

    还有个小问题就是,如果写了两行“ HAL_UART_Transmit(&huart1,(unsigned char *)"Hello World!\r\n",sizeof("Hello World!\r\n"),50);”,是不会两次“Hello World!”的,因为第一个sizeof("Hello World!\r\n")的存在,导致第一个“Hello World!”发送完就已经有了一个停止位了,后面的就不显示了。所以如果改成两行“ HAL_UART_Transmit(&huart1,(unsigned char *)"Hello World!\r\n",sizeof("Hello World!\r\n")-1,50);”,就可以收到两次“Hello World!”了。

    除此之外我们还可以发送中文字符,只要定义一个缓存:u8 tx_buf[]={"你好\r\n"}。

    然后“HAL_UART_Transmit(&huart1,(unsigned char *)tx_buf,sizeof(tx_buf),50);”就可以发送中文了。

    三、串口发送printf重定向

    HAL_UART_Transmit函数有个不好的地方是他的字符串的长度要用sizeof()-1确定。所以我们可以重新定义printf函数,这样就可以用printf作为串口输出。printf的好处是可以格式化字符串进行输出,它可以自动把数据转化成字符的形式进行输出。

    我们写在usart.c中:

    int fputc(int ch,FILE *f)
    {
        HAL_UART_Transmit(&huarl1,(unsigned char *)&ch,1,50);
        return ch;
    }
    

    重新定义了一个函数fputc,其中int ch就是我们要发送的数据。然后调用串口发送函数,输出ch的数据,长度是一个字节。

    这个函数考试的时候要自己写,但是可以不用记,我们可以点help,然后有个uvision help,打开,我们可以搜索retarget,即重定向,第三个就是。

    这上面写了我们先要包含stdio.h,如果没有包含可以包含一下。然后就可以看到对应的fputc这个函数的定义了,我们直接复制下来,后面的串口发送函数自己写一下就可以。所以函数本身我们不用记,只要会用HAL_UART_Transmit就行。

    还有个注意点是,在魔术棒里面要勾选Micro LIB,否则会工作不正常。

    然后我们在主函数里面就可以用printf了,例如:

    printf("Hello world!\r\n");
    printf("N: %d \r\n",123);
    printf("N: %f \r\n",3.1415);
    

    这样printf就能输出到串口了,非常好用。

    四、串口接收程序的设计

    1.程序设计步骤

    1.1 “模板”作为CubeMX生成代码的工程。

    1.2 配置USART1的PA9、PA10为串口收发管脚。

    1.3 根据需求,配置USART的波特率,数据位长度、奇偶校验位、停止位和时钟。

    (前三步与串口发送中的一致)

    1.4 勾选NVIC Settings中的使能USART1的中断。

    我们打开串口1 的NVIC Settings,勾选一下就是使能了串口1的全局中断。后面可以设置抢占优先级和响应优先级。然后我们在System Core里面可以看到有个NVIC,里面就是整个中断的表格。

    中断里的抢占优先级一般是用于中断嵌套中,比如程序正在执行的时候来了一个中断,然后开始处理这部分工作,这个时候如果又来了一个中断,如果它的抢占优先级比第一个高,那么就会先执行第二个中断,处理完了之后再继续执行第一个中断的内容。这个就是中断嵌套。

    而响应优先级就是两个中断同时发生的时候优先执行响应优先级高的,执行完了之后再执行另一个中断。一般中断嵌套在蓝桥杯中考的不多,往往是在做大型项目的时候会考虑到这方面的问题。

    所以我们一般这样配置:Group要选择4bit给抢占优先级,0bit给响应优先级。也就是只配置4bit的抢占优先级,也就是优先级范围是0到15,数字越小的优先级越高。

    一般来说我们把串口的响应优先级要比Systick要高,这样我们就把串口设置成0,Time base设置成15,让Systick不要打断串口接收的过程。当然我们也可以不设置,因为也不太需要中断的嵌套。这里我们就不配置了,全都为0。我们的目的只是为了得到让串口使能中断的初始化代码,用来实现串口接收的中断,就不用研究它抢不抢占了,这里影响不大。

    生成工程后,我们打开usart.c找到HAL_UART_MspInit的定义里面可以看到/* USART1 interrupt Init */,即串口的中断初始化设置。

    这里设置了中断的优先级,并且使能了一下IRQ,就是使能了一下USART1的中断,这样我们以后如果接收到一个数据的话,就可以在中断处理函数里面处理这些数据了。所以这部分要移植到之前的那份工程里去,当然如果之前一开始做串口发送的时候就勾选上中断的话就不需要这个移植的过程了。

    除此之外还要把中断的函数移植一下,在stm32g4xx_it.c的文件中有个串口1的中断处理函数,复制到编程工程即可。这里出现错误是因为结构体huart1没有extern,即extern UART_HandleTypeDef huart1;这行代码,可以让huart1在外部文件中可以调用。这行代码也要复制过去。

    这时我们编译可以看到没有错误。

    于是我们就可以用中断进行接收数据了,这是为了防止数据的丢失,发送数据的时候就不需要中断,但是接收数据的时候最好还是中断一下。

    1.5 将usart.c和usart.h移植到“编程工程”。

    (与串口发送中的一致)

    2.串口接收函数

    HAL_UART_Receive_IT(&huart1,uart_buf,1)    //开启串口接收中断,函数要自己写,其中第一个参数是结构体变量,第二个是用于接收数组的缓存,第三个是接收缓存的大小。

    所以要先定义一个缓存位置,用于接收数组,接收的大小设置为1就可以,因为一收到数据就产生中断。

    产生串口中断后会执行一个弱定义的函数HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart),它是一个串口接收的回调函数(如果回调函数不会写可以找定义),它的功能需要我们自己来写,所以我们要定义一下这个函数。我们可以用这个函数自己编写中断后需要实现哪些功能,如点亮数据相对应的LED灯等等。功能写完之后一定要再次使能HAL_UART_Receive_IT,否则就只能接收一次。

    u8 uart_buf[2];//保存接收到的字节的缓存位置,大小是3个字节
    //......
    int main()
    {
        //......
        MX_USART1_UART_Init();//串口初始化
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//一接收到数据就产生中断
        //......
        while(1)
        {
            //......
        }
    }
    //......
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数
    {
        LED_Control(uart_buf[0]);//用uart_buf里面的第0个字节控制LED灯
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//再次开启串口接收中断,方便接收下一个数据
    }

    3.串口接收固定长度数据

    如果我们想接收三个字节的数据,并且每个字节有不同的作用,比如“11 22 33”,那我们要怎么进行编程呢?

    我们首先要定义一个rx_buf[10],把uart_buf接收到的数据存下来。因为我们只接收3个字节的数据,所以10是够用的。然后在回调函数中让rx_buf接收uart_buf中的数据,并且定义一个变量rx_cnt用于计数,每接收一个,变量加一,加到3之后又返回1。并且判断,只有收到三个字节后才执行程序,然后用rx_buf中的数据控制单片机。

    u8 uart_buf[2];//保存接收到的字节的缓存位置,大小是3个字节
    u8 rx_buf[10];//用于保存收到的多个字节
    u8 rx_cnt=0;//用于计数,表示rx_buf的第几位
    //......
    int main()
    {
        //......
        MX_USART1_UART_Init();//串口初始化
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//一接收到数据就产生中断
        //......
        while(1)
        {
            //......
        }
    }
    //......
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数
    {
        rx_buf[rx_cnt++]=uart_buf[0];//把每次uart_buf的变量存入rx_buf的第rx_cnt位,存完加1
        if(rx_cnt==3)
        {
            LED_Control(rx_buf[1]);//用第二个字节控制LED灯
            rx_cnt=0;
        }
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//再次开启串口接收中断,方便接收下一个数据
    }

    这样的话如果输入三个字节的数据,我们就可以用第二个字节来控制LED灯。但是又出现了一个问题,如果输入的不是三个字节而是一个字节呢?如果用户发现了自己发错命令了,又发了三个字节的数据,结果会怎样?结果是rx_buf会缓存一个字节的错误数据,导致数据错位,这要怎么避免呢?

    所以一个完美的串口接收函数需要一个判断函数,用于判断数据是否是连续发送的。可以用定时器,接收到一个字节后如果50ms内没有收到第二个字节,就说明不是连续发送的,可以将rx清零。

    我们定义一个函数RxIdle_Process()用于计时50ms清零,每次调用回调函数都重新计时50ms。清空数组的时候要用到一个memset()函数,需要包含一下string.h。RxIdle_Process()是要放在主程序里面循环计时的。

    #include "main.h"
    #include "gpio.h"
    #include "usart.h"
    #include "string.h" //要用memset函数
    //......
    u8 uart_buf[2];//保存接收到的字节的缓存位置,大小是3个字节
    u8 rx_buf[10];//用于保存收到的多个字节
    u8 rx_cnt=0;//用于计数,表示rx_buf的第几位
    _IO uint32_t uartTick=0;
    void RxIdle_Process()//每50ms数据清零
    {
        if(uwTick-uartTick<50) return;
        uartTick=uwTick;
        rx_cnt=0;//清空数据
        memset(rx_buf,'\0',sizeof(rx_buf));//清空接收缓存数组
    }
    //......
    int main()
    {
        //......
        MX_USART1_UART_Init();//串口初始化
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//一接收到数据就产生中断
        //......
        while(1)
        {
            RxIdle_Process();
            //......
        }
    }
    //......
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数
    {
        uartTick=uwTick;//每次收到数据重新开始计时50ms
        rx_buf[rx_cnt++]=uart_buf[0];//把每次uart_buf的变量存入rx_buf的第rx_cnt位,存完加1
        if(rx_cnt==3)
        {
            LED_Control(rx_buf[1]);//用第二个字节控制LED灯
            rx_cnt=0;
        }
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//再次开启串口接收中断,方便接收下一个数据
    }

    4.串口接收带帧尾的不定长数据

    串口除了能接收定长数据之外,也能接收不定长数据,但是不定长数据一定要有一个字节来表示一连串字符的结束,这个就是帧尾。

    那我们就需要对rx_buf[rx_cnt]里面的内容进行判断,需要注意,rx_cnt-1才是当前值(因为前面数据传输完之后rx_cnt++了),所以应该是对rx_buf[rx_cnt-1]里面的内容进行判断。或者直接对uart_buf[0]判断也可以。

    我们假设接收到换行符('\n'),接收就结束。

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数
    {
        uartTick=uwTick;//每次收到数据重新开始计时50ms
        rx_buf[rx_cnt++]=uart_buf[0];//把每次uart_buf的变量存入rx_buf的第rx_cnt位,存完加1
        if(rx_buf[0]=='\n')//接收到换行符
        {
            LED_Control(rx_buf[1]);//用第二个字节控制LED灯
            rx_cnt=0;
        }
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//再次开启串口接收中断,方便接收下一个数据
    }

    这样不论我们发了多少个字节,不按回车它都不会有反应,按了回车,它就按第二个字节点亮LED。

    再举个综合的例子(结合串口输入和输出):如果接收到“R37”,则输出变量R37_value内的值:

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数
    {
        uartTick=uwTick;//每次收到数据重新开始计时50ms
        rx_buf[rx_cnt++]=uart_buf[0];//把每次uart_buf的变量存入rx_buf的第rx_cnt位,存完加1
        if(rx_buf[0]=='R' && rx_buf[1]=='3' && rx_buf[2]=='7') //用与,当连续收到R、3、7才返回
        {
            printf("R37:%f",R37_value) //显示R37_value的值
            rx_cnt=0;
        }
        HAL_UART_Receive_IT(&huart1,uart_buf,1);//再次开启串口接收中断,方便接收下一个数据
    }

    总结

    本节讲了串口通信USART的内容,因为我们没有接时钟线,所以只有异步通信也就是UART的内容。首先了解了一下通信的基本原理,需要了解硬件电路,以及数据收发的时序图,还有波特率的概念。然后分别学习了串口发送程序和接收程序的步骤。串口发送比较简单,只要掌握HAL_UART_Transmit()函数就行,我们还可以重定向printf,利用printf来进行串口发送。串口接收程序的内容比较多,首先要了解中断的相关概念,然后就要学习串口接收中断函数HAL_UART_Receive_IT()以及串口接收回调函数HAL_UART_RxCpltCallback()的使用,这样就能实现基本的串口接收功能。但是实际应用中还需要考虑“固定长度数据的接收”和“带帧尾的不定长数据的接收”这两种情况,依此设计不同的程序。

    作者:我不是板神

    物联沃分享整理
    物联沃-IOTWORD物联网 » 嵌入式开发笔记:STM32G431 USART串口通讯学习(基于HAL库)

    发表评论