STM32 DMA 零基础入门(HAL库)
·
一、什么是 DMA?——先用生活比喻理解
🏪 快递站的比喻
没有 DMA 的世界(CPU 搬运数据):
┌─────────┐
│ 老板(CPU) │ ——> 老板亲自去仓库取货,送到柜台,再回来继续工作
│ │ 每搬一次货就要停下手头的工作
└─────────┘
⚠️ 老板很累,效率低!
有 DMA 的世界:
┌─────────┐ ┌─────────┐
│ 老板(CPU) │ │ 搬运工(DMA)│ ——> 搬运工负责搬货
│ 专心算账 │ │ 自动搬运 │ 老板可以专心做别的事
└─────────┘ └─────────┘
✅ 老板轻松,效率高!
📖 正式定义
DMA = Direct Memory Access(直接存储器访问)
核心思想:数据搬运不需要 CPU 参与
DMA 控制器自动在 "外设" 和 "内存" 之间搬运数据
二、DMA 的基本概念
2.1 数据搬运的三个要素
┌──────────────────────────────────────────────────┐
│ DMA 搬运数据三要素 │
├──────────────────────────────────────────────────┤
│ │
│ 1️⃣ 从哪里搬?(源地址 Source) │
│ - 可以是外设的数据寄存器(如 UART 的 DR 寄存器) │
│ - 也可以是内存中的数组 │
│ │
│ 2️⃣ 搬到哪里?(目的地址 Destination) │
│ - 可以是内存中的数组 │
│ - 也可以是外设的数据寄存器 │
│ │
│ 3️⃣ 搬多少?(数据长度 Length) │
│ - 要传输的数据个数 │
│ │
└──────────────────────────────────────────────────┘
2.2 DMA 的传输方向(3 种)
方向 1:外设 → 内存(最常用)
┌──────────┐ DMA ┌──────────┐
│ UART 接收 │ ────────→ │ 内存数组 │
│ ADC 采集 │ │ buffer[] │
└──────────┘ └──────────┘
例子:串口接收数据存到数组、ADC 采集结果存到数组
方向 2:内存 → 外设
┌──────────┐ DMA ┌──────────┐
│ 内存数组 │ ────────→ │ UART 发送 │
│ buffer[] │ │ SPI 发送 │
└──────────┘ └──────────┘
例子:把数组中的数据通过串口发送出去
方向 3:内存 → 内存
┌──────────┐ DMA ┌──────────┐
│ 数组 A │ ────────→ │ 数组 B │
│ src[] │ │ dst[] │
└──────────┘ └──────────┘
例子:快速复制一块内存数据
2.3 DMA 的工作模式
模式 1:普通模式(Normal)
┌─────────────────────────────────────┐
│ 搬完指定数量的数据后 → 自动停止 │
│ 想再搬一次 → 需要重新启动 │
│ │
│ 数据: [1][2][3][4][5] → 搬完 → 停止! │
└─────────────────────────────────────┘
适用场景:发送一段固定数据
模式 2:循环模式(Circular)
┌─────────────────────────────────────┐
│ 搬完指定数量后 → 自动从头开始继续搬 │
│ 不需要重新启动,一直循环 │
│ │
│ 数据: [1][2][3] → [1][2][3] → [1]...│
│ 不停地循环搬运 │
└─────────────────────────────────────┘
适用场景:ADC 连续采集、持续接收数据
2.4 地址是否自增
┌─────────────────────────────────────────────────┐
│ 外设地址自增? │
│ 通常 ❌ 不自增 │
│ 因为外设数据寄存器地址是固定的 │
│ 比如 UART 的数据寄存器永远是那一个地址 │
│ │
│ 内存地址自增? │
│ 通常 ✅ 要自增 │
│ 因为要把数据存到数组的不同位置 │
│ buffer[0], buffer[1], buffer[2]... │
└─────────────────────────────────────────────────┘
图示说明:
外设(固定地址) 内存(地址自增)
┌──────┐ ┌──────┐
│ 0x4001│ ──→ 数据 1 ──→│ [0] │ 地址+1
│ 0x4001│ ──→ 数据 2 ──→│ [1] │ 地址+1
│ 0x4001│ ──→ 数据 3 ──→│ [2] │ 地址+1
└──────┘ └──────┘
三、STM32 中的 DMA 资源
3.1 DMA 控制器和通道
以 STM32F103 为例:
┌───────────────────────────────────────┐
│ DMA1(7 个通道) │
├───────────────────────────────────────┤
│ 通道 1: ADC1 │
│ 通道 2: SPI1_RX / USART3_TX / TIM1_CH1 │
│ 通道 3: SPI1_TX / USART3_RX / TIM1_CH2 │
│ 通道 4: SPI2_RX / USART1_TX / I2C2_TX │ ← 每个通道可以
│ 通道 5: SPI2_TX / USART1_RX / I2C2_RX │ 连接多个外设
│ 通道 6: USART2_RX / I2C1_TX │ 但同一时间
│ 通道 7: USART2_TX / I2C1_RX │ 只能用一个
└───────────────────────────────────────┘
以 STM32F4 为例:
┌───────────────────────────────────────┐
│ DMA1(8 个数据流 Stream,每个流 8 个通道) │
│ DMA2(8 个数据流 Stream,每个流 8 个通道) │
├───────────────────────────────────────┤
│ 比 F1 更灵活,用 Stream + Channel 组合 │
└───────────────────────────────────────┘
⚠️ 重点:不是随便选通道的!每个外设对应固定的 DMA 通道,要查数据手册或 CubeMX 自动分配。
四、CubeMX 配置 DMA(图文步骤)
以"串口 1 DMA 接收"为例
步骤 1:打开 CubeMX,选好芯片,配置好 USART1
┌────────────────────────────────┐
│ Connectivity → USART1 │
│ Mode: Asynchronous │
│ Baud Rate: 115200 │
└────────────────────────────────┘
步骤 2:在 USART1 配置页面,点击 "DMA Settings" 选项卡
┌────────────────────────────────┐
│ [DMA Settings] │
│ │
│ 点击 "Add" 按钮 │
│ 选择 USART1_RX │
└────────────────────────────────┘
步骤 3:配置 DMA 参数
┌────────────────────────────────────────────┐
│ DMA Request: USART1_RX │
│ Stream/Channel: 自动分配(不用改) │
│ Direction: Peripheral To Memory(外设→内存)│
│ Priority: Low/Medium/High/Very High │
│ │
│ Mode: Normal 或 Circular │
│ │
│ Peripheral: │
│ Increment Address: ❌ 不勾选(外设地址不自增)│
│ Data Width: Byte(字节,8 位) │
│ │
│ Memory: │
│ Increment Address: ✅ 勾选(内存地址自增) │
│ Data Width: Byte(字节,8 位) │
└────────────────────────────────────────────┘
步骤 4:开启 NVIC 中断(可选但推荐)
┌────────────────────────────────┐
│ [NVIC Settings] │
│ ✅ USART1 global interrupt │
│ ✅ DMA1 Stream/Channel interrupt│
└────────────────────────────────┘
步骤 5:生成代码
五、生成代码解析
5.1 CubeMX 生成的关键代码
/* ============= 1. DMA 句柄声明 ============= */
DMA_HandleTypeDef hdma_usart1_rx; // DMA 句柄,代表"这个 DMA 通道"
UART_HandleTypeDef huart1; // 串口句柄
/* ============= 2. DMA 初始化(自动生成) ============= */
void MX_DMA_Init(void) // ⚠️ 这个函数要在外设初始化之前调用!
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE(); // 开启 DMA1 时钟
/* DMA interrupt init */
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn); // 使能 DMA 中断
}
/* ============= 3. 串口初始化中关联 DMA ============= */
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
// ... GPIO 初始化 ...
/* USART1 DMA Init */
/* USART1_RX Init */
hdma_usart1_rx.Instance = DMA1_Channel5; // 用哪个 DMA 通道
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; // 方向:外设→内存
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不自增
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址自增
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据宽度:字节
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 内存数据宽度:字节
hdma_usart1_rx.Init.Mode = DMA_NORMAL; // 普通模式
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW; // 优先级:低
HAL_DMA_Init(&hdma_usart1_rx); // 初始化 DMA
__HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx); // 🔑 关键!把 DMA 和串口关联起来
}
5.2 理解 __HAL_LINKDMA 宏
// 这个宏做了两件事:
__HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx);
// 展开后等价于:
huart1.hdmarx = &hdma_usart1_rx; // 串口的 DMA 接收指针 → 指向 DMA 句柄
hdma_usart1_rx.Parent = &huart1; // DMA 的父指针 → 指向串口句柄
// 图示:
// ┌────────────┐ ┌──────────────────┐
// │ huart1 │ ──────→ │ hdma_usart1_rx │
// │ .hdmarx ───┤ │ .Parent ────────┤──→ 指回 huart1
// └────────────┘ └──────────────────┘
// 双向关联,互相知道对方
六、DMA HAL 库常用 API 函数
6.1 函数总览
/* ========== 串口 + DMA 相关函数 ========== */
// 1. DMA 方式发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
// 2. DMA 方式接收(固定长度)
HAL_UART_Receive_DMA(&huart1, rx_buf, len);
// 3. DMA 方式接收(不定长度)—— 非常实用!
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, buf_size);
/* ========== ADC + DMA 相关函数 ========== */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, len);
/* ========== SPI + DMA 相关函数 ========== */
HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len);
HAL_SPI_Receive_DMA(&hspi1, rx_buf, len);
/* ========== 通用 DMA 函数 ========== */
HAL_DMA_Start(&hdma, src_addr, dst_addr, len); // 启动 DMA 传输
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, timeout); // 轮询等待完成
HAL_DMA_Abort(&hdma); // 中止 DMA 传输
6.2 回调函数(中断完成后自动调用)
/* ========== 串口 DMA 回调函数 ========== */
// 发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
// DMA 发送完成后,HAL 库自动调用这个函数
// 你在这里写"发送完成后要做的事"
}
// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// DMA 接收完指定数量数据后,自动调用
}
// 半传输完成回调(接收了一半数据时)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
// 用于双缓冲处理
}
// 空闲中断回调(接收不定长数据时用)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// Size = 实际接收到的数据长度
}
七、实战案例
案例 1:DMA 串口发送
/* ===== 最简单的 DMA 发送 ===== */
uint8_t tx_data[] = "Hello DMA!\r\n";
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_DMA_Init(); // ⚠️ DMA 初始化必须在串口之前!
MX_USART1_UART_Init();
while (1)
{
// DMA 方式发送,CPU 不用等待,发完自动停止
HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data) - 1);
HAL_Delay(1000); // CPU 可以做别的事情
}
}
// 可选:发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
// 发送完成,可以设置标志位或做其他操作
// 比如点亮 LED 表示发送成功
}
}更多推荐


所有评论(0)