跳到主要内容

【二十】串口DMA接收和中断接收

1.1.串口中断接收配置流程

一般串口中断接收都需要有以下几个步骤

  • 开启时钟(包括串口时钟和GPIO时钟)
  • 配置GPIO复用模式
  • 配置GPIO的模式
  • 配置GPIO的输出
  • 配置串口(配置一些参数)
  • 使能串口(串口使能、发送使能和接收使能)
  • 中断配置(串口接收中断和中断优先级)

关于串口的一些常规配置步骤在串口打印信息章节中已经讲过了,这里就不再赘述。下面从中断接收部分进行介绍。串口接收配置就是在前面串口配置的基础上加上串口接收功能和中断配置

1.1.1.使能串口接收

在串口打印信息章节,我们提了一下如果要使用串口接收功能的话,需要调用usart_receive_config这个函数去使能串口接收

usart_receive_config(BSP_USART, USART_RECEIVE_ENABLE); // 使能串口接收

1.1.2.中断配置

接收数据是放在中断中进行接收的,所以要使能串口的接收中断,既然要操作中断那肯定要配置中断的优先级,还是按照原来的中断分组,2个抢占优先级2个响应优先级。 这里配置串口的优先级可以配置为抢占优先级为2,响应优先级为2。配置好中断后,要使能什么情况下进入中断,比如是刚接收数据就进入中断还是接收完成之后再进入中断。 这里关于串口中断选项如图1-2-1所示:

1
  • USART_INT_PERR:优先级错误中断
  • USART_INT_TBE:发送缓冲区空中断。
  • USART_INT_TC:发送完成中断。
  • USART_INT_RBNE:接收缓冲区不为空中断和溢出错误中断。如果开启了这个中断,每当接收到一个字符,就会触发这个中断。
  • USART_INT_IDLE:空闲检测中断。如果开启了这个中断,将会在一帧数据传输完成之后触发中断,一般用来判断一帧数据是否传输完成。
  • USART_INT_LBD:LIN断开信号检测中断
  • USART_INT_CTS:CTS中断
  • USART_INT_ERR:错误中断
  • USART_INT_EB:块结束事件中断
  • 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); // 配置中断优先级

1.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; // 接收完成标志位

1.1.4.串口接收中断服务函数

串口接收数据要到中断中去处理,前面串口中断配置的时候,我们打开了USART_INT_RBNE和USART_INT_IDLE这两个中断,那在中断服务函数里面也需要对这两个中断分别进行判断

FlagStatus usart_interrupt_flag_get(uint32_t usart_periph, usart_interrupt_flag_enum int_flag);

这个函数获取串口中断的标志位状态。有两个参数,第一个参数就是要获取的串口外设,第二个参数就是要获取的串口中断的标志位。关于串口中断的标志如图1-4-1所示

1

从图1-4-1可以看到,串口中断标志和之前的串口中断非常相似,根据命令可以很方便的找到对应的关系, 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);

这个函数可以接收数据。有一个参数,就是要读取的串口外设

需要注意的是这个函数读取数据的时候会自动将标志位请零,所以我们就不用手动清除标志位了

在接收数据的过程中可以判断USART_INT_FLAG_IDLE这个标志是否置1,如果这个标志位置1说明一帧数据传输完成,然后将g_recv_complete_flag变量置1, 说明可以进行数据处理。需要注意的是,在这个中断满足的时候,需要执行usart_data_receive(BSP_USART);去清除中断标志,不然数据会出现混乱

串口中断服务函数的宏定义如下:

#define BSP_USART_IRQHandler  USART0_IRQHandler

1.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;
}

每次处理完数据后都要记得把对应的标志位和数组都清零

到此,编译下载到开发板就可以使用串口中断接收功能了,关于实验现象后面会介绍

1.2.串口DMA接收配置流程

一般使用DMA功能,都需要有以下几个步骤

  • 开启时钟
  • 配置参数结构体
  • 使能通道外设
  • 使能DMA中断
  • 使能外设DMA(串口)
  • DMA中断服务函数

1.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);

1.2.2.配置参数结构体

首先要先了解DMA传输有单一传输和增量突发传输两种,单一传输一次可以传输8位、16位和32位,增量突发传输可配置为4个节拍、8个节拍和16个节拍进行传输, 每次传输量是单一传输的4倍,8倍和16倍。这里以单一传输进行配置

单一传输参数结构体定义如图2-2-1所示

1
  • periph_addr:外设基地址
  • periph_inc:外设地址生成算法模式
  • memory0_addr:存储器基地址
  • memory_inc:存储器地址生成算法模式
  • periph_memory_width:外设和存储器数据传输宽度
  • circular_mode:DMA循环模式
  • direction:DMA通道数据传输方向
  • number:DMA通道数据传输数量
  • priority:DMA通道传输软件优先级

下面分析一下这个结构体该如何配置

direction:DMA通道数据传输方向。关于direction有几个可选选项如图2-2-2所示

1
  • DMA_PERIPH_TO_MEMORY:外设到内存
  • DMA_MEMORY_TO_PERIPH:内存到外设
  • DMA_MEMORY_TO_MEMORY:内存到内存

首先知道我们操作的是串口的接收DMA功能,相当于是从串口将数据搬运出来放到定义的数组中,串口就是外设, 定义的数组就是存储器,那自然这个传输方向就是外设到内存,选择DMA_PERIPH_TO_MEMORY

periph_addr:外设基地址。外设就是串口,外设基地址就是串口基地址,选择配置为 (uint32_t)&USART_DATA(BSP_USART)

periph_inc:外设地址生成算法模式。关于periph_inc有几个可选选项如图2-2-3所示

1
  • DMA_PERIPH_INCREASE_ENABLE:增量模式
  • DMA_PERIPH_INCREASE_DISABLE:固定模式
  • DMA_PERIPH_INCREASE_FIX

外设的地址也就是串口的地址是不变的,所以配置为固定模式,也就是选择 DMA_PERIPH_INCREASE_DISABLE

memory0_addr:存储器基地址。存储器基地址也就是我们定义的数组的地址,也就是配置为 (uint32_t)g_recv_buff

memory_inc:存储器地址生成算法模式。存储器要存储数据,每次传输之后都需要存放在不同的地址里,要配置为增量模式

1

也就是选择 DMA_MEMORY_INCREASE_ENABLE

periph_memory_width:外设和存储器数据传输宽度。在单一模式中,外设传输数据宽度和存储器传输数据宽度一致,可选选项如图2-2-4所示

1
  • 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的软件优先级,可选选项如图2-2-5所示

1
  • DMA_PRIORITY_LOW:低优先级
  • DMA_PRIORITY_MEDIUM:中优先级
  • DMA_PRIORITY_HIGH:高优先级
  • DMA_PRIORITY_ULTRA_HIGH:超高优先级

可根据我们需求去配置

circular_mode:DMA循环模式。可选选项如图2-2-6所示

1

这里关闭循环模式,配置为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通道,第三个参数是单一传输参数配置结构体

定义单一传输参数结构体如下:

    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)))

1.2.3.使能通道外设

在配置好DMA参数之后,还需要使能通道外设才能正常使用通道

void dma_channel_subperipheral_select(uint32_t dma_periph,

dma_channel_enum channelx, dma_subperipheral_enum sub_periph);

这个函数是DMA通道外设选择。有三个参数,第一个参数就是要使用的DMA,第二个参数就是要使用的通道,第三个参数就是要开启的通道外设。关于外设通道如图2-3-1所示

1

我们使用的是USART0_RX的DMA功能,其配置如图2-3-2所示

1

可见最前面的数值是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通道

配置代码如下:

/* 使能DMA通道 */
dma_channel_enable(BSP_DMA, BSP_DMA_CH);

1.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通道,第三个参数是要使能的中断资源

关于第三个参数的可选选项如图2-4-1所示

1
  • 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); // 使能传输完成中断

1.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所示

1

有两个参数,分别对应使能和失能

使能串口DMA接收代码如下:

/* 使能串口DMA接收 */
usart_dma_receive_config(BSP_USART, USART_DENR_ENABLE);

1.2.6.DMA中断服务函数

DMA中断服务函数和串口中断服务函数一样,也是通过判断对应的标志位来判断进入中断

FlagStatus dma_interrupt_flag_get(uint32_t dma_periph,

dma_channel_enum channelx, uint32_t interrupt);

这个函数是DMA中断标志位获取。有三个参数,第一个参数就是DMA外设,第二个参数就是DMA通道,第三个要获取的中断标志位。第三个参数的定义如图2-6-1所示

1

在进入中断之后,记得要清除中断,有对应的中断标志位清除函数dma_interrupt_flag_clear,参数和DMA中断标志位获取函数一样

这里配置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; // 数据传输完成
}
}

1.2.7.串口DMA接收数据处理

在前面的DMA配置过程中我们使能了数据传输量为一整个数组的长度,实际传输过程中可能并不能够传输足够的数据,所以需要在串口空闲中断中对数据进行处理。 实际的DMA传输的数据长度等于数组的长度减去当前DMA还剩余的数据量。然后标记一次数据传输完成

uint32_t dma_transfer_number_get(uint32_t dma_periph, dma_channel_enum channelx);

这个函数是获取DMA剩余的还没有传输的数据量。有两个参数,第一个参数是DMA外设,第二个参数是DMA通道

在处理完数据之后,如果还要继续下次传输,还需要再重新配置一下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进行串口的接收了

1.3.条件编译

前面我们分别操作了串口中断接收数据和串口DMA接收数据,但是分开编写的,也就是说是两个工程,不利于我们移植和使用。 下面给大家介绍一种条件编译的处理手段。比如我们要切换串口中断接收和串口DMA接收,只需要定义一个宏定义,当为1的时候是DMA接收,当为0的时候是中断接收

宏定义如下:

/* 是否使用DMA 0:串口中断接收  1:DMA接收 */
#define USB_USART_DMA 1

然后我们将DMA处理的代码和串口中断处理的代码分别用#if进行定义,如3-1-1所示

1

从图3-1-1可以看到当USB_USART_DMA为1的时候,显示的就是要编译的,全部都是DMA的配置, 反而看#else下面的串口中断的语句将会不显示,也就是不会编译到我们的代码中。其它部分也是如此,具体操作看资源包中的代码例程

1.4.实验现象

关于这一章节的代码,在资源包/04软件资料/代码例程/里面的012串口中断DMA接收二合一

烧写我们的代码之后,在串口助手中发送对应的数据,将会在串口助手中显示出来 中断接收和DMA接收请修改USB_USART_DMA宏定义的值。

中断接收测试结果如图4-1-1所示

1

DMA接收测试结果如图4-1-2所示

1