提交 00e33cb2 编写于 作者: M mculover666

finish AT_SAL_User_Guide document

上级 ad5f75d4
......@@ -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
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 air724_echo_close(void)
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,<data_len>:
<data context>
+RECEIVE: prefix
0: scoket id
模组上报的数据格式:
+QIURC: "recv",<sockid>,<datalen>
<data content>
*/
//读走+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)
/* 读取'\r'之后的'\n',不作任何处理 */
if (tos_at_uart_read(&data, 1) != 1)
{
if (tos_at_uart_read(&data, 1) != 1)
{
return;
}
return;
}
if (data == '\n')
{
break;
}
}
//根据之前解析到的socket id和data_len,循环读取数据,并写入到socket id对应通道的接收缓冲区
/* 根据解析出的数据长度和缓冲区的长度,循环读取数据内容,写入到对应 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;
}
```
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册