【GD32】从0开始学GD32单片机(7)—— DMA直接存储器访问控制器详解+DMA串口发送和接收例程
简介
DMA的作用简单说就是用来传输数据的,那么它又有什么优势呢?
像我们平时的数据传输,如把某一字符串通过串口发送出去,我们就需要CPU把内存中对应的数据逐个复制到串口的发送缓冲区。对于轻负载的使用下没有上面问题,但对于重负载的使用下这样做比较浪费CPU的资源。
而DMA的优势就在于通过它传输数据不需要CPU的介入,那么这时CPU就可以做其他的工作,大大节约了CPU的资源。
DMA有3种数据传输方向,分别是外设到存储器、存储器到外设、存储器到存储器,支持3种数据宽度,分别是字节、半字、字。
字节=8bit,半字=16bit,字=32bit
外设握手与仲裁
下面是DMA的结构框图。
一个DMA外设里面有多个通道,这些通道是可以同时工作互不干扰的,每个通道里面可以接收若干个外设的请求。
如下表就列出了GD32F103C8T6中DMA0各通道对于的外设请求。
当外设请求DMA进行数据传输时,需要对外设进行握手。
外设请求DMA传输,若DMA空闲且当前通道优先级较高,那么DMA会进行应答,外设请求释放;否则外设请求信号一直存在直到DMA发送答应信号。
从上面可以看到,DMA也有类似于中断的优先级系统,DMA一共有4种优先级,分别是低、中、高、极高。
当DMA在同一时刻接收到多个外设的请求时,DMA内部的仲裁器会根据请求的优先级决定,先响应那个外设的请求。
首先,仲裁器先比较通道的优先级,优先级更高的外设先得到响应;但如果通道的优先级一样,则比较通道的编号,通道编号越小,优先级越高。
DMA的原理还是比较简单的,在使用过程中只需要设置好,其他的操作基本都是DMA自动完成的。
地址自增
要使用DMA进行数据传输,首先要确定的是数据从哪里来到哪里去;比如我们使用外设到存储器模式,我们就必须设置外设的基地址和存储器的基地址。
为了通过运行的效率,DMA提供了地址的自增功能。比如我们要通过DMA发送一串字符串,我们需要把数据逐一送到串口的数据寄存器,通过开启内存地址的自增功能,DMA在发送完一个字符后会自动将地址自增,以发送下一个字符。
循环模式
在一些特殊的应用场合,我们想DMA在完成一次传输后,立即复位并准备响应下一次传输,那么DMA也提供了循环模式来满足这个要求。
还是以上面的例子为例,我们提供DMA向串口发送一串字符串,在开启了存储器地址自增;当DMA传输完成后,因为存储器的地址产生了偏移,为了传输一模一样的内容,我们需要重新设置存储器的基地址,很明显这样十分麻烦。
但通过开启DMA的循环模式,DMA在每次传输完成后会自动将基地址复位,所有的寄存器都恢复到我们一开始设置的样子,并准备下一次的传输。
例程
DMA串口发送
现象:串口每秒向上位机发送一串字符串。
usart.c文件
#include "usart.h"
void USART_Config(void)
{
/* 初始化GPIO外设 */
rcu_periph_clock_enable(RCU_GPIOA);
/* TX管脚,PA9,复用推挽输出,速度50MHz */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
/* RX管脚,PA10,下拉输入,速度50MHz */
gpio_init(GPIOA, GPIO_MODE_IPD, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
/* DMA外设初始化 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化USART外设 */
rcu_periph_clock_enable(RCU_USART0); // 使能串口0时钟
usart_baudrate_set(USART0, 115200); // 波特率115200
usart_parity_config(USART0, USART_PM_NONE); // 无校检
usart_word_length_set(USART0, USART_WL_8BIT); // 8位数据位
usart_stop_bit_set(USART0, USART_STB_1BIT); // 1位停止位
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); // 使能串口发送
usart_receive_config(USART0, USART_RECEIVE_ENABLE); // 使能串口接收
usart_dma_transmit_config(USART0, USART_DENT_ENABLE); // 使能串口DMA发送
usart_enable(USART0); // 使能串口
}
void USART_SendByte_DMA(char ch)
{
dma_parameter_struct dma_struct = {0};
dma_struct.direction = DMA_MEMORY_TO_PERIPHERAL; // 内存到外设
dma_struct.memory_addr = (uint32_t)&ch; // 内存基地址
dma_struct.memory_inc = DMA_MEMORY_INCREASE_DISABLE; // 关闭内存自增
dma_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度8bit
dma_struct.number = 1; // 1个数据
dma_struct.periph_addr = (uint32_t)0x40013804; // 串口缓冲区基地址
dma_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 关闭外设地址自增
dma_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度8bit
dma_struct.priority = DMA_PRIORITY_HIGH; // 优先级高
dma_init(DMA0, DMA_CH3, &dma_struct);
dma_circulation_disable(DMA0, DMA_CH3); // 关闭循环模式
dma_flag_clear(DMA0, DMA_CH3, DMA_FLAG_FTF); // 清除DMA传输完成标志位
dma_channel_enable(DMA0, DMA_CH3); // 使能DMA传输
while(dma_flag_get(DMA0, DMA_CH3, DMA_FLAG_FTF) == RESET); // 等待DMA传输完成
dma_channel_disable(DMA0, DMA_CH3); // 关闭DMA传输
}
void USART_SendString_DMA(char* str, uint8_t len)
{
dma_parameter_struct dma_struct = {0};
dma_struct.direction = DMA_MEMORY_TO_PERIPHERAL; // 内存到外设
dma_struct.memory_addr = (uint32_t)str; // 内存基地址
dma_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 开启内存自增
dma_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度8bit
dma_struct.number = len; // len个数据
dma_struct.periph_addr = (uint32_t)0x40013804; // 串口缓冲区基地址
dma_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 关闭外设地址自增
dma_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度8bit
dma_struct.priority = DMA_PRIORITY_HIGH; // 优先级高
dma_init(DMA0, DMA_CH3, &dma_struct);
dma_circulation_disable(DMA0, DMA_CH3); // 关闭循环模式
dma_flag_clear(DMA0, DMA_CH3, DMA_FLAG_FTF); // 清除DMA传输完成标志位
dma_channel_enable(DMA0, DMA_CH3); // 使能DMA传输
while(dma_flag_get(DMA0, DMA_CH3, DMA_FLAG_FTF) == RESET); // 等待DMA传输完成
dma_channel_disable(DMA0, DMA_CH3); // 关闭DMA传输
}
说明:
在代码中提供了两个DMA串口发送的函数,一个是只发送一个字符,另一个是发送一个字符串。
两种需求在DMA设置的区别在于:发送一个字符,DMA的数据块数为1个不变,并且存储器不需要开启地址自增;而发送一串字符串则要根据需要设置DMA要传输数据块的数量,并且要开启存储器地址自增。
使用DMA串口发送不能使用重定义的printf函数
main.c文件
#include "gd32f10x.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>
int main(void)
{
systick_config();
USART_Config();
while(1)
{
char buf[] = "Hello\n";
USART_SendString_DMA(buf, sizeof(buf));
delay_ms(1000);
}
}
DMA串口接收
现象:单片机向上位机发送接收到的字符串。
usart.c文件
#include "usart.h"
void USART_Config(void)
{
/* 初始化GPIO外设 */
rcu_periph_clock_enable(RCU_GPIOA);
/* TX管脚,PA9,复用推挽输出,速度50MHz */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
/* RX管脚,PA10,下拉输入,速度50MHz */
gpio_init(GPIOA, GPIO_MODE_IPD, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
/* 初始化DMA外设 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化USART外设 */
rcu_periph_clock_enable(RCU_USART0); // 使能串口0时钟
usart_baudrate_set(USART0, 115200); // 波特率115200
usart_parity_config(USART0, USART_PM_NONE); // 无校检
usart_word_length_set(USART0, USART_WL_8BIT); // 8位数据位
usart_stop_bit_set(USART0, USART_STB_1BIT); // 1位停止位
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); // 使能串口发送
usart_receive_config(USART0, USART_RECEIVE_ENABLE); // 使能串口接收
usart_dma_transmit_config(USART0, USART_DENT_ENABLE); // 使能串口DMA发送
usart_dma_receive_config(USART0, USART_DENR_ENABLE); // 使能串口DMA接收
usart_interrupt_enable(USART0, USART_INT_RBNE); // 使能串口接收中断
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 2位抢占优先级,2位响应优先级
nvic_irq_enable(USART0_IRQn, 1, 1); // 抢占优先级为1,响应优先级为1
usart_enable(USART0); // 使能串口
}
uint8_t USART_ReceiveData_DMA(void)
{
uint8_t dat = 0x00;
dma_parameter_struct dma_struct = {0};
dma_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 外设到内存
dma_struct.memory_addr = (uint32_t)&dat; // 内存基地址
dma_struct.memory_inc = DMA_MEMORY_INCREASE_DISABLE; // 关闭内存自增
dma_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; // 内存数据宽度8bit
dma_struct.number = 1; // 1个数据
dma_struct.periph_addr = (uint32_t)0x40013804; // 串口缓冲区基地址
dma_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 关闭外设地址自增
dma_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 外设数据宽度8bit
dma_struct.priority = DMA_PRIORITY_HIGH; // 优先级高
dma_init(DMA0, DMA_CH4, &dma_struct);
dma_circulation_disable(DMA0, DMA_CH4); // 关闭循环模式
dma_flag_clear(DMA0, DMA_CH4, DMA_FLAG_FTF); // 清除DMA传输完成标志位
dma_channel_enable(DMA0, DMA_CH4); // 使能DMA传输
while(dma_flag_get(DMA0, DMA_CH4, DMA_FLAG_FTF) == RESET); // 等待DMA传输完成
dma_channel_disable(DMA0, DMA_CH4); // 关闭DMA传输
return dat;
}
gd32f10_it.c文件
#include "gd32f10x_it.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>
struct receive_struct
{
char buf[20];
uint8_t len;
} usart_buf;
void USART0_IRQHandler(void)
{
// 检查是否为接收缓冲区非空
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE) == SET)
{
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE); // 清除中断标志位
char dat = USART_ReceiveData_DMA();
usart_buf.buf[usart_buf.len++] = dat;
if (dat == '\n')
{
printf("received: %s", usart_buf.buf);
usart_buf.len = 0;
memset(usart_buf.buf, 0x00, sizeof(usart_buf.buf));
}
}
}
DMA串口接收的例程和串口中断接收的例程是差不多的,也是在串口接收中断服务函数中接收数据,接收完成后向上位机打印。只需要把中断服务函数里面的读串口缓冲区函数换成我们写的带DMA的函数即可。
相关文章
- 温故《单片机基础》之——AD转换
- 普大喜奔:智能车竞赛STC 16位、8位单片机免费样品申请开始啦!
- 《51单片机应用开发范例大全(第3版)》——2.2 扩展芯片实现端口扩展
- 《51单片机应用开发从入门到精通》——第 1 章 单片机开发预备知识 1.1 单片机开发流程
- 《51单片机应用开发从入门到精通》——2.10 变频报警实例
- 《51单片机应用开发从入门到精通》——2.13 软件陷阱实例
- 《例说8051:单片机程序设计案例教程》——1-3 8051的开发流程与工具
- 《例说51单片机(C语言版)(第3版)》一1.6 实例演练
- 《单片机串口通信及测控应用实战详解》——第6章 多个单片机与PC串口 通信的数据传送 6.1 系统设计说明
- 《单片机串口通信及测控应用实战详解》——导读
- 手把手教你单片机HAL库开发——中断,通信、串口
- 单片机学习——存储器详解(程序存储器、片内RAM、拓展RAM、EEPROM)
- STC89C52单片机I2C通信以及AT24C02介绍使用代码演示
- 【蓝桥杯单片机组国赛】国赛前最后一更
- 单片机的内存分配(变量的存储位置)详解
- 【GD32】从0开始学GD32单片机(12)—— TIMER高级定时器详解+DMA修改PWM波占空比例程
- 【GD32】从0开始学GD32单片机(10)—— TIMER基本定时器详解+1毫秒延时例程
- 【GD32】从0开始学GD32单片机(6)—— EXTI中断/事件控制器详解+串口接收中断及外部按键中断例程