在上一節中,介紹了 ESP8266 的使用方法。不過上一節中都是通過串列埠除錯工具手動傳送資訊的方式來操作 ESP8266 ,這肯定不能用於實際開發。因此,本節介紹如何編寫合適的程式來和 ESP8266 互動,從而收發並解析網路資料。
TCP伺服器
在 TCP 伺服器下,可以使用移動裝置主動連線 ESP8266 提供的 WiFi 。如果編寫正確的程式,那麼可以使用移動裝置控制 ESP8266 。
建立TCP伺服器
從上一節的介紹可以瞭解到,程式和 ESP8266 的互動主要是通過傳送 AT 指令完成的,因此程式中首要的任務就是編寫合適的程式向 ESP8266 傳送指令。
不過在傳送指令後,可能還需要判斷指令是否被成功接收。一般來說,ESP8266 執行失敗時可能返回各種資訊,但在成功執行指令後都會返回 OK 。傳送指令可以通過以下函式完成:
uint8_t ESP8266_SendCmd(char* cmd, uint8_t timeout) {
ESP8266_Buffer.Length = 0;
memset(ESP8266_Buffer.Body, 0, USART_RX_BUF_SIZE);
USART_printf(USART3, "%s\r\n", cmd);
while (timeout--) {
delay_ms(100);
if (strstr(ESP8266_Buffer.Body, "OK"))
return 0;
}
return 1;
}
由於不同指令處理的時間也不一致,因此在程式中引入了一個倒數計時器,在倒數計時結束前不斷檢查接收到的資訊中是否包含 "OK"
,如果是則結束當前倒數計時,這樣可以確保在指令執行完後就可以立即退出延時,提高程式執行效率。
程式中與 ESP8266 互動基本是都採用這種方式。例如,在程式下載後若希望使 ESP8266 也重啟,則可以通過拉低 RST 引腳復位 ESP8266 ,復位後會接到 "ready"
資訊,則可以編寫以下函式:
uint8_t ESP8266_Reset(uint16_t timeout) {
ESP8266_RST(RESET);
delay_ms(500);
ESP8266_RST(SET);
while (timeout--) {
delay_ms(100);
if (strstr(ESP8266_Buffer.Body, "ready"))
return 0;
}
return 1;
}
如果某條指令有其餘回覆的情況,只需要參照以上略做修改即可。
有了以上函式後,就可以編寫程式碼,逐條傳送指令了。這裡將 ESP8266 設定為 AP 模式,使其變成一個 WiFi 熱點,使計算機可以直接連線上 ESP8266 並收發資訊,因此首先需要傳送 AT+CWMODE=2
指令:
if (ESP8266_Reset(50))
return 1;
if (ESP8266_SendCmd("AT+CWMODE=2", 50))
return 2;
然後可以使用 AT+CWSAP="<ssid>","<password>",<chl>,<enc>
設定 WiFi 引數,一般來說通道號和加密型別都設定為 4 即可:
char cmd[64];
sprintf(cmd, "AT+CWSAP=\"%s\",\"%s\",%d,%d\r\n", SSID, PASSWORD, 4, WPA_WPA2_PSK);
if (ESP8266_SendCmd(cmd, 50))
return 3;
可以將這些引數設定為巨集定義以方便修改。接下來的許多設定都和以上程式碼類似,可以以此為模板替換為其它命令,因此不再展示程式碼,僅介紹主要命令。
如果要設定固定的區域網 IP ,可以通過 AT+CIPAP="<ip>"
完成。
接下來可以通過指令 AT+CIPMODE=<mode>
設定 ESP8266 的傳輸模式。該命令可以設定 ESP8266 的兩種傳輸模式:
- 普通傳輸模式(Normal Transmission Mode),該模式下,使用者可以通過 AT 指令傳送 TCP 資料,同時 ESP8266 也會將接收到的資料以 +IPD 等指令的形式返回
- 透傳接收模式(Passthrough Receiving Mode):該模式下,ESP8266 無法傳送 TCP 資料,同時 ESP8266 會將接收到的資料以原始的形式返回給 STM32
透傳接收模式一般用於開啟透傳模式。關於透傳模式會在後續介紹。
ESP8266 支援多路連線,即一個 TCP 埠可以建立多個連線。通過 AT+CIPMUX=1
可以啟用多連線,每個連線到埠上的客戶端通過 <id>
標識,連線的數量最後為 5 個,因此 <id>
的取值範圍為 0~4 。
多連線必須在所有連線都斷開且伺服器也關閉時才可以設定,並且只有普通傳輸模式下才能設定為多連線。
接下來,可以通過 AT+CIPSERVER=1,8266
開啟一個位於埠 8266 上的 TCP 伺服器。根據以上步驟,TCP 伺服器便建立完成,可以準備接收客戶端發來的資料了。
資料獲取與解析
在建立了 TCP 伺服器後,ESP8266 便會等待客戶端的連線。
TCP 客戶端在接到客戶端的資料時,會以 +IPD,<id>,<len>:<data>
的指令形式轉交資料給 STM32 。由於以上開啟了多路連線,因此接收的資料中多了一個欄位 <id>
。
因此判斷是否有資料收到也很簡單,只需要判斷接收緩衝區內是否有子串 "+IPD"
即可:
bool ESP8266_HasData(void) {
return strstr(ESP8266_Buffer.Body,"+IPD")
&& strstr(ESP8266_Buffer.Body,":");
}
以上同時查詢子串 ":"
確保資料有效性。根據以上格式,拆解該字串並擷取有效資料如下:
int8_t ESP8266_MuxGetData(char* data, uint16_t* len) {
uint8_t id;
char* data_ptr = strstr(ESP8266_Buffer.Body, "+IPD");
if (sscanf(data_ptr,"+IPD,%d,%d", &id, len) == 2) {
memcpy(data, strstr(data_ptr, ":") + 1, *len);
data[*len] = '\0';
ESP8266_Buffer.Length = 0;
memset(ESP8266_Buffer.Body, 0, USART_RX_BUF_SIZE);
return id;
}
return -1;
}
以上函式略顯複雜。之所以要這麼複雜,主要有以下兩個方面的原因:scanf()
類函式使用字串轉換說明時,它在讀入資料時如果遇到一個空格或回車符,會丟棄後面的所有資料,這顯然不能用於擷取使用者資料。
另外上一節說過工程中接收串列埠傳來的不定長資料的方式是使用串列埠空閒中斷,然而空閒中斷接收的一包資料並不都是符合期望的一包資料:在接收到 TCP 連線時,ESP8266 會傳送 <id>,CONNECT
表示連線已建立,如果此時建立的連線接收到任何資料,ESP8266 也會立即轉發該資料。因此如果連線建立後馬上收到資料,那麼兩次傳送的資料時間相隔過短,可能會沒有引起空閒中斷而被 STM32 認為是同一包資料。在連線取消時,也有同樣的問題。
不過傳送資料的函式可能更加複雜:
uint8_t ESP8266_MuxSendData(uint8_t* data, uint16_t length, uint8_t id, uint8_t timeout) {
ESP8266_ClearBuffer();
USART_printf(USART3, "AT+CIPSEND=%d,%d\r\n", id, length);
while (timeout--) {
delay_ms(10);
if (strstr(ESP8266_Buffer.Body, ">"))
break;
}
if (timeout > 0) {
ESP8266_ClearBuffer();
USART_SendBytes(USART3, data, length);
while (timeout--) {
delay_ms(10);
if (strstr(ESP8266_Buffer.Body, "SEND OK")) {
ESP8266_ClearBuffer();
return 0;
}
if (strstr(ESP8266_Buffer.Body, "link is not valid")) {
ESP8266_ClearBuffer();
return 2;
}
}
return 3;
}
else
return 1;
}
上一節介紹了傳送資料主要使用 AT+CIPSEND
指令完成(多連線下需要一個額外的欄位指示傳送給的 <id>
),如果可以傳送 ESP8266 會返回 "> "
作提示。如果傳送成功,ESP8266 會返回 "SEND OK"
,通過返回提示就可以知道傳送狀態。
有了以上函式以後,就可以著手編寫主程式了。主程式的處理邏輯非常簡單,在建立 TCP 伺服器後,便不斷判斷是否有資料到達,如果有那麼便讀取資料並回覆資訊:
char ipd_data[512];
int8_t ipd_id;
uint16_t ipd_len;
while (ESP8266_CreateTcpServer())
delay_ms(200);
while (1) {
if (ESP8266_HasData()) {
ipd_id = ESP8266_MuxGetData(ipd_data, &ipd_len);
ESP8266_MuxSendData("Acknowledge", 12, ipd_id, 30);
}
delay_ms(500);
}
可以將得到的資料顯示在串列埠中。在計算機客戶端,編寫如下套接字程式:
import socket, time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.10.1', 8266))
client.send(time.ctime().encode())
message = client.recv(1024)
print(message.decode())
client.close()
將計算機連線到 ESP8266 建立的 WiFi 上並執行該套接字程式,即可觀察到實驗現象。如果為程式編寫合適的使用者介面並在 STM32 上進一步解析資料,那麼便可以實現手機端控制 STM32 了。
TCP客戶端與HTTP請求
TCP 客戶端的建立與 TCP 伺服器類似,這裡先使 ESP8266 連線到路由器,藉助路由器訪問公網上的伺服器。
前幾步操作與 TCP 客戶端類似:在復位 ESP8266 後,首先通過指令 AT+CWMODE=1
設定 Sta 模式,然後通過 AT+CWJAP="<ssid>","<password>"
連線到路由器中。由於客戶端無需多個連線,可以使用 AT+CIPMUX=0
關閉多連線。
本次採用透傳模式(Passthrough Mode)來收發資料。透傳模式是一種特殊的收發資料模式,在透傳模式下,使用者不能傳送 AT 指令,傳送的任何資料都會作為原始的資料傳送到傳輸對端;從傳輸對端收到的資料也會不經由任何 +IPD
封裝而原封不動地返回給 STM32 。
使用 AT+CIPMODE=1
可以設定傳輸模式為透傳模式。通過 AT+CIPSTART
連線上伺服器後,直接執行 AT+CIPSEND
,待 ESP8266 返回 "> "
後就進入了透傳模式。透傳模式下,每包資料以 20ms 間隔區分,每包最大 2048 位元組,傳送和接收資料都不需要封裝成指令,方便處理。
正常退出透傳模式的唯一方式就是單獨傳送一包傳送指令 +++
。
根據以上原理,可以使用 STM32 傳送相應指令,連線到伺服器後進入透傳模式,並準備傳送相應的資料。其程式碼和上文服務端類似,例如:
if (ESP8266_Reset(50))
while (1);
if (ESP8266_SendCmd("AT+CWMODE=1", 20))
while (1);
if (ESP8266_SendCmd("AT+CWJAP=\"TP_LINK\",\"abc123456\"", 100))
while (1);
// ... and so on
當然,考慮到一些指令執行成功時不總是返回 OK
,並且為了使程式邏輯更清晰,可以將一些指令封裝成函式。例如,以下函式根據地址(可以是 IP 地址或域名,DNS 解析將自動完成)和埠號,連線到特定的 TCP 伺服器中並進入透傳模式:
uint8_t ESP8266_ConnectServer(char* address, uint16_t port, uint8_t timeout) {
ESP8266_ClearBuffer();
USART_printf(USART3, "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", address, port);
while (timeout--) {
delay_ms(100);
if (strstr(ESP8266_Buffer.Body, "CONNECT")) {
ESP8266_ClearBuffer();
USART_printf(USART3, "AT+CIPSEND\r\n");
while (timeout--) {
delay_ms(100);
if (strstr(ESP8266_Buffer.Body, "\r\nOK\r\n\r\n>"))
return 0;
}
return 4;
}
if (strstr(ESP8266_Buffer.Body, "CLOSED"))
return 1;
if (strstr(ESP8266_Buffer.Body, "ALREADY CONNECTED"))
return 2;
}
return 3; //超時錯誤,返回3
}
可以仿照該函式將其它指令封裝成具有抽象功能的對應函式。
在本示例中,在使用 TCP 連線到遠端伺服器的 80 埠的基礎上,手動構造合適的 HTTP 請求併傳送:
while(1) {
USART_SendString(USART3, "GET /api/temperature?time=now HTTP/1.1\r\n"
"Connection: keep-alive\r\n"
"Host: 192.168.1.105:80\r\n\r\n");
delay_s(5);
printf("%s", ESP8266_Buffer.Body);
}
這裡 5 秒鐘便查詢一次資料。如果間隔過長,連線可能斷開,那麼可以先主動斷開連線,等需要查詢時再發起 TCP 連線。
通過 HTTP 伺服器提供的合適介面,ESP8266 便可以從網際網路中獲取到非常廣泛的資料。在測試用的伺服器中,該介面返回一個 json 響應並被轉發到 STM32 中,串列埠除錯工具中顯示的原始資料如下:
HTTP/1.1 200 OK
Date: Wed, 13 Jul 2022 10:57:48 GMT
Server: WSGIServer/0.2 CPython/3.9.1
Content-Type: application/json
X-Frame-Options: DENY
Content-Length: 93
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
{"temperature": {"high": 37.6, "low": 28.1, "now": 36.7}, "humidity": "50%", "wind": "11mph"}
通過解析請求頭 Content-Length
就可以獲取資料的長度,然後查詢子串 "{"
的位置便可以提取出介面返回的 json 資料,並可以使用 cJSON 等第三方庫解析其中的資料。網際網路中存在許多類似的介面,只需要構造合適的請求頭,便可以抓取很多有用的資訊,不過這需要有一定的抓包或爬蟲的基礎。
通過路由器可以接入網際網路,在 TCP 服務的基礎上,構造出合適的 HTTP 等應用層協議的封裝,便可以採集網際網路中的各種資料,或者向伺服器報告自身感測器的資料,由此真正實現物聯網的基礎。
例如,可以向 HTTP 伺服器提供的介面傳送 POST 請求,將感測器資料作為引數傳送給伺服器,伺服器解析 POST 請求並更新資料庫,然後便可以顯示在前端上,這樣便可以在任何地點檢視 STM32 的狀態了。不過由於其實現涉及到的知識點過於廣泛,無論是環境的配置還是程式的編寫都不是一篇文章能完整介紹的,這裡便不再涉及。
一個比較有趣的實現是利用 SMTP 傳送電子郵件,可以閱讀這篇文章瞭解 SMTP 應用層協議的原理與基本報文格式,文章中附帶了 Python 套接字程式實現,它和 AT 指令的思路具有一定相似性,移植到 STM32 的主要難點是使用 base64 編碼完成身份驗證,有興趣的讀者可以嘗試自行實現。
參考資料/延伸閱讀
TCP/IP 相關 AT 指令集的官方文件。