打印
[应用相关]

细说STM32单片机USART中断实现收发控制的方法

[复制链接]
309|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-10-27 15:50 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
   本文作者通过实例细说STM32 单片机通过USART串口中断与上位机(比如串口助手)进行收发控制的实现方法。实例使用的开发板型号NUCLEO-G474RE,MCU型号STM32G474RET6。

一、工程的目的   
1、实例项目的设计目的
下载,首次运行后,串口助手先接受字符串,然后每隔1秒接受一次RTC的时间,不被打扰的时候,连续接收,无止尽;
按照串口协议,在串口助手发送修改小时、分钟、秒、暂停上传、恢复上传的指令。
发送修改小时、分钟、秒指令后,在串口助手里依次显示指令字符并更新相应的RTC时间。
发送暂停上传指令后,显示指令字符串,并停止显示新的内容。
发送恢复上传指令后,显示指令字符串,并恢复显示新的时间内容。
PC端发送的指令字符串为固定的长度,比如5字节;超过指定的指令长度,会引起下位机工作混乱,直至得不到正确的结果。
2、串口通讯的协议
       串口 发送的 数据要符合格式,起始符#,结束符;,第2位:H代表小时,M代表分钟,S代表秒,U代表禁止上传或恢复上传。其它字符是非法字符。第3-4位代表时间(时分秒),占两位数,空位用0补,不得空位。




二、工程设置
1、时钟
         外部高速时钟,24MHz,HSE,APB等都是170MHz;

         外部低速时钟,32.768KHz,LSE=32.768KHz to RTC;

2、DEBUG
         Serial Wire;

3、 RTC
首先启用LSE和RTC,在时钟树上设置LSE作为RTC的时钟源。
勾选Activate Clock Source和Activate Calendar,选择Internal Wakeup;
Calendar Time:Data Format为Binary data format,Hours=15,Minutes=23,Seconds=10
Wake Up: Wake Up Clock(唤醒时钟源)为1Hz信号,Wake Up Counter(唤醒计数器)值为0,也就是每秒唤醒一次。
其它参数默认;
4、USART2
Mode:工作模式,设置为Asynchronous(异步),也是串口最常用的模式;
Hardware Flow Control (RS232):硬件流控制设置为Disable。
参数设置部分包括串口通信的4个基本参数和STM32的2个扩展参数。
        4个基本参数如下:

Baud Rate:设置为115200 bit/s。
Word Length:字长(包括奇偶校验位)设置为8位。
Parity:设置为None。如果设置有奇偶校验,字长应该设置为9位。
Stop Bits:设置为1位。
        STM32 MCU扩展的2个参数如下:

Data Direction:数据方向设置为Receive and Transmit(接收和发送)。还可以设置为只接收或只发送。
Over Sampling:过采样设置为16 Samples,可选16 Samples或8 Samples。选择不同的过采样数值会影响波特率的可设置范围,而CubeMX会自动更新波特率的可设置范围。
        其它参数默认;

5、NVIC
         RTC唤醒中断、USART中断,抢占式优先级均=1,Time Base中断=0。

6、Project Manager Code Generater
        在这个页面中,把下面图中的多选框选中,启用外设.c/.h数据对。



二、代码
1、main.c
int main(void)
{

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_RTC_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  uint8_t hello1[]="Hello,blocking 1\n";
  HAL_UART_Transmit(&huart2,hello1,sizeof(hello1),500);        //阻塞模式
  HAL_Delay(10);

  uint8_t hello2[]="Recent RTC time\n";
  HAL_UART_Transmit_IT(&huart2,hello2,sizeof(hello2));
  HAL_Delay(10);

  HAL_UART_Receive_IT(&huart2,rxBuffer,RX_CMD_LEN);        //中断方式接收5字节
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
          if(isUploadTime == 1)
                     {
                             HAL_RTCEx_WakeUpTimerEventCallback(&hrtc);
                     }
  }
  /* USER CODE END 3 */
}

        在进入while循环之前,程序调用函数HAL_UART_Transmit(),以阻塞方式发送了字符串“ ”,又调用HAL_UART_Transmit IT(),以非阻塞方式发送了字符串“ ”。在最后进入while循环之前执行的是下面的语句:

