STM32 教學系列 第 7 節:DMA(直接記憶體存取)
🎯 學習目標
- 理解 DMA 工作原理 - 無需 CPU 介入的高效資料轉移
- 配置 ADC + DMA - 高速連續採樣
- 配置 UART + DMA - 快速收發數據
🎓 DMA 基礎概念
DMA 是什麼?
DMA (Direct Memory Access) 是一種硬體機制,允許外設直接存取記憶體,無需 CPU 介入。優點:
- 提高效率:CPU 可執行其他任務
- 降低延遲:資料轉移更快
- 減少功耗:CPU 工作量降低
STM32F446 DMA 資源
| 特性 | 規格 |
|---|---|
| DMA 控制器 | 2 個(DMA1、DMA2) |
| 傳輸流 | 共 16 條流(Streams) |
| 通道 | 每流 8 個通道(Channels) |
| 最大傳輸速率 | 168 MB/s |
DMA 傳輸模式
| 模式 | 說明 | 應用 |
|---|---|---|
| Normal | 單次傳輸指定數量 | 一次性資料轉移 |
| Circular | 循環傳輸,自動重新開始 | 連續採樣/流傳輸 |
| Mem-to-Mem | 記憶體到記憶體 | 資料複製 |
⚙️ ADC + DMA 配置
步驟 1:在 CubeMX 中啟用 DMA
- 開啟之前的 ADC 專案
- 選擇 Analog → ADC1
- 在 DMA Settings 中點擊 Add
- 設定:
- DMA Controller: DMA2
- Stream: Stream 0 (或其他可用流)
- Channel: Channel 0 (ADC1)
- Priority: High
步驟 2:配置 DMA 參數
- 在左側找到 DMA1 或 DMA2
- 點擊相應 Stream 進行配置:
- Mode: Circular(循環模式用於連續採樣)
- Increment Address: Enable (Memory)
- Data Width: Word (32-bit)
步驟 3:生成並修改代碼
💻 完整程式碼
main.c - ADC + DMA 高速採樣
/* STM32 Lesson 07 - ADC with DMA Circular Mode
* 功能:使用 DMA 進行高速連續 ADC 採樣
* 難度:中級-高級
*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include <stdio.h>
#include <string.h>
/* 定義 */
#define ADC_BUFFER_SIZE 100
/* 函數宣告 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_ADC1_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
void UART_Print(const char *format, ...);
/* 全域變數 */
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
UART_HandleTypeDef huart1;
/* ADC 採樣緩衝區(DMA 會自動寫入) */
uint16_t adc_buffer[ADC_BUFFER_SIZE];
volatile uint8_t adc_half_complete = 0;
volatile uint8_t adc_complete = 0;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
UART_Print("\r\n=== ADC + DMA Test ===\r\n");
/* 啟動 ADC DMA 模式 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE);
uint32_t sample_count = 0;
while (1)
{
if (adc_complete)
{
adc_complete = 0;
/* 計算平均值 */
uint32_t sum = 0;
for (uint16_t i = 0; i < ADC_BUFFER_SIZE; i++)
{
sum += adc_buffer[i];
}
uint16_t average = sum / ADC_BUFFER_SIZE;
float voltage = (average / 4095.0f) * 3.3f;
sample_count++;
UART_Print("Sample %ld: ADC=%d, V=%.2f\r\n", sample_count, average, voltage);
HAL_Delay(500);
}
}
}
/**
* @brief ADC DMA 轉移完成回調
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
adc_complete = 1;
}
}
/**
* @brief ADC DMA 半完成回調
*/
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
adc_half_complete = 1;
}
}
/**
* @brief UART 列印函數
*/
void UART_Print(const char *format, ...)
{
char buffer[100];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
HAL_UART_Transmit(&huart1, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY);
}
/**
* @brief 系統時脈配置
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = 2;
RCC_OscInitStruct.PLL.PLLQ = 7;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief GPIO 初始化
*/
static void MX_GPIO_Init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
}
/**
* @brief DMA 初始化
*/
static void MX_DMA_Init(void)
{
__HAL_RCC_DMA2_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
/**
* @brief ADC1 初始化(DMA 模式)
*/
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = ENABLE; /* 啟用 DMA 連續請求 */
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_144CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief USART1 初始化
*/
static void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
void Error_Handler(void)
{
while(1);
}
🔍 測試與除錯
✅ 預期結果:DMA 自動填充緩衝區,CPU 無需干預
🔗 延伸學習
下節預覽 - 第 8 節:I2C 通訊 + DMA
第 8 節將實現:
- I2C 時序與協定
- 裝置尋址與讀寫
- I2C + DMA 快速傳輸
✨ DMA 掌握完成!🚀