串口的缓存发送以及缓存接收机制

#创作灵感#

在我们实际使用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,就完成了帧间隔的设置,因为发送帧的时候是需要判断帧是否繁忙的,不繁忙才会启动帧的发送。

以上仅供大家参考使用,如有错误之处,还请多多体谅。

相关推荐
achene_ql9 分钟前
缓存置换:用c++实现最不经常使用(LFU)算法
c++·算法·缓存
尽兴-11 分钟前
缓存分片哈希 vs 一致性哈希:优缺点、区别对比及适用场景(图示版)
算法·缓存·哈希算法
小智学长 | 嵌入式1 小时前
单片机-STM32部分:1、STM32介绍
stm32·单片机·嵌入式硬件
zhbi982 小时前
STM32移植U8G2
stm32·单片机·嵌入式硬件·u8g2
程序猿阿伟2 小时前
《缓存策略:移动应用网络请求的“效能密钥” 》
网络·缓存
Hans_Rudle2 小时前
MSP430G2553驱动0.96英寸OLED(硬件iic)
单片机·嵌入式硬件·msp430
爱奥尼欧3 小时前
【STM32】ADC的认识和使用——以STM32F407为例
stm32·单片机·嵌入式硬件
WenGyyyL3 小时前
《Android 应用开发基础教程》——第十一章:Android 中的图片加载与缓存(Glide 使用详解)
android·缓存·glide
不吃肘击5 小时前
Redis基本使用
数据库·redis·缓存
Waitccy8 小时前
CentOS 7 磁盘阵列搭建与管理全攻略
linux·运维·缓存·centos