STM32 淺談MCU的DMA技術


淺談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的優勢就很明顯了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM