DMA(Direct Memory Access)意思就是直接访问内存

我使用的是GD32F470VET6进行学习

目录

前言

USART串口的中断触发通信

串口的中断配置

串口接收数据需要的变量

编写中断接收服务函数

串口USART使用DMA方式

了解DMA传输需要的结构体

配置DMA结构体

配置DMA中断

DMA中断服务函数


前言

他可以用在存储器到外设,外设到存储器,存储器到存储器之间的数据传输,这是一种无需经过cpu处理就能进行数据传输的方式,例如串口,我们平常都是经过中断处理让CPU拷贝到内部变量来进行处理,这样简单程序是可以的,但是一旦数据量增大,就会频繁的触发中断,就会浪费大量资源,增加CPU的负担,所以就会推荐使用DMA方式对其直接进行数据到存储器,无需过问CPU,大大的节省效率

这里就进行串口通信(USART)的DMA与其中断的通信方式

USART串口的中断触发通信

根据前一篇文章所写的学习其串口通信基础上(包括串口重定向函数直接使用printf进行调试https://blog.csdn.net/MCSDN6233/article/details/151903978?spm=1001.2014.3001.5501

先进行最基础的串口配置

#define USART0_Port      (GPIOA)
#define USART0_Rx        (GPIO_PIN_10)
#define USART0_Tx        (GPIO_PIN_9)

void USART0_Init(uint32_t baudval )
{
  rcu_periph_clock_enable(RCU_GPIOA);//对端口进行时钟使能
	rcu_periph_clock_enable(RCU_USART0);//对串口0进行时钟使能
	
	gpio_mode_set(USART0_Port,GPIO_MODE_AF,GPIO_PUPD_PULLUP,USART0_Rx);//设置RX引脚为复用上拉模式
	gpio_af_set(USART0_Port,GPIO_AF_7,USART0_Rx);//对RX引脚进行复用
	gpio_output_options_set(USART0_Port,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,USART0_Rx);//设置RX为推挽输出
	gpio_mode_set(USART0_Port,GPIO_MODE_AF,GPIO_PUPD_PULLUP,USART0_Tx);//设置TX引脚为复用上拉模式
	gpio_af_set(USART0_Port,GPIO_AF_7,USART0_Tx);//对TX引脚进行复用
	gpio_output_options_set(USART0_Port,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,USART0_Tx);//设置TX为推挽输出
	
	//配置串口
	usart_deinit(USART0);//串口0重置
	usart_baudrate_set(USART0,baudval);//设置波特率
	usart_parity_config(USART0,USART_PM_NONE);//设置为无效验位
	usart_word_length_set(USART0,USART_WL_8BIT);//设置数据位长度8bit
	usart_stop_bit_set(USART0,USART_STB_1BIT);//设置停止位长度1bit
	usart_receive_config(USART0,USART_RECEIVE_ENABLE);//使能串口接收
	usart_transmit_config(USART0,USART_TRANSMIT_ENABLE);//使能串口发送
	
	//串口0使能
	usart_enable(USART0);
	
}

串口的中断配置

通过查找gd32f4xx_usart.h文件

查看到有关中断标志,分别是

USART_INT_PERR(Parity Error):效验位错误
USART_INT_TBE(Transmit Buffer Empty):发送缓冲区空中断
USART_INT_TC(Transmission Complete):发送完成中断
USART_INT_RBNE(Read Buffer Not Empty):接收缓冲区非空中断
USART_INT_IDLE(Idle Line Detected):空闲线中断检测
USART_INT_LBD(LIN Break Detected):LIN总线断开检测中断
USART_INT_CTS(Clear To Send):硬件流控中断
USART_INT_ERR(Error Interrupt):错误中断总开关中断
USART_INT_EB(End of Block):块接收完成中断
USART_INT_RT(Receive Timeout);接收超时中断

一般我们只需要使用到USART_INT_RBNE中断标志来中断接收每一个字符,和USART_INT_IDLE空闲检测中断来判断一帧数据是否传输完成,所以接下来就要进行使能串口中断,在配置完标准串口设置的初始化函数USART0_Init();函数中最后需要加入

//配置中断
usart_interrupt_enable(USART0,USART_INT_RBNE);//使能接收非空缓冲区中断
usart_interrupt_enable(USART0,USART_INT_ERR);//使能总错误中断
usart_interrupt_enable(USART0,USART_INT_IDLE);//使能空闲检测中断
//配置优先级
nvic_irq_enable(USART0_IRQn,2,2);

串口接收数据需要的变量

因为单片机需要接收数据,所以单片机需要先接收到数据将其存储起来之后再进行操作,就需要预先设置一些变量来进行存储操作

#define USART_RECEIVE_LENGTH      (4096)//串口缓冲区长度
uint8_t  g_recv_buff[USART_RECEIVE_LENGTH]; // 接收缓冲区
uint16_t g_recv_length = 0; // 接收数据长度
uint8_t  g_recv_complete_flag = 0; // 接收完成标志位

这些就可以进行串口的接收中断了

接收中断自然就得到中断函数里面进行编写代码

获取中断标志位的状态函数(第一个参数是要获取的串口外设,第二个是要获取的串口标志位)

FlagStatus usart_interrupt_flag_get(uint32_t usart_periph, usart_interrupt_flag_enum int_flag);

这个枚举主要意思就是将中断标志位使能和对应的状态和错误标志位打包,他可以让同一个值实现三件事

1:打开/关闭中断使能

2;查询中断是否挂起

3:判断到底是哪种错误/事件引发了中断

串口的接收函数就是uint16_t usart_data_receive(uint32_t usart_periph);

编写中断接收服务函数

void USART0_IRQHandler(void)
{
    if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE)!=RESET)//接收缓冲区不为空
    {
         g_recv_buff[g_recv_length++]=usart_data_receive(USART0);//把接收到的数据放到缓冲区中
    }
    if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_IDLE) == SET)//检测到帧中断
    {
        usart_data_receive(USART0);//必须要读,读出来的值不能要
        g_recv_buff[g_recv_length]='\0';
        g_recv_complete_flag=SET;//接收完成
    }
}

