【ARM Cortex-M 开发实战指南(基础篇)】第11章 CPU的高级代理-DMA

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

11.1 DMA工作原理

11.1.1 DMA介绍

DMA (Direct Memory Access,直接存储器存取),是一种可以大大减轻 CPU 工作量的数据存取方式,DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,因而被广泛地使用。早在 8086 的应用中就已经有 Intel 的 8237 这种典型的 DMA 控制器,而 STM32 的 DMA 则是以类似外设的形式添加到 Cortex 内核之外的。可以说,DMA就是CPU的高级代理。

在硬件系统中,主要由 CPU(内核)、外设、内存(SRAM)、总线等结构组成,数据经常要在内存与外设之间转移,或从外设 A 转移到外设 B。例如 :当 CPU 需要处理由 ADC 外设采集回来的数据时,CPU 首先要把数据从 ADC外设的寄存器读取到内存中(变量),然后进行运算处理,这是一般的处理方法。

在转移数据的过程中会占用 CPU 十分宝贵的资源,所以我们希望 CPU 更多地被用在数据运算或响应中断之中,而数据转移的工作交由其他部件完成,是不是能够更好的利用CPU的资源呢?DMA 正是为 CPU 分担了数据转移的工作。因为 DMA 的存在 CPU 才被解放出来,它可以在 DMA 转移数据的过程中同时进行数据运算、响应中断,大大提高效率。再次总结下DMA,DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节。

v7FqsS.md.png

DMA数据传输主要涉及三种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。三种情况的数据传输分别时:外设到内存、内存到外设、内存到内存。

11.1.2 Cortex-M 的DMA主要特征

Cortex-M系列的MCU一般有两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。

要使用 DMA,需要确定一系列的控 制参数,如外设数据的地址、内存地址、传输方向等,在开启 DMA 传输前还要先发出 DMA 请求。

DMA的主要特点如下:

● 12个独立的可配置的通道(请求):DMA1有7个通道,DMA2有5个通道
● 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
● 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推) 。
● 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
● 支持循环的缓冲器管理
● 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
● 存储器和存储器间的传输,外设和存储器、存储器和外设之间的传输
● 闪存、SRAM、外设的SRAM、APB1 、APB2和AHB外设均可作为访问的源和目标。
● 可编程的数据传输数目:最大为65535

11.1.3 Cortex-M的DMA请求映像

从外设(TIMx[x=1、 2、 3、 4]、 ADC1、 SPI1、 SPI/I2S2、 I2Cx[x=1、 2]和USARTx[x=1、 2、 3])产生的7个请求,通过逻辑或输入到DMA1控制器,这意味着同时只能有一个请求有效。参见下图的DMA1请求映像。

v7FXZQ.md.png

外设的DMA请求,可以通过设置相应外设寄存器中的控制位,被独立地开启或关闭。

v7FvIs.md.png

