- 物聯網ESP32教程
- 首頁
- 物聯網簡要概述
- ESP32簡介
- 在Arduino IDE中安裝ESP32開發板
- 設定RTOS以實現雙核和多執行緒操作
- ESP32與MPU6050介面
- ESP32與模擬感測器介面
- ESP32引數設定
- ESP32 SPIFFS儲存器(晶片自帶的迷你SD卡)
- ESP32與OLED顯示屏介面
- ESP32上的WiFi
- 使用HTTP透過WiFi傳輸資料
- 使用HTTPS透過WiFi傳輸資料
- 使用MQTT透過WiFi傳輸資料
- 透過藍牙傳輸資料
- 使用NTP客戶端獲取當前時間
- 執行ESP32韌體的(OTA)更新
- ESP32的應用
- 開發人員的下一步
- 物聯網ESP32有用資源
- 快速指南
- 有用資源
- 討論
物聯網ESP32快速指南
物聯網簡要概述
在軟體方面,您需要在您的機器上安裝Arduino IDE。請訪問 https://www.arduino.cc/en/software。
在硬體方面,您需要以下元件:
ESP32開發板 - 必需
Micro USB資料線 - 用於為ESP32供電和程式設計
MPU6050模組 - 可選(僅在MPU6050相關的章節中需要)
光敏電阻(LDR)和一個阻值相當的普通電阻或任何其他模擬感測器 - 可選(僅在ADC章節中需要)
OLED顯示屏 - 可選(僅在OLED介面相關的章節中需要)
跳線 - 可選(將ESP32與MPU6050、LDR和/或OLED顯示屏連線時需要)
關於GitHub使用的說明
如概述部分所述,每個章節都提供了一個GitHub連結,其中包含程式碼演練。其中許多程式碼取自Arduino中ESP32開發板附帶的示例。因此,您無需額外努力即可在本地機器上執行它們。安裝ESP32開發板到Arduino後(我們為此專門設定了一個章節),您可以在Arduino IDE中找到它們(檔案 -> 示例)。在使用示例程式碼的地方,都會提到示例程式碼的確切路徑。
所有不在示例中的程式碼都可以在以下程式碼庫中找到:https://github.com/yash-sanghvi/ESP32。現在,如果您希望下載並在本地機器上執行這些程式碼,您需要執行以下操作:
點選顯示“程式碼”的綠色按鈕。
如果您不熟悉Git,您可以簡單地下載zip檔案並將其解壓縮到您選擇的資料夾中。子資料夾包含所需的Arduino(.ino)檔案,您可以隨後在Arduino IDE中開啟這些檔案,並編譯並燒錄到ESP32中。
如果您熟悉Git並在您的機器上安裝了Git,您可以複製HTTPS地址(https://github.com/yash-sanghvi/ESP32.git),導航到您希望克隆此程式碼庫的資料夾,開啟您的Git命令列並輸入git clone https://github.com/yash-sanghvi/ESP32.git
如果您不熟悉Git,您可能想知道為什麼我們應該費力克隆程式碼庫,而下載和解壓zip檔案會產生相同的效果。答案是下載zip檔案是一次性過程。如果將來此程式碼庫發生某些更改,本地機器上下載的版本無法直接反映這些更改。您需要再次下載zip檔案。如果您克隆了程式碼庫,則只需呼叫git pull即可獲取所有將來的更改。使用克隆還可以做更多的事情。如果程式碼庫中有多個分支,則只需git checkout branch-name即可切換分支。如果您下載zip檔案,則需要為每個分支下載單獨的zip檔案。總而言之,克隆通常更方便。但是,對於此特定用例,由於我們預計將來此程式碼庫不會發生重大更改,並且您只需要主分支,如果您不熟悉Git,您可以繼續下載zip檔案。
您是否注意到最近很多日常用品都變得“智慧化”了?有智慧電視、智慧空調、智慧冰箱等等。這些裝置的“智慧化”指的是什麼?雖然每個裝置的答案略有不同,但智慧化的一個共同要素是“連線性”。您的電視連線到您的WiFi,因此您可以串流以前只能在手機上觀看的節目。您的空調連線到網際網路。您可以從另一個城市透過手機發送命令,家裡的空調就會開啟/關閉。您的手錶連線到您的手機(透過BLE),您可以使用手錶本身接聽電話。您通常處理的所有事物都連線在一起,就像在一個網路中一樣。這是一個物聯網。
以上段落應該讓您對物聯網有了一些瞭解。根據維基百科,物聯網的定義如下:
物聯網 (IoT) 描述的是物理物件的網路——“事物”——這些物件嵌入了感測器、軟體和其他技術,用於透過網際網路連線和交換資料與其他裝置和系統。
上述定義非常準確。嵌入感測器的物體,包含軟體,透過網際網路與其他裝置/系統共享資料。此定義還清楚地突出了任何物聯網裝置的三個主要功能模組中的兩個:
感知
處理和儲存
傳輸
感知
物聯網裝置感知什麼?它們可以感知任何值得感知的東西。如果您的物聯網裝置安裝在垃圾場,它可能會檢查垃圾的填充水平。如果您的物聯網裝置安裝在工廠,它可能會感知電力消耗。如果您的物聯網裝置安裝在機器上,它可能會感知機器的振動特徵以確定機器是開啟、關閉還是切割。如果您的裝置安裝在車輛上,它可能會感知車輛的移動和位置。
您的物聯網裝置將感知任何可以幫助您節省成本、增加利潤或警告您即將發生災難的事情。傳統的火災報警器非常接近物聯網裝置。它感知煙霧,對其進行處理以確定煙霧濃度是否高於安全水平。它只是沒有將此資訊傳輸到任何地方。但是,如果您將建築物中的所有火災報警器連線到網際網路,並在安全室中設定一個儀表板,顯示哪個房間發生了火災,那麼您的火災報警器將非常像一個物聯網裝置。
處理和儲存
物聯網裝置上進行哪些處理/儲存?此答案很大程度上取決於您的用例。有些物聯網裝置不進行板載處理,只是將原始感測器資料傳輸到伺服器。有些物聯網裝置在板載進行即時影片處理以識別物體/人員。這取決於您的資料量、可用RAM、所需的最終輸出和可用的傳輸頻寬。如果您的裝置每毫秒獲取一次機器的振動特徵,那麼您在一秒鐘內就會有1000個讀數。在這種情況下,將如此大量的資料傳送到伺服器可能沒有意義(特別是如果您使用的是低頻寬網路,例如NB-IoT)。在這種情況下,您可能希望在裝置上執行FFT,只需將振動的頻率和幅度傳送到伺服器。如果您的裝置每5分鐘感知一次大氣中的溫度和溼度,您可能只需要一個公式將原始讀數轉換為溫度和溼度並將其傳送出去。或者您可以只發送原始讀數,讓伺服器進行轉換。在這種情況下,您可以傳送每個讀數。
幾乎所有物聯網裝置都有一些板載記憶體,用於在網路錯誤的情況下儲存丟失的資料包。有些裝置有配置檔案,也需要板載儲存。有些裝置將其記憶體中的最後X小時資料保留以供將來訪問。進行大量板載處理的物聯網裝置肯定需要儲存空間才能在處理開始前收集足夠的資料。例如,如果您的裝置每10,000個讀數後對振動資料執行FFT,則需要儲存傳入的讀數,直到數量達到10,000。
傳輸
物聯網裝置如何傳輸資料?嗯,有幾種解決方案可用。其中一些是:
選擇合適的傳輸解決方案本身就是一個重大決定,很大程度上取決於您可用的電源、頻寬要求、通訊距離、成本和可接受的延遲。您的智慧手錶可以使用BLE與您的手機通訊,您的智慧電視可以使用WiFi,而安裝在車輛上的裝置可以使用蜂窩網路。為農業應用(例如土壤溼度測量)而製造的物聯網裝置,尤其是在偏遠地區,可以使用LoRa與另一個裝置通訊,而該裝置反過來可能具有WiFi或乙太網連線。最終目標幾乎總是將資料放在伺服器上,和/或在儀表板/應用程式上向用戶顯示資料。
總結
如果您是物聯網新手,本章將為您很好地概述物聯網的重點所在。如果您對此感到興奮,請繼續閱讀下一章,我們將討論ESP32,這是一款系統級晶片 (SoC) 微控制器,本教程將圍繞它展開。我們將討論ESP32為什麼在物聯網領域如此受歡迎,以及它在感測、處理、儲存和傳輸領域的各項功能。我們下一章見。
ESP32簡介
ESP32是一款系統級晶片 (SoC) 微控制器,最近獲得了巨大的普及。ESP32的流行是因為物聯網的發展,還是物聯網的發展是因為ESP32的推出,這是一個值得商榷的問題。如果您認識10位參與過任何物聯網裝置韌體開發的人,那麼其中7-8位很可能在某個時候使用過ESP32。那麼,這究竟是怎麼回事呢?為什麼ESP32能如此迅速地普及呢?讓我們來了解一下。
在我們深入探討ESP32流行的實際原因之前,讓我們先來看看它的一些重要規格。以下列出的規格屬於ESP32 WROOM 32版本。
整合晶體:40 MHz
模組介面:UART、SPI、I2C、PWM、ADC、DAC、GPIO、脈衝計數器、電容式觸控感測器
整合SPI快閃記憶體:4 MB
ROM:448 KB(用於引導和核心功能)
SRAM:520 KB
整合連線協議:WiFi、藍牙、BLE
片上感測器:霍爾感測器
工作溫度範圍:-40至85攝氏度
工作電壓:3.3V
工作電流:80 mA(平均)
有了上述規格,很容易就能解釋ESP32流行的原因。考慮一下物聯網裝置對其微控制器 (μC) 的需求。如果您閱讀了上一章,您就會意識到任何物聯網裝置的主要操作模組都是感測、處理、儲存和傳輸。因此,首先,μC應該能夠與各種感測器介面。它應該支援感測器介面所需的所有常用通訊協議:UART、I2C、SPI。它應該具有ADC和脈衝計數功能。ESP32滿足所有這些要求。最重要的是,它還可以與電容式觸控感測器介面。因此,大多數常用感測器可以與ESP32無縫對接。
其次,μC應該能夠對傳入的感測器資料進行基本處理,有時需要高速處理,並具有足夠的記憶體來儲存資料。ESP32的最大工作頻率為40 MHz,足夠高。它有兩個核心,允許並行處理,這是一個額外的優勢。最後,它的520 KB SRAM對於處理大量板載資料來說足夠大。許多流行的流程和轉換,如FFT、峰值檢測、RMS計算等,都可以在ESP32上進行。在儲存方面,ESP32比傳統的微控制器更進一步,並在快閃記憶體中提供了一個檔案系統。在4 MB的板載快閃記憶體中,預設情況下,1.5 MB被預留為SPIFFS(SPI快閃記憶體檔案系統)。可以把它想象成一個位於晶片內部的迷你SD卡。您不僅可以儲存資料,還可以儲存文字檔案、影像、HTML和CSS檔案等等。人們已經使用ESP32建立的WiFi伺服器顯示了精美的網頁,方法是將HTML檔案儲存在SPIFFS中。
最後,對於資料傳輸,ESP32集成了WiFi和藍牙協議棧,這被證明是一個改變遊戲規則的功能。無需連線單獨的模組(如GSM模組或LTE模組)即可測試雲通訊。只需擁有ESP32開發板和執行中的WiFi,即可開始使用。ESP32允許您在接入點和站點模式下使用WiFi。雖然它支援TCP/IP、HTTP、MQTT和其他傳統的通訊協議,但它也支援HTTPS。是的,您沒聽錯。它有一個加密核心或加密加速器,這是一個專門的硬體,其工作是加速加密過程。因此,您不僅可以與您的Web伺服器通訊,還可以安全地進行通訊。BLE支援對於許多應用也很關鍵。當然,您可以將LTE、GSM或LoRa模組與ESP32介面。因此,在“資料傳輸”方面,ESP32也超出了預期。
擁有如此多的功能,ESP32的價格一定很貴,對吧?這是最好的部分。ESP32開發模組的價格在500盧比左右。不僅如此,晶片尺寸也很小(25毫米x 18毫米,包括天線區域),允許將其用於需要非常小尺寸的裝置。
最後,ESP32可以使用Arduino IDE進行程式設計,這使得學習曲線變得平緩得多。是不是很棒?您是否渴望開始使用ESP32?那麼,讓我們在下一章開始在Arduino IDE中安裝ESP32開發板。我們下一章見。
在Arduino IDE中安裝ESP32開發板
ESP32的一個非常大的優勢,也是它快速普及和廣受歡迎的原因,是在Arduino IDE中程式設計ESP32的功能。
現在,我需要指出的是,Arduino並不是唯一一個幫助您編譯ESP32程式碼並將其燒錄到微控制器的IDE。還有ESP-IDF,它是ESP32的官方開發框架,在配置選項方面提供了更大的靈活性。但是,它遠不如Arduino IDE直觀和使用者友好,如果您剛開始使用ESP32,Arduino IDE是上手的理想選擇。此外,由於龐大的開發者社群,為ESP32在Arduino中構建了大量的支援庫,幾乎ESP32的任何功能都可以透過Arduino IDE實現。ESP-IDF更適合那些需要將ESP32發揮到極致的高階和經驗豐富的程式設計師。如果您是其中之一,您需要查詢ESP-IDF入門指南。其他人可以繼續。
安裝步驟
現在,要在Arduino IDE中安裝ESP32開發板,您需要按照以下步驟操作:
確保您的機器上已安裝Arduino IDE(最好是最新版本)
開啟Arduino,然後轉到檔案 -> 首選項
在“附加開發板管理器網址”中,輸入
https://dl.espressif.com/dl/package_esp32_index.json
如果您在首選項中已有JSON檔案的網址(如果您已在IDE中安裝了ESP8266、stm32duino或任何其他附加開發板,則很可能如此),您可以使用逗號將上述路徑附加到現有路徑。下面顯示了ESP8266和ESP32開發板的示例:
http://arduino.esp8266.com/stable/package_esp8266com_index.json, https://dl.espressif.com/dl/package_esp32_index.json
轉到工具 -> 開發板 -> 開發板管理器。將開啟一個彈出視窗。搜尋ESP32並安裝由Espressif Systems提供的esp32開發板。下圖顯示了已安裝的開發板,因為我在準備本教程之前已經安裝了該開發板。
驗證安裝
安裝ESP32開發板後,您可以透過轉到工具 -> 開發板來驗證安裝。您可以在ESP32 Arduino部分看到許多開發板。選擇您選擇的開發板。如果您不確定哪個開發板最能代表您擁有的開發板,您可以選擇ESP32 Dev Module。
接下來,使用USB線將您的開發板連線到您的機器。您應該在工具 -> 埠下看到一個額外的COM埠。選擇該額外埠。如果您看到多個埠,您可以斷開USB連線並檢視哪個埠消失了。該埠對應於ESP32。
確定埠後,從檔案 -> 示例中選擇任何一個示例草圖。我們將從檔案 -> 示例 -> 首選項 -> StartCounter中選擇StartCounter示例。
開啟該草圖,編譯它,然後透過單擊上傳按鈕(編譯按鈕旁邊的右箭頭按鈕)將其燒錄到ESP32中。
然後使用工具 -> 序列埠監視器開啟序列埠監視器,或者只需按鍵盤上的Ctrl + Shift + M。您應該看到計數器值在每次ESP32重啟後遞增。
恭喜您!您已設定好使用ESP32的環境。
為雙核和多執行緒操作設定RTOS
ESP32的一個關鍵特性使其比其前身ESP8266更受歡迎,那就是晶片上存在兩個核心。這意味著我們可以讓兩個程序在兩個不同的核心上並行執行。當然,您可以爭辯說,也可以使用FreeRTOS/任何其他等效的RTOS在一個執行緒上實現並行操作。但是,在一個核心上並行執行兩個程序與在不同的核心上並行執行兩個程序之間存在差異。在一個核心上,通常一個執行緒必須等待另一個執行緒暫停才能開始執行。在兩個核心上,並行執行確實是並行的,因為它們實際上佔據了不同的處理器。
聽起來很激動人心?讓我們從一個真實的例子開始,演示如何建立兩個任務並將它們分配給ESP32中的特定核心。
程式碼演練
GitHub連結:https://github.com/
要在Arduino IDE中使用FreeRTOS,不需要額外的匯入。它是內建的。我們需要做的是定義兩個我們希望在兩個核心上執行的函式。它們首先被定義。一個函式計算斐波那契數列的前25項,並列印其中每第5項。它在一個迴圈中這樣做。第二個函式計算從1到100的數字之和。它也在一個迴圈中這樣做。換句話說,在計算從1到100的和一次之後,它會在列印它正在執行的核心的ID之後再次這樣做。我們沒有列印所有數字,而只是列印兩個序列中的每第5個數字,因為兩個核心都會嘗試訪問同一個序列埠監視器。因此,如果我們列印每個數字,它們將頻繁地同時嘗試訪問序列埠監視器。
void print_fibonacci() {
int n1 = 0;
int n2 = 1;
int term = 0;
char print_buf[300];
sprintf(print_buf, "Term %d: %d\n", term, n1);
Serial.print(print_buf);
term = term + 1;
sprintf(print_buf, "Term %d: %d\n", term, n1);
Serial.print(print_buf);
for (;;) {
term = term + 1;
int n3 = n1 + n2;
if(term%5 == 0){
sprintf(print_buf, "Term %d: %d\n", term, n3);
Serial.println(print_buf);
}
n1 = n2;
n2 = n3;
if (term >= 25) break;
}
}
void sum_numbers() {
int n1 = 1;
int sum = 1;
char print_buf[300];
for (;;) {
if(n1 %5 == 0){
sprintf(print_buf, " Term %d: %d\n", n1, sum);
Serial.println(print_buf);
}
n1 = n1 + 1;
sum = sum+n1;
if (n1 >= 100) break;
}
}
void codeForTask1( void * parameter ) {
for (;;) {
Serial.print("Code is running on Core: ");Serial.println( xPortGetCoreID());
print_fibonacci();
}
}
void codeForTask2( void * parameter ) {
for (;;) {
Serial.print(" Code is running on Core: ");Serial.println( xPortGetCoreID());
sum_numbers();
}
}
您可以看到上面我們已將任務2的列印語句向右移動。這將幫助我們區分任務1和任務2的列印。
接下來,我們定義任務控制代碼。任務控制代碼用於在程式碼的其他部分引用該特定任務。由於我們有兩個任務,我們將定義兩個任務控制代碼。
TaskHandle_t Task1, Task2;
現在函式已準備就緒,我們可以轉到setup部分。在setup()中,我們只需將兩個任務固定到各自的核心。首先,讓我向您展示程式碼片段。
void setup() {
Serial.begin(115200);
/*Syntax for assigning task to a core:
xTaskCreatePinnedToCore(
coreTask, // Function to implement the task
"coreTask", // Name of the task
10000, // Stack size in words
NULL, // Task input parameter
0, // Priority of the task
NULL, // Task handle.
taskCore); // Core where the task should run
*/
xTaskCreatePinnedToCore( codeForTask1, "FibonacciTask", 5000, NULL, 2, &Task1, 0);
//delay(500); // needed to start-up task1
xTaskCreatePinnedToCore( codeForTask2, "SumTask", 5000, NULL, 2, &Task2, 1);
}
現在讓我們深入研究xTaskCreatePinnedToCore函式。如您所見,它總共接受7個引數。它們的描述如下。
第一個引數codeForTask1是任務將執行的函式
第二個引數 **"FibonacciTask"** 是該任務的標籤或名稱。
第三個引數 **1000** 是分配給此任務的棧大小(以位元組為單位)。
第四個引數 **NULL** 是任務輸入引數。基本上,如果您希望向任務輸入任何引數,則將其放在此處。
第五個引數 **1** 定義任務的優先順序。值越高,任務的優先順序越高。
第六個引數 **&Task1** 是任務控制代碼。
最後一個引數 **0** 是任務將執行在其上的核心程式碼。如果值為 0,則任務將在核心 0 上執行;如果值為 1,則任務將在核心 1 上執行。
最後,迴圈可以留空,因為此處在兩個核心上執行的兩個任務更重要。
void loop() {}
您可以在序列埠監視器上看到輸出。請注意,程式碼中沒有任何延遲。因此,兩個遞增序列都表明計算是並行進行的。序列埠監視器上列印的核心 ID 也證實了這一點。
請注意,Arduino 草圖預設情況下在核心 1 上執行。可以使用 **Serial.print( xPortGetCoreID());** 驗證這一點。因此,如果您在 **loop()** 中新增一些程式碼,它將作為另一個執行緒在核心 1 上執行。在這種情況下,核心 0 將執行單個任務,而核心 1 將執行兩個任務。
ESP32與MPU6050介面
加速度計和陀螺儀廣泛用於工業物聯網,用於測量各種機器的執行狀況和執行引數。MPU6050 是一款流行的六軸加速度計+陀螺儀。它是一種 MEMS(微機電系統)感測器,這意味著它非常緊湊(從下圖可以看出),並且在很寬的頻率範圍內也非常精確。
在本教程中,我們將瞭解如何將 ESP32 與 MPU6050 介面連線。在此過程中,您將學習 I2C(積體電路間通訊)協議的使用方法,這將使您能夠將 ESP32 與使用 I2C 協議進行通訊的多個感測器和外圍裝置連線起來。本教程需要您的 ESP32、MPU6050 和幾根跳線。
將 MPU6050 與 ESP32 連線
如下圖所示,您需要將 MPU6050 的 SDA 線連線到 ESP32 的 21 引腳,SCL 線連線到 22 引腳,GND 連線到 GND,VCC 連線到 3V3 引腳。MPU6050 的其他引腳無需連線。
程式碼演練
GitHub 連結 − https://github.com/
ESP32 和 Arduino 通常將 I2C 協議稱為“Wire”。因此,所需的庫匯入是 Wire.h。
#include<Wire.h>
接下來,我們定義常量和全域性變數。
const int MPU_ADDR = 0x68; // I2C address of the MPU-6050 int16_t AcX, AcY, AcZ, Tmp, GyX, GyY, GyZ;
每個 I2C 裝置都有一個固定的地址,其他裝置使用該地址來識別它並與其通訊。對於 MPU6050,該地址為 0x68。我們稍後將在初始化與 MPU6050 的 I2C 通訊時使用它。接下來我們轉到 setup 程式碼。
void setup() {
Serial.begin(115200);
Wire.begin(21, 22, 100000); // sda, scl, clock speed
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B); // PWR_MGMT_1 register
Wire.write(0); // set to zero (wakes up the MPU−6050)
Wire.endTransmission(true);
Serial.println("Setup complete");
}
第一行很簡單。我們正在以 115200 波特率啟動與序列埠監視器的通訊。接下來,我們開始 I2C 通訊。為此,我們向 **Wire.begin()** 函式提供 3 個引數。
這些是 SDA 和 SCL 引腳以及時鐘速度。現在,I2C 通訊需要兩條線:資料線 (SDA) 和時鐘線 (SCL)。在 ESP32 上,引腳 21 和 22 通常保留用於 I2C,其中 21 為 SDA,22 為 SCL。為了與 MPU6050 通訊,我們有兩個速度選項:100kbit/s 和 400kbit/s。我們在這裡選擇了 100kHz。如果您的用例需要,您也可以選擇更高的速度選項。
接下來,我們使用 **Wire.beginTransmission()** 命令指示 ESP32 我們想要與地址等於 MPU_ADDR 的晶片通訊。此時,您可能已經猜到一個 ESP32 晶片可以與多個 I2C 外設通訊。實際上,共有 128 個唯一的地址(地址欄位為 7 位),因此 ESP32 可以使用 I2C 與 128 個不同的外設通訊,前提是它們都具有不同的地址。
在接下來的幾行中,我們將 MPU6050 的 PWR_MGMT_1 暫存器設定為 0。這用於喚醒 MPU6050。PWR_MGMT_1 暫存器的地址 0x6B 是 MPU6050 記憶體中的地址。
它與 MPU6050 的 I2C 地址無關。MPU 喚醒後,我們結束這段特定的 I2C 傳輸,我們的設定就完成了,我們使用 print 語句在序列埠監視器上指示這一點。現在讓我們進入迴圈。您會注意到我們將布林值 **true** 作為引數傳遞給 **Wire.endTransmission**。這告訴 ESP32 傳送停止命令並釋放 I2C 線路。如果我們將 true 替換為 false,則 ESP32 將傳送重新啟動而不是停止,保持連線活動。
void loop() {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B); // starting with register 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(true);
Wire.beginTransmission(MPU_ADDR);
Wire.requestFrom(MPU_ADDR, 14, true); // request a total of 14 registers
AcX = Wire.read() −− 8 | Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
AcY = Wire.read() −− 8 | Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
AcZ = Wire.read() −− 8 | Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
Tmp = Wire.read() −− 8 | Wire.read(); // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
GyX = Wire.read() −− 8 | Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
GyY = Wire.read() −− 8 | Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
GyZ = Wire.read() −− 8 | Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
Serial.print(AcX); Serial.print(" , ");
Serial.print(AcY); Serial.print(" , ");
Serial.print(AcZ); Serial.print(" , ");
Serial.print(GyX); Serial.print(" , ");
Serial.print(GyY); Serial.print(" , ");
Serial.print(GyZ); Serial.print("\n");
}
在迴圈中,如果您掃描上面的程式碼片段,您會看到我們總共執行了兩次傳輸。在第一次傳輸中,我們指示 MPU6050 我們想要從中開始讀取資料的地址,或者更確切地說,將 MPU6050 的內部指標設定為此特定地址。在第二次傳輸中,我們告訴 MPU 我們請求從前面傳送的地址開始的 14 個位元組。然後我們逐個讀取位元組。您可能會注意到我們在讀取結束時沒有 **Wire.endTransmission(true)** 命令。這是因為 **Wire.requestFrom(MPU,14,true)** 的第三個引數指示 ESP32 在讀取所需位元組數後傳送停止命令。如果我們傳遞 false 而不是 true,ESP32 將傳送重新啟動命令而不是停止命令。
現在,您可能想知道如何確定哪個暫存器對應於哪個讀數。答案是 MPU6050 暫存器對映。顧名思義,它提供了有關可以從哪個暫存器獲得哪個值的資訊。根據此對映,我們意識到我們理解 0x3B 和 0x3C 對應於 16 位 X 方向加速度值的較高和較低位元組。接下來的兩個暫存器 (0x3D 和 0x3E) 包含 16 位 Y 方向加速度值的較高和較低位元組,依此類推。在加速度計和陀螺儀讀數之間,有兩個位元組包含溫度讀數,我們讀取並忽略它們,因為我們不需要它們。因此,透過這種方式,您可以成功地從 ESP32 上的 MPU6050 獲取資料。恭喜!!繼續下一個教程,學習如何從 ESP32 上的模擬感測器獲取資料。
參考文獻
將 ESP32 與模擬感測器介面連線
您需要與 ESP32 介面連線的另一類重要感測器是模擬感測器。模擬感測器有很多型別,LDR(光敏電阻)、電流和電壓感測器是流行的例子。現在,如果您熟悉任何 Arduino 板(如 Arduino Uno)上 analogRead 的工作原理,那麼本章對您來說將是小菜一碟,因為 ESP32 使用相同的函式。您只需要注意一些細微之處,本章將介紹這些細微之處。
關於模數轉換 (ADC) 過程的簡要說明
每個支援 ADC 的微控制器都將具有定義的解析度和參考電壓。參考電壓通常是電源電壓。提供給 ADC 引腳的模擬電壓應小於或等於參考電壓。解析度表示將用於表示數字值的位數。因此,如果解析度為 8 位,則該值將由 8 位表示,並且可能的最大值為 255。此最大值對應於參考電壓的值。其他電壓的值通常是透過縮放得出的。
因此,如果參考電壓為 5V 且使用 8 位 ADC,則 5V 對應於 255 的讀數,1V 對應於 (255/5*1) = 51 的讀數,2V 對應於 (255/5*2) = 102 的讀數,依此類推。如果我們有一個 12 位 ADC,則 5V 將對應於 4095 的讀數,1V 將對應於 (4095/5*1) = 819 的讀數,依此類推。
反向計算可以類似地執行。如果您在參考電壓為 3.3V 的 12 位 ADC 上得到 1000 的值,則它大約對應於 (1000/4095*3.3) = 0.8V 的值。如果您在參考電壓為 5V 的 10 位 ADC 上得到 825 的讀數,則它大約對應於 (825/1023*5) = 4.03V 的值。
透過以上解釋,很明顯,用於 ADC 的參考電壓和位數都決定了可以檢測到的最小可能的電壓變化。如果參考電壓為 5V 且解析度為 12 位,則您有 4095 個值來表示 0-5V 的電壓範圍。因此,可以檢測到的最小變化為 5V/4095 = 1.2mV。類似地,對於 5V 和 8 位參考電壓,您只有 255 個值來表示 0-5V 的範圍。因此,可以檢測到的最小變化為 5V/255 = 19.6mV,大約是 12 位解析度檢測到的最小變化的 16 倍。
將 ADC 感測器與 ESP32 連線
考慮到感測器的普及性和可用性,我們將使用 LDR 進行演示。我們將基本上將 LDR 與常規電阻串聯連線,並將連線兩個電阻的點的電壓饋送到 ESP32 的 ADC 引腳。哪個引腳?好吧,有很多。ESP32 擁有 18 個 ADC 引腳(通道 1 中有 8 個,通道 2 中有 10 個)。但是,通道 2 引腳不能與 WiFi 一起使用。並且某些電路板上的通道 1 的某些引腳未暴露。因此,我通常堅持使用以下 6 個 ADC 引腳——32、33、34、35、36、39。在下圖中,將 90K 電阻的 LDR 連線到 150K 電阻。LDR 的自由端連線到 ESP32 的 3.3V 引腳,電阻的自由端連線到 GND。LDR 和電阻的公共端饋送到 ESP32 的 ADC 引腳 36 (VN)。
程式碼演練
GitHub 連結 − https://github.com/
此處的程式碼很簡單。不需要包含庫。我們只需將 LDR 引腳定義為常量,在 setup() 中初始化序列埠,並設定 ADC 的解析度。在這裡,我們設定了 10 位的解析度(這意味著最大值為 1023)。預設情況下,解析度為 12 位,對於 ESP32,最小可能解析度為 9 位。
const int LDR_PIN = 36;
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
analogReadResolution(10); //default is 12. Can be set between 9-12.
}
在迴圈中,我們只需讀取 LDR 引腳的值並將其列印到序列埠監視器。此外,我們將其轉換為電壓並列印相應的電壓。
void loop() {
// put your main code here, to run repeatedly:
// LDR Resistance: 90k ohms
// Resistance in series: 150k ohms
// Pinouts:
// Vcc −> 3.3 (CONNECTED TO LDR FREE END)
// Gnd −> Gnd (CONNECTED TO RESISTOR FREE END)
// Analog Read −> Vp (36) − Intermediate between LDR and resistance.
int LDR_Reading = analogRead(LDR_PIN);
float LDR_Voltage = ((float)LDR_Reading*3.3/1023);
Serial.print("Reading: ");Serial.print(LDR_Reading); Serial.print("\t");Serial.print("Voltage: ");Serial.println(LDR_Voltage);
}
我們使用 1023 作為除數,因為我們將 ADC 解析度設定為 10 位。如果您將 ADC 值更改為 N,則需要將除數更改為 (2^N −1)。現在將您的手放在 LDR 上
我們使用 1023 作為除數,因為我們將 ADC 解析度設定為 10 位。如果您將 ADC 值更改為 N,則需要將除數更改為 (2^N −1)。現在將您的手放在 LDR 上,看看對電壓的影響,然後用火炬照射 LDR,看看序列埠監視器上電壓的劇烈變化。就是這樣。您已成功從 ESP32 上的模擬感測器捕獲資料。
參考文獻
ESP32 中的偏好設定
非易失性儲存器是嵌入式系統的重要需求。我們經常希望晶片即使在電源迴圈之間也能記住一些東西,例如設定變數、WiFi憑據等。如果每次裝置進行電源重置後都必須執行設定或配置,那將非常不方便。ESP32有兩種常用的非易失性儲存方法:Preferences和SPIFFS。Preferences通常用於儲存鍵值對,而SPIFFS(SPI Flash File System),顧名思義,用於儲存檔案和文件。本章,讓我們關注Preferences。
Preferences儲存在主快閃記憶體的一個區域中,型別為data,子型別為nvs。nvs代表非易失性儲存器。預設情況下,為Preferences保留20 KB的空間,因此不要嘗試在Preferences中儲存大量龐大的資料。對於大量資料,請使用SPIFFS(SPIFFS預設保留1.5 MB的空間)。Preferences可以儲存哪些型別的鍵值對?讓我們透過示例程式碼來理解。
程式碼演練
我們將使用提供的示例程式碼。前往檔案 -> 示例 -> Preferences -> StartCounter。它也可以在GitHub上找到。
此程式碼記錄ESP32重置的次數。因此,每次喚醒時,它都會從Preferences中獲取現有計數,將其加1,並將更新後的計數儲存回Preferences。然後它會重置ESP32。您可以透過ESP32上的列印語句看到計數的值在重置之間不會丟失,它確實是持久儲存的。
此程式碼有非常詳細的註釋,因此在很大程度上是不言自明的。儘管如此,讓我們逐步瀏覽程式碼。
我們首先包含Preferences庫。
#include <Preferences.h>
接下來,我們建立一個Preferences類的物件。
Preferences preferences;
現在讓我們逐行檢視setup。我們首先初始化Serial。
void setup() {
Serial.begin(115200);
Serial.println();
接下來,我們使用名稱空間開啟Preferences。現在,將Preferences儲存想象成一個銀行儲物櫃室。有很多儲物櫃,你可以一次開啟一個。名稱空間就像儲物櫃的名稱。在每個儲物櫃中,都有你可以訪問的鍵值對。如果你的名稱空間對應的儲物櫃不存在,則會建立它,然後你可以向該儲物櫃新增鍵值對。為什麼會有不同的儲物櫃?為了避免名稱衝突。假設你有一個使用Preferences儲存憑據的WiFi庫,還有一個也使用Preferences儲存憑據的藍牙庫。假設這兩個庫是由不同的開發者開發的。如果兩者都使用相同的鍵名credentials怎麼辦?這顯然會造成很大的混亂。但是,如果兩者都在不同的儲物櫃中儲存其鍵,則根本不會出現任何混亂。
// Open Preferences with my-app namespace. Each application module, library, etc
// has to use a namespace name to prevent key name collisions. We will open storage in
// RW-mode (second parameter has to be false).
// Note: Namespace name is limited to 15 chars.
preferences.begin("my−app", false);
preferences.begin()的第二個引數false表示我們想要讀取和寫入這個儲物櫃。如果是true,我們只能讀取儲物櫃,而不能寫入。此外,註釋中提到的名稱空間長度不應超過15個字元。
接下來,程式碼有一些註釋掉的語句,您可以根據需要使用它們。一個可以讓你清除儲物櫃,另一個可以幫助你從儲物櫃中刪除特定的鍵值對(鍵為“counter”)。
// Remove all preferences under the opened namespace
//preferences.clear();
// Or remove the counter key only
//preferences.remove("counter");
下一步,我們獲取與鍵“counter”關聯的值。現在,第一次執行此程式時,可能不存在這樣的鍵。因此,我們還將0作為預設值傳遞給preferences.getUInt()函式。這告訴ESP32,如果鍵“counter”不存在,則建立一個新的鍵值對,鍵為“counter”,值為0。還要注意,我們使用getUInt是因為值是無符號整型。需要根據值的型別呼叫其他函式,例如getFloat、getString等。完整的選項列表可以在這裡找到。
unsigned int counter = preferences.getUInt("counter", 0);
接下來,我們將此計數加1,並在序列埠監視器上打印出來。
// Increase counter by 1
counter++;
// Print the counter to Serial Monitor
Serial.printf("Current counter value: %u\n", counter);
然後我們將此更新後的值儲存回非易失性儲存器。我們基本上是在更新鍵“counter”的值。下次ESP32讀取鍵“counter”的值時,它將獲得增量後的值。
// Store the counter to the Preferences
preferences.putUInt("counter", counter);
最後,我們關閉Preferences儲物櫃並在10秒後重啟ESP32。
// Close the Preferences
preferences.end();
// Wait 10 seconds
Serial.println("Restarting in 10 seconds...");
delay(10000);
// Restart ESP
ESP.restart();
}
因為我們在進入迴圈之前重啟了ESP32,所以迴圈從未執行。因此,它保持為空。
void loop() {}
此示例很好地演示了ESP32 Preferences儲存確實是持久儲存的。當你在序列埠監視器上檢查列印的語句時,你可以看到計數在連續重置之間遞增。如果使用區域性變數,則不會發生這種情況。只有透過Preferences使用非易失性儲存才有可能。
參考文獻
ESP32中的SPIFFS
在上一章中,我們研究了Preferences作為一種在非易失性儲存器中儲存資料的方法,並瞭解瞭如何使用它們來儲存鍵值對。本章,我們研究SPIFFS(SPI Flash File Storage),它用於以檔案形式儲存更大的資料。可以將SPIFFS想象成ESP32晶片本身上的一個非常小的SD卡。預設情況下,大約1.5 MB的板載快閃記憶體分配給SPIFFS。你可以透過工具 -> 分割槽方案來檢視。
您可以看到還有其他幾種分割槽選項可用。但是,我們現在先不討論這些。對於大多數應用程式,更改分割槽方案都是不需要的。本教程中的所有章節都可以與預設分割槽方案良好地配合使用。
現在讓我們看看使用示例建立、修改、讀取和刪除SPIFFS檔案的過程。
程式碼演練
我們將再次使用提供的示例程式碼。前往檔案 -> 示例 -> SPIFFS -> SPIFFS_Test。此程式碼非常適合理解SPIFFS可能執行的所有檔案操作。它也可以在GitHub上找到。
我們首先包含兩個庫:FS.h和SPIFFS.h。FS代表檔案系統。
#include "FS.h" #include "SPIFFS.h"
接下來,你看到一個宏定義,FORMAT_SPIFFS_IF_FAILED。有一個相關的註釋建議你只需要在第一次執行測試時格式化SPIFFS。這意味著你可以在第一次執行後將此宏的值設定為false。格式化SPIFFS需要時間,不需要每次執行程式碼時都執行。因此,人們通常的做法是為格式化SPIFFS編寫單獨的程式碼,在燒錄主程式碼之前先燒錄它。主程式碼不包含格式化命令。但是,在這個例子中,為了完整性,這個宏被保留為true。
/* You only need to format SPIFFS the first time you run a test or else use the SPIFFS plugin to create a partition https://github.com/me−no−dev/arduino−esp32fs−plugin */ #define FORMAT_SPIFFS_IF_FAILED true
接下來,您可以看到為不同的檔案系統操作定義了許多函式。它們是:
listDir - 列出所有目錄
readFile - 讀取特定檔案
writeFile - 寫入檔案(這將覆蓋檔案中已存在的內容)
appendFile - 向檔案追加內容(當你想新增現有內容而不是覆蓋它時使用)
renameFile - 更改檔名
deleteFile - 刪除檔案
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("− failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println(" − not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if(!file || file.isDirectory()){
Serial.println("− failed to open file for reading");
return;
}
Serial.println("− read from file:");
while(file.available()){
Serial.write(file.read());
}
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("− failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("− file written");
}else {
Serial.println("− frite failed");
}
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\r\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("− failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("− message appended");
} else {
Serial.println("− append failed");
}
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\r\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("− file renamed");
} else {
Serial.println("− rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\r\n", path);
if(fs.remove(path)){
Serial.println("− file deleted");
} else {
Serial.println("− delete failed");
}
}
請注意,以上所有函式都沒有請求檔名。它們請求的是完整的路徑。因為這是一個檔案系統。你可能有目錄、子目錄以及這些子目錄中的檔案。因此,ESP32需要知道你要操作的檔案的完整路徑。
接下來是一個不完全是檔案操作函式的函式-testFileIO。這更像是一個時間基準測試函式。它執行以下操作:
將大約1 MB(2048 * 512位元組)的資料寫入你提供的檔案路徑並測量寫入時間
讀取相同的檔案並測量讀取時間
void testFileIO(fs::FS &fs, const char * path){
Serial.printf("Testing file I/O with %s\r\n", path);
static uint8_t buf[512];
size_t len = 0;
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("− failed to open file for writing");
return;
}
size_t i;
Serial.print("− writing" );
uint32_t start = millis();
for(i=0; i<2048; i++){
if ((i & 0x001F) == 0x001F){
Serial.print(".");
}
file.write(buf, 512);
}
Serial.println("");
uint32_t end = millis() − start;
Serial.printf(" − %u bytes written in %u ms\r\n", 2048 * 512, end);
file.close();
file = fs.open(path);
start = millis();
end = start;
i = 0;
if(file && !file.isDirectory()){
len = file.size();
size_t flen = len;
start = millis();
Serial.print("− reading" );
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
if ((i++ & 0x001F) == 0x001F){
Serial.print(".");
}
len −= toRead;
}
Serial.println("");
end = millis() - start;
Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
file.close();
} else {
Serial.println("- failed to open file for reading");
}
}
請注意,buf陣列從未初始化為任何值。我們很可能會將垃圾位元組寫入檔案。這沒關係,因為該函式的目的是測量寫入時間和讀取時間。
定義函式後,我們繼續進行設定,其中顯示了每個函式的呼叫。
void setup(){
Serial.begin(115200);
if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
Serial.println("SPIFFS Mount Failed");
return;
}
listDir(SPIFFS, "/", 0);
writeFile(SPIFFS, "/hello.txt", "Hello ");
appendFile(SPIFFS, "/hello.txt", "World!\r\n");
readFile(SPIFFS, "/hello.txt");
renameFile(SPIFFS, "/hello.txt", "/foo.txt");
readFile(SPIFFS, "/foo.txt");
deleteFile(SPIFFS, "/foo.txt");
testFileIO(SPIFFS, "/test.txt");
deleteFile(SPIFFS, "/test.txt");
Serial.println( "Test complete" );
}
設定主要執行以下操作:
它首先使用SPIFFS.begin()初始化SPIFFS。這裡使用了開頭定義的宏。為true時,它格式化SPIFFS(耗時);為false時,它在不進行格式化的情況下初始化SPIFFS。
然後它列出根級別上的所有目錄。請注意,我們已將級別指定為0。因此,我們沒有列出目錄中的子目錄。你可以透過遞增levels引數來增加巢狀。
然後它將“Hello”寫入根目錄中的hello.txt檔案。(如果檔案不存在,則會建立它)
然後它讀取hello.txt。
然後它將hello.txt重新命名為foo.txt。
然後它讀取foo.txt以檢視重新命名是否成功。你應該看到打印出“Hello”,因為這就是儲存在檔案中的內容。
然後它刪除foo.txt。
然後它在新的檔案test.txt上執行testFileIO例程。
例程執行完畢後,它刪除test.txt。
就是這樣。此示例程式碼很好地列出了並測試了您可能想要與SPIFFS一起使用的所有函式。您可以繼續修改此程式碼,並嘗試不同的函式。
由於我們不想在這裡執行任何重複活動,因此迴圈為空。
void loop(){
}
序列埠監視器中顯示的輸出可能如下面的影像所示:
注意 - 如果在執行草圖時出現“SPIFFS Mount Failed”,請將FORMAT_SPIFFS_IF_FAILED的值設定為false,然後重試。
參考文獻
ESP32與OLED顯示屏介面
OLED與ESP32的組合非常流行,以至於有些ESP32開發板集成了OLED。但是,我們將假設您將使用單獨的OLED模組與您的ESP32開發板一起使用。如果您有OLED模組,它可能看起來像下面的影像。
將OLED顯示模組連線到ESP32
與我們在上一章中討論的MPU6050模組一樣,OLED模組通常也使用I2C進行通訊。因此,連線將類似於MPU6050模組。您需要將SDA線連線到ESP32上的21引腳,SCL線連線到22引腳,GND連線到GND,VCC連線到3V3引腳。
OLED顯示器的庫
有許多庫可用於將OLED顯示器與ESP32連線。您可以隨意使用任何您感到舒適的庫。在本例中,我們將使用ThingPulse和Fabrice Weinberg的“ESP8266和ESP32 OLED驅動程式,用於SSD1306顯示器”。您可以從工具 -> 管理庫中安裝此庫。它也可以在GitHub上找到。
程式碼演練
由於我們剛剛安裝的庫,程式碼變得非常簡單。我們將執行一個計數器程式碼,它將計算自上次重置以來的秒數,並在OLED模組上打印出來。程式碼可以在GitHub上找到。
我們首先包含SSD1306庫。
#include "SSD1306Wire.h"
接下來,我們定義OLED引腳及其I2C地址。請注意,有些OLED模組包含額外的復位引腳。一個很好的例子是ESP32 TTGO開發板,它帶有內建的OLED顯示屏。對於該開發板,引腳16是復位引腳。如果您將外部OLED模組連線到ESP32,則很可能不會使用復位引腳。0x3c的I2C地址通常對所有OLED模組都適用。
//OLED related variables #define OLED_ADDR 0x3c #define OLED_SDA 21//4 //TTGO board without SD Card has OLED SDA connected to pin 4 of ESP32 #define OLED_SCL 22//15 //TTGO board without SD Card has OLED SCL connected to pin 15 of ESP32 #define OLED_RST 16 //Optional, TTGO board contains OLED_RST connected to pin 16 of ESP32
接下來,我們建立OLED顯示物件和計數器變數。
SSD1306Wire display(OLED_ADDR, OLED_SDA, OLED_SCL); int counter = 0;
之後,我們定義兩個函式。一個用於初始化OLED顯示屏(如果您的OLED模組不包含復位引腳,則此函式是冗餘的),另一個用於在OLED顯示屏上列印文字訊息。showOLEDMessage()函式將OLED顯示區域劃分為3行,並要求提供3個字串,每行一個。
void initOLED() {
pinMode(OLED_RST, OUTPUT);
//Give a low to high pulse to the OLED display to reset it
//This is optional and not required for OLED modules not containing a reset pin
digitalWrite(OLED_RST, LOW);
delay(20);
digitalWrite(OLED_RST, HIGH);
}
void showOLEDMessage(String line1, String line2, String line3) {
display.init(); // clears screen
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, line1); // adds to buffer
display.drawString(0, 20, line2);
display.drawString(0, 40, line3);
display.display(); // displays content in buffer
}
最後,在setup函式中,我們只需初始化OLED顯示屏;在loop函式中,我們只需使用顯示屏的前兩行來顯示計數器。
void setup() {
// put your setup code here, to run once:
initOLED();
}
void loop() {
// put your main code here, to run repeatedly
showOLEDMessage("Num seconds is: ", String(counter), "");
delay(1000);
counter = counter+1;
}
就是這樣。恭喜您在OLED顯示屏上顯示了您的第一個文字語句。
ESP32上的WiFi
WiFi堆疊的可用性是ESP32與其他微控制器之間主要區別之一。本章將簡要概述ESP32上可用的各種WiFi模式。後續章節將介紹使用HTTP、HTTPS和MQTT透過WiFi傳輸資料。ESP32上可以配置WiFi的3種主要模式:
站模式(Station Mode)− 這類似於WiFi客戶端模式。ESP32連線到可用的WiFi網路,該網路又連線到您的網際網路。這與將您的手機連線到可用的WiFi網路完全相同。
接入點模式(Access Point Mode)− 這相當於開啟手機上的熱點,以便其他裝置可以連線到它。類似地,ESP32在其周圍建立一個WiFi網路,其他裝置可以連線到它。但是,ESP32本身沒有網際網路訪問許可權。因此,使用此模式,您通常只能顯示硬編碼到ESP32記憶體中的幾個網頁。此模式通常用於安裝期間執行裝置設定。例如,您將ESP32帶到一個未知的客戶端站點,事先不知道其WiFi憑據。您將對ESP32進行程式設計以接入點模式開始執行。一旦您的手機連線到ESP32建立的WiFi網路,就會開啟一個頁面(Captive Portal),並提示您輸入WiFi憑據。輸入這些憑據後,ESP32將切換到站模式,並嘗試使用提供的憑據連線到可用的WiFi網路。
組合AP-STA模式(Combined AP-STA mode)− 正如您可能猜到的那樣,在此模式下,ESP32連線到現有的WiFi網路,同時它也建立了自己的網路,其他裝置可以連線到它。
大多數情況下,您將以站模式使用ESP32。在接下來的3章中,我們也將以站模式使用ESP32。但是,您也應該瞭解AP模式,並鼓勵您自己探索AP模式的示例。
使用HTTP透過WiFi傳輸資料
HTTP(超文字傳輸協議)是最常見的通訊形式之一,使用ESP32,我們可以使用HTTP請求與任何Web伺服器互動。讓我們在本節瞭解一下。
關於HTTP請求的簡要介紹
HTTP請求發生在客戶端和伺服器之間。顧名思義,伺服器根據請求向客戶端“提供”資訊。Web伺服器通常提供網頁。例如,當您在Internet瀏覽器中鍵入https://www.linkedin.com/login時,您的PC或筆記型電腦充當客戶端,並向託管linkedin.com的伺服器請求與/login地址對應的頁面。您將獲得一個HTML頁面作為返回,然後由您的瀏覽器顯示。
HTTP遵循請求-響應模型,這意味著通訊始終由客戶端發起。伺服器不能毫無徵兆地與任何客戶端通訊,也不能與任何客戶端啟動通訊。通訊始終必須由客戶端以請求的形式發起,伺服器只能響應該請求。伺服器的響應包含狀態程式碼(記得404嗎?這是一個狀態程式碼),以及(如果適用)請求的內容。所有狀態程式碼的列表可以在這裡找到這裡。
那麼,伺服器如何識別HTTP請求呢?透過請求的結構。HTTP請求遵循固定的結構,該結構由3部分組成:
請求行後跟回車換行符(CRLF = \r\n)
零個或多個標題行後跟CRLF和一個空行,再次後跟CRLF
可選正文
典型的HTTP請求如下所示:
POST / HTTP/1.1 //Request line, containing request method (POST in this case)
Host: www.example.com //Headers
//Empty line between headers
key1=value1&key2=value2 //Body
伺服器響應如下所示:
HTTP/1.1 200 OK //Response line; 200 is the status code
Date: Mon, 23 May 2005 22:38:34 GMT //Headers
Content-Type: text/html; charset=UTF-8
Content-Length: 155
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
ETag: "3f80f−1b6−3e1cb03b"
Accept-Ranges: bytes
Connection: close
//Empty line between headers and body
<html>
<head>
<title>An Example Page</title>
</head>
<body>
<p>Hello World, this is a very simple HTML document.</p>
</body>
</html>
事實上,TutorialsPoint上有一個關於HTTP請求結構的非常好的教程。它還介紹了各種請求方法(GET、POST、PUT等)。在本節中,我們將關注GET和POST方法。
GET請求包含所有引數,這些引數以請求URL本身中的鍵值對形式存在。例如,如果使用GET而不是POST傳送上面的相同示例請求,它將如下所示:
GET /test/demo_form.php?key1=value1&key2=value2 HTTP/1.1 //Request line
Host: www.example.com //Headers
//No need for a body
正如您現在所猜到的那樣,POST請求包含在正文中的引數,而不是URL。GET和POST之間還有其他一些區別,您可以在這裡閱讀。但關鍵是,您將使用POST與伺服器共享敏感資訊,例如密碼。
程式碼演練
在本節中,我們將從頭開始編寫我們的HTTP請求。有像httpClient這樣的庫專門用於處理ESP32 HTTP請求,這些庫負責構建HTTP請求,但我們將自己構建請求。這給了我們更大的靈活性。在本教程中,我們將限制在ESP32客戶端模式。ESP32也可以使用HTTP伺服器模式,但這留給您去探索。
我們將使用httpbin.org作為我們的伺服器。它基本上是為測試您的HTTP請求而構建的。您可以使用此伺服器測試GET、POST和各種其他方法。參見此處。
程式碼可以在GitHub上找到。
我們首先包含WiFi庫。
#include <WiFi.h>
接下來,我們將定義一些常量。對於HTTP,使用的埠是80。這是標準的。同樣,我們對HTTPS使用443,對FTP使用21,對DNS使用53,等等。這些是保留的埠號。
const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; const char* server = "httpbin.org"; const int port = 80;
最後,我們建立我們的WiFiClient物件。
WiFiClient client
在setup函式中,我們只需使用提供的憑據以站模式連線到WiFi。
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA); //The WiFi is in station mode. The other is the softAP mode
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(""); Serial.print("WiFi connected to: "); Serial.println(ssid); Serial.println("IP address: "); Serial.println(WiFi.localIP());
delay(2000);
}
loop函式在這裡變得很重要。HTTP請求就是在那裡執行的。我們首先讀取ESP32的晶片ID。我們將將其作為引數與我們的名稱一起傳送到伺服器。我們將使用這些引數構造HTTP請求的正文。
void loop() {
int conn;
int chip_id = ESP.getEfuseMac();;
Serial.printf(" Flash Chip id = %08X\t", chip_id);
Serial.println();
Serial.println();
String body = "ChipId=" + String(chip_id) + "&SentBy=" + "your_name";
int body_len = body.length();
請注意SentBy欄位之前的&。&用作HTTP請求中不同鍵值對之間的分隔符。接下來,我們連線到伺服器。
Serial.println(".....");
Serial.println(); Serial.print("For sending parameters, connecting to "); Serial.println(server);
conn = client.connect(server, port);
POST請求
如果我們的連線成功,client.connect()將返回1。我們在發出請求之前檢查這一點。
if (conn == 1) {
Serial.println(); Serial.print("Sending Parameters...");
//Request
client.println("POST /post HTTP/1.1");
//Headers
client.print("Host: "); client.println(server);
client.println("Content-Type: application/x−www−form−urlencoded");
client.print("Content-Length: "); client.println(body_len);
client.println("Connection: Close");
client.println();
//Body
client.println(body);
client.println();
//Wait for server response
while (client.available() == 0);
//Print Server Response
while (client.available()) {
char c = client.read();
Serial.write(c);
}
} else {
client.stop();
Serial.println("Connection Failed");
}
正如您所看到的,我們使用client.print()或client.println()傳送我們的請求行。請求、標頭和正文透過註釋清楚地指示。在請求行中,POST /post HTTP/1.1等效於POST http://httpbin.org/post HTTP/1.1。由於我們已經在client.connect(server,port)中提到了伺服器,因此可以理解/post指的是伺服器/post URL。
特別是對於POST請求,Content-Length標頭非常重要。如果沒有它,許多伺服器會假設內容長度為0,這意味著沒有正文。Content-Type已保留為application/x−www−form−urlencoded,因為我們的正文表示表單資料。在典型的表單提交中,您將擁有Name、Address等鍵以及相應的值。您可以擁有其他幾種內容型別。有關完整列表,請參見此處。
Connection: Close標頭告訴伺服器在請求處理完畢後關閉連線。如果您希望在請求處理完畢後保持連線,則可以改用Connection: Keep-Alive。
這些只是我們可以包含的一些標頭。HTTP標頭的完整列表可以在此處找到。
現在,httpbin.org/post URL通常只會回顯我們的正文。示例響應如下:
HTTP/1.1 200 OK
Date: Sat, 21 Nov 2020 16:25:47 GMT
Content−Type: application/json
Content−Length: 402
Connection: close
Server: gunicorn/19.9.0
Access−Control−Allow−Origin: *
Access−Control−Allow−Credentials: true
{
"args": {},
"data": "",
"files": {},
"form": {
"ChipId": "1780326616",
"SentBy": "Yash"
},
"headers": {
"Content−Length": "34",
"Content−Type": "application/x−www−form−urlencoded",
"Host": "httpbin.org",
"X-Amzn−Trace−Id": "Root=1−5fb93f8b−574bfb57002c108a1d7958bb"
},
"json": null,
"origin": "183.87.63.113",
"url": "http://httpbin.org/post"
}
正如您所看到的,“form”欄位中回顯了POST正文的內容。您應該在序列監視器上看到類似以上內容的輸出。還要注意URL欄位。它清楚地表明請求行中的/post地址被解釋為http://httpbin.org/post。
最後,我們將等待5秒鐘,然後結束迴圈,從而再次發出請求。
delay(5000); }
GET請求
此時,您可能想知道,將此POST請求轉換為GET請求需要進行哪些更改。實際上這非常簡單。首先,您將呼叫/get地址而不是/post地址。然後,您將在問號(?)後將正文內容附加到URL。最後,您將方法替換為GET。此外,不再需要Content-Length和Content−Type標頭,因為您的正文為空。因此,您的請求塊將如下所示:
if (conn == 1) {
String path = String("/get") + String("?") +body;
Serial.println(); Serial.print("Sending Parameters...");
//Request
client.println("GET "+path+" HTTP/1.1");
//Headers
client.print("Host: "); client.println(server);
client.println("Connection: Close");
client.println();
//No Body
//Wait for server response
while (client.available() == 0);
//Print Server Response
while (client.available()) {
char c = client.read();
Serial.write(c);
}
} else {
client.stop();
Serial.println("Connection Failed");
}
相應的響應將如下所示:
HTTP/1.1 200 OK
Date: Tue, 17 Nov 2020 18:05:34 GMT
Content-Type: application/json
Content-Length: 497
Connection: close
Server: gunicorn/19.9.0
Access-Control−Allow−Origin: *
Access-Control-Allow-Credentials: true
{
"args": {
"ChipID": "3F:A0:A1:77:0D:84",
"SentBy": "Yash"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "deflate, gzip",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"X−Amzn−Trace−Id": "Root=1−5fb410ee−3630963b0b7980c959c34038"
},
"origin": "206.189.180.4",
"url": "https://httpbin.org/get?ChipID=3F:A0:A1:77:0D:84&SentBy=Yash"
}
正如您所看到的,傳送到伺服器的引數現在返回在args欄位中,因為它們作為引數傳送到URL本身。
恭喜!!您已成功使用ESP32傳送HTTP請求。
參考文獻
使用HTTPS透過WiFi傳輸資料
在上一章中,我們探討了使用 ESP32 透過 HTTP 傳輸資料。本章,我們將學習透過 HTTPS 傳輸資料。HTTPS 中的“S”代表“安全”。基本上,您傳輸的任何資料都會使用傳輸層安全協議 (TLS) 進行加密。這意味著,如果有人竊聽您的通訊,他們將無法理解您傳輸的內容。他們看到的將是一些亂碼。本章不涵蓋 HTTPS 的工作原理。但只需簡單的 Google 搜尋即可找到多個有用的資源供您入門。本章,我們將學習如何在 ESP32 上實現 HTTPS。
將任何 HTTP 請求轉換為 ESP32 上的 HTTPS
一般來說,如果您有傳送 HTTP 請求到伺服器的程式碼,您可以按照以下簡單的步驟將其轉換為 HTTPS:
將庫從 WiFiClient 更改為 WiFiClientSecure(您需要包含 WiFiClientSecure.h)
將埠從 80 更改為 443
可選的第四步:為伺服器新增 CA 證書。此步驟是可選的,因為它不會影響通訊的安全。它只是確保您正在與正確的伺服器通訊。如果您不提供 CA 證書,您的通訊仍然是安全的。
程式碼演練
您看到的以下程式碼與用於 HTTP 通訊的程式碼非常相似。強烈建議您重新閱讀上一章。在本演練中,我們將僅重點介紹與 HTTP 程式碼不同的部分。
程式碼可在 GitHub 上找到
我們首先包含 WiFi 庫。我們還需要在此處包含 WiFiClientSecure 庫。
#include <WiFi.h> #include <WiFiClientSecure.h>
接下來,我們將定義常量。請注意,埠現在是 443 而不是 80。
const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; const char* server = "httpbin.org"; const int port = 443;
接下來,我們將建立 WiFiClientSecure 物件,而不是 WiFiClient 物件。
WiFiClientSecure client;
接下來,我們為我們的伺服器 (httpbin.org) 定義 CA 證書。現在,您可能想知道如何獲取我們伺服器的 CA 證書。此處 提供了使用 Google Chrome 獲取任何伺服器 CA 證書的詳細步驟。在同一篇文章中,還提供了一份關於 CA 證書有效性的說明,建議使用證書頒發機構的證書,而不是伺服器的證書,尤其是在您只編程裝置一次並將其用於現場多年應用的場景中。證書頒發機構的證書具有更長的有效期(15年以上),而伺服器證書的有效期較短(1-2年)。因此,我們使用 Starfield Class 2 證書頒發機構的證書(有效期至 2034 年),而不是 httpbin.org 的證書(有效期至 2021 年 2 月)。
const char* ca_cert = \ "-----BEGIN CERTIFICATE-----\n" \ "MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\n"\ "MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\n"\ "U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\n"\ "NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\n"\ "ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\n"\ "ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\n"\ "DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n"\ "8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n"\ "+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\n"\ "X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\n"\ "K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n"\ "1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\n"\ "A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\n"\ "zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\n"\ "YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\n"\ "bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\n"\ "DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\n"\ "L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\n"\ "eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\n"\ "xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\n"\ "VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\n"\ "WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n"\ "-----END CERTIFICATE-----\n";
在設定中,我們像以前一樣使用提供的憑據以站模式連線到 WiFi。在這裡,我們還有額外的步驟來為我們的 WiFiSecureClient 設定 CA 證書。透過這樣做,我們告訴客戶端只有在它的 CA 證書與提供的證書匹配時才與伺服器通訊。
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA); //The WiFi is in station mode. The other is the softAP mode
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(""); Serial.print("WiFi connected to: "); Serial.println(ssid); Serial.println("IP address: "); Serial.println(WiFi.localIP());
client.setCACert(ca_cert); //Only communicate with the server if the CA certificates match
delay(2000);
}
迴圈與 HTTP 示例中使用的迴圈完全相同。
void loop() {
int conn;
int chip_id = ESP.getEfuseMac();;
Serial.printf(" Flash Chip id = %08X\t", chip_id);
Serial.println();
Serial.println();
String body = "ChipId=" + String(chip_id) + "&SentBy=" + "your_name";
int body_len = body.length();
Serial.println(".....");
Serial.println(); Serial.print("For sending parameters, connecting to "); Serial.println(server);
conn = client.connect(server, port);
if (conn == 1) {
Serial.println(); Serial.print("Sending Parameters...");
//Request
client.println("POST /post HTTP/1.1");
//Headers
client.print("Host: "); client.println(server);
client.println("Content-Type: application/x−www−form−urlencoded");
client.print("Content-Length: "); client.println(body_len);
client.println("Connection: Close");
client.println();
//Body
client.println(body);
client.println();
//Wait for server response
while (client.available() == 0);
//Print Server Response
while (client.available()) {
char c = client.read();
Serial.write(c);
}
} else {
client.stop();
Serial.println("Connection Failed");
}
delay(5000);
}
伺服器應返回的響應也與 HTTP 示例類似。唯一的區別是收到的響應也將是安全的。但我們不必擔心解密加密的訊息。ESP32 會為我們完成。
注意伺服器響應中的 URL 欄位。它包含 https 而不是 http,這確認我們的傳輸是安全的。事實上,如果您稍微修改 CA 證書,例如刪除一個字元,然後嘗試執行草圖,您將看到連線失敗。
但是,如果您從設定中刪除了`client.setCACert()`行,即使使用有問題的 CA 證書,連線也將再次安全地建立。這證明設定 CA 證書不會影響我們通訊的安全。它只是幫助我們驗證我們正在與正確的伺服器通訊。如果我們設定證書,那麼 ESP32 將不會與伺服器通訊,除非提供的 CA 證書與伺服器的 CA 證書匹配。如果我們不設定證書,ESP32 仍然可以安全地與伺服器通訊。
恭喜!!您已成功使用 ESP32 傳送 HTTPS 請求。
注意 - ESP32 上執行 HTTPS 訊息加密的硬體加速器最多支援 16384 位元組(16 KB)的資料。因此,如果您的訊息大小超過 16 KB,您可能需要將其分解成塊。
參考文獻
使用MQTT透過WiFi傳輸資料
MQTT(訊息佇列遙測傳輸)在物聯網裝置領域獲得了廣泛的關注。它是一種通常在 TCP/IP 上執行的協議。MQTT 使用代理-客戶端模型,而不是我們看到的 HTTP 的伺服器-客戶端模型。維基百科將 MQTT 代理和客戶端定義為:
MQTT 代理是一個伺服器,它接收來自客戶端的所有訊息,然後將訊息路由到相應的目標客戶端。MQTT 客戶端是任何執行 MQTT 庫並透過網路連線到 MQTT 代理的裝置(從微控制器到成熟的伺服器)。
將代理想象成像 Medium 這樣的服務。主題將是 Medium 的出版物,客戶端將是 Medium 的使用者。使用者(客戶端)可以釋出到出版物,訂閱該出版物(主題)的另一個使用者(客戶端)將被告知有新的帖子可供閱讀。到目前為止,您應該已經瞭解了 HTTP 和 MQTT 之間的一個主要區別。在 HTTP 中,您的訊息直接傳送到目標伺服器,您甚至會以狀態碼的形式獲得確認。在 MQTT 中,您只是將訊息傳送到代理,希望您的目標伺服器會從中獲取。如果您資源受限,MQTT 的幾個特性將成為一大優勢。它們列在下面:
使用 MQTT,報頭開銷非常短,吞吐量很高。這有助於節省時間和電池電量。
MQTT 以位元組陣列的形式傳送資訊,而不是文字格式。這使訊息更輕量級。
因為 MQTT 不依賴於伺服器的響應,所以客戶端是獨立的,一旦傳輸訊息,就可以進入休眠狀態(節省電池電量)。
這些只是導致 MQTT 受歡迎的一些方面。您可以此處 獲取 MQTT 和 HTTP 之間更詳細的比較。
程式碼演練
一般來說,測試 MQTT 需要您註冊一個免費/付費代理帳戶。AWS IoT 和 Azure IoT 是非常流行的提供 MQTT 代理服務的平臺,但它們需要冗長的註冊和配置過程。幸運的是,有一個來自 HiveMQ 的免費代理服務,可用於在無需註冊或配置的情況下測試 MQTT。它非常適合那些剛接觸 MQTT 並只想動手操作的人,還可以讓您更專注於 ESP32 的韌體。因此,我們將使用本章中的代理。當然,因為它是一項免費服務,所以會有侷限性。您不能共享敏感資訊,因為所有訊息都是公開的,任何人都可以訂閱您的主題。當然,出於測試目的,這些限制無關緊要。
程式碼可在 GitHub 上找到
我們將使用 PubSubClient 庫。您可以從工具 -> 管理庫中安裝它。
安裝庫後,我們將包含 WiFi 和 PubSubClient 庫。
#include <WiFi.h> #include <PubSubClient.h>
接下來,我們將定義一些常量。請記住替換 WiFi 憑據。mqttServer 和 mqttPort 是 http://www.mqtt−dashboard.com/ 規定的。mqtt_client_name、mqtt_pub_topic 和 mqtt_sub_topic 可以是您選擇的任何字串。只需確保更改它們的值。如果多個使用者從本教程複製相同的程式碼,您將在測試時收到來自未知客戶端的大量訊息。
我們還定義了 WiFiClient 和 mqttClient 物件。MQTTClient 物件需要網路客戶端作為引數。如果您使用乙太網,則會提供乙太網客戶端作為引數。由於我們使用的是 WiFi,因此我們提供了 WiFi 客戶端作為引數。
const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; //The broker and port are provided by http://www.mqtt−dashboard.com/ char *mqttServer = "broker.hivemq.com"; int mqttPort = 1883; //Replace these 3 with the strings of your choice const char* mqtt_client_name = "ESPYS2111"; const char* mqtt_pub_topic = "/ys/testpub"; //The topic to which our client will publish const char* mqtt_sub_topic = "/ys/testsub"; //The topic to which our client will subscribe WiFiClient client; PubSubClient mqttClient(client);
接下來,我們定義回撥函式。回撥函式是一箇中斷函式。每當從已訂閱的主題接收到新訊息時,都會觸發此函式。它有三個引數:接收訊息的主題、作為位元組陣列的訊息以及訊息的長度。您可以對該訊息執行任何操作(將其儲存在 SPIFFS 中,將其傳送到另一個主題,等等)。在這裡,我們只是列印主題和訊息。
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message received from: "); Serial.println(topic);
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
Serial.println();
}
在設定中,我們像在其他每個草圖中一樣連線到 WiFi。最後兩行與 MQTT 有關。我們設定 MQTT 的伺服器和埠以及回撥函式。
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
WiFi.mode(WIFI_STA); //The WiFi is in station mode
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(""); Serial.print("WiFi connected to: "); Serial.println(ssid); Serial.println("IP address: "); Serial.println(WiFi.localIP());
delay(2000);
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCallback(callback);
}
在迴圈中,我們執行以下操作
如果客戶端未連線到代理,我們將使用我們的客戶端名稱連線它。
連線後,我們還將客戶端訂閱到 mqtt_sub_topic。
然後,我們將訊息釋出到 mqtt_pub_topic
然後我們執行`mqttClient.loop()`。此 loop() 函式應定期呼叫。它維護客戶端與代理的連線,並幫助客戶端處理傳入的訊息。如果您沒有此`mqttClient.loop()`行,您將能夠釋出到 mqtt_pub_topic,但不會從 mqtt_sub_topic 獲取訊息,因為只有在此行被呼叫時才會處理傳入的訊息。
最後,我們等待 5 秒,然後再開始此迴圈。
void loop() {
// put your main code here, to run repeatedly:
if (!mqttClient.connected()){
while (!mqttClient.connected()){
if(mqttClient.connect(mqtt_client_name)){
Serial.println("MQTT Connected!");
mqttClient.subscribe(mqtt_sub_topic);
}
else{
Serial.print(".");
}
}
}
mqttClient.publish(mqtt_pub_topic, "TestMsg");
Serial.println("Message published");
mqttClient.loop();
delay(5000);
}
測試程式碼
為了測試上述程式碼,您需要訪問 www.hivemq.com
進入該網頁後,請按照以下步驟操作:
單擊連線
單擊“新增新的主題訂閱”,然後輸入 ESP32 將釋出到的主題名稱(在本例中為 /ys/testpub)
重新整理 ESP32 後,您將每 5 秒開始接收該主題上的訊息。
- 接下來,要測試 ESP32 上的訊息接收,請輸入 ESP32 訂閱的主題名稱(在本例中為 ys/testsub),然後在訊息框中鍵入訊息並單擊發布。您應該在序列埠監視器上看到該訊息。
恭喜!!您已經測試了使用 ESP32 釋出和訂閱 MQTT。
參考文獻
透過經典藍牙傳輸資料
本章介紹了使用 ESP32 透過藍牙傳輸資料。Arduino 為 ESP32 提供了一個專用的 BluetoothSerial 庫,它使透過藍牙傳輸資料就像向序列埠監視器傳輸資料一樣簡單。我們將學習如何建立 ESP32 周圍的藍牙欄位,將智慧手機連線到該欄位,並與 ESP32 通訊。
程式碼演練
我們將使用本章的示例程式碼。您可以在檔案 -> 示例 -> BluetoothSerial -> SerialToSerialBT 中找到它。它也可以在 GitHub 上找到。
我們首先包含 BluetoothSerial 庫。
#include <BluetoothSerial.h>
如果你之前沒有使用過 ESP32,那麼接下來的幾行程式碼可能有些無關緊要。它們檢查藍牙配置是否已啟用,如果未啟用則會顯示警告。ESP32 預設情況下啟用藍牙配置,因此,如果你僅使用 Arduino IDE 來操作 ESP32,可以直接註釋掉這些行。錯誤訊息中提到的 `make menuconfig` 實際上是透過 ESP-IDF 訪問的,而不是透過 Arduino IDE。所以,不用擔心這些程式碼。
#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) #error Bluetooth is not enabled! Please run `make menuconfig` to and enable it #endif
接下來,我們定義 BluetoothSerial 物件。
BluetoothSerial SerialBT;
在 `setup` 函式中,我們將使用 `**SerialBT.begin()**` 函式啟動 ESP32 的藍牙功能。此函式需要一個引數,即你的藍牙裝置名稱(本例中為 ESP32)。這是你在手機上掃描藍牙網路時顯示的名稱。
void setup() {
Serial.begin(115200);
SerialBT.begin("ESP32test"); //Bluetooth device name
Serial.println("The device started, now you can pair it with bluetooth!");
}
現在,迴圈部分非常簡單。如果序列埠 (例如,你在序列埠監視器上輸入的文字) 有任何傳入文字,則將其透過 SerialBT 傳送出去。如果 SerialBT 有任何傳入文字,則將其透過序列埠傳送,或者換句話說,將其列印在序列埠監視器上。
void loop() {
if (Serial.available()) {
SerialBT.write(Serial.read());
}
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
delay(20);
}
程式碼測試
要測試此程式碼,建議你在智慧手機上下載一個序列埠藍牙終端應用程式(可以使用下面顯示的應用程式或任何等效的應用程式)。它可以幫助你與 ESP32 配對,顯示從 ESP32 收到的訊息,並幫助你向 ESP32 傳送訊息。
要在 Android 裝置上安裝它,請點選 這裡。iOS 的等效應用程式可以是 BluTerm。
你可以檢視下面使用序列埠藍牙終端應用程式執行的測試截圖。我已經將 ESP32 的藍牙名稱更改為“ESP32test345”,因為我的手機已經與另一個藍牙名稱為“ESP32test”的 ESP32 配對了。配對完成後,可以在序列埠藍牙終端應用程式中新增該裝置,然後你可以像在訊息應用程式中與其他使用者通訊一樣與你的裝置通訊。
配對和通訊
Arduino 序列埠終端的對應檢視
恭喜你!你已經使用藍牙與你的 ESP32 通訊了。繼續探索 BluetoothSerial 庫附帶的其他示例。
注意 - 你可能會想同時在 ESP32 上使用 WiFi 和藍牙。這並不推薦。雖然 ESP32 為 WiFi 和藍牙提供了獨立的堆疊,但它們共享一個公共無線電天線。因此,當兩個堆疊都試圖訪問天線時,模組的行為變得不可預測。建議一次只允許一個堆疊訪問天線。
從 NTP 伺服器獲取當前時間
在物聯網裝置中,時間戳成為裝置和伺服器之間交換的資料包的一個重要屬性。因此,始終在裝置上擁有正確的時間非常必要。一種方法是使用與 ESP32 相連的 RTC(即時時鐘)。你甚至可以使用 ESP32 的內部 RTC。一旦給定參考時間,它就可以正確輸出未來的時間戳。但是你如何獲取參考時間呢?一種方法是在程式設計 ESP32 時硬編碼當前時間。但這並不是一個好方法。其次,RTC 容易漂移,定期向其提供參考時間戳是一個好主意。在本章中,我們將學習如何從 NTP 伺服器獲取當前時間,將其饋送到 ESP32 的內部 RTC 一次,並列印未來的時間戳。
關於 NTP 的簡要介紹
NTP 代表網路時間協議 (Network Time Protocol)。它是一種用於計算機系統之間時鐘同步的協議。簡單來說,某個地方有一個伺服器準確地維護著時間。每當客戶端向 NTP 伺服器請求當前時間時,它都會返回精確到 100 毫秒以內的當前時間。你可以這裡瞭解更多關於 NTP 的資訊。對於 ESP32,有一個內建的 `time` 庫可以處理與 NTP 伺服器的所有通訊。讓我們在下面的程式碼演練中探索該庫的使用。
程式碼演練
我們將使用一個內建示例進行此演練。它可以在 檔案 -> 示例 -> ESP32 -> 時間 -> SimpleTime 中找到。它也可以在 GitHub 上找到。
我們首先包含 WiFi 和 time 庫。
#include <WiFi.h> #include "time.h"
接下來,我們定義一些全域性變數。將 WiFi SSID 和密碼替換為你 WiFi 的相應值。接下來,我們定義了 NTP 伺服器的 URL。`gmtOffset_sec` 指的是你的時區相對於 GMT 或密切相關的 UTC 的秒數偏移量。例如,在印度,時區比 UTC 快 5 小時 30 分鐘,`gmtOffset_sec` 將為 (5+0.5)*3600 = 19800。
`daylightOffset_sec` 與實行夏令時的國家相關。在其他國家,可以簡單地將其設定為 0。
const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASS"; const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 3600; const int daylightOffset_sec = 3600;
接下來,你可以看到一個函式 `**printLocalTime()**`。它只是從內部 RTC 獲取本地時間並將其列印到序列埠。
void printLocalTime()
{
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
return;
}
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}
你可能在這裡有三個問題:
- `struct tm` 在哪裡定義?
- `getLocalTime()` 函式在哪裡定義?
- `%A`、`%B` 等格式化程式是什麼?
`struct tm` 在我們在頂部包含的 `time.h` 檔案中定義。事實上,time 庫不是 ESP32 專用的庫。它是一個與 ESP32 相容的 AVR 庫。你可以在 這裡 找到原始碼。如果你檢視 `time.h` 檔案,你會看到 `struct tm`。
struct tm {
int8_t tm_sec; /**< seconds after the minute - [ 0 to 59 ] */
int8_t tm_min; /**< minutes after the hour - [ 0 to 59 ] */
int8_t tm_hour; /**< hours since midnight - [ 0 to 23 ] */
int8_t tm_mday; /**< day of the month - [ 1 to 31 ] */
int8_t tm_wday; /**< days since Sunday - [ 0 to 6 ] */
int8_t tm_mon; /**< months since January - [ 0 to 11 ] */
int16_t tm_year; /**< years since 1900 */
int16_t tm_yday; /**< days since January 1 - [ 0 to 365 ] */
int16_t tm_isdst; /**< Daylight Saving Time flag */
};
現在,`getLocalTime` 函式是 ESP32 專用的。它在 `esp32-hal-time.c` 檔案中定義。它是 ESP32 的 Arduino 核心的一部分,不需要在 Arduino 中單獨包含。你可以在 這裡 檢視原始碼。
現在,格式化程式的含義如下:
/* %a Abbreviated weekday name %A Full weekday name %b Abbreviated month name %B Full month name %c Date and time representation for your locale %d Day of month as a decimal number (01−31) %H Hour in 24-hour format (00−23) %I Hour in 12-hour format (01−12) %j Day of year as decimal number (001−366) %m Month as decimal number (01−12) %M Minute as decimal number (00−59) %p Current locale's A.M./P.M. indicator for 12−hour clock %S Second as decimal number (00−59) %U Week of year as decimal number, Sunday as first day of week (00−51) %w Weekday as decimal number (0−6; Sunday is 0) %W Week of year as decimal number, Monday as first day of week (00−51) %x Date representation for current locale %X Time representation for current locale %y Year without century, as decimal number (00−99) %Y Year with century, as decimal number %z %Z Time-zone name or abbreviation, (no characters if time zone is unknown) %% Percent sign You can include text literals (such as spaces and colons) to make a neater display or for padding between adjoining columns. You can suppress the display of leading zeroes by using the "#" character (%#d, %#H, %#I, %#j, %#m, %#M, %#S, %#U, %#w, %#W, %#y, %#Y) */
因此,使用我們的格式方案 `**%A, %B %d %Y %H:%M:%S**`,我們可以預期輸出類似於以下內容:星期日,2020 年 11 月 15 日 14:51:30。
現在,來看 `setup` 和 `loop` 函式。在 `setup` 函式中,我們初始化序列埠,使用我們的 WiFi 連線到網際網路,並使用 `configTime()` 函式配置 ESP32 的內部 RTC。正如你所看到的,該函式接受三個引數:`gmtOffset`、`daylightOffset` 和 `ntpServer`。它將從 `ntpServer` 獲取 UTC 時間,在本地應用 `gmtOffset` 和 `daylightOffset`,並返回輸出時間。這個函式,就像 `getLocalTime` 一樣,是在 esp32-hal-time.c 檔案中定義的。正如你從檔案中看到的,TCP/IP 協議用於從 NTP 伺服器獲取時間。
一旦我們從 NTP 伺服器獲取時間並將其饋送到 ESP32 的內部 RTC,我們就不再需要 WiFi 了。因此,我們斷開 WiFi 連線,並在迴圈中每秒列印一次時間。你可以在序列埠監視器上看到時間每列印一次就會增加一秒。這是因為 ESP32 的內部 RTC 在獲得參考時間後會維護時間。
void setup()
{
Serial.begin(115200);
//connect to WiFi
Serial.printf("Connecting to %s ", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" CONNECTED");
//init and get the time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
printLocalTime();
//disconnect WiFi as it's no longer needed
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
void loop() {
delay(1000);
printLocalTime();
}
序列埠監視器輸出將如下所示:
就是這樣。你已經學習瞭如何從 NTP 伺服器獲取正確的時間並配置 ESP32 的內部 RTC。現在,在傳送到伺服器的任何資料包中,你都可以新增時間戳。
參考文獻
執行 ESP32 韌體的空中升級 (OTA)
假設你在現場有一千臺物聯網裝置。現在,如果有一天你發現生產程式碼中存在錯誤,並希望修復它,你會召回所有一千臺裝置並在其中刷入新的韌體嗎?可能不會!你更希望有一種方法可以遠端更新所有裝置,即進行空中升級。OTA 更新如今非常常見。你不時會收到 Android 或 iOS 智慧手機的軟體更新。就像軟體更新可以遠端進行一樣,韌體更新也可以。在本章中,我們將瞭解如何遠端更新 ESP32 的韌體。
OTA 更新過程
這個過程非常簡單。裝置首先分塊下載新韌體並將其儲存在記憶體的另一個區域中。讓我們將這個區域稱為“OTA 空間”。讓我們將儲存當前程式碼或應用程式程式碼的記憶體區域稱為“應用程式空間”。一旦整個韌體下載完畢並經過驗證,裝置引導載入程式就會啟動。將引導載入程式視為寫在記憶體另一個區域(讓我們將其稱為“引導載入程式空間”)中的程式碼,其唯一目的是在每次裝置重啟時載入應用程式空間中的正確程式碼。
因此,每次裝置重啟時,引導載入程式空間中的程式碼都會首先執行。大多數情況下,它只是將控制權傳遞給應用程式空間中的程式碼。但是,在下載較新的韌體後,當裝置重啟時,引導載入程式會注意到有較新的應用程式程式碼可用。因此,它會將較新的程式碼從 OTA 空間刷入應用程式空間,然後將控制權交給應用程式空間中的程式碼。結果將是裝置韌體已升級。
現在,稍微偏離一下,如果應用程式程式碼已損壞或傳送了恢復出廠設定命令,引導載入程式也可以將恢復出廠設定程式碼從“恢復出廠設定空間”刷入應用程式空間。此外,如果微控制器空間不足,OTA 程式碼和恢復出廠設定程式碼通常儲存在外部儲存裝置上,例如 SD 卡或外部 EEPROM 或 FLASH 晶片。但是,在 ESP32 的情況下,OTA 程式碼可以儲存在微控制器的記憶體中本身。
程式碼演練
我們將使用一個示例程式碼來講解本章。你可以在 檔案 -> 示例 -> 更新 -> AWS_S3_OTA_Update 中找到它。它也可以在 GitHub 上找到。
這是 Arduino 上 ESP32 的一個非常詳細的示例之一。此草圖的作者甚至在註釋中提供了草圖的預期序列埠監視器輸出。因此,雖然大部分程式碼透過註釋可以自解釋,但我們將介紹大致思路並涵蓋重要細節。此程式碼使用了 `Update` 庫,就像許多其他庫一樣,它使 ESP32 的操作非常容易,同時將繁重的工作隱藏在幕後。
在這個例子中,作者將新的韌體二進位制檔案儲存在 AWS S3 儲存桶中。詳細介紹 AWS S3 超出了本章的範圍,但簡單來說,S3(Simple Storage Service)是亞馬遜網路服務 (AWS) 提供的雲端儲存服務。可以把它想象成 Google Drive。你將檔案上傳到你的網盤,然後與他人分享連結。類似地,你可以將檔案上傳到 S3 並透過連結訪問它。S3 更受歡迎,因為許多其他 AWS 服務可以與之無縫整合。開始使用 AWS S3 非常簡單。你可以透過簡單的 Google 搜尋找到許多可用的資源來幫助你。在程式碼開頭處的註釋中,也提到了幾個入門步驟。
需要注意的一個重要建議是,你應該使用你自己的二進位制檔案。程式碼頂部的註釋建議你可以使用作者使用的相同二進位制檔案。但是,下載在另一臺機器/另一個版本的 Arduino IDE 上編譯的二進位制檔案有時會在 OTA 過程中導致錯誤。此外,使用你自己的二進位制檔案會使你的學習更加“完整”。你可以透過轉到“草圖” -> “匯出已編譯的二進位制檔案”來匯出任何 ESP32 草圖的二進位制檔案。二進位制檔案 (.bin) 將儲存到你儲存 Arduino (.ino) 檔案的同一資料夾中。
儲存二進位制檔案後,你只需要將其上傳到 S3,並將儲存桶連結和二進位制檔案的地址新增到你的程式碼中。你儲存的二進位制檔案應該包含一些列印語句,以表明它與你寫入 ESP32 的程式碼不同。例如,“Hello from S3”之類的語句。另外,不要直接在程式碼中保留 S3 儲存桶連結和二進位制檔案地址。
好了!廢話少說!現在開始逐步講解。我們將首先包含 WiFi 和 Update 庫。
#include <WiFi.h> #include <Update.h>
接下來,我們定義一些變數、常量以及 WiFiClient 物件。請記住新增你自己的 WiFi 憑據和 S3 憑據。
WiFiClient client; // Variables to validate // response from S3 long contentLength = 0; bool isValidContentType = false; // Your SSID and PSWD that the chip needs // to connect to const char* SSID = "YOUR−SSID"; const char* PSWD = "YOUR−SSID−PSWD"; // S3 Bucket Config String host = "bucket−name.s3.ap−south−1.amazonaws.com"; // Host => bucket−name.s3.region.amazonaws.com int port = 80; // Non https. For HTTPS 443. As of today, HTTPS doesn't work. String bin = "/sketch−name.ino.bin"; // bin file name with a slash in front.
接下來,定義了一個輔助函式 getHeaderValue(),它主要用於檢查特定標頭的值。例如,如果我們得到標頭“Content-Length: 40”,並且它儲存在一個名為 headers 的字串中,getHeaderValue(headers,“Content−Length: ”) 將返回 40。
// Utility to extract header value from headers
String getHeaderValue(String header, String headerName) {
return header.substring(strlen(headerName.c_str()));
}
接下來是主函式 execOTA(),它執行 OTA。此函式包含與 OTA 相關的全部邏輯。如果你檢視 Setup 部分,我們只是連線到 WiFi 並呼叫 execOTA() 函式。
void setup() {
//Begin Serial
Serial.begin(115200);
delay(10);
Serial.println("Connecting to " + String(SSID));
// Connect to provided SSID and PSWD
WiFi.begin(SSID, PSWD);
// Wait for connection to establish
while (WiFi.status() != WL_CONNECTED) {
Serial.print("."); // Keep the serial monitor lit!
delay(500);
}
// Connection Succeed
Serial.println("");
Serial.println("Connected to " + String(SSID));
// Execute OTA Update
execOTA();
}
所以你應該理解,理解 execOTA 函式就意味著理解整個程式碼。因此,讓我們開始逐步講解該函式。
我們首先連線到我們的主機,在本例中是 S3 儲存桶。連線後,我們使用 GET 請求從儲存桶中獲取 bin 檔案(有關 GET 請求的更多資訊,請參閱 HTTP 教程)。
void execOTA() {
Serial.println("Connecting to: " + String(host));
// Connect to S3
if (client.connect(host.c_str(), port)) {
// Connection Succeed.
// Fecthing the bin
Serial.println("Fetching Bin: " + String(bin));
// Get the contents of the bin file
client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Cache-Control: no-cache\r\n" +
"Connection: close\r\n\r\n");
接下來,我們等待客戶端連線。我們最多給連線建立 5 秒的時間,否則我們將表示連線超時並返回。
unsigned long timeout = millis();
while (client.available() == 0) {
if (millis() - timeout > 5000) {
Serial.println("Client Timeout !");
client.stop();
return;
}
}
假設程式碼沒有在上一步返回,我們就建立了成功的連線。伺服器的預期響應在註釋中提供。我們首先解析該響應。逐行讀取響應,每行都儲存在一個名為 line 的變數中。我們特別檢查以下三件事:
響應狀態碼是否為 200 (OK)
Content-Length 是多少
內容型別是否為 application/octet-stream(這是二進位制檔案的預期型別)
第一個和第三個是必需的,第二個只是為了提供資訊。
while (client.available()) {
// read line till /n
String line = client.readStringUntil('\n');
// remove space, to check if the line is end of headers
line.trim();
// if the the line is empty,
// this is end of headers
// break the while and feed the
// remaining `client` to the
// Update.writeStream();
if (!line.length()) {
//headers ended
break; // and get the OTA started
}
// Check if the HTTP Response is 200
// else break and Exit Update
if (line.startsWith("HTTP/1.1")) {
if (line.indexOf("200") < 0) {
Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
break;
}
}
// extract headers here
// Start with content length
if (line.startsWith("Content-Length: ")) {
contentLength = atol((getHeaderValue(line, "Content-Length: ")).c_str());
Serial.println("Got " + String(contentLength) + " bytes from server");
}
// Next, the content type
if (line.startsWith("Content-Type: ")) {
String contentType = getHeaderValue(line, "Content-Type: ");
Serial.println("Got " + contentType + " payload.");
if (contentType == "application/octet-stream") {
isValidContentType = true;
}
}
}
這樣,檢查與伺服器連線是否成功的 if 塊就結束了。後面跟著 else 塊,它只是列印我們無法建立與伺服器的連線。
} else {
// Connect to S3 failed
// May be try?
// Probably a choppy network?
Serial.println("Connection to " + String(host) + " failed. Please check your setup");
// retry??
// execOTA();
}
接下來,如果我們希望從伺服器收到正確的響應,我們將得到一個正的 contentLength(記住,我們在頂部將其初始化為 0,因此如果我們 somehow 沒有到達解析 Content−Length 標頭的行,它仍然為 0)。此外,我們將有 isValidContentType 為 true(記住,我們將其初始化為 false)。因此,我們檢查這兩個條件是否都為真,如果是,則繼續進行實際的 OTA。請注意,到目前為止,我們只使用了 WiFi 庫來與伺服器互動。現在,如果伺服器互動結果正常,我們將開始使用 Update 庫,否則,我們只打印伺服器響應中沒有內容,並重新整理客戶端。如果響應確實正確,我們首先檢查記憶體中是否有足夠的空閒空間來儲存 OTA 檔案。預設情況下,大約保留 1.2 MB 的空間用於 OTA 檔案。因此,如果 contentLength 超過該值,Update.begin() 將返回 false。這個 1.2MB 的數字可能會根據你的 ESP32 分割槽而改變。
// check contentLength and content type
if (contentLength && isValidContentType) {
// Check if there is enough to OTA Update
bool canBegin = Update.begin(contentLength);
現在,如果我們確實有空間在記憶體中儲存 OTA 檔案,我們將使用 Update.writeStream() 函式將位元組寫入為 OTA 保留的記憶體區域(OTA 空間)。如果沒有,我們只打印該訊息,重新整理客戶端並退出 OTA 過程。Update.writeStream() 函式返回寫入 OTA 空間的位元組數。然後我們檢查寫入的位元組數是否等於 contentLength。如果 Update 完成,在這種情況下 Update.end() 函式將返回 true,我們將使用 Update.isFinished() 函式檢查它是否已正確完成,即所有位元組都已寫入。如果它返回 true,表示所有位元組都已寫入,我們將重新啟動 ESP32,以便引導載入程式可以將新程式碼從 OTA 空間快閃記憶體到應用程式空間,並且我們的韌體將被升級。如果它返回 false,我們將列印收到的錯誤。
// If yes, begin
if (canBegin) {
Serial.println("Begin OTA. This may take 2 − 5 mins to complete. Things might be quite for a while.. Patience!");
// No activity would appear on the Serial monitor
// So be patient. This may take 2 - 5mins to complete
size_t written = Update.writeStream(client);
if (written == contentLength) {
Serial.println("Written : " + String(written) + " successfully");
} else {
Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" );
// retry??
// execOTA();
}
if (Update.end()) {
Serial.println("OTA done!");
if (Update.isFinished()) {
Serial.println("Update successfully completed. Rebooting.");
ESP.restart();
} else {
Serial.println("Update not finished? Something went wrong!");
}
} else {
Serial.println("Error Occurred. Error #: " + String(Update.getError()));
}
} else {
// not enough space to begin OTA
// Understand the partitions and
// space availability
Serial.println("Not enough space to begin OTA");
client.flush();
}
}
當然,你現在應該已經意識到我們不需要在這裡做任何迴圈操作。
就是這樣。你已經成功地遠端升級了 ESP32 晶片的韌體。如果你對 Update 庫的每個函式的作用更感興趣,可以參考 Update.h 檔案中的註釋。
參考文獻
ESP32的應用
既然你已經相當熟悉 ESP32 了,讓我們來看看它的應用。對於這一章,我覺得我不需要多說。在學習了本教程中的各個章節之後,你應該已經在腦海中形成了各種想法。你可能已經建立了一個粗略的應用列表,說明你可以在哪些地方使用 ESP32。好訊息是,你列出的許多應用都是可行的。
但是,對於某些應用來說,ESP32 比其他應用更可行。在本章中,我的重點是讓你理解在決定是否為某個應用使用 ESP32 時應考慮的因素。請注意,本章的重點是生產環境,即當我們談論的是數千甚至數百萬臺裝置的規模時。如果你只需要少量裝置並且 ESP32 可以滿足需求,那麼可以直接使用 ESP32,無需猶豫。此外,對於原型設計/概念驗證 (PoC),你可以毫無顧慮地使用 ESP32。
ESP32 的主要優勢之一是內建的 WiFi 和藍牙協議棧以及硬體。因此,在 WiFi 連線良好的靜態應用中,例如實驗室中的環境監控應用,ESP32 將是你的首選微控制器。模組本身的 WiFi 協議棧意味著你將節省額外的網路模組的成本。但是,如果你在資產跟蹤應用中使用 ESP32,它會不斷移動,則必須依賴 GSM 或 LTE 模組來連線到伺服器(因為你無法保證 WiFi 的可用性)。在這種情況下,ESP32 會失去競爭優勢,你最好使用更便宜的微控制器來滿足你的需求。
同樣,擁有用於加密訊息的硬體加速器使 ESP32 成為需要安全通訊 (HTTPS) 的應用的理想選擇。因此,如果你正在處理敏感資訊,不想讓它落入壞人之手,那麼使用 ESP32 比使用不支援加密的其他微控制器更有優勢。一個示例應用可以是國防領域的工業物聯網。
兩個核心的存在再次使 ESP32 成為處理密集型應用的首選微控制器,例如那些以非常高的波特率接收資料並需要在單獨的核心上執行資料處理和傳輸的應用。在工業物聯網中可以找到許多這樣的應用。但是對於一個非常輕量的應用,你甚至不需要安全通訊,一個具有適中規格的微控制器可能會更有用。畢竟,如果你只需要一個核心,為什麼要擁有(併為此付費)兩個核心呢?
另一個需要考慮的因素是 GPIO 和外設的數量。ESP32 有 3 個 UART 通道。如果你的應用需要超過 3 個 UART 通道,你可能需要尋找另一個微控制器。同樣,ESP32 有 34 個可程式設計 GPIO,對於大多數應用來說已經足夠了。但是,如果你的應用確實需要更多 GPIO,你可能需要切換到另一個微控制器。
ESP32 的 1.5 MB 預設 SPIFFS 提供的板載儲存空間比大多數其他微控制器都多。如果你的儲存需求在 1.5 MB 以內,ESP32 可以為你節省外部 SD 卡或快閃記憶體晶片的成本。ESP32 本身會在 SPIFFS 內進行磨損均衡,也為你節省了很多開發工作。但是,如果 ESP32 無法滿足你的儲存需求,那麼它的競爭優勢就會消失。
ESP32 的 520 KB RAM 也足以滿足大多數應用的需求。只有對於影像/影片處理等非常繁重的應用,這才會成為瓶頸。
總而言之,ESP32 的規格足以滿足你的大多數應用需求。在擴大生產規模時,你只需要確保這些規格不會對你來說過於高效能。換句話說,如果你可以使用適中的規格獲得所需的輸出,那麼最好使用更便宜的微控制器來節省成本。當你的產量成倍增長時,這些節省將變得非常顯著。但是,除了生產之外,ESP32 絕對是原型設計和建立 PoC 的理想微控制器。
開發人員的下一步
如果你堅持到了現在,恭喜你!你已經達到了一個階段,你不僅熟悉 ESP32,而且還具備足夠的知識來進一步探索它。事實上,還有很多東西值得探索。如果你瞭解一些額外的概念,就可以實現許多很酷的應用。在本章中,我只會為你提供探索的方向。事實上,如果我只是列出來,會更好。下面是你可以進一步探索的主題/領域以及可以學習的概念的非詳盡列表。
韌體
休眠模式 - 在電源匱乏的應用中,你需要了解這些模式
定時器 - 用於計劃任務
中斷 - 用於由非同步事件觸發的任務
看門狗超時 - 如果你的微控制器長時間卡在某個地方,請重置它
互斥鎖和訊號量(與 RTOS 相關) - 當多個執行緒想要訪問共享資源時
調整 ESP32 的分割槽以提供更多空間給 SPIFFS
感測器介面
使用 ESP32 的觸控感測器
使用 ESP32 的霍爾感測器
使用 ESP32 的 GNSS 接收器(幾乎所有物聯網裝置都使用 GNSS 接收器來獲取位置資訊)
網路相關
使用 ESP32 的 BLE(低功耗藍牙)
將 ESP32 連線到外部藍牙模組
使用ESP32的接入點(AP) WiFi
ESP32作為Web伺服器
使用ESP32的登入頁面(在餐廳或機場,連線WiFi後彈出的要求輸入手機號碼的頁面,這就是登入頁面)
使用UDP進行資料傳輸
使用TCP進行資料傳輸
DNS伺服器
透過LoRa進行資料傳輸
與AWS/Azure上的代理伺服器的MQTT連線
外設連線
CAN協議(用於汽車應用)
具有多個從機的I2C和SPI
ESP32的SD卡介面
單線協議
資料處理
ESP32上的FFT
功率計算(RMS值、峰峰值、功率因數等)
不要被以上列表嚇到。您不必一天之內學習所有內容,也不需要學習所有內容。如開頭所述,以上列表只是為您提供進一步探索的方向。您可以選擇適合您應用的主題。對於所有您有示例程式碼的主題,沒有什麼比從示例程式碼開始更好的了。對於其他主題,您可以閱讀與這些主題相關的可用庫的文件,檢視它們的.h檔案,並在網際網路上搜索示例。在如此龐大的線上社群中,很難找不到線上幫助。因此,繼續學習,繼續探索。