PUYA DMA — 内存到内存搬运(SRAM→SRAM)(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 DMA 内存到内存搬运(DMA_SramToSram)示例教学(LL 库版本)

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 闪烁(数据错误分支)。

常见问题

  1. DMA 完全没动作 / LED 不亮?

    • 确认 LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1) 已调用。
    • 确认 LL_DMA_EnableChannel() 在配置和中断使能之后调用。
    • M2M 搬运需要 DMA 支持内存到内存(PY32F0 的 DMA1 支持)。
  2. 中断没进?

    • 检查 NVIC_EnableIRQ(DMA1_Channel1_IRQn) 是否配置;确认中断服务程序里调用了 APP_TransferCompleteCallback()
  3. 想连续搬运(如 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 优先级。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计