从外设(TIMx[5、 6、 7、 8]、 ADC3、 SPI/I2S3、 UART4、 DAC通道1、 2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,这意味着同时只能有一个请求有效。参见下图的DMA2请求映像。

v7Fzin.md.png

外设的DMA请求,可以通过设置相应外设寄存器中的DMA控制位,被独立地开启或关闭。

v7kpR0.md.png

注意: DMA2控制器及相关请求仅存在于大容量产品和互联型产品中。

当外设发出DMA请求后,仲裁器根据通道请求的优先级来启动外设/存储器的访问。优先权管理分2个阶段:

● 软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:最高优先级。高优先级,中等优先级,低优先级。
● 硬件:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4。
注意: 在大容量产品和互联型产品中, DMA1控制器拥有高于DMA2控制器的优先级

11.1.4 STM32的DMA工作过程

下图为STM32的DMA的系统框图。

v7kisU.md.png

我们可以看到STM32内核,存储器,外设及DMA的连接,这些硬件最终通过各种各样的线连接到总线矩阵中,硬件结构之间的数据转移都经过总线矩阵的协调,使各个外设和谐的使用总线来传输数据。
如果不使用DMA,CPU传输数据还要以内核作为中转站,比如要将USART1的数据转移到SRAM中,这个过程是这样的:

第一步:内核通过DCode经过总线矩阵协调,从获取AHB存储的外设USART1的数据。
第二步:内核再通过DCode经过总线矩阵协调把数据存放到内存SRAM中。

v7kFLF.md.png

如果使用DMA的话,数据传输需要以下步骤:

1.DMA传输时外设对DMA控制器发出请求。DMA控制器收到请求,触发DMA工作。
2.DMA控制器从AHB外设获取USART1的数据,存储到DMA通道中
3.DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设USART1的数据经由DMA通道存放到SRAM中,这个数据的传输过程中,完全不需要内核的参与,也就是不需要CPU的参与。

v7kAZ4.md.png

在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。DMA传输结束,如果有更多的请求时,外设可以启动下一个周期。

DMA控制器数据流都能够提供源和目标之间的单向传输链路。每个数据流配置后都可以执行以下事务:

●常规类型事务:存储器到外设、外设到存储器或存储器到存储器的传输。
●双缓冲区类型事务:使用存储器的两个存储器指针的双缓冲区传输(当 DMA 正在进行自/至缓冲区的读/写操作时,应用程序可以进行至/自其它缓冲区的写/读操作)。

要传输的数据量(多达 65535)可以编程,并与连接到外设 AHB 端口的外设(请求 DMA 传输)的源宽度相关。每个事务完成后,包含要传输的数据项总量的寄存器都会递减。

总之,每次DMA传送由3个操作组成:

1.从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
2.存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
3.执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目。

11.2 DMA的寄存器描述

第一个是 DMA 中断状态寄存器( DMA_ISR)。该寄存器的各位描述如下图所示。我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx,即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。

v7kVo9.md.png

每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断。

v7keiR.png

使能开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx位,即数据流 x 的 DMA 传输完成与否标志。

第二个是 DMA 中断标志清除寄存器( DMA_IFCR)。该寄存器的各位描述如下图所示。DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后,我们必须通过向该位寄存器对应的位写入 0 来清除。

v7kuz6.md.png

第三个是 DMA 通道 x 配置寄存器( DMA_CCRx)( x=1~7,下同)。该寄存器的我们在这里就不贴出来了,见《STM32 参考手册》。该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。

第四个是 DMA 通道 x 传输数据量寄存器( DMA_CNDTRx)。这个寄存器控制 DMA 通道 x 的每次传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。

第五个是 DMA 通道 x 的外设地址寄存器( DMA_CPARx)。该寄存器用来存储 STM32 外设的地址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使用其他外设,就修改成相应外设的地址就行了。

最后一个是 DMA 通道 x 的存储器地址寄存器( DMA_CMARx),该寄存器和 DMA_CPARx 差不多,是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在DMA_CMARx 中写入&SendBuff 就可以了。

11.3 DMA具体代码实现-标准库

11.3.1 DMA发送数据

本节我们要用到串口1 的发送,属于 DMA1 的通道 4,接下来我们就介绍库函数下 DMA1 通道 4 的配置步骤:
1)使能 DMA 时钟

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟

2)初始化 DMA 通道 4 参数
前面讲解过, DMA 通道配置参数种类比较繁多,包括内存地址,外设地址,传输数据长度,数据宽度,通道优先级等等。这些参数的配置在库函数中都是在函数 DMA_Init 中完成,下面我们看看函数定义:

void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,
DMA_InitTypeDef* DMA_InitStruct)

函数的第一个参数是指定初始化的 DMA 通道号,这个很容易理解,下面我们主要看看第二个参数。跟其他外设一样,同样是通过初始化结构体成员变量值来达到初始化的目的,下面我们来看看 DMA_InitTypeDef 结构体的定义:

typedef struct
{
uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_MemoryBaseAddr;
uint32_t DMA_DIR;
uint32_t DMA_BufferSize;
uint32_t DMA_PeripheralInc;
uint32_t DMA_MemoryInc;
uint32_t DMA_PeripheralDataSize;
uint32_t DMA_MemoryDataSize;
uint32_t DMA_Mode;
uint32_t DMA_Priority;
uint32_t DMA_M2M;
}DMA_InitTypeDef;

这个结构体的成员比较多,这里我们一一做个介绍。

第一个参数 DMA_PeripheralBaseAddr 用来设置 DMA 传输的外设基地址,比如要进行串口DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为&USART1->DR。

第二个参数 DMA_MemoryBaseAddr为内存基地址,也就是我们存放 DMA传输数据的内存地址。

第三个参数 DMA_DIR 设置数据传输方向, 决定是从外设读取数据到内存还送从内存读取数据发送到外设, 也就是外设是源地还是目的地, 这里我们设置为从内存读取数据发送到串口,所以外设自然就是目的地了,所以选择值为 DMA_DIR_PeripheralDST。

