GD32 DMA — Flash → RAM

GD32 GD32F30x_Firmware_Library V3.0.3 DMA Flash → RAM 示例教学

DMA — Flash → RAM

固件库: GD32F30x_Firmware_Library V3.0.3
芯片: GD32F30x(以 GD32F307 评估板为例)
源文件: GD32F30x_Firmware_Library_V3.0.3/Examples/DMA/Flash_to_ram/main.c
配套: gd32f30x_it.c 中的 DMA0_Channel0_IRQHandler(置完成标志)


功能简介

本示例演示 DMA 最纯粹的用途:把一块 Flash 上的数据,自动搬到 RAM 数组里。为了"造出"可搬运的 Flash 内容,例程先做了三件事:

  1. 擦除 Flash 的 0x08004000 起始处的若干页(擦成全 0xFF)。
  2. 编程 这些 Flash 页,每 4 字节写一个固定值 0x3210ABCD
  3. DMA0 通道0(内存到内存模式)把这 0x400(1024)字节从 Flash 自动搬进 RAM 数组 g_destbuf[]

搬运结束后,在 DMA 完成中断里把 g_dmacomplete_flag 置 1,主循环解除等待,再逐字节比对:如果 g_destbuf 里全是 0x3210ABCD,说明 DMA 搬运成功 → 4 颗 LED 全亮;否则只亮 LED2、LED4 表示出错。

为什么搬 Flash 还要先擦再写? 因为 Flash 只能把 1 写成 0、不能把 0 直接写成 1,所以写入前必须整页擦除(擦除后全为 1,即 0xFF)。这是 Flash 的物理特性,和前面 FMC 例程一致。本例的重点其实不是 Flash 操作,而是"DMA 怎么搬"——Flash 那部分只是给 DMA 准备一个数据源。


硬件准备

  • GD32F30x 评估板一块(LED2/LED3/LED4/LED5 由 gd_eval_led_init 初始化)。
  • 无需任何外部接线。本例完全是片内 Flash ↔ RAM 的搬运,下载程序即可观察 LED。
  • 若用调试器(如 J-Link / DAP),可在搬运前后观察 g_destbuf[]0x08004000 处的内容,验证搬运正确性。

⚠️ 地址安全提醒BANK0_WRITE_START_ADDR = 0x08004000。这个地址在 Flash 靠前的"用户区",务必确认它不在你的程序代码区,否则擦写会破坏正在运行的代码。评估板例程默认这里空闲;你自己移植时请选程序末尾之后的空闲页。


完整代码(带中文注释)

注:DMA0_Channel0_IRQHandlergd32f30x_it.c 里,搬运完成后被调用,把标志位置 1。此处一并列出。

  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
138
139
140
141
142
143
144
145
#include "gd32f30x.h"
#include <string.h>
#include "gd32f307c_eval.h"

#define TRANSFER_NUM           0x400    /* 搬运总字节数:1024 字节 */
#define FMC_PAGE_SIZE          ((uint16_t)0x800)        /* Flash 每页 2KB */
#define BANK0_WRITE_START_ADDR ((uint32_t)0x08004000)   /* 被读/写的 Flash 起始地址 */

void rcu_config(void);
void nvic_config(void);
void led_config(void);

/* DMA 完成标志:中断里置 1,主循环里等待它 */
__IO uint32_t g_dmacomplete_flag = 0;
/* DMA 的目标缓冲区(在 RAM 里) */
uint8_t g_destbuf[TRANSFER_NUM];
/* 要写进 Flash 的固定数据 */
const uint32_t transdata = 0x3210ABCD;
fmc_state_enum fmcstatus = FMC_READY;