HAL_UART_Receive_IT(&huart1,rxBuffer,RX_CMD_LEN);
        其中,RX_CMD_LEN是在usart.h文件中定义的宏,含义是接收到的指令的长度,数值为5;rxBuffer是在文件usart.c中定义的长度为5字节的数组,作为接收数据的缓冲区。执行这行语句后,USART2就以中断方式接收5字节数据,接收到5字节数据后,数据会保存到数组rxBuffer里,并产生UART_IT_RXNE事件中断,执行回调函数HAL_UART_RxCpltCallback()。

        while(1) 的无限循环中,始终在执行RTC的唤醒中断的回调函数,保证设计目的中的要求的“串口助手先接受字符串,然后每隔1秒接受一次RTC的时间,不被打扰的时候,连续接收,无止尽”。

        如果在while(1) 的无限循环为空循环,不能得到稳定的设计目的所需要上面那样的显示效果,debug调试的时候能达到设计目的所需的显示效果,首次下载运行后能连续显示如设计目的所需的的显示效果,再次下载运行后只显示一行就停下来了。这里作者遇到的问题如何来克服,就留给聪明的网友吧。

2、usart.h
/* USER CODE BEGIN Private defines */
#define        RX_CMD_LEN 5                //指令长度5字节
extern uint8_t rxBuffer[];  //5字节的输入缓冲区,如#H15;
extern uint8_t isUploadTime;//是否上传时间数据
/* USER CODE END Private defines */

void MX_USART2_UART_Init(void);

/* USER CODE BEGIN Prototypes */
void on_UART_IDLE(UART_HandleTypeDef *huart);                //IDLE中断检测
void updateRTCTime();                                                                //对接收指令的处理
/* USER CODE END Prototypes */
3、usart.c
         USART的初始化程序是CubeIDE自动生成的。

/* USER CODE BEGIN 0 */
#include "rtc.h"
#include <string.h>

uint8_t        proBuffer[10] = "#S45;\n";        //用于处理数据, #H12; #M23; #S43;
uint8_t        rxBuffer[10] = "#H15;\n";                //接收缓存数据, #H12; #M23; #S43;
uint8_t        rxCompleted = RESET;                //HAL_UART_Receive_IT()接收是否完成

uint8_t        isUploadTime = 1;                                //是否上传时间数据
/* USER CODE END 0 */
         回调函数:

/* USER CODE BEGIN 1 */
/*串口接收完毕中断回调函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if (huart->Instance == USART2)
        {
                /* 接收到固定长度数据后使能UART_IT_IDLE中断,在UART_IT_IDLE中断里再次接收 */
                rxCompleted = SET;
                /* 如果接收完毕,就把rxBuffer的前RX_CMD_LEN位映射给proBuffer */
                for(uint16_t i=0;i<RX_CMD_LEN;i++)
                        proBuffer=rxBuffer;

                __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); //允许IDLE中断
        }
}

/*IDLE事件中断的检测与处理*/
void on_UART_IDLE(UART_HandleTypeDef *huart)
{
        if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) == RESET) //IDLE中断挂起标志位是否reset
                return;

        __HAL_UART_CLEAR_IDLEFLAG(huart);             //清除IDLE挂起标志
        __HAL_UART_DISABLE_IT(huart, UART_IT_IDLE);   //禁止IDLE事件中断

        if (rxCompleted)
        {
                //把接收到的指令字符显示到串口助手
                HAL_UART_Transmit(huart,proBuffer, strlen((char*)(proBuffer)), 200);
                HAL_Delay(10);                 //适当地延时,否则updateRTCTime()可能出错

                updateRTCTime();         //把接收到的指令更新RTC时间

                /*更新完RTC时间后,要把rxCompleted复位,并再次启动串口接收,让程序处于等待串口输入的状态*/
                rxCompleted = RESET;

                /* 再次启动串口接收 */
                HAL_UART_Receive_IT(huart, rxBuffer, RX_CMD_LEN);
        }
}

