執行 ESP32 韌體的空中升級



假設您在現場有 1000 臺物聯網裝置。現在,如果有一天,您在生產程式碼中發現了一個錯誤,並且希望修復它,您會召回所有 1000 臺裝置並在其中刷入新的韌體嗎?可能不會!您更希望有一種方法可以遠端更新所有裝置,透過無線方式。OTA 更新如今非常普遍。您時不時地會收到 Android 或 iOS 智慧手機的軟體更新。就像軟體更新可以遠端發生一樣,韌體更新也可以。在本章中,我們將瞭解如何遠端更新 ESP32 的韌體。

OTA 更新過程

這個過程非常簡單。裝置首先分塊下載新的韌體並將其儲存在記憶體的另一個區域。讓我們將此區域稱為“OTA 空間”。讓我們將儲存當前程式碼或應用程式程式碼的記憶體區域稱為“應用程式空間”。一旦整個韌體下載並驗證完成,裝置引導載入程式就會開始工作。將引導載入程式視為寫入記憶體單獨區域(讓我們將其稱為“引導載入程式空間”)的程式碼,其唯一目的是在每次裝置重啟時將正確的程式碼載入到應用程式空間中。

因此,每次裝置重啟時,引導載入程式空間中的程式碼都會首先執行。大多數情況下,它只是將控制權傳遞給應用程式空間中的程式碼。但是,在下載了較新的韌體後,當裝置重啟時,引導載入程式會注意到有較新的應用程式程式碼可用。因此,它會將該較新的程式碼從 OTA 空間快閃記憶體到應用程式空間,然後將控制權交給應用程式空間中的程式碼。結果將是裝置韌體將被升級。

現在,稍微偏離一下,如果應用程式程式碼已損壞或傳送了恢復出廠設定命令,引導載入程式還可以將“恢復出廠設定空間”中的恢復出廠設定程式碼快閃記憶體到應用程式空間。此外,通常,OTA 程式碼和恢復出廠設定程式碼儲存在外部儲存裝置上,例如 SD 卡或外部 EEPROM 或 FLASH 晶片,如果微控制器沒有足夠的儲存空間。但是,在 ESP32 的情況下,OTA 程式碼可以儲存在微控制器的記憶體本身中。

程式碼演練

我們將使用本章中的示例程式碼。您可以在 File -> Examples -> Update -> AWS_S3_OTA_Update 中找到它。它也可以在 GitHub 上找到。

這是 Arduino 上可用的 ESP32 最詳細的示例之一。該草圖的作者甚至在註釋中提供了草圖的預期序列埠監視器輸出。因此,雖然大部分程式碼透過註釋不言而喻,但我們將回顧主要思想並涵蓋重要細節。此程式碼利用了 **Update** 庫,該庫與許多其他庫一樣,使 ESP32 的使用變得非常容易,同時將繁重的工作隱藏在後臺。

在此特定示例中,作者將新韌體的二進位制檔案儲存在 AWS S3 儲存桶中。詳細介紹 AWS S3 超出了本章的範圍,但從廣義上講,S3(簡單儲存服務)是 Amazon Web Services (AWS) 提供的雲端儲存服務。可以將其想象成 Google Drive。您將檔案上傳到您的雲端硬碟並與他人共享連結以共享它。類似地,您可以將檔案上傳到 S3 並透過連結訪問它。S3 更加流行,因為許多其他 AWS 服務可以與之無縫互動。AWS S3 的入門非常簡單。您可以透過快速 Google 搜尋獲得多個可用資源的幫助。草圖開頭處的註釋中也提到了開始使用的一些步驟。

需要注意的重要建議是,您應該為此程式碼使用自己的二進位制檔案。草圖頂部的註釋建議您可以使用作者使用的相同二進位制檔案。但是,下載在另一臺機器/另一個版本的 Arduino IDE 上編譯的二進位制檔案有時會導致 OTA 過程中出現錯誤。此外,使用您自己的二進位制檔案將使您的學習更加“完整”。您可以透過轉到 Sketch -> Export Compiled Binary 匯出任何 ESP32 草圖的二進位制檔案。二進位制檔案 (.bin) 將儲存在 Arduino (.ino) 檔案所在的同一資料夾中。

Saving binary

儲存二進位制檔案後,您只需將其上傳到 S3 並將儲存桶的連結和二進位制檔案的地址新增到程式碼中即可。您儲存的二進位制檔案應該包含一些列印語句以指示它與您在 ESP32 中刷寫的程式碼不同。例如,“Hello from S3”語句。此外,不要按原樣將 S3 儲存桶連結和 bin 地址保留在程式碼中。

好了!不要再說了!現在讓我們開始演練。我們將從包含 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 的變數中。我們特別檢查以下 3 件事 -

  • 如果響應狀態程式碼為 200(OK)

  • 內容長度是多少

  • 內容型別是否為 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,因此如果我們以某種方式沒有到達解析 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 檔案中的註釋。

參考

廣告

© . All rights reserved.