1. AT客戶端框架
在之前的三篇教程中,我們都是直接使用串口助手發送AT指令與模組通信,本篇教程就來探索一下如何使用 MCU 中的串口模組交互。
什么是AT客戶端
在使用AT指令的時候,直接發送AT指令的一端稱為客戶端(AT Client),接收AT指令並返回響應的一端稱為服務端(AT Server)。
ESP8266、M26、BC35-G這些通信模組都是接收我們發送的AT指令,所以稱為AT命令服務端,MCU 需要向模組主動發送AT指令,稱為AT客戶端,它們之間的通信架構如下:
為什么需要AT客戶端框架
首先來看上圖中的三個數據流:
- 發送AT指令:可以直接調用HAL庫提供的API發送,AT框架並無太大作用;
- 等待接收返回結果:可以直接調用HAL庫的API使用中斷方式接收;
- 接收服務端主動發送的數據:可以直接調用HAL庫的API使用中斷方式接收;
三條數據流都可以調用HAL庫的API直接實現呀,為什么要設計一層AT框架呢?
在直接調用HAL庫實現的時候,首先無法保證每次模組向 MCU 發送的數據都能完整的被接收,所以,我們需要設計一層串口驅動以保證數據在任何時候都可以被完整的接收進緩沖區。
其次,在接收數據之后,難點在於對數據的處理,判斷AT指令發送的數據是不是正常的返回結果,從返回結果中提取有效信息等等,這些如果每條指令接收之后,都去寫代碼依次判斷,代碼量陡增暫且不說,編程的難度也是直接上升,所以,我們需要基於串口驅動,在保證數據被完整接收的前提之上,再根據AT命令通信的特點,設計一層AT框架,專門負責解析數據,提取有效信息。
2. 剖析串口驅動框架實現
串口驅動直接使用LiteOS提供的驅動框架實現,由於其特殊性,最底層的驅動框架實現文件放在了工程目錄中,調用HAL庫提供的API實現:
uart_at.c文件中,主要完成了兩個功能:
- 串口初始化
- 實現串口驅動框架的讀寫,並注冊串口設備到系統中
2.1. 串口初始化
串口初始化函數的調用架構如圖:
其中默認初始化的是LPUART1,如果將其它串口作為AT指令的串口,修改這兩行代碼即可:
2.2. ring_buffer
ring_buffer
是專門實現的用戶存放接收數據的緩沖區,用戶只需要調用read和write操作緩沖區即可,其實現文件在iot-link SDK的IoT_LINK_1.0.0\iot_link\link_misc
路徑下:
ring_buffer
在串口初始化函數中被調用初始化:
緩沖區大小在宏定義中聲明:
初始化之后,向LiteOS注冊的中斷服務函數只需要調用ring_buffer_write向緩沖區不停的寫入接收到的數據,即可保證串口數據被完整的接收。
2.3. 串口驅動框架實現
串口驅動框架中,因為已經有了初始化函數,所以只需要實現read函數和write函數即可,實現的函數架構如下:
因為數據全部保存在了ring_buffer中,所以串口驅動的read API實現用緩沖區提供的讀取函數實現即可。
實現read和write兩個函數之后,調用如下的宏定義,即可將設備和驅動注冊到系統中:
OSDRIV_EXPORT(uart_at_driv,CONFIG_AT_DEVICENAME,(los_driv_op_t *)&s_at_op,NULL,O_RDWR);
CONFIG_AT_DEVICENAME
由用戶指定,不重復即可,在iot_link_config.h
文件中,稍后會講解。
3. 剖析AT客戶端框架
AT客戶端框架的實現源碼在SDK的IoT_LINK_1.0.0\iot_link\at
文件夾下:
AT框架的架構如下:
如圖,因為串口設備已經注冊到了系統中,所以AT框架的底層發送和接收函數直接調用LiteOS設備驅動框架提供的API實現,除了上述圖中的這些,還涉及到大量的使用信號量、互斥鎖、字符串比較等函數進行AT指令匹配處理,提取結果的代碼,這些不是理解AT框架的重點,所以圖中未給出。
在實現了AT框架之后,最終留給用戶使用的接口只要三個,即可完成AT指令的交互,非常簡潔:
at_init
:初始化AT框架,啟動AT數據接收引擎(優先級為10)at_command
:發送AT指令並匹配指定的返回結果at_oobregister
:監控AT主動上報的數據
接下來,我們以ESP8266模組入網為例,講述如何使用AT框架提供的簡潔API與模組交互。
4. AT客戶端框架的使用
AT框架使能及配置
經過上面的講解,完整的AT框架其實包括設備驅動框架和AT框架實現兩部分,所以首先需要在配置文件中使能驅動框架和AT框架。
打開之前新建的HelloWorld工程(如果沒有可以參考之前的教程新建一個HelloWorld工程),在.sdkconfig
中進行配置,如圖:
使能之后,不僅驅動框架的源碼和AT框架的源碼會被加入工程,還會進行自動初始化。
打開SDK中下的IoT_LINK_1.0.0\iot_link
目錄中的link_main.c
文件,其中在link_main
函數即可看到:
首先是驅動框架的初始化:
其次是串口硬件和AT框架的初始化:
在自動初始化的時候,可以看到串口通信波特率由宏定義CONFIG_AT_BAUDRATE
指定,串口設備注冊到系統的名稱由宏定義CONFIG_AT_DEVICENAME
指定,那么,這兩個宏定義在哪里指定呢?
不用的模組波特率不同,設備名稱當然也不盡相同,所以這兩個設置在工程目錄中的OS_CONFIG/iot_link_config.h
中,這里我們使用ESP8266模組實驗,設置如圖:
發送AT命令
發送AT指令的API原型及參數說明如下:
/**
* @brief:use this function to register a function that monitor the URC message
* @param[in]:cmd, the command to send
* @param[in]:cmdlen, the command length
* @param[in]:index, the command index, if you don't need the response, set it to NULL; this must be a string
* @param[in]:respbuf, if you need the response, you should supply the buffer
* @param[in]:respbuflen,the respbuf length
* @param[in]:timeout, the time you may wait for the response;and the unit is ms
*
* @return:0 success while -1 failed
* */
int at_command(const void *cmd, size_t cmdlen,const char *index,\
void *respbuf,size_t respbuflen,uint32_t timeout);
接下來我們在Demo
文件夾之下創建一個文件夾at_test_demo
,用於存放實驗文件,並在該文件夾之下新建本節所使用的實驗文件at_esp8266_demo.c
:
然后在文件中編輯以下內容:
#include <osal.h>
#include <string.h>
#include <at.h>
#define SSID "FAST_88A6"
#define PASSWD "18324701020"
static bool_t esp8266_atcmd(const char *cmd,const char *index)
{
int ret = 0;
ret = at_command((unsigned char *)cmd,strlen(cmd),index,NULL,0,5000);
if(ret >= 0)
{
return true;
}
else
{
return false;
}
}
static int at_esp8266_demo_entry()
{
char cmd[64];
int ret;
/* 測試AT是否OK,超時時間5S */
memset(cmd,0,64);
snprintf(cmd,64,"AT\r\n");
while(false == esp8266_atcmd(cmd, "OK"))
{
printf("AT Test fail, repeat.\r\n");
}
printf("AT test ok.\r\n");
/* 關閉回顯 */
memset(cmd,0,64);
snprintf(cmd,64,"ATE0\r\n");
ret = esp8266_atcmd(cmd, "OK");
if(ret == false)
{
printf("ATE0 test fail.\r\n");
}
else
{
printf("ATE0 test ok.\r\n");
}
/* 設置模式為AP+STA */
memset(cmd,0,64);
snprintf(cmd,64,"AT+CWMODE=3\r\n");
ret = esp8266_atcmd(cmd, "OK");
if(ret == false)
{
printf("AT+CWMODE=3 test fail.\r\n");
}
else
{
printf("AT+CWMODE=3 test ok.\r\n");
}
/* 連接路由器 */
memset(cmd,0,64);
snprintf(cmd,64,"AT+CWJAP=\"%s\",\"%s\"\r\n", SSID, PASSWD);
while(false == esp8266_atcmd(cmd, "OK"))
{
printf("try to join AP:%s fail, repeat.\r\n", SSID);
}
printf("AT+CWMODE=3 test ok.\r\n");
return 0;
}
int standard_app_demo_main()
{
osal_task_create("at_esp8266_demo",at_esp8266_demo_entry,NULL,0x800,NULL,12);
return 0;
}
結果:
獲取AT指令返回結果並提取有效信息
對於AT命令返回的結果,如果其中存放有效信息,我們可以在調用at_command時傳入一個緩沖區,如下,發送查詢模組的ip地址,並從中提取出ip地址:
在連接路由器的代碼之后,添加如下代碼:
/* 獲取ip地址 */
const char cs_cmd[] = "AT+CIFSR\r\n";
char buffer[150];
char *str;
uint8_t ip[4];
memset(buffer,0,150);
ret = at_command(cs_cmd,strlen(cs_cmd),"OK", buffer, 150, 5000);
if(ret < 0)
{
printf("AT+CIFSR test fail.\r\n");
}
else
{
printf("AT+CIFSR test ok.\r\n");
/* 提取ip地址 */
str = strstr(buffer,"STAIP");
str = str + 7;
sscanf(str,"%d.%d.%d.%d",&ip[0],&ip[1],&ip[2],&ip[3]);
printf("ip: %d.%d.%d.%d\r\n", ip[0], ip[1], ip[2], ip[3] );
}
實驗結果如圖: