细说STM32单片机DMA中断收发RTC实时时间并改善其鲁棒性的方法

目录

一、DMA基础知识

1、DMA简介

(1)DMA控制器

(2)DMA流

(3)DMA请求

(4)仲裁器

(5)DMA传输属性

2、源地址和目标地址

3、DMA传输模式

4、传输数据量的大小

5、数据宽度

6、地址指针递增

7、DMA工作模式

8、DMA流的优先级别

9、FIFO或直接模式

10、单次传输或突发传输

11、双缓冲区模式

二、DMA的HAL驱动

1、DMA的HAL函数

2、DMA传输初始化

3、启动DMA数据传输

4、DMA的中断

三、工程配置

1、设计目的和通讯协议

2、工程设置

(1)时钟

(2)DEBUG

[(3) RTC](#(3) RTC)

(4)USART2

(5)NVIC

[(6)Project Manager Code Generater](#(6)Project Manager Code Generater)

四、软件代码

1、main.c

2、usart.h

3、usart.c

4、rtc.c

五、运行与调试

1、合规的指令

2、proBuffer[0]不是#或proBuffer[4]不是;

3、指令长度小于5

4、仅proBuffer[2]或proBuffer[3]不是数字

[5、 ';'位于proBuffer[2]或proBuffer[3]位置](#5、 ';'位于proBuffer[2]或proBuffer[3]位置)

6、proBuffer[2]和proBuffer[3]数字超范围

7、指令长度大于5


本文通过STM32G474RET6介绍DMA基础知识,然后通过USART2以DMA方式从上位机接收指令数据、处理指令数据、增加程序的容错能力、最后向上位机发送RTCtime。

本文通过测试环节,也发现了作者在前几篇利用串口中断接收、处理和发送RTCtime的文章里没有发现的、可能的错误处理方法与疏漏(挖掘的不够深刻):当指令长度小于5或大于5时,只有在其后累计输入的字符长度恰好等于5的倍数时,程序才会跳转到正常。否则,即使输入长度不等于5的指令后,接着输入正确的指令,程序也逃不出出错的死循环。但是当错误的指令长度是5的倍数的时候,比如指令长度是10,直接或多次发送指令,就能顺利地跳转到正常。

一、DMA基础知识

直接存储器访问(Direct Memory Access,DMA)是实现存储器与外设、存储器与存储之间高效数据传输的方法。DMA数据传输无须CPU操作,是一种硬件化的高速数据传输,减少CPU的负载。在需要进行大量或高速数据传输时,DMA传输方式特别有用。

1、DMA简介

STM32G474RET6有两个DMA控制器,即DMA1和DMA2。一个DMA控制器的框图如图:

(1)DMA控制器

DMA控制器(上图左侧蓝色区域)是管理DMA的硬件资源,实现DMA数据传输的控制器,一个硬件模块。MCU上有2个DMA控制器,即DMA1和DMA2。这两个DMA控制器的本结构和功能相同,STM32G474的两个DMAs支持:

cpp 复制代码
● Memory-to-memory transfer
● Peripheral-to-memory, memory-to-peripheral, and peripheral-to-peripheral transfers

其他规格MCU的DMA不尽相同,比如STM32F407,仅DMA2具有存储器到存储器的传输方式,而DMA1没有这种方式。

(2)DMA流

DMA流就是能进行DMA数据传输的链路,是一个硬件结构,所以每个DMA有独立的中断地址,具有多个中断事件源,如传输半完成中断事件、传输完成中事件等。每个DMA控制器有8个DMA流,每个DMA流有独立的4级32位FIFO缓冲区。DMA流有很多参数,这些参数的配置决定了DMA传输属性。

(3)DMA请求

DMA请求就是外设或存储器发起的DMA传输需求,又称为DMA通道。每个DMA流最多有8个可选的DMA请求,一个DMA请求一般有两个可选的DMA流。

(4)仲裁器

DMA控制器中有一个仲裁器,仲裁器为两个AHB主端口(存储器和外设端)提供基于优先级别的DMA请求管理。每个DMA流有一个可设置的软件优先级别,如果个DMA流的软件优先级别相同,则流编号更小的优先级别更高。流编号就是DMA流的硬件优先级别。

(5)DMA传输属性

一个DMA流配置一个DMA请求后,就构成一个单方向的DMA数据传输链路,DMA传输属性就由DMA流的参数配置决定。DMA传输有如下一些属性:

  • DMA流和通道。一个DMA流需要选择一个通道后,才能组成一个DMA传输链路,通道就是外设或存储器的DMA请求。
  • DMA流的优先级别。需要为DMA流设置软件优先级别。
  • 源地址和目标地址。DMA传输是单方向的,需要设置DMA传输的源地址和目标地址。
  • 源和目标的数据宽度,即单个数据点的大小,有字节、半字和字。
  • 传输数据量的大小。一次DMA传输的数据缓冲区大小。
  • 源地址和目标地址指针是否自增加。
  • DMA工作模式,即正常(Normal)模式或循环(Circular)模式。
  • DMA传输模式。根据源和目标的特性所确定的数据传输方向,DMA传输模式包括外设到存储器、存储器到外设以及存储器到存储器。
  • 是否使用FIFO,以及使用FIFO时的阈值(Threshold)。
  • 是否使用突发传输,以及源和目标突发传输数据量大小。
  • 是否使用双缓冲区模式。
  • 流量控制。

一个DMA传输链路的主要硬件是DMA流,DMA传输属性的设置就是DMA流的参数配置。

2、源地址和目标地址

在32位的STM32 MCU中,所有寄存器、外设和存储器是在4GB范围内统一编址的,地址范围为0x00000000至0xFFFFFFFF。每个外设都有自己的地址,外设的地址就是外设的寄存器基址。DMA传输由源地址和目标地址决定,也就是整个4GB范围内可寻址的外设和存储器。

3、DMA传输模式

根据设置的DMA源和目标地址以及DMA请求的特性,STM32G474的DMA数据传输有如下4种传输模式(其它规格的MCU不尽相同,比如STM32F407,仅有3钟传输模式),也就是数据传输方向。

  • 外设到存储器(Peripheral To Memory),例如,ADC采集的数据存入内存中的缓冲区。
  • 存储器到外设(Memory To Peripheral),例如,通过UART接口发出内存中的数据。
  • 存储器到存储器(Memory To Memory),例如,将外部SRAM中的数据复制到内存中。只有DMA2控制器有这种传输模式。
  • 外设到外设(Peripheral To Peripheral),STM32G474支持,STM32F407不支持。

4、传输数据量的大小

默认情况下,使用DMA作为流量控制器,需要设置传输数据量的大小,也就是从源到目标传输的数据总量。实际使用时,传输数据量的大小就是一个DMA传输数据缓冲区的大小。

5、数据宽度

数据宽度(Data Width)是源和目标传输的基本数据单元的大小,有字节(Byte)、半字(HanWord)和字(Word)3种大小。

源和目标的数据宽度是需要单独设置的。一般情况下,源和目标的数据宽度是一样的。例如,USART2使用DMA方式发送数据,传输方向是存储器到外设,因为USART2发送数据的基本单元是字节,所以存储器和外设的数据宽度都应该设置为字节。

6、地址指针递增

可以设置在每次传输后,将外设或存储器的地址指针递增,或保持不变。

通过单个寄存器访问外设源或目标数据时,应该禁止递增,但是在某些情况下,使地址递增可以提高传输效率。例如,将ADC转换的数据以DMA方式存入内存时,可以使存储器的地址递增,这样每次传输的数据自动存入新的地址。外设和存储器的地址递增量的大小就是其各自的数据宽度。

7、DMA工作模式

DMA配置中要设置传输数据量大小,也就是DMA发送或接收的数据缓冲区的大小。根据是否自动重复传输缓冲区的数据,DMA工作模式分为正常模式和循环模式两种。

  • 正常(Normal)模式是指传输完一个缓冲区的数据后,DMA传输就停止了,若需要再传输一次缓冲区的数据,就需要再启动一次DMA传输。例如,在正常模式下,执行函数HAL_UART_Receive_DMA()接收固定长度的数据,接收完成后就不再继续接收了,这与中断方式接收函数HAL_UART_Receive_IT()类似。
  • 循环(Circular)模式是指启动一个缓冲区的数据传输后,会循环执行这个DMA数据传输任务。例如,在循环模式下,只需执行一次HAL_UART_Receive_DMA(),就可以连续重复地进行串口数据的DMA接收,接收满一个缓冲区的数据后,产生DMA传输完成事件中断。这可以很好地解决串口输入连续监测的问题,使程序结构简化。

8、DMA流的优先级别

每个DMA流都有一个可设置的软件优先级别(Priority level),优先级别有4种:Very high(非常高)、High(高)、Medium(中等)和Low(低)。如果两个DMA流的软件优先级别相同,则流编号更小的优先级别更高。流编号就是DMA流的硬件优先级。

DMA控制器中的仲裁器基于DMA流的优先级别进行DMA请求管理。

要区分DMA流中断优先级和DMA流优先级别这两个概念。DMA流中断优先级是NVIC管理的中断系统里的优先级,而DMA流优先级别是DMA控制器里管理DMA请求用到的优先级。

9、FIFO或直接模式

每个DMA流有4级32位FIFO缓冲区,DMA传输具有FIFO模式或直接模式。

不使用FIFO时就是直接模式,直接模式就是发出DMA请求时,立即启动数据传输。如果是存储器到外设的DMA传输,DMA会预先取数据放在FIFO里,发出DMA请求时,立即将数据发送出去。

使用FIFO缓冲区时就是FIFO模式。可通过软件将阈值设置为FIFO的1/4、1/2、3/4或1倍大小。FIFO中存储的数据量达到阈值时,FIFO中的数据就传输到目标中。

当DMA传输的源和目标的数据宽度不同时,FIFO非常有用。例如,源输出的数据是字节数据流,而目标要求32位的字数据,这时,可以设置FIFO阈值为1倍,这样就可以自动将4字节数据组合成32位字数据。

10、单次传输或突发传输

单次(Single)传输就是正常的传输方式,在直接模式下(就是不使用FIFO时),只能是单次传输。

要使用突发(Burst)传输,必须使用FIFO模式,可以设置为4个、8个或16个节拍的增量突发传输。这里的节拍数并不是字节数。每个节拍输出的数据大小还与地址递增量大小有关,每个节拍输出字节、半字或字。

为确保数据一致性,形成突发的每一组传输都不可分割。在突发传输序列期间,AHB传输会锁定,并且AHB总线矩阵的仲裁器不解除对DMA主总线的授权。

11、双缓冲区模式

可以为DMA传输启用双缓冲区模式,并自动激活循环模式。双缓冲区模式就是设置两个存储器指针,在每次一个缓冲区传输完成后交换存储器指针,DMA流的工作方式与常规单缓冲区一样。

在双缓冲区模式下,每次传输完一个缓冲区时,DMA控制器都从一个存储器目标切换到另一个存储器目标。这种模式在ADC数据采集时非常有用,例如,为ADC的DMA传输设置两个缓冲区,即Buffer1和Buffer2。DMA交替使用这两个缓冲区存储数据,当DMA使用Buffer1时,程序就可以对已保存在Buffer2中的数据进行处理;DMA完成一个缓冲区的传输,切换使用Buffer2时,程序又可以对Buffer1中的数据进行处理,如此交替往复。

二、DMA的HAL驱动

1、DMA的HAL函数

DMA的HAL驱动程序头文件是stm32g4xx_hal_dma.h和stm32g4xx_hal_dma_ex.h。(STM32 F407单片机是stm32f4xx_hal_dma.h和stm32f4xx_hal_dma_ex.h),主要驱动函数如表:

|------------|--------------------------------|---------------------------|
| 分组 | 函数名 | 功能描述 |
| 初始化 | HAL_DMA_Init() | DMA传输初始化配置 |
| 轮询方式 | HAL_DMA_Start() | 启动DMA传输,不开启DMA中断 |
| 轮询方式 | HAL_DMA_PollForTransfer() | 轮询方式等待DMA传输结束,可设置一个超时等待时间 |
| 轮询方式 | HAL_DMA_Abort() | 中止以轮询方式启动的 DMA传输 |
| 中断方式 | HAL_DMA_Start_IT() | 启动DMA传输,开启DMA中断 |
| 中断方式 | HAL_DMA_Abort_IT() | 中止以中断方式启动的 DMA传输 |
| 中断方式 | HAL_DMA_GetState() | 获取DMA当前状态 |
| 中断方式 | HAL_DMA_IRQHandler() | DMA中断ISR里调用的通用处理函数 |
| 双缓冲区模式 | HAL_DMAEx_MultiBufferStar | 启动双缓冲区DMA,不开启DMA中断 |
| 双缓冲区模式 | HA_DMAEx_MultiBufferStart_IT() | 启动双缓冲区DMA传输,开启DMA中断 |
| 双缓冲区模式 | HAL_DMAEx_ChangeMemory() | 传输过程中改变缓冲区地址 |

DMA是MCU上的一种比较特殊的硬件,它需要与其他外设结合起来使用,不能单独使用。一个外设要使用DMA传输数据,必须先用函数HAL_DMA_Init()进行DMA初始化配置,设置DMA流和通道、传输方向、工作模式(循环或正常)、源和目标数据宽度、DMA流优先级别等参数,然后才可以使用外设的DMA传输函数进行DMA方式的数据传输。

DMA传输有轮询方式和中断方式。如果以轮询方式启动DMA数据传输,则需要调用函数HAL_DMA_PollForTransfer()查询,并等待DMA传输结束。如果以中断方式启动DMA数据传输,则传输过程中DMA流会产生传输完成事件中断。每个DMA流都有独立的中断地址,使用中断方式的DMA数据传输更方便,所以在实际使用DMA时,一般是以中断方式启动DMA传输。

DMA传输还有双缓冲区模式,可用于一些高速实时处理的场合。例如,ADC的DMA传输方向是从外设到存储器的,存储器一端可以设置两个缓冲区,在高速ADC采集时,可以交替使用两个数据缓冲区,一个用于接收ADC的数据,另一个用于实时处理。

2、DMA传输初始化

函数HAL_DMA_Init()用于DMA传输初始化配置,其原型定义如下:

cpp 复制代码
HAL_StatusTypeDef  HAL_DMA_Init(DMA_HandleTypeDef *hdma);

其中,hdma是DMA_HandleTypeDef结构体类型指针。

结构体DMA_HandleTypeDef的成员指针变量Instance要指向一个DMA流的寄存器基址。其成员变量Init是结构体类型DMA_InitTypeDef,它存储了DMA传输的各种属性参数。结构体DMA_HandleTypeDef还定义了多个用于DMA事件中断处理的回调函数指针。

结构体DMA_InitTypeDef的很多成员变量的取值是宏定义常量,具体的取值和意义通过CubeMX的设置和自动生成的代码来解释。

在CubeMX中为外设进行DMA配置后,在自动生成的代码里会有一个DMA_HandleTypeDef结构体类型变量。例如,为USART2的DMA请求USART2_TX配置DMA后,在生成的文件usart.c中有如下的变量定义,称之为DMA流对象变量:

cpp 复制代码
DMA_HandleTypeDef hdma_usart2_rx; //DMA流对象变量

在USART2的外设初始化函数里,为变量hdma_usart2_rx赋值(hdma_usart2_rx.Instance指向一个具体的DMA流的寄存器基址,hdma_usart2_ rx.Init的各成员变量设置DMA传输的各个属性参数);然后执行HAL_DMA_Init(&hdma_usart2_rx)进行DMA传输初始化配置。变量hdma_usart2_rx的基地址指针Instance指向一个DMA流的寄存器基址,它还包含DMA传输的各种属性参数,以及用于DMA事件中断处理的回调函数指针。所以,将用结构体DMA_HandleTypeDef定义的变量称为DMA流对象变量。

3、启动DMA数据传输

完成DMA传输初始化配置后,就可以启动DMA数据传输了。DMA数据传输有轮询方式和中断方式。每个DMA流都有独立的中断地址,有传输完成中断事件,使用中断方式的DMA数据传输更方便。函数HAL_DMA_Start_IT()以中断方式启动DMA数据传输,其原型定义如下:

cpp 复制代码
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma,uint32_t SrcAddress,uint32_t DstAddress,uint32_t DataLength)

其中,hdma是DMA流对象指针,SrcAddress是源地址,DstAddress是目标地址,DataLength是需要传输的数据长度。

在使用具体外设进行DMA数据传输时,一般无须直接调用函数HAL_DMA_Start_IT()启动DMA数据传输,而是由外设的DMA传输函数内部调用函数HAL_DMA_Start IT()启动DMA数据传输。例如,串口传输数据除了有阻塞方式和中断方式外,还有DMA方式。串口以DMA方式发送数据和接收数据的两个函数的原型定义如下:

cpp 复制代码
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart,uint8_t *pData,uint16_t Size)
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart,uint8_t*pData,uint16_t Size)

其中,huart是串口对象指针;pData是数据缓冲区指针,缓冲区是uint8_t类型数组,因为串口传输数据的基本单位是字节;Size是缓冲区长度,单位是字节。USART2使用DMA方式发送一个字符串的示意代码如下:

cpp 复制代码
uint8_t hello1[]="Hello,DMA transmit\n";
HAL_UART_Transmit_DMA(&huart1,hello1,sizeof(hello1));

函数HAL_UART_Transmit_DMA()内部会调用HAL_DMA_Start_IT(),而且会根据USART2关联的DMA流对象的参数自动设置函数HAL_DMA_Start_IT()的输入参数,如源地址、目标地址等。

4、DMA的中断

DMA的中断实际就是DMA流的中断。每个DMA流有独立的中断号,有对应的ISR。DMA中断有多个中断事件源,DMA中断事件类型的宏定义(也就是中断事件使能控制位的宏定义)如下:

cpp 复制代码
#define DMA_IT_TC ((uint32_t)DMA_SxCR_TCIE)    //DMA传输完成中断事件
#define DMA_IT_HT ((uint32_t)DMA_SxCR_HTIE)    //DMA传输半完成中断事件
#define DMA_IT_TE ((uint32_t)DMA_SxCR_TEIE)    //DMA传输错误中断事件
#define DMA_IT_DME ((uint32_t)DMA_SxCR_DMEIE)  //DMA直接模式错误中断事件
#define DMA_IT_FE 0x00000080U                  //DMA FIFO上溢/下溢中断事件

对一般的外设来说,一个事件中断可能对应一个回调函数,这个函数的名称是HAL库固定好了的,例如,UART的发送完成事件中断对应的回调函数名称是HAL_UART_TxCpltCallback()。但是在DMA的HAL驱动程序头文件stm32g4xx_hal_dma.h中,并没有定义这样的回调函数,因为DMA流是要关联不同外设的,所以它的事件中断回调函数没有固定的函数名,而是采用函数指针的方式指向关联外设的事件中断回调函数。DMA流对象的结构体DMA_HandleTypeDef的定义代码中有这些函数指针。

HAL_DMA_IRQHandler()是DMA流中断通用处理函数,在DMA流中断的ISR里被调用。这个函数的原型定义如下,其中的参数hdma是DMA流对象指针:

cpp 复制代码
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)

通过分析函数HAL_DMA_IRQHandler()的源代码,我们整理出DMA流中断事件与DMA流对象(也就是结构体DMA_HandleTypeDef)的回调函数指针之间的关系。

|---------------------|------------------|------------------------------------|
| DMA流中断事件类型宏 | DMA流中断事件 | DMA_HandleTypeDef结构体中的函数指针 |
| DMA_IT_TC | 传输完成中断 | XferCpltCallback |
| DMA_IT_HT | 传输半完成中断 | XferHalfCpltCallback |
| DMA_IT_TE | 传输错误中断 | XferErrorCallback |
| DMA_IT_FE | FIFO错误中断 | 无 |
| DMA_IT_DME | 直接模式错误中断 | 无 |

