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

目录

一、工程的目的    

1、实例项目的设计目的

2、串口通讯的协议 

二、工程设置

1、时钟

2、DEBUG

3、 RTC

4、USART2

5、NVIC

6、Project Manager Code Generater

二、代码

1、main.c

2、usart.h

3、usart.c

4、stm32g4xx_it.c

5、rtc.c

三、运行与调试


        本文作者通过实例细说STM32 单片机通过USART串口中断与上位机(比如串口助手)进行收发控制的实现方法。实例使用的开发板型号NUCLEO-G474RE,MCU型号STM32G474RET6。

一、工程的目的    

1、实例项目的设计目的

  • 下载,首次运行后,串口助手先接受字符串,然后每隔1秒接受一次RTC的时间,不被打扰的时候,连续接收,无止尽;
  • 按照串口协议,在串口助手发送修改小时、分钟、秒、暂停上传、恢复上传的指令。
  • 发送修改小时、分钟、秒指令后,在串口助手里依次显示指令字符并更新相应的RTC时间。
  • 发送暂停上传指令后,显示指令字符串,并停止显示新的内容。
  • 发送恢复上传指令后,显示指令字符串,并恢复显示新的时间内容。
  • PC端发送的指令字符串为固定的长度,比如5字节;超过指定的指令长度,会引起下位机工作混乱,直至得不到正确的结果。

2、串口通讯的协议 

       串口 发送的 数据要符合格式,起始符#,结束符;,第2位:H代表小时,M代表分钟,S代表秒,U代表禁止上传或恢复上传。其它字符是非法字符。第3-4位代表时间(时分秒),占两位数,空位用0补,不得空位。

上位机发送的指令字符串

指令功能

#H13;

设置小时,将RTC时间的小时修改为13

#M32;

设置分钟,将RTC时间的分钟修改为32

#S05;

设置秒,将RTC时间的秒修改为5

#U01;

恢复上传时间数据

#U00;

停止上传时间数据

二、工程设置

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[i]=rxBuffer[i];

		__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,然后再开启中断接收新的数据;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wenchm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值