PUYA DMA — 内存到内存搬运 DMA_SramToSram

PUYA PY32F403_Firmware V1.4.5 DMA 内存到内存搬运示例教学

DMA — 内存到内存搬运 DMA_SramToSram

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)
源文件: PY32F403-STK/Example/DMA/DMA_SramToSram/Src/main.c
核心外设: DMA1(通道 1,M2M 模式)


1. 功能简介(大白话)

本例演示 DMA 最朴素的用法:内存 → 内存 的数据搬运。

程序准备两个数组:源数组 aSRC_Const_Buffer[32](先填成 0,1,2,…,31),目的数组 aDST_Buffer[32](初始为 0)。然后让 DMA 把这 32 个「字(32 位)」从源数组自动搬到目的数组。搬完后在中断里核对两边是否一致:一致则点亮 LED,不一致/出错则让 LED 闪烁。

这个例子里没有真正的「外设」,纯粹演示 DMA 控制器本身怎么干活——理解它之后,把「源」换成 ADC 数据寄存器、「目的」换成数组,就是真实项目里的用法。

2. 硬件准备

物品 说明
PY32F403-STK 开发板 自带 LED(PA1)
USB 线 供电 + 下载

现象观察:板载 LED(绿色)。无外部连线,纯片上运行。

注意:本例是纯内存搬运,不需要接任何外设,烧录后直接看 LED 状态即可。

3. 完整代码(带中文注释)

  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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include "main.h"

/* 全局变量(BUFFER_SIZE 在 main.h 中定义为 32)*/
DMA_HandleTypeDef DmaHandle;
uint32_t aSRC_Const_Buffer[BUFFER_SIZE];   /* 源数组 */
uint32_t aDST_Buffer[BUFFER_SIZE];         /* 目的数组(初始全 0)*/
__IO uint32_t transferCompleteDetected = 0; /* 传输完成标志 */
__IO uint32_t transferErrorDetected    = 0; /* 传输出错标志 */
__IO uint32_t transferFailedDetected   = 0; /* 数据校验失败标志 */

/* 函数声明 */
static void APP_DmaConfig(void);
static void APP_TransferComplete(DMA_HandleTypeDef *DmaHandle);
static void APP_TransferError(DMA_HandleTypeDef *DmaHandle);
static void APP_SystemClockConfig(void);

int main(void)
{
  /* 1) HAL 初始化 */
  HAL_Init();
  /* 2) 系统时钟:HSI 8MHz(本例 DMA 搬运速度与时钟无关,仅维持运行)*/
  APP_SystemClockConfig();

  /* 3) LED 初始化 */
  BSP_LED_Init(LED_GREEN);

  /* 4) 源数组填初值 0,1,2,...,31 */
  for (uint8_t i = 0; i < BUFFER_SIZE; i++)
    aSRC_Const_Buffer[i] = i;

  /* 5) 配置并启动 DMA */
  APP_DmaConfig();
  if (HAL_DMA_Start_IT(&DmaHandle,
                       (uint32_t)&aSRC_Const_Buffer,
                       (uint32_t)&aDST_Buffer,
                       BUFFER_SIZE) != HAL_OK)
    APP_ErrorHandler();

  /* 6) 主循环:根据标志位判断结果(CPU 此时已「脱手」,DMA 后台搬运)*/
  while (1)
  {
    /* 传完了,但数据对不上 → 失败闪烁 */
    if (transferFailedDetected == 1 && transferCompleteDetected == 1)
    {
      while (1) { BSP_LED_Toggle(LED_GREEN); HAL_Delay(500); }
    }
    /* 传完且数据正确 → 常亮 */
    if (transferFailedDetected == 0 && transferCompleteDetected == 1)
    {
      BSP_LED_On(LED_GREEN);
      while (1) {}
    }
    /* 传输出错 → 另一种闪烁(这里与失败同为闪烁表现)*/
    if (transferErrorDetected == 1)
    {
      BSP_LED_On(LED_GREEN);
      while (1) { BSP_LED_Toggle(LED_GREEN); HAL_Delay(500); }
    }
  }
}

