掌握DMA基于GD32F407VE的天空星的配置
1. 什么是DMA?
DMA(Direct Memory Access,直接存储器存取)是一种能够在无需CPU参与的情况下,将数据从一个地址空间复制到另一个地址空间的高效传输硬件机制。
1.1 基础概念类比
为了更好地理解DMA,我们可以用一个生动的类比:
组件 | 类比 | 作用 |
---|---|---|
CPU | 公司员工 | 处理各种任务,如计算、决策和指挥,但不适合花时间亲自搬运每个文件 |
RAM | 办公桌 | 存储需要处理的信息和任务,便于员工随时取用 |
DMA控制器 | 快递员 | 负责在公司(RAM)和仓库(外设)之间搬运数据,让员工不必亲自处理 |
1.2 为什么需要DMA?
不使用DMA的情况:
- CPU需要亲自搬运数据,手头工作被中断,效率低下
使用DMA的情况:
- CPU发出指令后,DMA控制器自动处理数据搬运
- CPU可以继续其他工作,不被搬运任务打断
2. DMA的核心特性
2.1 三种传输方式
- 存储器到外设 - 如内存数据发送到串口
- 外设到存储器 - 如从串口接收数据到内存
- 存储器到存储器 - 仅DMA1支持
2.2 优先级机制
- 软件优先级:低、中、高、超高
- 硬件优先级:通道号越低,优先级越高
3. DMA内存到内存传输
3.1 基本配置
c
// 内存拷贝到内存(必须使用DMA1, 随便选一个通道)
#define DMA_PERIPH_CH DMA1, DMA_CH0
#define ARR_LEN 1024
char src[ARR_LEN] = "hello";
char dst[ARR_LEN] = {0};
void DMA_config() {
// 时钟使能
rcu_periph_clock_enable(RCU_DMA1);
// 重置
dma_deinit(DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 内存到内存拷贝
// 方向: 内存到内存 如果是内存到内存, periph作为内存的源头
init_struct.direction = DMA_MEMORY_TO_MEMORY;;
// 内存源头periph 内存到内存periph成为内存的源头
init_struct.periph_addr = (uint32_t)src;
init_struct.periph_inc = DMA_PERIPH_INCREASE_ENABLE; // 增长
// 内存目的
init_struct.memory0_addr = (uint32_t)dst;
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
init_struct.number = ARR_LEN;
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(DMA_PERIPH_CH, &init_struct);
// ================= 中断相关
nvic_irq_enable(DMA1_Channel0_IRQn, 2, 2); // 优先级
dma_interrupt_enable(DMA_PERIPH_CH, DMA_INT_FTF); // 启动中断
}
//中断函数
void DMA1_Channel0_IRQHandler() {
if (SET == dma_interrupt_flag_get(DMA_PERIPH_CH, DMA_INT_FLAG_FTF)) {
dma_interrupt_flag_clear(DMA_PERIPH_CH, DMA_INT_FLAG_FTF);
printf("DMA1_Channel0_IRQHandler dst = %s\n", dst);
}
}
3.2 启动传输与等待完成
c
//中断函数完成传输后执行中断
void DMA1_Channel0_IRQHandler() {
if (SET == dma_interrupt_flag_get(DMA_PERIPH_CH, DMA_INT_FLAG_FTF)) {
//清除中断
dma_interrupt_flag_clear(DMA_PERIPH_CH, DMA_INT_FLAG_FTF);
//输出内容
printf("DMA1_Channel0_IRQHandler dst = %s\n", dst);
}
}
3.3 动态配置数据源
c
void USART0_on_recv(uint8_t* data, uint32_t len) {
printf("recv[%d]:%s\n", len, data);
// 动态指定内存源头 如果是内存到内存, periph作为内存的源头
dma_periph_address_config(DMA_PERIPH_CH, (uint32_t)data);
// 指定搬运个数
dma_transfer_number_config(DMA_PERIPH_CH, len + 1); // +1, 字符串增加一个结束符
// 通知DMA干活,它开开始搬运
dma_channel_enable(DMA_PERIPH_CH);
}
4. DMA内存到外设传输(以USART发送为例)
4.1 配置步骤
c
#define USART0_DATA_ADDR (uint32_t)&USART_DATA(USART0) // 外设地址
#define USART0_TX_DMA_RCU RCU_DMA1
#define USART0_TX_DMA_PERIPH_CH DMA1, DMA_CH7
#define USART0_TX_DMA_PERIPH_SUB DMA_SUBPERI4
// DMA 内存到外设 函数定义
static void DMA_tx_config() {
// 时钟使能
rcu_periph_clock_enable(USART0_TX_DMA_RCU);
// 重置
dma_deinit(USART0_TX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 内存到外设拷贝
// 方向: 内存到外设
init_struct.direction = DMA_MEMORY_TO_PERIPH;
// 源头:内存
// init_struct.memory0_addr = (uint32_t)?; // 内存内容动态变化,不固定
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 目的:外设
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
// init_struct.number = ?; // 动态变化
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(USART0_TX_DMA_PERIPH_CH, &init_struct);
// 设置子外设
dma_channel_subperipheral_select(USART0_TX_DMA_PERIPH_CH, USART0_TX_DMA_PERIPH_SUB);
}
4.2 DMA发送数据
c
#include "USART0.h"
#include <string.h>
#define USART0_DATA_ADDR (uint32_t)&USART_DATA(USART0) // 外设地址
#define USART0_TX_DMA_RCU RCU_DMA1
#define USART0_TX_DMA_PERIPH_CH DMA1, DMA_CH7
#define USART0_TX_DMA_PERIPH_SUB DMA_SUBPERI4
// DMA 内存到外设 函数定义
static void DMA_tx_config() {
// 时钟使能
rcu_periph_clock_enable(USART0_TX_DMA_RCU);
// 重置
dma_deinit(USART0_TX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 内存到外设拷贝
// 方向: 内存到外设
init_struct.direction = DMA_MEMORY_TO_PERIPH;
// 源头:内存
// init_struct.memory0_addr = (uint32_t)?; // 内存内容动态变化,不固定
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 目的:外设
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
// init_struct.number = ?; // 动态变化
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(USART0_TX_DMA_PERIPH_CH, &init_struct);
// 设置子外设
dma_channel_subperipheral_select(USART0_TX_DMA_PERIPH_CH, USART0_TX_DMA_PERIPH_SUB);
}
void USART0_init() {
DMA_tx_config(); // DMA 内存到外设 函数调用
// 串口0 TX 复用功能
GPIO_output_af(USART0_TX_RCU, USART0_TX_PORT, USART0_TX_PIN, GPIO_OTYPE_PP, USART0_TX_AF);
// 串口0 RX 复用功能
GPIO_output_af(USART0_RX_RCU, USART0_RX_PORT, USART0_RX_PIN, GPIO_OTYPE_PP, USART0_RX_AF);
// =========== 串口配置
// 时钟使能
rcu_periph_clock_enable(RCU_USART0);
// 重置串口(可选)
usart_deinit(USART0);
// 设置波特率(必须要配置)
usart_baudrate_set(USART0, USART0_BAUDRATE);
// 允许串口发送
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
// ==============++ 串口允许DMA发送
usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
// 允许串口接收
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
// ================中断配置
// 中断优先级,使能中断请求 nvic_irq_enable在misc.h中
// 参数1: 中断类型枚举常量, 在 gd32f4xx.h第159行
// 参数2: 抢占优先级
// 参数3: 响应优先级
nvic_irq_enable(USART0_IRQn, USART0_PRIORITY);
// (使能串口中断)RBNE, 有数据可读,自动触发中断,配置中断触发条件
usart_interrupt_enable(USART0, USART_INT_RBNE);
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕),即可触发中断
usart_interrupt_enable(USART0, USART_INT_IDLE);
// 使能串口
usart_enable(USART0);
}
// 发送1个byte数据
void USART0_send_byte(uint8_t byte){
#if 0 // 上面为非DMA
usart_data_transmit(USART0, byte);
// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环
// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空
// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1
while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMA
USART0_send_data(&byte, 1);
#endif
}
// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){
if (data == NULL) return;
#if 0
for(uint32_t i = 0; i < len; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
// 指定DMA内存地址
dma_memory_address_config(USART0_TX_DMA_PERIPH_CH, DMA_MEMORY_0, (uint32_t)data);
// 指定DMA发送个数
dma_transfer_number_config(USART0_TX_DMA_PERIPH_CH, len);
// 通知DMA干活,它开开始搬运
dma_channel_enable(USART0_TX_DMA_PERIPH_CH);
// 等待搬运完成 只有没有搬完RESET,进入循环
while(RESET == dma_flag_get(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF));
// 循环的外面,清除标志位, 不清除不能重复搬运
dma_flag_clear(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF);
#endif
}
// 发送字符串 (结尾标记\0)
void USART0_send_string(char *data){
if (data == NULL) return;
#if 0
for(uint32_t i = 0; data[i] != '\0'; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
USART0_send_data((uint8_t*)data, strlen(data)); // 需要#include <string.h>
#endif
}
#if USART0_PRINTF
#include <stdio.h>
// 重写fputc, printf即可使用
int fputc(int ch, FILE *f) {
USART0_send_byte(ch); // 串口发送1个字节
return ch;
}
#endif
/*
1. 串口中断处理函数,函数名,不能乱写,因为汇编启动文件已经规定好
USART0_IRQHandler 在 startup_gd32f407_427.s 的124行
2. 触发中断的条件有很多,一定要通过标志位区分,处理完也要清零
STC8 接收逻辑
if(COM1.RX_Cnt >= COM_RX1_Lenth) COM1.RX_Cnt = 0;
RX1_Buffer[COM1.RX_Cnt++] = SBUF;
*/
#define RX0_LENTH 128 // 最大大小,数组长度
static uint8_t RX0_Buffer[RX0_LENTH + 1]; // +1,多一个位置给字符串结束符
static uint32_t rx0_cnt = 0; // 元素个数
void USART0_IRQHandler() {
// RBNE(有数据可读) 触发的中断,硬件置1(SET)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) {
// 软件清零, 清零后,才能反复读取
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
// 读取1个字节
uint8_t data = usart_data_receive(USART0);
if(rx0_cnt >= RX0_LENTH) rx0_cnt = 0; // 越界处理
// 来一个数据,保存到一个数组中
RX0_Buffer[rx0_cnt++] = data;
}
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
}
}
5. DMA外设到内存传输(以USART接收为例)
5.1 配置步骤
c
#define RX0_LENTH 128 // 最大大小,数组长度
static uint8_t RX0_Buffer[RX0_LENTH + 1]; // +1,多一个位置给字符串结束符
static uint32_t rx0_cnt = 0; // 元素个数
#define USART0_DATA_ADDR (uint32_t)&USART_DATA(USART0) // 外设地址
#define USART0_RX_DMA_RCU RCU_DMA1
#define USART0_RX_DMA_PERIPH_CH DMA1, DMA_CH5
#define USART0_RX_DMA_PERIPH_SUB DMA_SUBPERI4
// DMA 外设到内存 函数定义
static void DMA_rx_config() {
// 时钟使能
rcu_periph_clock_enable(USART0_RX_DMA_RCU);
// 重置
dma_deinit(USART0_RX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 外设到内存拷贝
// 方向: 外设到内存
init_struct.direction = DMA_PERIPH_TO_MEMORY;
// 源头:外设
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不增长
// 目的:内存
init_struct.memory0_addr = (uint32_t)RX0_Buffer; // 指定的数组
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
init_struct.number = RX0_LENTH; // 最大的搬运个数,实际有可能少于它
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(USART0_RX_DMA_PERIPH_CH, &init_struct);
// 设置子外设
dma_channel_subperipheral_select(USART0_RX_DMA_PERIPH_CH, USART0_RX_DMA_PERIPH_SUB);
// 通知DMA干活,它开始搬运,可以用DMA接收数据
// 这里不要等待搬运完成,因为不知道啥时候有数据来
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
void USART0_init() {
DMA_tx_config(); // DMA 内存到外设 函数调用
DMA_rx_config(); // DMA 外设到内存 函数定义
// 串口0 TX 复用功能
GPIO_output_af(USART0_TX_RCU, USART0_TX_PORT, USART0_TX_PIN, GPIO_OTYPE_PP, USART0_TX_AF);
// 串口0 RX 复用功能
GPIO_output_af(USART0_RX_RCU, USART0_RX_PORT, USART0_RX_PIN, GPIO_OTYPE_PP, USART0_RX_AF);
// =========== 串口配置
// 时钟使能
rcu_periph_clock_enable(RCU_USART0);
// 重置串口(可选)
usart_deinit(USART0);
// 设置波特率(必须要配置)
usart_baudrate_set(USART0, USART0_BAUDRATE);
// 允许串口发送
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
// ==============++ 串口允许DMA发送
usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
// 允许串口接收
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
// ==============++ 串口允许DMA接收
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
// ================中断配置
// 中断优先级,使能中断请求 nvic_irq_enable在misc.h中
// 参数1: 中断类型枚举常量, 在 gd32f4xx.h第159行
// 参数2: 抢占优先级
// 参数3: 响应优先级
nvic_irq_enable(USART0_IRQn, USART0_PRIORITY);
#if 0 // 非DMA才用
// (使能串口中断)RBNE, 有数据可读,自动触发中断,配置中断触发条件
usart_interrupt_enable(USART0, USART_INT_RBNE);
#endif
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕),即可触发中断
usart_interrupt_enable(USART0, USART_INT_IDLE); // 如果用DMA,空闲了,说明数据全部搬运完成,再处理数据
// 使能串口
usart_enable(USART0);
}
// 发送1个byte数据
void USART0_send_byte(uint8_t byte){
#if 0 // 上面为非DMA
usart_data_transmit(USART0, byte);
// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环
// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空
// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1
while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMA
USART0_send_data(&byte, 1);
#endif
}
// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){
if (data == NULL) return;
#if 0
for(uint32_t i = 0; i < len; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
// 指定DMA内存地址
dma_memory_address_config(USART0_TX_DMA_PERIPH_CH, DMA_MEMORY_0, (uint32_t)data);
// 指定DMA发送个数
dma_transfer_number_config(USART0_TX_DMA_PERIPH_CH, len);
// 通知DMA干活,它开开始搬运
dma_channel_enable(USART0_TX_DMA_PERIPH_CH);
// 等待搬运完成 只有没有搬完RESET,进入循环
while(RESET == dma_flag_get(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF));
// 循环的外面,清除标志位, 不清除不能重复搬运
dma_flag_clear(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF);
#endif
}
// 发送字符串 (结尾标记\0)
void USART0_send_string(char *data){
if (data == NULL) return;
#if 0
for(uint32_t i = 0; data[i] != '\0'; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
USART0_send_data((uint8_t*)data, strlen(data)); // 需要#include <string.h>
#endif
}
#if USART0_PRINTF
#include <stdio.h>
// 重写fputc, printf即可使用
int fputc(int ch, FILE *f) {
USART0_send_byte(ch); // 串口发送1个字节
return ch;
}
#endif
/*
1. 串口中断处理函数,函数名,不能乱写,因为汇编启动文件已经规定好
USART0_IRQHandler 在 startup_gd32f407_427.s 的124行
2. 触发中断的条件有很多,一定要通过标志位区分,处理完也要清零
STC8 接收逻辑
if(COM1.RX_Cnt >= COM_RX1_Lenth) COM1.RX_Cnt = 0;
RX1_Buffer[COM1.RX_Cnt++] = SBUF;
*/
void USART0_IRQHandler() {
#if 0 // 非DMA
// RBNE(有数据可读) 触发的中断,硬件置1(SET)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) {
// 软件清零, 清零后,才能反复读取
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
// 读取1个字节
uint8_t data = usart_data_receive(USART0);
if(rx0_cnt >= RX0_LENTH) rx0_cnt = 0; // 越界处理
// 来一个数据,保存到一个数组中
RX0_Buffer[rx0_cnt++] = data;
}
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
}
#else // DMA
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// 禁用DMA
dma_channel_disable(USART0_RX_DMA_PERIPH_CH);
// 获取剩余没有搬运的个数
uint32_t left_num = dma_transfer_number_get(USART0_RX_DMA_PERIPH_CH);
// 搬运的个数 = 总个数 - 剩余没有搬运的个数
rx0_cnt = RX0_LENTH - left_num;
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
// 清除标志位, 放在启动前面
dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);
// 启动DMA
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
#endif
5.2 处理接收数据
c
/*
1. 串口中断处理函数,函数名,不能乱写,因为汇编启动文件已经规定好
USART0_IRQHandler 在 startup_gd32f407_427.s 的124行
2. 触发中断的条件有很多,一定要通过标志位区分,处理完也要清零
STC8 接收逻辑
if(COM1.RX_Cnt >= COM_RX1_Lenth) COM1.RX_Cnt = 0;
RX1_Buffer[COM1.RX_Cnt++] = SBUF;
*/
void USART0_IRQHandler() {
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// 禁用DMA
dma_channel_disable(USART0_RX_DMA_PERIPH_CH);
// 获取剩余没有搬运的个数
uint32_t left_num = dma_transfer_number_get(USART0_RX_DMA_PERIPH_CH);
// 搬运的个数 = 总个数 - 剩余没有搬运的个数
rx0_cnt = RX0_LENTH - left_num;
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
// 清除标志位, 放在启动前面
dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);
// 启动DMA
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
}
6. 完整封装示例:DMA版USART0
USART0.h文件
c
#ifndef __USART0_H__
#define __USART0_H__
#include "gd32f4xx.h"
#include "systick.h"
#include "gpio_cfg.h"
#include <stdio.h>
// ============================================== DMA
// DMA发送功能开关
#define USART0_TX_DMA_ENABLE 1
// DMA接收功能开关
#define USART0_RX_DMA_ENABLE 1
#define USART0_DATA_ADDR (uint32_t)&USART_DATA(USART0) // 外设地址
#define USART0_TX_DMA_RCU RCU_DMA1
#define USART0_TX_DMA_PERIPH_CH DMA1, DMA_CH7
#define USART0_TX_DMA_PERIPH_SUB DMA_SUBPERI4
#define USART0_RX_DMA_RCU RCU_DMA1
#define USART0_RX_DMA_PERIPH_CH DMA1, DMA_CH5
#define USART0_RX_DMA_PERIPH_SUB DMA_SUBPERI4
// ============================================== 非DMA
// 功能开关, printf配置开关
#define USART0_PRINTF 1
// 开关打开为1,同时,在合适位置定义函数void USART0_on_recv(uint8_t* data, uint32_t len)
#define USART0_RECV_CALLBACK 1
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
void USART0_on_recv(uint8_t* data, uint32_t len);
#endif
// PA9 USART0_TX AF7
#define USART0_TX_RCU RCU_GPIOA
#define USART0_TX_PORT GPIOA
#define USART0_TX_PIN GPIO_PIN_9
#define USART0_TX_AF GPIO_AF_7
// PA10 USART0_RX AF7
#define USART0_RX_RCU RCU_GPIOA
#define USART0_RX_PORT GPIOA
#define USART0_RX_PIN GPIO_PIN_10
#define USART0_RX_AF GPIO_AF_7
// 波特率
#define USART0_BAUDRATE 115200UL
// 优先级
#define USART0_PRIORITY 0,0
// 初始化
void USART0_init();
// 发送1个byte数据
void USART0_send_byte(uint8_t byte);
// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len);
// 发送字符串 (结尾标记\0)
void USART0_send_string(char *data);
#endif
USART0.c文件
c
#include "USART0.h"
#include <string.h>
#define RX0_LENTH 128 // 最大大小,数组长度
static uint8_t RX0_Buffer[RX0_LENTH + 1]; // +1,多一个位置给字符串结束符
static uint32_t rx0_cnt = 0; // 元素个数
// DMA 内存到外设 函数定义
static void DMA_tx_config() {
// 时钟使能
rcu_periph_clock_enable(USART0_TX_DMA_RCU);
// 重置
dma_deinit(USART0_TX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 内存到外设拷贝
// 方向: 内存到外设
init_struct.direction = DMA_MEMORY_TO_PERIPH;
// 源头:内存
// init_struct.memory0_addr = (uint32_t)?; // 内存内容动态变化,不固定
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 目的:外设
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
// init_struct.number = ?; // 动态变化
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(USART0_TX_DMA_PERIPH_CH, &init_struct);
// 设置子外设
dma_channel_subperipheral_select(USART0_TX_DMA_PERIPH_CH, USART0_TX_DMA_PERIPH_SUB);
}
// DMA 外设到内存 函数定义
static void DMA_rx_config() {
// 时钟使能
rcu_periph_clock_enable(USART0_RX_DMA_RCU);
// 重置
dma_deinit(USART0_RX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 结构体参数初始化
dma_single_data_para_struct_init(&init_struct);
// ============ 外设到内存拷贝
// 方向: 外设到内存
init_struct.direction = DMA_PERIPH_TO_MEMORY;
// 源头:外设
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不增长
// 目的:内存
init_struct.memory0_addr = (uint32_t)RX0_Buffer; // 指定的数组
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 增长
// 搬运一个数据的大小
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT; // 8位
// 搬运数据个数
init_struct.number = RX0_LENTH; // 最大的搬运个数,实际有可能少于它
// 优先级
init_struct.priority = DMA_PRIORITY_HIGH;
// DMA初始化, 设置搬运规则
dma_single_data_mode_init(USART0_RX_DMA_PERIPH_CH, &init_struct);
// 设置子外设
dma_channel_subperipheral_select(USART0_RX_DMA_PERIPH_CH, USART0_RX_DMA_PERIPH_SUB);
// 通知DMA干活,它开始搬运,可以用DMA接收数据
// 这里不要等待搬运完成,因为不知道啥时候有数据来
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
void USART0_init() {
#if USART0_TX_DMA_ENABLE
DMA_tx_config(); // DMA 内存到外设 函数调用
#endif
#if USART0_RX_DMA_ENABLE
DMA_rx_config(); // DMA 外设到内存 函数定义
#endif
// 串口0 TX 复用功能
GPIO_output_af(USART0_TX_RCU, USART0_TX_PORT, USART0_TX_PIN, GPIO_OTYPE_PP, USART0_TX_AF);
// 串口0 RX 复用功能
GPIO_output_af(USART0_RX_RCU, USART0_RX_PORT, USART0_RX_PIN, GPIO_OTYPE_PP, USART0_RX_AF);
// =========== 串口配置
// 时钟使能
rcu_periph_clock_enable(RCU_USART0);
// 重置串口(可选)
usart_deinit(USART0);
// 设置波特率(必须要配置)
usart_baudrate_set(USART0, USART0_BAUDRATE);
// 允许串口发送
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
// ==============++ 串口允许DMA发送
#if USART0_TX_DMA_ENABLE
usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
#endif
// 允许串口接收
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
// ==============++ 串口允许DMA接收
#if USART0_RX_DMA_ENABLE
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
#endif
// ================中断配置
// 中断优先级,使能中断请求 nvic_irq_enable在misc.h中
// 参数1: 中断类型枚举常量, 在 gd32f4xx.h第159行
// 参数2: 抢占优先级
// 参数3: 响应优先级
nvic_irq_enable(USART0_IRQn, USART0_PRIORITY);
#if !USART0_RX_DMA_ENABLE // 非DMA才用
// (使能串口中断)RBNE, 有数据可读,自动触发中断,配置中断触发条件
usart_interrupt_enable(USART0, USART_INT_RBNE);
#endif
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕),即可触发中断
usart_interrupt_enable(USART0, USART_INT_IDLE); // 如果用DMA,空闲了,说明数据全部搬运完成,再处理数据
// 使能串口
usart_enable(USART0);
}
// 发送1个byte数据
void USART0_send_byte(uint8_t byte){
#if !USART0_TX_DMA_ENABLE // 上面为非DMA
usart_data_transmit(USART0, byte);
// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环
// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空
// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1
while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMA
USART0_send_data(&byte, 1);
#endif
}
// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){
if (data == NULL) return;
#if !USART0_TX_DMA_ENABLE
for(uint32_t i = 0; i < len; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
// 指定DMA内存地址
dma_memory_address_config(USART0_TX_DMA_PERIPH_CH, DMA_MEMORY_0, (uint32_t)data);
// 指定DMA发送个数
dma_transfer_number_config(USART0_TX_DMA_PERIPH_CH, len);
// 通知DMA干活,它开开始搬运
dma_channel_enable(USART0_TX_DMA_PERIPH_CH);
// 等待搬运完成 只有没有搬完RESET,进入循环
while(RESET == dma_flag_get(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF));
// 循环的外面,清除标志位, 不清除不能重复搬运
dma_flag_clear(USART0_TX_DMA_PERIPH_CH, DMA_FLAG_FTF);
#endif
}
// 发送字符串 (结尾标记\0)
void USART0_send_string(char *data){
if (data == NULL) return;
#if !USART0_TX_DMA_ENABLE
for(uint32_t i = 0; data[i] != '\0'; i++) {
USART0_send_byte(data[i]); // 发送单个字符
}
#else
USART0_send_data((uint8_t*)data, strlen(data)); // 需要#include <string.h>
#endif
}
#if USART0_PRINTF
#include <stdio.h>
// 重写fputc, printf即可使用
int fputc(int ch, FILE *f) {
USART0_send_byte(ch); // 串口发送1个字节
return ch;
}
#endif
/*
1. 串口中断处理函数,函数名,不能乱写,因为汇编启动文件已经规定好
USART0_IRQHandler 在 startup_gd32f407_427.s 的124行
2. 触发中断的条件有很多,一定要通过标志位区分,处理完也要清零
STC8 接收逻辑
if(COM1.RX_Cnt >= COM_RX1_Lenth) COM1.RX_Cnt = 0;
RX1_Buffer[COM1.RX_Cnt++] = SBUF;
*/
void USART0_IRQHandler() {
#if !USART0_RX_DMA_ENABLE // 非DMA
// RBNE(有数据可读) 触发的中断,硬件置1(SET)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) {
// 软件清零, 清零后,才能反复读取
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
// 读取1个字节
uint8_t data = usart_data_receive(USART0);
if(rx0_cnt >= RX0_LENTH) rx0_cnt = 0; // 越界处理
// 来一个数据,保存到一个数组中
RX0_Buffer[rx0_cnt++] = data;
}
// IDLE, 空闲触发中断,数据读取完毕(或发送完毕)
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
}
#else // DMA
if (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
// 清除标志位
usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位
// 禁用DMA
dma_channel_disable(USART0_RX_DMA_PERIPH_CH);
// 获取剩余没有搬运的个数
uint32_t left_num = dma_transfer_number_get(USART0_RX_DMA_PERIPH_CH);
// 搬运的个数 = 总个数 - 剩余没有搬运的个数
rx0_cnt = RX0_LENTH - left_num;
// %s 打印只针对字符串,如果内容为16进制,%s打印会乱码
RX0_Buffer[rx0_cnt] = '\0'; // 字符串结束符, 如果内容为16进制,这个处理没有意义
#if USART0_RECV_CALLBACK
// 收到串口0数据,回调函数
USART0_on_recv(RX0_Buffer, rx0_cnt); // 函数调用
#endif
rx0_cnt = 0; // 元素个数置零
// 清除标志位, 放在启动前面
dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);
// 启动DMA
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
#endif
}
7. 总结
DMA技术通过将数据搬运任务从CPU中解放出来,显著提高了系统的整体效率。关键优势包括:
- 提高CPU利用率:CPU可以专注于计算任务,不被数据传输打断
- 提高数据传输效率:专门的DMA控制器优化了数据传输流程
- 降低系统功耗:减少CPU参与数据搬运的时间
在实际应用中,合理使用DMA可以大幅提升嵌入式系统的性能,特别是在需要大量数据传输的场景中,如音频处理、图像传输、网络通信等。