From 00e33cb2841b71462fbe4fd7733695d715ba5588 Mon Sep 17 00:00:00 2001 From: mculover666 <2412828003@qq.com> Date: Tue, 11 Aug 2020 11:32:52 +0800 Subject: [PATCH] finish AT_SAL_User_Guide document --- ...AT_Firmware_and_SAL_Firmware_User_Guide.md | 373 +++++++++++++++--- 1 file changed, 325 insertions(+), 48 deletions(-) diff --git a/doc/27.AT_Firmware_and_SAL_Firmware_User_Guide.md b/doc/27.AT_Firmware_and_SAL_Firmware_User_Guide.md index aa420d4f..3129124f 100644 --- a/doc/27.AT_Firmware_and_SAL_Firmware_User_Guide.md +++ b/doc/27.AT_Firmware_and_SAL_Firmware_User_Guide.md @@ -665,11 +665,11 @@ socket_id_1 = tos_sal_module_connect("117.50.111.72", "8001", TOS_SAL_PROTO_TCP) ![tcp_example_result_uart](./image/AT_Firmware/tcp_example_result_uart.png) -在socket0的服务端查看模组发送的消息: +在 socket0 的服务端查看模组发送的消息: ![tcp_example_result_server1](./image/AT_Firmware/tcp_example_result_server1.png) -在socket1的服务端查看模组发送的消息: +在 socket1 的服务端查看模组发送的消息: ![tcp_example_result_server2](./image/AT_Firmware/tcp_example_result_server2.png) @@ -677,23 +677,180 @@ socket_id_1 = tos_sal_module_connect("117.50.111.72", "8001", TOS_SAL_PROTO_TCP) # 5. 如何适配一个新的通信模组驱动 -适配一个新的通信模组,就是实现SAL框架的整套函数指针所定义的函数。 +基于模组的AT指令集,使用 TencentOS-tiny AT 框架与模组交互,实现SAL框架所定义的函数,这个过程称为通信模组适配。 -建议按照以下的流程进行适配: +本文中我以移远通信的4G通信模组 EC20 作为示例,讲述一个全新的通信模组适配流程。 -① **寻找相同类型、相同厂商已有的模组驱动,复制之后开始修改适配**。 +## Step1. 使用串口助手调试,熟悉该模组的AT指令集 -比如我要适配 4G Cat.1 模组Air724,肯定是找一份差异不大的模组驱动开始改,比如仓库里已有的4G Cat.4 模组EC20的驱动。 -② 普通的AT指令交互流程实现示例(一般为配置指令): +适配SAL层需要熟悉模组的三类AT指令: + +- 基本查询配置指令 +- TCP/IP网络协议栈AT指令 +- TCP/IP网络协议栈数据接收机制 + +比如EC20相关的AT指令如下: + +### ① 基本查询配置指令 + +- 测试AT指令是否正常? + +``` +AT + +OK +``` + +- 查询SIM卡是否正常? ```c -static int air724_echo_close(void) +AT+CPIN? + ++CPIN: READY + +OK +``` + +- 查询模组的信号强度 + +```c +AT+CSQ + ++CSQ: 17,0 + +OK +``` + +- 查询模组是否注册到GSM网络 + +``` +AT+CREG? + ++CREG: 0,1 + +OK +``` + +- 查询模组是否注册上GPRS网络 + +``` +AT+CGREG? + ++CGREG: 0,1 + +OK +``` + +- 设置GPRS的APN + +``` +AT+QICSGP=1,1,"CMNET" + +OK +``` + +- 激活移动场景 + +``` +AT+QIACT=1 + +OK +``` + +### ② TCP/IP网络协议栈AT指令 + +TCP/IP网络协议栈至少需要TCP/UDP socket的通信AT指令,其它上层协议的AT指令暂时不用。 + +- 建立Socket +``` +AT+QIOPEN=1,0,"TCP","117.50.111.72",8902,0,0 + +OK + ++QIOPEN: 0,0 +``` + +>在建立socket的时候需要注意,有的通信模组需要提前使用AT指令配置单链路模式还是多链路模式(eg. ESP8266),而有的通信模组默认直接支持多链路模式,无需配置。 + +- 发送数据 + +```c +AT+QISEND=0 + +> hello<0x1a> + +SEND OK +``` + +>发送数据的时候需要注意,有的通信模组发送数据使用ASCII字符(eg. ESP8266、EC20等),而有的通信模组发送数据使用十六进制(eg. NB-IoT类模组) + +- 接收数据 + +接下来的一节重点讲述。 + +- 关闭Socket + +``` +AT+QICLOSE=0 + +OK +``` + +### ③ TCP/IP网络协议栈数据接收机制 + +通信模组在接收到服务器发来的数据时,会有两种方式上报给MCU: + +- 使用固定的IP头上报socket id和数据长度,需要再次去读取数据(缓冲模式接收) + +```c +//模组上报 ++QIURC: "recv",0 + +//MCU发出AT指令去读取数据 +AT+QIRD=0,1500 + ++QIRD: 14 +Hello, client! + +OK +``` + +- 使用固定的IP头上报socket id和数据长度,同时一起上报数据(直接模式接收) +```c ++QIURC: "recv",0,14 +Hello, client! +``` + +有的模组只支持某一种模式,有的模组两种模式都支持,可以自己配置,对于TencentOS-tiny的AT框架来说,第二种直接上报模式解析起来会更加方便。 + +## Step2. 三类AT指令,三种实现方式 + +上面讲述了适配SAL层需要熟悉模组的三类AT指令:基本查询配置指令、TCP/IP网络协议栈AT指令、TCP/IP网络协议栈数据接收机制,这节讲述如何使用AT框架实现这三类AT指令。 + +① 只需要判断是否返回OK的AT指令 + +这一类AT指令的实现函数除了指令内容不同,别的都相同,比如关闭回显的AT指令: + +``` +ATE0 + +OK +``` + +对应的实现方法如下: +```c +static int ec20_echo_close(void) { at_echo_t echo; + /* 创建一个echo 对象,缓冲区为NULL,期望字符串为NULL */ tos_at_echo_create(&echo, NULL, 0, NULL); - tos_at_cmd_exec(&echo, 300, "ATE0\r\n"); + + /* 执行AT命令,超时时长1000ms */ + tos_at_cmd_exec(&echo, 1000, "ATE0\r\n"); + + /* 判断执行结果是否为OK */ if (echo.status == AT_ECHO_STATUS_OK) { return 0; @@ -702,26 +859,43 @@ static int air724_echo_close(void) } ``` -③ 需要解析AT指令解析结果的交互流程实现示例(一般为查询指令): +② 需要判断是否OK,也需要解析执行结果的AT指令 +这一类AT指令的实现函数中,不同点在于,创建echo对象的时候需要传入一个buffer来存放指令执行的结果,比如查询信号强度的AT指令: + +``` +AT+CSQ + ++CSQ: 17,0 + +OK +``` + +对应的实现方法如下: ```c -static int air724_signal_quality_check(void) +static int ec20_signal_quality_check(void) { int rssi, ber; at_echo_t echo; char echo_buffer[32], *str; int try = 0; - + /* 创建echo对象,传入一个缓冲区存放AT命令执行结果 */ + tos_at_echo_create(&echo, echo_buffer, sizeof(echo_buffer), NULL); + + /* 尝试检测10次,一旦有一次正常,返回 */ while (try++ < 10) { - tos_at_echo_create(&echo, echo_buffer, sizeof(echo_buffer), NULL); + /* 执行AT命令,超时时长1000ms */ tos_at_cmd_exec(&echo, 1000, "AT+CSQ\r\n"); + + /* 判断执行结果是否返回了OK */ if (echo.status != AT_ECHO_STATUS_OK) { return -1; } + /* 从AT指令的执行结果中解析提取CSQ值进行判断 */ str = strstr(echo.buffer, "+CSQ:"); sscanf(str, "+CSQ:%d,%d", &rssi, &ber); if (rssi != 99) { @@ -733,37 +907,40 @@ static int air724_signal_quality_check(void) } ``` -④ 事件处理(一般为模组主动上报的指令): +③ 模组主动上报的数据处理 +这一类AT指令对应TCP/IP协议栈接收数据的上报机制,使用AT框架的事件机制进行处理。 + +首先将固定的ip头和事件处理回调函数注册: ```c -at_event_t air724_at_event[] = { - { "+RECEIVE,", air724_incoming_data_process}, +at_event_t ec20_at_event[] = { + { "+QIURC: \"recv\",", ec20_incoming_data_process}, //处理远程服务器发来的数据 + { "+QIURC: \"dnsgip\",", ec20_domain_data_process}, //处理域名解析结果,暂时不管 }; ``` -回调函数的处理示例如下(添加了处理过程的注释): + +事件处理回调函数自己编写,主要作用是提取模组上报的scoketid、数据长度、数据内容,然后将数据内容写入到对应socket id 的channel中。 + +>需要注意,AT框架一旦读取解析到固定的IP头,则停止解析,拉起对应的回调函数,所以在回调函数中可以继续从缓冲区中一边读取一边解析。 + +解析示例如下: ```c -__STATIC__ void air724_incoming_data_process(void) +__STATIC__ void ec20_incoming_data_process(void) { uint8_t data; int channel_id = 0, data_len = 0, read_len; uint8_t buffer[128]; /* - +RECEIVE, 0,: - - - +RECEIVE: prefix - 0: scoket id + 模组上报的数据格式: + +QIURC: "recv",, + */ - //读走+RECEIVE头之后的一个空格 - if (tos_at_uart_read(&data, 1) != 1) - { - return; - } + /* 注册的ip头是:[+QIURC: "recv",]回调函数被拉起执行后,接着处理后边的数据即可 */ - //读取并解析之后的socket id值 + /* 读取解析socket id */ while (1) { if (tos_at_uart_read(&data, 1) != 1) @@ -778,43 +955,38 @@ __STATIC__ void air724_incoming_data_process(void) channel_id = channel_id * 10 + (data - '0'); } - //读取并解析之后的data_len值 - while (1) + /* 读取解析数据长度 */ + while (1) { if (tos_at_uart_read(&data, 1) != 1) { return; } - if (data == ':') + if (data == '\r') { break; } data_len = data_len * 10 + (data - '0'); } - - //读走回车和换行 - while (1) - { - if (tos_at_uart_read(&data, 1) != 1) - { - return; - } - if (data == '\n') - { - break; - } - } - - //根据之前解析到的socket id和data_len,循环读取数据,并写入到socket id对应通道的接收缓冲区 + /* 读取'\r'之后的'\n',不作任何处理 */ + if (tos_at_uart_read(&data, 1) != 1) + { + return; + } + + /* 根据解析出的数据长度和缓冲区的长度,循环读取数据内容,写入到对应 socket id 的channel中 */ do { #define MIN(a, b) ((a) < (b) ? (a) : (b)) read_len = MIN(data_len, sizeof(buffer)); + + /* 读取数据 */ if (tos_at_uart_read(buffer, read_len) != read_len) { return; } + /* 写入到对应的channel中 */ if (tos_at_channel_write(channel_id, buffer, read_len) <= 0) { return; } @@ -826,7 +998,112 @@ __STATIC__ void air724_incoming_data_process(void) } ``` - +## Step3. 整体适配流程 + +前两步都是细节性的处理,这一步从整体的角度讲述适配流程。 + +① 模组初始化 + +```c +static int ec20_init(void) +{ + printf("Init ec20 ...\n" ); + + /* 关闭回显 */ + + if (ec20_echo_close() != 0) + { + printf("echo close failed,please check your module\n"); + return -1; + } + + /* 检测SIM卡是否正常 */ + if(ec20_sim_card_check() != 0) + { + printf("sim card check failed,please insert your card\n"); + return -1; + } + + /* 检测信号强度是否正常 */ + if (ec20_signal_quality_check() != 0) + { + printf("signal quality check status failed\n"); + return -1; + } + + /* 检测是否注册到GSM网络 */ + if(ec20_gsm_network_check() != 0) + { + printf("GSM network register status check fail\n"); + return -1; + } + + /* 检测是否注册到GPRS网络 */ + if(ec20_gprs_network_check() != 0) + { + printf("GPRS network register status check fail\n"); + return -1; + } + + /* 关闭APN */ + if(ec20_close_apn() != 0) + { + printf("close apn failed\n"); + return -1; + } + + /* 设置APN,激活移动场景 */ + if (ec20_set_apn() != 0) { + printf("apn set FAILED\n"); + return -1; + } + + printf("Init ec20 ok\n" ); + return 0; +} +``` + +② 将实现的函数映射到SAL框架上: + +```c +sal_module_t sal_module_ec20 = { + .init = ec20_init, + .connect = ec20_connect, + .send = ec20_send, + .recv_timeout = ec20_recv_timeout, + .recv = ec20_recv, + .sendto = ec20_sendto, + .recvfrom = ec20_recvfrom, + .recvfrom_timeout = ec20_recvfrom_timeout, + .close = ec20_close, + .parse_domain = ec20_parse_domain, +}; +``` + +③ 再次封装,留出一个外部调用接口,供上层应用程序调用: + +```c +int ec20_sal_init(hal_uart_port_t uart_port) +{ + /* 初始化AT框架及其串口 */ + if (tos_at_init(uart_port, ec20_at_event, + sizeof(ec20_at_event) / sizeof(ec20_at_event[0])) != 0) { + return -1; + } + + /* 将第②步中映射的函数关系,注册到SAL框架 */ + if (tos_sal_module_register(&sal_module_ec20) != 0) { + return -1; + } + + /* 调用SAL初始化函数,因为接口和映射的存在,最终调用到ec20_init */ + if (tos_sal_module_init() != 0) { + return -1; + } + + return 0; +} +```