int main(void)
{
    uint32_t i, count;
    uint32_t *ptrd;
    uint32_t address = 0x00;
    ErrStatus access_flag = SUCCESS;
    dma_parameter_struct dma_init_struct;
    uint32_t wperror = 0;

    rcu_config();        /* 开 DMA0 时钟 */
    nvic_config();       /* 配置 DMA0 通道0 中断 */
    led_config();        /* LED 初始化 */
    fmc_unlock();        /* 解锁 Flash 控制器(写 Flash 前必须) */

    /* ---- 第 1 步:擦除 Flash 页(擦成 0xFF)---- */
    count = TRANSFER_NUM / FMC_PAGE_SIZE;   /* 1024/2048 = 0,循环会擦 2 页(0,1) */
    fmc_flag_clear(FMC_FLAG_BANK0_PGERR | FMC_FLAG_BANK0_WPERR | FMC_FLAG_BANK0_END);
    for(i = 0; i <= count; i++){
        fmcstatus = fmc_page_erase(BANK0_WRITE_START_ADDR + (FMC_PAGE_SIZE * i));
        wperror += (fmcstatus == FMC_WPERR);
        fmc_flag_clear(FMC_FLAG_BANK0_PGERR | FMC_FLAG_BANK0_WPERR | FMC_FLAG_BANK0_END);
    }
    if(wperror != 0){ while(1); }   /* 擦除出错就卡死 */
    fmc_lock();                      /* 擦完先上锁 */

    /* 校验:擦除后读出来应该全是 0xFFFFFFFF */
    ptrd = (uint32_t*)BANK0_WRITE_START_ADDR;
    count = TRANSFER_NUM / sizeof(*ptrd);   /* 按 32 位(4字节)算个数 */
    for(i = 0; i < count; i++){
        if(0xFFFFFFFF != *ptrd){
            access_flag = ERROR;
            break;
        }
        ptrd++;
    }
    if(ERROR == access_flag){ while(1); }

    /* ---- 第 2 步:编程 Flash,每 4 字节写 0x3210ABCD ---- */
    fmc_unlock();
    fmc_flag_clear(FMC_FLAG_BANK0_PGERR | FMC_FLAG_BANK0_WPERR | FMC_FLAG_BANK0_END);
    address = BANK0_WRITE_START_ADDR;
    wperror = 0;
    count = BANK0_WRITE_START_ADDR + TRANSFER_NUM;   /* 结束地址 */
    while(address < count){
        fmcstatus = fmc_word_program(address, transdata);
        address = address + 4;
        wperror += (FMC_WPERR == fmcstatus);
        fmc_flag_clear(FMC_FLAG_BANK0_PGERR | FMC_FLAG_BANK0_WPERR | FMC_FLAG_BANK0_END);
    }
    if(wperror != 0){ while(1); }
    fmc_lock();

    /* 先把 RAM 目标缓冲清零,方便后面比对 */
    memset(g_destbuf, 0, TRANSFER_NUM);

    /* ---- 第 3 步:配置并启动 DMA,把 Flash 搬到 RAM ---- */
    dma_deinit(DMA0, DMA_CH0);
    dma_init_struct.direction    = DMA_PERIPHERAL_TO_MEMORY;  /* 方向:外设(Flash)→内存(RAM) */
    dma_init_struct.memory_addr  = (uint32_t)g_destbuf;       /* 目标:RAM 数组 */
    dma_init_struct.memory_inc   = DMA_MEMORY_INCREASE_ENABLE;/* 目标地址自增 */
    dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;     /* 每次搬 8 位 */
    dma_init_struct.number       = TRANSFER_NUM;              /* 共搬 1024 次 */
    dma_init_struct.periph_addr  = (uint32_t)BANK0_WRITE_START_ADDR; /* 源:Flash 地址 */
    dma_init_struct.periph_inc   = DMA_PERIPH_INCREASE_ENABLE;/* 源地址自增 */
    dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; /* 每次搬 8 位 */
    dma_init_struct.priority     = DMA_PRIORITY_ULTRA_HIGH;   /* 最高优先级 */
    dma_init(DMA0, DMA_CH0, &dma_init_struct);

    dma_circulation_disable(DMA0, DMA_CH0);          /* 不循环,只搬一次 */
    dma_memory_to_memory_enable(DMA0, DMA_CH0);      /* 开启内存到内存模式(仅 DMA0 支持) */
    dma_interrupt_enable(DMA0, DMA_CH0, DMA_INT_FTF);/* 搬运完成(FTF)时进中断 */
    dma_channel_enable(DMA0, DMA_CH0);               /* 启动!DMA 开始自动搬运 */

    /* 主循环在此等待 DMA 完成中断置位标志(CPU 此时是"被通知"而非死等外设) */
    while(0 == g_dmacomplete_flag);

    /* ---- 第 4 步:比对搬运结果 ---- */
    ptrd = (uint32_t *)g_destbuf;
    count = TRANSFER_NUM / sizeof(*ptrd);
    for(i = 0; i < count; i++){
        if(transdata != *ptrd){
            access_flag = ERROR;
            break;
        }
        ptrd++;
    }

    /* 搬运成功 → 4 颗 LED 全亮;失败 → 只亮 LED2、LED4 */
    if(access_flag != ERROR){
        gd_eval_led_on(LED2);
        gd_eval_led_on(LED3);
        gd_eval_led_on(LED4);
        gd_eval_led_on(LED5);
    } else {
        gd_eval_led_on(LED2);
        gd_eval_led_on(LED4);
    }

    while(1);
}

/* LED 初始化 */
void led_config(void)
{
    gd_eval_led_init(LED2);
    gd_eval_led_init(LED3);
    gd_eval_led_init(LED4);
    gd_eval_led_init(LED5);
    gd_eval_led_off(LED2);
    gd_eval_led_off(LED3);
    gd_eval_led_off(LED4);
    gd_eval_led_init(LED5);
}