在DMA传输初始化配置函数HAL_DMA_Init()中,程序不会为DMA流对象的事件中断回调函数指针赋值,一般在外设以DMA方式启动传输时,为这些回调函数指针赋值。例如对于UART,执行函数HAL_UART Transmit_DMA()启动DMA方式发送数据时,就会将串口关联的DMA流对象的函数指针XferCpltCallback指向UART的发送完成事件中断回调函数HAL_UART_TxCpltCallback()。

UART以DMA方式发送和接收数据时,常用的DMA流中断事件与回调函数之间的关系如表所示。注意,这里发生的中断是DMA流的中断,而不是UART的中断,DMA流只是使用了UART的回调函数。特别地,DMA流有传输半完成中断事件(DMA_IT_HT),而UART是没有这种中断事件的,UART的HAL驱动程序中定义的两个回调函数就是为了DMA流的传输半完成事件中断调用的。

|-------------------------|---------------------------|------------------------------|------------------------------------|
| UART的DMA传输函数 | DMA流 中断事件 | DMA流对象的 函数指针 | DMA流事件中断关联的 具体回调函数 |
| HAL_UART_Transmit_DMA() | DMA_IT_TC | XferCpltCallback | HAL_UART_TxCpltCallback() |
| HAL_UART_Transmit_DMA() | DMA_IT_HT | XferHalfCpltCallback | HAL_UART_TxHalfCpltCallback() |
| HAL_UART_Receive_DMA() | DMA_IT_TC | XferCpltCallback | HAL_UART_RxCpltCallback() |
| HAL_UART_Receive_DMA() | DMA_IT_HT | XferHalfCpltCallback | HAL_UART_RxHalfCpltCallback() |

