【MakerPRO】ESP32專欄 善用ESP32雙核心,平行處理提昇效能

ESP32是一款具有WiFi及低功率藍牙BLE的晶片,相容於Arduino架構,可使用Arduino IDE來開發,不僅程式與Arduino UNO幾乎相同,感測器也完全相容,因此非常適合用來取代現有國、高中UNO的教材。

作者尤濬哲(夜市小霸王),本文經MakerPRO同意轉載,原文連結
ESP32腳位定義圖

ESP32雖然擁有兩個核心,不過以平常Arduino的程式寫法都只會用到一個核心,這樣是不是很浪費?我們如何善用增加效能呢?或者如何使用平行處理呢?本篇文章將介紹ESP32的雙核心架構,並分為三個部份解說。

1. 多核心概念

我們常聽到說我的手機有4核心,電腦有8核心,所謂的多核心(Multi-core)簡單的說就是CPU能同時處理較多的任務且不會互相干擾;另外一種虛擬的多核心稱為多執行緒(Thread)則是CPU的分時多工,非本篇的討論範圍。由於ESP32有著240/160MHz的雙核心CPU,因此本篇將要說明是能將任務指定給核心執行的「xTaskCreatePinnedToCore」函數。

ESP32功能方塊圖中可看出它為雙核心架構(Source)

 

雙核心執行有什麼好處?例如可以增加效能,或者平行處理。舉例而言,我們有一個工作是要偵測現場溫濕度並上傳資料庫存檔,每20秒感測上傳一次,假定這項規定很嚴格,必須準時上傳不能有延遲,在以往程式的撰寫流程下,感測與網路上傳是寫在同一個程序內,也就是讀取溫濕度→上傳資料庫→等候20秒,但是上傳資料庫須看網路是否擁塞,假設上傳時間花了3秒,再加上等候20秒後,等於是23秒過了,長久下來就會延遲越來越多。

也許你會想說我們可以將等候時間改成17秒,這樣上傳時間3秒加起來剛好20秒,不過你也知道網路不是那麼好預估,有時候鄰居下載迷片,速度就慢了,而半夜時沒人速度又變得飛快,這樣我們總是沒辦法找到一個時間來補回。

而多核心在這裡就非常適用,也就是一個核心負責讀取溫濕度資料,另外一個核心則將資料送到資料庫,平行處理兩者互不相干,就可以固定總時間不變,而這就是雙核心CPU時的好處。

2. 顯示執行核心

一般我們的程式都僅在ESP32的核心1執行(核心編號1),所以根本沒用到第二個核心(核心編號0),為了證明,我們可以透過xPortGetCoreID()函數來顯示現在使用到哪一個核心,例如我們寫一個簡單的HelloWorld,來測試看看。

void setup() {
Serial.begin(115200);
}
 
void loop() {
Serial.println("HelloWorld!");
Serial.print("使用核心編號:");
Serial.println(xPortGetCoreID());//xPortGetCoreID()顯示執行核心編號
delay(1000);
}

上圖可以知道,在未特別指定執行核心前,程序都在核心1中執行,我們稱為單線程,而未使用到另外一個核心,我們拿ThingSpeak的一個範例來演練看看,順便了解執行的時間變化。

#include
#include
#include
//請修改以下參數--------------------------------------------
char ssid[] = "SSID";
char password[] = "SSIDpassword";
//請修改為你自己的API Key,並將https改為http
String url = "http://api.thingspeak.com/update?api_key=換成你的APIKey";
int pinDHT11 = 14;//假設DHT11接在腳位GPIO14,麵包板左側序號8
//---------------------------------------------------------
SimpleDHT11 dht11(pinDHT11);//宣告SimpleDHT11物件
 
void setup()
{
Serial.begin(115200);
Serial.print("開始連線到無線網路SSID:");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.println("連線完成");
}
 
