AT-Command/docs/Expert.md
2022-12-31 22:42:31 +08:00

20 KiB
Raw Blame History

高级教程

了解完一般命令处理之后下面我们来讲一下一些特殊场景下的AT命令请求及其处理方式以及了解处理这些请求涉及到的相关概念和接口同时我也会详细说明有关URC消息处理办法和在OS上的应用。

本节导读如下:

  • 组合命令处理
  • 自定义AT作业
  • URC消息处理
  • 多实例并存
  • AT作业上下文
  • OS应用之异步转同步
  • 内存监视器

组合命令处理

在一些场景下主机与从机之前间需要通过组合命令来交换信息即主机与从机间完成一次业务需要进行多次命令交互例如sms收发socket数据收发不同模组产商命令可能是不一样的下面是几个组合命令的例子。

发送短信流程(参考SIM900A模组)

主机  =>
      AT+CMGS=<Phone numer> + \x1A     //发送目标手机号+ctrl+z
从机  <=
      '<'                              //从机回复提示符'<'
主机  =>   
      <sms message>                    //发送短信内容
从机  <=
      OK                               //从机回复OK

发送Socket数据流程(参考移远EC21模组)

主机  =>
      AT+QISEND=<connectID><send_length> 
从机  <=
      '<'                              //从机回复提示符'<'
主机  =>   
      <data>                           //发送二进制数据内容
      \x1A                             //发送CTRL+z启动发送
从机  <=
      OK                               //从机回复OK

发送TCP Socket数据流程(参考Sierra 模组)

主机  =>
      AT+KTCPSND=<session>,<send_length> 
从机  <=
      'CONNECT'                        //从机回复提示符'CONNECT'
主机  =>   
      <data>                           //发送二进制数据内容
      "--EOF--Pattern--""              //发送结束符
从机  <=
      OK                               //从机回复OK

接收TCP Socket数据(参考Sierra 模组)

主机  =>
      AT+KTCPRCV=<session_id>,<recv_length> 
从机  <=
      'CONNECT'                        //从机回复提示符'CONNECT'
      <data>                           //二进制数据内容
      "--EOF--Pattern--""              //结束符      
      OK                               //状态码

组合命令与一般AT命令最大的不同体现在命令交互的流程结构上它们进行一次通信(数据)业务时需要交互2次或多次以上所以软件设计需要考虑并发冲突的可能因为在进行某项业务交互时如果需要进行两次AT命令请求才能完成有可能在进行第一次时过程中就被其它命令穿插打断了从而造成该项业务执行失败。

对于这个问题除了利用OS的锁解决之外还可以利用自定义作业来处理它允许你在一个AT作业中进行多次命令交互同时能够让应用层自行控制命令收发流程。

自定义AT作业

自定义AT作业使用at_work_t表示,它实际是一个状态机轮询程序,通过'at_env_t'参数提供了基本的状态变量数据收发超时管理等接口。除了能用来收发命令之外你甚至可以用它来控制硬件IO,例如有时序控制要求开关机操作,这样做也有利于处理设备状态同步的问题。

原型定义如下:

/**
 *@brief   AT作业轮询处理程序
 *@param   env  AT作业的公共运行环境包括一些通用的变量和进行AT命令通信所需的相关接口。
 *@return  作业处理状态, 它决定了是否在下一个循环中是否继续运行该作业。
 *     @arg true  指示当前作业已经处理完成可以被中止同时作业的状态码会被设置为AT_RESP_OK。
 *     @arg false 指示当前作业未处理完成,继续运行。
 *@note   需要注意的是如果在当前作业中执行了env->finish()操作,则作业立即终止运行。
 */
typedef int  (*at_work_t)(at_env_t *env);

其中: at_env_t 定义了一些通信上下文环境相关接口与公共状态变量,通过它你可以实现自己的通信交互逻辑。

/**
 * @brief AT作业公共运行环境
 */
typedef struct at_env {
    struct at_obj *obj;      
    //公共状态(根据需要添加),每次新作业启动时,这些值会被重置
    int i, j, state;     
    //附属参数(引用自->at_attr_t)  
    void        *params;
    //设置下一个轮询等待间隔(只生效1次)
    void        (*next_wait)(struct at_env *self, unsigned int ms);
    //复位计时器
    void        (*reset_timer)(struct at_env *self);               
    //作业超时判断
    bool        (*is_timeout)(struct at_env *self, unsigned int ms);
    //带换行的格式化打印输出  
    void        (*println)(struct at_env *self, const char *fmt, ...);
    //接收内容包含判断      
    char *      (*contains)(struct at_env *self, const char *str); 
    //获取接收缓冲区       
    char *      (*recvbuf)(struct at_env *self);
    //获取接收缓冲区长度  
    unsigned int(*recvlen)(struct at_env *self);
    //清空接收缓冲区    
    void        (*recvclr)(struct at_env *self);
    //指示当前作业是否已被强行终止
    bool        (*disposing)(struct at_env *self);
    //结束作业,并设置响应码
    void        (*finish)(struct at_env *self, at_resp_code code);
} at_env_t;

示例1:(Sierra 模组发送socket数据)

命令格式:

主机  =>
      AT+KTCPSND=<session>,<send_length> 
从机  <=
      'CONNECT'                        //从机回复提示符'CONNECT'
主机  =>   
      <data>                           //发送二进制数据内容
      "--EOF--Pattern--""              //发送结束符
从机  <=
      OK                               //从机回复OK

代码实现:


//socket定义
typedef struct {
    //....
    int id;
    unsigned char *sendptr;
    int sendcnt;
    //...
}socket_t;

/*
 * @brief       socket 数据发送处理
 * @return      true - 结束运行 false - 保持运行
 */
static int socket_send_handler(at_env_t *env)
{
    socket_t *sk = (socket_t *)env->params;
    switch (env->state) {
        case 0:
            env->println(env, "AT+KTCPSND=%d,%d", sk->id, sk->sendcnt);                       
            env->reset_timer(env);                               /*重置定时器*/
            env->state++; 
        break;
        case 1:
            if (env->contains(env, "CONNECT")) {
                env->obj->adap->write(sk->sendptr, sk->sendcnt); /*发送数据*/    
                env->println(env, "--EOF--Pattern--");            /*发送结束符*/                   
                env->reset_timer(env);
                env->recvclr(env);
                env->state++;                
            } else if (env->contains(env, "ERROR")) {            /*匹配到错误,结束作业*/
                env->finish(env, AT_RESP_ERROR);
            } else if (env->is_timeout(env, 1000)) {
                if (++env->i > 3) {
                    env->finish(env, AT_RESP_ERROR);           
                }
                env->state--;                                    /*重新发送*/                
            }            
        break;   
        case 2:
            if (env->contains(env, "OK"))                        
                env->finish(env, AT_RESP_OK);                    /*发送成功设置状态为OK后退出*/
            else if (env->contains(env, "ERROR") ||
                     env->is_timeout(env, 1000)) {
                env->finish(env, AT_RESP_ERROR);   
            }          
        break;          
    }
    return 0;    
}


/**
 * @brief socket数据发送请求
 */
static void sock_send_data(socket_t *sock)
{
    at_do_work(at_obj, sock, socket_send_handler);              
}

URC消息处理

未经请求主动上报的消息又称URC(Unsolicited Result Code),在主机方未下发送命令请求的情况下,设备会根据自身的运行状态或者事件主动上报消息给主机。

根据URC消息格式的不同,可以分为以下几类:

  1. 对于大多数URC消息通常是单行输出的一般是以“+”为前缀,回车换行结束,例如:
+SIM: 1 \r\n                          //SIM卡状态

+CREG: 1,"24A4","000012CF",1\r\n      //网络注册状态更新

  1. 也有不带前缀'+'的URC
RDY \r\n                              //开机就绪
WIFI DISCONNECTED                     //WIFI断开
  1. 非回车换行结束的URC

socket数据接收(参考sim800 模组)。

+IPD,<socket id>,<data length>:<bin data>

可以看到每个URC消息都有其特定的前缀信息前两类是以'\n'作为结束符的最后一种只有前缀而整个URC消息帧是可变长度的没有特定的结束符。那么该如何将这几类消息识别并提取出来的呢实际上URC处理程序也是通过匹配"前缀"+"结束符"的方式来提取URC消息的对于前两种消息它们都有特定的结束符('\n'),只要匹配到消息"前缀"+"后缀"就可以将整条消息完整的提取出来而对于最后一种消息由于没有固定的结束符而且消息长度是可变的所以URC处理程序无法一次性将整条消息匹配并提取出来往往需要分成多次进行第一次是匹配它的头部信息然后再由订阅者(通过回调)告诉它剩余待接收的数据长度当剩余数据长度为0时则该则URC消息接收完毕。这种处理方法其实同样也适用于前两类URC不过这些消息的剩余数据长度为0罢了。

如下所示我们可以把第3类URC消息拆分成两部分看待固定头部+剩余数据。

带二进制URC消息

由于URC具有一定的随机性所以URC处理程序会实时读取来自设备端的上报的信息然后再进行消息匹配处理。至于匹配哪种URC消息则是由用户通过at_obj_set_urc设置的表来决定的。另外考虑到性能的原因URC处理程序不会一接收到任何一个字符都会启动匹配程序工作而是遇到AT_URC_END_MARKS中规定的字符才会触发执行。

下面是at_obj_set_urc原型:

/**
 * @brief   Set the AT urc table.
 */
void at_obj_set_urc(at_obj_t *at, const urc_item_t *tbl, int count);

URC处理项(urc_item_t)

URC表的基本单位是urc_item_t,它用于描述每一条URC消息的处理规则包括了消息头结束标志还有该消息的处理程序。

/**
 * @brief urc处理项
 */
typedef struct {
    const char *prefix;            /* 需要匹配的帧前缀,如+CSQ:25,*/
    const char  endmark;           /* urc结束标志(参考@AT_URC_END_MARKS*/
    /**
     * @brief   urc处理程序(prefix与endmark满足时触发)
     * @params  ctx   - 上下文(Context)
     * @return  表示当前URC帧剩余未接收字节数
     *          @arg 0 当前URC帧已接收完成,可以接收下一个URC
     *          @arg n 仍需要等待接收n个字节(AT管理器继续接收剩余数据并继续回调此接口)
     */    
    int (*handler)(at_urc_ctx_t *ctx);
} urc_item_t;

其中at_urc_ctx_t存储URC的接收状态和数据。

/**
 * @brief URC 上下文(Context) 定义
 */
typedef struct {
    urc_recv_state state;          /* urc接收状态*/
    char *urcbuf;                  /* urc数据缓冲区 */
    int   urclen;                  /* urc缓冲区已接收数据长度*/
} at_urc_ctx_t;

示例1(WIFI断开连接)

消息格式:

WIFI DISCONNECTED <\r\n>

代码实现:


/**
 * @brief   wifi断开事件
 */
static int wifi_connected_handler(at_urc_info_t *info)
{
    printf("WIFI connection detected...\r\n");
    return 0;
}
//...
/**
 * @brief urc订阅表
 */
static const urc_item_t urc_table[] = 
{
    //其它URC...
    {.prefix = "WIFI DISCONNECTED",      .endmark = '\n',  .handler = wifi_connected_handler}
};

示例2(socket数据接收)

URC消息格式:

+IPD,<socket id>,<data length>:.....

代码实现:

/**
 * @brief socket数据接收处理
 */
