**
GD32-UART接收不定长数据
**
简要说明
最近,公司项目用到国产芯片GD32做开发,刚好搞好了UART数据接收这一块。这部分内容由于一开始完全采用中断接收,后来测试发现对应用性能有影响,因此又对接收这部分进行大的改动,改成采用中断+DMA的方式接收数据,花了一些时间,今天在这里进行整理总结,分享给大家,若是哪里有错,希望大家能够指教改出。
平台说明
这里我们演示的芯片型号是GD32F450ZKT6,我们通过UART3的演示来对这部分内容进行简单的叙述。
(注:以下代码实现都是基于GD官方固件库开发)
方法
下面我们简单介绍如何通过UART接收不定长数据包的两种方法:
- 接收中断(USART_INTEN_RBNEIE)+空闲中断(USART_INTEN_IDLEIE)
- DMA+空闲中断(USART_INTEN_IDLEIE)
代码实现
不管使用上面说的哪一种方法,在操作uart前,我们都需要对uart进行初始化操作。
以uart3为例
首先是相关的宏定义
#define GD32_COM4 UART3
#define GD32_COM4_CLK RCU_UART3
#define GD32_COM4_TX_PIN GPIO_PIN_0 //PA0
#define GD32_COM4_RX_PIN GPIO_PIN_1 //PA1
#define GD32_COM4_GPIO_PORT GPIOA
#define GD32_COM4_GPIO_CLK RCU_GPIOA
#define GD32_COM4_AF GPIO_AF_8
初始化操作如下
void gd_com_init(uint32_t com, uint32_t baudrate)
{
//232
rcu_periph_clock_enable(GD32_COM4_CLK);
/* enable USART clock */
rcu_periph_clock_enable(GD32_COM4_GPIO_CLK);
/* connect port to USARTx_Tx */
gpio_af_set(GD32_COM4_GPIO_PORT, GD32_COM4_AF, GD32_COM4_TX_PIN);
/* connect port to USARTx_Rx */
gpio_af_set(GD32_COM4_GPIO_PORT, GD32_COM4_AF, GD32_COM4_RX_PIN);
/* configure USART Tx as alternate function push-pull */
gpio_mode_set(GD32_COM4_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GD32_COM4_TX_PIN);
gpio_output_options_set(GD32_COM4_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GD32_COM4_TX_PIN);
/* configure USART Rx as alternate function push-pull */
gpio_mode_set(GD32_COM4_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GD32_COM4_RX_PIN);
gpio_output_options_set(GD32_COM4_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GD32_COM4_RX_PIN);
/* USART configure */
usart_deinit(com);
// usart_sample_bit_config(com, USART_OSB_1bit);
usart_oversample_config(com, USART_OVSMOD_8);
usart_baudrate_set(com,baudrate); //波特率
usart_parity_config(com, USART_PM_NONE); //校验位:NONE
usart_word_length_set(com, USART_WL_8BIT); //数据位:8
usart_stop_bit_set(com, USART_STB_1BIT); //停止位:1
usart_receive_config(com, USART_RECEIVE_ENABLE);
usart_transmit_config(com, USART_TRANSMIT_ENABLE);
usart_enable(com);
}
方法一
通过接收中断(USART_INTEN_RBNEIE)+空闲中断(USART_INTEN_IDLEIE)接收不定长数据
1. 首先,我们需要初始化UART3
gd32_com_init(GD32_COM4, MODBUS_BAUD_115200);
2. 接着,使能UART3的接收中断和空闲中断,打开UART3中断的使能的同时配置中断的优先级
/* Uart3中断使能 */
/* enable USART3 receive interrupt */
usart_interrupt_enable(GD32_COM4, USART_INTEN_RBNEIE);
/* enable USART3 IDLEIE interrupt */
usart_interrupt_enable(GD32_COM4, USART_INTEN_IDLEIE);
/* USART interrupt configuration */
nvic_irq_enable(UART3_IRQn, 1, 1);
3. 接下来就是中断函数的UART3_IRQ的实现
void UART3_IRQHandler(void)
{
//若接收到一个字节
if(RESET != usart_flag_get(GD32_COM4, USART_FLAG_RBNE))
{
g_ucRecvBuf_FIFO[MODBUS_UART_PORT3][g_cxRecvCnt_FIFO[MODBUS_UART_PORT3]++] = usart_data_receive(GD32_COM4);
}
//若接收检测到空闲帧中断
else if(RESET != usart_flag_get(GD32_COM4, USART_FLAG_IDLEF))
{
usart_data_receive(GD32_COM4);
}
}
在这里我们对接收中断和空闲帧中断做一个简要的说明
关于这部分,用户手册上对于这两个中断对应的标志位说明是这样描述的
简单来说,RBNE接收中断,就是只要读数据缓冲区接收到数据时,就会触发中断,而空闲中断IDLEF则是需要完整地接收到一帧数据(多个字节),才会去触发对应的中断。不管是接收中断还是空闲中断,在触发中断进入中断函数后,我们都需要在中断函数中去清除对应的标志位,才能正常进入等待下一次中断的触发。
以下是整个过程的流程图说明
以上是通过UART接收中断(RBNE)+空闲中断(IDLEF)来接收不定长数据的方法,从上述我们可以看到,这种方法需要频繁地进入中断来接收一帧数据中的每个字节,假如说一帧数据有8个字节,则接到到这帧完整地数据,前前后后会进入9次中断函数,在频繁的数据传输中,这对CPU的运行速率是一个很大的负担,在个别应用上,这种方法是不适用的。
下面,我们介绍下另外一种方法来接收不定长的UART数据
方法二
DMA+空闲中断(USART_INTEN_IDLEIE)接收不定长数据
这里我们先简单介绍下DMA的作用,这里同样直接把用户手册的内容挂上来
通过对DMA功能的大概了解,我们可以先想到这么一种思路,在串口检测到有数据输入的时候,让DMA直接往我们定义好的存储器中搬收到的UART数据,在这个过程中,CPU还是在处理自己的事,最后,在一帧数据传输完成后,通过中断告诉主任务一帧数据已经传输完毕,主任务再对接收到的数据进行相应的处理。整个过程下来,程序代码只会触发一次中断,就是DMA搬移完数据后通知CPU的那一次中断,对比前一种方法,每接收到一个字节就需要进中断把数据读到自定的的FIFO中,第二种方法不论一帧数据的数据量有多大,我们都只会触发一次中断。
以下是代码实现
1. 同样,首先,我们第一步仍然需要初始化UART3
gd32_com_init(GD32_COM4, MODBUS_BAUD_115200);
2. 接着,这里我们只需要使能UART3的空闲中断,打开UART3中断的使能的同时配置中断的优先级
/* enable USART3 IDLEIE interrupt */
usart_interrupt_enable(GD32_COM4, USART_INTEN_IDLEIE);
/* USART interrupt configuration */
nvic_irq_enable(UART3_IRQn, 1, 1);
3. DMA的初始化配置
在对DMA进行初始化配置前,我们需要通过翻看手册确认我们需要使用DMA的哪个通道。
接着进行相关的配置操作
usart_dma_config(GD32_COM4);
void usart_dma_config(uint32_t com){
dma_single_data_parameter_struct dma_init_struct;
/* enable DMA0 */
rcu_periph_clock_enable(RCU_DMA0);
/* deinitialize DMA channel */
dma_deinit(DMA0, DMA_CH1);
dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;
//存储器地址
dma_init_struct.memory0_addr = (uint32_t)&g_ucRecvBuf_FIFO[MODBUS_UART_PORT2][0];
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
dma_init_struct.number = 4;
dma_init_struct.periph_addr = USART2_DATA_ADDRESS;
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_init_struct.priority = DMA_PRIORITY_MEDIUM;
dma_single_data_mode_init(DMA0, DMA_CH1, dma_init_struct);
/* configure DMA mode */
// dma_circulation_disable(DMA0, DMA_CH1);
dma_circulation_enable(DMA0, DMA_CH1); //循环模式
dma_channel_subperipheral_select(DMA0, DMA_CH1, DMA_SUBPERI4);
//放在使能通道前,使能通道后不可写
dma_transfer_number_config(DMA0, DMA_CH1, MODBUS_ASCII_FRAME_SIZE);
//使能通道
dma_channel_enable(DMA0, DMA_CH1);
usart_dma_receive_config(USART2, USART_DENR_ENABLE);
}
对于uart DMA的初始化,大致可按照用户手册上的说明来配置
通过对dma_single_data_parameter_struct 结构体变量的配置,按需求设置好DMA的源地址(UART3_DATA),目的地址(自定义FIFO地址),以及一次传输的字节数等后,使能DMA通道,最后打开串口的DMA接收模式,这样,我们就可以在UART接收中正常使用DMA功能了。
在DMA的配置函数中,我们还调用了这样一个函数:
//放在使能通道前,使能通道后不可写
dma_transfer_number_config(DMA0, DMA_CH1, MODBUS_ASCII_FRAME_SIZE);
这里是对通道计数寄存器的配置,我们需要先配置好这个寄存器,在接收到一帧数据后再去读这个寄存器的值,来获取接收到的数据帧的大小。
4. 最后就是中断函数的UART3_IRQ的实现
void UART3_IRQHandler(void){
//DMA接收,只触发空闲中断
if(RESET != usart_flag_get(GD32_COM4, USART_FLAG_IDLEF))
{
Receive_DataPack(GD32_COM4);
usart_data_receive(GD32_COM4); //清楚标志位
OSSemPost(g_pRecvSEM[MODBUS_UART_PORT3]);
}
}
UART3_IRQ中断函数主要做了三件事,调用了Receive_DataPack函数,清除标志位并通知主任务已接收到一帧数据。
Receive_DataPack函数
void Receive_DataPack(uint32_t com){
dma_single_data_parameter_struct dma_init_struct;
/* 关闭DMA,防止干扰 */
dma_channel_disable(DMA0, DMA_CH2);
/* 清DMA标志位 */
dma_flag_clear(DMA0, DMA_CH2, DMA_INTF_FTFIF);
/* 获取接收到的数据长度,单位:字节 */
g_cxRecvCnt_FIFO[MODBUS_UART_PORT3] = MODBUS_ASCII_FRAME_SIZE - dma_transfer_number_get(DMA0, DMA_CH2);
dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;
//设置存储器地址
dma_init_struct.memory0_addr = (uint32_t)&g_ucRecvBuf_FIFO[MODBUS_UART_PORT3][0];
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
dma_init_struct.number = 4;
dma_init_struct.periph_addr = UART3_DATA_ADDRESS;
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
dma_single_data_mode_init(DMA0, DMA_CH2, dma_init_struct);
/* 重新赋值计数值 */
dma_transfer_number_config(DMA0, DMA_CH2, MODBUS_ASCII_FRAME_SIZE);
/* 重新打开DMA */
dma_channel_enable(DMA0, DMA_CH2);
}
Receive_DataPack函数主要做了两件事,一是通过计算对应通道计数寄存器值的变化来得到接收到的数据的长度;二则是重新进行DMA的配置(主要是DMA目的地址的配置)。
以下是对上述整个流程的简要说明:
对于这部分的介绍大致就介绍到这里,如果哪里有误,欢迎大家指出!