目录
一、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
(4)USART2
(5)NVIC
(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]位置
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支持:
● 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传输初始化配置,其原型定义如下:
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流对象变量:
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数据传输,其原型定义如下:
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方式发送数据和接收数据的两个函数的原型定义如下:
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方式发送一个字符串的示意代码如下:
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中断事件类型的宏定义(也就是中断事件使能控制位的宏定义)如下:
#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流对象指针:
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() |
DMA_IT_HT | XferHalfCpltCallback | HAL_UART_TxHalfCpltCallback() | |
HAL_UART_Receive_DMA() | DMA_IT_TC | XferCpltCallback | HAL_UART_RxCpltCallback() |
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
/* 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的接收完成ITuint8_t hello1[]="Hello,DMA transmit\n";HAL_UART_Transmit_DMA(&huart2,hello1,sizeof(hello1)); //DMA方式transmitHAL_UART_Receive_DMA(&huart2, rxBuffer,RX_CMD_LEN); //DMA方式循环接收
/* USER CODE END 2 */
/* 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
/* USER CODE BEGIN Includes */
#define RX_CMD_LEN 5 // string length
extern uint8_t rxBuffer[]; // Serial port receiving data bufferextern uint8_t isUploadTime; // upload RTCtime switch
/* USER CODE END Includes */
/* USER CODE BEGIN Prototypes */
void updateRTCTime();
/* USER CODE END Prototypes */
3、usart.c
* 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 */
/* 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 notif (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 RTCtimeRTC_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;//pausereturn;}elseisUploadTime = 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 notif (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 RTCtimeRTC_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;//pausereturn;}elseisUploadTime = 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
/* 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 */
/* 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:ssuint8_t timeStr[20]; //RTCtime stringsprintf((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时(作者会在另一文章中给一分享),容错程序会较好地解决此类情况,程序的鲁棒性变得很好。