STM32F1的DMA使用

2023-08-01  

在前面我们提到过 DMA,这一章我们就来学习 STM32F1 的DMA 使


用。要实现的功能是:通过 K_UP 按键控制 DMA 串口 1 数据的传送,在传送过程中让 D2 指示灯不断闪烁,直到数据传送完成。D1 指示灯闪烁提示系统正常运行。学习时可以参考《STM32F10x 中文参考手册》-10 DMA 控制器(DMA)章节。



DMA 简介


DMA,全称是 Direct Memory Access,中文意思为直接存储器访问。DMA 可用于实现外设与存储器之间或者存储器与存储器之间数据传输的高效性。之所以称为高效, 是因为 DMA 传输数据移动过程无需 CPU 直接操作, 这样节省的 CPU 资源就可供其它操作使用。从硬件层面来理解,DMA 就好像是 RAM 与 I/O 设备间数据传输的通路, 外设与存储器之间或者存储器与存储器之间可以直接在这条通路上进行数据传输。这里说的外设一般指外设的数据寄存器, 比如 ADC、SPI、I2C、DCMI等外设的数据寄存器, 存储器一般是指片内 SRAM、 外部存储器、 片内 Flash等。


STM32F1 最多有 2 个 DMA 控制器 ( DMA2 仅存在大容量产品中) ,DMA1 有7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个 DMA 请求的优先权。

STM32F1 的 DMA 有以下主要特性:


● 12 个独立的可配置的通道(请求):DMA1 有 7 个通道, DMA2 有 5 个通



● 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触


发。这些功能通过软件来配置。


● 在同一个 DMA 模块上, 多个请求间的优先权可以通过软件编程设置(共有


四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求 0 优先于请求1,依此类推) 。


● 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。


● 支持循环的缓冲器管理


● 每个通道都有 3 个事件标志(DMA 半传输、 DMA 传输完成和 DMA 传输出错),这3 个事件标志逻辑或成为一个单独的中断请求。


● 存储器和存储器间的传输


● 外设和存储器、存储器和外设之间的传输


● 闪存、 SRAM、外设的 SRAM、 APB1、 APB2 和 AHB 外设均可作为访问的源和目标。


● 可编程的数据传输数目:最大为 65535

DMA 结构框图


DMA 控制器独立于内核,属于一个单独的外设,结构比较简单,从编程的角度来看,我们只需掌握结构框图中的三部分内容即可。如图所示(大家也可以查看《STM32F10x中文参考手册》-10 DMA 控制器(DMA)章节内容)。

c110bf15fc521148d94a3af22cce4a86_wKgaomSOZ8KASkVxAATfRtqYmgU392.jpg

我们把 DMA 结构框图分成3个子模块,按照顺序依次进行简单介绍。

(1)标号 1:DMA 请求


如果外设要想通过 DMA 来传输数据, 必须先给 DMA 控制器发送 DMA 请求,DMA 收到请求信号之后, 控制器会给外设一个应答信号, 当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。


根据前面介绍我们知道,DMA 含有 DMA1 和 DMA2 两个控制器,其中 DMA1 含有 7个通道,DMA2 含有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求。


从DMA 请求映射图中可以知道各通道所对应的外设请求,如图分别是DMA1和DMA2请求映射图:

图片

图片

DMA2 请求通道中 ADC3、 SDIO 和 TIM8 的 DMA 请求只在大容量产品中存在,这个在具体项目时要注意。

(2)标号 2:DAM 通道


DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道, DMA2 有 5个通道,每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。

(3)标号 3:仲裁器


当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。

DMA 数据配置


使用 DMA, 最核心就是配置要传输的数据, 包括数据从哪里来, 要到哪里去,传输的数据的单位是什么,要传多少数据,是一次传输还是循环传输等。

(1)从哪里来到哪里去


我们知道 DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。具体的方向 DMA_CCR 位 4 DIR 配置:0 表示从外设到存储器, 1 表示从存储器到外设。这里面涉及到的外设地址由 DMA_CPAR 配置,存储器地址由 DMA_CMAR配置。

外设到存储器


当我们使用从外设到存储器传输时,以 ADC 采集为例。DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。

存储器到外设


当我们使用从存储器到外设传输时, 以串口向电脑端发送数据为例。DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。

存储器到存储器


当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH当作一个外设来看)的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位14:MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。

(2)要传多少,单位是什么


当我们配置好数据要从哪里来到哪里去之后, 我们还需要知道我们要传输的数据是多少,数据的单位是什么。


以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由 DMA_CNDTR 配置,这是一个 32 位的寄存器,一次最多只能传输 65535 个数据。


要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8 位的,所以我们定义的要发送的数据也必须是 8 位。外设的数据宽度由 DMA_CCR 的 PSIZE[1:0]配置,可以是 8/16/32 位,存储器的数据宽度由DMA_CCR 的 MSIZE[1:0]配置,可以是 8/16/32 位。


在 DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。外设的地址指针由 DMA_CCRx 的PINC 配置,存储器的地址指针由 MINC 配置。以串口向电脑发送数据为例,要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。

