一.DMA概念

  STM32 的 DMA(Direct Memory Access,直接存儲器訪問)是一種無需 CPU 干預,直接在存儲器(如 RAMFlash)與外設(如 UARTSPI、ADC 等)之間或存儲器之間傳輸數據的技術,能顯著減輕 CPU 負擔,提高數據傳輸效率,尤其適合高速、大數據量的場景(如傳感器數據採集、通信數據收發等)。

  1. 獨立於 CPU:數據傳輸由 DMA 控制器直接完成,CPU 可同時執行其他任務。
  2. 多通道支持:STM32 不同系列的 DMA 控制器通道數量不同(如 STM32F1 有 2 個 DMA 控制器,共 12 個通道;F4/F7/H7 等系列有更多通道和更復雜的仲裁機制)。
  3. 靈活的傳輸方向
  • 外設→存儲器(如 ADC 採集數據到 RAM);
  • 存儲器→外設(如 RAM 數據通過 UART 發送);
  • 存儲器→存儲器(如 RAM 內部數據複製)。
  1. 傳輸模式
  • 單次傳輸:傳輸完成後停止,需重新配置啓動;
  • 循環傳輸:傳輸完成後自動重新開始,適合週期性數據(如 ADC 連續採樣)。
  1. 數據寬度:支持 8 位、16 位、32 位數據傳輸,可匹配外設和存儲器的數據格式。
  2. 中斷與標誌:傳輸完成、半傳輸、錯誤等事件可觸發中斷或通過標誌位查詢狀態。

二、DMA 控制器結構(以 STM32F1 為例)

  • 2 個 DMA 控制器(DMA1 和 DMA2),DMA2 僅在大容量型號中存在。
  • DMA1有7個通道,DMA2有5個通道,每個通道對應特定的外設請求(如 DMA1_CH1 可對應 TIM2_UP、ADC1 等),通道與外設的映射關係由芯片手冊定義。
  • 仲裁器:當多個通道同時請求時,通過優先級(軟件設置 + 硬件固定)決定傳輸順序。

STM32—DMA超詳解入門(內存->內存、內存->外設、外設->內存)_數據

三、DMA 基本工作流程

  1. 配置 DMA 通道
  • 設定外設地址(如 UART 的數據寄存器 DR、ADC 的數據寄存器 DR);
  • 設定存儲器地址(如 RAM 中的數組);
  • 設定傳輸數據量(字節數、半字數、字數);
  • 設定傳輸方向、數據寬度、是否循環模式、優先級等。
  1. 使能外設 DMA 請求:外設需開啓 DMA 模式(如 UART 的 DMAT 位、ADC 的 DMA 位)。
  2. 啓動 DMA 傳輸:使能 DMA 通道,當外設產生請求(如 UART 發送緩衝區空、ADC 轉換完成)時,DMA 自動開始傳輸。
  3. 傳輸完成處理:通過中斷或查詢標誌位,確認傳輸完成後進行後續操作(如處理數據、關閉 DMA 等)

四.關於基地址的解釋

“外設基地址” 和 “存儲器基地址” 是廣義上的概念,尤其是在 “存儲器到存儲器(M2M)” 模式下,它們的含義需要結合 DMA 的工作機制來理解,並非嚴格對應硬件上的 “外設”(如 GPIO、USART 等)。

  1. 外設基地址(DMA_PeripheralBaseAddr)在 STM32 的 DMA 架構中,“外設” 是一個相對概念:
  • 當 DMA 用於 “外設到存儲器”(如 ADC 採集數據到內存)或 “存儲器到外設”(如內存數據發送到 USART)時,“外設基地址” 確實對應硬件外設的寄存器地址(如 ADC 的數據寄存器、USART 的發送寄存器)。
  • 但在存儲器到存儲器(M2M)模式下(代碼中通過DMA_M2M_Enable使能),DMA 的傳輸發生在兩個內存區域之間,此時並沒有實際的硬件外設參與。這種情況下,代碼中將 “源地址(原數組首地址AddrA)” 定義為 “外設基地址”,僅僅是因為 DMA 的邏輯框架要求區分 “源端” 和 “目的端”,這裏的 “外設” 只是作為 “源端” 的代稱,並非真正的硬件外設。
  1. 存儲器基地址(DMA_MemoryBaseAddr)同樣是廣義概念:
  • 在常規的 “外設到存儲器” 模式中,“存儲器基地址” 通常指內存中的緩衝區(如數組、變量地址),用於存放從外設讀取的數據。
  • 在 M2M 模式中,“存儲器基地址” 對應 “目的地址(目標數組首地址AddrB)”,即數據最終要寫入的內存區域。這裏的 “存儲器” 是相對於 “源端(被當作外設的內存區域)” 而言的,本質上兩者都是內存空間。

  STM32 的 DMA 控制器設計時,需要兼容多種傳輸場景(外設↔存儲器存儲器↔外設存儲器↔存儲器),因此採用了 “外設端” 和 “存儲器端” 的通用框架來描述傳輸的兩端。在 M2M 模式下,這種框架仍然適用,只是 “外設端” 被複用為其中一個內存區域的地址,並非實際的硬件外設。

五.DMA_InitStructure.DMA_M2M

DMA_InitStructure.DMA_M2M用於設置是否啓用 存儲器到存儲器(Memory-to-Memory,簡稱 M2M)傳輸模式,其 Enable 和Disable的含義如下:

1. DMA_M2M_Enable(使能存儲器到存儲器模式)

  • 含義:允許 DMA 直接在兩個存儲器地址之間傳輸數據(例如:從一個數組複製到另一個數組,或從內存的一塊區域複製到另一塊區域)。
  • 特點
  • 無需外部硬件外設(如 ADC、USART、SPI 等)觸發,完全由軟件啓動傳輸(通過 DMA_Cmd 使能 DMA 即可開始)。
  • 傳輸的兩端都是內存地址(代碼中用 AddrA 和 AddrB 分別表示源和目的地址),此時 DMA 的 “外設端” 和 “存儲器端” 本質上都是內存空間(如前所述的 “廣義概念”)。
  • 適用於批量數據的快速複製(例如:緩存數據遷移、大數據塊搬運),效率遠高於 CPU 逐字節複製(CPU 只需啓動傳輸,後續由 DMA 硬件自動完成)。

2. DMA_M2M_Disable(禁用存儲器到存儲器模式,默認值)

  • 含義:DMA 傳輸需要由外部硬件外設的事件觸發,此時傳輸方向為 “外設←→存儲器”(而非兩個存儲器之間)。
  • 特點
  • 傳輸的一端是硬件外設的寄存器(如 ADC 的數據寄存器、USART 的發送 / 接收寄存器),另一端是內存地址。
  • 傳輸由外設的特定事件觸發(例如:ADC 轉換完成、USART 收到數據、定時器溢出等),無需 CPU 主動干預啓動。
  • 適用於外設與內存之間的數據交互(例如:ADC 採集數據自動存入內存、內存數據自動發送到 USART)。

總結

  • DMA_M2M_Enable:用於 內存到內存 的數據傳輸,軟件啓動,無需外設參與。
  • DMA_M2M_Disable:用於 外設與內存之間 的數據傳輸,由外設事件觸發,是默認且更常用的模式(適配大多數外設場景)。

六.兩數組DMA傳輸

1.MyDMA.h

#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif

2.MyDMA.c

#include "stm32f10x.h"                  // Device header
uint16_t MyDMA_Size;					//定義全局變量,用於記住Init函數的Size,供Transfer函數使用
/**
  * 函    數:DMA初始化
  * 參    數:AddrA 原數組的首地址
  * 參    數:AddrB 目的數組的首地址
  * 參    數:Size 轉運的數據大小(轉運次數)
  * 返 回 值:無
  整個流程分為 “初始化” 和 “傳輸觸發” 兩部分:
   初始化:配置 DMA 的源 / 目的地址、數據寬度、自增模式、傳輸方向等參數,保存傳輸大小,為傳輸做準備。
   傳輸觸發:重新設置計數器,啓動 DMA,等待傳輸完成並清除標誌,實現內存到內存的高效數據轉運
   (無需 CPU 干預,僅在開始和結束時佔用 CPU)。
  */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;					//將Size寫入到全局變量,記住參數Size
	/*STM32 的外設必須開啓對應時鐘才能工作。
	  DMA1 掛載在 AHB 總線上,因此通過RCC_AHBPeriphClockCmd函數開啓DMA1的時鐘。*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;			//定義結構體變量
    // 外設基地址,給定形參AddrA 這裏的 “外設” 是廣義的:
	// 在 “存儲器到存儲器(M2M)” 模式下,源地址(原數組)被當作 “外設端” 處理,
	// 因此AddrA(原數組首地址)作為源地址。
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
	/*配置每次傳輸的數據大小為 “字節(8 位)”。可選值還有半字(16 位)、字(32 位),
	   需與傳輸的數據類型匹配*/
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	/* 使能後,每次傳輸完成後,源地址(AddrA)會自動遞增(遞增步長 = 數據寬度,這裏為 1 字節),
	實現連續傳輸數組的下一個元素。若禁用,會一直傳輸源地址的同一個數據。*/
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
	/*“存儲器” 指目的地址(目標數組),因此AddrB(目標數組首地址)作為目的地址。 */
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;
	/*與外設數據寬度保持一致(均為字節),
	避免數據截斷或錯位(例如:源傳 1 字節,目的按 4 字節接收會導致數據錯誤)。 */
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	//存儲器地址自增,選擇使能
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	//數據傳輸方向,選擇由外設到存儲器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	/*配置需要傳輸的總次數(每次傳輸 1 字節,因此Size即總字節數)。
	DMA 會通過計數器遞減計數,當計數器歸零時表示傳輸完成。 */
	DMA_InitStructure.DMA_BufferSize = Size;
	/*正常模式:傳輸完成後 DMA 自動停止,計數器歸零,需重新配置計數器才能再次傳輸。
      若選循環模式(DMA_Mode_Circular),傳輸完成後會自動重啓,計數器恢復初始值,適合連續重複傳輸。 */
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
	/*使能 M2M 模式:表示傳輸在兩個存儲器(內存數組)之間進行,無需外設觸發(如 ADC、USART 等),直接由軟件啓動。
      若禁用(默認),則 DMA 傳輸需由外設事件觸發(如 USART 接收到數據時)。*/
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
	/*當多個 DMA 通道同時請求傳輸時,優先級高的通道先執行。
	可選:低(Low)、中(Medium)、高(High)、極高(VeryHigh)。*/
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure); //將結構體變量交給DMA_Init,配置DMA1的通道1(一共7個通道)
	/*DMA使能*/
	DMA_Cmd(DMA1_Channel1, DISABLE);	//這裏先不給使能,初始化後不會立刻工作,等後續調用Transfer後,再開始
}
/**
  * 函    數:啓動DMA數據轉運
  * 參    數:無
  * 返 回 值:無
  */
void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);					//DMA 在運行時無法修改計數器(BufferSize),因此需先禁用,確保修改安全。
	/*由於 DMA 在 “正常模式” 下傳輸完成後計數器會歸零,
	因此下次傳輸前需用全局變量MyDMA_Size重新設置計數器值(即傳輸大小)。*/
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	DMA_Cmd(DMA1_Channel1, ENABLE);						//DMA使能,開始工作
	/*DMA1_FLAG_TC1是 DMA1 通道 1 的 “傳輸完成標誌位”:
	  當傳輸完成(計數器歸 0)時,該標誌位會被硬件置 1;未完成時為 0。
      循環等待標誌位為 1,確保 CPU 在傳輸完成後再執行後續操作。*/
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	/*標誌位被置 1 後不會自動清零,需手動清除,避免下次傳輸時誤判為 “已完成”。*/
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

3.main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};				//定義測試數組DataA,為數據源
uint8_t DataB[] = {0, 0, 0, 0};							//定義測試數組DataB,為數據目的地
int main(void)
{
	/*模塊初始化*/
	OLED_Init();				//OLED初始化
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);	//DMA初始化,把源數組和目的數組的地址傳入
	/*顯示靜態字符串*/
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	/*顯示數組的首地址*/
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
	while (1)
	{
		DataA[0] ++;		//變換測試數據
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		OLED_ShowHexNum(2, 1, DataA[0], 2);		//顯示數組DataA
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2);		//顯示數組DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);
		Delay_ms(1000);		//延時1s,觀察轉運前的現象
		MyDMA_Transfer();	//使用DMA轉運數組,從DataA轉運到DataB
		OLED_ShowHexNum(2, 1, DataA[0], 2);		//顯示數組DataA
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2);		//顯示數組DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);
		Delay_ms(1000);		//延時1s,觀察轉運後的現象
	}
}

4.程序現象

STM32—DMA超詳解入門(內存->內存、內存->外設、外設->內存)_數據_02

可以看到DataA不斷地運往DataB

七.ADC多通道DMA轉運

1.流程

 ADC 持續採集多個模擬信號,並通過 DMA 自動將結果存入內存,無需 CPU 干預。整體流程可總結為以下6 大步驟

(1) 時鐘配置(硬件工作的前提)

  • 開啓ADC1(模數轉換器)、GPIOA(ADC 輸入引腳)、DMA1(數據傳輸控制器)的時鐘,確保三者能正常工作。
  • 配置 ADC 時鐘:將 APB2 總線時鐘(72MHz)6 分頻,得到 12MHz 的 ADCCLK(符合 ADC 最大時鐘≤14MHz 的要求)。

(2)GPIO 配置(模擬信號輸入通道)

  • PA0~PA3引腳配置為模擬輸入模式GPIO_Mode_AIN),使其與 ADC 模塊的模擬通道 0~3 連接,用於接收外部模擬信號(如電壓)。

(3) ADC 規則組通道配置(採樣序列定義)

  • 在 ADC 的 “規則組” 中,按順序配置 4 個通道:序列 1→通道 0(PA0)、序列 2→通道 1(PA1)、序列 3→通道 2(PA2)、序列 4→通道 3(PA3)。
  • 每個通道的採樣時間設為 55.5 個 ADCCLK 週期(平衡採樣精度和速度)。

(4)ADC 核心參數配置(工作模式設定)

  • 掃描模式ScanConvMode = ENABLE):ADC 按規則組序列依次轉換 4 個通道,而非只轉換第一個。
  • 連續轉換模式ContinuousConvMode = ENABLE):一次掃描完成後自動啓動下一次,實現不間斷採樣。
  • 數據右對齊:12 位轉換結果存於 16 位寄存器的低 12 位,方便直接讀取。
  • 軟件觸發:無需外部硬件信號,通過軟件啓動一次後持續工作。

(5)DMA 配置(自動傳輸橋樑)

  • 傳輸方向:外設(ADC 數據寄存器ADC1->DR)→存儲器(數組AD_Value)。
  • 地址與寬度
  • 外設地址固定為ADC1->DR(始終從這裏讀轉換結果),數據寬度 16 位(半字)。
  • 存儲器地址為AD_Value數組,地址自增(每次存下一個元素),數據寬度 16 位(匹配數組類型)。
  • 循環模式DMA_Mode_Circular):傳輸 4 個數據後自動重啓,覆蓋舊數據,保持數組始終是最新結果。
  • 使能 ADC 觸發 DMA:ADC 每次轉換完成後,自動觸發 DMA 傳輸數據。

(6)啓動與校準(進入工作狀態)

  • ADC 校準:復位並啓動校準,補償硬件誤差,保證採樣精度。
  • 啓動工作:使能 DMA 和 ADC,通過軟件觸發 ADC 開始轉換。由於 ADC 是連續模式,觸發一次後會持續掃描 4 個通道,DMA 則自動將結果存入AD_Value數組。

最終效果

系統會自動、持續地採集 PA0~PA3 的模擬信號,轉換結果實時存於AD_Value[0]~AD_Value[3]中,用户直接讀取該數組即可獲取最新採樣值,CPU 幾乎無需參與採樣過程,效率極高。

2.代碼

(1) AD.h

#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif

(2) AD.c

#include "stm32f10x.h"                  // Device header
uint16_t AD_Value[4];					//定義用於存放AD轉換結果的全局數組
/**
  * 函    數:AD初始化
  * 參    數:無
  * 返 回 值:無
  */
void AD_Init(void)
{
	/*開啓時鐘*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//開啓ADC1的時鐘
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//開啓GPIOA的時鐘
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//開啓DMA1的時鐘
	/*設置ADC時鐘*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//選擇時鐘6分頻,ADCCLK = 72MHz / 6 = 12MHz
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//將PA0、PA1、PA2和PA3引腳初始化為模擬輸入
	/*規則組通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);	//規則組序列1的位置,配置為通道0
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);	//規則組序列2的位置,配置為通道1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);	//規則組序列3的位置,配置為通道2
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);	//規則組序列4的位置,配置為通道3
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;											//定義結構體變量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;							//模式,選擇獨立模式,即單獨使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;						//數據對齊,選擇右對齊
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;			//外部觸發,使用軟件觸發,不需要外部觸發
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;							//連續轉換,使能,每轉換一次規則組序列後立刻開始下一次轉換
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;								//掃描模式,使能,掃描規則組的序列,掃描數量由ADC_NbrOfChannel確定
	ADC_InitStructure.ADC_NbrOfChannel = 4;										//通道數,為4,掃描規則組的前4個通道
	ADC_Init(ADC1, &ADC_InitStructure);											//將結構體變量交給ADC_Init,配置ADC1
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;											//定義結構體變量
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//外設基地址,給定形參AddrA
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//外設數據寬度,選擇半字,對應16為的ADC數據寄存器
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//外設地址自增,選擇失能,始終以ADC數據寄存器為源
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;					//存儲器基地址,給定存放AD轉換結果的全局數組AD_Value
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			//存儲器數據寬度,選擇半字,與源數據寬度對應
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存儲器地址自增,選擇使能,每次轉運後,數組移到下一個位置
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//數據傳輸方向,選擇由外設到存儲器,ADC數據寄存器轉到數組
	DMA_InitStructure.DMA_BufferSize = 4;										//轉運的數據大小(轉運次數),與ADC通道數一致
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;								//模式,選擇循環模式,與ADC的連續轉換一致
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								//存儲器到存儲器,選擇失能,數據由ADC外設觸發轉運到存儲器
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;						//優先級,選擇中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);								//將結構體變量交給DMA_Init,配置DMA1的通道1
	/*DMA和ADC使能*/
	DMA_Cmd(DMA1_Channel1, ENABLE);							//DMA1的通道1使能
	ADC_DMACmd(ADC1, ENABLE);								//ADC1觸發DMA1的信號使能
	ADC_Cmd(ADC1, ENABLE);									//ADC1使能
	/*ADC校準*/
	ADC_ResetCalibration(ADC1);								//固定流程,內部有電路會自動執行校準
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	/*ADC觸發*/
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//軟件觸發ADC開始工作,由於ADC處於連續轉換模式,故觸發一次後ADC就可以一直連續不斷地工作
}

(3) main.c 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
	/*模塊初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	/*顯示靜態字符串*/
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	while (1)
	{
		OLED_ShowNum(1, 5, AD_Value[0], 4);		//顯示轉換結果第0個數據
		OLED_ShowNum(2, 5, AD_Value[1], 4);		//顯示轉換結果第1個數據
		OLED_ShowNum(3, 5, AD_Value[2], 4);		//顯示轉換結果第2個數據
		OLED_ShowNum(4, 5, AD_Value[3], 4);		//顯示轉換結果第3個數據
		Delay_ms(100);							//延時100ms,手動增加一些轉換的間隔時間
	}
}

(4)現象

程序現象和上一節一樣,但是速度會變快的多

八.使能 ADC 觸發 DMA是如何做到的

在 STM32 中,“使能 ADC 觸發 DMA” 是通過硬件機制實現的:當 ADC 完成一次轉換後,會自動產生一個 DMA 請求信號,觸發 DMA 控制器將轉換結果從 ADC 數據寄存器傳輸到內存。具體實現步驟和原理如下:

核心函數:ADC_DMACmd(ADC1, ENABLE);

代碼中通過這一行實現 “使能 ADC 觸發 DMA”,其作用是允許 ADC 在轉換完成後,自動向 DMA 控制器發送請求信號,啓動數據傳輸。

背後的工作機制:

  1. 硬件連接關係STM32 的 ADC 和 DMA 控制器之間存在內部硬件連線(無需軟件配置引腳)。例如,ADC1 的轉換完成信號會連接到 DMA1 的通道 1(不同通道映射需參考芯片手冊),形成固定的 “ADC→DMA 通道” 觸發鏈路。(代碼中使用DMA1_Channel1,正是因為 ADC1 的 DMA 請求默認映射到該通道。)
  2. 使能後的流程ADC_DMACmd(ADC1, ENABLE)使能後,整個觸發 - 傳輸過程如下:
  • 步驟 1:ADC 完成轉換ADC 按配置完成一次規則組掃描(例如轉換 4 個通道),每個通道的轉換結果會依次存入 ADC 的數據寄存器(ADC1->DR)。
  • 步驟 2:ADC 自動發送 DMA 請求每次轉換完成(或掃描完一組通道後),ADC 硬件會自動產生一個 “DMA 請求信號”,通知 DMA 控制器 “有新數據待傳輸”。
  • 步驟 3:DMA 響應請求並傳輸數據DMA 控制器收到請求後,按預設配置(源地址ADC1->DR、目的地址AD_Value、數據寬度等),將數據從 ADC 寄存器傳輸到內存數組。
  • 步驟 4:循環觸發(配合連續模式)由於代碼中 ADC 配置為連續轉換模式,且 DMA 配置為循環模式,上述過程會不斷重複:ADC 轉換→觸發 DMA 傳輸→ADC 繼續轉換→DMA 繼續傳輸…… 形成全自動的數據採集鏈路。
  1. 為何需要單獨使能?ADC_DMACmd是一個開關:
  • 禁用(DISABLE)時,ADC 轉換完成後不會發送 DMA 請求,數據需通過 CPU 手動讀取(ADC_GetConversionValue)。
  • 使能(enable)時,才允許 ADC 觸發 DMA 自動傳輸,解放 CPU。

總結

“使能 ADC 觸發 DMA” 本質是通過ADC_DMACmd函數打開 ADC 到 DMA 的硬件請求鏈路。之後,ADC 每完成一次轉換,就會自動向 DMA 發送信號,觸發 DMA 將數據從ADC1->DR傳輸到內存(如AD_Value數組),整個過程無需 CPU 干預,實現了高效的 “ADC 採集→DMA 傳輸” 自動化流程。

九.關於ADC->DR的疑問

  在多通道 ADC 轉換中(如代碼中 4 個通道的掃描模式),ADC 的數據寄存器(ADC1->DR)是逐個覆蓋存儲轉換結果的,而非同時存儲多個數據。具體過程如下:

關鍵原理:ADC_DR是單寄存器,多通道時按順序更新

STM32 的 ADC 數據寄存器(DR)是一個 16 位的單寄存器,無論多少個通道,每次轉換完成後,當前通道的結果會直接寫入DR,並覆蓋上一個通道的結果

以代碼中 4 個通道(通道 0~3)的掃描模式為例,轉換流程為:

  1. 先轉換通道 0,結果存入ADC1->DR
  2. 接着轉換通道 1,結果覆蓋ADC1->DR中原通道 0 的數據;
  3. 再轉換通道 2,結果覆蓋ADC1->DR中原通道 1 的數據;
  4. 最後轉換通道 3,結果覆蓋ADC1->DR中原通道 2 的數據。

為何 DMA 能正確獲取所有通道數據?

雖然DR會被逐個覆蓋,但配合 DMA 的 “連續請求” 機制,可確保所有數據被正確傳輸到內存:

  • 掃描模式 + DMA 使能下,ADC 每完成一個通道的轉換(而非等待所有通道掃描完),就會立即觸發一次 DMA 請求;
  • DMA 收到請求後,會立即將當前DR中的數據(當前通道的結果)傳輸到內存數組(如AD_Value),並通過 “存儲器地址自增” 功能,依次存到AD_Value[0]AD_Value[1]AD_Value[2]AD_Value[3]
  • 當 DMA 傳輸完成 4 次(對應 4 個通道)後,由於配置了 “循環模式”,會重新開始下一輪傳輸,覆蓋舊數據。

總結

多通道掃描時,ADC1->DR會被逐個通道的結果覆蓋,但 DMA 會在每個通道轉換完成後立即 “搶讀” 數據並傳輸到內存,因此不會丟失數據。最終AD_Value數組中會按通道順序保存完整的 4 個結果,實現多通道數據的連續採集與存儲。

十.ADC和DMA為何能準確同步

硬件級同步機制固定時序設計,具體原因如下:

1. 硬件觸發:無軟件延遲的請求響應

ADC 與 DMA 之間的觸發關係是純硬件連接,而非軟件干預:

  • 當 ADC 完成一個通道的轉換後,硬件會自動產生一個 “DMA 請求信號”(無需 CPU 指令),這個信號直接連接到 DMA 控制器的對應通道(如代碼中 ADC1→DMA1_Channel1)。
  • DMA 控制器收到請求後,會立即啓動數據傳輸(從ADC1->DRAD_Value數組),整個過程由硬件電路驅動,響應時間固定(通常在幾個時鐘週期內),幾乎無延遲。

這種 “硬件觸發 - 硬件響應” 的機制,避免了軟件中斷或函數調用帶來的隨機延遲,保證了觸發的及時性。

2. ADC 轉換節奏固定:傳輸請求的時間間隔可預測

ADC 的轉換時序是嚴格由時鐘和配置決定的,每個通道的轉換時間固定,因此 DMA 請求的間隔也固定:

  • 單通道轉換時間 = 採樣時間 + 12.5 個 ADCCLK 週期(ADC 核心轉換時間,固定值)。例如代碼中採樣時間為 55.5 個 ADCCLK(12MHz 下約 4.625μs),則單通道轉換時間 = 55.5 + 12.5 = 68 個 ADCCLK ≈ 5.67μs。
  • 4 通道掃描總時間 = 4 × 單通道轉換時間(約 22.67μs),且連續轉換模式下,每輪掃描的間隔完全一致。

因此,ADC 觸發 DMA 的時間間隔(即每個通道的轉換完成時刻)是嚴格固定的,形成了 “週期性的 DMA 請求”,節奏穩定。

3. DMA 傳輸速度匹配:確保數據不丟失、不錯位

DMA 的傳輸配置與 ADC 的轉換節奏完全匹配,保證數據能被及時取走:

  • 傳輸速度:DMA 掛載在 AHB 總線(最高 72MHz),傳輸一個 16 位數據(半字)僅需 1-2 個總線週期(約 14ns),遠快於 ADC 的轉換間隔(約 5.67μs)。因此,DMA 有充足時間在 “下一個通道轉換完成前” 完成當前數據的傳輸,不會出現 “前一個數據未傳完,新數據已覆蓋ADC1->DR” 的情況。
  • 地址自增與數量匹配:DMA 配置為 “存儲器地址自增” 且BufferSize=4,與 ADC 的 4 個通道一一對應。每次傳輸後,DMA 自動指向數組的下一個位置,確保 4 個通道的數據按順序存入AD_Value[0]~[3],不會錯位。

4. 同步模式設計:ADC 與 DMA 的 “閉環聯動”

代碼中 ADC 和 DMA 的模式配置形成了完美的同步閉環:

  • ADC 配置為 “連續轉換 + 掃描模式”:轉換完 4 個通道後,立即自動開始下一輪掃描,持續產生週期性的 DMA 請求。
  • DMA 配置為 “循環模式”:傳輸完 4 個數據後,自動復位地址指針和計數器,準備接收下一輪 ADC 的請求,與 ADC 的連續轉換形成 “無縫銜接”。

這種 “ADC 連續轉換→DMA 循環傳輸” 的聯動,使得兩者始終保持節奏一致,不會出現 “ADC 等待 DMA” 或 “DMA 等待 ADC” 的情況。

總結

ADC 與 DMA 的精準同步,本質是硬件觸發的即時性ADC 轉換時序的固定性DMA 傳輸速度的匹配性以及模式配置的同步性共同作用的結果。整個過程無需 CPU 參與,完全由硬件按固定節拍執行,因此能實現極高的時間精度和可靠性。