串口中断接收配置流程
一般串口中断接收都需要有以下几个步骤。
- 开启时钟(包括串口时钟和GPIO时钟)
- 配置GPIO复用模式
- 配置GPIO的模式
- 配置GPIO的输出
- 配置串口(配置一些参数)
- 使能串口(串口使能、发送使能和接收使能)
- 中断配置(串口接收中断和中断优先级)
关于串口的一些常规配置步骤在串口打印信息章节中已经讲过了,这里就不再赘述。下面从中断接收部分进行介绍。串口接收配置就是在前面串口配置的基础上加上串口接收功能和中断配置。
使能串口接收
void usart_receive_config(uint32_t usart_periph, uint32_t rxconfig);
这个函数配置串口接收。有两个参数,其中第二个参数对应的选项如图所示。
从图中可以看到,如果使能串口接收就配置为USART_RECEIVE_ENABLE
。
中断配置
接收数据按照之前的串口章节,是放在串口中断中,通过读数据缓冲区非空中断进行接收的。现在我们修改为使用DMA方式接收,当读串口缓冲区有数据时,自动通过DMA将数据搬运到我们的内存中,我们需要的只是判断是否接受完成即可,所以要使能串口的空闲中断,既然要操作中断那肯定要配置中断的优先级,还是按照原来的中断分组,2个抢占优先级2个响应优先级。这里配置串口的优先级可以配置为抢占优先级为2,响应优先级为2。
串口中断的宏定义如下:
#define BSP_USART_IRQ USART0_IRQn
串口中断配置如下:
/* 使能读数据缓冲区非空中断和过载错误中断 */
//usart_interrupt_enable(BSP_USART, USART_INT_RBNE);
/* 使能IDLE线检测中断 */
usart_interrupt_enable(BSP_USART, USART_INT_IDLE);
/* 配置中断优先级 */
eclic_irq_enable(BSP_USART_IRQ, 2, 2);
2
3
4
5
6
这里需要注意,IDLE线检测中断会在一上电的时候就会自动进入一次。需要我们清除中断标志位。关于空闲中断的开启,在后面中断服务函数章节进行说明。
串口变量定义
单片机要实现接收数据,也就是说要通过上位机给单片机发送数据,那单片机需要接收数据然后存储起来,所以需要定义一个数组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 255
2
变量定义如下:
uint8_t g_recv_buff[USART_RECEIVE_LENGTH]; // 接收缓冲区
uint16_t g_recv_length = 0; // 接收数据长度
volatile uint8_t g_recv_complete_flag = 0; // 接收完成标志位
2
3
串口DMA接收配置流程
一般使用DMA功能,都需要有以下几个步骤。
- 开启时钟
- 配置参数结构体
- 使能通道外设
- 使能DMA中断
- 使能外设DMA(串口)
- DMA中断服务函数
开启时钟
老规矩,使用外设之前要先开启时钟,GD32VW553HQM6只有一个DMA。我们要使用的是串口0的接收功能,查找用户手册的第166页可知,USART0_RX对应于DMA的通道2和通道4。这里选择DMA的通道2进行配置。
宏定义如下:
#define BSP_DMA_RCU RCU_DMA // DMA时钟
#define BSP_DMA_CH DMA_CH2 // DMA通道
2
时钟使能如下:
/* 开启时钟 */
rcu_periph_clock_enable(BSP_DMA_RCU);
2
配置参数结构体
首先要先了解DMA传输有单一传输和增量突发传输两种,单一传输一次可以传输8位、16位和32位,增量突发传输可配置为4个节拍、8个节拍和16个节拍进行传输,每次传输量是单一传输的4倍,8倍和16倍。这里以单一传输进行配置。
单一传输参数结构体定义如图所示。
periph_addr
:外设基地址。外设就是串口,外设基地址就是串口基地址,选择配置为(uint32_t)&USART_DATA(BSP_USART)
。periph_inc
:外设地址生成算法模式。关于periph_inc
有几个可选选项如图所示。
DMA_PERIPH_INCREASE_ENABLE
:增量模式DMA_PERIPH_INCREASE_DISABLE
:固定模式
外设的地址也就是串口的地址是不变的,所以配置为固定模式,也就是选择DMA_PERIPH_INCREASE_DISABLE
。
memory0_addr
:存储器基地址。存储器基地址也就是我们定义的数组的地址,也就是配置为(uint32_t)g_recv_buff。
memory_inc
:存储器地址生成算法模式。存储器要存储数据,每次传输之后都需要存放在不同的地址里,要配置为增量模式。
也就是选择DMA_MEMORY_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
。
circular_mode
:DMA循环模式。可选选项如图所示。
这里关闭循环模式,配置为DMA_CIRCULAR_MODE_DISABLE
。
direction
:DMA通道数据传输方向。关于direction
有几个可选选项如图所示。
DMA_PERIPH_TO_MEMORY
:外设到内存DMA_MEMORY_TO_PERIPH
:内存到外设DMA_MEMORY_TO_MEMORY
:内存到内存
首先知道我们操作的是串口的接收DMA功能,相当于是从串口将数据搬运出来放到定义的数组中,串口就是外设,定义的数组就是存储器,那自然这个传输方向就是外设到内存,选择DMA_PERIPH_TO_MEMORY
。
number
:DMA通道数据传输数量。这个就是通过DMA传输的数据量,当这个传输量为0的时候,说明当前 传输完成,会进入中断。如果数据量超过这个配置值,超出的部分不会接收到。可根据我们的需求去配置。priority
:DMA通道传输软件优先级。DMA通道传输软件优先级。这个是配置DMA的软件优先级,可选选项如图所示。
DMA_PRIORITY_LOW
:低优先级;DMA_PRIORITY_MEDIUM
:中优先级;DMA_PRIORITY_HIGH
:高优先级;DMA_PRIORITY_ULTRA_HIGH
:超高优先级;
可根据我们需求去配置。
在配置完结构体参数之后,需要去初始化结构体,可以调用下面这个函数。
void dma_single_data_mode_init(dma_channel_enum channelx, dma_single_data_parameter_struct* init_struct);
这个函数是初始化单一传输模式,有两个参数,第一个参数channelx
是要使用的DMA通道,第二个参数init_struct
是单一传输参数配置结构体。
定义单一传输参数结构体如下:
dma_single_data_parameter_struct dma_init_struct;
关于配置结构体和初始化代码如下:
/* 初始化DMA通道 */
dma_deinit(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_RDATA(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_CH, &dma_init_struct);
2
3
4
5
6
7
8
9
10
11
12
这里需要解释一下ARRAYNUM这个宏定义,这个宏定义是求传进来的数组的长度的。
宏定义如下:
#define ARRAYNUM(arr_name) (uint32_t)(sizeof(arr_name) / sizeof(*(arr_name)))
使能通道外设
在配置好DMA参数之后,还需要使能通道外设才能正常使用通道。
void dma_channel_subperipheral_select(dma_channel_enum channelx, dma_subperipheral_enum sub_periph);
这个函数是DMA通道外设选择。有两个参数,第一个参数channelx
就是要使用的通道,第二个参数sub_periph
就是要开启的通道外设。关于外设通道如图所示。
我们使用的是USART0_RX的DMA功能,其配置如图所示。
可见最前面的数值是100,对应的10进制也就是4,对应于枚举定义就是DMA_SUBPERI4
。
使能通道外设代码如下:
/* 使能通道外设 */
dma_channel_subperipheral_select(BSP_DMA_CH, DMA_SUBPERI4);
2
使能通道外设之后还需要使能对应的通道。
void dma_channel_enable(dma_channel_enum channelx);
这个函数是使能DMA通道。有一个参数,这个参数channelx
是要使用的DMA通道。
配置代码如下:
/* 使能DMA通道 */
dma_channel_enable(BSP_DMA_CH);
2
使能DMA中断
如果我们需要用到DMA的中断功能的话就需要配置DMA的中断。
DMA中断和DMA中断服务函数宏定义如下:
#define BSP_DMA_CH_IRQ DMA_Channel2_IRQn // DMA中断
#define BSP_DMA_CH_IRQHandler DMA_Channel2_IRQHandler// DMA中断服务函数
2
使能中断,啥也不说,先配置中断优先级。之后再使能中断,配置进入中断的触发条件。
void dma_interrupt_enable(dma_channel_enum channelx, uint32_t interrupt);
这个函数是使能DMA中断。有两个参数,第一个参数是要使用的DMA通道,第二个参数是要使能的中断资源。
关于第二个参数的可选选项如图所示。
DMA_INT_SDE
:通道单数据传输模式异常中断DMA_INT_TAE
:通道发生传输错误中断DMA_INT_HTF
:通道半传输完成中断DMA_INT_FTF
:通道传输完成中断DMA_INT_FEE
:使能FIFO异常中断
可以根据自己的应用需求去选择,我这里选择使能传输完成中断。
/* 使能DMA通道中断 */
nvic_irq_enable(BSP_DMA_CH_IRQ,2,1);
/* 使能DMA中断 */
dma_interrupt_enable(BSP_DMA_CH,DMA_INT_FTF); // 使能传输完成中断
2
3
4
使能外设DMA(串口)
在前面我们单独配置串口和DMA,要使数据从DMA和串口之间进行传输,还需要配置串口的DMA接收请求使能。如果是使用DMA的发送功能,那就是配置串口的DMA的发送请求。
void usart_dma_receive_config(uint32_t usart_periph, uint8_t dmaconfig);
这个函数是串口DMA接收配置。有两个参数,第一个参数usart_periph
是要配置的串口外设,第二个参数dmaconfig
是要使能还是失能。
关于DMA的第二个参数定义如图所示。
有两个参数,分别对应使能和失能。
使能串口DMA接收代码如下:
/* 使能串口DMA接收 */
usart_dma_receive_config(BSP_USART, USART_RECEIVE_DMA_ENABLE);
2
具体的DMA初始化配置如下:
void USART0ReceiveDmaConfig(void)
{
dma_single_data_parameter_struct dma_init_struct;
rcu_periph_clock_enable(BSP_DMA_RCU); // 开启DMA时钟
// 以默认参数先填充DMA参数
dma_single_data_para_struct_init(&dma_init_struct);
/* 初始化DMA通道 */
dma_deinit(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_RDATA(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_CH, &dma_init_struct);
/* 禁用DMA循环 */
dma_circulation_disable(BSP_DMA_CH);
/* 使能通道外设 */
dma_channel_subperipheral_select(BSP_DMA_CH, DMA_SUBPERI4);
/* 使能DMA通道 */
dma_channel_enable(BSP_DMA_CH);
/* 使能串口DMA接收 */
usart_dma_receive_config(BSP_USART, USART_RECEIVE_DMA_ENABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DMA中断服务函数
DMA中断服务函数和串口中断服务函数一样,也是通过判断对应的标志位来判断进入中断。
FlagStatus dma_interrupt_flag_get(dma_channel_enum channelx, uint32_t int_flag);
这个函数是DMA中断标志位获取。有两个参数,第一个参数channelx
就是DMA通道,第二个int_flag
是要获取的中断标志位。第二个参数的定义如图所示。
在进入中断之后,记得要清除中断,有对应的中断标志位清除函数dma_interrupt_flag_clear
,参数和DMA中断标志位获取函数一样。
这里配置DMA传输完成中断对应的代码如下:
void BSP_DMA_CH_IRQHandler(void){
/* 传输完成中断 */
if(dma_interrupt_flag_get(BSP_DMA_CH, DMA_INT_FLAG_FTF))
{
dma_interrupt_flag_clear(BSP_DMA_CH, DMA_INT_FLAG_FTF);
}
}
2
3
4
5
6
7
串口DMA接收数据处理
在前面的DMA配置过程中我们使能了数据传输量为一整个数组的长度,实际传输过程中可能并不能够传输足够的数据,所以需要在串口空闲中断中对数据进行处理。实际的DMA传输的数据长度等于数组的长度减去当前DMA还剩余的数据量。然后标记一次数据传输完成。
uint32_t dma_transfer_number_get(dma_channel_enum channelx);
这个函数是获取DMA剩余的还没有传输的数据量。有一个参数,参数是DMA通道。
在处理完数据之后,如果还要继续下次传输,还需要再重新配置一下DMA。
具体的串口中断服务函数代码如下:
void BSP_USART_IRQHandler(void)
{
if(usart_interrupt_flag_get(BSP_USART,USART_INT_FLAG_IDLE) != RESET) // 检测到帧中断
{
/* 清除空闲中断标志位 */
usart_interrupt_flag_clear(BSP_USART, USART_INT_FLAG_IDLE);
/* 处理DMA接收到的数据 */
// 获取实际接收到的数据长度
g_recv_length = USART_RECEIVE_LENGTH - dma_transfer_number_get(BSP_DMA_CH);
g_recv_buff[g_recv_length] = '\0';
g_recv_complete_flag = 1; // 数据传输完成
/* 重新设置DMA传输 */
dma_channel_disable(BSP_DMA_CH); // 失能DMA通道
USART0ReceiveDmaConfig(); // 重新配置DMA进行传输
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
之后我们还要在main函数中的while下打印接收到的数据供我们使用。
/* 等待数据传输完成 DMA */
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;
}
2
3
4
5
6
7
8
9
到此,编译下载到开发板就可以使用DMA进行串口的接收了。
条件编译
前面我们分别操作了串口中断接收数据和串口DMA接收数据,但是分开编写的,也就是说是两个工程,不利于我们移植和使用。下面给大家介绍一种条件编译的处理手段。比如我们要切换串口中断接收和串口DMA接收,只需要定义一个宏定义,当为1的时候是DMA接收,当为0的时候是中断接收。
宏定义如下:
/* 是否使用DMA 0:串口中断接收 1:DMA接收 */
#define USB_USART_DMA 1
2
然后我们将DMA处理的代码和串口中断处理的代码分别用#if
进行定义,如所示。
从图可以看到当USB_USART_DMA
为1
的时候,显示的就是要编译的,全部都是DMA的配置,反而看#else
下面的串口中断的语句将会不显示,也就是不会编译到我们的代码中。其它部分也是如此,具体操作看资源包中的代码例程。
实验现象
现象与之前的串口收发实验一致,但是不同的是接收方式的不同。
关于这一章节的代码,可以在开发板资料/03 - 软件资料/代码例程/里面的012串口DMA接收
。
下载中心跳转📦
资料下载中心:点击跳转🚀