(3)什么时候传输完成


数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来判断。每个 DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器DMA_ISR 的详细描述。


传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCR 寄存器的 CIRC 循环模式位控制。

DMA 配置步骤


接下来我们介绍下如何使用库函数对 DMA 进行配置。这个也是在编写程序中必须要了解的。具体步骤如下:(DMA 相关库函数在 stm32f10x_dma.c 和stm32f10x_dma.h 文件中)

(1)使能 DMA 控制器(DMA1 或 DMA2)时钟


要使能 DMA 时钟,需通过AHB1ENR 寄存器来控制,使能 DMA时钟库函数为:

void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState

NewState);

例如使能 DMA1 时钟,函数如下:


RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

(2)初始化 DMA 通道,包括配置通道、外设和内存地址、传输数据量等要使用 DMA,必须对其相关参数进行设置,包括通道选择、外设和内存地址、


通道优先级、传输数据量的配置等。该部分设置通过 DMA 初始化函数 DMA_Init完成的:


void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,DMA_InitTypeDef*

DMA_InitStruct);

函数中第一个参数是用来确定 DMA 通道,参数范围为:

DMA1_Channel_0~DMA1_Channel_7(DMA2 是DMA2_Channel_0-DMA2_Channel_5)

第二个参数是一个结构体指针变量,结构体类型是 DMA_InitTypeDef,其内包含了 DMA 相关参数的设置。下面我们简单介绍下它的成员:

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_CPAR 寄存器设置,一般设置为外设的数据寄存器地址,比如要进行串口 DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1- >DR 的地址,表示方法为&USART1- >DR。如果是存储器到存储器模式则设置为其中一个存储区地址。

DMA_Memory0BaseAddr:存储器地址,通过 DMA_CMAR 寄存器设置,一般设置为我们自定义存储区的首地址,即我们存放 DMA 传输数据的内存地址。比如我们定义一个 u32 类型数组,将数组首地址(直接使用数组名即可)赋值给DMA_Memory0BaseAddr,在DMA传输的时候就可以把数组内的数据发送或接收。


DMA_DIR:数据传输方向选择,可选择外设到存储器、存储器到外设以及存


储器到存储器。通过设定 DMA_CCR 寄存器的 DIR[1:0]位的值决定。比如本章实验是从内存读取数据发送到串口,所以数据传输方向为存储器到外设,配置为DMA_DIR_MemoryToPeripheral。


DMA_BufferSize:用来设置一次传输数据的大小,通过 DMA_CNDTR 寄存器设置。


DMA_PeripheralInc:用来设置外设地址是递增还是不变,通过 DMA_CCR寄存器的 PINC 位设置,如果设置为递增,那么下一次传输的时候地址加 1。通常外 设 只 有 一 个 数 据 寄 存 器 , 所 以 一 般 不 会 使 能 该 位 , 即 配 置 为DMA_PeripheralInc_Disable。


DMA_MemoryInc:用来设置内存地址是否递增,通过 DMA_CCR 寄存器的MINC位设置。我们自定义的存储区一般都是存放多个数据的,所以需要使能存储器地址自动递增功能,即配置为 DMA_MemoryInc_Enable。


DMA_PeripheralDataSize:外设数据宽度选择,可以为字节(8 位)、半字


(16位)、字(32 位),通过 DMA_CCR 寄存器的 PSIZE[1:0]位设置。例如本实验数据是按照 8 位字节传输,所以配置为DMA_PeripheralDataSize_Byte。


DMA_MemoryDataSize:存储器数据宽度选择,可以为字节(8 位)、半字(16位)、字(32 位),通过 DMA_CCR 寄存器的 MSIZE[1:0]位设置。本章实验同样设置为 8 位字节传输,这个要和我们定义的数组对应,所以配置为DMA_MemoryDataSize_Byte。


DMA_Mode:DMA 传输模式选择, 可选择一次传输或者循环传输, 通过DMA_CCR寄存器的 CIRC 位来设定。比如我们要从内存 (存储器) 中传输 64 个字节到串口,如果设置为循环传输,那么它会在 64 个字节传输完成之后继续从内存的第一个地址传输,如此循环。这里我们设置为一次传输完成之后不循环。所以设置值为DMA_Mode_Normal。


DMA_Priority:用来设置 DMA 通道的优先级,有低,中,高,超高四种级别,可通过 DMA_CCR 寄存器的PL[1:0]位来设定。DMA 优先级只有在多个 DMA 通道同时使用时才有意义,本章实验我们只使用了一个 DMA 通道,所以可以任意设置DMA 优先级,这里我们就设置为中等优先级,配置参数为DMA_Priority_Medium。


DMA_Priority:用来设置 DMA 通道的优先级,有低,中,高,超高四种级别,可通过 DMA_CCR 寄存器的PL[1:0]位来设定。DMA 优先级只有在多个 DMA 通道同时使用时才有意义,本章实验我们只使用了一个 DMA 通道,所以可以任意设置DMA 优先级,这里我们就设置为中等优先级,配置参数为DMA_Priority_Medium。


