关键词:AT命令、AT命令处理机、单向链表、函数指针、匹配分发
上一章我们介绍了通过UART串口,构建一个解析处理“命令行”输入的方法,由于比较类似,趁热打铁,我们再介绍一下处理“AT命令”的方法,因为他们的处理流程是一样的,只是具体的解析有所差异,"AT命令"在IOT领域目前有着广泛的应用,诸如2G/3G/4G通信模组、WIFI模组、蓝牙模组、Lora功能模组、UWB功能模组以及其他很多终端设备都使用AT命令与外部进行交互通信,所以有必要介绍一下。
AT命令的历史由来
AT命令是AT指令集的简称,AT 是英文Attention的缩写,AT命令的特点是每条命令均以字母"AT"开头,"AT"后面再跟上不同的符号、字母、数字来表明不同的功能,并以\n为结束的一套文本指令集。AT指令最早由美国贺氏公司发明并应用于其1981年研制的贺氏智能调制解调器(Hayes Smartmodem)中,计算机可以通过这些命令来控制MODEM实现摘机、拨号、挂机等操作,所以AT 命令集也称Hayses 指令集(Hayes command set)。随着技术的进步,贺氏公司和其推出的低速拨号MODEM 早已退出历史舞台,但他们开发的AT命令因其方便实用的特点被保留并沿用下来。在第2代移动通信时代,当时移动通信领域主要的移动电话生产厂商诺基亚、爱立信、摩托罗拉和HP共同为GSM 研制了一整套 AT命令,作为控制GSM通信模块的通信协议,AT命令在此基础上继续扩充完善并加入GSM 07.05 标准以及后来的 GSM 07.07 标准,使得AT命令作为一种标准通信协议得以广泛应用。随后的GPRS/3G/4G时代AT命令继续发挥着重要的作用,在物联网时代的今天,AT命令广泛的使用在嵌入式物联网开发领域,不仅在传统的蜂窝模块的应用上,在诸如WIFI、蓝牙、NFC等其他物联网接入的开发上,AT命令作为一种通用控制协议也得到广泛使用。
我们先来介绍一下AT命令,完整的AT命令分为两部分,AT请求和AT响应。
AT命令语句
AT命令是一种文本命令,即字符串命令,由请求端发送AT命令请求,接收端接收AT命令,处理并作出响应。从AT命令请求响应的角色划分,我们把AT命令请求端称作AT命令client,把AT命令接收处理的一端称作AT Server。在通信领域,请求方一般是DCE(数据通信设备),接收方是DTE(数据终端设备)。AT命令语句类型通常有以下三种:AT命令请求、AT命令响应、未经请求的主动上报URC。详见下:
AT命令请求
AT命令有固定的语法格式,每条命令必须以前缀AT或at为开头,以<CR>回车符为命令行终止。AT命令的响应形式为:<CR><LF><response><CR><LF> 。为表述方便,有时我们会省略<CR><LF>,仅显示命令和响应内容。
通常AT命令可以在语法上分为三类:基础类、S参数类和扩展类,如下所列。
- 基础类
基础类AT命令的格式为AT<x><n>或AT&<x><n>,其中<x>是命令,<n>是该命令的参数。常见的AT<x><n>格式命令如下:
命令 | 功能 | 示例 |
ATD<电话号码> | 拨号 | ATD1502955xxxx(拨打电话1502955xxxx) |
ATA | 应答来电 | ATA(手动应答) |
ATH | 挂断 | ATH(挂断当前连接) |
ATE<n> | 回显设置 | ATE0(关闭回显);ATE1(开回显) |
常见的AT&<x><n>格式命令如下:
其中 &
- 表示硬件相关的设置;<x>
- 单字母参数类别标识符;<n>
- 数字参数值(通常0或1)
命令 | 功能 | 示例 |
AT&Rn | RTS/CTS流控 | AT&R0 禁用流控;AT&R1 使能流控 |
AT&Fn | 恢复预设配置 | AT&F0(恢复出厂设置) |
- S参数类
此类AT命令格式有ATS<n>=<m>,其中<n>是将要设置的S寄存器的索引,<m>是赋予的参数
值。
- 扩展类
扩展类AT命令可以在多种模式下运行,如下表所示:
扩展类AT命令类型 语句 说明 |
测试命令 AT+<cmd>=? 测试是否存在相应的命令,并返回有关其 参数的 类型、值或范围的信息。 |
查询命令 AT+<cmd>? 查询相应命令的当前参数值。 |
设置命令 AT+<cmd>=<p1>[,<p2>[,<p3>[...]]] 设置用户可定义的参数值。 |
执行命令 AT+<cmd> 返回特定的参数信息或执行特定的操作。 |
AT命令响应
AT命令的响应由两部分组成,一部分是本条命令的响应消息,一部分是最终的响应结果,成功会返回OK;失败或错误返回ERROR或+CME ERROR:<err>。 当AT命令Server处理完一条命令,会发送请求的响应消息,并会返回OK、ERROR或+CME ERROR: <err>作为响应结束,表示本条命令全部处理完成,并准备好接收下一条新命令。
以下是响应消息的格式:
<CR><LF>+CMD: <parameters><CR><LF>
<CR><LF>OK<CR><LF>
或者
<CR><LF><parameters><CR><LF>
<CR><LF>OK<CR><LF>
AT命令未经请求的结果码URC
AT server端的一些状态变化、事件消息等需要主动上报或通知给AT client时会通过未经请求的结果码URC形式进行发送。也就是说这类上报不需要client请求,属于Server端的自发的主动上报行为,主要由某些事件触发上报,如网络断连、短信接收、来电提醒。另外需要注意的是虽然URC一般由相应的关键字作区分提示,但是server端还是要尽量避免将URC上报同AT响应混合在一起进行响应,避免client误将URC作为响应造成处理错误。
构建AT命令处理机
经过上面的介绍大家都对AT命令有了一定的了解,我们这里就介绍一种构建扩展类的AT命令的处理机的方法,使系统可以和外部进行交互,对外部的AT请求进行响应。如上所述,
命令结构元素
-
AT
- 命令前缀(Attention) -
名称- 基础指令中的"D"、"H"或“&F”,S指令中的"S",扩展类指令中的“+<cmd>”均视作命令名
-
符号-等号、问号、等号问号组合
-
参数-分无参、单个参数、多个参数
根据一条AT命令中是否包含以上元素,我们将AT命令进行分类如下:
(1)无名称 如命令AT,单独一个前缀
(2)只有名称,无参数,无符号,如基础指令:ATA,扩展指令AT+<cmd>
(3)有名称,有参数,如基础指令:ATE<n>
(4)有名称和问号,如扩展指令AT+<cmd>?
(5)有名称和等号、参数,如扩展指令AT+<cmd>=<p1>[,<p2>[,<p3>[...]]]
(6)有名称、等号和问号,如扩展指令AT+<cmd>=?
根据以上分析,我们构建命令结构体如下:
typedef struct
{
uint32_t element;
char name[AT_CMD_NAME_MAX_LEN];
uint32_t parameter_num;
parameter_t *p_para_line;
}AtCmd_t;
其中成员element记录了该命令的构成,即由元素名称、等号、问号、参数等构成的不同组合,用于判断命令格式;name存储命令名称,parameter_num记录参数个数,p_para_line是一个链表结构,将参数通过单向链表的形式连接起来,可以满足多个参数的需求。
解析AT命令字符串,将解析结果记录到AtCmd_t结构体中:
static int at_cmd_decode(char *at_cmd,uint16_t cmd_len)
{
uint16_t i=0;
char *pat_cmd=NULL;
if(at_cmd==NULL||cmd_len<2)
{
printf("\r\nERROR\r\n");
return -1;
}
for(i=1;i<=cmd_len;i++)
{
if( (at_cmd[i-1]=='a'||at_cmd[i-1]=='A')&&(at_cmd[i]=='t'||at_cmd[i]=='T') )
{
pat_cmd=at_cmd+i-1;
break;
}
}
if(pat_cmd==NULL)
{
printf("ERROR:no 'AT' found\r\n");
return -1;
}
if(cmd_len==2)//'AT'
{
g_at_cmd.element=0x00;
}
else if('&'==pat_cmd[2])//&参数类指令解析:AT&<x><n>
{
char para[8];
g_at_cmd.element=ELEM_NAME|ELEM_PARAMETER;
g_at_cmd.name[0]='&';
g_at_cmd.name[1]=pat_cmd[3];
g_at_cmd.name[2]='\0';
para[0]=pat_cmd[4];
para[1]='\0';
insert_at_cmd_parameter(para);
}
else if('S'==pat_cmd[2])//S参数类指令解析:ATS<n>=<m>
{
char *eq=strstr((char *)pat_cmd,"=");
if(eq)
{
char n_str[20], m_str[20];
int n,m;
g_at_cmd.element=ELEM_NAME|ELEM_EQUAL_SIGN|ELEM_PARAMETER;
g_at_cmd.name[0]='S';
g_at_cmd.name[1]='\0';
memset(n_str,'\0',sizeof(n_str));
memset(m_str,'\0',sizeof(m_str));
if (sscanf(pat_cmd,"ATS%d=%d",&n,&m) == 2)
{
sprintf(n_str,"%d",n);
sprintf(m_str,"%d",m);
insert_at_cmd_parameter(n_str);
insert_at_cmd_parameter(m_str);
}
else
{
printf("sscanf error\n");
}
}
}
else if('+'==pat_cmd[2])//扩展指令解析
{
if(strstr((char *)pat_cmd,"?"))
{
if(strstr((char *)pat_cmd,"="))
{
g_at_cmd.element=ELEM_NAME|ELEM_QUESTION_MARK|ELEM_EQUAL_SIGN;
if(get_extend_at_cmd_name(pat_cmd,cmd_len,'=',g_at_cmd.name))
{
printf("=? err\r\n");
return -1;
}
}
else
{
g_at_cmd.element=ELEM_NAME|ELEM_QUESTION_MARK;
if(get_extend_at_cmd_name(pat_cmd,cmd_len,'?',g_at_cmd.name))
{
printf("? err\r\n");
return -1;
}
}
}
else if(strstr((char *)pat_cmd,"="))
{
g_at_cmd.element=ELEM_NAME|ELEM_EQUAL_SIGN|ELEM_PARAMETER;
if(get_extend_at_cmd_name(pat_cmd,cmd_len,'=',g_at_cmd.name))
{
printf("= err\r\n");
return -1;
}
if(get_extend_at_cmd_parameters(pat_cmd,cmd_len,&(g_at_cmd.parameter_num)))
{
printf("Parameter err\r\n");
return -1;
}
}
else
{
g_at_cmd.element=ELEM_NAME;
if(get_extend_at_cmd_name(pat_cmd,cmd_len,'\r',g_at_cmd.name))
{
printf("PART_NAME err\r\n");
return -1;
}
}
}
else //单字符名称类指令解析
{
}
return 0;
}
根据得到的AtCmd_t结构体结果,进行命令分发处理,思路同上章命令行处理方式类似,
typedef int (*at_cmd_func)(const AtCmd_t *pCmd);
typedef struct
{
const char * pAtCmd;
at_cmd_func pCmdFunc;
const char * pCmdInfo;
}AtCmdMap_t;
static int at_cmd_s_cmd_Handle(const AtCmd_t *pCmd);
static int at_cmd_atcmd_Handle(const AtCmd_t *pCmd);
static int at_cmd_test_parse_parameter_Handle(const AtCmd_t *pCmd);
static int at_cmd_softver_handle(const AtCmd_t *pCmd);
static int at_cmd_reboot_handle(const AtCmd_t *pCmd);
static int at_cmd_led_handle(const AtCmd_t *pCmd);
static int at_cmd_gps_handle(const AtCmd_t *pCmd);
static int at_cmd_battery_handle(const AtCmd_t *pCmd);
static int at_cmd_factory_handle(const AtCmd_t *pCmd);
static AtCmdMap_t at_cmd_func_table[]=
{
//基础指令
{"&F" ,at_cmd_atcmd_Handle , "&F cmd"},
//S指令
{"S" ,at_cmd_s_cmd_Handle , "S cmd"},
//扩展指令
{"+test" ,at_cmd_test_parse_parameter_Handle , "reboot the system"},
{"+version" ,at_cmd_softver_handle , "show version info"}, //at+version/at+version?
{"+reboot" ,at_cmd_reboot_handle ,"reboot the system\r\n"},//at+reboot
{"+led" ,at_cmd_led_handle ,"set led example:led on/led off\r\n"},//at+led=on
{"+gps" ,at_cmd_gps_handle ,"gps open example:gps on/gps off\r\n"},
{"+battery" ,at_cmd_battery_handle ,"get battery voltage\r\n"},
{"+factory" ,at_cmd_factory_handle ,"factory test example:factory speaker\r\n"},
};
static int at_cmd_func_distributer(const AtCmd_t *pAtCmd)
{
uint32_t i=0;
uint32_t cmd_num=sizeof(at_cmd_func_table)/sizeof(at_cmd_func_table[0]);
if(NULL==pAtCmd)
{
printf("Err:pAtCmd NULL\r\n");
return -1;
}
for(i=0;i<cmd_num;i++)
{
if(0==strncasecmp(pAtCmd->name,at_cmd_func_table[i].pAtCmd,(strlen(pAtCmd->name)<strlen(at_cmd_func_table[i].pAtCmd)) ?(strlen(pAtCmd->name)): (strlen(at_cmd_func_table[i].pAtCmd))))
{
return at_cmd_func_table[i].pCmdFunc(pAtCmd);
}
}
return 0;
}
具体到每条处理函数,AtCmd_t的类型的结果作为入参,可以通过其中的element成员来判断命令的格式是否正确,通过parameter_num成员来判断参数个数是否正确,p_para_line链表来访问参数数据。如:
static int at_cmd_gps_handle(const AtCmd_t *pCmd)
{
char *arg0=pCmd->p_para_line->data;//参数
if(pCmd->element==ELEM_NAME|ELEM_EQUAL_SIGN|ELEM_PARAMETER)//判断命令格式是否满足要求
{
if(1==pCmd->parameter_num)//判断参数个数
{
if(strstr(arg0,"on")) //识别参数进行具体的处理
{
//调用gps on接口
//gps_turn_on();
printf("\r\ngps on\r\n");
}
else
{
//调用gps off接口
//gps_turn_off();
printf("\r\ngps off\r\n");
}
}
}
else
{
printf("ERROR\r\n");
}
}
以上就是构建AT命令处理机的整个实现过程,适用于基础类、S类、扩展类型指令,我们实际开发中用到的更多的是扩展类指令。AT命令处理的流程和思路和上章处理命令行基本是一致的,
在命令的解析方法上有差异,这里解析出了命令的结构元素用于匹配命令格式,通过单向链表的方式来存储多个参数,不受参数个数限制,非常方便。
到此结束,感觉各位同学的阅读、批评,指正!