打印
[STM32F1]

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

[复制链接]
2014|17
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
通宵敲代码|  楼主 | 2017-1-4 15:21 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 通宵敲代码 于 2017-1-4 15:25 编辑



原文链接:http://bbs.mydigit.cn/read.php?tid=1249525


    这个命题最开始诞生于前段时间玩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;
            *UART1RBUFFP = 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指令后,随即执行此函数,然后将返回的字符串开头的非字符字节和结尾的非字符字节移除,再判断内容并作出相应动作。但是本算法的一个致命缺陷在于,无法随机接收,每次必须执行接收函数,在程序准备好后才能做出有效的接收,如果想处理随机的串口事件,这个算法就显得力不从心了。

沙发
通宵敲代码|  楼主 | 2017-1-4 15:22 | 只看该作者
有些编程基础的朋友应该都清楚,计算机中的字符串是以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;
    }

使用特权

评论回复
板凳
通宵敲代码|  楼主 | 2017-1-4 15:24 | 只看该作者
    理论上好像没什么问题,但我将程序编译后烧录到芯片中并没有得到想要的效果,串口对于接收信息没有任何反应,于是仔细分析程序后发现,由于所有标记变量的初值均为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或其他资源更多、速度更快的单片机,可以尝试定义更大的缓存和编写更复杂的接收缓冲算法,应用范围应该会更广泛。
    以上,个人拙见,欢迎探讨,如果还有哪位有更高明的办法,还请不吝赐教。

使用特权

评论回复
地板
huzi2099| | 2017-1-4 16:32 | 只看该作者
分包无非就两个方法,1用时间间隔 2用特定字符

使用特权

评论回复
5
通宵敲代码|  楼主 | 2017-1-4 18:35 | 只看该作者
huzi2099 发表于 2017-1-4 16:32
分包无非就两个方法,1用时间间隔 2用特定字符

随机迸发情况下,
时间间隔不好控制,
特殊字符倒是可以,
不过就8个字节,容易误判。
上面的讨论中,
也没有涉及丢包、溢出、
等冗余校验措施。

使用特权

评论回复
6
oufuqiang| | 2017-1-4 23:34 | 只看该作者
这个应该要好好利用CRLF,可以很容易断句。就相当于CRLF开始,CRLF结束

使用特权

评论回复
7
mohanwei| | 2017-1-5 08:42 | 只看该作者
搞复杂了。一般用法是上电后哔哩吧啦发一通配置指令,其中往往就包括关闭回显。这个过程都是不需要管接收的。
目的是把不同模块、不同版本搞成同一个配置,后面的主程序好处理。
每次发指令前,先清空串口接收缓冲区。发完后,检查CRLF的个数或超时(你自己知道应答信息是什么样的,以及最大执行时间)

使用特权

评论回复
8
mohanwei| | 2017-1-5 08:45 | 只看该作者
如果你需要看调试信息,就做一根特殊串口线:模块TXD和RXD各反串一个二极管到上位机的RXD。这样在上位机就能看到你发出的指令和接收到的应答

使用特权

评论回复
9
linzx2017| | 2017-1-5 14:35 | 只看该作者
如果你需要看调试信息,就做一根特殊串口线:模块TXD和RXD各反串一个二极管到上位机的RXD。这样在上位机就能看到你发出的指令和接收到的应答

使用特权

评论回复
10
huangcunxiake| | 2017-1-5 17:08 | 只看该作者
每个字符串结束应该有个标志吧

使用特权

评论回复
11
fclmyl2| | 2017-1-5 22:44 | 只看该作者
GSM有的有硬件流

使用特权

评论回复
12
通宵敲代码|  楼主 | 2017-1-6 12:12 | 只看该作者
linzx2017 发表于 2017-1-5 14:35
如果你需要看调试信息,就做一根特殊串口线:模块TXD和RXD各反串一个二极管到上位机的RXD。这样在上位机就 ...

恩,目前在用着串口线

使用特权

评论回复
13
通宵敲代码|  楼主 | 2017-1-6 12:13 | 只看该作者
huangcunxiake 发表于 2017-1-5 17:08
每个字符串结束应该有个标志吧

没有特殊标志位,
都是\r\n,网上也都是判断这个。

使用特权

评论回复
14
通宵敲代码|  楼主 | 2017-1-6 12:14 | 只看该作者
fclmyl2 发表于 2017-1-5 22:44
GSM有的有硬件流

有是有,
不过对单片机来说,
要多占用3、4个引脚,
太浪费了

使用特权

评论回复
15
240011814| | 2017-1-6 17:01 | 只看该作者
dma加串口空闲中断

使用特权

评论回复
16
通宵敲代码|  楼主 | 2017-1-6 19:41 | 只看该作者
240011814 发表于 2017-1-6 17:01
dma加串口空闲中断

恩,是个不错的办法,不过缓冲区容易乱序

使用特权

评论回复
17
huzi2099| | 2017-1-6 20:57 | 只看该作者
通宵敲代码 发表于 2017-1-4 18:35
随机迸发情况下,
时间间隔不好控制,
特殊字符倒是可以,

每次收到数据开启一个定时,每次收到数据复位定时器重新计时,定时时间到就是包结束。
这个间隔时间是要两边有协议的,modbus上是3个半数据周期。
以前用飞思卡尔的MK10串口上有个类似的中断,我也没验证过。

使用特权

评论回复
18
fclmyl2| | 2017-1-7 13:01 | 只看该作者
通过其它方式AT指令先关闭硬件流,之后再用单片机发AT指令控制

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:年轻不是资本,奋斗才是良策!

302

主题

7538

帖子

69

粉丝