第四个参数 DMA_BufferSize 设置一次传输数据量的大小,这个很容易理解。

第五个参数 DMA_PeripheralInc 设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 1,这里因为我们是一直往固定外设地址&USART1->DR发送数据,所以地址不递增,值为 DMA_PeripheralInc_Disable;

第六个参数 DMA_MemoryInc 设置传输数据时候内存地址是否递增。这个参数和DMA_PeripheralInc 意思接近,只不过针对的是内存。 这里我们的场景是将内存中连续存储单元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为 DMA_MemoryInc_Enable。

第七个参数 DMA_PeripheralDataSize 用来设置外设的数据长度是为字节传输( 8bits),半字传输(16bits)还是字传输 (32bits) ,这里我们是8位字节传输,所以值设置为DMA_PeripheralDataSize_Byte。

第八个参数 DMA_MemoryDataSize 是用来设置内存的数据长度,和第七个参数意思接近,这里我们同样设置为字节传输 DMA_MemoryDataSize_Byte。

第九个参数 DMA_Mode 用来设置 DMA 模式是否循环采集,也就是说,比如我们要从内存中采集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内存的第一个地址采集,如此循环。这里我们设置为一次连续采集完成之后不循环。所以设置值为 DMA_Mode_Normal。在我们下面的实验中,如果设置此参数为循环采集,那么你会看到串口不停的打印数据,不会中断,大家在实验中可以修改这个参数测试一下。循环模式可用于处理循环缓冲区和连续数据流(例如 ADC 扫描模式)。可以使用 DMA_SxCR 寄存器中的 CIRC 位使能此特性。当激活循环模式时,要传输的数据项的数目在数据流配置阶段自动用设置的初始值进行加载,并继续响应 DMA 请求。

第十个参数是设置 DMA 通道的优先级,有低,中,高,超高四种模式,这个在前面讲解过,这里我们设置优先级别为中级,所以值为 DMA_Priority_Medium。如果要开启多个通道,那么这个值就非常有意义。

第十一个参数 DMA_M2M设置是否是存储器到存储器模式传输, 这里我们选择DMA_M2M_Disable。

这里我们给出上面场景的实例代码:

DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = &USART1->DR; //DMA 外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = 64; //DMA 通道的 DMA 缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8 位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 8 位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA 通道 x 拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据指定的参数初始化

3)使能串口 DMA 发送
进行DMA配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是:

USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);

如果是要使能串口 DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。

4)使能 DMA1 通道 4,启动传输。
使能串口 DMA 发送之后,我们接着就要使能 DMA 传输通道:DMA_Cmd(DMA_CHx, ENABLE);通过以上 3 步设置,我们就可以启动一次 USART1 的 DMA 传输了。

5)查询 DMA 传输状态

在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是:

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)

比如我们要查询 DMA 通道 4 传输是否完成,方法是:

DMA_GetFlagStatus(DMA2_FLAG_TC4);

这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx)

比如我们要获取 DMA 通道 4 还有多少个数据没有传输,方法是:

DMA_GetCurrDataCounter(DMA1_Channel4);

最后看看UART1的DMA整体配置。

/**
  * @brief  USART1 TX DMA 配置,内存到外设(USART1->DR)
  * @param  None
  * @retval None
  */
void USART_DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    /*开启DMA时钟*/
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    /*设置DMA源:串口数据寄存器地址*/
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base;

    /*内存地址(要传输的变量的指针)*/
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;

    /*方向:从内存到外设*/
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

    /*传输大小DMA_BufferSize=SENDBUFF_SIZE*/
    DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;

    /*外设地址不增*/
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 

    /*内存地址自增*/
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 

    /*外设数据单位*/
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

    /*内存数据单位 8bit*/
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;  

    /*DMA模式:不断循环*/
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
    //DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

    /*优先级:中*/
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  

    /*禁止内存到内存的传输  */
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    /*配置DMA1的4通道*/
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);

    /*使能DMA*/
    DMA_Cmd (DMA1_Channel4,ENABLE);
}

