如何判断串口接收完成一帧数据

1 使用定时器判断

这种方式建立在两帧数据不可能连续发送的基础上,也是modbus判断帧结束的方式,在接收到第一个字节的时候打开定时器,如果继续接收到数据则更新定时器,在被设定时间内没有接收到数据则定时器超时。


enter image description here

关于定时器的设定时间有这样几个问题

  • 其一是如果定时器超时时间大于发送两帧数据的时间间隔,则接收到的一帧数据实际上是几帧,更可能定时器无法超时,一直处于接收状态。
  • 其二是如果定时器超时小于发生两个字节的时间间隔,则在接收到1个字节定时器就超时了。
    我们于是只能设定一个尽量小但又不影响接收连续字节的时间,例如ModBus通信时规定发送完一组命令必须间隔3.5个字符时间间隔再发送下一组新命令,这里规定的便是定时器的超时时间。

关于时间的计算

首先,1个字符窗口包含起始位,数据位,校验位,停止位,其中有些位长度不一定,这里我们按1+8+1+1来计算。波特率表示的意思是在1000ms内可以传送的位数,设3.5个字节所用时间为X,波特率为9600则:
3.5*11 / X = 9600 / 1000
X = 4.010416666666667 ms
X代表的意思是两帧数据间隔时间至少为此,我们程序的超时定时器可以设定为4ms。同时也知道波特率变化是会影响该值。

示例

这里使用的是STM32F103单片机,没有使用操作系统,使用的串口1和定时器3的通道1,这样定时器还能被用来干其他事,而不是完全被串口绑定。方式和上文有一点点差距,也就是在接收到非首字节数据时,不是重置定时器,而是更新通道超时的时间。通道1使用的是输出比较,也就是定时器计数达到这个比较值就会发生中断,我在接收数据时不断修改这个比较值,达到和更新定时器同样的目的。
下列便是相关代码,其中的fifo操作见另一篇文章

  • 标志位
static uint8_t u1_Data_Start_Recive_Flag=0;//标志数据开始接收
static uint8_t u1_Data_recive_Flag=0;//标志数据正在接收
  • 串口1初始化
void usart_init(int buad)
{
  //GPIO端口设置
  GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
     
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
    USART_DeInit(USART1);                                   //复位串口1

    //USART1_TX   PA.9
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;               //PA.9
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;         //复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure);                      //初始化PA9
   
    //USART1_RX   PA.10
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;   //浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);                  //初始化PA10

   //Usart1 NVIC 配置
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;//抢占优先级3
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;      //子优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;         //IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                         //根据指定的参数初始化VIC寄存器
  
   //USART 初始化设置
    USART_InitStructure.USART_BaudRate = buad;                  //一般设置为9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
    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_Rx | USART_Mode_Tx; //收发模式

  USART_Init(USART1, &USART_InitStructure);         //初始化串口
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);    //开启中断
  USART_Cmd(USART1, ENABLE);                        //使能串口 
    FifoInit( &usart1_recive_fifo, usart1_recive_buffer, 200 );
}
  • 定时器初始化
    这里设定的计数器自动填充值和预分配可以按照整体需求来配置,当然得大于准备设定的区分字节间隔的最小时间
void timer3_init()
{
            TIM_OCInitTypeDef  TIM_OCInitStructure;
            TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
            NVIC_InitTypeDef NVIC_InitStructure;
            
        //  RCC_PCLK1Config(RCC_HCLK_Div1); 
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

            TIM_TimeBaseStructure.TIM_Period = 0xfffe; //设定计数器自动重装值 
            TIM_TimeBaseStructure.TIM_Prescaler =999;   //预分频器    72000
            TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
            TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
            TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
        //  TIM_PrescalerConfig(TIM1, PrescalValue,TIM_PSCReloadMode_Immediate);
            //输出比较时间模式配置:通道1
            TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Timing;
            TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
            TIM_OCInitStructure.TIM_Pulse = 100;
            TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
            TIM_OC1Init(TIM3, &TIM_OCInitStructure);
            
            //TIM_OC1PreloadConfig(TIM2,TIM_OCPreload_Disable);//禁止预装载使能
            //TIM_ITConfig(TIM2,TIM_IT_CC1,ENABLE);//使能中断
            //TIM_ITConfig(TIM2,TIM_IT_CC2,ENABLE);//使能中断
            //TIM_Cmd(TIM2,ENABLE );    //使能定时器 
            
             TIM_ClearITPendingBit(TIM3, TIM_IT_CC2);
             TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);//清楚中断标识位
             
             NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
             NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级
             NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;     //子优先级
             NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;            //IRQ通道使能
             NVIC_Init(&NVIC_InitStructure);
}
  • 串口1中断服务程序
    该函数是库函数提供,事件的中断函数
void USART1_IRQHandler(void)                    
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
    {
        usart_irq_free(1);
        if( FifoPush( &usart1_recive_fifo, (char)(USART_ReceiveData(USART1)) ) == 0 ) //存入数据
        { 
        }
        else
        { 
            //error
        }   
  } 
}
  • 状态切换函数,在上面的系统中断被调用
