基礎知識 進程 內核的功能和作用:文件系統管理、網路管理、進程管理、記憶體管理等,屬於linux最基礎的功能 進程:process,正在運行中的程式的一個副本。允許有多個進程同時執行。 #操作系統負責分配cpu運行進程的順序和時間 #副本:把磁碟上的指定文件載入到記憶體進行運行 運行多次就會有多個副本 ...
在上一節中,介紹了 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 指令集的官方文檔。