主函数如下所示:

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    int i;

    /*SysTick Init*/
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    /*DMA初始化*/
    USART_DMA_Config();

    /*LED初始化*/
    LED_GPIO_Config();

    printf("USART1 DMA TX Test \r\n");

    /*填充将要发送的数据*/
    for(i=0;i<SENDBUFF_SIZE;i++)
    {
        SendBuff[i]  = 'Y';
    }
    /* USART1 向 DMA发出TX请求 */
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

    /* 此时CPU是空闲的,可以干其他的事情 */
    //例如同时控制LED
    for(;;)
    {
        LED1(ON);
        Delay_ms(500);
        LED1(OFF);
        Delay_ms(500);
    }
}

主函数很简单,初始化串口,DMA后,向DMA发出请求即可进行数据传输了,这时CPU就可以干其他事情了。

将程序编译好后下载到板子中,通过串口助手可以看到在接收区有”Y”不断的打印输出,同时LED1不停闪烁。

v7kQsO.md.png

【注】在本例中串口是DMA操作的,而LED的闪烁是CPU控制,请读者朋友注意。

11.3.2 DMA中断接收数据

前面讲解了DMA的接收,接下来讲解DMA的接收。接收和发送差不多,对于USART1,DMA的接收通道为DMA1_Channel5,因此USART1 DMA的接收和发送配置如下所示。

/**
  * @brief  USART1 TX, RX DMA 配置
  * @param  None
  * @retval None
  */
void USART_DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    /*开启DMA时钟*/
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // RX DMA1 通道5
    DMA_InitStructure.DMA_BufferSize = BUFF_SIZE;      // 定义了接收的最大长度
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;             // 串口接收,方向是外设->内存
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                   // 外设到内存,所以关闭内存到内存
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RecvBuff;// 内存的基地址,要存储在哪里
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;// 内存数据宽度,按照字节存储
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;        // 内存递增,每次串口收到数据存在内存中,下次收到自动存储在内存的下一个位置
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;                  // 正常模式
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设的基地址,串口的数据寄存器
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    // 外设的数据宽度,按照字节存储,与内存的数据宽度一致
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 接收只有一个数据寄存器 RDR,所以外设地址不递增
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;            // 优先级
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);

    // TX DMA1 通道4  
    DMA_InitStructure.DMA_BufferSize = 0;                          // 发送缓冲区的大小,初始化为0不发送
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;             // 发送是方向是外设到内存,外设作为目的地
    DMA_InitStructure.DMA_MemoryBaseAddr =(uint32_t)SendBuff; // 发送内存地址,从哪里发送
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);

    //DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);
    DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);

    // 使能 USART RX DMA , 关闭 UART TX DMA
    DMA_Cmd(DMA1_Channel5, ENABLE); 
    DMA_Cmd(DMA1_Channel4, DISABLE);

    // 配置中断
    DMA_NVIC_Configuration();
}

上述配置中在DMA的发送的基础上新增了DMA中断接收,因此需要增加DMA接收的NVIC,其配置如下。

/**
  * @brief  配置DMA中断
  * @param  None
  * @retval None
  */
static void DMA_NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure; 
    /* Configure the NVIC Preemption Priority Bits */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

    /* Enable the USARTy Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    //NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;
    //NVIC_Init(&NVIC_InitStructure); 
}

值得注意的,这里需要打开DMA的接收中断,对应的寄存器是DMA_CCRx,前面已经讲过了。

最后,还需要编写USART1的DMA中断接收函数。

/**
  * @brief  This function handles DMA1_Channel5 Handler.
  * @param  None
  * @retval None
  */
void DMA1_Channel5_IRQHandler(void)
{
    uint8_t RecvLen;
    if(DMA_GetITStatus(DMA1_IT_TC5) != RESET)
    {
        DMA_ClearITPendingBit(DMA1_IT_TC5);   // 清除传输完成中断标志位    
        // 总的buf长度减去剩余buf长度,得到接收到数据的长度
        RecvLen = sizeof(RecvBuff) - DMA_GetCurrDataCounter(DMA1_Channel5);

        USART1_DMA_RX_Clear();    // 清空DMA接收通道

        USART1_Send_Array(RecvBuff, RecvLen);        // 使用DMA发送数据
        memset(RecvBuff, '\0', sizeof(RecvBuff)); // 清空接收缓冲区
    }
}

中断接收中封装了两个函数,当USART1 DMA接收中断到了后,将接收的数据放入缓存区,然后需要清除DMA的接收通道,具体函数如下。

/**
  * @brief  清除DMA的传输数量寄存器
  * @param  None
  * @retval None
  */