static int urc_socket_data_handler(at_urc_info_t *ctx)
{
    int  length, total_length, sockid, i;
    char *data;    
    if (sscanf(ctx->urcbuf, "+IPD,%d,%d:", &sockid, &length) == 2) {     //解析出总数据长度
        data = strchr(ctx->urcbuf, ':');
        if (data == NULL)
            return 0;
        data++;
        total_length = (data - ctx->urcbuf) + length;        //计算头部长度
        if (ctx->urclen < total_length) {                    //未接收全,返回剩余待接收数据
            printf("Need to receive %d more bytes\r\n", total_length - ctx->urclen);
            return total_length - ctx->urclen;
        }
        printf("%d bytes of data were received form socket %d!\r\n", length, sockid);
    }
    return 0;
}

//....

/**
 * @brief urc订阅表
 */
static const urc_item_t urc_table[] = 
{
    //其它URC...
    {.prefix = "+IPD,",      .endmark = ':',  .handler = urc_socket_data_handler}
};

多实例并存

AT通信对象并不只限于一个, at_obj_create允许你在同一个系统中创建多个共存的AT通信设备而且每个都拥有自己独立的用于配置和资源.


//wifi 适配器
const at_adapter_t adap_wifi = {
    //...
};

//modem适配器
const at_adapter_t adap_modem = {
    //...
};

//...
at_obj_t *at_modem = at_obj_create(&modem_adapter);

at_obj_t *at_wifi = at_obj_create(&wifi_adapter);

//...

//轮询任务

/**
 * @brief AT轮询程序
 */
void at_device_process(void)
{
    static unsigned int timer = 0;
    //(为了加快AT命令处理响应速度,建议5ms以内轮询一次)
    if (at_get_ms() - timer > 5) {
        timer = at_get_ms();
        at_obj_process(at_modem);
        at_obj_process(at_wifi);  
    }
}

AT作业上下文(at_context_t)

使用异步的一个弊端是它会让程序执行状态过于分散增加程序编码和理解难度。比如一些代码需要根据异步的结果来执行下一步动作一般是在异步回调中添加对应的状态标识然后主程序根据这些状态标识来控制状态机或者程序分支的跳转这使得代码间没有明显的流程线代码执行流不好追踪管理。那么在不使用同步或者不支持OS的情况下如何避免异步带来的状态分散问题? 一种比较常用的方式是使用状态机轮询法,通过实时查询每一个异步请求的状态,并根据根据上一个结果执行下一个请求,这样代码执行上下文就紧密衔接在一块了,达到类似同步的效果。

对于异步AT请求可以使用at_context_t获取其实时信息包含当前的作业运行状态命令执行结果及命令响应信息。使用AT上下文相关功能需要先启用AT_WORK_CONTEXT_EN宏, 相关API定义如下

函数原型 说明
void at_context_init(at_context_t *ctx, void *respbuf, unsigned bufsize); 初始化一个at_context_t同时设置命令接收缓冲区。
void at_context_attach(at_attr_t *attr, at_context_t *ctx); at_context_t绑定到AT属性中。
at_work_state at_work_get_state(at_context_t *ctx); 通过at_context_t获取AT作业运行状态。
bool at_work_is_finish(at_context_t *ctx); 通过at_context_t获取AT作业完成状态。
at_resp_code at_work_get_result(at_context_t *ctx); 通过at_context_t获取AT请求状态码。

OS应用之异步转同步

如果是在OS环境下使用很多时候我们更希望采用同步的方式执行AT命令请求即原地等待命令执行完成下面介绍两种异步转同步的方式。

轮询方式

轮询方式主要是基于at_context_t实现的,发送请求前通过绑定一个at_context_t实时监视命令的执行状态,然后循环检测命令是否执行完成。

示例:

/**
 * @brief 发送命令(同步方式)
 * @param respbuf 响应缓冲区
 * @param bufsize 缓冲区大小
 * @param timeout 超时时间
 * @param cmd 命令
 * @retval 命令执行状态
 */
static at_resp_code at_send_cmd_sync(char *respbuf, int bufsize, int timeout, const char *cmd, ...)
{
    at_attr_t    attr;
    at_context_t ctx;
    va_list      args; 
    bool         ret;
    //属性初始化
    at_attr_deinit(&attr);
    attr.timeout = timeout;
    attr.retry   = 1;
    //初始化context,并设置响应缓冲区
    at_context_init(&ctx, respbuf, bufsize); 
    //为工作项绑定context
    at_context_attach(&attr, &ctx);
    
    va_start(args, cmd);
    ret = at_exec_vcmd(at_obj, &attr, cmd, args);
    va_end(args);

    if (!ret) {
         return AT_RESP_ERROR;     
    }

    //等待命令执行完毕
    while (!at_work_is_finish(&ctx)) {  
        usleep(1000);
    }    
    return at_work_get_result(&ctx);
}

信号量方式

使用轮询方式会造成CPU空转可以通过使用信号量来优化不过实现起来比轮询方式要稍微复杂一些

示例:

//...
#include "at_chat.h"
#include <pthread.h>
#include <semaphore.h>

typedef struct {
    pthread_mutex_t cmd_lock;         //命令锁
    sem_t           sem_finish;       //完成信号    
    char            *recvbuf;         //命令接收缓冲区
    unsigned short  bufsize;          //缓冲区大小
    unsigned short  recvcnt;          //接收计数器
    at_resp_code    resp_code;        //命令响应码
} at_watch_t;

static at_watch_t at_watch;

/**
 * @brief 命令锁及信号相关初始化
 */
void at_sync_init(void)
{
    sem_init(&at_watch.sem_finish, 0, 0);
    pthread_mutex_init(&at_watch.cmd_lock, NULL);    
}

/**
 * @brief AT命令响应处理
 */
static void at_callback_handler(at_response_t *r)
{
    int cnt = r->recvcnt;
    at_watch.resp_code = r->code;
    if (at_watch.recvbuf != NULL) {
        if (cnt > at_watch.bufsize) {
            //接收缓存不足
            cnt = at_watch.bufsize;
        }
        memcpy(at_watch.recvbuf, r->recvbuf, cnt);
        at_watch.recvcnt = cnt;
    }
    //通知命令已执行完毕
    sem_post(&at_watch.sem_finish);
}


/**
 * @brief 发送命令(同步方式)
 * @param respbuf 响应缓冲区
 * @param bufsize 缓冲区大小
 * @param timeout 超时时间
 * @param cmd 命令
 * @retval 命令执行状态
 */
static at_resp_code at_send_cmd_sync(char *respbuf, int bufsize, int timeout, const char *cmd, ...)
{
    at_attr_t    attr;
    va_list      args; 
    bool         ret;
    //属性初始化
    at_attr_deinit(&attr);
    attr.timeout = timeout;
    attr.retry   = 1;
    attr.cb      = at_callback_handler;
    va_start(args, cmd);

    pthread_mutex_lock(&gw.sync_cmd_lock);
    //设置接收缓冲区
    at_watch.recvbuf = respbuf;
    at_watch.bufsize = bufsize;
    at_watch.recvcnt = 0;
    at_watch.resp_code = AT_RESP_ERROR;
    if (at_exec_vcmd(at_obj, &attr, cmd, args)) {
        sem_wait(&at_watch.sem_finish);   //等待命令执行完成
    }
    pthread_mutex_unlock(&gw.sync_cmd_lock);

    va_end(args); 

    return at_watch.resp_code;
}

内存监视器

嵌入式系统的内存资源极其有限,不当的使用动态内存,除了产生内存碎片、内存泄露这些问题外,严重时会导致死机,崩溃等事故,所以在使用动态内存时有必要加上一定限制手段,确保系统在一定安全边际下正常运行。AT_MEM_LIMIT_SIZE规定了AT请求所用的最大内存数量这样可以避免程序异常执行时过度执行AT请求导致内存不足的问题。至于分配多少主要取决于你的应用如果你一开始并不确定用多少比较合适可以先设置一个相对来说大一些的值然后让程序运行一段时间观察使用情况再设置通过at_max_used_memoryat_cur_used_memory可以获取历史最大内存使用量和当前内存使用量。