/* 开 DMA0 时钟 */
void rcu_config(void)
{
    rcu_periph_clock_enable(RCU_DMA0);
}

/* 使能 DMA0 通道0 中断 */
void nvic_config(void)
{
    nvic_irq_enable(DMA0_Channel0_IRQn, 0, 0);
}

gd32f30x_it.c 中的 DMA 完成中断(搬运结束后自动调用):

1
2
3
4
5
6
7
void DMA0_Channel0_IRQHandler(void)
{
    if(dma_interrupt_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_FTF)) {
        dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF);  /* 清中断标志 */
        g_dmacomplete_flag = 1;   /* 通知主循环:搬运完成! */
    }
}

代码分段讲解

1. Flash 操作:先擦、再写、再读

DMA 之前那一长串其实是"给 DMA 准备数据源":

1
2
3
4
5
fmc_unlock();                       // 写 Flash 前必须解锁
fmc_page_erase(0x08004000 + i*0x800);  // 按页擦除(2048 字节/页)
...
fmc_word_program(address, 0x3210ABCD); // 每 4 字节写固定值
fmc_lock();                         // 写完上锁
  • TRANSFER_NUM = 0x400 = 1024 字节,每页 0x800 = 2048 字节,所以 count = 0for(i=0; i<=0; i++) 实际擦了 2 页(i=0 和 i=1,地址 0x080040000x08004800),足够覆盖 1024 字节。
  • 擦除后先校验是否全 0xFFFFFFFF,写之前再校验,是为了提前发现 Flash 操作异常,避免"脏数据"被 DMA 搬走。

这一部分本质就是 FMC(Flash 控制器)的用法,详见同模块的 Erase_Program 例程。本例的重点始终是 DMA,Flash 只是"造数据"。

2. DMA 配置:告诉它"从哪搬、搬到哪、搬多少"

这是全例的灵魂,对应 dma_init_struct 的字段:

字段 取值 含义
direction DMA_PERIPHERAL_TO_MEMORY 源是"外设侧",目标是"内存侧"(内存到内存也用这个枚举)
periph_addr 0x08004000 源地址 = Flash 起始
periph_inc ENABLE 搬完一个字节,源地址 +1
periph_width 8BIT 每次搬 1 字节
memory_addr g_destbuf 目标地址 = RAM 数组
memory_inc ENABLE 搬完一个字节,目标地址 +1
memory_width 8BIT 每次搬 1 字节
number 0x400 一共搬 1024 次(共 1024 字节)
priority ULTRA_HIGH 总线仲裁优先级最高

number 的"单位"容易踩坑:它是"传输次数",每次搬运的字节数由 width 决定。这里 width=8bit、number=1024,所以总共搬 1024 字节。若 width 改成 32bit,则 1024 次会搬 4096 字节——要和你的缓冲区大小匹配。

3. 三种关键"开关"

1
2
3
dma_circulation_disable(DMA0, DMA_CH0);       // 普通模式:搬完一次就停
dma_memory_to_memory_enable(DMA0, DMA_CH0);   // 内存到内存模式(仅 DMA0 支持!)
dma_interrupt_enable(DMA0, DMA_CH0, DMA_INT_FTF);  // 搬完触发"全传输完成"中断
  • 循环模式关:本例只搬一次。若开循环(如 ADC 连续采样),搬完会自动从头再来。
  • 内存到内存开:Flash→RAM 本质是"内存到内存"。注意这是 DMA0 的专属能力,DMA1 不支持。
  • FTF 中断开:搬完 1024 字节后硬件自动进 DMA0_Channel0_IRQHandler

4. 启动与"等通知"

1
2
dma_channel_enable(DMA0, DMA_CH0);   // 发令枪:DMA 开始自动搬运
while(0 == g_dmacomplete_flag);      // 等中断把标志置 1

DMA 一旦使能,硬件就在后台把 Flash 的 1024 字节逐字节搬进 g_destbufCPU 不用参与搬运。主循环这句 while(0 == g_dmacomplete_flag) 看起来像"死等",但和"CPU 一个个字节轮询读 Flash"不同——这里是等中断通知,DMA 在搬的时候 CPU 本可以去干别的事(本例只是简单等待)。

5. 完成中断里做了什么?

1
2
3
4
void DMA0_Channel0_IRQHandler(void) {
    dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF); // 必须清中断标志
    g_dmacomplete_flag = 1;
}
  • 进中断第一件事清标志,否则会反复进中断。
  • g_dmacomplete_flag = 1,主循环随即跳出 while,进入最后的"比对"环节。

6. 结果比对与 LED 指示

