DMA — 内存到内存搬运(SRAM → SRAM)(LL 库)
HAL vs LL:PUYA 固件库提供 HAL 和 LL 两套 API。
- HAL 库:封装度高,可移植性强,适合快速开发
- LL 库:接近寄存器操作,代码精简效率高,适合性能敏感场景
本文使用 LL 库版本。
固件库: PY32F0xx_Firmware V1.5.0
芯片: PY32F030(PY32F002A / F003 同库可参考)
源文件: PUYA/PY32F0xx_Firmware_V1.5.0/Projects/PY32F030-STK/Example_LL/DMA/DMA_SramToSram/Src/main.c
功能简介
本示例演示 DMA 最基础的能力:内存到内存(M2M)搬运。程序在源缓冲区填入 0~31 共 32 个字,然后让 DMA 控制器在后台把它们整批复制到目的缓冲区,期间 CPU 完全空闲。传输完成后在中断里校验数据是否一致:一致则点亮 LED,不一致/出错则 LED 闪烁。
与 HAL 版(HAL_DMA_Start_IT() + HAL_DMA_XferCpltCallback)相比,LL 版让你自己填 LL_DMA_InitTypeDef 并调用 LL_DMA_Init(),再显式使能 TC/TE 中断、配置 NVIC。每一步都对应一个 DMA 寄存器字段,适合想“看清 DMA 怎么配”的读者。
硬件准备
- PY32F030 开发板
- 板载绿 LED(用于指示传输结果)
- 纯软件示例,无需外部连线
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"
#define BUFFER_SIZE 32
uint32_t aSRC_Const_Buffer[BUFFER_SIZE]; /* 源缓冲 */
uint32_t aDST_Buffer[BUFFER_SIZE]; /* 目的缓冲 */
__IO uint32_t transferCompleteDetected = 0;
__IO uint32_t transferErrorDetected = 0;
__IO uint32_t transferFailedDetected = 0;
static void APP_DmaConfig(void);
static void APP_SystemClockConfig(void);
int main(void)
{
APP_SystemClockConfig();
BSP_LED_Init(LED_GREEN);
/* 源缓冲填入 0..31 */
for (uint8_t i = 0; i < BUFFER_SIZE; i++)
aSRC_Const_Buffer[i] = i;
APP_DmaConfig(); /* 配置并启动 DMA */
while (1)
{
if (transferFailedDetected == 1 && transferCompleteDetected == 1)
{
while (1) { BSP_LED_Toggle(LED_GREEN); LL_mDelay(500); } /* 数据错误 */
}
if (transferFailedDetected == 0 && transferCompleteDetected == 1)
{
BSP_LED_On(LED_GREEN); /* 数据正确 */
while (1) {}
}
if (transferErrorDetected == 1)
{
while (1) { BSP_LED_Toggle(LED_GREEN); LL_mDelay(500); } /* 传输错误 */
}
}
}
/**
* @brief DMA 配置:M2M,32 位,地址自增,普通模式
*/
static void APP_DmaConfig(void)
{
LL_DMA_InitTypeDef dma_initstruct = {0};
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1); /* 开 DMA1 时钟 */
dma_initstruct.PeriphOrM2MSrcAddress = (uint32_t)&aSRC_Const_Buffer; /* 源地址 */
dma_initstruct.MemoryOrM2MDstAddress = (uint32_t)&aDST_Buffer; /* 目的地址 */
dma_initstruct.Direction = LL_DMA_DIRECTION_MEMORY_TO_MEMORY; /* M2M */
dma_initstruct.Mode = LL_DMA_MODE_NORMAL; /* 普通模式(非循环) */
dma_initstruct.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_INCREMENT;/* 源地址自增 */
dma_initstruct.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;/* 目的地址自增 */
dma_initstruct.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD; /* 32 位 */
dma_initstruct.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD; /* 32 位 */
dma_initstruct.NbData = BUFFER_SIZE;
dma_initstruct.Priority = LL_DMA_PRIORITY_HIGH;
if (LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &dma_initstruct) != SUCCESS)
APP_ErrorHandler();
/* 使能传输完成 / 传输错误中断,并配置 NVIC */
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);
LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_1);
NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); /* 启动通道 */
}
/* 以下回调在 DMA1_Channel1_IRQHandler 中被调用 */
void APP_TransferCompleteCallback(void)
{
/* 校验目的缓冲是否等于源缓冲 */
for (uint16_t i = 0; i < BUFFER_SIZE; i++)
{
if (aDST_Buffer[i] != aSRC_Const_Buffer[i])
{
transferFailedDetected = 1;
break;
}
}
transferCompleteDetected = 1;
}
void APP_TransferErrorCallback(void)
{
transferErrorDetected = 1;
}
/* APP_SystemClockConfig / APP_ErrorHandler 略 */
|
代码分段讲解
1. 开 DMA 时钟
DMA 挂在 AHB1 上,要用 LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1) 开时钟。漏掉这步 DMA 不工作——和所有外设一样,LL 不会自动帮你开。
2. 配置结构体:每个字段一个寄存器位
LL_DMA_InitTypeDef 的字段几乎与 DMA 通道配置寄存器一一对应:
Direction = MEMORY_TO_MEMORY:内存到内存(需硬件支持 M2M 的通道)。
PeriphOrM2MSrcIncMode / MemoryOrM2MDstIncMode:源/目的地址是否自增(默认外设不自增、内存自增,M2M 下两者都自增)。
PeriphOrM2MSrcDataSize / MemoryOrM2MDstDataSize:数据宽度,这里 32 位(WORD)。
Mode = NORMAL:传完 NbData 个就停(循环模式用 LL_DMA_MODE_CIRCULAR)。
Priority:通道优先级,多通道竞争时生效。
3. 启动:使能中断 + 使能通道
HAL 里 HAL_DMA_Start_IT() 一句把“填寄存器 + 使能中断 + 使能通道”全做了;LL 里你要:先 LL_DMA_Init() 写配置,再 LL_DMA_EnableIT_TC/TE() 开中断,配 NVIC,最后 LL_DMA_EnableChannel() 真正启动。顺序很重要——先配好再使能。
4. 完成回调里校验
传输完成进 APP_TransferCompleteCallback(),这里逐字比对源/目的缓冲。注意:DMA 只是“搬数据”,不会告诉你数据对不对,校验逻辑得你自己写(HAL 同样要你自己在 XferCpltCallback 里校验)。
实验现象
- 下载运行后,DMA 自动把 32 个字从源搬到目的。
- 校验一致 → 板载绿 LED 常亮。
- 若把源缓冲改成随机/未初始化,或人为制造不匹配 → LED 闪烁(数据错误分支)。
常见问题
-
DMA 完全没动作 / LED 不亮?
- 确认
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1) 已调用。
- 确认
LL_DMA_EnableChannel() 在配置和中断使能之后调用。
- M2M 搬运需要 DMA 支持内存到内存(PY32F0 的 DMA1 支持)。
-
中断没进?
- 检查
NVIC_EnableIRQ(DMA1_Channel1_IRQn) 是否配置;确认中断服务程序里调用了 APP_TransferCompleteCallback()。
-
想连续搬运(如 ADC+DMA 循环采样)?
- 把
Mode 改为 LL_DMA_MODE_CIRCULAR,并去掉 NbData 完成后停止的逻辑。
小结
- LL 的 DMA = 开时钟 → 填 LL_DMA_InitTypeDef → LL_DMA_Init → 使能中断 → 使能通道。
- 相比 HAL 的
HAL_DMA_Start_IT,LL 让你精确控制每个寄存器字段和启动顺序。
- DMA 只负责“搬”,数据正确性要靠自己在完成回调里校验。
拓展应用
- 改成
LL_DMA_MODE_CIRCULAR 做 ADC 连续采样(参考 ADC_MultiChannelSingleConversion_TriggerSW_DMA)。
- 外设到内存:把源地址换成
&ADC1->DR、方向设为 PERIPH_TO_MEMORY,实现“ADC 自动存数组”。
- 内存到外设:把目的地址换成
&USARTx->DR,用 DMA 自动发串口大数据块。
- 多通道搬运时用不同
LL_DMA_CHANNEL_x 并分别配 NVIC 优先级。