/*根据串口接受来的指令字符串,更新修改RTC时间*/
void updateRTCTime()                 //根据串口接收的指令字符串进行处理
{
        if (proBuffer[0] != '#')         //收到无效指令
                return;

        /* -0x30 操作用于将ASCII码表示的字符(假设是数字'0'~'9')转换为其对应的整数 */
        uint8_t timeSection = proBuffer[1]; //类型字符
        uint8_t tmp10 = proBuffer[2]-0x30;         //十位
        uint8_t tmp1 = proBuffer[3]-0x30;         //个位
        uint8_t val= 10*tmp10+tmp1;

        if (timeSection=='U')
        {
                if( tmp1 == 0)
                {
                        isUploadTime = 0;        //pause
                        return;
                }
                else
                        isUploadTime = 1;        //resume
        }

        RTC_TimeTypeDef sTime;
        RTC_DateTypeDef sDate;
        if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
        {
                //调用HAL_RTC_GetTime()之后必须调用HAL_RTC_GetDate()以解锁数据,才能连续更新Date and Time
                HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
                if (timeSection=='H')                 //修改hour
                        sTime.Hours=val;
                else if (timeSection=='M')        //修改minute
                        sTime.Minutes=val;
                else if (timeSection=='S')        //修改second
                        sTime.Seconds=val;
                HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); //设置RTC时间影响到下一次唤醒
        }
}
/* USER CODE END 1 */

       updateRTCTime()用于对接收的一条指令进行解析和执行。上位机发来的指令的格式定义如表,程序就按照指令格式规范提取指令类型和指令参数,然后做出相应的处理,例如,接收的指令字符串是“#H10;”,就表示要将RTC时间的小时修改为10。        

        updateRTCTime()代码中的 -0x30 操作用于将ASCII码表示的字符(假设是数字'0'到'9')转换为其对应的整数值。在ASCII码表中,数字'0'的编码是0x30(十进制中的48),数字'1'的编码是0x31(十进制中的49),依此类推,直到数字'9'的编码是0x39(十进制中的57)。因此,当你从某个数组(如proBuffer)中读取一个字节,该字节被假定为表示一个ASCII编码的数字字符时,你可以通过从这个字符的ASCII码值中减去'0'的ASCII码值(即0x30)来得到该数字字符所代表的实际整数值。例如:如果proBuffer[2]包含的是数字字符'5'的ASCII码(即0x35),那么proBuffer[2] - 0x30的计算结果就是0x35 - 0x30 = 0x05,这等于十进制中的5。

4、stm32g4xx_it.c
/* USER CODE BEGIN Includes */
#include "usart.h"
/* USER CODE END Includes */
void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */

  /* USER CODE END USART2_IRQn 0 */
  HAL_UART_IRQHandler(&huart2);
  /* USER CODE BEGIN USART2_IRQn 1 */
  on_UART_IDLE(&huart2);
  /* USER CODE END USART2_IRQn 1 */
}
          在此ISR中,增加了一条语句on_UART_IDLE(&huart2),用于检测USART2空闲事件中断并做相应处理。函数on_UART_IDLE()在usart.h文件中定义。串口的空闲事件中断(事件类型UART_IT_IDLE)没有对应的回调函数,所以需要自己创建该函数。

5、rtc.c
         RTC的初始化程序是CubeIDE自动生成的。

/* USER CODE BEGIN 0 */
#include "usart.h"
#include <stdio.h>                //用到函数sprintf()
#include <string.h>                //用到函数strlen()

uint8_t second = 100;        //大于60的int,sTime.Seconds

/* USER CODE END 0 */
         RTC时钟唤醒中断回调函数:

/* USER CODE BEGIN 1 */

/*间隔1s的RTC时钟唤醒事件中断回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
        RTC_TimeTypeDef sTime;
        RTC_DateTypeDef sDate;

        if (HAL_RTC_GetTime(hrtc, &sTime,  RTC_FORMAT_BIN) == HAL_OK)
        {
                HAL_RTC_GetDate(hrtc, &sDate,  RTC_FORMAT_BIN);
                uint8_t        timeStr[20];

                //时间字符串格式hh:mm:ss
                sprintf((char*)timeStr,"%2d:%2d:%2d\n",sTime.Hours,sTime.Minutes,sTime.Seconds);

                //按RTC格式和周期,把RTC实时时钟发到串口助手
                if ((isUploadTime ==1)&&((uint8_t)sTime.Seconds != second))
                {
                                second = (uint8_t)sTime.Seconds;

                                //strlen()以结束符'\0'为标志计算字符串长度,但是不包含'\0',上位机只要换行符'\n'
                                HAL_UART_Transmit(&huart2,timeStr,strlen((char*)(timeStr)),200);
                }
        }
}
/* USER CODE END 1 */

        两个字节型数组rxBuffer和proBuffer,其中,rxBuffer是串口接收数据缓冲区,proBuffer是接收完成后复制rxBuffer的内容,然后用于指令解析操作的数组。变量rxCompletet表示是否已完成一个缓冲区的中断方式接收,变量isUploadTime用于控制RTC周期唤醒中断里是否上传时间字符串数据。

函数updateRTCTime()的功能
        RTC时钟唤醒回调函数的功能是读取RTC当前时间,将这个时间转换为字符串timeStr。如果变量isUploadTime的值不为零,就通过串口向上位机发送此字符串。这里使用了阻塞式函数HAL_UART_Transmit(),也可以使用非阻塞模式的函数HAL_UART_Transmit_IT()。变量isUploadTime是在文件usart.c里定义的,其值可以根据串口接收的指令改变,当isUploadTime的值变为0时,就不向上位机传输时间字符串了。

        在使用sprintf()函数创建时间字符串时,为了让上位机自动换行显示,在字符串最后加了换行符\n,实际上,还会在换行符后面自动加上结束符\0。在使用函数HAL_UART_Transmit()向上位机传输字符串时,实际传输字符的个数用strlen(timeStr)计算,strlen()以结束符\0为标志计算字符串实际长度,但是不包含结束符\0。不能用sizeof()替代strlen(),因为sizeof(timeStr)得到的结果是数组的维数20。

        second变量用于规避显示重复的RTC数值,因为RTC更新的频率和串口发送的速率很难做到步调一致的,所以只允许发送不一样的RTC时间内容。

