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

21 KiB
Raw Blame History

快速上手

有了对整个框架的基本认识之后,接下来我们看一下如何在你的平台下使用。

准备工作

在使用前,您需要做一些准备工作,包括:

  • 工程配置与移植
  • 驱动接口适配
  • 创建AT通信对象
  • 加入轮询任务

工程配置与移植

如下所示,使用前您需要先将AT通信组件添加到你的项目中, 涉及的文件只有inlcude及src这两个目录,另外根据你所有平台不同,还需要进行简单的移植配置才能正常使用,有关这部分可以参考平台移植指南平台移植及配置.


at_chat
│
|───docs          使用文档
|───samples       演示案例
|───inlcude       头文件目录
|    │───at_chat.h
|    │───at_port.h
|    │───linux_list.h
└───src
    │───at_port.c
    │───at_chat.c    

驱动接口适配

完成平台移植这些工作之后为了能够使AT通信对象与硬件关联起来你需要提供相关的驱动接口进行适配对接,包括:

  1. 串口读写接口

因为通常使用是串口进行AT通信所以只需要提供串口中读写驱动接口即可不过这里需要注意的是串口驱动接口必须是带缓冲区非阻塞对于LINUX的平台需要将串口文件IO设置为O_NONBLOCK而如果你使用的是MCU可以使用队列或者环形缓存区实现死等收发不保证能正常工作)有经验的工程师对这个应该已经比较熟悉了对于刚入门不久的小伙可以参考samples/none_os中的例子。

  1. 互斥锁接口

如果你是在OS环境下使用并且跨线程执行了AT请求,你需要提供上锁/解锁接口如果你都是在单线程中运行的则直接填NULL即可。

  1. 缓冲区大小设置

包括接收缓冲与URC缓冲区,接收缓冲区用于缓存命令请求时设备端响应的数据,它取决于你的设备最大响应内容长度;URC缓冲区也是取决于最长上报的URC数据长度。

  1. 日志打印与异常接口

如果你需要查看所有AT命令通信的细节你需要实现debug接口在进行AT命令通信过程中他会将所有收发消息输出出来如果执行过中出现异常(出错,超时)则可以通过实现error接口来进行监视。

完整的适配器定义如下:

/**
 * @brief AT interface adapter
 */
typedef struct  {
    //Lock, used in OS environment, fill in NULL if not required.
    void (*lock)(void);
    //Unlock, used in OS environment, fill in NULL if not required.
    void (*unlock)(void);
    //Data write operation (non-blocking)
    unsigned int (*write)(const void *buf, unsigned int len); 
    //Data read operation (non-blocking)
    unsigned int (*read)(void *buf, unsigned int len);       
    //AT error event ( if not required, fill in NULL)
    void (*error)(at_response_t *);
    //Log output interface, which can print the complete AT interaction process, fill in NULL if not required.
    void (*debug)(const char *fmt, ...);  
#if AT_URC_WARCH_EN
    //URC buffer size, set according to the actual maximum URC frame when used.
    unsigned short urc_bufsize;
#endif    
    //Command response receiving buffer size, set according to the actual maximum command response length
    unsigned short recv_bufsize;
} at_adapter_t;

示例:

/**
 * @brief AT适配器
 */
static const at_adapter_t at_adapter = {
    .lock          = at_mutex_lock,           //多任务上锁(非OS下填NULL)
    .unlock        = at_mutex_unlock,         //多任务解锁(非OS下填NULL)
    .write         = at_device_write,         //数据写接口
    .read          = at_device_read,          //数据读接口)
    .debug         = at_debug,                //调试打印接口
    .recv_bufsize  = 256,                     //接收缓冲区大小
    .urc_bufsize   = 512
};

创建AT通信对象

下一步就是使用刚刚创建好的驱动接口来创建AT通信对象。

原型如下:

at_obj_t *at_obj_create(const at_adapter_t *);

!> 这里有一点需要特别注意,为了节省内存使用AT组件只存储了at_adapter_t对象的指针,而不是对象本身,所以at_adapter_t定义的对象必须是常驻内存的,建议使用const进行修饰,这样可以存储在ROM中减少内存使用。

示例:

    at_obj_t *at_obj;
    //....
    at_obj = at_obj_create(&at_adapter);
    if (at_obj == NULL) {
        printf("at object create failed\r\n");
    }  
    //...

加入轮询任务

最后你需要再提供一个任务定时轮询以便它能够及时处理AT请求以及URC消息轮询时间越短越好原型如下

/**
 * @brief  AT轮询处理程序
 */
void at_obj_process(at_obj_t *at);

示例:

/**
 * @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_obj);  
    }
}

基础应用

完成上面几个步骤之后你就可以使用AT命令请求相关接口了不过在开始使用之前,我先带你了解一下有关一般AT命令格式和使用过程中涉及到的一些基本概念了解这些有助深入理解整个AT组件是如何针对各种不同场景处理那些业务交互的实现原理。

一般AT命令类型与处理

以常见的GPRS/WIFI模组AT命令为例它们常用格式的可分以下几种类型

  1. 测试命令 命令格式为AT+<x>=?,用于查询设置命令设置的参数以及其取值范围。

  2. 查询命令 命令格式为AT+<x>?,用于返回参数的当前值。

  3. 设置命令 命令格式为AT+<x>=<...>,用于设置用户配置参数。

  4. 执行命令 命令格式为AT+<x>不带参数常用于执行系统相关操作如查询系统信息读取IMEI号等。

上面列举的4种格式基本上覆盖了常见AT通信场景通信双方都是一问一答式的主机发送1行命令请求从机响应数据及状态双方只交互一次。由于流程结构一样所以在软件设计时比较容易统一设计成一种类型接口。

主机:
            => AT+XXX...
从机:
            <= +XXX
            <= OK/ERROR

对主机端来说,命令都是以行为单位执行的,只是携带的参数类型或者个数不同,唯一需要特殊处理的是数据响应信息解析,因为各个命令响应并没有统一格式,所以无法在底层实现一个通用解析模块,这需要上层应用根据不同的命令类型进行分别处理。我们可以按照谁发的命令谁来处理的原则,将命令响应内容返还给命令发送者,由它来进行处理。但是在此之前,需要鉴别响应内容从哪开始到哪结束,这就涉及到内容匹配提取的问题,归纳起来如有下两种情况:

  • 对于设置类命令,命令响应全是以"OK"状态返回。
  • 对于查询类命令,返回的内容包含数据+状态,命令状态也是以"OK"返回,而数据部分对于不同命令有不一样的信息格式。

由此可以通过统一为匹配"前缀+后缀"的方式提取到AT命令响应的内容这也是这个组件处理AT命令请求的基本实现原理它通过可以为每个命令附带一个属性信息(包含响应前缀,响应后缀)用于告诉AT解析器如何正确提取响应的内容。

以无线模组查询信号质量命令为例,如下图所示,"+CSQ:"作为响应内容前缀,"OK"作为响应内容后缀。

命令发送-响应示意图

命令属性

AT命令属性使用at_attr_t描述在发送AT请求之前你可以为每个AT请求设置一个属性它可以控制AT状态机的行为如控制命令响应内容匹配规则、超时时间、错误重试等。所有AT作业对象都共享以下属性:

  • 用户私有参数(params)
  • 响应内容前后缀(prefix,suffix)
  • 回调处理程序(cb)
  • 响应超时时间(timeout)
  • 重次次数(retry)
  • 优先级(priority)

AT属性数据结构定义如下:


/**
 *@brief AT Command attributes.
 */
typedef struct {
    void          *params;       /* User parameter, fill in NULL if not required. */
    const char    *prefix;       /* Response prefix, fill in NULL if not required. */ 
    const char    *suffix;       /* Response suffix, fill in NULL if not required. */
    at_callback_t  cb;           /* Response callback handler, fill in NULL if not needed. */
    unsigned short timeout;      /* Response timeout(ms). */
    unsigned char  retry;        /* Response error retries. */
    at_cmd_priority priority;    /* Command execution priority. */
} at_attr_t;

在使用AT属性之前你需要为它的每个字段都进行初始化设置,但每次发送请求都将所有属性设置一遍太过繁琐,大多数情况下命令的属性都差不多,您可以先使用at_attr_deinit进行默认设置,然后再设置关注的属性即可。

示例1:

at_attr_t attr;
//设置属性为默认值
at_attr_deinit(&attr);        
attr.cb = my_handler;   //设置回调处理程序

示例2:

at_attr_t attr;
//设置属性为默认值
at_attr_deinit(&attr);        
attr.cb = my_handler;   //设置回调处理程序
attr.prefix = "+CSQ:"   //设置响应内容前缀
attr.suffix = "OK";     //设置响应内容后缀
...

AT回调与响应

对于是异步的命令所有请求的结果都是通过回调方式通知应用程序的同时它会返回命令响应的相关信息你可以在AT属性中指定相应的回调处理程序。

回调程序原型如下:

/**
 *@brief AT  执行回调
 *@param r   AT响应信息(包含执行结果,设备端返回的内容信息)
 */
typedef void (*at_callback_t)(at_response_t *r);  

其中at_response_t包含了所有请求响应相关的信息,它包含了请求命令的执行状态和命令响应内容。

/**
 * @brief AT响应信息
 */
typedef struct {
    struct at_obj  *obj;                                    /* 所属AT通信对象*/   
    void           *params;                                 /* 附属参数*/	
    at_resp_code    code;                                   /* AT命令响应码*/    
    unsigned short  recvcnt;                                /* 接收数据长度*/    
    char           *recvbuf;                                /* 接收缓冲区(原始数据)*/
    /* 接收内容前缀当code=AT_RESP_OK时有效如果未指定匹配前缀则它指向recvbuf*/
    char           *prefix;      
    /* 接收内容后缀当code=AT_RESP_OK时有效如果未指定匹配后缀则它指向recvbuf*/
    char           *suffix;
} at_response_t;

  • obj:所属AT通信对象你可以通过它在响应处理程序中根据状态发送下一个请求。
  • param:附属参数,对应at_attr_t中的params。
  • code:AT命令响应码
  • recvcnt:指示了接收缓冲区recvbuf有效接收数据长度。
  • recvbuf:缓存了此次命令请求时返回的数据。
  • prefix:接收内容前缀,例如当你期望接收到'+CSQ:'开始的数据时当code==AT_RESP_OK则prefix就是指向了'recvbuf'中'+CSQ:'的开始位置。
  • suffix:接收内容后缀。

了解完上面的几个基本概念之后,下面看一下如何使用。

单行命令发送(at_send_singlline)

通过at_send_singlline函数可以执行单行命令请求,其原型如下:

/**
 * @brief   Send a single -line command
 * @param   cb        Response callback handler Fill in NULL if not required
 * @param   timeout   command execution timeout(ms)
 * @param   retry     command retries( >= 0)
 * @param   singlline command
 * @retval  Indicates whether the asynchronous work was enqueued successfully.
 * @note    Only the address is saved so the 'singlline' can not be a local variable which will be destroyed.
 */
bool at_send_singlline(at_obj_t *at, at_callback_t cb, int timeout, int retry, const char *singlline);

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • cb : 回调处理程序,当命令执行完成之后,将以这个接口通知上层应用,并携带命令响应内容及状态。
  • timeout命令执行超时时间单位为ms。
  • retry命令出错或者超时重试次数。
  • singlline命令名称。

注:AT作业只会缓存singlline的指针这意味者它指向的命令不能是局部数据。

示例1查询网络注册状态(SIM900模块):

命令格式:

=> AT+CREG?

<= +CREG:<mode>,<stat>[,< lac>,< ci>]

<= OK

代码实现:

/**
 * @brief  命令响应处理程序
 */
static void read_creg_callback(at_response_t *r)   
{
    int mode, state;
    if (r->code == AT_RESP_OK) {
        //提取出工作模式及注册状态
        if (sscanf(r->prefix, "+CREG:%d,%d", &mode, &state) == 2) {
            printf("Mode:%d, state:%d\r\n", mode, state);
        }
    } else {
        printf("'CREG' command response failed!\r\n");
    }
}

/**
 * @brief  读网络注册状态
 */
static void read_creg(void)
{
    //发送CREG查询命令,超时时间为1000ms,重发次数为0
    at_send_singlline(at_obj, read_creg_callback, 500, 1, "AT+CREG?"); 
}

示例2,IO控制:

如果你不关心命令响应结果,直接可以这样使用。

at_send_singlline(at_obj, NULL, 500, 0, "AT+SETIO=1,FF");   //设置设备IO类型
at_send_singlline(at_obj, NULL, 500, 0, "AT+OUTIO=1,55");   //设置IO电平

多行命令发送(at_send_multiline)

实际工程应用中,特别是进行设备初始化配置时,往往一次需要发送大量命令,一种实现方式是通过at_send_singlline在每个命令的回调中根据命令状态进行下一条命令的请求,命令较少时还可以接受,命令一多就会形成恐怖的回调地狱链,造成代码难以理解和维护,现实中应尽量避免这种写法。这里提供了一个支持批量发送的接口来应对这种情况,其原型如下:


/**
 * @brief   Send multiline commands
 * @param   attr AT attributes(NULL to use the default value)
 * @param   multiline Command table, with the last item ending in NULL.
 * @example :
 *          const char *multiline = {
 *              "AT+XXX",
 *              "AT+XXX",
 *              NULL
 *          };
 *          at_send_multiline(at_dev, NULL, multiline);
 * 
 * @retval  Indicates whether the asynchronous work was enqueued successfully.
 * @note    Only the address is saved, so the array can not be a local variable which will be destroyed.
 */
bool at_send_multiline(at_obj_t *at, const at_attr_t *attr, const char **multiline);

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • attr : 命令属性,通过它可以设置命令回调程序,超时时间等。
  • multiline命令表以NULL结尾。

示例SIM800C 模组初始化):

/**
 * @brief  命令响应处理程序
 */
static void simcom_init_callback(at_response_t *r)   
{
    printf("SIM800C Init %s!\r\n",r->code == AT_RESP_OK ? "ok" : "error");
}
/* 
 * @brief 模块初始化
 */
static void simcom_init(void)
{
    at_attr_t attr;
    static const char *cmds[] = {
    "AT+CSCLK=0",                                              
    "AT+CIPHEAD=1",                                             //+RECEIVE,<n>,<data length>:
    "AT+CIPQSEND=1",                                            //快发模式
    "AT+CIPMUX=1",                                              //多IP支持
    "AT+CIPSPRT=>",
    "AT+CIPSRIP=0",         
    NULL
    };
    at_attr_deinit(&attr);
    attr.cb = simcom_init_callback;                             //设置命令回调
    at_send_multiline(at_obj, &attr, cmds);
}

!> 批量命令表中最后一项必须是NULL且整个命令表不能是局部变量。

可变参数命令(at_exec_cmd)

前面讲的单行命令与多行命令都是用于发送固定参数的命令的,如果你的命令参数(如设置类命令)是可变的,可以通过 at_exec_cmd执行请求,其原型如下:

/**
 * @brief   Execute one command
 * @param   attr AT attributes(NULL to use the default value)
 * @param   cmd  Formatted arguments
 * @param   ...  Variable argument list (same usage as printf)
 * @retval  Indicates whether the asynchronous work was enqueued successfully
 */
bool at_exec_cmd(at_obj_t *at, const at_attr_t *attr, const char *cmd, ...);

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • attr : 命令属性,通过它可以设置命令回调程序,超时时间等。
  • cmd : 格式化命令用法与printf一样。

示例1,EC20设置APN:

/**
 * @brief  命令响应处理程序
 */
static void set_apn_callback(at_response_t *r)   
{
    printf("Setting APN %s!\r\n",r->code == AT_RESP_OK ? "ok" : "error");
}
/** 
 * @brief 设置APN
 */
static bool set_apn(const char *apn, const char *username, const char *pwd)
{
    at_attr_t attr;
    at_attr_deinit(&attr);      
    attr.cb = set_apn_callback; //设置命令回调
    at_exec_cmd(at_obj, &attr, "AT+QICSGP=1,1,\"%s\",\"%s\",\"%s\",0", apn, username, pwd);
}

可变参数列表(at_exec_vcmd)

通常at_obj_t对象并不会直接公开给第第三方模块使用为了能够使其它模块也能进行AT请求你可能需要对命令请求的接口进行2次封装当需要支持可变参数时可以使用at_exec_vcmd,它支持传递变参列表,其原型如下:

/**
 * @brief   execute command (with variable argument list)
 * @param   attr AT attributes(NULL to use the default value)
 * @param   cmd  Format the command.
 * @param   va   Variable parameter list
 * @return  Indicates whether the asynchronous work was enqueued successfully
 */
bool at_exec_vcmd(at_obj_t *at, const at_attr_t *attr, const char *cmd, va_list va);

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • attr : 命令属性,通过它可以设置命令回调程序,超时时间等。
  • cmd : 格式化命令
  • va : 参数列表

示例:

/** 
 * @brief 发送AT命令
 * @param   attr 命令属性
 * @param   cmd,.. 格式化命令参数用法与printf一样
 */
bool at_send_cmd(const at_attr_t *attr, const char *cmd, ...)
{
    bool ret;
    va_list args;
    va_start(args, cmd);
    ret = at_exec_vcmd(at_obj, attr, cmd, args);
    va_end(args); 
    return ret;       
}

发送原始数据(at_send_data)

当命令类型非纯文本或者格式化的命令无法满足要求时,可以使用at_send_data接口,以原始数据的方式发送。

/**
 * @brief   Send (binary) data
 * @param   attr AT attributes(NULL to use the default value)
 * @param   databuf Binary data
 * @param   bufsize Binary data length
 * @retval  Indicates whether the asynchronous work was enqueued successfully
 */
bool at_send_data(at_obj_t *at, const at_attr_t *attr, const void *databuf, unsigned int bufsize);

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • attr : 命令属性,通过它可以设置命令回调程序,超时时间等。
  • databuf : 原始数据
  • bufsize : 数据长度

自定义命令(at_custom_cmd)

在进行变参命令或者原始数据发送时系统会他们单独分配一块区域进行暂存然后跟随AT作业一块缓存到队列中如果短期需要发送大量的数据或者命令队列不能及时被处理那么那段时间内命令队列占用的总内存会激增严重时导致无可用内存。即使在内存使用的上做限制但是遇到这种情况也会造成命令请求失败使用自定义命令的方式可以在一定程度缓解这个问题它不直接缓存调用者的任何数据而只暂存一个发送器指针当真正发送命令时再上调用者请求数据其原型如下

/**
 * @brief   Execute custom command
 * @param   attr AT attributes(NULL to use the default value)
 * @param   sender Command sending handler (such as sending any type of data through the env->obj->adap-write interface)
 * @retval  Indicates whether the asynchronous work was enqueued successfully
 */
bool at_custom_cmd(at_obj_t *at, const at_attr_t *attr, void (*sender)(at_env_t *env));

返回值: 当请求成功返回true否则返回false。

参数说明:

  • at : AT对象
  • attr : 命令属性,通过它可以设置命令回调程序,超时时间等。
  • sender : 数据发送器调用者可以通过env->obj->write接口发送任何形式的数据。

示例:(ESP8266创建TCP socket)

/**
 * @brief  命令响应处理程序
 */
static void create_socket_callback(at_response_t *r)   
{
    printf("Create socket %s!\r\n",r->code == AT_RESP_OK ? "ok" : "error");
}
/** 
 * @brief 发送创建socket命令
 */
void create_socket_sender(at_env_t *env)
{
    socket_t *sock = (socket_t *)env->params;
    env->println(env, "AT+CIPSTART=\"TCP\",\"%s\",%d", sock->ip, sock->port);
}

/** 
 * @brief 创建socket
 */
static bool create_socket(socket_t *sock)
{
    at_attr_t attr;
    at_attr_deinit(&attr);   
    attr.params = sock;                   //用户参数(里面存储了socket id,目标IP端口)
    attr.cb     = create_socket_callback; //设置命令回调
    attr.suffix = "CONNECT OK";           //设置响应后缀
    return at_custom_cmd(at_obj, &attr, create_socket_sender);
}