一、引言
最近在为自制的十路灰度传感器设计UART串口通信接收功能,灰度传感器使用STM32-F103-CBT6作为主控芯片。软件系统采用了FreeRTOS操作系统。RTOS系统存在的情况下,应当尽量减少中断的使用,因此常规基于中断的UART数据接收策略应当摒弃。基于中断的UART数据接收策略会频繁地进入中断,从而导致RTOS的任务调度实时性下降。此时我们便需要寻找一个新的接收策略,在RTOS系统实时性不受影响的条件下,保证接收串口数据完整并无丢失(非流控方式)。
本文给读者提供了一种RTOS轮询串口接收策略, 此策略基于FreeRTOS操作系统创建了一个串口接收线程,线程中采用UART+DMA的方式对串口数据进行10ms轮询,并为读者提供了UART不同传输速率和不同轮询时间下的缓存大小计算保证数据不会丢失的方法。
二、UART初始化
使用UART前,我们需要为UART通信选择一个合适的传输速率和传输信号格式,这个速率就叫做波特率,波特率的定义就是通信过程中每秒能传输多少位数据,单位即为:bps(bit per second)。因此在设计串口通信前我们需要对串口进行一些初始化,初始化包括串口的通信速率、校验位、停止位和数据位,初始化配置会影响到后续计算RAM缓存的大小。
串口初始化配置代码:
#define UART1_DMARX_BUF_SIZE 128U
UInt8 rxBuf[UART1_DMARX_BUF_SIZE]
static void UART1_DMARX_Init(UInt8* buf,UInt16 size);
void UART1_Init(UInt32 Baud)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
/* 初始化USART1_TX(PA9)\USART1_RX(PA8) */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
/* 初始化UART1 */
USART_InitStructure.USART_BaudRate = Baud; /* 设置波特率 */
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; /* 设置流控方式: 不采用流控 */
USART_InitStructure.USART_Mode = USART_Mode_Rx; /* 使能串口接收 */
USART_InitStructure.USART_Parity = USART_Parity_No; /* 不使用校验位 */
USART_InitStructure.USART_StopBits = USART_StopBits_1; /* 停止位设置成1位 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b; /* 数据长度是这成8位 */
USART_Init(USART1,&USART_InitStructure);
/* 配置DMA接收地址 */
UART1_DMARX_Init(rxBuf,UART1_DMARX_BUF_SIZE);
USART_Cmd(USART1,ENABLE);
}
static void UART1_DMARX_Init(UInt8* buf,UInt16 size)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); /* 打开 USART1_Rx 的DMA功能 */
DMA_InitStructure.DMA_BufferSize = (UInt32)size;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; /* 设置方向从外设到RAM */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr = (UInt32)buf; /* 设置RAM缓存地址(输入地址) */
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; /* 数据接收采用循环模式 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (UInt32)(&USART1->DR); /* 设置外设为USART_DR寄存器(输出地址) */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel5,&DMA_InitStructure);
DMA_Cmd(DMA1_Channel5,ENABLE);
}
初始化过程配置了UART数据格式为8个数据位、1个停止位、0个校验位、再加上额外1个启始位,每传输1个字节数据,即传输了10位。假若我们以波特率为9600作为传输速率,则每秒能传输的字节数为:
,
即在RX总线上以9600波特率连续不断地进行通信,平均每秒中能传输960字节,转化为毫秒单位则为:
STM32F103C8T6中UART外设,硬件本身不具备FIFO缓存功能,若采用10ms轮询接收,不采用DMA的情况下,硬件最多只能保存1字节数据,10ms内RX线上若有超过1字节接收,此时数据必定会丢失,这种情况下为硬件丢失了数据。因此想使用轮询策略,有两种方法可选:
1、减少CPU对串口数据的轮询间隔,从10ms改为不断轮询,可以解决数据丢失问题;
2、使用DMA辅助,配置DMA模块,当UART接收到数据时,让DMA自动将数据放到芯片内部RAM缓存中。
DMA全称Direct Memory Access,中文叫做直接内存访问,它可以在没有CPU的干预下,从外设搬运数据到内部RAM、从RAM搬运数据到外设或从RAM搬运数据到RAM,这些搬运过程都是全自动的,CPU不需要干预数据搬运过程,只需要发送一个搬运命令,然后使用DMA中断来通知CPU已经搬运完,或者CPU轮询是否搬运完即可。使用DMA可以让CPU有更多的时间去处理其他的任务,减少数据搬运的时间。
很明显,上述两种方法中,第一种是不可取的,CPU不可能不断地去查询串口是否有数据接收,因为产品设计过程中整个系统中不止这一个任务,其他任务也需要CPU去处理,其他任务执行时间过长(例如1ms)的情况下,在其他任务执行过程中,若接收到数据此时是无法识别的,同样会造成串口数据的丢失。另外在RTOS系统中,串口接收线程中不能一直采用轮询方式,这样比串口接收线程优先级低的程序就无法运行,因此方法一理论上可行,但是实际项目中不可取。
方法二的优势此时便体现出来了,配置DMA模块,当串口接收到数据时,DMA自动识别并自动搬运到RAM中,这样就相当于UART有了硬件的FIFO的加持,FIFO的大小取决于DMA中配置的大小。通过DMA配置的RAM缓存大小,我们就可以不用一直轮询串口是否有数据接收,我们只需要查询当前DMA接收到多少数据即可,这样在缓存足够的情况下,一定不会因为硬件的原因造成数据丢失,但是若缓存设置不当,仍然会造成数据丢失,因为此时新来的数据会覆盖还未处理的数据,这种情况为软件造成的串口数据丢失。
在初始化过程中我们配置了DMA1_Channel5,这个通道支持USART1_RX接收,此配置中我们配置了RAM中的数据为Byte格式,在搬运一个数据后,DMA数据指针会自动递增;UART外设数据为Byte格式,外设数据指针不会递增,即外设指针一直为USART1的DR寄存器。DMA工作在循环模式,此模式下一旦接收完size个字节数据,则会自动又开启size字节的接收,不需要我们手动再次使能DMA接收。
三、RTOS接收线程设计
FreeRTOS接收的逻辑代码:
UInt16 lastCnt = 0; /* 上一次DMA计数器的值 */
UInt16 currCnt = 0; /* 本次DMA计数器的值 */
UInt32 Counter = 0; /* 测试变量,用于计数接收到了多少字节数据 */
UInt32 pToRxBuf = 0; /* 指向当前应该处理的数据下标 */
UInt32 ValidLength = 0;/* 10ms内接收到的数据长度 */
static void UART_DMARx_DataEntry(UInt32 size);
/* 串口接收处理线程,10ms轮询一次 */
static void UART_RTOSTask(void *patameters)
{
while(1)
{
UART_RX_Handler();
vTaskDelay(10);
}
}
/* 接收线程详细处理过程 */
void UART_RX_Handler()
{
lastCnt = currCnt;
currCnt = DMA_GetCurrDataCounter(DMA1_Channel5);
if(lastCnt > currCnt)
{
/* 数据无溢出 */
ValidLength = lastCnt - currCnt;
Counter+=ValidLength;
}
else if(lastCnt < currCnt)
{
/* 数据有溢出 */
ValidLength = UART1_DMARX_BUF_SIZE + lastCnt - currCnt;
Counter+=ValidLength;
}
else
{
/* 无数据变化,直接退出 */
return;
}
/* 数据处理 */
UART_DMARx_DataEntry(ValidLength);
}
/* 接收数据处理函数 */
static void UART_DMARx_DataEntry(UInt32 size)
{
/* 遍历每个接收到的字节 */
while(size)
{
/******************************************
此处可添加对每个字节处理的代码
******************************************/
size--;
pToRxBuf++;
if(pToRxBuf >= UART1_DMARX_BUF_SIZE)
{
pToRxBuf = 0;
}
}
}
处理代码中仅展示了重要的处理逻辑,其他的变量的声明、任务的创建请自行编写,此处仅向读者介绍处理思路。此逻辑的处理思路很简单,每10ms查询一次当前DMA1_Channel5的传输计数值,因为计数值是从UART1_DMARX_BUF_SIZE开始递减的,所以每次轮询得到的计数值都是越来越小,递减到0之后又会以UART1_DMARX_BUF_SIZE开始计数递减。通过上一次计数值与当前计数值做差运算,即可得到10ms周期内接收到了多少字节数据,然后对所有接收到的数据进行逐字节处理,读者可以根据自己软件的效率来决定是否需要逐字节处理。
四、波特率与缓存大小匹配设置
通过上述代码逻辑,我们可以实现在RTOS轮询中,保证串口数据(不使用流控信号时)完整地不丢失地接收到我们内部RAM缓存中, 但是由于嵌入式芯片通常资源是有限,RAM资源更是宝贵,UART应用缓存自然是越大越好,但是UART数据缓存越大,其他应用可使用的RAM就变少了,因此合适地选择UART缓存大小是有必要的,此处介绍缓存大小的设置方法。
从上述的串口接收软件策略来看,我们每隔10ms 对所有接收到的数据进行处理,此处有两个关键速率,一个是缓存放入速率,还有一个是缓存取出速率,我们以115200波特率、1个停止位、0个校验位、8个数据位、1个起始位为例来计算这些速率。
首先假设一个条件,串口在不断地以上述配置格式发送数据到芯片内部。理想状态下,缓存放入速率为115200(bit/s) = 11520(Byte/s) = 115.2(Byte/10ms),则在不断接收数据的情况下,每隔10ms,芯片内部会接收到115.2字节;而缓存取出速率则取决于UART内部RAM缓存设置的大小,此处我们若设置x字节的缓存,缓存取出速率最大值为x(Byte/10ms),要想保证数据不丢失,则需要保证缓存取出速率 >= 缓存放入速率条件成立即可,因此在115200波特率、1个停止位、0个校验位、8个数据位、1个起始位、轮询时间间隔为10ms的情况下,缓存x应该设置为x >= 115.2即可,此处设置成128字节,即可满足在不断地接收数据时,保证数据不丢失。
五、软件测试验证
在上述软件中,我们定义了一个全局变量ValidLength,这个变量用于保存每10ms接收到的数据量;另外软件中还定义了一个全局变量Counter,这个变量用于计数接收的总字节数。现在我们来对其进行仿真测试,观察在以115200波特率,10位串口数据格式,10ms轮询,缓存RAM大小为128字节时,传输过程中ValidLength的值如图1所示其值保持在114和115之间跳动,符合设置的波特率大小。图2为串口软件发送的字节总数,图3为传输结束时Counter的值,其值和串口软件发送的字节总数一致,说明芯片内部通过10ms轮询将串口软件发送的所有数据接收到芯片内部了。
图1.传输过程中ValidLength的长度(缓存128字节)
图2.串口软件发送的总字节数(缓存128字节)
图3.传输结束时Counter的值(缓存128字节)
为了进行对比,此处再将缓存RAM设置为100字节,图4为串口软件发送的字节数,图5为传输完毕的数据总字节数,接收数据总字节数小于发送字节数,说明缓存不足时,数据部分丢失。
图4.串口软件发送的总字节数(缓存100字节)
图5.传输结束时Counter的值(缓存100字节)
六、小结
本篇文章讲述了在RTOS系统下,采用UART+DMA线程轮询方式对串口数据设计接收策略,该策略在保证RTOS系统实时性的同时也能保证串口数据不会丢失。 测试结果展示,该接收策略展现的性能优良,符合文中所述内容。同时还向读者展示如何根据自己的需求,将轮询速度与缓存大小进行匹配,读者可按照方法自行设计通信参数。