函数on_UART_IDLE()的功能
        on_UART_IDLE()用于检测是否发生了空闲事件中断(UART_IT_IDLE类型事件中断),并且做出相应的处理。UART_IT_IDLE类型事件中断在串口初始化时默认是关闭的,而且没有相应的回调函数,所以编写了函数on_UART_IDLE(),并且在USART2的ISR函数USART2_IRQHandler()里调用。

        如果发生了UART_IT_IDLE类型事件中断,是因为在HAL_UART_RxCpltCallback()函数里开启了UART_IT_IDLE类型事件中断,表示串口数据接收完成了,就清除该中断标志,并禁止UART_IT_IDLE类型事件中断。因为串口经常处于空闲状态,如果此事件中断一直开启,将非常占用处理器时间。

        如果rxCompleted被置位,就表示上次执行HAL_UART_Receive_IT()接收一个缓冲区的数据已经完成,就调用updateRTCTime()函数对接收的指令数据进行解析处理,处理完成后将rxCompleted置零,并再次执行HAL_UART_Receive_IT(huart,rxBuffer,RX_CMD_LEN)开启下一次串口中断方式接收。

回调函数HAL_UART_RxCpltCallback()的功能
        HAL_UART_RxCpltCallback是在串口发生UART_IT_RXNE事件中断时的回调函数,也就是以HAL_UART_Receive_IT()启动串口数据接收,并完成指定长度数据接收后调用的回调函数。这个回调函数的代码功能是置位rxCompleted,将接收数据缓冲区rxBuffer里的数据复制到指令解析处理缓冲区proBuffer,然后开启UART_IT_IDLE类型事件中断。

        HAL_UART_Receive_IT()完成一次数据接收后就关闭了串口接收中断,不会自动进行下一次的接收,需要再次调用HAL_UART_Receive_IT()才能启动下一次的接收,但不能在回调函数HAL_UART_RxCpltCallback()里调用HAL_UART_Receive_IT()。为了能连续进行中断方式的串口接收,在完成一次接收,并且串口状态为空闲,也就是发生UART_IT_IDLE类型事件中断时,对接收到的指令数据进行处理,然后再次调用HAL_UART_Receive_IT()以启动下一次的接收。

三、运行与调试
         下载,运行,首先显示两行字符串,然后连续显示时间,间隔1s。



         从PC端发送修改时间、分钟、秒,先依次显示命令字符串,然后按修改后的数值,继续连续显示时间;

        从PC端发送暂停发送命令,PC端先显示命令字符串,然后停止了继续显示;

        从PC端发送恢复发送命令,PC端先显示命令字符串,然后恢复继续显示时间;



         暂停发送后,发送修改小时、分钟、秒等命令,只显示命令字符串,并不显示修改后的时间内容,但不代表修改时间的内容没有发生,事实上,在后台已经修改完毕;此后,PC端如发送恢复发送命令后,PC端会显示更新后的实时时间内容。



        本实例实现的串口发送指令字符串,是指定长度的。MCU一次固定接收5个字节的数据。如果PC端发送的数据长了,导致MCU端一次接收到的数据大于5个字节,那么程序就混乱了。比如PC端发送#S456;第一次发送后,PC端显示#S456,RTC时间内容也被更新到了45s。但是,再发送一次#S456;PC端显示;#S45,并且RTC时间也没有被更新。此后,无论发送什么指令,长度如何,MCU的程序都视若罔闻。



         串口助手不要开启\r\n(这个功能是发送新行),因为,一旦开启,就相当于改变了发送端发送的数据的长度,发送端自动增加了一个字节的数据0x0A,接收端接收到的指令的长度不是5字节。产生如上类似的事故。

        第一次发送第一条指令后,RTC时间得到了更新,但是此后无论发送什么数据,MCU都视若罔闻。



        上述问题的解决的办法:检测起始符#,检测结束符;判断指令内容和长度。最关键的是数据复制proBuffer后,清空rxBuffer,然后再开启中断接收新的数据;
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/wenchm/article/details/143189217

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2028

主题

15903

帖子

13

粉丝