/**
  * @brief  DMA 配置:M2M、地址自增、字对齐、普通模式、高优先级
  */
static void APP_DmaConfig(void)
{
  __HAL_RCC_DMA1_CLK_ENABLE();                 /* 开 DMA1 时钟 */

  DmaHandle.Init.Direction = DMA_MEMORY_TO_MEMORY;   /* 内存→内存 */
  DmaHandle.Init.PeriphInc = DMA_PINC_ENABLE;        /* 源地址自增 */
  DmaHandle.Init.MemInc    = DMA_MINC_ENABLE;        /* 目的地址自增 */
  DmaHandle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; /* 源 32 位宽 */
  DmaHandle.Init.MemDataAlignment    = DMA_MDATAALIGN_WORD; /* 目的 32 位宽 */
  DmaHandle.Init.Mode     = DMA_NORMAL;              /* 普通模式(一次)*/
  DmaHandle.Init.Priority = DMA_PRIORITY_HIGH;       /* 高优先级 */

  DmaHandle.Instance = DMA1_Channel1;               /* 选 DMA1 通道1 */
  if (HAL_DMA_Init(&DmaHandle) != HAL_OK) APP_ErrorHandler();

  /* 注册「传输完成」和「传输出错」两个回调 */
  HAL_DMA_RegisterCallback(&DmaHandle, HAL_DMA_XFER_CPLT_CB_ID,  APP_TransferComplete);
  HAL_DMA_RegisterCallback(&DmaHandle, HAL_DMA_XFER_ERROR_CB_ID, APP_TransferError);

  /* 开 DMA1 通道1 中断 */
  HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

/**
  * @brief  传输完成回调:逐字比对目的数组与源数组
  */
static void APP_TransferComplete(DMA_HandleTypeDef *DmaHandle)
{
  for (uint16_t i = 0; i < BUFFER_SIZE; i++)
  {
    if (aDST_Buffer[i] != aSRC_Const_Buffer[i])
    {
      transferFailedDetected = 1;   /* 有不一致 → 标记失败 */
      break;
    }
  }
  transferCompleteDetected = 1;     /* 无论对错,先标记「已完成」*/
}

/**
  * @brief  传输出错回调
  */
static void APP_TransferError(DMA_HandleTypeDef *DmaHandle)
{
  transferErrorDetected = 1;
}

/**
  * @brief  系统时钟:HSI 8MHz(与 DMA 搬运逻辑无强相关)
  */
static void APP_SystemClockConfig(void)
{
  RCC_OscInitTypeDef  OscInitstruct = {0};
  RCC_ClkInitTypeDef  ClkInitstruct = {0};

  OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  OscInitstruct.HSIState       = RCC_HSI_ON;
  OscInitstruct.HSI48MState    = RCC_HSI48M_OFF;
  OscInitstruct.LSEState       = RCC_LSE_OFF;
  OscInitstruct.LSIState       = RCC_LSI_OFF;
  OscInitstruct.PLL.PLLState   = RCC_PLL_OFF;
  if (HAL_RCC_OscConfig(&OscInitstruct) != HAL_OK) APP_ErrorHandler();

  ClkInitstruct.ClockType      = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
                                 RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  ClkInitstruct.SYSCLKSource   = RCC_SYSCLKSOURCE_HSI;
  ClkInitstruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;
  ClkInitstruct.APB1CLKDivider = RCC_HCLK_DIV1;
  ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2;
  if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}

void APP_ErrorHandler(void) { while (1) {} }

说明:main.h 里还定义了 DMA_INSTANCE = DMA_Channel2 等宏,但本例 main.c 实际硬编码使用了 DMA1_Channel1(示例以源码为准)。若改用 DMA_Channel2,需同步修改中断号与 NVIC 配置。

4. 代码分段讲解

4.1 为什么「地址自增」必须开?

源数组有 32 个元素,地址连续。如果不开启 PeriphInc/MemInc,DMA 会一直搬同一个地址的数据 32 次,结果目的数组全是第一个元素。开了自增,DMA 每搬一个字就把地址 +4(32 位 = 4 字节),才能把整个数组顺序搬过去。

4.2 数据宽度为什么选 WORD(32 位)?

数组元素是 uint32_t(4 字节)。必须让 PeriphDataAlignment/MemDataAlignment 与实际元素宽度一致,否则搬的字节数/地址步进会错。若元素是 uint8_t 数组,就要用 BYTE 对齐。

4.3 普通模式 vs 循环模式

DMA_NORMAL = 搬完 BUFFER_SIZE 个就停;DMA_CIRCULAR = 搬完自动从头再来(适合 ADC 连续采样、音频环形缓冲)。本例只搬一次,用普通模式。

4.4 中断回调是怎么被调用的?

HAL_DMA_Start_IT 启动后,DMA 后台搬运;硬件搬完最后一个数据时触发 DMA 中断 → HAL 在中断服务程序里调用你注册的 APP_TransferComplete。整个过程 CPU 主循环完全没被阻塞,这就是 DMA 的精髓。

4.5 校验逻辑

完成回调里逐字比较 aDST_Buffer[i] == aSRC_Const_Buffer[i]。全部相等 → 只置 transferCompleteDetected;一旦不等 → 额外置 transferFailedDetected。主循环据此决定 LED 常亮(成功)还是闪烁(失败)。

5. 实验现象

  • 烧录运行后:
    • LED 常亮 = DMA 传输完成且 32 个字全部正确搬移;
    • LED 闪烁 = 传输出错或数据不一致(正常正确运行下应为常亮)。

6. 常见问题

现象 原因 解决
LED 一直闪(失败) 地址没自增 / 对齐错 确认 PeriphInc/MemInc=ENABLE、对齐 WORD
目的数组全是同一个值 忘了开地址自增 打开 DMA_PINC_ENABLE/DMA_MINC_ENABLE
完全没反应 没开 DMA 时钟 / 没使能 NVIC 确认 __HAL_RCC_DMA1_CLK_ENABLE()HAL_NVIC_EnableIRQ
想用别的通道 只改 Instance 同步改中断号与 NVIC,确保通道可用
数据错位 源/目的宽度不一致 两边对齐宽度必须相同
想持续搬运 改 CIRCULAR Mode 设为 DMA_CIRCULAR

7. 小结

  • DMA 是不占 CPU 的数据搬运引擎;M2M 模式下「源/目的」都是内存;
  • 四个关键配置:方向、地址自增、数据对齐、模式
  • 必须用中断回调HAL_DMA_RegisterCallback + HAL_DMA_Start_IT + NVIC)才能「搬完通知」;
  • 本例把 32 个 32 位数据从源数组搬到目的数组并校验,LED 常亮即成功;
  • 真实项目把「源」换成 ADC/SPI 等外设寄存器地址,即可实现外设↔内存零 CPU 搬运。

8. 拓展应用(学了能做什么项目)

  • ADC+DMA 多通道扫描:参考 ADC_MultiChannelsSingleConversion_TriggerSW_DMA,连续采样不丢点;
  • SPI/I2C 大数据收发:参考 SPI_xxx_DMAI2C_xxx_DMA,刷屏/读 Flash 不卡主循环;
  • 串口打印缓冲:把要发送的字符块交给 DMA_UART 搬,释放 CPU;
  • 音频环形缓冲:DMA 循环模式 + 双缓冲,做播放/录音;
  • RAM 块拷贝 / 帧缓冲搬运:图形界面里整屏数据移动;
  • TIM 触发 ADC+DMA:定时器定时触发采样,DMA 自动存,做示波器/采集卡。

返回:DMA 总览

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计