#创作灵感#
在我们实际使用MCU进行多串口任务分配的时候,我们会碰到这样一种情况,即串口需要短间隔周期性发送数据,且相邻两帧之间需要间隔一段时间,防止连帧。我们常常需要在软件层面对串口的发送和接受做一个缓存的处理方式。
针对发送,我们使用以下策略:
即1、上层应用在发送数据的时候其实是先将数据送入我们自己设定的发送缓存内(一个数组);
2、真正的发送是主循环在判断出缓存内有数据的时候,自行启动的发送;
所以我们的发送函数其实有两个,一个用于上层应用将数据写入缓存,一个用于串口自启动发送过程。串口自启动发送连续字节的时候 我给大家提供了有三种发送方式:堵塞型发送、轮询式发送、中断式发送
对于MCU主频不高的情况,通常跑完一整个while(1)循环 需要的时间相对而言比较长,而串口连续数据的发送是常常要满足一定超时时间范围的,比如你的串口连的是某个可以通信的电机,一帧的完整性需要得到保证,超出这个超时时间,电机可能会识别不出来这一帧指令。当然,现在大部分的MCU主控选用高速晶振的时候 都是可以满足要求的。
堵塞型发送
通常一帧会包含很多字节数据,堵塞型发送其实就是程序在原地等待发送缓冲区空的标志位 或者发送完成标志位 ,一般发送完成肯定是比发送缓冲区空的时间晚。
cpp
u8 i = 0;
if (USARTx == USART1)
{
for (; i < len; i++)
{
if ((USART1->STATR & USART_STATR_TXE)) {
USART1->DATAR = Data[i];
}
while (!(USART1->STATR & USART_STATR_TXE)); // 检查发送缓冲区空标志
}
}
我们会使用while循环在这里等待发送缓冲区空 ,当然也会容易导致系统在此堵塞,从而对于其他控制响应很慢,如果你的系统并没有短时间的连续发送帧的要求,响应也没有要求很高,使用这种方式也是可以的。
轮询型发送
轮询的核心思想其实就是利用最外层的while(1)循环,每次都只用if来判断条件是否成立,这样就不会导致系统的堵塞,但是这对你的整个系统大循环一次的时间有要求,现在基本高主频都是可以满足要求的,也不需占用中断资源,是个不错的方法。在使用状态机理论的时候,其实也是在轮询,这样的思想在很多产品中都会出现。
具体的实现代码见上述堵塞型发送,只不过不再有while循环原地等待,同时要记录下当前发送字节的位置,这样下次循环进来可以根据位置直接发掉下个对应的字节数据。
中断型发送
核心思想其实是先发一个字节触发发送完成中断,而后在中断里将下一个需要发送的数据塞入串口的数据寄存器内,等待下一次触发中断,直到缓存内的帧数据全部被发送完,期间如果有多帧,那么需要延后一段时间Ts来避免连帧。
对此我常常会定义这么两个结构体
cs
typedef struct
{
u8 length; //帧长 写入数据的时候需要更新
u8 DataBuff[20]; //帧的内容 这里的最大帧长就是20
u8 ProcessLoc; //当前帧处理的字节位置 如果不是一次性发完所有字节 建议都保留使用
}Frame;
cs
struct
{
UartFrame FBuff[20]; //帧的内容
u8 UsartFrameNum; //帧的数量 每次写入这个数据就会加一 到20末尾的话回环到0
u8 UsartFinishLoc; //当前处理帧的位置 每次发送完一帧 就加一 同样也需要回环到0
u8 UsartBusyFlag : 1; //帧处理间隔 就设置这个标志位 表示繁忙 20ms以后自动解封
u16 UsartTimeNap; //放在定时器中断内用来累加
}UsartSendStorage;
第一个是帧的结构体,用来保存与单帧有关系的各项数据;
第二个是串口的发送存储,如果有很多串口需要使用,可以单独设置;
下面是关于串口的初始化设置,每种MCU可能都不太一样,但是建议大家使用宏定义来选择当前的工作方式,比较方便。
cpp
void UsartInit (void) {
USART_InitTypeDef USART_InitStructure = {0};
NVIC_InitTypeDef NVIC_InitStructure = {0};
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init (USART2, &USART_InitStructure);
USART_Init (USART3, &USART_InitStructure);
USART_Init (USART1, &USART_InitStructure);
#define USART_Interrupt_Mode // 中断发送模式
#ifdef USART_Interrupt_Mode
USART_ITConfig (USART2, USART_IT_RXNE, ENABLE); // 接收寄存器非空中断
USART_ITConfig (USART2, USART_IT_TC, ENABLE); // 发送寄存空中断
USART_ITConfig (USART3, USART_IT_RXNE, ENABLE); // 接收寄存器非空中断
USART_ITConfig (USART3, USART_IT_TC, ENABLE); // 发送寄存空中断
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init (&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init (&NVIC_InitStructure);
#endif
USART_Cmd (USART3, ENABLE);
USART_Cmd (USART2, ENABLE);
USART_Cmd (USART1, ENABLE);
}
抢占优先级要设置一样,这样两个串口不会互相打断,响应优先级可以设置不一样。
下面是几个函数用于添加帧到发送缓存以及自启动发送过程。
cs
void UsartSetFrame(USART_TypeDef *USARTx, unsigned char *Data, unsigned char len)
{
if(USARTx== USART1)
{
memcpy(UsartSendStoorage.FBuff[UsartSendStoorage.UsartFrameNum].DataBuff,Data,len)
UsartSendStoorage.FBuff[UsartSendStoorage.UsartFrameNum].length = len;
UsartSendStoorage.UsartFrameNum++;
if(UsartSendStoorage.UsartFrameNum >=10)
UsartSendStoorage.UsartFrameNum = 0;
}
}
注意我们这里能缓存的帧最长只有10帧,超出这个范围以后,num的值就回头覆盖掉第一项了,你可以认为它就是一个会回环的写指针,用来指示当前可以存储帧的第一个位置。
这里是把我们要发送的帧送到发送缓存里,
cs
void UsartSendFrameTask(USART_TypeDef *USARTx)
{
if (USARTx == USART1)
{
if(UsartSendStorage.UsartFrameNum != UsartSendStorage.UsartFinishLoc &&
!UsartSendStorage.UsartBusyFlag ) //有数据需要发 且不繁忙
#ifdef USART_Interrupt_Mode
USART1->DATAR = UsartSendStorage.FBuff[UsartSendStorage.UsartFinishLoc];
#else
UsartSendData(UsartSendStorage.FBuff[UsartSendStorage.UsartFinishLoc]);
UsartSendStorage.UsartFinishLoc++;
UsartSendStorage.UsartFrameNum--; //可选
if(UsartSendStorage.UsartFinishLoc>=20)
UsartSendStorage.UsartFinishLoc = 0;
#endif
}
}
每次发送完一帧,记得需要把finishLoc加一,这样只要加入帧的位置和最后处理完帧的位置是一致的,说明此时就没有帧要发送,相反,如果此时有帧需要发送,num的值和finishLoc的值是不一致的。当然,这个num我的本意是将它作为写入帧的位置,如果你要将它理解为帧的数量,当你处理完一帧的时候,可以将它减一,只用于记录帧的个数,是个标量。这样的话你判断是否有帧的标准就是num的值是不是为0,就不是两者是否相等。
cs
void USART_IRQHandler (void)
{
u8 i;
if (USART_GetITStatus (USART2, USART_IT_TC)) // 发送完成标志 表示上一次数据已经发送
{
if (USARTST.USART2SendBUFF[USARTST.USART1SendFrameFinishLoc].FrameProcessLoc < USARTST.USART2SendBUFF[USARTST.USART1SendFrameFinishLoc].FrameLength) {
// 当前帧处理字节的位置还没到末尾 继续发送
i = USARTST.USART2SendBUFF[USARTST.USART1SendFrameFinishLoc].FrameProcessLoc; // 当前需要处理数据的位置
USART1->DATAR =
USARTST.USART1SendBUFF[USARTST.USART1SendFrameFinishLoc].FrameBuff[i];
USARTST.USART2SendBUFF[USARTST.USART1SendFrameFinishLoc].FrameProcessLoc++;
} //
else // 帧处理完毕了 可以调用busyflag 来产生间隔
{
USARTST.USART1BusyFlag = 1;
USARTST.USART1SendFrameFinishLoc++; // 这一帧发送完毕
if (USARTST.USART1SendFrameFinishLoc >= 10) // 回环处理
USARTST.USART1SendFrameFinishLoc = 0;
}
}
}
大家可能对结构体内的TimeNap感到疑惑,其实它就是用来进行累加,这个累加函数会被调用在滴答定时器中断内,当busyflag = 1 的时候可以将TimeNap清0,而后在SendFrameTask()函数内部给一个if判断,当TimeNap >= 200的时候,自动将busyflag = 0,就完成了帧间隔的设置,因为发送帧的时候是需要判断帧是否繁忙的,不繁忙才会启动帧的发送。
以上仅供大家参考使用,如有错误之处,还请多多体谅。