AT 组件是基于 RT-Thread 系统的
AT Server
和
AT Client
的实现,组件完成 AT 命令的发送、命令格式及参数判断、命令的响应、响应数据的接收、响应数据的解析、URC 数据处理等整个 AT 命令数据交互流程。
通过 AT 组件,设备可以作为 AT Client 使用串口连接其他设备发送并接收解析数据,可以作为 AT Server 让其他设备甚至电脑端连接完成发送数据的响应,也可以在本地 shell 启动 CLI 模式使设备同时支持 AT Server 和 AT Client 功能,该模式多用于设备开发调试。
AT 组件资源占用:
- AT Client 功能:4.6K ROM 和 2.0K RAM;
- AT Server 功能:4.0K ROM 和 2.5K RAM;
- AT CLI 功能: 1.5K ROM ,几乎没有使用 RAM。
整体看来,AT 组件资源占用极小,因此非常适用应用于资源有限的嵌入式设备中。AT 组件代码主要位于
rt-thread/components/net/at/
目录中。主要的功能包括如下,
AT Server 主要功能特点:
- 基础命令: 实现多种通用基础命令(ATE、ATZ 等);
- 命令兼容: 命令支持忽略大小写,提高命令兼容性;
- 命令检测: 命令支持自定义参数表达式,并实现对接收的命令参数自检测功能;
- 命令注册: 提供简单的用户自定义命令添加方式,类似于
命令添加方式;finsh/msh
- 调试模式: 提供 AT Server CLI 命令行交互模式,主要用于设备调试。
AT Client 主要功能特点:
- URC 数据处理: 完备的 URC 数据的处理方式;
- 数据解析: 支持自定义响应数据的解析方式,方便获取响应数据中相关信息;
- 调试模式: 提供 AT Client CLI 命令行交互模式,主要用于设备调试。
- AT Socket:作为 AT Client 功能的延伸,使用 AT 命令收发作为基础,实现标准的 BSD Socket API,完成数据的收发功能,使用户通过 AT 命令完成设备连网和数据通讯。
- 多客户端支持: AT 组件目前支持多客户端同时运行。
以上是RT-Thread官方对AT组件的介绍,看完介绍后发现这个组件功能很强大,于是想看看AT组件的架构是怎样,它是如何处理不定长数据,如何进行不同平台AT指令的解析的。
一、AT设备初始化流程
AT组件初始化做了如下几个事情(不是特别完全,不知道的没写出来):
1.AT组件客户端初始化
a. 串口设备初始化
b.注册串口接收回调函数
c.为此客户端分配内存
d.建立互斥信号量(避免多个线程同时发送AT指令,导致相关结构体中的数据出现错误)
e.创建串口数据接收信号量(每当串口接收到一个字节时,串口接收回调函数会释放一次信号量,解析线程此时开始工作)
f.创建响应接收完成信号量(当接收到AT指令回复时会释放一次信号量通知应用线程)
g.创建指令解析线程(仅初步解析,原始响应数据去除结束符(
"\r\n"
)后每行数据以'\0'分割)
2.网卡初始化(分层思想,最终封装成了一个网卡,方便应用层调用)
a.执行AT指令流程(包括检测BC26工作状态、参数设置、SIM卡状态、注册状态、IP地址)
b.创建网络维护线程(网络状态通过"AT+CGREG?"指令获取)
二、AT指令发送流程
1.首先发送指令前调用at_create_resp函数初始化at_response_t结构体,设置最大接收长度,超时时间
2.调用at_obj_exec_cmd函数,传入要发送的指令字符串,等待响应信号量释放。
3.根据at_obj_exec_cmd函数的返回结果进行相应处理
/** 指令发送实例 */
static int bc26_power_off(struct at_device *device)
{
at_response_t resp = RT_NULL;
struct at_device_bc26 *bc26 = RT_NULL;
/** 初始化at_response_t 结构体 */
resp = at_create_resp(64, 0, rt_tick_from_millisecond(300));
if (resp == RT_NULL)
{
LOG_D("no memory for resp create.");
return (-RT_ERROR);
}
/** 发送指令并等待返回 */
if (at_obj_exec_cmd(device->client, resp, "AT+QPOWD=0") != RT_EOK)
{
LOG_D("power off fail.");
at_delete_resp(resp);
return (-RT_ERROR);
}
at_delete_resp(resp);
bc26 = (struct at_device_bc26 *)device->user_data;
bc26->power_status = RT_FALSE;
return (RT_EOK);
}
int at_obj_exec_cmd(at_client_t client, at_response_t resp, const char *cmd_expr, ...)
{
va_list args;
rt_size_t cmd_size = 0;
rt_err_t result = RT_EOK;
const char *cmd = RT_NULL;
RT_ASSERT(cmd_expr);
if (client == RT_NULL)
{
LOG_E("input AT Client object is NULL, please create or get AT Client object!");
return -RT_ERROR;
}
/* check AT CLI mode */
if (client->status == AT_STATUS_CLI && resp)
{
return -RT_EBUSY;
}
rt_mutex_take(client->lock, RT_WAITING_FOREVER);
client->resp_status = AT_RESP_OK;
client->resp = resp;
if (resp != RT_NULL)
{
resp->buf_len = 0;
resp->line_counts = 0;
}
va_start(args, cmd_expr);
at_vprintfln(client->device, cmd_expr, args); /** 这里调用串口发送API发送指令 */
va_end(args);
if (resp != RT_NULL)
{
/** 发送指令后在这里等待信号量被释放 */
if (rt_sem_take(client->resp_notice, resp->timeout) != RT_EOK)
{
cmd = at_get_last_cmd(&cmd_size);
LOG_D("execute command (%.*s) timeout (%d ticks)!", cmd_size, cmd, resp->timeout);
client->resp_status = AT_RESP_TIMEOUT;
result = -RT_ETIMEOUT;
goto __exit;
}
if (client->resp_status != AT_RESP_OK)
{
cmd = at_get_last_cmd(&cmd_size);
LOG_E("execute command (%.*s) failed!", cmd_size, cmd);
result = -RT_ERROR;
goto __exit;
}
}
__exit:
client->resp = RT_NULL;
rt_mutex_release(client->lock);
return result;
}
三、接收流程
串口接收到一个字节数据后,进入中断服务函数,最终进入中断回调函数at_client_rx_ind
/** 调用此回调函数前数据已经保存下来了,此回调只是用来释放信号量通知解析线程有数据来了 */
static rt_err_t at_client_rx_ind(rt_device_t dev, rt_size_t size)
{
int idx = 0;
for (idx = 0; idx < AT_CLIENT_NUM_MAX; idx++)
{
if (at_client_table[idx].device == dev && size > 0)
{
rt_sem_release(at_client_table[idx].rx_notice); /** 释放信号量 */
}
}
return RT_EOK;
}
四、指令解析线程
相关代码如下:
/** 解析线程解除阻塞后会从中断服务函数存放数据的缓存中读取一个字节数据,因为线程开始执行后会运行到此等待信号量释放 */
static rt_err_t at_client_getchar(at_client_t client, char *ch, rt_int32_t timeout)
{
rt_err_t result = RT_EOK;
__retry:
result = rt_sem_take(client->rx_notice, rt_tick_from_millisecond(timeout)); /** 等待串口来数据 */
if (result != RT_EOK)
{
return result;
}
if(rt_device_read(client->device, 0, ch, 1) == 1) /** 来数据了,接收 */
{
return RT_EOK;
}
else
{
goto __retry;
}
}
/** 循环读取整行数据判断依据为:(ch == '\n' && last_ch == '\r') || (client->end_sign != 0 && ch == client->end_sign) */
static int at_recv_readline(at_client_t client)
{
rt_size_t read_len = 0;
char ch = 0, last_ch = 0;
rt_bool_t is_full = RT_FALSE;
/** 缓存清零 */
rt_memset(client->recv_line_buf, 0x00, client->recv_bufsz);
client->recv_line_len = 0;
while (1) /** 直到接收到了一整行 */
{
at_client_getchar(client, &ch, RT_WAITING_FOREVER);
if (read_len < client->recv_bufsz)
{
client->recv_line_buf[read_len++] = ch;
client->recv_line_len = read_len;
}
else
{
is_full = RT_TRUE;
}
/* is newline or URC data */
if ((ch == '\n' && last_ch == '\r') || (client->end_sign != 0 && ch == client->end_sign)
|| get_urc_obj(client))
{
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;
}
break;
}
last_ch = ch;
}
#ifdef AT_PRINT_RAW_CMD
at_print_raw_cmd("recvline", client->recv_line_buf, read_len);
#endif
return read_len;
}
/** 接收一行数据后解析一行,解析完成告诉应用线程收到回复了,再由应用线程判断回复是否正确 */
/** AT解析线程 */
static void client_parser(at_client_t client)
{
const struct at_urc *urc;
while(1)
{
if (at_recv_readline(client) > 0)
{
if ((urc = get_urc_obj(client)) != RT_NULL)
{
/* current receive is request, try to execute related operations */
if (urc->func != RT_NULL)
{
urc->func(client, client->recv_line_buf, client->recv_line_len);
}
}
else if (client->resp != RT_NULL)
{
at_response_t resp = client->resp;
/* current receive is response */
client->recv_line_buf[client->recv_line_len - 1] = '\0';
if (resp->buf_len + client->recv_line_len < resp->buf_size)
{
/* copy response lines, separated by '\0' */
rt_memcpy(resp->buf + resp->buf_len, client->recv_line_buf, client->recv_line_len);
/* update the current response information */
resp->buf_len += client->recv_line_len;
resp->line_counts++;
}
else
{
client->resp_status = AT_RESP_BUFF_FULL;
LOG_E("Read response buffer failed. The Response buffer size is out of buffer size(%d)!", resp->buf_size);
}
/* check response result */
if (rt_memcmp(client->recv_line_buf, AT_RESP_END_OK, rt_strlen(AT_RESP_END_OK)) == 0
&& resp->line_num == 0)
{
/* get the end data by response result, return response state END_OK. */
client->resp_status = AT_RESP_OK;
}
else if (rt_strstr(client->recv_line_buf, AT_RESP_END_ERROR)
|| (rt_memcmp(client->recv_line_buf, AT_RESP_END_FAIL, rt_strlen(AT_RESP_END_FAIL)) == 0))
{
client->resp_status = AT_RESP_ERROR;
}
else if (resp->line_counts == resp->line_num && resp->line_num)
{
/* get the end data by response line, return response state END_OK.*/
client->resp_status = AT_RESP_OK;
}
else
{
continue;
}
client->resp = RT_NULL;
rt_sem_release(client->resp_notice); /** 通知应用线程,收到指令回复了 */
}
else
{
// log_d("unrecognized line: %.*s", client->recv_line_len, client->recv_line_buf);
}
}
}
}
五、总结
通过对AT组件底层代码分析,对他的架构有了整体了解,对于不定长数据的接收是以超时和行结束符或者自定义结束符来判断的,同时单独开启一个线程专门用来接收串口数据并做初步处理。和应用线程的通信依靠互斥量和信号量来实现同步。此架构是为了观测分层思想,将底层与应用层分开,因此会比较复杂,同时对RT-Thread的依赖较多,不便于移植到其他RTOS,希望有人能将此组件的架构重新写一份代码,这个代码应该做到方便移植,减少对系统的依赖。