目录
一、为什么要使用DMA
DMA(Direct Memory Access,直接内存访问) 是一种硬件特性,允许外设(如 ADC、UART、SPI 等)直接与内存进行数据传输,而无需 CPU 的干预。DMA 可以显著提高数据传输效率,减少 CPU 的负担,特别是在大量数据传输或实时性要求高的场景中。
通常情况下,ADC 转换完成后,我们可以通过 中断 或 查询 方式读取数据,但这种方式会占用 CPU 资源。如果 ADC 采样频率高,或串口发送/接收数据频繁,CPU 需要频繁响应中断或查询标志位,会导致系统效率下降。
DMA(Direct Memory Access)允许外设(如 ADC 和 USART)直接访问内存,而无需 CPU 介入。这样,CPU 可以专注于其他任务,而 DMA 负责数据搬运,提高系统性能。
本文将以GD32F103(兼容STM32F103)为例进行相关代码编写与功能实现。
二、ADC 采样并通过 DMA 传输
<1>、ADC通道初始化:
查阅芯片手册可知
MCU片上集成了12位逐次逼近式模数转换器模块(ADC),可以采样来自于16个外部通道 和2个内部通道上的模拟信号。这18个ADC采样通道都支持多种运行模式,采样转换后,转 换结果可以按照最低有效位对齐或最高有效位对齐的方式保存在相应的数据寄存器中。
可知此ADC采集是可以进行DMA功能配给的。
那么我们先编ADC通道基本初始化,
需要完成了如下功能
配置 PA0 为 ADC 输入引脚
使能 ADC0 时钟,并配置 ADC 时钟为 APB2 6 分频(72MHz → 12MHz)
设置 ADC 为连续转换模式,采样完自动进行下一个
选择规则通道 0(PA0),设置采样时间 55.5 个时钟周期
ADC 数据右对齐,方便读取
外部触发源设为 NONE(软件触发),但允许触发
开启 ADC0,开始工作
ADC 采样公式
转换时间=采样时间+12.5×ADC 时钟周期
1. 开启必要的时钟
rcu_periph_clock_enable(RCU_AF); // AF 时钟使能
rcu_periph_clock_enable(RCU_GPIOA); // 使能 GPIOA 端口时钟
解析:
RCU_AF
:这是 AFIO(Alternate Function I/O),用于外设复用功能(如 USART、I2C、SPI 等)。
RCU_GPIOA
:使能 GPIOA 端口时钟,因为 ADC 需要在某个 GPIO 引脚上读取模拟信号。
2. 配置 PA0 为 ADC 输入
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
解析:
GPIO_MODE_AIN
:模拟输入模式,ADC 只能在 AIN(Analog Input)模式 下工作。
GPIO_PIN_0
:PA0 作为 ADC 采样引脚。
GPIO_OSPEED_50MHZ
:虽然 GPIO 速度参数在模拟输入模式下没有意义,但仍然需要写入。
3. 使能 ADC 时钟
rcu_periph_clock_enable(RCU_ADC0); // 使能 ADC0 的时钟,ADC0 才能正常工作
解析:
ADC0 需要开启时钟,否则无法工作。
4. 配置 ADC 时钟
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6);
解析:
RCU_CKADC_CKAPB2_DIV6
:ADC 时钟来自 APB2(高级外设总线 2),这里设置 6 分频。
为什么 6 分频?
GD32F103 的 ADC 最高时钟 不能超过 14MHz,而 APB2 通常是 72MHz,6 分频后 72MHz / 6 = 12MHz,符合 ADC 规格要求。
5. 复位 ADC
adc_deinit(ADC0);
解析:
复位 ADC0,清除所有寄存器,恢复默认状态。
6. 配置 ADC 工作模式
adc_mode_config(ADC_MODE_FREE);
解析:
ADC_MODE_FREE
:自由模式(即 独立 ADC 运行模式)。
GD32F103C8T6 有两个 ADC(ADC0 和 ADC1),它们可以独立或双重模式工作,此处选择独立模式。
7. 使能 ADC 连续转换模式
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE);
解析:
ADC_CONTINUOUS_MODE
:开启 连续转换模式,即 ADC 采样后会自动开始下一次转换,不需要手动触发。
8. 配置 ADC 数据对齐
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT);
解析:
数据右对齐(Right Alignment):
GD32F103C8T6 的 ADC 是 12 位分辨率,转换结果存储在 16 位寄存器 中。
右对齐:ADC 值填充低 12 位,高 4 位为 0
。
左对齐:ADC 值填充高 12 位,低 4 位为 0
。
9. 配置 ADC 规则通道
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1);
解析:
这里 设置了 1 个规则通道,意味着 ADC0 会轮流采样 一个通道。
10. 选择 ADC 规则通道
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5);
解析:
0
:第 1 个规则通道(索引从 0 开始)。
ADC_CHANNEL_0
:选择 ADC0 的通道 0,即 PA0。
ADC_SAMPLETIME_55POINT5
:
设置 采样时间 为 55.5 个 ADC 时钟周期。采样时间 = 55.5 × ADC 时钟周期(如果 ADC 时钟是 12MHz,则采样时间约 4.6µs)。
采样时间越长,采样精度越高,但转换速度会变慢。
11. 配置 ADC 触发源
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE);
解析:
选择 无外部触发,意味着 ADC 采样完全由软件控制(或者连续模式)。
12. 使能 ADC 外部触发
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
解析:
允许 外部触发 ADC 采样,但由于我们设置了 NONE
,所以没有实际作用。
13. 使能 ADC
adc_enable(ADC0);
解析:
开启 ADC0,使其进入工作状态。
完整代码如下:
rcu_periph_clock_enable(RCU_AF); // AF时钟使能
// 配置PA4 ADC引脚
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
/* 使能 ADC0 时钟 */
rcu_periph_clock_enable(RCU_ADC0); // 使能 ADC0 的时钟,ADC0 才能正常工作
/* 配置 ADC 时钟 */
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6); // 配置 ADC 时钟为 APB2 时钟的 6 分频
/* 复位 ADC */
adc_deinit(ADC0); // 复位 ADC0,将其寄存器恢复为默认值
/* ADC 模式配置 */
adc_mode_config(ADC_MODE_FREE); // 配置 ADC 为自由运行模式(独立模式)
/* 使能 ADC 连续转换模式 */
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); // 使能 ADC0 的连续转换模式
/* ADC 数据对齐配置 */
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 配置 ADC0 数据右对齐
/* ADC 规则通道长度配置 */
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); // 配置 ADC0 规则通道的转换序列长度为 1(只有一个通道需要转换)
/* ADC 规则通道配置 */
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5); // 配置 ADC0 规则通道的通道 4,采样时间为 55.5 个时钟周期
/* ADC 触发源配置 */
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); // 配置 ADC0 规则通道的外部触发源为无(软件触发)
/* 使能 ADC 外部触发 */
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); // 使能 ADC0 规则通道的外部触发功能
/* 使能 ADC 接口 */
adc_enable(ADC0); // 使能 ADC0,ADC0 开始工作
<2>、ADC通道的DMA配置
通过查看相关芯片手册可知道ADC0数据可通过DMA0的通道0进行读取
编写ADC 采样数据通过 DMA 传输到内存 ,核心功能如下:
开启 DMA0 时钟,确保 DMA 可用
初始化 DMA0 通道 0,配置为 外设 → 内存 传输模式
设置 DMA 传输参数,ADC 采样数据自动存入 adc_dma_buffer
启用 DMA 循环模式,确保连续传输
使能 DMA0 通道 0,让 DMA 开始监听 ADC 数据
使能 ADC 的 DMA 模式,每次 ADC 采样完成后自动触发 DMA 传输
执行 ADC 校准,提高采样精度
触发 ADC 采样,让 ADC 开始工作
1. 使能 DMA 时钟
rcu_periph_clock_enable(RCU_DMA0);
解析:
RCU_DMA0
:使能 DMA0(直接存储器访问控制器) 的时钟,使 DMA 可用。
2. 初始化 DMA 通道
dma_deinit(DMA0, DMA_CH0); // 复位 DMA0 通道 0
dma_struct_para_init(&dma_init_struct); // 初始化 DMA 结构体参数
解析:
dma_deinit(DMA0, DMA_CH0);
复位 DMA0 通道 0(CH0),清除之前的配置,防止误操作。
dma_struct_para_init(&dma_init_struct);
预设 dma_init_struct
结构体为默认值,避免脏数据。
3. 配置 DMA 传输参数
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_init_struct.memory_addr = (uint32_t)adc_dma_buffer; // 目标内存地址
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_DISABLE; // 内存地址不自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; // 内存数据宽度 16 位
dma_init_struct.number = 1; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&ADC_RDATA(ADC0); // ADC 数据寄存器
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不变
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; // 外设数据宽度 16 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级设为高
dma_init(DMA0, DMA_CH0, &dma_init_struct); // 初始化 DMA0 通道 0
解析:
direction = DMA_PERIPHERAL_TO_MEMORY
数据流向:ADC 外设 → 内存。
memory_addr = (uint32_t)adc_dma_buffer
目标地址:ADC 采样数据存放到 adc_dma_buffer
(用户定义的数组)。
memory_inc = DMA_MEMORY_INCREASE_DISABLE
内存地址不自增:这里设置 固定存储地址,但这通常用于单个变量存储,如果是数组存储多个采样值,应该设为 DMA_MEMORY_INCREASE_ENABLE
。
memory_width = DMA_MEMORY_WIDTH_16BIT
数据宽度:16 位,因为 GD32F103 的 ADC 采样数据是 12 位,但存储在 16 位寄存器中。
number = 1
传输长度 = 1,表示每次传输 1 个 16 位数据。如果想让 ADC 采样多个值并存入数组,应该设为 adc_dma_buffer
的大小。
periph_addr = (uint32_t)&ADC_RDATA(ADC0)
ADC 读取地址:这是 ADC0 结果数据寄存器(RDATA) 的地址,每次 ADC 采样完成后,数据就存储在 ADC_RDATA(ADC0)
里。
periph_inc = DMA_PERIPH_INCREASE_DISABLE
外设地址不变:ADC 采样数据始终从 ADC_RDATA
读取,因此不需要地址自增。
periph_width = DMA_PERIPHERAL_WIDTH_16BIT
外设数据宽度:ADC 采样数据是 16 位,因此这里选择 16 位宽度。
priority = DMA_PRIORITY_HIGH
DMA 传输优先级:设为 高优先级,保证数据传输不会因其他外设而受阻。
4. 使能 DMA 循环模式
dma_circulation_enable(DMA0, DMA_CH0); // 开启循环模式
解析:
dma_circulation_enable()
:DMA 循环模式开启后,DMA 传输完成不会停止,而是自动重新开始。
用于持续 ADC 采样并存入缓冲区(如果 number > 1
,它会不断填充 adc_dma_buffer
)。
5. 使能 DMA 通道
dma_channel_enable(DMA0, DMA_CH0);
解析:
使能 DMA0 通道 0,让 DMA 开始监听 ADC 采样数据 传输。
6. 使能 ADC DMA 模式
adc_dma_mode_enable(ADC0);
解析:
让 ADC0 通过 DMA 传输数据。
每次 ADC 完成转换后,自动触发 DMA,将数据存入 adc_dma_buffer
。
7. 进行 ADC 校准
adc_calibration_enable(ADC0);
解析:
执行 ADC 校准(Calibration),提高 ADC 采样精度。
这个步骤必须在 ADC 使能后执行,否则校准无效。
8. 启动 ADC 采样
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
解析:
启动 ADC 转换,让 ADC 开始工作。
完整代码如下:
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH0); // 使用 DMA0 通道
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_init_struct.memory_addr = (uint32_t)adc_dma_buffer; // 内存地址
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_DISABLE; // 内存地址bu自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; // 内存数据宽度 16 位
dma_init_struct.number =1; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&ADC_RDATA(ADC0); // 外设地址(ADC 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; // 外设数据宽度 16 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH0, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_enable(DMA0, DMA_CH0); // 开启循环模式
dma_channel_enable(DMA0, DMA_CH0); /* 使能 DMA 通道 */
adc_dma_mode_enable(ADC0); /* 使能 ADC DMA 模式 */
adc_calibration_enable(ADC0); /* ADC 校准 */
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); /* 启动 ADC 转换 */
<3>、完整代码文件
之后便是完整的AD.H文件如下:
#ifndef __AD_H
#define __AD_H
#include "gd32f10x.h"
#include"systick.h"
extern uint16_t adc_value;
extern uint16_t Vol_Value;
extern uint16_t adc_dma_buffer[1];
void adc_config(void);
void get_adc(void);
#endif
完整的AD.C文件如下:
#include"ad.h"
uint16_t adc_value;
uint16_t Vol_Value;
uint16_t adc_dma_buffer[1]; // DMA缓冲区,用于存储ADC转换结果
void adc_config(void)
{
rcu_periph_clock_enable(RCU_AF); // AF时钟使能
// 配置PA4 ADC引脚
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
/* 使能 ADC0 时钟 */
rcu_periph_clock_enable(RCU_ADC0); // 使能 ADC0 的时钟,ADC0 才能正常工作
/* 配置 ADC 时钟 */
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6); // 配置 ADC 时钟为 APB2 时钟的 6 分频
/* 复位 ADC */
adc_deinit(ADC0); // 复位 ADC0,将其寄存器恢复为默认值
/* ADC 模式配置 */
adc_mode_config(ADC_MODE_FREE); // 配置 ADC 为自由运行模式(独立模式)
/* 使能 ADC 连续转换模式 */
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); // 使能 ADC0 的连续转换模式
/* ADC 数据对齐配置 */
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 配置 ADC0 数据右对齐
/* ADC 规则通道长度配置 */
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 2); // 配置 ADC0 规则通道的转换序列长度为 1(只有一个通道需要转换)
/* ADC 规则通道配置 */
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5); // 配置 ADC0 规则通道的通道 4,采样时间为 55.5 个时钟周期
/* ADC 触发源配置 */
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); // 配置 ADC0 规则通道的外部触发源为无(软件触发)
/* 使能 ADC 外部触发 */
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); // 使能 ADC0 规则通道的外部触发功能
/* 使能 ADC 接口 */
adc_enable(ADC0); // 使能 ADC0,ADC0 开始工作
dma_parameter_struct dma_init_struct;
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH0); // 使用 DMA0 通道
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_init_struct.memory_addr = (uint32_t)adc_dma_buffer; // 内存地址
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_DISABLE; // 内存地址bu自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; // 内存数据宽度 16 位
dma_init_struct.number =1; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&ADC_RDATA(ADC0); // 外设地址(ADC 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; // 外设数据宽度 16 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH0, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_enable(DMA0, DMA_CH0); // 开启循环模式
dma_channel_enable(DMA0, DMA_CH0); /* 使能 DMA 通道 */
adc_dma_mode_enable(ADC0); /* 使能 ADC DMA 模式 */
adc_calibration_enable(ADC0); /* ADC 校准 */
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); /* 启动 ADC 转换 */
}
void get_adc(void)
{
adc_flag_clear(ADC0, ADC_FLAG_EOC); //
while (SET != adc_flag_get(ADC0, ADC_FLAG_EOC)) {} //
adc_value = ADC_RDATA(ADC0);
Vol_Value = adc_value * 3300 / 4095; // 将ADC值转换为电压值
}
<4>、使用示例
#include "gd32f10x.h"
#include "gd32f10x_libopt.h"
#include "systick.h"
#include"ad.h"
#include"uart.h"
int main()
{
rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1);//设置主频108M
systick_config();//配置1ms SysTick
uart_init(115200);/* 初始化 USART0 */
adc_config();//初始化ADC0
while(1)
{
get_adc();
printf(" adc_value=%d,Vol_Value=%d dma_buffer0=%d \r\n",adc_value,Vol_Value,adc_dma_buffer[0] );
delay_1ms(1000);
}
}
三、 串口 USART 初始化与 DMA 发送/接收
通用同步异步收发器(USART)提供了一个灵活方便的串行数据交换接口,数据帧可以通过全 双工或半双工,同步或异步的方式进行传输。USART提供了可编程的波特率发生器,能对UCLK 进行分频产生USART发送和接收所需的特定频率。 USART不仅支持标准的异步收发模式,还实现了一些其他类型的串行数据交换模式,如红外编 码规范,SIR,智能卡协议,LIN,半双工以及同步模式。它还支持多处理器通信和Modem流控 操作(CTS/RTS)。 USART支持DMA功能,以实现高速率的数据通信,除了UART4。
串口通信通常用于调试和数据传输。这里我们使用 USART0 进行 UART 发送和接收,并利用 DMA 传输数据。
<1>、 串口 USART0初始化与相关功能函数
1、uart_init()
- 串口初始化
void uart_init(uint32_t baudrate) {
// 使能 GPIO 和 USART 时钟
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
// 配置 USART TX (PA9) 为复用推挽输出
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
// 配置 USART RX (PA10) 为浮空输入
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
// 复位 USART0
usart_deinit(USART0);
usart_baudrate_set(USART0, baudrate);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
// 使能 USART0 IDLE 中断
usart_interrupt_enable(USART0, USART_INT_IDLE);
nvic_irq_enable(USART0_IRQn, 0, 0);
usart_enable(USART0);
}
代码解析:
使能 GPIOA 和 USART0 时钟
配置 PA9(TX) 为 复用推挽输出,PA10(RX) 为 浮空输入
设置 波特率、数据位、停止位、校验位
开启 USART 传输和接收
使能 IDLE 中断(用于 DMA 数据接收)
2、 uart_send_byte()
- 串口发送一个字节
void uart_send_byte(uint8_t data) {
usart_data_transmit(USART0, data);
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE)); // 等待发送完成
}
代码解析:
将数据写入 USART 数据寄存器
等待 TBE(Transmit Buffer Empty) 标志置位,表示发送完成
3、uart_send_string()
- 串口发送字符串
void uart_send_string(char *str) {
while (*str) {
uart_send_byte(*str++);
}
}
代码解析:
逐个字节发送字符串
直到遇到 '\0'
结束符
4、重新定向printf函数
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
// 重定向 printf 到 UART
int fputc(int ch, FILE *f) {
uart_send_byte((uint8_t)ch);
return ch;
}
#endif
<2>、串口 DMA 发送
2.2.1 dma_usart_tx_config()
- 串口 DMA 发送初始化
所有数据帧都传输完成后,USART_STAT寄存器中TC位置1。如果USART_CTL0寄存器中 TCIE置位,将产生中断。 当DMA用于USART接收时,DMA将数据从接收缓冲区传送到片内SRAM。
void dma_usart_tx_config(uint8_t *buffer, uint16_t length) {
dma_parameter_struct dma_init_struct;
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH3); // USART0_TX 使用 DMA0 通道 3
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; // 内存到外设
dma_init_struct.memory_addr = (uint32_t)buffer; // 内存地址(发送缓冲区)
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度 8 位
dma_init_struct.number = length; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0); // 外设地址(USART0 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度 8 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH3, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_disable(DMA0, DMA_CH3); // 禁用循环模式
dma_memory_to_memory_disable(DMA0, DMA_CH3); // 禁用内存到内存模式
/* 使能 DMA 通道 */
dma_channel_enable(DMA0, DMA_CH3);
/* 使能 USART0 的 DMA 发送 */
usart_dma_transmit_config(USART0, USART_DENT_ENABLE);
}
代码解析:
- 配置 DMA 通道 3 负责 USART0_TX
- 内存到外设 传输,数据宽度 8bit
- 使能 DMA 并开启 USART0 DMA 发送
<3>、串口 DMA 接收
2.2.2 dma_usart_rx_config()
- 串口 DMA 接收初始化
当USART接收到的数据数量达到了DMA传输数据数量,DMA模块将产生传输完成中断。
void dma_usart_rx_config(uint8_t *buffer, uint16_t length) {
dma_parameter_struct dma_init_struct;
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH4); // USART0_RX 使用 DMA0 通道 2
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_init_struct.memory_addr = (uint32_t)buffer; // 内存地址(接收缓冲区)
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度 8 位
dma_init_struct.number = length; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0); // 外设地址(USART0 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度 8 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH4, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_enable(DMA0, DMA_CH4);
// dma_memory_to_memory_disable(DMA0, DMA_CH4); // 禁用内存到内存模式
/* 使能 DMA 通道 */
dma_channel_enable(DMA0, DMA_CH4);
/* 使能 USART0 的 DMA 接收 */
usart_dma_receive_config(USART0, USART_DENR_ENABLE);
}
代码解析:
- 配置 DMA 通道 4 负责 USART0_RX
- 外设到内存 传输,数据宽度 8bit
- 循环模式,用于 持续接收 数据
依据GD32F10x用户手册
软件可以通过读USART_DATA寄存器或者DMA方式获取接收到的数据。不管是直接读寄存器还是通过DMA,只要是对USART_DATA寄存器的一个读操作都可以清除RBNE位。 在接收过程中,需使能REN位,不然当前的数据帧将会丢失。 在默认情况下,接收器通过获取三个采样点的值来估计该位的值。
volatile uint16_t usart0_rx_len = 0; // 记录实际接收数据长度
uint8_t usart0_rx_buffer[USART0_RX_BUFFER_SIZE]; // DMA接收缓冲区
void USART0_IRQHandler(void) {
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE))
{
// 读取状态寄存器和数据寄存器以清除 IDLE 标志
volatile uint32_t temp;
temp = USART_STAT(USART0);
temp = USART_DATA(USART0);
(void)temp;
dma_channel_disable(DMA0, DMA_CH4); // 关闭 DMA 以获取正确长度
/* 计算接收数据长度 */
usart0_rx_len = USART0_RX_BUFFER_SIZE - dma_transfer_number_get(DMA0, DMA_CH4);
/* 重新启动 DMA 接收 */
dma_channel_enable(DMA0, DMA_CH4);
}
}
代码解析:进行数据接收处理
<4>、完整代码文件
#ifndef UART_H
#define UART_H
#include "gd32f10x.h"
#include <stdio.h>
#define USART0_RX_BUFFER_SIZE 128 // 定义接收缓冲区大小
extern uint8_t usart0_rx_buffer[USART0_RX_BUFFER_SIZE];
extern volatile uint16_t usart0_rx_len ;
// 函数声明
void uart_init(uint32_t baudrate); // 初始化 UART
void uart_send_byte(uint8_t data); // 发送一个字节
void uart_send_string(char *str); // 发送字符串
int fputc(int ch, FILE *f); // 重定向 printf 到 UART
void uart_send_data_dma(uint8_t *data);
void dma_usart_rx_config(uint8_t *buffer, uint16_t length);
#endif // UART_H
#include "uart.h"
#include <string.h>
#include <stdio.h>
//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
// 重定向 printf 到 UART
int fputc(int ch, FILE *f) {
uart_send_byte((uint8_t)ch);
return ch;
}
#endif
// 初始化 UART
void uart_init(uint32_t baudrate) {
// 使能 GPIO 和 USART 时钟
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
// 配置 USART TX 引脚为复用推挽输出
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
// 配置 USART RX 引脚为浮空输入
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
// 配置 USART 参数
usart_deinit(USART0);
usart_baudrate_set(USART0, baudrate);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE);
usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_interrupt_enable(USART0, USART_INT_IDLE);
nvic_irq_enable(USART0_IRQn, 0, 0);
usart_enable(USART0);
}
// 发送一个字节
void uart_send_byte(uint8_t data) {
usart_data_transmit(USART0, data);
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE));
}
// 发送字符串
void uart_send_string(char *str) {
while (*str) {
uart_send_byte(*str++);
}
}
void dma_usart_tx_config(uint8_t *buffer, uint16_t length) {
dma_parameter_struct dma_init_struct;
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH3); // USART0_TX 使用 DMA0 通道 3
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; // 内存到外设
dma_init_struct.memory_addr = (uint32_t)buffer; // 内存地址(发送缓冲区)
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度 8 位
dma_init_struct.number = length; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0); // 外设地址(USART0 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度 8 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH3, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_disable(DMA0, DMA_CH3); // 禁用循环模式
dma_memory_to_memory_disable(DMA0, DMA_CH3); // 禁用内存到内存模式
/* 使能 DMA 通道 */
dma_channel_enable(DMA0, DMA_CH3);
/* 使能 USART0 的 DMA 发送 */
usart_dma_transmit_config(USART0, USART_DENT_ENABLE);
}
void dma_usart_rx_config(uint8_t *buffer, uint16_t length) {
dma_parameter_struct dma_init_struct;
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA 通道 */
dma_deinit(DMA0, DMA_CH4); // USART0_RX 使用 DMA0 通道 2
dma_struct_para_init(&dma_init_struct);
/* 配置 DMA 参数 */
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_init_struct.memory_addr = (uint32_t)buffer; // 内存地址(接收缓冲区)
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度 8 位
dma_init_struct.number = length; // 传输数据长度
dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART0); // 外设地址(USART0 数据寄存器)
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不自增
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度 8 位
dma_init_struct.priority = DMA_PRIORITY_HIGH; // DMA 优先级
dma_init(DMA0, DMA_CH4, &dma_init_struct);
/* 配置 DMA 模式 */
dma_circulation_enable(DMA0, DMA_CH4);
// dma_memory_to_memory_disable(DMA0, DMA_CH4); // 禁用内存到内存模式
/* 使能 DMA 通道 */
dma_channel_enable(DMA0, DMA_CH4);
/* 使能 USART0 的 DMA 接收 */
usart_dma_receive_config(USART0, USART_DENR_ENABLE);
}
void uart_send_data_dma(uint8_t *data) {
// 计算数据长度(假设 data 是以 '\0' 结尾的字符串)
uint16_t length = strlen((char *)data);
// 配置 DMA 发送
dma_usart_tx_config(data, length);
// 等待 DMA 传输完成
while (dma_flag_get(DMA0, DMA_CH3, DMA_FLAG_FTF) == RESET);
dma_flag_clear(DMA0, DMA_CH3, DMA_FLAG_FTF);
}
volatile uint16_t usart0_rx_len = 0; // 记录实际接收数据长度
uint8_t usart0_rx_buffer[USART0_RX_BUFFER_SIZE]; // DMA接收缓冲区
void USART0_IRQHandler(void) {
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE))
{
// 读取状态寄存器和数据寄存器以清除 IDLE 标志
volatile uint32_t temp;
temp = USART_STAT(USART0);
temp = USART_DATA(USART0);
(void)temp;
dma_channel_disable(DMA0, DMA_CH4); // 关闭 DMA 以获取正确长度
/* 计算接收数据长度 */
usart0_rx_len = USART0_RX_BUFFER_SIZE - dma_transfer_number_get(DMA0, DMA_CH4);
/* 重新启动 DMA 接收 */
dma_channel_enable(DMA0, DMA_CH4);
}
}
<5>、使用示例
#include "gd32f10x.h"
#include "gd32f10x_libopt.h"
#include "systick.h"
#include"uart.h"
uint8_t tx_buffer[] = "Hello, DMA UART!\r\n\r\n";
int main()
{
rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1);//设置主频108M
systick_config();//配置1ms SysTick
uart_init(115200);/* 初始化 USART0 */
dma_usart_rx_config(usart0_rx_buffer, USART0_RX_BUFFER_SIZE); // 初始化 DMA 接收
while(1)
{
uart_send_data_dma(tx_buffer);/* 使用 DMA 发送数据 */
if (usart0_rx_len > 0)
{
printf("收到数据: %s\n", usart0_rx_buffer);
usart0_rx_len = 0; // 清除长度,准备下一次接收
}
delay_1ms(1000);
}
}
四、 总结
本文介绍了 DMA 如何用于 USART 和 ADC 数据传输,提高系统性能,减少 CPU 负担。这在 实时数据采集、传感器监测 等应用中非常重要。