zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

【GD32】从0开始学GD32单片机(7)—— DMA直接存储器访问控制器详解+DMA串口发送和接收例程

单片机 详解 访问 开始 发送 直接 串口 控制器
2023-09-11 14:21:44 时间

简介

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的函数即可。

在这里插入图片描述