1、DMA简介
DMA控制器提供了一种硬件的方式在外设和存储器之间或者存储器与存储器直接之间传输数据,而无需CPU的介入,正因如此,DMA传输至关重要。DMA控制器有12个通道(DMA0有7个通道,DMA1有5个通道),每个通道都可以专门用来处理一个或多个外设的存储器访问请求,可以控制DMA请求的优先级。
DMA与Cortex®-M4 内核共享系统总线,当DMA和CPU访问相同地址时,因为DMA不占用CPU资源,所以可以与CPU争夺地址访问权,导致CPU访问系统总线延误几个总线周期,总线矩阵中实现了循环仲裁算法来分配DMA和CPU的访问权,确保CPU得到至少一半的总线系统带宽。
2、主要特性
1. 传输数据的长度可以编程,最高可达到,即65536;
2. DMA有12个通道,每个通道都可以单独配置,都连接有固定的硬件DMA请求;
3. 片上闪存FLASH、SRAM、AHB(时钟总线)和APB(时钟总线)外设都可以作为访问的源端和目的端;
4.可以设置DMA优先级:硬件优先级(通道号越低,优先级越高)和软件优先级(低、中、高、极高);
5. 存储器和外设之间数据传输宽度支持字节、字和半字,也可以选择固定寻址和增量式寻址(地址自增或自减或不变);
6. 支持外设到存储器、存储器到外设、存储器到存储器三种传输方式;
7. 每个通道都有3种类型的事件标志(不常用)和独立的中断,每个中断都可以使能和清除;
8. 支持循环传输模式(不常用)
3、功能
这是DMA结构框图,其中AHB从接口用于配置DMA,AHB主接口用于进行数据传输,仲裁器用于管理DMA的优先级。
3.1 传输
DMA传输大概分为两步操作,从源地址读取数据,之后将读取到的数据存储到目的地址。传输主要由DMA_CHxCNT寄存器控制,该寄存器表明还有多少数据等待被传输。DMA_CHxCNT寄存器的CNT位域必须在CHEN位置位前被配置,其控制传输的次数。在传输过程中,CNT位域的值表示还有多少次数据传输将被执行,将其清零可以停止DMA传输。CHEN位为1时,该位域不能被配置一旦通道使能,该寄存器为只读的,并在每个DMA传输之后值减1。如果该寄存器的值为0,无论通道开启与否,都不会有数据传输。如果该通道工作在循环模式下,一旦通道的传输任务完成,该寄存器会被 自动重装载为初始设置值。
3.2 地址生成
存储器和外设都有两种地址生成算法,分别是固定模式和增量模式两种。在固定模式中,地址一直固定为初始化的基地址;而增量模式传输数据的地址时当前地址加1(2或者4),这个值取决于数据传输宽度。
为更好理解,假设有一百个元素的数组,其数据时0-99慢慢增,0对应的地址是0x00,99对应的地址是0x99,固定模式下即使固定传输某一固定地址的值,不会改变,而在增量模式下,则从0x00一直传输到0x99,依次递增。
3.3 存储器到存储器模式
存储器到存储器模式依赖DMA_CHxCTL寄存器的M2M位置位使能,在此模式下可以不需要收到外设的请求信号,寄存器的值置1立刻释放资源直至为0。
3.4 通道配置
配置之前,可以观察DMA通道表,选定合适的DMA通道,本次选择的是USART0的Channel3进行发送数据,Channel4进行接收接收。
要启动一次新的DMA数据传输,建议遵循以下步骤进行操作:
1. 读取CHEN位,如果为1(通道已使能),清零该位。当CHEN为0时,请按照下列步骤 配置DMA开始新的传输;
2.配置DMA_CHxCTL寄存器的M2M及DIR位,选择传输模式;
3. 配置DMA_CHxCTL寄存器的CMEN位,选择是否使能循环模式;
4. 配置DMA_CHxCTL寄存器的PRIO位域,选择该通道的软件优先级;
5. 通过 DMA_CHxCTL 寄存器配置存储器和外设的传输宽度以及存储器和外设地址生成算法;
6. 通过 DMA_CHxCTL 寄存器配置传输完成中断,半传输完成中断,传输错误中断的使能位;
7. 通过DMA_CHxPADDR寄存器配置外设基地址;
8. 通过DMA_CHxMADDR寄存器配置存储器基地址;
9. 通过DMA_CHxCNT寄存器配置数据传输总量;
10. 将DMA_CHxCTL寄存器的CHEN位置1,使能DMA通道。
具体配置代码下面有
3.5 中断
每一个DMA通道都有一个专用的中断,有三种类型,分别为传输完成、半传输、传输错误。具体的标志位、清除位、使能位如下。
4、代码实现
uart.c
#include"uart.h"
#include"string.h"
#include <stdio.h>
#include"stdarg.h"
#include"systick.h"
/* 发送缓存 */
uint8_t UART0_TX_BUF[UART0_TX_LEN]; /* 发送缓冲区 */
//uint8_t DMA_BUF_BUSY = 0 ; /* 缓冲区是否已被占用 */
///* 接收缓存 */
uint8_t UART0_RX_BUF[UART0_RX_LEN*2]; /* 双接收缓冲区 */
uint8_t UART0_RX_STAT = 0; /* 接受状态 0x01:已接收到数据 0x03:接收缓冲区半满 0x07:接收缓冲区全满 */
uint32_t UART0_RX_NUM = 0;
void uart0_init(uint32_t bound)
{
dma_parameter_struct dma_init_struct; //定义一个DMA配置结构体,放于后面会报错
/* 使能 GPIOA 时钟 */
rcu_periph_clock_enable(RCU_GPIOA);
/* 使能 USART0 时钟 */
rcu_periph_clock_enable(RCU_USART0);
/* PA9 复用为 USART0_Tx */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_9);
/* PA10 复用为 USARTx_Rx */
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ,GPIO_PIN_10);
/* USART0 初始化配置 */
usart_deinit(USART0);
usart_baudrate_set(USART0, bound); /* 设置波特率 */
usart_receive_config(USART0, USART_RECEIVE_ENABLE); /* 使能接收 */
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); /* 使能发送 */
usart_enable(USART0);
/* 使能 DMA 时钟 */
rcu_periph_clock_enable(RCU_DMA0);
/* 初始化 DMA0 通道3 */
dma_deinit(DMA0, DMA_CH3);
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; /* 存储器到外设方向 */
dma_init_struct.memory_addr = (uint32_t)UART0_TX_BUF; /* 存储器基地址 */
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; /* 存储器地址自增 */
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; /* 存储器位宽为8位 */
dma_init_struct.number = UART0_TX_LEN; /* 传输数据个数 */
dma_init_struct.periph_addr = ((uint32_t)0x40013804); /* 外设基地址,即USART数据寄存器地址 */
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_ULTRA_HIGH; /* 软件优先级为极高*/
dma_init(DMA0, DMA_CH3, &dma_init_struct);
/* DMA循环模式配置,不使用循环模式 */
dma_circulation_disable(DMA0, DMA_CH3);
/* DMA存储器到存储器模式模式配置,不使用存储器到存储器模式*/
dma_memory_to_memory_disable(DMA0, DMA_CH3);
/* USART DMA 发送使能 */
usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
/* DMA0 通道3 中断优先级设置并使能 */
// nvic_irq_enable(DMA0_Channel3_IRQn, 0, 0);
// /* 使能 DMA0 通道3 传输完成、传输错误中断 */
// dma_interrupt_enable(DMA0, DMA_CH3, DMA_INT_FTF|DMA_INT_ERR);
/* 使能 DMA0 通道3 */
dma_channel_enable(DMA0, DMA_CH3);
/* 初始化 DMA0 通道4 */
dma_deinit(DMA0, DMA_CH4);
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; /* 外设到存储器方向 */
dma_init_struct.memory_addr = (uint32_t)UART0_RX_BUF; /* 存储器基地址 */
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; /* 存储器地址自增 */
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; /* 存储器位宽为8位 */
dma_init_struct.number = UART0_TX_LEN*2; /* 传输数据个数 */
dma_init_struct.periph_addr = ((uint32_t)0x40013804); /* 外设基地址,即USART数据寄存器地址 */
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_ULTRA_HIGH; /* 软件优先级为极高*/
dma_init(DMA0, DMA_CH4, &dma_init_struct);
/* DMA循环模式配置,不使用循环模式 */
dma_circulation_disable(DMA0, DMA_CH4);
/* DMA存储器到存储器模式模式配置,不使用存储器到存储器模式*/
dma_memory_to_memory_disable(DMA0, DMA_CH4);
/* USART DMA 接收使能 */
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
/* DMA0 通道4 中断优先级设置并使能 */
// nvic_irq_enable(DMA0_Channel4_IRQn, 0, 0);
// /* 使能 DMA0 通道4 半传输、传输完成、传输错误中断 */
// dma_interrupt_enable(DMA0, DMA_CH4, DMA_INT_FTF|DMA_INT_HTF|DMA_INT_ERR);
/* 使能 DMA0 通道4 */
dma_channel_enable(DMA0, DMA_CH4);
/* USART中断设置,抢占优先级0,子优先级0 */
nvic_irq_enable(USART0_IRQn, 0, 0);
/* 使能USART0空闲中断 */
usart_interrupt_enable(USART0, USART_INT_IDLE);
}
/* 自定义UART0 printf 函数
* 参数:带发送的字符串,确保一次发送数据不超过UART0_TX_LEN字节
* 返回值:无 */
void u1_printf(char* fmt,...)
{
uint32_t i;
va_list ap; //定义一个指向参数的指针,可以通过指针运算来调整访问的对象。va_list可以存储可变参数
va_start(ap,fmt);//初始化,用ap去指向fmt
vsprintf((char*)UART0_TX_BUF,fmt,ap);//第一个参数是指向字符数组的指针、fmt是用于指定输出的格式、包含格式化字符串中使用的可变参数
va_end(ap);//结束可变参数的获取,清空va_list
UART0_TX_BUF[UART0_TX_LEN-1] = '\0';
/* 计算此次发送数据的长度 */
i=strlen((const char*)(UART0_TX_BUF));
/* 设置DMA传输 */
dma_channel_disable(DMA0, DMA_CH3); /* 关闭DMA传输才可以进行设置 */
dma_memory_address_config(DMA0,DMA_CH3,(uint32_t)(UART0_TX_BUF));
dma_transfer_number_config(DMA0,DMA_CH3,i);
dma_channel_enable(DMA0, DMA_CH3); /* 开启DMA传输 */
}
///* DMA0 通道3 中断服务函数
// * 参数:无
// * 返回值:无 */
//void DMA0_Channel3_IRQHandler(void)
//{
// /* 清除DMA0 通道3 中断标志位 */
// dma_interrupt_flag_clear(DMA0, DMA_CH3, DMA_INT_FLAG_G);
// /* 进入中断,表示已经传输完成缓冲区,释放缓冲区 */
// if(DMA_BUF_BUSY == 1) DMA_BUF_BUSY = 0;
//}
///* DMA0 通道4 中断服务函数
// * 参数:无
// * 返回值:无 */
//void DMA0_Channel4_IRQHandler(void)
//{
// /* 清除DMA0 通道3 中断标志位 */
dma_interrupt_flag_clear(DMA0, DMA_CH3, DMA_INT_FLAG_G);
// if(dma_interrupt_flag_get(DMA0, DMA_CH4, DMA_INT_FLAG_HTF))
// {
UART0_RX_NUM = UART0_RX_LEN;
// UART0_RX_BUF[UART0_RX_LEN] = '\0'; /* 添加字符串结束符 */
// UART0_RX_STAT = 0x03; /* 接受状态 0x03:接收缓冲区半满 */
// }
// if(dma_interrupt_flag_get(DMA0, DMA_CH4, DMA_INT_FLAG_FTF))
// {
UART0_RX_NUM = UART0_RX_LEN*2;
// UART0_RX_BUF[UART0_RX_LEN*2-1] = '\0'; /* 添加字符串结束符 */
// UART0_RX_STAT = 0x07; /* 接受状态 0x07:接收缓冲区全满 */
// }
dma_interrupt_flag_clear(DMA0, DMA_CH4, DMA_INT_FLAG_G);
//}
/* USART0 中断服务函数
* 参数:无
* 返回值:无 */
/* 串口0中断服务程序 */
void USART0_IRQHandler(void)
{
if(RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) //空闲中断
{
usart_data_receive(USART0); /* 清除接收完成标志位 */
dma_channel_disable(DMA0, DMA_CH4); /* 关闭DMA传输 */
// u1_printf("这是%d \n\r",sizeof(UART0_RX_BUF));
// u1_printf("这是%d \r\n",dma_transfer_number_get(DMA0,DMA_CH4));
UART0_RX_NUM = sizeof(UART0_RX_BUF) - dma_transfer_number_get(DMA0,DMA_CH4);
UART0_RX_BUF[UART0_RX_NUM] = '\0'; /* 添加字符串结束符 */
UART0_RX_STAT = 0x01; /* 接受状态 0x01:已接收到数据 */
/* 重新设置DMA传输 */
dma_memory_address_config(DMA0,DMA_CH4,(uint32_t)UART0_RX_BUF);
dma_transfer_number_config(DMA0,DMA_CH4,sizeof(UART0_RX_BUF));
dma_channel_enable(DMA0, DMA_CH4); /* 开启DMA传输 */
usart_interrupt_flag_clear(USART0,USART_INT_FLAG_IDLE); /* 清除空闲中断标志位 */
}
}
此次传输可以不使用DMA的中断,使用与否对结果不产生影响,主要是为了展示DMA的使用方法,可以看到注释掉了DMA的中断依旧可以运行。
uart.h
#ifndef __USART_H
#define __USART_H
#include <stdio.h>
#include "gd32f30x.h"
#define UART0_RX_LEN 256 /* 单个缓存区字节数 */
#define UART0_TX_LEN 256 /* 单次最大发送缓存字节数 */
/* 发送缓存 */
extern uint8_t UART0_TX_BUF[UART0_TX_LEN]; /* 发送缓冲区 */
extern uint8_t DMA_BUF_BUSY ; /* 缓冲区是否已被占用 */
///* 接收缓存 */
extern uint8_t UART0_RX_BUF[UART0_RX_LEN*2]; /* 双接收缓冲区 */
extern uint8_t UART0_RX_STAT ; /* 接受状态 0x01:已接收到数据 0x03:接收缓冲区半满 0x07:接收缓冲区全满 */
extern uint32_t UART0_RX_NUM ;
void uart0_init(uint32_t bound);
void u1_printf(char* fmt,...) ;
#endif
main.c
#include "gd32f30x.h"
#include "systick.h"
#include "uart.h"
int main(void)
{
systick_config();
uart0_init(115200); //初始化
u1_printf("start \r\n");
u1_printf("运行到这");
while(1)
{
if(UART0_RX_STAT>0)
{
UART0_RX_STAT=0;
u1_printf("receive %d ,data: %s \r\n",UART0_RX_NUM,UART0_RX_BUF);
}
delay_1ms(500);
}
}
接下来就可以去串口工具进行验证,可以观察到,电脑输入什么数据,单片机就会回复数据长度和内容。
此次历程的uart0_init()函数可以在后续例程进行常驻,其中的u0_printf函数可以避免掉printf的重定向操作,且该u0_pringf函数不占用CPU运行资源。