关于是否接收到了数据,我们就需要在main的while循环中对接收到的数据进行输出来判断是否是接收的数据,在while中添加

   if(g_recv_complete_flag)//数据接收完成
   {
     g_recv_complete_flag=RESET;//等待下次接收
     printf("g_recv_length:%d  ",g_recv_length);
     printf("Receive:%s\r\n",g_recv_buff);//打印接收的数据
     memset(g_recv_buff,0,g_recv_length);//清空数组
     g_recv_length=0;
   }

每次接收和处理完之后都需要把对应的标志位和数组进行清零重置处理

注意,这里使用printf的前提是必须进行了串口重定向配置,具体见我上一篇串口文章,开头附有链接

编译下载一气呵成,打开上位机进行输出

显然已经成功使用中断进行数据接收并且发送了出来,符合预期

串口USART使用DMA方式

通过查找用户手册可以得知USART0_RX是对应的DMA1的通道2,5

依旧在启用任何一个外设的时候都得先初始化他的外设时钟

rcu_periph_clock_enable(RCU_DMA1);//使能DMA1时钟

了解DMA传输需要的结构体

dma_single_data_parameter_struct;这个结构体是用于配置DMA的单数传输模式的参数

periph_addr:外设用于数据传输的寄存器地址
periph_inc:外设地址生成算法模式
memory0_addr:存储器0的基地址
memory_inc:存储器地址的递增模式
periph_memory_width:外设和存储器之间的数据传输宽度
circular_mode:DMA循环模式
direction:数据传输方向
number:传输次数
priority:DMA通道的优先级                        

一个一个来就是,首先需要打开gd32f4xx_usart.h

periph_addr数据传输寄存器地址

需要通过这些宏定义来获取到外设的寄存器地址,这里我们需要的是数据寄存器

所以就使用USART_DATA(USART0)来获取

periph_inc选择外设地址的生成模式

打开gd32f4xx_dma.h

DMA_PERIPH_INCREASE_ENABLE:增量模式

说明:每次 DMA 传输完成后,外设的地址会自动按照数据宽度(8/16/32 位)递增

DMA_PERIPH_INCREASE_DISABLE:固定模式(适用于串口,SPI,ADC灯外设)

说明:DMA 传输过程中,外设地址始终保持不变,每次都访问同一个地址

DMA_PERIPH_INCREASE_FIX:递增固定模式(极少使用)

memory0_addr存储器基地址

这里就是我们定义的接收数组变量的地址g_recv_buff

memory_inc是存储器地址的生成算法模式

存储器需要存储数据,所以每次传输之后就要放在不同的地址,就直接选择DMA_CIRCULAR_MODE_ENABLE增量就行了

periph_memory_width数据传输宽度

依次是8位,16位,32位宽度,因为串口是8位传输,所以这里选择DMA_PERIPH_WIDTH_8BIT

circular_mode DMA循环模式使能

因为串口的传输是离散性的,有需求才传输,而DMA的特性是当传输次数number完成后,会制动重置地址和计数器,重新开始传输,如果不是那种连续,固定长度,没有暂停的场景,就关闭DMA的循环模式,否者会导致数据丢失和乱码,这一这里选DMA_CIRCULAR_MODE_DISABLE

numberDMA通道传输数量

通过DMA传输的数据量,当数据量超过这个配置值的时候,超出的部分不会接收到,需要按照自己的要求配置

direction DMA的数据传输方向

分别是外设到内存,内存到外设,内存到内存

现在我们做的是串口的DMA接收数据所以就是外设到内存DMA_PERIPH_TO_MEMORY

priority 传输优先级

从上到下依次为低,中,高,超高

了解完这个结构体,接下来就开始配置

配置DMA结构体

#define  ARRAYNUM(arr_name)   (uint32_t)(sizeof(arr_name) / sizeof(*(arr_name)))//数组长度
void USART0_DMA_Init(void)
{
	rcu_periph_clock_enable(RCU_DMA1);//使能DMA1时钟
	
	dma_single_data_parameter_struct dma_init_struct;
	dma_deinit(DMA1,DMA_CH2);
	dma_init_struct.periph_addr=(uint32_t)&USART_DATA(USART0);//外设地址
	dma_init_struct.periph_inc=DMA_PERIPH_INCREASE_DISABLE;//使用固定模式
	dma_init_struct.memory0_addr=(uint32_t)g_recv_buff;//存储地址
	dma_init_struct.memory_inc=DMA_MEMORY_INCREASE_ENABLE;//增量模式
	dma_init_struct.periph_memory_width=DMA_PERIPH_WIDTH_8BIT;//传输的数据宽度为8位
	dma_init_struct.circular_mode=DMA_CIRCULAR_MODE_DISABLE;//关闭循环模式
	dma_init_struct.direction=DMA_PERIPH_TO_MEMORY;//外设到内存
	dma_init_struct.number=ARRAYNUM(g_recv_buff);//需要传输的数据量
	
	//初始化结构体
	dma_single_data_mode_init(DMA1,DMA_CH2,&dma_init_struct);
}

配置好参数后还需要进行通道使能,翻看用户手册可得知,USART0_RX是通道2,并且前面的二进制100,也就是4

dma_channel_subperipheral_select(DMA1, DMA_CH2, DMA_SUBPERI4);//使能外设通道
dma_channel_enable(DMA1,DMA_CH2);//使能DMA通道

于是就可以进行下一步配置dma中断了

配置DMA中断

就需要使用void dma_interrupt_enable(uint32_t dma_periph, dma_channel_enum channelx, uint32_t source);需要传入三个参数,分别是DMA外设,DMA通道,需要使能的中断资源

DMA_CHXCTL_SDEIE (bit 1) : 单数据模式异常中断使能(不合法配置时触发)

DMA_CHXCTL_TAEIE (bit 2) : 传输访问错误中断使能(总线错误时触发)

DMA_CHXCTL_HTFIE (bit 3) : 半传输完成中断使能(传输一半数据后触发)

DMA_CHXCTL_FTFIE (bit 4) : 全传输完成中断使能(传输完成后触发)

这里我们选择传输完成中断

//DMA中断配置
nvic_irq_enable(DMA1_Channel2_IRQn,2,1);
dma_interrupt_enable(DMA1,DMA_CH2,DMA_CHXCTL_FTFIE);//使能传输完成中断
//串口DMA接收
usart_dma_receive_config(USART0,USART_RECEIVE_DMA_ENABLE);

DMA中断服务函数

void DMA1_Channel2_IRQHandler(void)
{
    if(dma_interrupt_flag_get(DMA1, DMA_CH2, DMA_INT_FLAG_FTF))
     {
         dma_interrupt_flag_clear(DMA1, DMA_CH2, DMA_INT_FLAG_FTF);
     }
}

对此串口的回调函数也因为DMA的加入需要进行更改

void USART0_IRQHandler(void)
{
	if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_IDLE)==SET) // 检测到帧中断
	{
			usart_data_receive(USART0);//必须要读,读出来的值不能要
			g_recv_length=ARRAYNUM(g_recv_buff)-dma_transfer_number_get(DMA1, DMA_CH2);//获取实际接收到的数据长度
			g_recv_buff[g_recv_length]='\0';
			g_recv_complete_flag=SET;  // 数据传输完成
		  //重新配置DMA
  		usart_interrupt_flag_clear(USART0,USART_INT_FLAG_IDLE);
			dma_channel_disable(DMA1, DMA_CH2);     // 失能DMA通道
		  dma_transfer_number_config(DMA1, DMA_CH2, ARRAYNUM(g_recv_buff));
      dma_flag_clear(DMA1, DMA_CH2, DMA_FLAG_FTF);
      dma_channel_enable(DMA1, DMA_CH2);
	}
}

而且在串口初始化函数中需要删去前文的对usart_interrupt_enable(USART0,USART_INT_RBNE);对非空缓冲区的中断使能

为了检测是否正确的接收了数据

我们需要在while中添加

if(SET == g_recv_complete_flag)
{
  g_recv_complete_flag = RESET;
  printf("g_recv_length:%d  ",g_recv_length);
  printf("DMA recv:%s\r\n", g_recv_buff);
  memset(g_recv_buff,0,g_recv_length);  // 清空数组
  g_recv_length = 0;
}

编译下载一气呵成,打开上位机进行测试

显然已经成功接收到了正确的数据,至此就简单的学习到了串口DMA传输

附上全部代码

#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"

#include "stdio.h"
#include "string.h"
#include "stdint.h"
#include "math.h"

void usart_send_data(uint8_t ucch)
{
    usart_data_transmit(USART0, (uint8_t)ucch);
    while(RESET == usart_flag_get(USART0, USART_FLAG_TBE)); // 等待发送数据缓冲区标志置位
}

void usart_send_String(uint8_t *ucstr)
{
      while(ucstr && *ucstr)  // 地址为空或者值为空跳出
      {
        usart_send_data(*ucstr++);
      }
}

int fputc(int ch, FILE *f)
{
    usart_data_transmit(USART0, (uint8_t)ch);
    while(RESET == usart_flag_get(USART0, USART_FLAG_TBE)); // 等待发送数据缓冲区标志置位
    return ch;
}


int fgetc(FILE *f)
{
    while(RESET == usart_flag_get(USART0, USART_FLAG_RBNE)); // 等待接收缓冲区标志置位
    int ch = usart_data_receive(USART0); // 从串口接收数据
    return ch;
}




#define USART0_Port      (GPIOA)
#define USART0_Rx        (GPIO_PIN_10)
#define USART0_Tx        (GPIO_PIN_9)
#define USART_RECEIVE_LENGTH      (4096)//串口缓冲区长度

uint8_t  g_recv_buff[USART_RECEIVE_LENGTH]; // 接收缓冲区
uint16_t g_recv_length = 0; // 接收数据长度
uint8_t  g_recv_complete_flag = 0; // 接收完成标志位

void USART0_Init(uint32_t baudval )
{
  rcu_periph_clock_enable(RCU_GPIOA);//对端口进行时钟使能
	rcu_periph_clock_enable(RCU_USART0);//对串口0进行时钟使能
	
	
	gpio_mode_set(USART0_Port,GPIO_MODE_AF,GPIO_PUPD_PULLUP,USART0_Rx);//设置RX引脚为复用上拉模式
	gpio_af_set(USART0_Port,GPIO_AF_7,USART0_Rx);//对RX引脚进行复用
//	gpio_output_options_set(USART0_Port,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,USART0_Rx);//设置RX为推挽输出
	gpio_mode_set(USART0_Port,GPIO_MODE_AF,GPIO_PUPD_PULLUP,USART0_Tx);//设置TX引脚为复用上拉模式
	gpio_af_set(USART0_Port,GPIO_AF_7,USART0_Tx);//对TX引脚进行复用
	gpio_output_options_set(USART0_Port,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,USART0_Tx);//设置TX为推挽输出
	
	//配置串口
	usart_deinit(USART0);//串口0重置
	usart_baudrate_set(USART0,baudval);//设置波特率
	usart_parity_config(USART0,USART_PM_NONE);//设置为无效验位
	usart_word_length_set(USART0,USART_WL_8BIT);//设置数据位长度8bit
	usart_stop_bit_set(USART0,USART_STB_1BIT);//设置停止位长度1bit
	usart_receive_config(USART0,USART_RECEIVE_ENABLE);//使能串口接收
	usart_transmit_config(USART0,USART_TRANSMIT_ENABLE);//使能串口发送
	
	//串口0使能
	usart_enable(USART0);
	
	//配置中断
	//usart_interrupt_enable(USART0,USART_INT_RBNE);//使能接收非空缓冲区中断
	usart_interrupt_enable(USART0,USART_INT_ERR);//使能总错误中断
	usart_interrupt_enable(USART0,USART_INT_IDLE);//使能空闲检测中断
	
	//配置优先级
	nvic_irq_enable(USART0_IRQn,2,2);
	
	
}
#define  ARRAYNUM(arr_name)   (uint32_t)(sizeof(arr_name) / sizeof(*(arr_name)))//数组长度
void USART0_DMA_Init(void)
{
	rcu_periph_clock_enable(RCU_DMA1);//使能DMA1时钟
	
	dma_single_data_parameter_struct dma_init_struct;
	dma_deinit(DMA1,DMA_CH2);
	dma_init_struct.periph_addr=(uint32_t)&USART_DATA(USART0);//外设地址
	dma_init_struct.periph_inc=DMA_PERIPH_INCREASE_DISABLE;//使用固定模式
	dma_init_struct.memory0_addr=(uint32_t)g_recv_buff;//存储地址
	dma_init_struct.memory_inc=DMA_MEMORY_INCREASE_ENABLE;//增量模式
	dma_init_struct.periph_memory_width=DMA_PERIPH_WIDTH_8BIT;//传输的数据宽度为8位
	dma_init_struct.circular_mode=DMA_CIRCULAR_MODE_DISABLE;//关闭循环模式
	dma_init_struct.direction=DMA_PERIPH_TO_MEMORY;//外设到内存
	dma_init_struct.number=ARRAYNUM(g_recv_buff);//需要传输的数据量
	dma_init_struct.priority=DMA_PRIORITY_ULTRA_HIGH;
	
	//初始化结构体
	dma_single_data_mode_init(DMA1,DMA_CH2,&dma_init_struct);
	
	dma_channel_subperipheral_select(DMA1, DMA_CH2, DMA_SUBPERI4);//使能外设通道
	dma_channel_enable(DMA1,DMA_CH2);//使能DMA通道
	
	//DMA中断配置
	nvic_irq_enable(DMA1_Channel2_IRQn,2,1);
	dma_interrupt_enable(DMA1,DMA_CH2,DMA_CHXCTL_FTFIE);//使能传输完成中断
	
	//串口DMA接收
	usart_dma_receive_config(USART0,USART_RECEIVE_DMA_ENABLE);
}
void DMA1_Channel2_IRQHandler(void)
{
    if(dma_interrupt_flag_get(DMA1, DMA_CH2, DMA_INT_FLAG_FTF))
     {
         dma_interrupt_flag_clear(DMA1, DMA_CH2, DMA_INT_FLAG_FTF);
     }
}

void USART0_IRQHandler(void)
{
	if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_IDLE)==SET) // 检测到帧中断
	{
			usart_data_receive(USART0);//必须要读,读出来的值不能要
			g_recv_length=ARRAYNUM(g_recv_buff)-dma_transfer_number_get(DMA1, DMA_CH2);//获取实际接收到的数据长度
			g_recv_buff[g_recv_length]='\0';
			g_recv_complete_flag=SET;  // 数据传输完成
		  //重新配置DMA
  		usart_interrupt_flag_clear(USART0,USART_INT_FLAG_IDLE);
			dma_channel_disable(DMA1, DMA_CH2);     // 失能DMA通道
		  dma_transfer_number_config(DMA1, DMA_CH2, ARRAYNUM(g_recv_buff));
      dma_flag_clear(DMA1, DMA_CH2, DMA_FLAG_FTF);
      dma_channel_enable(DMA1, DMA_CH2);
	}
}


int main(void)
{

    systick_config();
	  USART0_Init(115200);
	  USART0_DMA_Init();
    while(1) {
		if(SET == g_recv_complete_flag)
		 {
				 g_recv_complete_flag = RESET;
				printf("g_recv_length:%d  ",g_recv_length);
				printf("DMA recv:%s\r\n", g_recv_buff);
				memset(g_recv_buff,0,g_recv_length);  // 清空数组
				g_recv_length = 0;
		 }
			
    }	
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