一、DMA原理介绍
1.1DMA基础知识
1.1.1数据传输方式
一般情况下实现存储器和外设之间的数据传输,有三种常用的方法:轮询法(polling),中断法(interrupt)以及DMA。
轮询法(polling): 在主循环中,CPU不断检查外设的相关标志位,来判断其是否需要进行数据的传输,如果需要,则CPU将数据在外设和内存之间搬运,实现数据传输。当数据传输服务请求频繁或者传输的数据量很大时,会影响其他任务的实时性。
中断法(interrupt): 当外设需要传输数据时,会触发中断,CPU会暂停正在处理的任务,转而去执行中断服务函数,接着处理外设的数据传输任务。CPU无需反复检查外设的标志位,中断机制会指示CPU何时去处理外设数据,但是依然需要CPU去完成数据搬运和传输过程。当外设数据传输服务不频繁且数据量不大时,中断法也是不错的选择。当中断连续不断且频繁发生时,中断法变得不再高效,因为在恢复主流程的执行和中断相应的上下文切换会占用大量的CPU时间。
DMA(Direct Memory Access,直接存储器访问): DMA控制器是单片机中的硬件单元,它在存储器和外设之间有专用的通道,允许外设和存储器之间高效传输数据,且传输过程无需CPU参与。
综上可见DMA是一种高效的数据传输方式。
1.1.2什么是DMA
DMA(Direct Memory Access)控制器提供了一种硬件的方式在外设和存储器之间或者存储器和存储器之间传输数据,而无需CPU的介入,避免了CPU多次进入中断进行大规模的数据拷贝,最终提高整体的系统性能。
DMA是一种能够在无需CPU参与的情况下,将数据块在内存和外设之间高效传输的硬件机制。实现这种功能的集成电路单元叫做DMA Controller,即DMA控制器。
DMA控制器在没有CPU参与的情况下从一个地址向另一个地址传输数据,它支持多种数据宽度,突发类型,地址生成算法,优先级和传输模式,可以灵活的配置以满足应用的需求。
1.1.3 GD32 DMA介绍
GD32F450ZGT6有两个DMA控制器(DMA0,DMA1),每个DMA控制器包含了两个AHB总线接口和8个4字深度的FIFO,使DMA可以高效的传输数据。每个DMA控制器有8个通道,一共是16个通道,每个通道可以被分配给一个或多个特定的外设进行数据传输。两个内置的总线仲裁器用来处理DMA请求的优先级问题。DMA控制器支持8位,16位和32位的数据宽度。
关于DMA的主要特性在用户手册的第192页,如图所示。
1.1.4 DMA传输方式
GD32F450ZGT6单片机的DMA控制器的两个AHB主机接口分别对应存储器和外设的数据访问。
DMA支持三种传输模式,如下:
l 外设到存储器:通过AHB外设主机接口从外设读取数据,通过AHB存储器主机接口向存储器写入数据。比如串口的DMA接收,就是从外设到存储器。
l 存储器到外设:通过AHB存储器主机接口从存储器读取数据,通过AHB外设主机接口向外设写入数据。比如串口的DMA发送,就是存储器到外设。
l 存储器到存储器:通过AHB外设主机接口从存储器读取数据,通过AHB存储器主机接口向存储器写入数据。
存储区和外设都可以配置为源端和目的端。
用户手册202页。
1.1.5 DMA外设请求映射
每个DMA控制器有8个通道,每个通道有多个外设请求。DMA0和DMA1的外设请求映射分别如图所示。用户手册195页。
1.1.6仲裁
每个DMA控制器有两个分别对应于外设和存储器的仲裁器。当DMA控制器在同一时间接收到多个外设请求时,仲裁器将根据外设请求的优先级来决定响应哪一个外设请求。
优先级规则如下:
软件优先级:分为4级,低,中,高和超高。可以通过寄存器的DMA_CHxCTL的PRIO位域配置。
硬件优先级:当通道具有相同的软件优先级时,编号低的通道优先级高。
1.1.7 FIFO
DMA控制器的每个通道都有一个4字深度的FIFO用于缓冲数据,从源地址读取的数据会暂时保存在FIFO中,再传输到目的地址。根据FIFO的配置,DMA控制器支持两种数据处理模式:单数据传输模式和多数据传输模式。在存储器到存储器模式下,DMA控制器仅支持多数据传输模式。
1.1.8地址生成算法
存储器和外设都独立的支持两种地址生成算法:固定模式和增量模式。
在固定模式中,地址一直固定为初始化的基地址。
在增量模式中,下一次传输数据的地址是当前地址加1(或2,4),这个值取决于数据传输宽度。
外设地址就是外设寄存器映射的地址,而存储器地址就是内存缓冲区数组的起始地址。由于外设寄存器地址是固定的,所以外设地址生成算法应该选择固定地址模式。而存储器对应的缓冲数组是包含多个数据元素的矢量,所以存储器的地址生成算法应该选择增量地址模式。
1.1.9循环模式
循环模式用来处理连续的外设请求。在循环模式中,当每次DMA传输完成后,CNT值会被重新载入,且传输完成标志位会被置1。DMA会一直响应外设的请求,直到出现传输错误或者通道使能位被清0。
单次传输模式:当传输结束时,触发DMA中断,在中断程序中首先失能DMA通道,然后修改该通道的传输数据量,最后重新使能DMA通道,注意只有失能的DMA通道才能成功修改传输数据量。
循环传输模式:当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。
1.1.10中断
每个DMA通道都有专有的中断,包括5个中断事件,传输完成中断,半传输完成中断,传输错误中断,单数据传输模式异常中断,FIFO错误和异常中断。任何一个中断事件都可以引发DMA中断。
这5个事件可以分为3种类型:
l 标志:传输完成和半传输完成
l 异常:单数据传输模式异常和FIFO异常
l 错误:传输错误和FIFO错误
发生异常事件时,正在进行的DMA传输不会被停止,仍将继续传输。发生错误事件时,正在进行的DMA传输会被停止。
1.2实验原理
DMA支持很多个外设,这里就以串口DMA接收进行介绍。
一般进行串口接收也有以下几种方式:
l 通过轮询接收数据
l 通过中断接收数据
l 通过DMA接收数据
一般使用情况可能是中断方式用的比较多,但是使用中断在数据量不大的时候使用效率是高的,但数据量一旦很大,这个效率就很低了。数据量很大的传输推荐使用DMA进行传输。
1.3串口DMA接收数据思路
在串口检测到有数据输入的时候,直接让DMA往准备好的存储器中搬运接收到的数据,在这个过程中,CPU还是在处理自己的事,在一帧数据传输完成后,通过空闲中断告诉主任务一帧数据已经传输完毕,主任务再对接收到的数据进行响应处理。整个过程中,程序代码只会触发一次中断,就是DMA搬移完数据后通知CPU的那一次中断,这种方法无论一帧数据的数据量有多大,都只会触发一次中断,且在传输过程中不占用CPU的时间,大大提升效率。
二、串口DMA接收和中断接收
2.1.串口中断接收配置流程
一般串口中断接收都需要有以下几个步骤。
l 开启时钟(包括串口时钟和GPIO时钟)
l 配置GPIO复用模式
l 配置GPIO的模式
l 配置GPIO的输出
l 配置串口(配置一些参数)
l 使能串口(串口使能、发送使能和接收使能)
l 中断配置(串口接收中断和中断优先级)
关于串口的一些常规配置步骤在串口打印信息章节中已经讲过了,这里就不再赘述。下面从中断接收部分进行介绍。串口接收配置就是在前面串口配置的基础上加上串口接收功能和中断配置。
2.1.1使能串口接收
在串口打印信息章节,我们提了一下如果要使用串口接收功能的话,需要调用usart_receive_config这个函数去使能串口接收。固件库手册728页
代码如下:
usart_receive_config(BSP_USART, USART_RECEIVE_ENABLE); // 使能串口接收
2.1.2中断配置
接收数据是放在中断中进行接收的,所以要使能串口的接收中断,既然要操作中断那肯定要配置中断的优先级,还是按照原来的中断分组,2个抢占优先级2个响应优先级。这里配置串口的优先级可以配置为抢占优先级为2,响应优先级为2。配置好中断后,要使能什么情况下进入中断,比如是刚接收数据就进入中断还是接收完成之后再进入中断。这里关于串口中断选项如图所示:
l USART_INT_PERR:优先级错误中断
l USART_INT_TBE:发送缓冲区空中断。
l USART_INT_TC:发送完成中断。
l USART_INT_RBNE:接收缓冲区不为空中断和溢出错误中断。如果开启了这个中断,每当接收到一个字符,就会触发这个中断。
l USART_INT_IDLE:空闲检测中断。如果开启了这个中断,将会在一帧数据传输完成之后触发中断,一般用来判断一帧数据是否传输完成。
l USART_INT_LBD:LIN断开信号检测中断
l USART_INT_CTS:CTS中断
l USART_INT_ERR:错误中断
l USART_INT_EB:块结束事件中断
l USART_INT_RT:接收超时事件中断
从上面可以看到串口中断的标志位有很多,但是串口中断接收需要配置的中断一般有两个,分别是USART_INT_RBNE和USART_INT_IDLE,用USART_INT_RBNE中断来接收每一个字符的数据,用USART_INT_IDLE中断来判断一帧数据是否传输完成。
串口中断的宏定义如下:
#define BSP_USART_IRQ USART0_IRQn
串口中断配置如下:
usart_interrupt_enable(BSP_USART, USART_INT_RBNE); // 读数据缓冲区非空中断和过载错误中断 usart_interrupt_enable(BSP_USART, USART_INT_IDLE); // DLE线检测中断 /* 配置中断优先级 */ nvic_irq_enable(BSP_USART_IRQ, 2, 2); // 配置中断优先级
2.1.3串口变量定义
单片机要实现接收数据,也就是说要通过上位机给单片机发送数据,那单片机需要接收数据然后存储起来,所以需要定义一个数组g_recv_buff去存储接收到的数据,那这个数组定义多大呢,这要根据数据量去判断,为了支持更好的修改,定义一个宏USART_RECEIVE_LENGTH来表示数组的大小。为了方便处理数据,可以定义一个变量g_recv_length去记录串口实际接收到的数据的个数。怎么去判断数据传输完成呢?也就是说什么时候开始处理数据呢?前面说了USART_INT_IDLE可以用来判断一帧数据是否传输完成,那可以定义一个变量g_recv_complete_flag在USART_INT_IDLE中调用,当g_recv_complete_flag置1的时候说明数据传输完成了,然后就可以去处理数据了。
宏定义如下:
/* 串口缓冲区的数据长度 */ #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; // 接收完成标志位
2.1.4串口接收中断服务函数
串口接收数据要到中断中去处理,前面串口中断配置的时候,我们打开了USART_INT_RBNE和USART_INT_IDLE这两个中断,那在中断服务函数里面也需要对这两个中断分别进行判断。
FlagStatus usart_interrupt_flag_get(uint32_t usart_periph, usart_interrupt_flag_enum int_flag); 这个函数获取串口中断的标志位状态。有两个参数,第一个参数就是要获取的串口外设,第二个参数就是要获取的串口中断的标志位。固件库手册756页
关于串口中断的标志如图所示。
从上图可以看到,串口中断标志和之前的串口中断非常相似,根据命令可以很方便的找到对应的关系,USART_INT_RBNE和USART_INT_IDLE分别对应于USART_INT_FLAG_RBNE和USART_INT_FLAG_IDLE。在检测到USART_INT_FLAG_RBNE中断标志被置1之后,就说明当前缓冲区不为空了,有数据到来了,我们要做的操作就是把当前数据读出来然后保存到到接收数组中,然后对数据长度进行加一,等待下一次数据存储。
uint16_t usart_data_receive(uint32_t usart_periph);这个函数可以接收数据。有一个参数,就是要读取的串口外设。需要注意的是这个函数读取数据的时候会自动将标志位请零,所以我们就不用手动清除标志位了。固件库手册734页
在接收数据的过程中可以判断USART_INT_FLAG_IDLE这个标志是否置1,如果这个标志位置1说明一帧数据传输完成,然后将g_recv_complete_flag变量置1,说明可以进行数据处理。需要注意的是,在这个中断满足的时候,需要执行usart_data_receive(BSP_USART);去清除中断标志,不然数据会出现混乱。
串口中断服务函数的宏定义如下:
#define BSP_USART_IRQHandler USART0_IRQHandler 串口中断服务函数如下: void BSP_USART_IRQHandler(void) { if(usart_interrupt_flag_get(BSP_USART,USART_INT_FLAG_RBNE) != RESET) // 接收缓冲区不为空 { g_recv_buff[g_recv_length++] = usart_data_receive(BSP_USART); // 把接收到的数据放到缓冲区中 } if(usart_interrupt_flag_get(BSP_USART,USART_INT_FLAG_IDLE) == SET) // 检测到帧中断 { usart_data_receive(BSP_USART); // 必须要读,读出来的值不能要 g_recv_buff[g_recv_length] = '\0'; g_recv_complete_flag = SET;// 接收完成 } }
2.1.5串口中断接收数据处理
为了更方便去观察数据,可以将每次接收的数据打印到串口上显示,看看是不是和我们发送的数据一致。在main函数中while下添加以下代码:
/* 等待数据传输完成 INTERRUPT */ if(g_recv_complete_flag) // 数据接收完成 { g_recv_complete_flag = RESET; // 等待下次接收 printf("g_recv_length:%d ",g_recv_length); printf("Interrupt recv:%s\r\n",g_recv_buff); // 打印接收的数据 memset(g_recv_buff,0,g_recv_length); // 清空数组 g_recv_length = 0; }
每次处理完数据后都要记得把对应的标志位和数组都清零。
到此,编译下载到开发板就可以使用串口中断接收功能了,关于实验现象后面会介绍。
2.2.串口DMA接收配置流程
一般使用DMA功能,都需要有以下几个步骤。
l 开启时钟
l 配置参数结构体
l 使能通道外设
l 使能DMA中断
l 使能外设DMA(串口)
l DMA中断服务函数
2.2.1开启时钟
老规矩,使用外设之前要先开启时钟,GD32F450ZGT6有两个DMA,这里要看使用哪一个就开启哪一个的DMA时钟。我们要使用的是串口0的接收功能,查找用户手册的第196可知,USART0_RX对应于DMA1的通道2和通道5。
这里选择DMA1的通道2进行配置。
宏定义如下:
#define BSP_DMA_RCU RCU_DMA1 // DMA时钟 #define BSP_DMA DMA1 // DMA #define BSP_DMA_CH DMA_CH2 // DMA通道
时钟使能如下:
/* 开启时钟 */ rcu_periph_clock_enable(BSP_DMA_RCU);
2.2.2配置参数结构体
首先要先了解DMA传输有单一传输和增量突发传输两种,单一传输一次可以传输8位、16位和32位,增量突发传输可配置为4个节拍、8个节拍和16个节拍进行传输,每次传输量是单一传输的4倍,8倍和16倍。这里以单一传输进行配置。
单一传输参数结构体定义如图所示。
l periph_addr:外设基地址
l periph_inc:外设地址生成算法模式
l memory0_addr:存储器基地址
l memory_inc:存储器地址生成算法模式
l periph_memory_width:外设和存储器数据传输宽度
l circular_mode:DMA循环模式
l direction:DMA通道数据传输方向
l number:DMA通道数据传输数量
l priority:DMA通道传输软件优先级
下面分析一下这个结构体该如何配置。
direction:DMA通道数据传输方向。关于direction有几个可选选项如图所示。
l DMA_PERIPH_TO_MEMORY:外设到内存
l DMA_MEMORY_TO_PERIPH:内存到外设
l DMA_MEMORY_TO_MEMORY:内存到内存
首先知道我们操作的是串口的接收DMA功能,相当于是从串口将数据搬运出来放到定义的数组中,串口就是外设,定义的数组就是存储器,那自然这个传输方向就是外设到内存,选择DMA_PERIPH_TO_MEMORY。
periph_addr:外设基地址。外设就是串口,外设基地址就是串口基地址,选择配置为(uint32_t)&USART_DATA(BSP_USART)。
periph_inc:外设地址生成算法模式。关于periph_inc有几个可选选项如图所示。
l DMA_PERIPH_INCREASE_ENABLE:增量模式
l DMA_PERIPH_INCREASE_DISABLE:固定模式
l DMA_PERIPH_INCREASE_FIX:
外设的地址也就是串口的地址是不变的,所以配置为固定模式,也就是选择DMA_PERIPH_INCREASE_DISABLE。
memory0_addr:存储器基地址。存储器基地址也就是我们定义的数组的地址,也就是配置为(uint32_t)g_recv_buff。
memory_inc:存储器地址生成算法模式。存储器要存储数据,每次传输之后都需要存放在不同的地址里,要配置为增量模式,也就是选择DMA_PERIPH_INCREASE_ENABLE。
periph_memory_width:外设和存储器数据传输宽度。在单一模式中,外设传输数据宽度和存储器传输数据宽度一致,可选选项如图所示。
DMA_PERIPH_WIDTH_8BIT:8位数据宽度
DMA_PERIPH_WIDTH_16BIT:16位数据宽度
DMA_PERIPH_WIDTH_32BIT:32位数据宽度
因为我们串口传输一次是一个字节也就是8位,所以配置为DMA_PERIPH_WIDTH_8BIT。
number:DMA通道数据传输数量。这个就是通过DMA传输的数据量,当这个传输量为0的时候,说明当前传输完成,会进入中断。如果数据量超过这个配置值,超出的部分不会接收到。可根据我们的需求去配置。
priority:DMA通道传输软件优先级。这个是配置DMA的软件优先级,可选选项如图所示。
l DMA_PRIORITY_LOW:低优先级
l DMA_PRIORITY_MEDIUM:中优先级
l DMA_PRIORITY_HIGH:高优先级
l DMA_PRIORITY_ULTRA_HIGH:超高优先级
可根据我们需求去配置。
circular_mode:DMA循环模式。可选选项如图所示。
这里关闭循环模式,配置为DMA_CIRCULAR_MODE_DISABLE。
在配置完结构体参数之后,需要去初始化结构体,可以调用下面这个函数。void dma_single_data_mode_init(uint32_t dma_periph,dma_channel_enum channelx, dma_single_data_parameter_struct* init_struct); 这个函数是初始化单一传输模式,有三个参数,第一个参数是要使用的DMA外设,第二个参数是要使用的DMA通道,第三个参数是单一传输参数配置结构体。
固件库手册179页
固件库手册178页
定义单一传输参数结构体如下:
dma_single_data_parameter_struct dma_init_struct;
关于配置结构体和初始化代码如下:
/* 初始化DMA通道 */ dma_deinit(BSP_DMA, BSP_DMA_CH); /* 配置DMA初始化参数 */ dma_init_struct.direction = DMA_PERIPH_TO_MEMORY; // 外设到内存 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; // 一次传输长度8bit dma_init_struct.number = ARRAYNUM(g_recv_buff); // 要传输的数据量 dma_init_struct.periph_addr = (uint32_t)&USART_DATA(BSP_USART); // 外设地址 dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 不使用增量模式,为固定模式 dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; // 超高优先级 /* 初始化DMA结构体 */ dma_single_data_mode_init(BSP_DMA, BSP_DMA_CH, &dma_init_struct);
这里需要解释一下ARRAYNUM这个宏定义,这个宏定义是求传进来的数组的长度的。
宏定义如下:
#define ARRAYNUM(arr_name) (uint32_t)(sizeof(arr_name) / sizeof(*(arr_name)))
2.2.3使能通道外设
在配置好DMA参数之后,还需要使能通道外设才能正常使用通道。void dma_channel_subperipheral_select(uint32_t dma_periph,dma_channel_enum channelx, dma_subperipheral_enum sub_periph);这个函数是DMA通道外设选择。有三个参数,第一个参数就是要使用的DMA,第二个参数就是要使用的通道,第三个参数就是要开启的通道外设。
关于外设通道如图所示。
我们使用的是USART0_RX的DMA功能,其配置如图所示。
可见最前面的通道数值是100(二进制),也就是4(十进制),对应于枚举定义就是DMA_SUBPERI4。
使能通道外设代码如下:
/* 使能通道外设 */ dma_channel_subperipheral_select(BSP_DMA, BSP_DMA_CH, DMA_SUBPERI4);
使能通道外设之后还需要使能对应的通道。void dma_channel_enable(uint32_t dma_periph, dma_channel_enum channelx);这个函数是使能DMA通道。有两个参数,第一个参数是要使用的DMA外设,第二个参数是要使用的DMA通道。
固件库手册193页
配置代码如下:
/* 使能DMA通道 */ dma_channel_enable(BSP_DMA, BSP_DMA_CH);
2.2.4使能DMA中断
如果我们需要用到DMA的中断功能的话就需要配置DMA的中断。
DMA中断和DMA中断服务函数宏定义如下:
#define BSP_DMA_CH_IRQ DMA1_Channel2_IRQn // DMA中断 #define BSP_DMA_CH_IRQHandler DMA1_Channel2_IRQHandler // DMA中断服务函数
使能中断,啥也不说,先配置中断优先级。之后再使能中断,配置进入中断的触发条件。void dma_interrupt_enable(uint32_t dma_periph, dma_channel_enum channelx, uint32_t source);这个函数是使能DMA中断。有三个参数,第一个参数是要使用的DMA外设,第二个参数是要使用的DMA通道,第三个参数是要使能的中断资源。
固件库手册205页
关于第三个参数的可选选项如图所示。
DMA_CHXCTL_SDEIE:通道单数据传输模式异常中断
DMA_CHXCTL_TAEIE:通道发生传输错误中断
DMA_CHXCTL_HTFIE:通道半传输完成中断
DMA_CHXCTL_FTFIE:通道传输完成中断
可以根据自己的应用需求去选择,我这里选择使能传输完成中断。
/* 使能DMA通道中断 */ nvic_irq_enable(BSP_DMA_CH_IRQ,2,1); /* 使能DMA中断 */ dma_interrupt_enable(BSP_DMA,BSP_DMA_CH,DMA_CHXCTL_FTFIE); // 使能传输完成中断
2.2.5使能外设DMA(串口)
在前面我们单独配置串口和DMA,要使数据从DMA和串口之间进行传输,还需要配置串口的DMA接收请求使能。如果是使用DMA的发送功能,那就是配置串口的DMA的发送请求。void usart_dma_receive_config(uint32_t usart_periph, uint32_t dmacmd);这个函数是串口DMA接收配置。有两个参数,第一个参数是要配置的串口外设,第二个参数是要使能还是失能。
关于DMA的第三个参数定义如图2-5-1所示。
有两个参数,分别对应使能和失能。
使能串口DMA接收代码如下:
/* 使能串口DMA接收 */ usart_dma_receive_config(BSP_USART, USART_DENR_ENABLE);
2.2.6DMA中断服务函数
DMA中断服务函数和串口中断服务函数一样,也是通过判断对应的标志位来判断进入中断。FlagStatus dma_interrupt_flag_get(uint32_t dma_periph,dma_channel_enum channelx, uint32_t interrupt);这个函数是DMA中断标志位获取。有三个参数,第一个参数就是DMA外设,第二个参数就是DMA通道,第三个要获取的中断标志位。第三个参数的定义如图所示。
固件库手册203页
在进入中断之后,记得要清除中断,有对应的中断标志位清除函数dma_interrupt_flag_clear,参数和DMA中断标志位获取函数一样。
固件库手册204页
这里配置DMA传输完成中断对应的代码如下:
void BSP_DMA_CH_IRQHandler(void){ /* 传输完成中断 */ if(dma_interrupt_flag_get(BSP_DMA, BSP_DMA_CH, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(BSP_DMA, BSP_DMA_CH, DMA_INT_FLAG_FTF); // g_transfer_complete = SET; // 数据传输完成 } }
2.2.7串口DMA接收数据处理
在前面的DMA配置过程中我们使能了数据传输量为一整个数组的长度,实际传输过程中可能并不能够传输足够的数据,所以需要在串口空闲中断中对数据进行处理。实际的DMA传输的数据长度等于数组的长度减去当前DMA还剩余的数据量。然后标记一次数据传输完成。
uint32_t dma_transfer_number_get(uint32_t dma_periph, dma_channel_enum channelx);这个函数是获取DMA剩余的还没有传输的数据量。有两个参数,第一个参数是DMA外设,第二个参数是DMA通道。
固件库手册184页
在处理完数据之后,如果还要继续下次传输,还需要再重新配置一下DMA。
具体的串口中断服务函数代码如下:
if(usart_interrupt_flag_get(BSP_USART,USART_INT_FLAG_IDLE) == SET) // 检测到帧中断 { usart_data_receive(BSP_USART); // 必须要读,读出来的值不能要 /* 处理DMA接收到的数据 */ g_recv_length = ARRAYNUM(g_recv_buff) - dma_transfer_number_get(BSP_DMA, BSP_DMA_CH); // 获取实际接收到的数据长度 g_recv_buff[g_recv_length] = '\0'; g_transfer_complete = SET; // 数据传输完成 /* 重新设置DMA传输 */ dma_channel_disable(BSP_DMA, BSP_DMA_CH); // 失能DMA通道 dma_config(); // 重新配置DMA进行传输 }
之后我们还要在main函数中的while下打印接收到的数据供我们使用。
/* 等待数据传输完成 DMA*/ if(SET == g_transfer_complete) { g_transfer_complete = 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进行串口的接收了。
2.3.条件编译
前面我们分别操作了串口中断接收数据和串口DMA接收数据,但是分开编写的,也就是说是两个工程,不利于我们移植和使用。下面给大家介绍一种条件编译的处理手段。比如我们要切换串口中断接收和串口DMA接收,只需要定义一个宏定义,当为1的时候是DMA接收,当为0的时候是中断接收。
宏定义如下:
/* 是否使用DMA 0:串口中断接收 1:DMA接收 */ #define USB_USART_DMA 1
然后我们将DMA处理的代码和串口中断处理的代码分别用#if进行定义,如所示。
从图可以看到当USB_USART_DMA为1的时候,显示的就是要编译的,全部都是DMA的配置,反而看#else下面的串口中断的语句将会不显示,也就是不会编译到我们的代码中。其它部分也是如此,具体操作看资源包中的代码例程。
2.4.实验现象
关于这一章节的代码,在资源包/04软件资料/代码例程/里面的012串口中断DMA接收二合一。
烧写我们的代码之后,在串口助手中发送对应的数据,将会在串口助手中显示出来。
中断接收和DMA接收请修改USB_USART_DMA宏定义的值。
中断接收测试结果如下图所示。
DMA接收测试结果如下图所示。
标签:DMA,13,USART,传输,中断,单片机,串口,外设 From: https://www.cnblogs.com/steven913/p/16884840.html