一种表驱动法解析URC消息的实现


  看完我昨天发的博客的朋友应该了解了几种接收模块响应的方法,如果没看过,可以点击分享几个单片机接收AT指令响应数据的方法先查看下。

前言

  今天我要分享一种单片机中分离URC数据的方法,和上文废话连篇不同的是,这次全篇干货哦!
  文章涉及到的表驱动法,可以说是本次的精髓,这里不对表驱动法做过多解释,有兴趣的朋友可以自行查阅下相关资料,我这里只是用它实现个功能罢了,这个设计方法真的很香。
  在表驱动法中还带有URC之间的粘包处理和命令回复与URC之间的粘包处理。

  为什么要做这个呢?主要是因为,之前刚毕业时,买淘宝的板子,发现他们的代码都是写死的,就是说,发AT给模块,模块本身是回OK的,但是他们的实现方法是不等OK而是等某个URC到了之后,再进行下一步,说实话太捞了,效果如下:

    AT          (主机发送)
    OK          (命令返回)

    +CMD1: XX   (URC1)
    +CMD2: XX   (URC2)
    ...
    +CMDn: XX   (URCn)

  在发送AT之后,等待的不是OK,而是某个URCn,这样做在经过调试之后确实可以达到目标效果,但是将AT响应与URC混在一起,是我极不喜欢的。
  将各部分分离之后,可以有效的管理模块的运行状态,比如在测试时,需要长期测试模块的稳定性,那我发完AT之后,得不到URCn,但是OK已经给我了,我当然就不可能认为,这个AT步骤是有问题的,而且,这样也很难定位到为题所在,中间的URCx在哪?


URC

  下面进入正题,让我们先看下,什么叫做URC?
  咳咳,百度没找到对应的词条,那我来翻译下吧

    URC: Unsolicited Result Code  

  可以翻译为未经请求的结果码,或者未请示的结果码。
  那么这是啥意思呢,和发送AT返回OK的问答模式不同,URC是主机也就是单片机未向模块发送指令,但是模块主动向主机发送某些具有特定含义的指令的一个形式。
  由于通讯模块每种协议的复杂度不同,厂商封装的AT指令各有差异,或在登录云平台时,模块下发URC告知主机当前运行到了哪一步骤,或网络状态发生改变时,模块主动下发指令通知主机,又或是平台下发数据,模块转发给主机等等等等。可以说除了AT指令问答交互,其余消息全部是通过URC来完成。
  看到没有,这么多的URC消息,各个含义还都不同,让我如何是好呢?不着急,让我们一步一步的往下分析。
  首先,要确定URC消息格式,一般的通讯模组URC格式如下:

    +CMD1: param1,param2\r\n
    +CMD2: param1,param2\r\n
    ...

  没错,这样看起来还蛮简单的。但是我在程序里怎么处理这些数据呢,细心的朋友已经发现,URC消息都时候以 +XXX为起始的,那我找到这个 +XXX不就找到这条数据了吗?对了!!!找到这个头部就成功一半了,这个也是我今天要讲述的重点,表驱动法实现解析URC消息

表驱动法

  何谓表驱动法?表驱动法(Table-Driven Approach)简而言之就是用查表的方法获取数据。此处的“表”通常为数组,但可视为数据库的一种体现。(抄的)
  其实我们都使用过表驱动法的实例。举一个大家都用过的东西,字典,根据字典的部首表检索字的拼音未知的汉字就是我们常见得表驱动法,即以某个字的字形为依据,计算出一个索引值,并映射到相应的页码。相比一页一页的翻看字典,部首检字法效率极高。
  现实生活还有好多例子,例如超市货架分类、菜市场分区、包括键盘都有表驱动法的身影。
  对应到编程方面时,在当数据不多的时候,可以用(if…else)或(switch…case)来完成,但是当数据量开始变大时,逻辑判断语句会越来越多,导致代码越来越繁杂,此时表驱动法的优势就显现出来了。
  表驱动法本身有很多方式实现,但是这里由于博主经验有限,没专业看过相关书籍,数据结构及算法都是按自己常用的方案来做的,有些不对的地方还请各位多多指教。

  

实现方案

  这里我以EC616系列模组连接CTWing为例,EC616在对接CTWing平台时,有几条必要的URC消息需要我们处理,分别如下

    +CTM2MRECV: <len>,<data>\r\n
    +CTM2M: <operation>,<status code>...\r\n
    +CTM2MCMD: <msgid>,<cmdtype>,<token>,<uri_str>...\r\n

  因为URC上述2、3两条URC消息参数过多,仅保留前面几个参数(参数啥的不是今天的重点)。
  这时候根据上面的说法,我只要找到前面的几个URC头部就可以了,是的,这就是今天的任务。但是,在第二条消息体里 <operation> 参数是不是一个定值,具有多个选择的一个参数,参数可取值如下:

    reg
    obsrv
    update
    ping 
    dereg
    send
    lwstatus

  也就是说,第二条URC实际上又可以分成7条不同的URC,虽然,我也可以把第二条消息分解成7份,只建一个表格,进行维护,但是为了形象的表示,主体URC只有3条,我又多建了一个表格,单独维护 +CTM2M: 的<operation>字段。
  分析完本次要解析的URC之后,下面就要开始对表格的数据结构进行规划,因为URC处理只需要查找对应的索引,所以,数据结构的建立也很简单,我这里以此方式建立结构体:

typedef struct{
    char *cmd;
    void (*CmdCallback)(void);
}Type_CmdCallback;

  数据结构建立的非常简单,一个关键字加上对应的回调函数即可。当然也可以将此数据结构再进行优化,例如:

typedef struct{
    char *cmd;
    void (*CmdCallback)(char *urcbuf, uint32_t urc_len, void *arg);
}Type_CmdCallback;

  若以第二种方式建表的话,不需要维护特意urc全局数组,在判断是谁索引后,直接将当前的urc数据传入回调函数中即可,并且预留一个空指针,以供用户使用。
  为了简单说明,我这里以第一种方式建立维护整体URC的表格:

Type_CmdCallback urc_cmd_table[] = {
    {"+CTM2MRECV:",     ctm2mrecv_handle    },
    {"+CTM2M:",         ctm2m_handle	    },
    {"+CTM2MCMD:",      ctm2mcmd_handle	    },
};

  至于 +CTM2M: 的分支表格,这里不作啰嗦,直接给出相应的表格:

Type_Ctm2mCallback urc_ctm2m_table[] = {
    {"reg",             ctm2m_reg_handle        },
    {"obsrv",           ctm2m_obsrv_handle      },
    {"update",          ctm2m_update_handle     },
    {"ping",            ctm2m_ping_handle       },
    {"dereg",           ctm2m_dereg_handle      },
    {"send",            ctm2m_send_handle       },
    {"lwstatus",        ctm2m_lwstatus_handle   },
};

  在 +CTM2M: 的回调函数中,按照同样的方法判断urcbuf存在的分支索引,继续再进行数据处理就可以了。
  根据上次我们学习的如何接收模组响应的一整包数据,在串口认为接收完成以后,开始进行判断,当前的响应是AT指令的期望返回还是错误代码,如果都不是,则认为此次是URC数据,就将urc_flag置位,并将当前数据包复制到urcbuf中去。在urc_loop函数中,不停的判断urc_flag是否被置位,若被置位就开始查找,表格中的cmd字段是否在urcbuf中,若在,则根据不同的索引,进入注册的回调函数。(说的有点乱)
  ~ps: strstr是查找b字符串在a字符串中第一次出现的位置.~

void urc_loop(void )
{
    uint8_t _cmd_index = 0 ;

    if(scp5_info.urc_flag == 1)
    {
        while(strstr((const char *)scp5_info.urc_rxbuf, urc_cmd_table[_cmd_index].cmd) == NULL)
        {
            _cmd_index++;
            if(_cmd_index >= 3)
                goto next;
        }
        urc_cmd_table[_cmd_index].CmdCallback();
        scp5_urc_clear();
    }
next:
    ;
}

  以上就是在urcbuf中查找所注册过的urc头部的办法,当然,这个办法有局限性,比如在使用超时中断来判断接收完成时,可能会将两条消息当成一条消息,这样找到的只有在表格中注册的靠前的那一条urc,靠后的一条就无法找到了,这就是后面要提到的urc与urc之间的粘包拆包问题。
  上述现象如下:

    +CTM2M: XXX
    +CTM2MCMD: XXX

  若遇到此现象,则上述办法无法生效,只能检测到表格中的第一条消息,第二条消息无法感知,此时,则需要另寻办法对数据包再次解析,以确保,消息不会丢掉。
  因为一般的粘包最多会在两条urc之间产生,所以暂时不考虑三个以上的场景,我的做法如下:

void urc_loop(void )
{
    int _cmd_index = 0 ;

    if(scp5_info.urc_flag == 1)
    {
        while(strstr((const char *)scp5_info.urc_rxbuf, urc_cmd_table[_cmd_index].cmd) == NULL)
        {
            _cmd_index++;
            if(_cmd_index >= 3)
            goto next;
        }
        urc_cmd_table[_cmd_index].CmdCallback();
        if(++_cmd_index > 3)
            goto clear;
        while(strstr((const char *)urc_buf, urc_cmd_table[_cmd_index].cmd) == NULL)
        {
            _cmd_index++;
            if(_cmd_index >= 3)
                goto clear;
        }
        urc_cmd_table[_cmd_index].CmdCallback();
clear:
        scp5_urc_clear();
    }
next:
    ;
}

  处理方法,很简单,只是做了重复的工作,但是当初实现时,却是在++_cmd_index上花费了不少功夫(大坑),不过,如果你使用的是前文中的 串口中断+\r\n 形式,则不会出现这个问题,这就是需要取舍的地方。
  对于AT指令响应和URC之间的粘包问题,方法和此处类似,只是在判断完AT响应后,需要再次判断数据包中是否含有URC部分,再进行处理。这里不再详细说明。
  从这里就可以看出,表驱动法的好处:

1、提高程序的可读性。一个消息如何处理,只要看一下驱动表就知道,非常明显。
2、减少了重复代码。程序不再含有超级多的if…else…。
3、可扩展性。若要新增某一类似功能,只需要在urc表中将其头部注册上去并完善回调函数即可。
4、降低了复杂度。


  说是全篇干货,结果到最后又是废话连篇,希望以后在文章写得多的时候,会有些进步吧!


  如有问题,欢迎在下方评论留言。

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值