DMA_M2M:用来设置存储器到存储器模式,使用存储器到存储器时用到,设定 DMA_CCR 的位 14 MEN2MEN 即可启动存储器到存储器模式。


了解结构体成员功能后,就可以进行配置,本实验配置代码如下:

DMA_InitTypeDef DMA_InitStructure;

DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址

DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址

DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外

设模式

DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量

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_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA 通道 x 没有

设置为内存到内存传输


DMA_Init(DMAy_Channelx, &DMA_InitStructure);//初始化DMA

(3)使能外设 DMA功能(DMA 请求映射图对应的外设)


配置好 DMA 后,我们就需要使能外设 DMA 功能,例如我们要使能串口的DMA发送功能,调用的库函数为:

USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1 的 DMA发送

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


如果是其他的外设需开启DMA功能, 只需要在对应的标准外设库函数中查找到对应的外设 DMA 使能函数。

(4)开启 DMA 的通道传输


初始化 DMA 后,要使用DMA还必须开启它,开启 DMA 通道传输的库函数为:

void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalStateNewState);

第一个参数为外设所对应的 DMA 通道,例如本章使用的是 USART1_TXDMA请求,因此它对应的是 DMA1_Channel4,可通过前面DMA 请求映射图选择。第二个参数相信不说也知道,就是使能或失能。


本实验使能 DMA1_Channel4函数为:

DMA_Cmd(DMA1_Channel4,ENABLE);

(5)查询 DMA 传输状态


通过以上 4 步设置,我们就可以启动一次 DMA 传输了。但是在 DMA 传输过程中,我们还需要查询 DMA传输通道的状态,使用的库函数是:

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

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

DMA_GetFlagStatus(DMA1_FLAG_TC4);

标准库中,还提供了获取当前剩余数据量大小的函数:

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef*DMAy_Channelx);

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

DMA_GetCurrDataCounter(DMA1_Channel4);

同样,标准库中还提供了设置传输数据量大小的函数:

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx,uint16_t DataNumber);

将以上几步全部配置好后,我们就可以使用 DMA 来传输对应外设的数据了。

硬件设计


本实验使用到硬件资源如下:

(1)D1 和 D2 指示灯


(2)K_UP 按键


(3)串口 1


(4)DMA


D1和 D2 指示灯、K_UP 按键、串口 1 电路在前面章节都介绍过,这里就不多说,至于 DMA 它属于 STM32F1 芯片内部的资源,只要通过软件配置好 DMA即可使用。D1指示灯用来提示系统运行状态,K_UP 按键用来控制 DMA发送,每按一次K_UP键,DMA 就将内存(自定义的一个数组)内数据发送 USART1,并通过串口 1将发送的内容打印出来,在 DMA 数据传输的过程中让 D2 指示灯不断闪烁,直到数据传输完成。D2 指示灯闪烁表示 CPU在执行其他的任务,说明 DMA 传输是不需要占用 CPU 的。


所要实现的功能是:通过 K_UP 按键控制 DMA 串口 1 数据的传送,在传送过程中让 D2 指示灯不断闪烁,直到数据传送完成。D1 指示灯闪烁提示系统正常运行。程序框架如下:

(1)初始化 USART1_TX对应的 DMA 通道相关参数


(2)编写主函数


前面介绍 DMA 配置步骤时, 就已经讲解如何初始化 DMA。下面我们打开 “DMA实验” 工程, 在 APP 工程组中可以看到添加了dma.c文件(里面包含了 DMA 驱动程序),在 StdPeriph_Driver 工程组中添加了stm32f10x_dma.c 库文件。DMA 操作的库函数都放在 stm32f10x_dma.c 和stm32f10x_dma.h 文件中,所以使用到 DMA 就必须加入 stm32f10x_dma.c文件,同时还要包含对应的头文件路径。


这里我们分析几个重要函数,其他部分程序大家可以打开工程查看。

DMA 初始化函数


要使用 DMA,我们必须先对它进行配置。初始化代码如下:

/****************************************************************

* 函 数 名 : DMAx_Init

* 函数功能 : DMA 初始化函数

* 输 入 :

DMAy_Channelx:DMA 通 道 选 择 ,@ref DMA_channel

DMA_Channel_0~DMA_Channel_7

par:外设地址

mar:存储器地址

ndtr:数据传输量

* 输 出 : 无

*****************************************************************/

void DMAx_Init(DMA_Channel_TypeDef* DMAy_Channelx,u32 par,u32

mar,u16 ndtr)

{

DMA_InitTypeDef DMA_InitStructure;

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

//DMA_DeInit(DMAy_Channelx);

/* 配置 DMA */

DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址

DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址

DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外设模式

DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量

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_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA 通道 x 没有设置为内存到内存传输

DMA_Init(DMAy_Channelx, &DMA_InitStructure);//初始化DMA

}

在 DMAx_Init()函数中,首先使能 DMA1 时钟,然后初始化 DMA 各参数,即配置 DMA_InitStructure结构体,初始化 DMA 通道,这一过程在前面步骤介绍中已经提了。

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。