1
if(transdata != *ptrd) access_flag = ERROR;  // 任一字不等于 0x3210ABCD 即判失败
  • 成功:4 颗 LED 全亮。
  • 失败:只亮 LED2 + LED4(方便肉眼区分)。

实验现象

  1. 下载程序并复位后,DMA 自动把 Flash 内容搬到 RAM。
  2. 搬运成功(绝大多数情况):LED2、LED3、LED4、LED5 全部常亮
  3. 若失败(如 Flash 地址被程序占用、擦写异常):仅 LED2、LED4 亮起。
  4. 用调试器查看:g_destbuf[0..1023] 全部为 0xCD 0xAB 0x10 0x32(小端,对应 0x3210ABCD);0x08004000 处 Flash 内容与之一致。

常见问题

Q1:为什么我的板子烧进去后不亮灯 / 程序跑飞?
最可能是 0x08004000 这个 Flash 地址落在了你的代码区。擦写代码所在页会破坏正在运行的程序。请改 BANK0_WRITE_START_ADDR 到程序之后的空闲页(可用链接脚本/map 文件确认空闲区),或确认评估板例程默认该地址空闲。

Q2:DMA_PERIPHERAL_TO_MEMORY 方向用在 Flash→RAM,名字是不是搞反了?
没有反。GD32 的 DMA 结构体里"peripheral"侧和"memory"侧是逻辑角色:这里把 Flash 当作"源(peripheral 侧)"、RAM 数组当"目标(memory 侧)",所以方向是 peripheral→memory。内存到内存搬运也复用这个枚举,关键是 dma_memory_to_memory_enable 开了。理解"谁源谁目标"即可,名字不必纠结。

Q3:内存到内存模式为什么只能用 DMA0?
这是 GD32 硬件设计限制:内存到内存的传输请求只能由 DMA0 产生。若你用 DMA1 开 dma_memory_to_memory_enable 是不会工作的。

Q4:想搬 32 位数据(比如整型数组)怎么改?
periph_width / memory_width 都改成 DMA_xxx_WIDTH_32BIT,同时 number 相应减半(字节数 / 4)。注意源/目标地址都要 4 字节对齐,否则可能出错。

Q5:搬运过程中 CPU 真的空闲吗?while(0==flag) 不是占着 CPU 吗?
DMA 搬运本身不占 CPU,全是硬件在做。本例主循环只是"等通知",没干别的;真实项目里你可以把这段等待换成"去做其他任务 / 进低功耗",等中断来了再回来处理数据,这正是 DMA 的价值。

Q6:和直接 memcpy 有什么区别?
memcpy 是 CPU 一条条指令搬,占满 CPU;DMA 是独立硬件搬,CPU 可并行。数据量越大、越频繁,DMA 优势越明显(如音频流、LCD 刷屏、ADC 连续采样)。


小结

本示例把 DMA 的"三要素 + 启动 + 中断"主线讲透:

  1. 准备数据:先用 FMC 擦除并编程 Flash,造出可被搬运的固定内容(重点在 DMA,Flash 只是数据源)。
  2. 开时钟 + 配中断RCU_DMA0 + nvic_irq_enable(DMA0_Channel0_IRQn)
  3. 填 DMA 三要素:源地址(Flash)、目标地址(RAM)、次数/宽度/自增。
  4. 开开关:关循环、开内存到内存、开 FTF 中断。
  5. 启动 + 等通知dma_channel_enable 后靠中断置标志,主循环解除等待。
  6. 校验结果:比对 RAM 内容,用 LED 给出成功/失败指示。

记住 DMA = 不占 CPU 的搬运工,凡是"大块数据搬来搬去"的地方,都该想到它。


拓展应用

掌握基础 DMA 后,可以把它用到大量"数据搬运"场景:

  • RAM → USART(串口):同目录 Ram_to_usart 例程,把发送数组交给 DMA 自动发出,CPU 不用死等 TBE。适合长文本/日志输出。
  • ADC → RAM(连续采样):开启 ADC 的 DMA 请求 + 循环模式,ADC 每转换完一个点 DMA 自动搬进数组,实现"无 CPU 干预"的高速采样(如示波器、音频采集)。
  • 内存 → SPI(刷屏):驱动 SPI LCD 时,把显存数组用 DMA 发往 SPI,刷屏不再卡 CPU。
  • 双缓冲(Ping-Pong):配合半传输(HTF) + 全传输(FTF)中断,一块在 DMA 搬、一块 CPU 处理,实现流水线式数据流(音频、图像)。
  • 与大数组拷贝结合:任何 for 循环批量拷贝,都可考虑用 DMA0 内存到内存替代以释放 CPU。

想把 DMA 接到"真外设"上,建议直接看同目录 Ram_to_usart,体会"内存 → 外设"方向和"减轻串口发送负担"的效果。

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