掌握DMA基于GD32F407VE的天空星的配置
掌握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 基本配置
// 内存拷贝到内存(必须使用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 启动传输与等待完成
//中断函数完成传输后执行中断
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 动态配置数据源
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 配置步骤
#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发送数据
#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 // 上面为非DMAusart_data_transmit(USART0, byte);// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMAUSART0_send_data(&byte, 1);
#endif
}// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){if (data == NULL) return;
#if 0for(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]); // 发送单个字符}
#elseUSART0_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零}
}
5. DMA外设到内存传输(以USART接收为例)
5.1 配置步骤
#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 // 上面为非DMAusart_data_transmit(USART0, byte);// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMAUSART0_send_data(&byte, 1);
#endif
}
// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){if (data == NULL) return;
#if 0for(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]); // 发送单个字符}
#elseUSART0_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零}
#else // DMAif (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) { // 清除标志位usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位// 禁用DMAdma_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零// 清除标志位, 放在启动前面dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);// 启动DMAdma_channel_enable(USART0_RX_DMA_PERIPH_CH); }
#endif
5.2 处理接收数据
/*
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); // 读取一次,不是为了读数据,为了清除空闲的标志位// 禁用DMAdma_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零// 清除标志位, 放在启动前面dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);// 启动DMAdma_channel_enable(USART0_RX_DMA_PERIPH_CH); }
}
6. 完整封装示例:DMA版USART0
USART0.h文件
#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文件
#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_ENABLEDMA_tx_config(); // DMA 内存到外设 函数调用#endif#if USART0_RX_DMA_ENABLEDMA_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_ENABLEusart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);#endif// 允许串口接收usart_receive_config(USART0, USART_RECEIVE_ENABLE);// ==============++ 串口允许DMA接收#if USART0_RX_DMA_ENABLEusart_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 // 上面为非DMAusart_data_transmit(USART0, byte);// 等待发送完成, 没有完成,说明返回值为RESET, 一直循环,直到变为SET退出循环// USART_FLAG_TBE: transmit data buffer empty 发送缓冲区为空// 发送缓冲区为空, 硬件置1,说明发送完成 SET就是1while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
#else // 下面为DMAUSART0_send_data(&byte, 1);
#endif
}// 发送多个byte数据
void USART0_send_data(uint8_t* data, uint32_t len){if (data == NULL) return;
#if !USART0_TX_DMA_ENABLEfor(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_ENABLEfor(uint32_t i = 0; data[i] != '\0'; i++) {USART0_send_byte(data[i]); // 发送单个字符}
#elseUSART0_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零}
#else // DMAif (SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) { // 清除标志位usart_data_receive(USART0); // 读取一次,不是为了读数据,为了清除空闲的标志位// 禁用DMAdma_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); // 函数调用#endifrx0_cnt = 0; // 元素个数置零// 清除标志位, 放在启动前面dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);// 启动DMAdma_channel_enable(USART0_RX_DMA_PERIPH_CH); }
#endif
}
7. 总结
DMA技术通过将数据搬运任务从CPU中解放出来,显著提高了系统的整体效率。关键优势包括:
- 提高CPU利用率:CPU可以专注于计算任务,不被数据传输打断
- 提高数据传输效率:专门的DMA控制器优化了数据传输流程
- 降低系统功耗:减少CPU参与数据搬运的时间
在实际应用中,合理使用DMA可以大幅提升嵌入式系统的性能,特别是在需要大量数据传输的场景中,如音频处理、图像传输、网络通信等。