掌握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 三种传输方式

  1. 存储器到外设 - 如内存数据发送到串口
  2. 外设到存储器 - 如从串口接收数据到内存
  3. 存储器到存储器 - 仅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中解放出来,显著提高了系统的整体效率。关键优势包括:

  1. 提高CPU利用率:CPU可以专注于计算任务,不被数据传输打断
  2. 提高数据传输效率:专门的DMA控制器优化了数据传输流程
  3. 降低系统功耗:减少CPU参与数据搬运的时间

在实际应用中,合理使用DMA可以大幅提升嵌入式系统的性能,特别是在需要大量数据传输的场景中,如音频处理、图像传输、网络通信等。

相关推荐
清风6666663 小时前
基于单片机的Boost升压斩波电源电路
单片机·嵌入式硬件·毕业设计·课程设计
qiuiuiu4133 小时前
正点原子RK3568学习日记-GIT
linux·c语言·开发语言·单片机
搞一搞汽车电子9 小时前
单片机的堆\栈\Flash\Ram区别和联系
单片机·嵌入式硬件
李永奉12 小时前
STM32-认识STM32
stm32·单片机·嵌入式硬件
La Pulga13 小时前
【STM32】I2C通信—软件模拟
c语言·stm32·单片机·嵌入式硬件·mcu
CiLerLinux13 小时前
第五十二章 ESP32S3 UDP 实验
网络·单片机·嵌入式硬件·网络协议·udp
CFZPL15 小时前
stm32延时函数
单片机·嵌入式硬件
li星野16 小时前
打工人日报#20251008
单片机·嵌入式硬件
Stanford_110618 小时前
关于嵌入式硬件需要了解的基础知识
开发语言·c++·嵌入式硬件·微信小程序·微信公众平台·twitter·微信开放平台