void USART_DMA_RX_Clear(void)
{
    // 关闭 DMA1_Channel5 通道
    DMA_Cmd(DMA1_Channel5, DISABLE);

    // 重新写入要传输的数据数量
    DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(RecvBuff));

    // 使能 DMA1_Channel5 通道
    DMA_Cmd(DMA1_Channel5, ENABLE);
}

然后就是将接收的数据通过USART1 DMA发送出去,其函数如下。

/**
  * @brief  串口1 DMA发送
  * @param  uint8_t *arr, uint8_t len
  * @retval None
  */
void USART_Send_Array(uint8_t *arr, uint8_t len)
{
    if(len == 0)
      return;

    uint8_t sendLen = len > sizeof(SendBuff) ? sizeof(SendBuff) : len;

    //while (DMA_GetCurrDataCounter(DMA1_Channel4));  // 检查DMA发送通道内是否还有数据
    if(arr) 
    {
        memcpy(SendBuff, arr, sendLen);
    }
    // 开启 UART TX DMA
    DMA_Cmd(DMA1_Channel4, DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel4, sendLen);   // 重新写入要传输的数据数量
    DMA_Cmd(DMA1_Channel4, ENABLE);     // 启动DMA发送    
}

接下来看看主函数的代码。

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    /*SysTick Init*/
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    /*DMA初始化*/
    USART_DMA_Config();

    /*LED初始化*/
    LED_GPIO_Config();

    printf("\rUSART1 DMA TX RX Test \r\n");

    /* 使能串口DMA接收  */
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

    /* USART1 向 DMA发出TX请求 */
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

    /* 此时CPU是空闲的,可以干其他的事情 */
    //例如同时控制LED
    for(;;)
    {
        LED1(ON);
        Delay_ms(500);
        LED1(OFF);
        Delay_ms(500);
    }
}

主函数主要增加了串口DMA接收请求,其他的和上一个实例是一样的。

最后编译下载固件,打开串口助手,这里发送一些数据,效果如下所示。

v7k8dH.md.png

11.4 DMA具体代码实现-HAL库

11.4.1 STM32Cube生成工程

和以前一样,还是使用串口的工程进行修改,当然也可重新新建工程。主要配置项如下。

1.配置串口

本文使用USART1,配置如下。设置MODE为异步通信(Asynchronous)。基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能。

vHEhAe.md.png

设置GPIO引脚自动设置 USART1_RX/USART_TX,默认即可。

vHE5hd.md.png

另外还要使能USART中断,在NVIC Settings 一栏使能接收中断。

vHET1I.md.png

上述的配置和前面章节的串口实验的配置是一样的,接下来需要配置USART1的DMA,配置方法如下。

根据DMA通道预览可以知道,我们用的USART1 的TX RX 分别对应DMA1 的通道4和通道5。

vHEHjP.md.png

点击DMASettings 点击 Add 添加通道,选择USART_RX USART_TX 传输速率设置为中速,DMA传输模式为正常模式,DMA内存地址自增,每次增加一个Byte(字节)。

【注1】DMA传输方式

方法1:DMA_Mode_Normal,正常模式,
当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次

方法2:DMA_Mode_Circular ,循环传输模式

当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。也就是多次传输模式。

Channel DMA传输通道设置
DMA1 : DMA1 Channel 0~DMA1 Channel 7
DMA2: DMA2 Channel 1~DMA1 Channel 5

【注2】DMA的传输方向
DMA的传输方向在前文也已经说过了,对应于STM32cubeMX的三种传输配置如下:外设到内存 Peripheral To Memory,内存到外设 Memory To Peripheral,内存到内存 Memory To Memory。

【注3】指针递增模式
Src Memory 表示外设地址寄存器
功能:设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节,

Dst Memory 表示内存地址寄存器
功能:设置传输数据时候内存地址是否递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节,这个Src Memory一样,只不过针对的是内存。

串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出。串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte。

2.DMA配置
右侧点击System Core 点击DMA。

vHEqnf.md.png

注意:上图显示了我们刚才添加的串口外设,如果你是在DMA设置界面添加DMA 而没有开启对应外设的话 ,默认为MENTOMEN,也只有此选择。如下图所示。

vHEOHS.md.png

3.时钟源设置
笔者使用外部8M时钟源,DMA1挂在AHP上的,USART1是挂在APB2上的。具体配置如下。

vHEjAg.md.png

