淺談MCU的DMA技術
DMA技術簡介
DMA外設和存儲器(或存儲器和存儲器)直接通過總線進行數據交換而不經過CPU的技術。在MCU中,DMA是一項十分重要的技術,它可以降低CPU的處理壓力,提高外設數據的處理效率。
概念:
- 通道:DMA的通道表示一組外設對存儲器的請求,
- 數據對齊:源和目的數據源的地址要對齊,傳輸寬度對齊
- 仲裁器:協調優先權,多個外設訪問同一個存儲器時可通過軟件設置優先級,優先級相同時由硬件決策
DMA的定義可以看出,這是一種利用總線的技術,降低CPU在數據讀取和存儲上面的壓力,可以執行其他操作。當CPU初始化這個傳輸動作,傳輸動作本身是由DMA 控制器來實行和完成。
DMA 控制器和Cortex-M3核共享系統數據總線執行直接存儲器數據傳輸。當CPU和DMA同時訪問相同的目標(RAM或外設)時,DMA請求可能會停止 CPU訪問系統總線達若干個周期,總線仲裁器執行循環調度,以保證CPU至少可以得到一半的系統總線(存儲器或外設)帶寬。
stm32F4中的DMA
DMA主要特性
直接存儲器訪問 (DMA) 用於在外設與存儲器之間以及存儲器與存儲器之間提供高速數據傳 輸。可以在無需任何 CPU 操作的情況下通過 DMA 快速移動數據。這樣節省的 CPU 資源可 供其它操作使用。
DMA 控制器基於復雜的總線矩陣架構,將功能強大的雙 AHB 主總線架構與獨立的 FIFO 結 合在一起,優化了系統帶寬。
兩個 DMA 控制器總共有 16 個數據流(每個控制器 8 個),每一個 DMA 控制器都用於管理 一個或多個外設的存儲器訪問請求。每個數據流總共可以有多達 8 個通道(或稱請求)。每 個通道都有一個仲裁器,用於處理 DMA 請求間的優先級。
-
每個數據流有單獨的四級 32 位先進先出存儲器緩沖區 (FIFO)
-
可供每個數據流選擇的通道請求多達 8 個。此選擇可由軟件配置,允許幾個(several,注意不是同時啟用)。在
DMA_SxCR
數據流配置寄存器中,CHSEL[2:0]
唯一確定一個通道的使用)外設啟動 DMA請求
手冊上的框圖為:
根據上表可以看出,一個DMA控制器管理8個數據流,每個數據流含8個通道,每個數據流在外設和存儲器之間均存在一個FIFO做數據緩沖。
每個數據流都與一個 DMA 請求相關聯,然而並不是每個通道都能與一個DMA請求相關聯,例如DMA1:
即每個數據流選擇一個通道內的DMA請求生效,源傳輸和目標傳輸在整個 4 GB 區域(地址在 0x0000 0000 和 0xFFFF FFFF 之間)都可以尋址外設和存儲器。
三種傳輸模式
- 外設到存儲器
- 使能這種模式(將
DMA_SxCR
寄存器中的位 EN 置 1)時,每次產生外設請求,數據流都會啟動數據源到 FIFO 的傳輸。 - 達到 FIFO 的閾值級別時,FIFO 的內容移出並存儲到目標中,直接模式下每完成一次從外設到 FIFO 的數據傳輸后,相應的數據立即就會移出並存儲到目標中。
- 只有贏得仲裁后,FIFO數據才會被取走
- 使能這種模式(將
- 存儲器到外設
- 使能這種模式(將
DMA_SxCR
寄存器中的 EN 位置 1)時,數據流會立即啟動傳輸,從源完全填充 FIFO。 - 每次發生外設請求,FIFO 的內容都會移出並存儲到目標中。
- 只有贏得了數據流的仲裁后,相應數據流才有權訪問 AHB 源或目標端口。
- 使能這種模式(將
- 存儲器到存儲器
- DMA 通道在沒有外設請求觸發的情況下同樣可以工作。
- 通過將
DMA_SxCR
寄存器中的使能位 (EN) 置 1 來使能數據流時,數據流會立即開始填充 FIFO,直至達到閾值級別。達到閾值級別后,FIFO 的內容便會移出,並存儲到目標中。 - 只有贏得了數據流的仲裁后,相應數據流才有權訪問 AHB 源或目標端口。
上面說的這三種模式,其實簡單的理解下就是數據可以先放入FIFO,等待觸發時一次取走或寫入,或者直接模式不用等FIFO到達閾值就操作。
DMA的配置與工作流程
DMA可以類比為倉庫的貨物搬移,因此需要配置以下幾個基本的條件:倉庫的位置,倉庫的單位,倉庫的大小,搬的方式。以HAL庫的配置方式為例:
- 配置目的地和源,這里可以認為是倉庫的位置
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&DCMI->DR; //外設地址
DMA_InitStructure.DMA_Memory0BaseAddr = DMA_Memory0BaseAddr; //DMA 存儲器0地址
- 配置DMA緩存大小,可以認為是倉庫的容量
DMA_InitStructure.DMA_BufferSize = DMA_BufferSize; //數據傳輸量
- 配置外設和存儲器地址寄存器是否遞增,這里的意思是數據是放在同一個地方還是遞增往下放,源和目的單獨可配
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外設非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc; //存儲器增量模式
- 設置外設和存儲器數據寬度,倉庫的單位,單位不一致放置會出問題。
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //外設數據長度:32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize; //存儲器數據長度
- 設置DMA工作模式(循環或正常(單次))
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 使用循環模式
- 設置優先級
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //高優先級
- 設置模式(外設到存儲器,存儲器到外設,存儲器到存儲器)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; //外設到存儲器模式
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; //FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; //使用全FIFO
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //外設突發單次傳輸
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //存儲器突發單次傳輸
以攝像頭DCMI的DMA配置為例
首先看代碼:
//DCMI DMA配置
//DMA_Memory0BaseAddr:存儲器地址 將要存儲攝像頭數據的內存地址(也可以是外設地址)
//DMA_BufferSize:存儲器長度 0~65535
//DMA_MemoryDataSize:存儲器位寬
//DMA_MemoryDataSize:存儲器位寬
//DMA_MemoryInc:存儲器增長方式
void DCMI_DMA_Init(u32 DMA_Memory0BaseAddr, u16 DMA_BufferSize, u32 DMA_MemoryDataSize, u32 DMA_MemoryInc)
{
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_DCMI, ENABLE);//DCMI
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2時鍾使能
DMA_DeInit(DMA2_Stream1);
while (DMA_GetCmdStatus(DMA2_Stream1) != DISABLE) //等待DMA2_Stream1可配置
{
}
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = DMA_Channel_1; //通道1 DCMI通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&DCMI->DR; //外設地址為:DCMI->DR
DMA_InitStructure.DMA_Memory0BaseAddr = DMA_Memory0BaseAddr; //DMA 存儲器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; //外設到存儲器模式
DMA_InitStructure.DMA_BufferSize = DMA_BufferSize; //數據傳輸量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外設非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc; //存儲器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //外設數據長度:32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize; //存儲器數據長度
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 使用循環模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //高優先級
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; //FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; //使用全FIFO
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //外設突發單次傳輸
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //存儲器突發單次傳輸
DMA_Init(DMA2_Stream1, &DMA_InitStructure); //初始化DMA Stream
DMA_ITConfig(DMA2_Stream1,DMA_IT_TC,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel= DMA2_Stream1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure); //根據指定的參數初始化VIC寄存器、
}
void DMA2_Stream1_IRQHandler(void)
{
if(DMA_GetFlagStatus(DMA2_Stream1,DMA_FLAG_TCIF1)==SET)//DMA2_Steam1,傳輸完成標志
{
DMA_ClearFlag(DMA2_Stream1,DMA_FLAG_TCIF1);//清除傳輸完成中斷
datanum++;
}
}
首先是手冊,可以使用DCMI的DMA數據流是DMA2的流1和流7的通道1,配置流程:
- while循環等待DMA2完成一次傳輸后配置DMA2,這里選擇的是流1的通道1。
- 之后配置外設地址和存儲器地址,傳輸模式,數據傳輸量為1,即每次傳1個字節。
- DCMI的地址是固定的,因此外設是非增量的,存儲器是LCD,地址固定的非增量,長度為16位,外設是RGB565長度選擇16位。
- 傳輸是循環顯示的,因此要循環模式,配置FIFO和傳輸方式,初始化DMA2和中斷
使用DMA讀寫數據與CPU操作的對比
我們做一個小實驗,可能不一定准確,即通過DMA方式給USART發送數據進行計時計數,和通過CPU調用USART發送數據的時間進行對比,代碼基礎是在原子代碼上修改而來。
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "sram.h"
#include "malloc.h"
#include "ILI93xx.h"
#include "led.h"
#include "timer.h"
#include "touch.h"
#include "GUI.h"
#include "GUIDemo.h"
#include "includes.h"
#include "../RESOURCES/logo/logo/Logo.h"
#include "usmart.h"
#include "spi.h"
#include "w25qxx.h"
#include "24cxx.h"
#include "main_tasks.h"
#include "ff.h"
#include "exfuns.h"
#include "fattester.h"
#include "adc.h"
#include "dma.h"
volatile uint32_t gcounter = 0;
const u8 TEXT_TO_SEND[]={"STM32F4 DMA TEST. "};
void extra_while_task(void);
//通用定時器5中斷初始化
//arr:自動重裝值。
//psc:時鍾預分頻數
//定時器溢出時間計算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定時器工作頻率,單位:Mhz
//這里使用的是定時器3!
void TIM5_Int_Init(u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); ///使能TIM4時鍾
TIM_TimeBaseInitStructure.TIM_Prescaler = psc; //定時器分頻
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上計數模式
TIM_TimeBaseInitStructure.TIM_Period = arr; //自動重裝載值
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM5, TIM_IT_Update, ENABLE); //允許定時器3更新中斷
TIM_Cmd(TIM5, ENABLE); //使能定時器3
NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //定時器4中斷
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //搶占優先級1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子優先級3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
//定時器5中斷服務函數
void TIM5_IRQHandler(void)
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) == SET) //溢出中斷
{
gcounter++;
}
TIM_ClearITPendingBit(TIM5, TIM_IT_Update); //清除中斷標志位
}
//DMAx的各通道配置
//這里的傳輸形式是固定的,這點要根據不同的情況來修改
//從存儲器->外設模式/8位數據寬度/存儲器增量模式
//DMA_Streamx:DMA數據流,DMA1_Stream0~7/DMA2_Stream0~7
//chx:DMA通道選擇,@ref DMA_channel DMA_Channel_0~DMA_Channel_7
//par:外設地址
//mar:存儲器地址
//ndtr:數據傳輸量
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr)
{
DMA_InitTypeDef DMA_InitStructure;
if((u32)DMA_Streamx>(u32)DMA2)//得到當前stream是屬於DMA2還是DMA1
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2時鍾使能
}else
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1時鍾使能
}
DMA_DeInit(DMA_Streamx);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}//等待DMA可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = chx; //通道選擇
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外設地址
DMA_InitStructure.DMA_Memory0BaseAddr = mar;//DMA 存儲器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存儲器到外設模式
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_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存儲器突發單次傳輸
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外設突發單次傳輸
DMA_Init(DMA_Streamx, &DMA_InitStructure);//初始化DMA Stream
}
//開啟一次DMA傳輸
//DMA_Streamx:DMA數據流,DMA1_Stream0~7/DMA2_Stream0~7
//ndtr:數據傳輸量
void MYDMA_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{
DMA_Cmd(DMA_Streamx, DISABLE); //關閉DMA傳輸
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){} //確保DMA可以被設置
DMA_SetCurrDataCounter(DMA_Streamx,ndtr); //數據傳輸量
DMA_Cmd(DMA_Streamx, ENABLE); //開啟DMA傳輸
}
int main(void)
{
u8 count = 0;
uint8_t buffer[256];
u8 res = 0;
POINT_COLOR = DARKBLUE;
delay_init(168); //延時初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中斷分組配置
uart_init(115200); //串口波特率設置
TFTLCD_Init(); //初始化LCD
KEY_Init();
LED_Init(); //LED初始化
TIM3_Int_Init(10000 - 1, 16800 - 1); //10Khz計數,1秒鍾中斷一次
//不需要經過OS的任務
extra_while_task();
}
#define SEND_BUF_SIZE 8000
void extra_while_task(void)
{
u32 i = 0;
u8 t=0,mask=0,j=sizeof(TEXT_TO_SEND);
u8 SendBuff[SEND_BUF_SIZE]; //發送數據緩沖區
if(1)
{
for(i=0;i<SEND_BUF_SIZE;i++)//填充ASCII字符集數據
{
if(t>=j)//加入換行符
{
if(mask)
{
SendBuff[i]=0x0a;
t=0;
}else
{
SendBuff[i]=0x0d;
mask++;
}
}else//復制TEXT_TO_SEND語句
{
mask=0;
SendBuff[i]=TEXT_TO_SEND[t];
t++;
}
}
Adc_Init(); //初始化ADC
TIM5_Int_Init(10 - 1, 16800 - 1); //10Khz計數,10個us中斷一次
while(1)
{
//Get_Adc_Average(ADC_Channel_5,20);//獲取通道5的轉換值,20次取平均
//按DMA發送
MYDMA_Config(DMA2_Stream7,DMA_Channel_4,(u32)&USART1->DR,(u32)SendBuff,SEND_BUF_SIZE);//DMA2,STEAM7,CH4,外設為串口1,存儲器為SendBuff,長度為:SEND_BUF_SIZE.
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA發送
gcounter = 0;
MYDMA_Enable(DMA2_Stream7,SEND_BUF_SIZE); //開始一次DMA傳輸!
while(1)
{
if(DMA_GetFlagStatus(DMA2_Stream7,DMA_FLAG_TCIF7)!=RESET)//等待DMA2_Steam7傳輸完成
{
DMA_ClearFlag(DMA2_Stream7,DMA_FLAG_TCIF7);//清除DMA2_Steam7傳輸完成標志
printf("dma over: %d\r\n",gcounter);
break;
}
}
//按CPU發送
printf("cpu start\r\n");
gcounter = 0;
for(i = 0;i<SEND_BUF_SIZE;i++)
{
while ((USART1->SR & 0X40) == 0); //循環發送,直到發送完畢
USART1->DR = (u8)SendBuff[i];
}
printf("cpu over: %d\r\n",gcounter);
while(1);//halt
}
}
}
實驗結果,DMA的計數是355:
USART直接發送是392:
發送的數據量是8000個字節,相當於8k,當數據量更大時DMA的優勢就很明顯了。