分享

关于串口随机接收不确定字符串的讨论

 心不留意外尘 2016-08-03



 
 

    这个命题最开始诞生于前段时间玩SIM900A的时候,接收AT指令的反馈时需要串口接收字符串,根据官方文档,上位机或MCU发送AT指令的格式为“AT+指令+CRLF(换行符)”,返回的字符串也以CRLF结尾。但是实际使用时发现,如果开启回显,模块的返回为“AT指令+CRLF+反馈+CRLF”,如果关闭回显,则返回为“CRLF+返回+CRLF”,头尾各一个CRLF叫我很是头疼,最后,我写了这样一个小函数解决问题。
    以下使用51单片机为例。

    首先,定义全局标记变量:
    BOOL UART1RING = FALSE;

    然后是串口接收的缓冲池与指针:
    BYTE UART1RBUFF[64] = {0};
    BYTE *UART1RBUFFP = UART1RBUFF;

    然后在串口中断服务函数中的接收部分编程如下:
    void UART1INTERRUPT() interrupt 4 using 1
    {
        if (RI)
        {
            RI = 0;
            *UART1RBUFFP++ = SBUF;  //先把BUFF内容赋值到指针所指内存,然后对指针进行自加操作。
            *UART1RBUFFP = 0x00;  //在更新后字符串的尾部添加0x00.
            UART1RING = TRUE;
        }
        if (TI)
        {
            TI = 0;
            UART1SING = 0;
        }
    }

    然后编写如下函数:
    void UART1ReceiveString()
    {
        UART1RBUFFP = UART1RBUFF;
        *UART1RBUFFP = 0x00;
        UART1RING = FALSE;
        while(!UART1RING);
        while(!UART1RING)
        {
            UART1RING = FALSE;
            DelayXms(2);    //延时,等待重新检测标志位
        }
    }

    上面的程序,如果有细心的朋友应该已经看懂了,原理就是设定一个标志位,在每次串口因接收触发中断时,将标志位设定为TRUE,然后检查函数每隔一段时间会检查该变量并重新置为FALSE并延时等待下一次检查,直到检测到这个标记变量为FALSE,则视为接受结束。延时的长度根据波特率而定,实验程序以9600波特率计,9600bps即每秒9600个二进制位,程序设定为八个数据位,一个停止位,无校验位,所以每个字节8位,加上起始位和停止位共10位,也就是每秒960字节。每字节间隔理论值约1.042ms(1000/960),也就是说,延时的长度应该高于每字节传输时间间隔的理论值,串行中断是每接收一字节触发一次而不是每接收一位触发一次。
    指导思想:接收超时,即指定时间内未接受到下一个有效字节,则视为本次接收结束。
    本算法经实际验证可用,每次向SIM900A发送AT指令后,随即执行此函数,然后将返回的字符串开头的非字符字节和结尾的非字符字节移除,再判断内容并作出相应动作。但是本算法的一个致命缺陷在于,无法随机接收,每次必须执行接收函数,在程序准备好后才能做出有效的接收,如果想处理随机的串口事件,这个算法就显得力不从心了。

    有些编程基础的朋友应该都清楚,计算机中的字符串是以0x00结尾的,串口发送字符串时会将这个0x00作为发送结束的标志但并不会发送出来,也就是说,如果有字符串“ABCD\0”,那么串口发送出来的只有“ABCD”。传统的编程方法都是在字符串尾增加一个特定的字符,比如’*’、’#’、”$”之类的,串行中断检测到这些字符后视为一次接收结束。但这个方法的前提在于,上位机的通信程序和下位机的通信程序都由自己编写,用户自定义通信规则。
    但是如果使用通过串行端口通信的已经封装好的模块,问题就来了。首先,模块的通信规则不一定适合你的程序,其次,模块可能根本就没有通信规则可言。所以,真正的问题就是,如何即时接收一个不确定内容的字符串。
    提出这个问题的背景是这样的,最近在玩安信可的ESP8266 Wifi串口模块,想做一个东西,在接收电脑指令后,开始一系列动作,但是任何时间,只要接收到电脑的“停止”命令,都必须要退出主循环,换而言之这个命令的发出是随机的,不可能让程序始终等待电脑发出一个“停止”指令。由于命题的定义是一个“不特定内容”的字符串的随机接收,那么,我们还是保留之前的指导思想:接收超时。这次的程序改进在于对串口接收的监视已经不在主程序中,而是使用了一个定时器,于是程序变成了这样。

    首先,追加定义全局标记变量:
    BOOL UART1REND = FALSE;

    增加的变量UART1REND为串口接收的停止标记,在监视定时器判定接收结束后置为TRUE。然后串口中断的服务函数变成了这个样子。
    void UART1INTERRUPT() interrupt 4 using 1
    {
        if (RI)
        {
            RI = 0;
            if(!UART1REND)
            {
                *UART1RBUFFP++ = SBUF;
                *UART1RBUFFP = 0x00;
                UART1RING = TRUE;
            }
        }
        if (TI)
        {
            TI = 0;
            UART1SING = 0;
        }
    }

    然后是监视用定时器,定时器的配置这里不再赘述,请参见STC官方数据文档,定时器的时间间隔请参照上文中对延时的分析。
    这里我们以定时器T0为例:
    void T0INTERRUPT() interrupt 1 using 1
    {
        if(UART1RING) 
        {
            UART1RING = FALSE; 
        }
        else
        {
            UART1REND = TRUE; 
        }
    }

    看到这,大家应该也明白了,这个方法其实就是将之前等待和监视串口接收的任务交给了定时器来处理,这样,主程序可以继续自己的任务,无需等待串口接收,当需要串口数据时,只需要访问UART1REND,当值为TRUE时,代表完成了一次接收,这时候我们就可以处理接收的数据了。在每次接收处理完成后,执行下面的子程序,就可以准备进行下一次接收。
    void ReadyToReceive()
    {
        UART1RBUFFP = UART1RBUFF;
        *UART1RBUFFP = 0x00;
        UART1REND = FALSE;
    }

    理论上好像没什么问题,但我将程序编译后烧录到芯片中并没有得到想要的效果,串口对于接收信息没有任何反应,于是仔细分析程序后发现,由于所有标记变量的初值均为FALSE,而定时器T0在程序起跑后就开始产生中断并对标志位进行处理,如果从头读一遍程序,就会发现,其实单片机一上电,程序就认为已经完成了一次接收,开始进行其他处理了,进而导致所有标志位都是错的,数据处理也就无从谈起。
    
    知道了原因就好办了,我们可以只在串口有接收时对串口进行监视,于是程序有了如下变化。
    
    追加定义全局标记变量:
    BOOL UART1MNTR = FALSE;
    只有这个变量为TRUE时,才开始对串行端口的监视。
    
    于是,串口中断服务函数有了如下变化。
    void UART1INTERRUPT() interrupt 4 using 1
    {
        if (RI)
        {
            RI = 0;
            if(!UART1REND)
            {
                UART1MNTR = TRUE;
                *UART1RBUFFP++ = SBUF;
                *UART1RBUFFP = 0x00;
                UART1RING = TRUE;
            }
        }
        if (TI)
        {
            TI = 0;
            UART1SING = 0;
        }
    }

    相应的,监视定时器的程序变化如下:
    void T0INTERRUPT() interrupt 1 using 1
    {
        if(UART1MNTR)
        {
            if(UART1RING)
            {
                UART1RING = FALSE; 
            }
            else
            {
                UART1REND = TRUE;
                UART1MNTR = FALSE; 
            }
        }
    }

    问题解决了,在串行中断被触发的时候,标志位UART1MNTR被置为TRUE,定时器开始对串行端口进行监视,当判定串行通信结束后,标志位UART1MNTR被置为FALSE,定时器就不再对此端口进行监视,如此一来,程序的逻辑就完整了。
    如果需要对串口数据进行处理,只需要访问UART1REND标志位,如果为TRUE,则表示已经存在完成的接收,在处理完成后调用ReadyToReceive()子程序则可以进入下一次接收准备。如果对接收的实时性还有进一步要求,可以在监视定时器中编入相应代码,对UART1REND标志位进行判断与处理,在监视定时器中的监视程序就是简单的位判断,如果转为汇编,不过就是几条简单的JB/JNB和MOV,可利用空间还有很大。
    这个方式的缺点就是需要占用一个额外的定时器资源,但是一个定时器资源是可以监视多个串口的,附件中的示例程序基于STC15W4K48S4单片机,22.1184MHz时钟频率,串口1占用定时器T1作为波特率发生器,串口2占用定时器2作为波特率发生器,定时器T0作为串口监视器,程序实现的功能就是将串口1接收的内容通过串口2转发出去,串口2接收的内容通过串口1转发出去。另外,串口1和串口2的中断优先级最好置高,其它中断置低,否则可能在接收数据时丢帧,此现象波特率高时尤为明显。
    如果使用STM32或其他资源更多、速度更快的单片机,可以尝试定义更大的缓存和编写更复杂的接收缓冲算法,应用范围应该会更广泛。
    以上,个人拙见,欢迎探讨,如果还有哪位有更高明的办法,还请不吝赐教。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多