其他默认即可,然后点击GENERATE CODE 创建工程

11.4.2 USART+ DMA发送数据

在具体实现代码之前,先看看USART1+DMA发送数据流程,如下图示所示。我们直接通过DMA通道将要发送的数据放入串口发送寄存器,数据经过串口发送到数据接收设备,笔者使用的是上位机接收。

vHEvNQ.md.png

在分析代码之前,先看看USART+ DMA数据发送是如何实现的,打开工程,我们新建一个变量。

uint8_t unSendBuffUsart1[] = "USART test by DMA\r\n"; 

然后在man.c中的主循环添加以下代码:

HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)unSendBuffUsart1, sizeof(unSendBuffUsart1));    
HAL_Delay(1000);

然后编译,将程序编译好后下载到板子中,通过串口助手可以看到在接收区有数据不断的打印输出,同时LED1不停闪烁。

vHVFBT.md.png

【注】在本例中串口是DMA操作的,而LED的闪烁是CPU控制,请读者朋友注意。
下面讲讲代码。通过DMA+USART数据发送和直接使用USART整体流程编程差不多,主要在于串口的初始化配置不同,数据发送函数不同。可以和上一章比较。DMA+USART流程如下:

1.初始化硬件,配置时钟
2.GPIO初始化(仅仅LED),DMA初始化,串口初始化参数配置(USART的RX和TX);
3.填充数据,发送数据。

这里主要关注DMA初始化,串口初始化参数配置,以下函数都是STM32cudeMX自动生成的,函数如下:

MX_DMA_Init();
MX_USART1_UART_Init();

先看MX_DMA_Init()函数,函数原型如下:

static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Channel4_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
  /* DMA1_Channel5_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);

}

很简单,主要打开DMA1的通道4和5的中断开关,也就是串口的RX和TX。当然啦,这里虽然配置了USART1 的DMA接收中断,但是必能没有使用,下一节内容会详细讲解USART1 DMA接收。
再看看MX_USART1_UART_Init()函数。函数原型如下:

static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

前面就不说了,在上一章已经讲过了,这里重点讲解HAL_UART_Init()函数

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
  /* Check the UART handle allocation */
  if (huart == NULL)
  {
    return HAL_ERROR;
  }

  /* Check the parameters */
  if (huart->Init.HwFlowCtl != UART_HWCONTROL_NONE)
  {
    /* The hardware flow control is available only for USART1, USART2 and USART3 */
    assert_param(IS_UART_HWFLOW_INSTANCE(huart->Instance));
    assert_param(IS_UART_HARDWARE_FLOW_CONTROL(huart->Init.HwFlowCtl));
  }
  else
  {
    assert_param(IS_UART_INSTANCE(huart->Instance));
  }
  assert_param(IS_UART_WORD_LENGTH(huart->Init.WordLength));
#if defined(USART_CR1_OVER8)
  assert_param(IS_UART_OVERSAMPLING(huart->Init.OverSampling));
#endif /* USART_CR1_OVER8 */

  if (huart->gState == HAL_UART_STATE_RESET)
  {
    /* Allocate lock resource and initialize it */
    huart->Lock = HAL_UNLOCKED;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    UART_InitCallbacksToDefault(huart);

    if (huart->MspInitCallback == NULL)
    {
      huart->MspInitCallback = HAL_UART_MspInit;
    }

    /* Init the low level hardware */
    huart->MspInitCallback(huart);
#else
    /* Init the low level hardware : GPIO, CLOCK */
    HAL_UART_MspInit(huart);
#endif /* (USE_HAL_UART_REGISTER_CALLBACKS) */
  }

  huart->gState = HAL_UART_STATE_BUSY;

  /* Disable the peripheral */
  __HAL_UART_DISABLE(huart);

  /* Set the UART Communication parameters */
  UART_SetConfig(huart);

  /* In asynchronous mode, the following bits must be kept cleared:
     - LINEN and CLKEN bits in the USART_CR2 register,
     - SCEN, HDSEL and IREN  bits in the USART_CR3 register.*/
  CLEAR_BIT(huart->Instance->CR2, (USART_CR2_LINEN | USART_CR2_CLKEN));
  CLEAR_BIT(huart->Instance->CR3, (USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN));

  /* Enable the peripheral */
  __HAL_UART_ENABLE(huart);

  /* Initialize the UART state */
  huart->ErrorCode = HAL_UART_ERROR_NONE;
  huart->gState = HAL_UART_STATE_READY;
  huart->RxState = HAL_UART_STATE_READY;

  return HAL_OK;
}