void usart_irq_free(uint8_t usart)
{
   switch(usart)
     {
       case 1:
                if(!u1_Data_Start_Recive_Flag && !u1_Data_recive_Flag) //需要获取数据且还未开始接收
                 {
                        usart_set_recive_start(1);
                 }
                 else if(!u1_Data_Start_Recive_Flag && u1_Data_recive_Flag) //需要获取数据且正在接收
                 {
                        usart_set_recive_ing(1);
                 }
            break;
}
  • 当接到首字节
    这里获取当前定时器计数值,然后设定匹配为2000个计数,这里我设定了一个比较大的值,在定时器初始化是进行了千分频,这里时间间隔约为27.7ms,当然是我随意设定的一个较大的值,更加需求修改即可。
void usart_set_recive_start(uint8_t usart)
{ 
      if(usart ==1)
        {
            FifoFlush(&usart1_recive_fifo);
            u1_Data_Start_Recive_Flag = 0;
            u1_Data_recive_Flag = 1;
                 
            TIM_Cmd(TIM3, ENABLE);//开启定时器
            TIM_ITConfig(TIM3,TIM_IT_CC1,ENABLE);
            TIM_SetCompare1(TIM3, TIM_GetCounter(TIM3)+2000); // 
        }
}
  • 数据接收中状态
    更新定时器匹配值,放置在接收图中定时器中断
void usart_set_recive_ing(uint8_t usart)
{
    if(usart ==1)
    {
         u1_Data_Start_Recive_Flag = 0;
       u1_Data_recive_Flag = 1;
       TIM_SetCompare1(TIM3, TIM_GetCounter(TIM3)+2000);
    }
}
  • 定时器中断
    库函数提供,当发生中断时也就是一帧数据接收完成时,这里进行状态切换,和调用回调,告诉上层完成了一帧数据的接收
void TIM3_IRQHandler(void)   //TIM3中断
{
    if(TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
    {
         TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);  //清除TIMx更新中断标志
     usart_set_recive_end(1);
         if(usart1_irq_callback !=NULL)
         {
             usart1_irq_callback();
         }
         usart_start_recive(1);
    }
    if(TIM_GetITStatus(TIM3, TIM_FLAG_Update) != RESET)
  {
     TIM_ClearITPendingBit(TIM3, TIM_FLAG_Update);
     //Timer3_Update_IRQ();  
  }
}
  • 一帧数据接收完成切换状态
void usart_set_recive_end(uint8_t usart)
{
      if(usart ==1)
        {
            TIM_Cmd(TIM3, DISABLE);
            TIM_ITConfig(TIM3,TIM_IT_CC1,DISABLE);
            USART_Cmd(USART1, DISABLE);
            u1_Data_Start_Recive_Flag = 1;
            u1_Data_recive_Flag = 0;    
        }
}
  • 帧完成回调函数
    定义函数指针
void (*usart1_irq_callback)(void);

通过接口函数让外部对其赋值

void set_usart1_irq_callback( void (*callback)(void) )
{
   usart1_irq_callback = callback;
}
  • 串口的数据发送(和本文没啥关系)
void usart_send_buffer(uint8_t usart,uint8_t *data,int len)
{
      int i=0;
    switch(usart)
        {
            case 1:
                 USART_Cmd(USART1, ENABLE);
                 for(i=0;i<len;i++)
                 {
                        USART_SendData( USART1,(unsigned char) data[i]);
                        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
                        USART_ClearFlag(USART1, USART_FLAG_TXE);
                 }
                 //USART_Cmd(USART1, DISABLE);
                break;
    
        }
}
  • 外部调用
    每当接收到一帧数据则function_callback函数被调用
usart1_init(); 
timer3_init();
set_usart1_irq_callback(function_callback);

2 解析协议方式

该方式便是通过协议中的保存的长度、包头、包尾等信息来判断帧接收完成。通常是将数据保存在一个循环buffer中,从中去找对应信息,根据信息判断取出一帧数据还是继续等待。

3 空闲中断

在STM32中有个串口空闲中断,在总线由忙碌转为空闲时(RXNE被置为)参数这个中断,我们可以利用这个中断来知道一帧传输接收,需要进行处理。
下面是SMT32L151的示例代码,首先在串口初始化时,也使能空闲中断

USART_ITConfig( uart, USART_IT_IDLE, ENABLE );

然后在对于的中断函数中进行处理,其中清除中断标志位根据手册所示,对SR、DR进行读操作,最后我调用回调函数来将帧接收完成的事告诉外层。

void USART1_IRQHandler( void )
{
 unsigned char temp;
···
        if(USART_GetITStatus(USART1,USART_IT_IDLE)==SET)
        {
             temp=USART1->SR;
             temp=USART1->DR; //清除标志位
             //完成接收
             if( UartIdel[0] != NULL )
             UartIdel[0]();
        }
···
}

被关联的回调函数,用来测试该功能,接收到什么就发送什么

void SerialBoardIdelCallback()
{
     char tempBuffer[10];
     uint8_t len=GetSerialBoardReciveDataSize();
     if(len<=10)
     {
       GetSerialBoardReciveData(tempBuffer,len);
         SerialBoardSendData((unsigned char *)tempBuffer,len);
     } 
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容