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等到回复应答。