重点关注HAL_UART_MspInit()函数,

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(huart->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* Peripheral clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 DMA Init */
    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA1_Channel5;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx);

    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA1_Channel4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(huart,hdmatx,hdma_usart1_tx);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }

}

这里就是DMA的USART初始化。有个重要的结构体:DMA_HandleTypeDef。

typedef struct __DMA_HandleTypeDef
{
  DMA_Channel_TypeDef   *Instance;                       /*!< Register base address                  */

  DMA_InitTypeDef       Init;                            /*!< DMA communication parameters           */ 

  HAL_LockTypeDef       Lock;                            /*!< DMA locking object                     */  

  HAL_DMA_StateTypeDef  State;                           /*!< DMA transfer state                     */

  void                  *Parent;                                                      /*!< Parent object state                    */  

  void                  (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);     /*!< DMA transfer complete callback         */

  void                  (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA Half transfer complete callback    */

  void                  (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);    /*!< DMA transfer error callback            */

  void                  (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);    /*!< DMA transfer abort callback            */  

  __IO uint32_t         ErrorCode;                                                    /*!< DMA Error code                         */

  DMA_TypeDef            *DmaBaseAddress;                                             /*!< DMA Channel Base Address               */

  uint32_t               ChannelIndex;                                                /*!< DMA Channel Index                      */  

} DMA_HandleTypeDef;

 Instance:DMA通道配置,也就是寄存器基地址;
 Init:也是一个结构体,主要初始化DMA通讯参数,后面会有讲解;
 Lock:对资源操作增加操作锁;
 State:DMA的传输状态,通过设置不同的状态设置,实现传输方式;
 剩余几个参数是回调的指针和变量,用于实现回调函数。

再看看DMA_InitTypeDef结构体,其原型如下;

typedef struct
{
  uint32_t Direction;                 /*!< Specifies if the data will be transferred from memory to peripheral, 
                                           from memory to memory or from peripheral to memory.
                                           This parameter can be a value of @ref DMA_Data_transfer_direction */

  uint32_t PeriphInc;                 /*!< Specifies whether the Peripheral address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Peripheral_incremented_mode */

  uint32_t MemInc;                    /*!< Specifies whether the memory address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Memory_incremented_mode */

  uint32_t PeriphDataAlignment;       /*!< Specifies the Peripheral data width.
                                           This parameter can be a value of @ref DMA_Peripheral_data_size */

  uint32_t MemDataAlignment;          /*!< Specifies the Memory data width.
                                           This parameter can be a value of @ref DMA_Memory_data_size */

  uint32_t Mode;                      /*!< Specifies the operation mode of the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_mode
                                           @note The circular buffer mode cannot be used if the memory-to-memory
                                                 data transfer is configured on the selected Channel */

  uint32_t Priority;                  /*!< Specifies the software priority for the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_Priority_level */
} DMA_InitTypeDef;

 Direction:DMA传输方向,有三种,外设到存储器,存储器到外设,存储器到存储器,根据工程要求来配置,这里选择DMA_MEMORY_TO_PERIPH参数;
 PeriphInc:配置外设地址寄存器是否自动增加,这里配置为不递增;
 MemInc:配置内存地址自动增加,一般都是使能自动增加;
 PeriphDataAlignment:外设数据长度,分别有字节、字、半字,这里配置为字。
 MemDataAlignment:内存数据长度,和外设的类似;
 Mode:配置传输模式,这里是常规模式。
 Priority:优先权,笔者配置的为中优先级。

以上的结构体配置参数就是通过STM32cudeMX配置得到的,和前文是的配置是一一对应的。DMA+USART1的数据发送就到这里了,如果还有什么迷惑,请读者朋友结合USART1 发送数据流程与代码在仔细琢磨吧。

11.4.3 USART+ DMA接收不定长数据

使用USART+DMA数据接收不需要再配置STM32cubeMX,在上一节已经配置过了,这里只是在发送的基础上添加代码即可。下面还是先将如何USART+ DMA数据接收实现。

在main.c添加如下变量:

uint8_t unRecvBuffUsart1;  //接收数据缓存

在MX_USART1_UART_Init()函数添加以下代码:

//DMA接收初始化函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
HAL_UART_Receive_DMA(&huart1, &unRecvBuffUsart1, 1);

然后添加回调函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{
  HAL_UART_Receive_DMA(&huart1, &unRecvBuffUsart1, 1);  
  HAL_UART_Transmit_DMA(&huart1, &unRecvBuffUsart1, 1); 
}

然后将程序编译好后下载到板子中,发送一个数据我们设置连续发送,如下图所示。

vHVn3R.md.png

这样就完了吗,我们换个数据测试下。

vHVKjx.md.png

有的朋友说,是我设置的接收数据是一个字符,换一下就好了,那么你可以是试试换成2个或者多个试试。我这里就不展示了,接下来笔者将带领大家实现任意字符串接收并输出。这里有种最简单的方式,就是使用DMA+USART1接收数据,通过USART1的普通方式发送数据。
也就是将

HAL_UART_Transmit_DMA(&huart1, &unRecvBuffUsart1, 1);   

换成

HAL_UART_Transmit(&huart1,& unRecvBuffUsart1,1,0);

然后编译,下载程序,看下结果。

vHVlDK.md.png

当然,只要是实现了,怎样都可以,笔者这章讲的是DMA,那么接收发送都得用吧,来吧,展示!

在main.c中添加以下变量:

uint8_t unRecvBuffUsart1 [BUFFER_SIZE] ;  //接收数据缓存数组
volatile uint8_t unRecvLengthUsart1 = 0;  //接收一帧数据的长度
volatile uint8_t unRecvDndFlagUsart1 = 0; //一帧数据接收完成标志

在MX_USART1_UART_Init()函数添加以下代码:

HAL_UART_Receive_DMA(&huart1, unRecvBuffUsart1, BUFFER_SIZE);

在main.h中添加以下宏定义与变量:

extern uint8_t unRecvBuffUsart1 [BUFFER_SIZE];  //接收数据缓存
extern volatile uint8_t unRecvLengthUsart1;  //接收一帧数据的长度
extern volatile uint8_t unRecvDndFlagUsart1; //一帧数据接收完成标志

接下来就是重点,我们使用IDLE 接收空闲中断+DMA接收数据,然后发送数据。STM32的IDLE的中断产生条件:在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断。我们修改USART1_IRQHandler()函数:

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  uint32_t temp;
  //获取IDLE标志位
  if((RESET != __HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)))//idle标志被置位
  { 
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位

    HAL_UART_DMAStop(&huart1); //
    temp  =  __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数   
    unRecvLengthUsart1  =  BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
    unRecvDndFlagUsart1  = 1;   // 接受完成标志位置1    
    HAL_UART_Transmit_DMA(&huart1, unRecvBuffUsart1, unRecvLengthUsart1);
    unRecvLengthUsart1 = 0;//清除计数
    unRecvDndFlagUsart1 = 0;//清除接收结束标志位

    memset(unRecvBuffUsart1,0,unRecvLengthUsart1);
    HAL_UART_Receive_DMA(&huart1, unRecvBuffUsart1, sizeof(unRecvBuffUsart1));//重新打开DMA接收,不然只能接收一次数据
  }
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

