细说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
(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时(作者会在另一文章中给以分享),容错程序会较好地解决此类情况,程序的鲁棒性变得很好。
相关文章:
细说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、单次传输或突…...
【Unity/Animator动画系统】多层动画状态机实现角色的基本移动
文章目录 前言实现顶层地面状态四方向混合树计算动画所需参数 空中状态分层动画 前言 最近打算做个Rougelike RPG 塔科夫 混搭风格的冒险游戏。暂且就当是一个有随机元素,有基地,死亡会掉落物品的近战塔科夫罢。 花了三天时间,整合了Mixa…...
每日算法一练:剑指offer——栈与队列篇(1)
1.图书整理II 读者来到图书馆排队借还书,图书管理员使用两个书车来完成整理借还书的任务。书车中的书从下往上叠加存放,图书管理员每次只能拿取书车顶部的书。排队的读者会有两种操作: push(bookID):把借阅的书籍还到图书馆。pop…...
【Java】ArrayList与LinkedList详解!!!
目录 一🌞、List 1🍅.什么是List? 2🍅.List中的常用方法 二🌞、ArrayList 1🍍.什么是ArrayList? 2🍍.ArrayList的实例化 3🍍.ArrayList的使用 4🍍.ArrayList的遍…...
怎么用VIM查看UVM源码
利用ctags工具可以建立源码的索引表,在使用VIM或其他文本编辑器时,就可以跳转查看所调用的UVM或VIP的funtcion/task/class等源码了。 首先需要确认ctags安装,一般安装VIM后都有,如果没有可以手动安装。在VIM中可以输入:help ctag…...
数据结构C语言描述3(图文结合)--双链表、循环链表、约瑟夫环问题
前言 这个专栏将会用纯C实现常用的数据结构和简单的算法;有C基础即可跟着学习,代码均可运行;准备考研的也可跟着写,个人感觉,如果时间充裕,手写一遍比看书、刷题管用很多,这也是本人采用纯C语言…...
第二十五章 TCP 客户端 服务器通信 - TCP 设备的 READ 命令
文章目录 第二十五章 TCP 客户端 服务器通信 - TCP 设备的 READ 命令TCP 设备的 READ 命令READ 修改 $ZA 和 $ZB$ZA 和 READ 命令 第二十五章 TCP 客户端 服务器通信 - TCP 设备的 READ 命令 TCP 设备的 READ 命令 从服务器或客户端发出 READ 命令以读取客户端或服务器设置的…...
【C++】哈希表的实现详解
哈希表的实现详解 一、哈希常识1.1、哈希概念1.2、哈希冲突1.3、哈希函数(直接定执 除留余数)1.4、哈希冲突解决闭散列(线性探测 二次探测)开散列 二、闭散列哈希表的模拟实现2.1、框架2.2、哈希节点状态的类2.3、哈希表的扩容2…...
高阶C语言之五:(数据)文件
目录 文件名 文件类型 文件指针 文件的打开和关闭 文件打开模式 文件操作函数(顺序) 0、“流” 1、字符输出函数fputc 2、字符输入函数fgetc 3、字符串输出函数fputs 4、 字符串输入函数fgets 5、格式化输入函数fscanf 6、格式化输出函数fpr…...
服务器上部署并启动 Go 语言框架 **GoZero** 的项目
要在服务器上部署并启动 Go 语言框架 **GoZero** 的项目,下面是一步步的操作指南: ### 1. 安装 Go 语言环境 首先,确保你的服务器上已安装 Go 语言。如果还没有安装,可以通过以下步骤进行安装: #### 1.1 安装 Go 语…...
【Java SE 】继承 与 多态 详解
🔥博客主页🔥:【 坊钰_CSDN博客 】 欢迎各位点赞👍评论✍收藏⭐ 目录 1. 继承 1.1 继承的原因 1.2 继承的概念 1.3 继承的语法 2. 子类访问父类 2.1 子类访问父类成员变量 2.1.1 子类与父类不存在同名成员变量 2.1.2 子类…...
【大语言模型】ACL2024论文-16 基于地图制图的罗马尼亚自然语言推理语料库的新型课程学习方法
【大语言模型】ACL2024论文-16 基于地图制图的罗马尼亚自然语言推理语料库的新型课程学习方法 目录 文章目录 【大语言模型】ACL2024论文-16 基于地图制图的罗马尼亚自然语言推理语料库的新型课程学习方法目录摘要:研究背景:问题与挑战:如何解…...
秋招大概到此结束了
1、背景 学院本,软工,秋招只有同程,快手和网易面试,后两家kpi(因为面试就很水),秋招情况:哈啰(实习转正ing),同程测开offer。 2、走测开的原因 很…...
华为OD机试真题---字符串化繁为简
华为OD机试真题中的“字符串化繁为简”题目是一个涉及字符串处理和等效关系传递的问题。以下是对该题目的详细解析: 一、题目描述 给定一个输入字符串,字符串只可能由英文字母(a~z、A~Z)和左右小括号((、)࿰…...
概念解读|K8s/容器云/裸金属/云原生...这些都有什么区别?
随着容器技术的日渐成熟,不少企业用户都对应用系统开展了容器化改造。而在容器基础架构层面,很多运维人员都更熟悉虚拟化环境,对“容器圈”的各种概念容易混淆:容器就是 Kubernetes 吗?容器云又是什么?容器…...
初识Arkts
创建对象: 类: 类声明引入一个新类型,并定义其字段、方法和构造函数。 定义类后,可以使用关键字new创建实例 可以使用对象字面量创建实例 在以下示例中,定义了Person类,该类具有字段name和surname、构造函…...
基本的SELECT语句
1.SQL概述 SQL(Structured Query Language)是一种用于管理和操作关系数据库的编程语言。它是一种标准化的语言,用于执行各种数据库操作,包括创建、查询、插入、更新和删除数据等。 SQL语言具有简单、易学、高效的特点,…...
51c自动驾驶~合集30
我自己的原文哦~ https://blog.51cto.com/whaosoft/12086789 #跨越微小陷阱,行动更加稳健 目前四足机器人的全球市场上,市场份额最大的是哪个国家的企业?A.美国 B.中国 C.其他 波士顿动力四足机器人 云深处 绝影X30 四足机器人 …...
Python Tutor网站调试利器
概述 本文主要是推荐一个网站:Python Tutor. 网站首页写道: Online Compiler, Visual Debugger, and AI Tutor for Python, Java, C, C++, and JavaScript Python Tutor helps you do programming homework assignments in Python, Java, C, C++, and JavaScript. It contai…...
h5小游戏实现获取本机图片
h5小游戏实现获取本机图片 本文使用cocos引擎 1.1 需求 用户通过文件选择框选择图片。将图片内容转换为Cocos Creator的纹理 (cc.Texture2D),将纹理设置到 cc.SpriteFrame 并显示到节点中。 1.2 实现步骤 创建文件输入框用于获取文件 let input document.createElement(&quo…...
前端 javascript a++和++a的区别
前端 javascript a和a的区别 a 是先执行表达式后再自增,执行表达式时使用的是a的原值。a是先自增再执行表达示,执行表达式时使用的是自增后的a。 var a0 console.log(a); // 输出0 console.log(a); // 输出1var a0 console.log(a); // 输出1 console.l…...
OceanBase V4.x应用实践:如何排查表被锁问题
DBA在日常工作中常常会面临以下两种常见情况: 业务人员会提出问题:“表被锁了,导致业务受阻,请帮忙解决。” 业务人员还会反馈:“某个程序通常几秒内就能执行完毕,但现在却运行了好几分钟,不清楚…...
ctfshow-web入门-SSRF(web351-web360)
目录 1、web351 2、web352 3、web353 4、web354 5、web355 6、web356 7、web357 8、web358 9、web359 10、web360 1、web351 看到 curl_exec 函数,很典型的 SSRF 尝试使用 file 协议读文件: urlfile:///etc/passwd 成功读取到 /etc/passwd 同…...
【日常记录-Git】如何为post-checkout脚本传递参数
1. 简介 在Git中,post-checkout 钩子是一个在git checkout 或git switch命令成功执行后自动调用的脚本。该脚本不接受任何来自Git命令的直接参数,因为Git设计该钩子是为了在特定的版本控制操作后执行一些预定义的任务,而不是作为一个通用的脚…...
《机器人控制器设计与编程》考试试卷**********大学2024~2025学年第(1)学期
消除误解,课程资料逐步公开。 复习资料: Arduino-ESP32机器人控制器设计练习题汇总_arduino编程语言 题-CSDN博客 试卷样卷: 开卷考试,时间: 2024年11月16日 001 002 003 004 005 ……………………装………………………...
后台管理系统(开箱即用)
很久没有更新博客了,给大家带上一波福利吧,大佬勿扰 现在市面上流行的后台管理模板很多,若依,芋道等,可是这些框架对我们来说可能会有点重,所以我自己从0到1写了一个后台管理模板,你们使用时候可扩展性也会更高 项目主要功能: 成员管理,部门管理&#…...
5G CPE与4G CPE的主要区别有哪些
什么是CPE? CPE是Customer Premise Equipment(客户前置设备)的缩写,也可称为Customer-side Equipment、End-user Equipment或On-premises Equipment。CPE通常指的是位于用户或客户处的网络设备或终端设备,用于连接用户…...
量化交易系统开发-实时行情自动化交易-4.1.3.A股平均趋向指数(ADX)实现
19年创业做过一年的量化交易但没有成功,作为交易系统的开发人员积累了一些经验,最近想重新研究交易系统,一边整理一边写出来一些思考供大家参考,也希望跟做量化的朋友有更多的交流和合作。 接下来继续说说A股平均趋向指数实现。 …...
tcp的网络惊群问题
1. SO_REUSEPORT 可以解决epoll的惊群问题 但是,现在的 TCP Server,一般都是 多进程多路IO复用(epoll) 的并发模型,比如我们常用的 nginx 。如果使用 epoll 去监听 accept socket fd 的读事件,当有新连接建立时,所有进…...
云原生之运维监控实践-使用Prometheus与Grafana实现对Nginx和Nacos服务的监测
背景 如果你要为应用程序构建规范或用户故事,那么务必先把应用程序每个组件的监控指标考虑进来,千万不要等到项目结束或部署之前再做这件事情。——《Prometheus监控实战》 去年写了一篇在Docker环境下部署若依微服务ruoyi-cloud项目的文章,当…...
微网站是用什么代码制作/沈阳网站关键词排名
重要 大环境对于我们能力要求越来越高,医学专家又说今年冬天新冠肺炎将“席卷重来”。 如果疫情再次爆发,势必将再次影响企业的正常运作,一波裁员浪潮你又能否抗住? 不管如何,明年金三银四又是一波跳槽时机…...
手表怎么在网站做推广/南昌seo方案
Silverlight 有没有对 FLV 视频提供支持? 好吧,所有的开发人员都是懒惰的,ME2。先查查微软的文档吧,FLV 视频是如此的普及,没准儿微软已经在 Silverlight 中提供了对 FLV 视频的支持。 结果,微软在 Silve…...
如何把网站放到域名上/扫一扫识别图片
本教程为2018年测试后写出,可能不适用现在得windows版本,因为现在window可以设置PIN,Microsoft Remote Desktop未测试可否在设置PIN情况下使用 第一步:设置电脑端 右键我的电脑,点击属性,点击右面的远程设置…...
网站维护怎么做/网址大全浏览器
http://www.zi-han.net/theme/hplus/?v4.1 http://webapplayers.com/inspinia_admin-v2.5/ http://baijunyao.com/article/67...
人气最旺的传奇手游/厦门seo排名外包
在应用有frameset或者iframe的页面时,parent是父窗口,top是最顶级父窗口(有的窗口中套了好几层frameset或者iframe),self是当前窗口, opener是用open方法打开当前窗口的那个窗口。 window.self 功能&#x…...
免费做网站怎么做网站/宁波网站优化公司价格
首先打开idea 搜索:RunDashboard 添加配置: <option name"configurationTypes"><set><option value"SpringBootApplicationConfigurationType" /></set></option>...