void loop()
{
Serial.print("使用核心編號:");
Serial.println(xPortGetCoreID());
//嘗試讀取溫濕度內容
byte temperature = 0;
byte humidity = 0;
int err = SimpleDHTErrSuccess;
if ((err = dht11.read(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
Serial.print("溫度計讀取失敗,錯誤碼="); Serial.println(err); delay(1000);
return;
}
//讀取成功,將溫濕度顯示在序列視窗
Serial.print("溫度計讀取成功: ");
Serial.print((int)temperature); Serial.print(" *C, ");
Serial.print((int)humidity); Serial.println(" H");
//開始傳送到thingspeak
Serial.println("啟動網頁連線");
HTTPClient http;
//將溫度及濕度以http get參數方式補入網址後方
String url1 = url + "&field1=" + (int)temperature + "&field2=" + (int)humidity;
//http client取得網頁內容
http.begin(url1);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
//讀取網頁內容到payload
String payload = http.getString();
//將內容顯示出來
Serial.print("網頁內容=");
Serial.println(payload);
} else {
//讀取失敗
Serial.println("網路傳送失敗");
}
http.end();
delay(20000);//休息20秒
}

為了計算網路傳送的延遲時間,我們可以開啟顯示時間戳記Show timestamp來觀察每次的傳輸時間,我們比較上圖2與3之間的時間差異發現,經過五次的傳送,時間已經慢了約4秒鐘,同時我們觀察執行核心也都是在編號1上執行。

3. 雙核心執行

了解概念後,我們先來實做ESP32多核心的寫法,步驟是這樣的。

1. 宣告任務變數:TaskHandle_t Task1;//宣告Task1任務
2. 設定並執行任務://設定任務
xTaskCreatePinnedToCore(
Task1code, //本任務實際對應的Function
“Task1”, //任務名稱(自行設定)
10000, //所需堆疊空間(常用10000)
NULL, //輸入值
0, //優先序:0代表最優先執行,1次之,以此類推
&Task1, //對應的任務handle變數
0) //指定執行核心編號(0、1或tskNO_AFFINITY:系統指定)

為了測試,我們將原本ThinkSpeak的流程改為多核心架構,核心1執行主流程loop()監控DHT11的溫溼度,將數值存放在公用變數區,並設定上傳旗標=True,核心0執行Task1,主要工作是將資料上傳,當發現上傳旗標=True後就執行上傳,並在完成上傳後修改上傳旗標=False,等待下次任務啟動。

此時我們以上圖方式加入多核心架構後,程式如下:

#include
#include
#include
//請修改以下參數--------------------------------------------
char ssid[] = "SSID";
char password[] = "SSIDpassword";
//請修改為你自己的API Key,並將https改為http
String url = "http://api.thingspeak.com/update?api_key=換成你的APIKey";
int pinDHT11 = 14;//假設DHT11接在腳位GPIO14,麵包板左側序號8
//---------------------------------------------------------
SimpleDHT11 dht11(pinDHT11);//宣告SimpleDHT11物件
 
//公用變數區
byte temperature = 0;
byte humidity = 0;
bool SendFlag = false;
//宣告任務Task1
TaskHandle_t Task1;
 
//任務1副程式Task1_senddata
void Task1_senddata(void * pvParameters ) {
//無窮迴圈
for (;;) {
//偵測上傳旗標是否為true
if (SendFlag) {
Serial.print("Task1:啟動網頁連線,at core:");
Serial.println(xPortGetCoreID());
HTTPClient http;
//將溫度及濕度以http get參數方式補入網址後方
String url1 = url + "&field1=" + (int)temperature + "&field2=" + (int)humidity;
//http client取得網頁內容
http.begin(url1);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
//讀取網頁內容到payload
String payload = http.getString();
//將內容顯示出來
Serial.print("網頁內容=");
Serial.println(payload);
} else {
//傳送失敗
Serial.println("網路傳送失敗");
}
//修改完畢,修改傳送旗標=false
SendFlag = false;
http.end();
} else {
//Task1休息,delay(1)不可省略
delay(1);
}
}
}
 
void setup()
{
Serial.begin(115200);
Serial.print("開始連線到無線網路SSID:");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.println("連線完成");
//在核心0啟動任務1
xTaskCreatePinnedToCore(
Task1_senddata, /*任務實際對應的Function*/
"Task1", /*任務名稱*/
10000, /*堆疊空間*/
NULL, /*無輸入值*/
0, /*優先序0*/
&Task1, /*對應的任務變數位址*/
0); /*指定在核心0執行 */
}
 
void loop()
{
Serial.print("loop主流程:溫濕度讀取,at core:");
Serial.println(xPortGetCoreID());
//嘗試讀取溫濕度內容
int err = SimpleDHTErrSuccess;
if ((err = dht11.read(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
Serial.print("溫度計讀取失敗,錯誤碼="); Serial.println(err); delay(1000);
return;
}
//讀取成功,將溫濕度顯示在序列視窗
Serial.print("溫度計讀取成功: ");
Serial.print((int)temperature); Serial.print(" *C, ");
Serial.print((int)humidity); Serial.println(" H");
//修改上傳旗標=true
SendFlag = true;
delay(20000);//休息20秒
}

經由將程式修改為多核心執行後,可以發現,儘管網路會有延遲,但是每次開始傳送的時間都不會相差太多,多次執行後,仍能保持在0.1秒內的誤差。

讀者了解多核心的優點後,可以思考要如何應用在不同的議題上喔。

作者為本刊共筆作者,其專欄文章同步發表於作者部落格

*本文經MakerPRO同意轉載,原文連結,特此感謝

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *