finish AT_SAL_User_Guide document
This commit is contained in:
@@ -665,11 +665,11 @@ socket_id_1 = tos_sal_module_connect("117.50.111.72", "8001", TOS_SAL_PROTO_TCP)
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
在socket0的服务端查看模组发送的消息:
|
在 socket0 的服务端查看模组发送的消息:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
在socket1的服务端查看模组发送的消息:
|
在 socket1 的服务端查看模组发送的消息:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -677,23 +677,180 @@ socket_id_1 = tos_sal_module_connect("117.50.111.72", "8001", TOS_SAL_PROTO_TCP)
|
|||||||
|
|
||||||
# 5. 如何适配一个新的通信模组驱动
|
# 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
|
```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;
|
at_echo_t echo;
|
||||||
|
|
||||||
|
/* 创建一个echo 对象,缓冲区为NULL,期望字符串为NULL */
|
||||||
tos_at_echo_create(&echo, NULL, 0, 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)
|
if (echo.status == AT_ECHO_STATUS_OK)
|
||||||
{
|
{
|
||||||
return 0;
|
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
|
```c
|
||||||
static int air724_signal_quality_check(void)
|
static int ec20_signal_quality_check(void)
|
||||||
{
|
{
|
||||||
int rssi, ber;
|
int rssi, ber;
|
||||||
at_echo_t echo;
|
at_echo_t echo;
|
||||||
char echo_buffer[32], *str;
|
char echo_buffer[32], *str;
|
||||||
int try = 0;
|
int try = 0;
|
||||||
|
|
||||||
|
/* 创建echo对象,传入一个缓冲区存放AT命令执行结果 */
|
||||||
|
tos_at_echo_create(&echo, echo_buffer, sizeof(echo_buffer), NULL);
|
||||||
|
|
||||||
|
/* 尝试检测10次,一旦有一次正常,返回 */
|
||||||
while (try++ < 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");
|
tos_at_cmd_exec(&echo, 1000, "AT+CSQ\r\n");
|
||||||
|
|
||||||
|
/* 判断执行结果是否返回了OK */
|
||||||
if (echo.status != AT_ECHO_STATUS_OK)
|
if (echo.status != AT_ECHO_STATUS_OK)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 从AT指令的执行结果中解析提取CSQ值进行判断 */
|
||||||
str = strstr(echo.buffer, "+CSQ:");
|
str = strstr(echo.buffer, "+CSQ:");
|
||||||
sscanf(str, "+CSQ:%d,%d", &rssi, &ber);
|
sscanf(str, "+CSQ:%d,%d", &rssi, &ber);
|
||||||
if (rssi != 99) {
|
if (rssi != 99) {
|
||||||
@@ -733,37 +907,40 @@ static int air724_signal_quality_check(void)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
④ 事件处理(一般为模组主动上报的指令):
|
③ 模组主动上报的数据处理
|
||||||
|
|
||||||
|
这一类AT指令对应TCP/IP协议栈接收数据的上报机制,使用AT框架的事件机制进行处理。
|
||||||
|
|
||||||
|
首先将固定的ip头和事件处理回调函数注册:
|
||||||
```c
|
```c
|
||||||
at_event_t air724_at_event[] = {
|
at_event_t ec20_at_event[] = {
|
||||||
{ "+RECEIVE,", air724_incoming_data_process},
|
{ "+QIURC: \"recv\",", ec20_incoming_data_process}, //处理远程服务器发来的数据
|
||||||
|
{ "+QIURC: \"dnsgip\",", ec20_domain_data_process}, //处理域名解析结果,暂时不管
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
回调函数的处理示例如下(添加了处理过程的注释):
|
|
||||||
|
事件处理回调函数自己编写,主要作用是提取模组上报的scoketid、数据长度、数据内容,然后将数据内容写入到对应socket id 的channel中。
|
||||||
|
|
||||||
|
>需要注意,AT框架一旦读取解析到固定的IP头,则停止解析,拉起对应的回调函数,所以在回调函数中可以继续从缓冲区中一边读取一边解析。
|
||||||
|
|
||||||
|
解析示例如下:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
__STATIC__ void air724_incoming_data_process(void)
|
__STATIC__ void ec20_incoming_data_process(void)
|
||||||
{
|
{
|
||||||
uint8_t data;
|
uint8_t data;
|
||||||
int channel_id = 0, data_len = 0, read_len;
|
int channel_id = 0, data_len = 0, read_len;
|
||||||
uint8_t buffer[128];
|
uint8_t buffer[128];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
+RECEIVE, 0,<data_len>:
|
模组上报的数据格式:
|
||||||
<data context>
|
+QIURC: "recv",<sockid>,<datalen>
|
||||||
|
<data content>
|
||||||
+RECEIVE: prefix
|
|
||||||
0: scoket id
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//读走+RECEIVE头之后的一个空格
|
/* 注册的ip头是:[+QIURC: "recv",]回调函数被拉起执行后,接着处理后边的数据即可 */
|
||||||
if (tos_at_uart_read(&data, 1) != 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//读取并解析之后的socket id值
|
/* 读取解析socket id */
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
if (tos_at_uart_read(&data, 1) != 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');
|
channel_id = channel_id * 10 + (data - '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
//读取并解析之后的data_len值
|
/* 读取解析数据长度 */
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
if (tos_at_uart_read(&data, 1) != 1)
|
if (tos_at_uart_read(&data, 1) != 1)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data == ':')
|
if (data == '\r')
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
data_len = data_len * 10 + (data - '0');
|
data_len = data_len * 10 + (data - '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
//读走回车和换行
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
if (tos_at_uart_read(&data, 1) != 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data == '\n')
|
/* 读取'\r'之后的'\n',不作任何处理 */
|
||||||
{
|
if (tos_at_uart_read(&data, 1) != 1)
|
||||||
break;
|
{
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据之前解析到的socket id和data_len,循环读取数据,并写入到socket id对应通道的接收缓冲区
|
/* 根据解析出的数据长度和缓冲区的长度,循环读取数据内容,写入到对应 socket id 的channel中 */
|
||||||
do {
|
do {
|
||||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||||
read_len = MIN(data_len, sizeof(buffer));
|
read_len = MIN(data_len, sizeof(buffer));
|
||||||
|
|
||||||
|
/* 读取数据 */
|
||||||
if (tos_at_uart_read(buffer, read_len) != read_len) {
|
if (tos_at_uart_read(buffer, read_len) != read_len) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 写入到对应的channel中 */
|
||||||
if (tos_at_channel_write(channel_id, buffer, read_len) <= 0) {
|
if (tos_at_channel_write(channel_id, buffer, read_len) <= 0) {
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user