因為手機以及鄉下與高雄的無線基地台之 SSID 與 PWD 都不同, 每次要用 ESP8266 連線不同的無線基地台做 Arduino 物聯網實驗時, 都必須先使用一條 USB-TTL 轉換線 (我用的是 PL2303HX) 連接 PC 與 ESP8266, 再用通訊程式如 Realterm 或 AccessPort 等連線 ESP8266 對其下達更改連線基地台之 AT 指令, 改好後又要拔掉 PL2303HX 再將 ESP8266 插回轉換板, 這樣杜邦線要拔來拔去實在很麻煩.
上回做 Arduino + ESP8266 連線 WiFi 實驗時, 意外地從 "阿喵就像家" 的簡報中發現 (page 83), 利用軟體序列埠函式庫就可以輕易地將 Arduino Nano 化身為 USB-TTL 轉換線, 可以直接用 Arduino IDE 對 ESP8266 下 AT 指令, 連 Realterm/AccessPort 等通訊程式都免了, 我覺得這方法蠻好用的, 我記錄在之前這篇 :
# ESP8266 WiFi 模組與 Arduino 連線測試
下面的 WiFi 函式測試也是使用同樣的方法. 在過去的兩周, 我把 AMS1117V33/LD1117V33 與 1K+2K 電阻都焊在一塊洞洞板上面, 外加一個滑動開關來切換 CH_PD 之可控/不可控, 以及排針排母等元件製作了一個 ESP8266 轉接板, 大大降低了麵包板連線的麻煩, 只要四條線即可將 5V 運作的 Nano 與 3.3V 運作的 ESP8266 相連 , 如下圖所示 :
轉接板參考前篇 :
# 製作 ESP8266 轉接板
如果不想費工地去焊轉接板的話, 也可以照下面電路圖在麵包板上接線, 只需要 1K+2K 兩個元件而已 :
ESP8266 的 Vcc 取自 Nano 的 3.3v 輸出即可, 但如果發現 ESP8266 不穩定, 通常是電源不夠力, 這時就需要獨立的 3.3V 電源供應, 可用兩個 1.5V 乾電池串聯起來供電. 接好線後, 將下列程式上傳到 Arduino Nano 就可以利用序列埠監視視窗對 ESP01 模組下 AT 指令了, 參考 :
# ESP8266 WiFi 模組 AT command 測試
SoftwareSerial sSerial(10,11); //(RX,TX)
void setup() {
sSerial.begin(9600); //軟體序列埠速率 (與硬體同步調整)
Serial.begin(9600); //硬體序列埠速率 (與軟體同步調整)
Serial.println("SoftSerial to ESP8266 AT commands test ...");
}
void loop() {
if (sSerial.available()) { //若軟體序列埠 Rx 收到資料 (來自 ESP8266)
Serial.write(sSerial.read()); //讀取後寫入硬體序列埠 Tx (PC)
}
if (Serial.available()) { //若硬體序列埠 Rx 收到資料 (來自 PC)
sSerial.write(Serial.read()); //讀取後寫入軟體序列埠 Tx (ESP8266)
}
}
送電後打開序列埠監視視窗, 若出現 "SoftSerial to ESP8266 AT commands test ...", 表示這顆 ESP8266 之 Baud rate 是設定在 9600, 如果出現怪碼或無反應, 那麼 Baud rate 就不對, 可以嘗試將程式中的 9600 都改為 115200 上傳試試看. 我之前跟露天賣家買的一批 20 顆 ESP01 模組 AT 版本為 0.9.5, 預設速率就是 115200 bps, 所以一開始用 9600 連線都出現怪碼, 改成 115200 後即可正常顯示. 連線成功後下指令更改 Baud rate :
AT+UART=9600,8,1,0,0
然後關掉序列埠監視視窗後重開, 更改右下角之 Baud rate 為 9600 即可. 注意, 這個 AT+UART 指令在 0.9.5 版可用, 但在 0.9.2 不行. 韌體版本 0.9.2 的 AT 指令測試參考 :
# ESP8266 WiFi 模組 AT command 測試
以下是我針對最穩定的 0.9.2 版韌體, 在上面 SoftwareSerial 函式庫支援下所撰寫的 WiFi 函式, 基本上就是把 AT 指令的操作細節包裝起來而已.
首先是定義一個讀取 ESP8266 回應字元的函式 get_ESP8266_response(), 它會將 ESP8266 傳回給軟體串列埠 RX 的回應字元以字串的 concat() 函式串接成回應字串, 去除頭尾的空白字元 (例如跳行與空格) 後傳回, 這樣我們便能從傳回的回應字串中搜尋關鍵字, 判斷 AT 指令是否執行成功, 或者擷取回應資訊, 例如 IP 等等 :
String get_ESP8266_response() { //取得 ESP8266 的回應字串
String str=""; //儲存接收到的回應字串
char c; //儲存接收到的回應字元
while (sSerial.available()) { //若軟體序列埠接收緩衝器還有資料
c=sSerial.read(); //必須放入宣告為 char 之變數 (才會轉成字元)
str.concat(c); //串接回應字元
delay(10); //務必要延遲, 否則太快
}
str.trim(); //去除頭尾空白字元
return str;
}
注意這裡必須把讀取的字元先放在宣告為 char 的變數中再用 concat 串接, 不可直接丟給 concat, 例如 concat(sSerial.read()), 這樣它會以字元的整數值來串接, 而不是字元. 其次是迴圈裡最後要用 delay() 來延遲一下讀取速度, 否則會太快而讀不到任何東西.
為甚麼要把回應字元抄個副本來處理, 不直接用序列埠的 find() 去搜尋關鍵字呢? 主要的原因是 find() 函式會把接收緩衝區裡讀過的字元讀取過的字元都移除, 導致整個接收緩衝區的完整性被第一次的 find() 函式破壞, 如果需要對回應進行兩次搜尋, 那麼第二次的 find() 可能永遠也不會找到關鍵字, 即使那個關鍵字確實存在, 因為它可能已經被第一次的 find() 抹除了. 參考 :
# Does Serial.find Clears Buffer If It Can't Find Anything
在這篇疑難的解答中有提到 "Serial.find() reads Serial buffer and removes every single byte from it", 亦即 find() 每讀取一個字元, 就會將其自接收緩衝區刪除. 但是在官網關於 Serial.find() 的說明卻沒有特別提到這一點, 參考 :
# https://www.arduino.cc/en/Serial/Find
其實這主要在 AT+CWMODE 指令會有此問題, 其他 AT 指令大概無此問題. 若工作模式有變, 例如從 1 變 3, 它會回應 OK; 但若不變, 例如原先是 1, 又下 AT+CHMODE=1, 則會回應 "no change". 從下面以 AT 指令操作的回應訊息即可了解 :
AT+CWMODE?
+CWMODE:1 (目前為模式 1=Station)
OK
AT+CWMODE=3 (改為模式 3=AP+Station)
OK (模式有改變回應 OK)
AT+CWMODE=3 (改為模式 3=AP+Station)
no change (模式沒變回應 no change)
#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
void setup() {
sSerial.begin(9600); //設定軟體序列埠速率 (to ESP8266)
Serial.begin(9600); //設定軟體序列埠速率 (to PC)
Serial.print("Set working mode as station ... ");
sSerial.println("AT+CWMODE=1"); //設定為 Station 模式
delay(1000);
if (sSerial.find("OK") || sSerial.find("no change")) {
Serial.println("OK");
}
else {
Serial.println("No response");
}
}
void loop() {
if (sSerial.available()) { //若軟體序列埠 Rx 收到資料 (來自 ESP8266)
Serial.write(sSerial.read()); //讀取後寫入硬體序列埠 Tx (PC)
}
if (Serial.available()) { //若硬體序列埠 Rx 收到資料 (來自 PC)
sSerial.write(Serial.read()); //讀取後寫入軟體序列埠 Tx (ESP8266)
}
}
在執行前先用手動方式用 AT+CWMODE=3 設定為模式 3, 然後再按兩次 Arduino 的 reset 鍵重新執行, 其回應結果為 :
AT+CWMODE=3
OK
Set working mode as station ... OK (第一次, mode 3 改成 mode 1, 回應 OK)
Set working mode as station ... No response (第二次, mode 1 改成 mode 1, 回應 No response)
#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
void setup() {
sSerial.begin(9600); //設定軟體序列埠速率 (to ESP8266)
Serial.begin(9600); //設定軟體序列埠速率 (to PC)
Serial.print("Set working mode as station ... ");
sSerial.println("AT+CWMODE=1"); //設定為 Station 模式
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {
Serial.println("OK");
}
else {Serial.println("No response");}
}
void loop() {
if (sSerial.available()) { //若軟體序列埠 Rx 收到資料 (來自 ESP8266)
Serial.write(sSerial.read()); //讀取後寫入硬體序列埠 Tx (PC)
}
if (Serial.available()) { //若硬體序列埠 Rx 收到資料 (來自 PC)
sSerial.write(Serial.read()); //讀取後寫入軟體序列埠 Tx (ESP8266)
}
}
String get_ESP8266_response() { //取得 ESP8266 的回應字串
String str=""; //儲存接收到的回應字串
char c; //儲存接收到的回應字元
while (sSerial.available()) { //若軟體序列埠接收緩衝器還有資料
c=sSerial.read(); //必須放入宣告為 char 之變數 (才會轉成字元)
str.concat(c); //串接回應字元
delay(10); //務必要延遲, 否則太快
}
str.trim(); //去除頭尾空白字元
return str;
}
其回應如下 :
AT+CWMODE? (手動查詢工作模式)
+CWMODE:1 (目前工作模式為 1)
OK
AT+CWMODE=3 (手動改為工作模式 3)
OK (按 Reset)
Set working mode as station ... OK (按 Reset)
Set working mode as station ... OK (按 Reset)
Set working mode as station ... OK (按 Reset)
Set working mode as station ... OK (按 Reset)
接下來就可以為 ESP8266 的每一個 AT 指令撰寫函式, 以後直接呼叫即可, 不用再處理繁雜的序列埠讀寫了. 下面是我針對 0.9.2 版 AT 指令韌體所撰寫的 AT WiFi 函式, 以及相關的測試程式, 主要是用在把 ESP8266 當 Station 用的情況, 沒有包含 Station + AP 以及當 Server 時的函式 (以後用到再說) :
#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
String ssid="H30-L02-webbot"; //無線基地台識別
String pwd="blablabla"; //無線基地台密碼
void setup() {
sSerial.begin(9600); //設定軟體序列埠速率 (to ESP8266)
Serial.begin(9600); //設定軟體序列埠速率 (to PC)
Serial.println("*** SoftSerial connection to ESP8266 ***");
Serial.println("Firmware version : " + get_version());
Serial.println("Baud rate : " + get_baud());
Serial.println("Get IP : " + get_ip());
Serial.println("Mode : " + get_mode());
Serial.println("Set Mode=3 : " + set_mode(3));
Serial.println("Mode : " + get_mode());
Serial.println("Set Mode=1 : " + set_mode(1));
Serial.println("Mode : " + get_mode());
Serial.println("Mux : " + get_mux());
Serial.println("Set Mux=1 : " + set_mux(1));
Serial.println("Mux : " + get_mux());
Serial.println("Set Mux=0 : " + set_mux(0));
Serial.println("Mux : " + get_mux());
Serial.println("Get AP : " + get_ap());
Serial.println("Quit AP : " + quit_ap());
Serial.println("Get AP : " + get_ap());
Serial.println("Get IP : " + get_ip());
Serial.println("Joint AP : " + joint_ap(ssid, pwd));
Serial.println("Get AP : " + get_ap());
Serial.println("Get IP : " + get_ip());
Serial.println("Connect Google : " + start_tcp("www.google.com",80));
Serial.println("Send GET : " + send_data("GET /"));
Serial.println("Connect Thingspeak : " + start_tcp("184.106.153.149",80));
Serial.println("Send GET : " + send_data("GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));
}
void loop() {
if (sSerial.available()) { //若軟體串列埠 RX 有收到來自 ESP8266 的回應字元
Serial.write(sSerial.read()); //在串列埠監控視窗顯示 ESP8266 的回應字元
}
if (Serial.available()) { //若串列埠 RX 有收到來自 PC 的 AT 指令字元 (USB TX)
sSerial.write(Serial.read()); //將 PC 的傳來的字元傳給 ESP8266
}
}
String get_ESP8266_response() { //取得 ESP8266 的回應字串
String str=""; //儲存接收到的回應字串
char c; //儲存接收到的回應字元
while (sSerial.available()) { //若軟體序列埠接收緩衝器還有資料
c=sSerial.read(); //必須放入宣告為 char 之變數 (才會轉成字元)
str.concat(c); //串接回應字元
delay(10); //務必要延遲, 否則太快
}
str.trim(); //去除頭尾空白字元
return str;
}
String get_version() {
sSerial.println("AT+GMR"); //取得韌體版本
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") == -1) {return "NG";}
else {return str.substring(0,str.indexOf("\r\n"));}
}
String get_baud() {
sSerial.println("AT+CIOBAUD?"); //取得傳送速率
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") == -1) {return "NG";}
else {return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));}
}
String get_ip() {
sSerial.println("AT+CIFSR"); //取得 ESP8266 IP
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") == -1) {return "NG";}
else {return str.substring(0,str.indexOf("\r\n"));}
}
String get_mode() {
sSerial.println("AT+CWMODE?"); //取得工作模式
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {
return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
}
else {return "NG";}
}
String set_mode(byte mode) {
sSerial.println("AT+CWMODE=" + String(mode)); //設定工作模式
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return "OK";}
else {return "NG";}
}
String get_mux() {
sSerial.println("AT+CIPMUX?"); //取得連線模式
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {
return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
}
else {return "NG";}
}
String set_mux(byte mux) { //0=single, 1=multiple
sSerial.println("AT+CIPMUX=" + String(mux)); //設定連線模式
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {return "OK";}
else {return "NG";}
}
String get_ap() {
sSerial.println("AT+CWJAP?"); //取得連線之AP
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {
return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
}
else {return "NG";}
}
String joint_ap(String ssid, String pwd) {
sSerial.println("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\""); //連線
sSerial.flush(); //等待序列埠傳送完畢
delay(7000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {return "OK";}
else {return "NG";}
}
String quit_ap() {
sSerial.println("AT+CWQAP"); //離線
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {return "OK";}
else {return "NG";}
}
String start_tcp(String address, byte port) {
sSerial.println("AT+CIPSTART=\"TCP\",\"" + address + "\"," + String(port));
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("Linked") != -1) {return "OK";}
else {return "NG";}
}
String send_data(String s) {
String s1=s + "\r\n"; //務必加上跳行
sSerial.println("AT+CIPSEND=" + String(s1.length()));
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf(">") != -1) { //收到 > 開始傳送資料
sSerial.println(s1); //傳送資料
sSerial.flush(); //等待序列埠傳送完畢
delay(7000);
str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("+IPD") != -1) {return "OK";} //傳送成功會自動拆線
else { //傳送不成功須自行拆線
close_ip(); //關閉 IP 連線
return "NG";
}
}
else { //傳送不成功須自行拆線
close_ip(); //關閉 IP 連線
return "NG";
}
}
String close_ip() {
sSerial.println("AT+CIPCLOSE"); //關閉 IP 連線
sSerial.flush(); //等待序列埠傳送完畢
delay(1000);
String str=get_ESP8266_response(); //取得 ESP8266 回應字串
if (str.indexOf("OK") != -1) {return "OK";}
else {return "NG";}
}
上面的函式中, 處理上比較特殊的是 set_mode(), start_tcp(), 以及 send_data() 這三個函式, set_mode() 已如上述, 有兩種成功的回應要處理; start_tcp() 是在剖析回應時尋找 "Linked" 這個關鍵字來判斷連線是否有建立; 而 send_data() 則有兩階段剖析, 第一階段是搜尋是否出現 > 符號以便傳送資料, 第二階段是搜尋是否有 +IPD 回應字串, 有的話才是傳送成功. 大部分的操作成功回應 "OK", 失敗回應 "NG"; 有些擷取資訊的例如, get_version(), get_baud() 等則是成功時傳回從回應訊息中過濾取得之資訊.
其次, 在 set_mode(), set_mux() 等函式中, 傳入參數為數值 (byte), 要組成 AT 指令字串時必須先用 String() 將其轉成字串才能串接, 否則會是空白. 另外, 在這個範例中的最後面, 我測試了 TCP 連線函式 start_tcp() 與 send_data(), 分別與 Google 首頁與物聯網伺服器 Thingspeak 建立 TCP 連線, 然後用 HTTP 的 GET 方法向伺服器提出 Request, 從回應可知均能得到正常回應, 檢查 Thingspeak 的 public 網頁, 確實有寫入資料庫. 關於利用 TCP 連線傳送資料, 參考 :
# ESP8266 WiFi 模組與 Arduino 連線測試
# 用 ESP8266 的 TCP/IP 連線抓取網頁
另外, 在上面的函式中, 向 ESP8266 傳送 AT 指令後, 都呼叫了序列埠物件的 flush() 函式, 功能是讓執行程序停住, 直到傳送緩衝區將 AT 指令全部傳送完畢再繼續往下執行, 參考 :
# https://www.arduino.cc/en/Serial/Flush
在下面這篇文章裡有特別提到 flush() 函數功能在 Arduino IDE 1.0 後的改變 :
# When do you use the Arduino’s Serial.flush()?
"The key to that statement is “outgoing”. Serial.flush() doesn’t clear the “incoming” buffer, like many people think. It pauses your program until the transmit buffer is finished."
這個函數名稱 flush() 容易讓人誤解為清掉接收緩衝區, 這在 Arduino IDE 1.0 以前確實是如此, 但現在不同了. 想要清空接收緩衝區的話, 必須呼叫 read() 函數一個一個字元去讀取, 每讀取一個字元, 接收緩衝區就會清掉一個字元, 這可以用 while 迴圈來完成 :
while (sSerial.available()) {sSerial.read();} //清空接收緩衝器
最後列出上面測試程式的回應並註解如下 :
*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Baud rate : 9600
Get IP : 192.168.43.151
Mode : 1 (目前為模式 1)
Set Mode=3 : OK (改為模式 3)
Mode : 3 (目前為模式 3)
Set Mode=1 : OK (改回模式 1)
Mode : 1 (目前為模式 1)
Mux : 0 (目前為單一連線)
Set Mux=1 : OK (改為多重連線)
Mux : 1 (目前為多重連線)
Set Mux=0 : OK (改回單一連線)
Mux : 0 (目前為單一連線)
Get AP : "H30-L02-webbot" (顯示目前連線之 AP)
Quit AP : OK (離線)
Get AP : NG (目前沒有與 AP 連線)
Get IP : 0.0.0.0 (離線時預設 IP 為 0.0.0.0)
Joint AP : OK (連線指定之 AP)
Get AP : "H30-L02-webbot" (顯示目前連線 AP 之 SSID)
Get IP : 192.168.43.151 (顯示 ESP8266 取得之 IP)
Connect Google : OK (連線 Google 首頁成功)
Send GET : OK (向 Google 首頁傳送 GET 要求成功)
Connect Thingspeak : OK (連線 Thingspeak 成功)
Send GET : OK (向 Thingspeak 傳送 GET 要求成功)
可見所有函式均能正常運作. 這次測試時發現, 不知道為何, TCP 連線在 AT+CIPMUX=1 (多重連線) 下無法完成連線, 但只要改回單一連線即可.
其他參考 :
# ESP8266 Wifi Temperature Logger
# Serial Input Basics
# Functions - return array or multiple variables?
# Convert serial.read() into a useable string using Arduino?
# How to convert int to string on Arduino?
2015-10-31 補充 :
今天在網路上找到跟我上面寫的 WiFi 函數類似的函式庫, 看起來更完整, 是專家寫的吧! 不過它主要是支援 UNO 與 MEGA, 而我寫的是針對 Nano/Pro mini, 參考 :
# ITEADLIB_Arduino_WeeESP8266
有時間再仔細看看人家是怎麼寫的, 偷學幾招!
2015-11-01 補充 :
下面這篇的 waitForResponse() 寫法也有參考價值 :
# Using an ESP8266 as a time source (Part 1)