分享几个单片机接收AT指令响应数据的方法


  哈哈,好久没有更新博客了,前段时间忙着项目,想着写点东西的,结果老是忙.
  今天忙里偷闲就给大家分享几个单片机接收通讯模组AT指令响应数据的方法吧!


  小故事:去年刚到这里的时候,有个很同事问我,你知道串口如何接收完成一整包数据吗?当时我的做法是,在串口中断中计数的形式,上一次和本次的计数值相同,则认为接收完成,这种方式也存在这下面的问题,可以看一下。


前言

  众所周知,现各厂家的通讯模组,大都采用AT指令交互的模式,当然,最近兴起了OpenCPU开发的热潮,但是OpenCPU对于个人用户,通讯模组厂家一般是不作技术支持的(某宙除外),而且,个人用户也很难拿到通讯模组的SDK代码,OpenCPU代码的上手难度以及开发难度都要大于AT指令开发。
  AT指令开发,对于大部分嵌入式软件工程师,或者说单片机工程师,玩过通讯模组的应该都不陌生,通过串口助手发AT得到OK已经成为了测试一块通信模组的第一个步骤,但是对于单片机如何解析这个“OK”,刚入门的新手还是有些模糊的。
  下面我就介绍三种接收一帧AT指令返回的方式,并附上相应的代码,以供各位看官了解其中的奥秘。

  1. 串口接收中断 + IDLE中断
  2. 串口接收中断 + 定时器中断
  3. 串口接收中断 + “\r\n”

  上述三种方式,是我现在用的比较多的方案,但是方案1是我比较不推荐的方式,因为有些模组的返回,并不一定是连续的返回,可能中间会有一定的间隔时间,本人测试过的模组,大都有过这个现象,只是每个模组的概率不等,但是一旦在某个比较重要的帧上出现了这个错误,可能就会影响到后续的程序运行。这里比较推荐方案2,方案2可以经过测试,寻找到一个相对稳定的超时时间,配合串口中断,准确的把一帧数据接收完成。其次是方案3,方案3也是RTOS的AT组件用的最多的一种方式,这里只需简单的处理,就能达到RTOS的效果,但是对于某些场景的AT返回,处理起来也稍微麻烦了点,这个问题等会在下文会详细解释。

  在正文开始前,我们先来看一下现在通讯模组的AT通讯规范吧:

  AT指令手册中,我们经常会看到一些奇怪的符号,例如: “CR”、“LF”、"<…>“和”[…]"等,对于刚刚接触通讯模组的玩家来说,会有些疑惑,这些奇形怪状是符号是啥意思,甚至有些对模组把玩过很久的玩家来说,也是一知半解。就我接触到的客户群体中,大部分的工程师是不看你的技术手册的。。。
  下面对这些奇怪的符号做个解释(适用于大部分模组):

    CR          回车符
    LF          换行符
    <...>       参数名称(实际填写不需要<>)
    [...]       可选参数(实际填写不需要[])

  然后是AT指令发送语法(适用于大部分模组):
  命令必须以AT或at作为开头。通常一个指令的结构为

    AT+CMD=param1,param2\r\n

  指令发送完成后,通讯模组会根据此指令的执行结果,反馈一个结果,比如: “OK”、“ERROR”、XXX ERROR: 等。就是本文讲述的重点: AT指令响应
  一般的AT指令响应格式为:

    \r\nOK\r\n
    \r\nERROR\r\n
    \r\n+CMD: param1\r\n

  看到这里是不是有一股想把设计AT规范的工程师打死,为什么我给你的指令,就一个\r\n,但是你给我的至少有两条\r\n(可能一个回复里会有4组或6组\r\n),为什么不写在同一行,当初我也是这样想的。
  哈哈哈。

  费了半天话,我们开始进入今天的正题。
  插个嘴,我想按照从简到烦的方式来写,没人会反对吧。


串口中断 + \r\n

  emmm…
  想不起来怎么写了,对了j就以RTOS的AT组件为例,RTOS的AT组件是 每行结束 就进行一次判断,也就是说,每遇到一个 \r\n 就要判断一次,对于无用的消息就丢弃,有用的消息就保留下来(如果需要的话),然后根据本消息的类别,进行反馈,或执行成功得到期望返回、或指令未执行、或指令执行失败又或者是URC列表注册的消息等等。
  但是裸机运行的话,很难做到RTOS般完美,除非移植AT组件,但是,有点小烦,所以就有了下面的这个方案:

  ~懒得画流程图了~
  口述一下整体解决流程:
  串口中断接收AT指令响应,每次判断接收缓冲区中是否含有 \r\n 并且判断当前缓冲区字符长度是否为2(为2的话就表示是响应中的第一个\r\n,这里我们不要他),为2则将长度置0,这样下次进入接收时,就会将前两个空间覆盖掉;若非2时,则说明,后一个\r\n到了,也就是带响应体的一帧(一行更好点)结束了,就可以愉快的判断是否得到了你的期望,或者执行成功失败还是URC等等。

  嗯,就是这么简单,连代码都只有几行。。。

//工程里摘出来的,不大修了
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART3)
    {
        if(scp5_info.rx_cnt < RX_BUFF_SIZE)
        {
            scp5_info.rx_buffer[scp5_info.rx_cnt++] = nb_data;
            if(strstr((char *)scp5_info.rx_buffer, "\r\n") !=  NULL && scp5_info.rx_cnt == 2)
            {
                scp5_info.rx_cnt = 0;
            }
            else if(strstr((char *)scp5_info.rx_buffer, "\r\n"))
            {
                deal();
            }
        }
        else
        {
            deal();
        }
        HAL_UART_Receive_IT(&huart3, (uint8_t *)&nb_data, 1);
    }
}

  嗯,后续的事就是判断你的缓冲区里的数据,是否是你的期望返回、ERROR代码或者URC数据,灰常简单。

  忘记说了,这个方案对于某些你需要获取某些参数,但是却没有对应头部的场景,处理起来要稍微麻烦一丢丢,但是也可以处理(可能是我写的太烂)。
  这个现象的原因是因为一次响应,包含多个数据,例如:

    123456789012345

    OK

  对于这种结构的返回,就需要谨慎处理了(此例是读IMSI指令AT+CIMI的返回格式)。


串口中断 + IDLE中断

  其实这呢,是我极不推荐的一个方式,弊端在上面已经说了,任何模组都或多或少会有这个问题,为了你产品的稳定性,还是放弃这个吧,而且不是所有单片机都有IDLE中断的呀!
  他万一就在你测试的时候没问题,实际使用的时候,就出现在了你代码没有处理过的地方呢。
  如果事情有变坏的可能,不管这种可能性有多小,它总会发生。
  不喜勿喷哦,喜欢用的也可以用。
  大不了出问题提桶跑路,然后骂一句又**的黑厂,跑路。
在这里插入图片描述

  所以这里就不做相关的介绍了,因为我的代码也没用过这种方式。(如果IDLE的间隔可配置的话,那就很棒了)


串口中断 + 定时器

  这种方式是我之前用的比较多的方案了,变相的扩展了IDLE的间隔时间达到我的要求,原理和IDLE中断是一样的,只是这个间隔时间,可以自由配置。

  大概讲下原理,就是串口在接收到第一帧数据的时候,打开定时器(要事先配置的!!!),后续进入串口接收的时候,将定时器计数值清0。在定时器中断里,判断是否超时,超时就认为接收完成,并关闭定时器中断,之后处理这次的数据就可以了。。
  这个方法也很简单,只是比上两个多配置个定时器,所以把他作为最复杂的一个。
  下面给出参考代码:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART3)
    {
        if(scp5_info.rx_cnt < RX_BUFF_SIZE)
        {
            __HAL_TIM_SET_COUNTER(&htim6, 0);
            if(scp5_info.rx_cnt == 0)
            {
                __HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
                HAL_TIM_Base_Start_IT(&htim6);
            }
            scp5_info.rx_buffer[scp5_info.rx_cnt++] = nb_data;
        }
        else
        {
			HAL_TIM_Base_Stop_IT(&htim6);
            deal();
        }
        HAL_UART_Receive_IT(&huart3, (uint8_t *)&nb_data, 1);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM6)
    {
        __HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
        HAL_TIM_Base_Stop_IT(&htim6);

        deal();
    }
}

  此方案的优势有:

可以避免第一种方案的某些无特定头部的返回
可以最大限度的避免IDLE中断可能引发的接收不完整现象

  当然,此方法也存在一定的缺点,就是可能会导致URC与URC之间的粘包和AT指令响应与URC之间的粘包,这个我们后面的文章再进行讨论。


  emmm,其实这些方法适用很多场合,只是我目前接触到的很少,用的最多的就是解析模组返回了。
  算是全篇废话了,重点只有一丢丢。。。


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

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页