直接内存访问-DMA
目前为止我们控制外设的方式就是通过程序一点一点的修改外设的寄存器,这样已经能够满足我们大部分的需求了。但是当我们需要通过片上外设进行大量的数据交换的时候, 这就成为了一种效率很低的操作,浪费了大量的CPU时间来搬运数据。后来,人们又发明了一种直接访问内存的方法,简称DMA(Direct Memory Access),用来解决类似的问题。
所谓的DMA是一种内存访问机制,它为外设与内存之间以及内存与内存之间提供了一种快速的数据传递方式。 借助DMA,外设可以直接写数据到内存中或者从内存中读取数据,而不需要占用宝贵的CPU的工作时间。
在本文中,我们先详细介绍STM32F407的DMA控制器,以及一次DMA操作的流程。再实现一个简单的内存到内存的DMA操作。
1. DMA控制器
图1是STM32的一个DMA控制器,其中的右下角显示了一个AHB Slave的编程接口,它用于写DMA控制器中的各个寄存器, 来配置各个Stream的通道选择、优先级、数据传送方向、原地址、目标地址、待传送数据大小等。
图1 DMA控制器框图 |
一次DMA操作主要涉及到三个要素:DMA操作请求信号、DMA数据的源地址、DMA数据的目标地址。在图1中我们可以找到这三个要素。
在F407中,一个DMA控制器中有8条Stream,它们一起连接到了仲裁器(Arbiter)上,由Arbiter根据各个Stream的优先级决定响应哪个Stream的DMA请求。 而每个Stream又有8个通道,同一时间只有一个通道是有效的,至于说某一外设可以通过哪个Stream的哪个通道来进行DMA操作, 可以查看参考手册的第309页。 软件上每个Stream都可以有四个优先级:Very High priority, High Priority, Medium Priority, Low Priority。如果两个Stream具有相同的软件优先级,那么编号小的Stream具有更高的硬件优先级, 比如说Stream 2和Stream 4的软件优先级同为Very High,但是Stream 2的优先级仍然比Stream 4要高。
Arbiter输出了两个选择信号用来控制AHB Master选择外设和内存的地址。 在F407中支持三种传送方式:内存到外设、外设到内存、内存到内存。在控制器的编程端口中,每个Stream都有两个寄存器DMA_SxPAR和DMA_SxM0AR, 根据传送数据的方向,一个用于源地址,另一个则用于目标地址,参考表1。需要说明的一点是,在F407中一共有两个DMA控制器,分别称为DMA1和DMA2, 其中只有DMA2支持内存到内存的传送方式。
表 1 DMA操作数据的目标地址和源地址
传送方式 | 源地址 | 目标地址 |
---|---|---|
内存到外设 | DMA_SxM0AR | DMA_SxPAR |
外设到内存 | DMA_SxPAR | DMA_SxM0AR |
内存到内存 | DMA_SxPAR | DMA_SxM0AR |
由于DMA操作涉及到多个外设和多段内存,同一时间必须只有一个DMA操作执行,因此需要在外设与DMA控制器之间建立一个握手机制,保证DMA操作可靠进行, 其流程如下:
- 外部触发了某事件,由外设通过某一条Stream向DMA控制器提出DMA请求。
- DMA控制器根据当前的请求的各个Stream的优先级决定响应其中优先级最高的请求。
- 当DMA控制器开始响应该外设时,会给该外设发送一个应答信号。
- 外设一旦接收到了应答信号,就会释放请求信号。
- DMA控制器检测到外设释放了请求信号,才会释放应答信号。
一个DMA操作需要经过三步,首先把数据从源地址中装载进控制器,然后把数据写到目标地址中,最后把寄存器DMA_SxNDTR中的值减一标志着还有多少数据需要传送。 每次传送的数据宽度可以是8位、16位或者32位,这可以通过软件写寄存器配置。
2. 一个内存到内存的DMA例子
在F407中DMA寄存器可以分为中断寄存器和Stream寄存器两个部分,参考如下的结构体定义。中断寄存器有四个分别用于标识和清除DMA的中断状态, 每个Stream都有一套寄存器,用来该Stream的工作方式、外设地址、内存地址、数据长度等内容。
typedef struct dma_stream_regs {
union dma_sxcr CR; /* DMA stream x 配置寄存器 */
union dma_sxndtr NDTR; /* DMA stream x 待发送数据寄存器 */
uint32 PAR; /* DMA stream x 外设地址寄存器(32bit) */
uint32 M0AR; /* DMA stream x 内存0地址寄存器(32bit) */
uint32 M1AR; /* DMA stream x 内存1地址寄存器(32bit),只在double buffer时有用 */
union dma_sxfcr FCR; /* DMA stream x FIFO控制寄存器 */
} dma_stream_regs_t;
typedef struct dma_regs {
volatile union dma_lir LISR; /* DMA中断状态寄存器Low, offset: 0x00 */
volatile union dma_hir HISR; /* DMA中断状态寄存器High, offset: 0x04 */
volatile union dma_lir LIFCR; /* DMA中断标识清除寄存器Low, offset: 0x08 */
volatile union dma_hir HIFCR; /* DMA中断标识清除寄存器High, offset: 0x0C */
volatile dma_stream_regs_t stregs[8]; /* Stream Regs, offset: 0x10 */
} dma_regs_t;
我们定义两个数组src_buffer和dst_buffer,在这个例子中我们通过DMA把src_buffer中的内容复制到dst_buffer中。
uint32 src_buffer[32] = {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80 };
uint32 dst_buffer[32] = { 0 };
首先,我们对DMA2进行初始化,因为只有DMA2可以进行内存到内存的DMA操作。
void dma_init() {
RCC->AHB1ENR.bits.dma2 = 1;
DMA_ResetStream(DMA2_Stream0);
这里先通过复位与时钟控制器RCC打开DMA控制器的驱动时钟。 然后调用函数DMA_ResetStream对DMA2_Stream0进行复位操作,将其相关的寄存器恢复到复位状态。
DMA2_Stream0->CR.bits.CHSEL = 0; // 通道选择
DMA2_Stream0->CR.bits.DIR = DMA_DIR_M2M; // 传输方向
DMA2_Stream0->CR.bits.CIRC = 0; // 关闭循环模式
DMA2_Stream0->CR.bits.PL = DMA_Priority_High;// 优先级
DMA2_Stream0->CR.bits.PINC = 1; // 外设增长
DMA2_Stream0->CR.bits.PSIZE = DMA_PSIZE_32Bits; // 外设数据宽度
DMA2_Stream0->CR.bits.MINC = 1; // 内存增长
DMA2_Stream0->CR.bits.MSIZE = DMA_PSIZE_32Bits; // 内存数据宽度
DMA2_Stream0->CR.bits.MBURST = DMA_Burst_0; // Single Transfer
DMA2_Stream0->CR.bits.PBURST = DMA_Burst_0; // Single Transfer
对控制寄存器进行各种配置,其中通道选择比较随意,因为我们使用的是内存到内存的DMA操作,任意选一个就行。 传输方向选择内存到内存。关闭了循环模式,所谓的循环模式是指一次传输完成后会自动填写NDTR寄存器开始新的DMA操作。 我们只需要拷贝一次就行了,所以将其关闭。 因为我们只是用了一个DMA操作,所以优先级的设置也是随意指定的。这里设置,完成每个DMA操作后,外设和内存的地址都会增加以拷贝下一个数据。 内存和外设的数据宽度都是32位。
读者可能会疑惑,我们进行的是内存到内存的DMA操作,为什么会涉及到外设的地址和数据宽度。 根据表1中记录源地址和目的地址的寄存器,我们知道在内存到内存的DMA操作中, 外设地址寄存器PAR将用作源地址,而内存地址寄存器M0AR将用作目标地址。
DMA2_Stream0->FCR.bits.DMDIS = 0; // 保持Direct Mode
DMA2_Stream0->FCR.bits.FTH = DMA_FIFO_4;
以上是对FIFO的配置,这里采用的是Direct Mode,设置FIFO阈值为4个Word。
DMA2_Stream0->PAR = (uint32)src_buffer;
DMA2_Stream0->M0AR = (uint32)dst_buffer;
DMA2_Stream0->NDTR.all = 32;
DMA2_Stream0->CR.bits.EN = 1;
}
然后我们配置源地址和目标地址,以及数据长度。最后打开DMA控制器开始进行DMA传送。
为了验证DMA正确的把src_buffer下的数据拷贝到了dst_buffer下了,我们在main函数中做如下的操作:调用dma_init对DMA控制器进行初始化之后, 检测控制寄存器判定DMA操作是否完成,然后在for循环中依次对比两端缓存中的内容,如果不一样就控制LED显示绿色。
dma_init();
while (!(0x01 && DMA2_Stream0->CR.all));
for (int i = 0; i < 32; i++) {
if (src_buffer[i] != dst_buffer[i]) {
LED_0 = LED_ON;
LED_1 = LED_ON;
}
}
3. 总结
保证DMA操作正常进行有DMA请求信号、数据源地址、数据目标地址三个要素:
- DMA请求信号:在F407中每个DMA控制器都有8个Stream,每个Stream有8个通道,我们需要根据手册中关于不同外设对应的通道进行选择。 此外,每个Stream都有4个优先级可以选择,DMA控制器中有一个仲裁器决定响应具体哪个请求。
- 数据源地址:根据数据传送方向的不同,DMA控制器会把外设地址寄存器或者内存地址寄存器中所记录的值判定为源地址。 在进行DMA操作时,首先从源地址中装载数据。
- 数据目标地址:与源地址一样,目标地址也是根据数据传送方向,从外设或者内存地址寄存器中获取的。 在DMA操作中,会把装载进的数据写到目标地址中。
在进行DMA操作之前,DMA控制器与外设之间需要先进性一次握手操作。首先由外设发起请求,DMA控制器根据请求的优先级对某一外设进行相应, 同时发送一个应答信号到被响应的外设。外设接收到响应信号后撤销请求信号,DMA控制器检测到外设撤销请求信号后撤销响应信号。 至此握手信号结束,可以进行DMA操作了。
一次DMA操作有三个部分组成:
- 从源地址中获取数据
- 向目标地址中写入数据
- 数据寄存器中的值减一标识还有多少数据需要传送