UART使用DMA方式传输数据时,UART的全局中断需要开启,但是UART的接收完成和发送完成中断事件源可以关闭。

三、工程配置

本文实例结合代码详细分析DMA的工作原理,特别是DMA流的中断事件与外设的回调函数之间的关系。

本文实例的工程参考作者的文章:细说STM32单片机USART中断收发RTC实时时间并改善其鲁棒性的方法_stm32串口中断时间-CSDN博客 https://wenchm.blog.csdn.net/article/details/143461698

1、设计目的和通讯协议

同参考文章。

2、工程设置

(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=13,Minutes=23,Seconds=15
  • Calendar Date:可以根据实际填写,比如:Week Day= Monday,Month = November,Date = 11,Year = 24;
  • 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会自动更新波特率的可设置范围。
  • 其它参数默认;

DMA Setting:

(5)NVIC

(6)Project Manager Code Generater

同参考文章。

四、软件代码

1、main.c

cpp 复制代码
 /* USER CODE BEGIN 2 */
  // The global interrupt of USART must be turned on, but the interrupt event can be turned off
  //__HAL_UART_DISABLE_IT(&huart2, UART_IT_TC);	 	//关闭USART2的发送完成IT
  //__HAL_UART_DISABLE_IT(&huart2, UART_IT_RXNE);	//关闭USART2的接收完成IT

  uint8_t	hello1[]="Hello,DMA transmit\n";
  HAL_UART_Transmit_DMA(&huart2,hello1,sizeof(hello1)); //DMA方式transmit

  HAL_UART_Receive_DMA(&huart2, rxBuffer,RX_CMD_LEN);	//DMA方式循环接收
/* USER CODE END 2 */
cpp 复制代码
 /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

	//这句很重要,目的总是连续显示RTC时间
	//没有这句,仅仅在程序下载后第�?次运行连续显示RTC时间,发送了指令后,
	//只显示发送的指令字符串,不再显示RTC时间,这显然不符合设计目的�??
	if(isUploadTime == 1)
	{
	 	HAL_RTCEx_WakeUpTimerEventCallback(&hrtc);
	}
  }
/* USER CODE END 3 */

while循环里的代码经过测试作者是必须的,如果没有,第一次下载的时候,是能够实现RTC时间连续显示的,但是MCU重启后,是不能连续下载的。具体到个人的应用,到底要不要这段程序,要根据个人的实测结果来决定。

2、usart.h

cpp 复制代码
/* USER CODE BEGIN Includes */
#define	RX_CMD_LEN	5		    // string length
extern uint8_t  rxBuffer[];     // Serial port receiving data buffer

extern	uint8_t	isUploadTime;	// upload RTCtime switch
/* USER CODE END Includes */
cpp 复制代码
/* USER CODE BEGIN Prototypes */
void updateRTCTime();
/* USER CODE END Prototypes */

3、usart.c

cpp 复制代码
* USER CODE BEGIN 0 */
#include "rtc.h"
#include "dma.h"
#include <string.h>
#include <ctype.h>

uint8_t	proBuffer[10];	//用于处理数据, #H12; #M23; #S43;
uint8_t	rxBuffer[10];	//接收缓存数据, #H12; #M23; #S43;
uint8_t	isUploadTime=1;	//是否上传时间数据

unsigned char hello1[]="Invalid command\n";
unsigned char hello2[]="Invalid data\n";

/* USER CODE END 0 */
cpp 复制代码
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart->Instance == USART2)
	{
		for(uint16_t i=0;i<RX_CMD_LEN;i++)
			proBuffer[i] = rxBuffer[i];

		// Upload the received command string and must be delayed,
		// otherwise updateRTCTime() will error.
		HAL_UART_Transmit_DMA(huart,rxBuffer,RX_CMD_LEN+1);
		HAL_Delay(10);


		// Identify the start_bit is '#' and the end_bit is ';'or not.
		// Determine whether the number of characters received is equal to 5.
		if (rxBuffer[0] != '#' ||  rxBuffer[RX_CMD_LEN -1] != ';')
		{
			HAL_UART_Init(&huart2);	//重启串口
			HAL_UART_Transmit(&huart2,hello1,sizeof(hello1),200);

			memset(rxBuffer, '\0', sizeof(rxBuffer));
			memset(proBuffer, '\0', sizeof(proBuffer));

			return;//已经发生错误,自然退出这个回调函数
		}

		// Identify the data_bit is digits or not
		if (isalpha(proBuffer[2])  || isalpha(proBuffer[3]))
		{
			HAL_UART_Init(&huart2);
			HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);

			memset(rxBuffer, '\0', sizeof(rxBuffer));
			memset(proBuffer, '\0', sizeof(proBuffer));
			return;
		}

		updateRTCTime();	//指令解析处理
	}
}

