二十、串口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 所示:
- 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); // 配置中断优先级
2
3
4
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
2
变量定义如下:
uint8_t g_recv_buff[USART_RECEIVE_LENGTH]; // 接收缓冲区
uint16_t g_recv_length = 0; // 接收数据长度
uint8_t g_recv_complete_flag = 0; // 接收完成标志位
2
3
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-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
串口中断服务函数如下:
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
3
4
5
6
7
8
9
10
11
12
13
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;
}
2
3
4
5
6
7
8
9
每次处理完数据后都要记得把对应的标志位和数组都清零。
到此,编译下载到开发板就可以使用串口中断接收功能了,关于实验现象后面会介绍。
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通道
2
3
时钟使能如下:
/* 开启时钟 */
rcu_periph_clock_enable(BSP_DMA_RCU);
2
1.2.2.配置参数结构体
首先要先了解 DMA 传输有单一传输和增量突发传输两种,单一传输一次可以传输 8 位、16 位和 32 位,增量突发传输可配置为 4 个节拍、8 个节拍和 16 个节拍进行传输,每次传输量是单一传输的 4 倍,8 倍和 16 倍。这里以单一传输进行配置。
单一传输参数结构体定义如图 2-2-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 所示。
- 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 所示。
- 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:存储器地址生成算法模式。存储器要存储数据,每次传输之后都需要存放在不同的地址里,要配置为增量模式。
也就是选择 DMA_MEMORY_INCREASE_ENABLE。
periph_memory_width:外设和存储器数据传输宽度。在单一模式中,外设传输数据宽度和存储器传输数据宽度一致,可选选项如图 2-2-4 所示。
- 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 所示。
- DMA_PRIORITY_LOW:低优先级
- DMA_PRIORITY_MEDIUM:中优先级
- DMA_PRIORITY_HIGH:高优先级
- DMA_PRIORITY_ULTRA_HIGH:超高优先级
可根据我们需求去配置。
circular_mode:DMA 循环模式。可选选项如图 2-2-6 所示。
这里关闭循环模式,配置为 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);
2
3
4
5
6
7
8
9
10
11
12
这里需要解释一下 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 所示。
我们使用的是 USART0_RX 的 DMA 功能,其配置如图 2-3-2 所示。
可见最前面的数值是 100,也就是 4,对应于枚举定义就是 DMA_SUBPERI4。
使能通道外设代码如下:
/* 使能通道外设 */
dma_channel_subperipheral_select(BSP_DMA, BSP_DMA_CH, DMA_SUBPERI4);
2
使能通道外设之后还需要使能对应的通道。
void dma_channel_enable(uint32_t dma_periph, dma_channel_enum channelx);
这个函数是使能 DMA 通道。有两个参数,第一个参数是要使用的 DMA 外设,第二个参数是要使用的 DMA 通道。
配置代码如下:
/* 使能DMA通道 */
dma_channel_enable(BSP_DMA, BSP_DMA_CH);
2
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中断服务函数
2
使能中断,啥也不说,先配置中断优先级。之后再使能中断,配置进入中断的触发条件。
void dma_interrupt_enable(uint32_t dma_periph, dma_channel_enum channelx, uint32_t source);
这个函数是使能 DMA 中断。有三个参数,第一个参数是要使用的 DMA 外设,第二个参数是要使用的 DMA 通道,第三个参数是要使能的中断资源。
关于第三个参数的可选选项如图 2-4-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); // 使能传输完成中断
2
3
4
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 所示。
有两个参数,分别对应使能和失能。
使能串口 DMA 接收代码如下:
/* 使能串口DMA接收 */
usart_dma_receive_config(BSP_USART, USART_DENR_ENABLE);
2
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 所示。
在进入中断之后,记得要清除中断,有对应的中断标志位清除函数 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; // 数据传输完成
}
}
2
3
4
5
6
7
8
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进行传输
}
2
3
4
5
6
7
8
9
10
11
之后我们还要在 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;
}
2
3
4
5
6
7
8
9
到此,编译下载到开发板就可以使用 DMA 进行串口的接收了。
1.3.条件编译
前面我们分别操作了串口中断接收数据和串口 DMA 接收数据,但是分开编写的,也就是说是两个工程,不利于我们移植和使用。下面给大家介绍一种条件编译的处理手段。比如我们要切换串口中断接收和串口 DMA 接收,只需要定义一个宏定义,当为 1 的时候是 DMA 接收,当为 0 的时候是中断接收。
宏定义如下:
/* 是否使用DMA 0:串口中断接收 1:DMA接收 */
#define USB_USART_DMA 1
2
然后我们将 DMA 处理的代码和串口中断处理的代码分别用#if 进行定义,如 3-1-1 所示。
从图 3-1-1 可以看到当 USB_USART_DMA 为 1 的时候,显示的就是要编译的,全部都是 DMA 的配置,反而看#else 下面的串口中断的语句将会不显示,也就是不会编译到我们的代码中。其它部分也是如此,具体操作看资源包中的代码例程。
1.4.实验现象
关于这一章节的代码,在资源包/04 软件资料/代码例程/里面的 012 串口中断 DMA 接收二合一。
烧写我们的代码之后,在串口助手中发送对应的数据,将会在串口助手中显示出来。
中断接收和 DMA 接收请修改 USB_USART_DMA 宏定义的值。
中断接收测试结果如图 4-1-1 所示。
DMA 接收测试结果如图 4-1-2 所示。