目录
[(3) RTC](#(3) RTC)
[(6)Project Manager Code Generater](#(6)Project Manager Code Generater)
2、proBuffer[0]不是#或proBuffer[4]不是;
4、仅proBuffer[2]或proBuffer[3]不是数字
[5、 ';'位于proBuffer[2]或proBuffer[3]位置](#5、 ';'位于proBuffer[2]或proBuffer[3]位置)
6、proBuffer[2]和proBuffer[3]数字超范围
本文通过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时(作者会在另一文章中给一分享),容错程序会较好地解决此类情况,程序的鲁棒性变得很好。