//根据串口接收的指令字符串进行update
void updateRTCTime()
{
	uint8_t	timeSection=proBuffer[1];	//类型字符, "#H12;"
	uint8_t	tmp10=proBuffer[2]-0x30;	//十位
	uint8_t	tmp1 =proBuffer[3]-0x30;		//个位
	uint8_t	val=10*tmp10+tmp1;

		//update RTCtime
		RTC_TimeTypeDef sTime;
		RTC_DateTypeDef sDate;

		if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
		{
			// After calling HAL_RTC_GetTime(),
			// you must call HAL_RTC_GetDate() to continuously update Date and Time.
			HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

			switch (timeSection)
			{
				case 'H': // Modify hours
					{
						if(val <= 24)
							sTime.Hours = val;
						else
							{
								HAL_UART_Init(&huart2);
								HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
								memset(proBuffer, '\0', sizeof(proBuffer));
								return;
							}
					}
					break;
				case 'M': // Modify minutes
					{
						if(val <= 60)
							sTime.Minutes = val;
						else
						{
							HAL_UART_Init(&huart2);
							HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
							memset(proBuffer, '\0', sizeof(proBuffer));
							return;
						}
					}
					break;
				case 'S': // Modify seconds
					{
						if(val <= 60)
							sTime.Seconds = val;
						else
						{
							HAL_UART_Init(&huart2);
							HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
							memset(proBuffer, '\0', sizeof(proBuffer));
							return;
						}
					}
					break;
				case 'U':
					{
						if( tmp1 == 0)
						{
							isUploadTime = 0;//pause
							return;
						}
						else
							isUploadTime = 1; //resume
						}
					break;
				default: // If it is not H, M, S, U then return
					{
						HAL_UART_Init(&huart2);
						HAL_UART_Transmit(&huart2,hello1,sizeof(hello1),200);
						memset(proBuffer, '\0', sizeof(proBuffer));
					}
					return;
			}

			//Set the RTC time and will affect the next RTC wake-up interrupt.
			HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
		}
}
/* USER CODE END 1 *//* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart->Instance == USART2)
	{
		for(uint16_t i=0;i<RX_CMD_LEN;i++)
			proBuffer[i] = rxBuffer[i];

		// Upload the received command string and must be delayed,
		// otherwise updateRTCTime() will error.
		HAL_UART_Transmit_DMA(huart,rxBuffer,RX_CMD_LEN+1);
		HAL_Delay(10);


		// Identify the start_bit is '#' and the end_bit is ';'or not.
		// Determine whether the number of characters received is equal to 5.
		if (rxBuffer[0] != '#' ||  rxBuffer[RX_CMD_LEN -1] != ';')
		{
			HAL_UART_Init(&huart2);	//重启串口
			HAL_UART_Transmit(&huart2,hello1,sizeof(hello1),200);

			memset(rxBuffer, '\0', sizeof(rxBuffer));
			memset(proBuffer, '\0', sizeof(proBuffer));

			return;//已经发生错误,自然退出这个回调函数
		}

		// Identify the data_bit is digits or not
		if (isalpha(proBuffer[2])  || isalpha(proBuffer[3]))
		{
			HAL_UART_Init(&huart2);
			HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);

			memset(rxBuffer, '\0', sizeof(rxBuffer));
			memset(proBuffer, '\0', sizeof(proBuffer));
			return;
		}

		updateRTCTime();	//指令解析处理
	}
}

//根据串口接收的指令字符串进行update
void updateRTCTime()
{
	uint8_t	timeSection=proBuffer[1];	//类型字符, "#H12;"
	uint8_t	tmp10=proBuffer[2]-0x30;	//十位
	uint8_t	tmp1 =proBuffer[3]-0x30;		//个位
	uint8_t	val=10*tmp10+tmp1;

		//update RTCtime
		RTC_TimeTypeDef sTime;
		RTC_DateTypeDef sDate;

		if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
		{
			// After calling HAL_RTC_GetTime(),
			// you must call HAL_RTC_GetDate() to continuously update Date and Time.
			HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

			switch (timeSection)
			{
				case 'H': // Modify hours
					{
						if(val <= 24)
							sTime.Hours = val;
						else
							{
								HAL_UART_Init(&huart2);
								HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
								memset(proBuffer, '\0', sizeof(proBuffer));
								return;
							}
					}
					break;
				case 'M': // Modify minutes
					{
						if(val <= 60)
							sTime.Minutes = val;
						else
						{
							HAL_UART_Init(&huart2);
							HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
							memset(proBuffer, '\0', sizeof(proBuffer));
							return;
						}
					}
					break;
				case 'S': // Modify seconds
					{
						if(val <= 60)
							sTime.Seconds = val;
						else
						{
							HAL_UART_Init(&huart2);
							HAL_UART_Transmit(&huart2,hello2,sizeof(hello2),200);
							memset(proBuffer, '\0', sizeof(proBuffer));
							return;
						}
					}
					break;
				case 'U':
					{
						if( tmp1 == 0)
						{
							isUploadTime = 0;//pause
							return;
						}
						else
							isUploadTime = 1; //resume
						}
					break;
				default: // If it is not H, M, S, U then return
					{
						HAL_UART_Init(&huart2);
						HAL_UART_Transmit(&huart2,hello1,sizeof(hello1),200);
						memset(proBuffer, '\0', sizeof(proBuffer));
					}
					return;
			}

			//Set the RTC time and will affect the next RTC wake-up interrupt.
			HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
		}
}
/* USER CODE END 1 */