好了,编译下,重新将程序下载到板子里。

vHV1HO.md.png

现在就完美了,当然在实际工程中,最基本的就是要实现功能,然后再谈效率。最后再总结下USART+ DMA数据接收过程,和USART中断接收流程差不多,只是这里增加了DMA。这里重点将讲解STM32 的IDLE中断,也就是STM32的接收不定长度字节数据的方法。由于STM32单片机带IDLE中断,所以利用这个中断,可以接收不定长字节的数据。

IDLE中断什么时候发生?IDLE就是在串口收到一帧数据后,发生的中断。什么是一帧数据呢?比如说给单片机一次发来1个字节,或者一次发来8个字节,这些一次发来的数据,就称为一帧数据,也可以叫做一包数据。关于STM32F1的IDLE可以看参考手册的27.6.4节的内容。

v7k5mF.md.png

这是串口CR1寄存器,其中,对bit4写1开启IDLE中断,对bit5写1开启接收数据中断。(注意:不同系列的STM32,对应的寄存器位可能不同)。

那么 RXNE中断和IDLE中断的区别?当MCU接收到1个字节,就会产生RXNE中断,当接收到一帧数据,就会产生IDLE中断。比如给MCU一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。

v7kIw4.md.png

关于更多IDLE中断,有时间再讲吧,今天就到这里了。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment