一.DMA概念
STM32 的 DMA(Direct Memory Access,直接存儲器訪問)是一種無需 CPU 干預,直接在存儲器(如 RAM、Flash)與外設(如 UART、SPI、ADC 等)之間或存儲器之間傳輸數據的技術,能顯著減輕 CPU 負擔,提高數據傳輸效率,尤其適合高速、大數據量的場景(如傳感器數據採集、通信數據收發等)。
- 獨立於 CPU:數據傳輸由 DMA 控制器直接完成,CPU 可同時執行其他任務。
- 多通道支持:STM32 不同系列的 DMA 控制器通道數量不同(如 STM32F1 有 2 個 DMA 控制器,共 12 個通道;F4/F7/H7 等系列有更多通道和更復雜的仲裁機制)。
- 靈活的傳輸方向:
- 外設→存儲器(如 ADC 採集數據到 RAM);
- 存儲器→外設(如 RAM 數據通過 UART 發送);
- 存儲器→存儲器(如 RAM 內部數據複製)。
- 傳輸模式:
- 單次傳輸:傳輸完成後停止,需重新配置啓動;
- 循環傳輸:傳輸完成後自動重新開始,適合週期性數據(如 ADC 連續採樣)。
- 數據寬度:支持 8 位、16 位、32 位數據傳輸,可匹配外設和存儲器的數據格式。
- 中斷與標誌:傳輸完成、半傳輸、錯誤等事件可觸發中斷或通過標誌位查詢狀態。
二、DMA 控制器結構(以 STM32F1 為例)
- 2 個 DMA 控制器(DMA1 和 DMA2),DMA2 僅在大容量型號中存在。
- DMA1有7個通道,DMA2有5個通道,每個通道對應特定的外設請求(如 DMA1_CH1 可對應 TIM2_UP、ADC1 等),通道與外設的映射關係由芯片手冊定義。
- 仲裁器:當多個通道同時請求時,通過優先級(軟件設置 + 硬件固定)決定傳輸順序。
三、DMA 基本工作流程
- 配置 DMA 通道:
- 設定外設地址(如 UART 的數據寄存器 DR、ADC 的數據寄存器 DR);
- 設定存儲器地址(如 RAM 中的數組);
- 設定傳輸數據量(字節數、半字數、字數);
- 設定傳輸方向、數據寬度、是否循環模式、優先級等。
- 使能外設 DMA 請求:外設需開啓 DMA 模式(如 UART 的 DMAT 位、ADC 的 DMA 位)。
- 啓動 DMA 傳輸:使能 DMA 通道,當外設產生請求(如 UART 發送緩衝區空、ADC 轉換完成)時,DMA 自動開始傳輸。
- 傳輸完成處理:通過中斷或查詢標誌位,確認傳輸完成後進行後續操作(如處理數據、關閉 DMA 等)
四.關於基地址的解釋
“外設基地址” 和 “存儲器基地址” 是廣義上的概念,尤其是在 “存儲器到存儲器(M2M)” 模式下,它們的含義需要結合 DMA 的工作機制來理解,並非嚴格對應硬件上的 “外設”(如 GPIO、USART 等)。
- 外設基地址(DMA_PeripheralBaseAddr)在 STM32 的 DMA 架構中,“外設” 是一個相對概念:
- 當 DMA 用於 “外設到存儲器”(如 ADC 採集數據到內存)或 “存儲器到外設”(如內存數據發送到 USART)時,“外設基地址” 確實對應硬件外設的寄存器地址(如 ADC 的數據寄存器、USART 的發送寄存器)。
- 但在存儲器到存儲器(M2M)模式下(代碼中通過
DMA_M2M_Enable使能),DMA 的傳輸發生在兩個內存區域之間,此時並沒有實際的硬件外設參與。這種情況下,代碼中將 “源地址(原數組首地址AddrA)” 定義為 “外設基地址”,僅僅是因為 DMA 的邏輯框架要求區分 “源端” 和 “目的端”,這裏的 “外設” 只是作為 “源端” 的代稱,並非真正的硬件外設。
- 存儲器基地址(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.程序現象
可以看到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 控制器發送請求信號,啓動數據傳輸。
背後的工作機制:
- 硬件連接關係STM32 的 ADC 和 DMA 控制器之間存在內部硬件連線(無需軟件配置引腳)。例如,ADC1 的轉換完成信號會連接到 DMA1 的通道 1(不同通道映射需參考芯片手冊),形成固定的 “ADC→DMA 通道” 觸發鏈路。(代碼中使用
DMA1_Channel1,正是因為 ADC1 的 DMA 請求默認映射到該通道。) - 使能後的流程當
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 繼續傳輸…… 形成全自動的數據採集鏈路。
- 為何需要單獨使能?
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)的掃描模式為例,轉換流程為:
- 先轉換通道 0,結果存入
ADC1->DR; - 接着轉換通道 1,結果覆蓋
ADC1->DR中原通道 0 的數據; - 再轉換通道 2,結果覆蓋
ADC1->DR中原通道 1 的數據; - 最後轉換通道 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->DR到AD_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 參與,完全由硬件按固定節拍執行,因此能實現極高的時間精度和可靠性。