usart.c的程序里包含异常情况下的容错处理。

4、rtc.c

cpp 复制代码
/* 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 */
cpp 复制代码
/* USER CODE BEGIN 1 */
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);
		//显示 时间  hh:mm:ss
		uint8_t  timeStr[20];	//RTCtime string
		sprintf((char *)timeStr,"%2d:%2d:%2d\n",sTime.Hours,sTime.Minutes,sTime.Seconds);

		if ((isUploadTime ==1) && ((uint8_t)sTime.Seconds != second))
		{
			second = (uint8_t)sTime.Seconds;

			HAL_UART_Transmit_DMA(&huart2,timeStr,strlen ((const char *)(timeStr)));	// send updated data.
			HAL_Delay(10);  //若要上位机正常显示换行,必须要有这个延时
		}
	}
}
/* USER CODE END 1 */

五、运行与调试

下载,运行,首先显示字符串"Hello,DMA transmit",然后连续显示时间,间隔1s。下面根据不同的指令输入情况,展示运行结果。

1、合规的指令

输入正确的时、分、秒、暂停、恢复、及再次输入正确的指令:

2、proBuffer[0]不是#或proBuffer[4]不是;

输入字符串长度=5,但首字符≠#或结束字符≠;时,能正常进行容错处理并消息提示,可以继续输入正确的指令:

3、指令长度小于5

输入字符串的长度<5,第一次输入没有显示,第二次及以后的输入有显示并错误提示,虽然还显示RTC时间,但是并没有改变RTC时间。直至累计输入的字符是5的倍数以后,才跳出错误循环,此后输入正确的指令后,执行并显示正确的结果。

比如输入#H8;,直到输入第5次时,才跳出错误循环,此后,输入#S34;,正确修改秒并显示,输入U00,暂停,U01恢复。

4、仅proBuffer[2]或proBuffer[3]不是数字

显示数据错误。

5、 ';'位于proBuffer[2]或proBuffer[3]位置

显示指令错误。

6、proBuffer[2]和proBuffer[3]数字超范围

显示数据错误。

7、指令长度大于5

当输入的指令长度大于5时,显示指令错误并不改变RTC时间,直到累计输入的指令的长度恰好等于5时,跳出纠错循环回到正确数据处理的状态,此时,如果输入正确的指令,将会修改RTC时间并连续显示。

比如,输入#H123;,指令长度=6,直到连续输入5次后,再输入正确的指令比如输入#S34;,正确地修改秒并连续显示,输入#U00,暂停,输入#U01,恢复。

特别地,当输入指令的长度恰好是5的倍数,比如10,那么每次输入都有出错提示,并且每次输入之后,都可以继续输入并执行正确的指令。

当输入的指令的长度不等于5时,程序容错能力是比较弱的,鲁棒性并不明显。这是因为串口接收设置数据长度=5导致的,rxBuffer[5]以后内容并不能被memset()清空,残余的数据影响了紧邻的下一次Recieve。

当串口接收设置数据长度=1时(作者会在另一文章中给一分享),容错程序会较好地解决此类情况,程序的鲁棒性变得很好。

相关推荐
美式小田1 小时前
单片机学习笔记 3. LED灯流水灯
笔记·单片机·嵌入式硬件·学习
美式小田2 小时前
单片机学习笔记 4. 蜂鸣器滴~滴~滴~
笔记·单片机·嵌入式硬件·学习
沐欣工作室_lvyiyi4 小时前
基于单片机的智能奶茶机设计(论文+源码)
stm32·单片机·嵌入式硬件·物联网·毕业设计·智能家居
非概念5 小时前
STM32学习笔记-----I2C协议
笔记·stm32·嵌入式硬件·学习
Qingniu017 小时前
【青牛科技】电动工具直流调速专用集成电路GS069,具有电源电压范围宽、功耗小、抗干扰能力强等特性
科技·单片机·嵌入式硬件·实时音视频·储能
小黄人软件7 小时前
【android USB 串口通信助手】stm32 源码demo 单片机与手机通信 Android studio 20241118
android·stm32·单片机
唯创知音8 小时前
WTV芯片在智能电子锁语音留言上的应用方案解析
人工智能·单片机·物联网·智能家居·语音识别
艾格北峰8 小时前
STM32 BootLoader 刷新项目 (十一) Flash写操作-命令0x57
单片机
嵌新程9 小时前
day04(单片机高级)硬件基础
stm32·单片机·嵌入式硬件·u575