at device源码阅读

2026 年 7 月 2 日 星期四(已编辑)
28
这篇文章上次修改于 2026 年 7 月 3 日 星期五,可能部分内容已经不适用,如有疑问可询问作者。

at device源码阅读

AT设备你们是如何解析的呢?笔者第一次使用at设备,是一个4G模块,大夏龙雀的L511-2。

看了新公司的同事的代码,这颗模块实现MQTT的方式,是直接用了AT+MQTT的方式来下发。

我打算换一个架构,之前学习的是socket做网络应用,我想把AT设备封装成socket接口。

这部分代码就不自己造轮子了,据我了解rtthread直接有at socket,今天就花一天时间阅读一下它的源码。

这里得分为两部分,一个是AT协议的解析框架。第二个是如何把AT指令封装成socket。

我们先来看第一个。

AT协议解析框架

底层收发接口

最底层的发送接口使用一个weak定义的函数at_utils_send实现:


rt_weak rt_size_t at_utils_send(rt_device_t dev,
                                rt_off_t    pos,
                                const void *buffer,
                                rt_size_t   size)
{
    return rt_device_write(dev, pos, buffer, size);
}

// 代码

它内部调用rt_device_write,如果我们物理接口使用的是device io框架,那么可以直接使用。

at_client 初始化

根据语义,主动的被称为Client,被动的被称为Server。在我们的应用场景中往往模块被当成Server,而调用模块的MCU当成Client。如果你是要自己做一个模块,那就是实现Server。

在rtthread的代码中,提供了at_client.c和at_server.c这样子两个文件。

作为使用模块的用户,这里启用at_client.c

它支持多实例,用单向链表管理多个client。g_at_client_list就是链表,在里面挂载了一个个节点at_client_t。

之前没有接触过这种写法,不同入参同样的entry,又学到了一招。

初始化函数为:int at_client_init(const char *dev_name, rt_size_t recv_bufsz, rt_size_t send_bufsz)

这里的入参有3个,第一个dev_name也就是输出物理层设备的名称。

我看到代码里的写法,应该是一个输出设备只能注册一个client。不存在一个串口,挂载多个client这种现象。我仔细想一下,好像确实是这样,这个抽象是合理的。一个串口挂载两个设备就乱套了。必须物理层和AT设备是一对一的。

整体at_client_init过程不是很复杂,完整代码我就不贴了,不然文章就太长了。

它里面,首先根据dev_name判断设备是否注册,防止重复注册。

然后malloc申请一个长度为sizeof(struct at_client) + recv_bufsz + send_bufsz内存。

这种做法貌似在RTOS代码里面满常见的。只申请一次,其中控制句柄放在这个内存的最开始。client = rt_malloc(sizeof(struct at_client) + recv_bufsz + send_bufsz);然后再对这段内存做分配。而不是申请三次

//申请一次,后续分配这段内存
client = rt_malloc(sizeof(struct at_client) + recv_bufsz + send_bufsz);
...
client->recv_bufsz    = recv_bufsz;
client->recv_line_buf = ((char *)client) + sizeof(struct at_client);

client->send_bufsz = send_bufsz;
client->send_buf   = ((char *)client) + sizeof(struct at_client) + client->recv_bufsz;
...

最后把client添加到list里面,注册串口RX的回调之类的。 rt_device_set_rx_indicate(client->device, at_client_rx_ind);

这个回调函数在串口收到数据后会被触发,里面是发送了事件标志组,去通知解析。

rt_event_send(&client->event, at_client_rx_notice_event);

解析数据

通过at_client_rx_notice_event事件来触发读取。把读到的数据放到自己的recv_line_buff中



if (event & at_client_rx_notice_event)
{
    while (RT_EOK == at_client_getchar(client, &ch))
    {
        if (client->recv_line_len < client->recv_bufsz)
        {
            client->recv_line_buf[client->recv_line_len++] = ch;
        }
        else
        {
            is_full = RT_TRUE;
        }

        /* is newline or URC data */
        client->urc = get_urc_obj(client);
        if (client->urc != RT_NULL || (ch == '\n' && last_ch == '\r')
            || (client->end_sign != 0 && ch == client->end_sign))
        {
            if (is_full)
            {
                LOG_E("read line failed. The line data length is out of buffer size(%d)!", client->recv_bufsz);
                rt_memset(client->recv_line_buf, 0x00, client->recv_bufsz);
                client->recv_line_len = 0;
                return -RT_EFULL;
            }

            /* Since the buffer state is uncertain, we proactively clear it; the overhead is negligible. */
            rt_event_send(&client->event, at_client_rx_notice_event);
            goto __next;
        }
        last_ch = ch;
    }
}

这里判断了urc

所谓urc就是指AT指令中,模块主动给MCU上报的信息,例如发送AT+CSQ收到的应答如下:

+CSQ: 20,99
OK

这种时候就需要解析应答的内容。

因为不同模块的应答格式不同,所以这个框架中采用了注册的方法,注册一个urc_table,然后这里是收到应答后去匹配,看看是不是urc的内容。

resp是应答包。在AT协议中需要OK,ERROR,FAIL来判断发送命令的知行情况。

伪代码如下:

/**
 * Waiting for connection to external devices.
 *
 * @param client current AT client object
 * @param timeout millisecond for timeout
 *
 * @return 0 : success
 *        -2 : timeout
 *        -5 : no memory
 */
int at_client_obj_wait_connect(at_client_t client, rt_uint32_t timeout)
{
    rt_err_t      result     = RT_EOK;
    at_response_t resp       = RT_NULL;
    rt_tick_t     start_time = 0;

    if (client == RT_NULL)
    {
        LOG_E("input AT client object is NULL, please create or get AT Client object!");
        return -RT_ERROR;
    }

    resp = at_create_resp(64, 0, rt_tick_from_millisecond(300));
    if (resp == RT_NULL)
    {
        LOG_E("no memory for AT client(%s) response object.", client->device->parent.name);
        return -RT_ENOMEM;
    }

    start_time = rt_tick_get();

    while (1)
    {
        /* Check whether it is timeout */
        if (rt_tick_get() - start_time > rt_tick_from_millisecond(timeout))
        {
            LOG_E("wait AT client(%s) connect timeout(%d tick).", client->device->parent.name, timeout);
            result = -RT_ETIMEOUT;
            break;
        }

        if (at_obj_exec_cmd(client, resp, "AT") == RT_EOK)
        {
            break;
        }
    }

    at_delete_resp(resp);
    return result;
}

先是创建resp,然后调用at_obj_exec_cmd等到回复应答。

封装socket

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...