2021年9月15日 星期三

ESP32 CAMRA 二維碼(QR Code)辨識之門鎖控制 03-FreeRTOS WiFi Watchdog 監控

網頁最後修改時間:2021/09/15

ESP32 內嵌 FreeRTOS 操作系統且 ESP32-CAM 主晶片是具有雙核心的 ESP32 晶片,對於 ESP32-CAM 二維碼辨識之門鎖控制(下面簡稱 ESP32QRDoorLock)這個應用來說,需要同時監控 WiFi 連線狀態、處理二維碼辨識和管理門鎖操作流程三個工作任務。其中,WiFi 連線狀態還牽扯到 OTA 韌體上傳是否能用的責任,它由 Watchdog 計時器來監控,一旦出現連線問題時,會在設定的時間重置 ESP32,避免系統一直停留在嘗試連線的循環之中。

本篇是該主題系列的第三篇,說明如何建立 FreerRTOS 任務,以及在任務裡加入 Watchdog 計時器,內容有:


【(00)簡介】

這一篇沒討論到二維碼辨識和管理門鎖操作流程這兩個部分,因為它們都是 ESP32QRDoorLock FreeRTOS 其中的一個任務,除了程式碼之外,三者之間任務的建立沒有太大的差異,因為其重要性的關係,所以不一併在這一篇說明。

三者中,只有 WiFi 這一部分有用到 Watchdog 計時器,其餘兩者沒有。

ESP32-CAM 是擁有雙核心晶片的開發板,也就是說建立每個任務的時候,除了可自動指定之外也能手動指定,並可設定優先執行的順序。所以本篇的重點就在於:Watchdog 計時器的建立與用法,和 FreeRTOS 工作任務的創建。

那麼,就開始進入主題!


【(01)Watchdog(看門狗)計時器的建立與用法】

Watchdog 計時器相關的函式定義在下面標頭檔中,程式碼的開頭處要把它引進來。

#include <esp_task_wdt.h>

使用前,要在 setup () 初始化任務看門狗計時器(Task Watchdog Timer, TWDT),函式原型如下:

esp_err_t esp_task_wdt_init(uint32_t timeout, bool panic)

其中,

  • 參數
    • timeout:TWDT 時間逾時到計時值(秒);
    • panic:TWDT 時間逾時時是否執行緊急處理程序的控制旗標;
  • 回傳
    • ESP_OK:初始化成功;
    • ESP_ERR_NO_MEM:由於記憶體不足導致的初始化失敗;

初始化完成之後,就必須將需要的任務(或程序)加到 TWDT 中,這個動作的函示原型如下:

esp_err_t esp_task_wdt_add(TaskHandle_t handle)

其中,

  • 參數
    • handle:工作任務的控制代碼。若輸入是 NULL,則代表將當前任務(或程序)加到  TWDT 中;
  • 回傳
    • ESP_OK:成功加入工作任務到 TWDT 中;
    • ESP_ERR_INVALID_ARG:工作任務已加入的錯誤;
    • ESP_ERR_NO_MEM:由於記憶體不足導致的初始化失敗的錯誤;
    • ESP_ERR_INVALID_STATE:TWDT 未初始化的錯誤;

最後,就是在每個工作任務中重置 Watchdog 計時器,避免逾時。

函式原型如下:

esp_err_t esp_task_wdt_reset(void)

其中,

  • 回傳
    • ESP_OK:代表在當前運行的任務成功重置 TWDT;
    • ESP_ERR_NOT_FOUND:當前運行的任務在 TWDT 找不到;
    • ESP_ERR_INVALID_STATE:TWDT 未初始化的錯誤;

ESP32QRDoorLock 只用到這三個 Watchdog 計時器的函式,更多的資訊請參閱下面網址的說明。

** (Watchdogs - ESP32 - — ESP-IDF Programming Guide latest documentation (espressif.com)


【(02)FreeRTOS 工作任務的建立】

FreeRTOS 工作任務的建立有幾個函式可用,這裡介紹 xTaskCreate (...)xTaskCreatePinnedToCore (...) 這兩個函式。

這兩個函式幾乎一模一樣,差別在 xTaskCreatePinnedToCore (...) 最後一個參數可用來指定該工作任務要在哪一核心執行,而 xTaskCreate (...) 則是由 FreeRTOS 來選擇。

下面來看看這兩個函式原型。

static BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char *const pcName,
const uint32_t usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pvCreatedTask
)
  • 參數
    • pvTaskCode:本工作任務實際對應的函式。函式是不需要回傳的,亦可使用 vTaskDelete 函式來做中斷。
      函式的宣告請看範例。
    • pcName:該工作任務的文字描述。主要是用在除錯時,可由輸出訊息分辨出是哪一個工作任務所出的訊息。該文字描述預設長度為 16,由 configMAX_TASK_NAME_LEN 所定義;
    • usStackDepth:工作任務的堆疊大小,單位為 bytes;
      注意,這與 vanilla FreeRTOS 不同。
    • pvParameters:工作任務的參數輸入;
    • uxPriority:指定工作任務的優先級別,數字越高則優先級越高;
    • pvCreatedTask:對應的工作任務的控制代碼(handle);
  • 回傳
    • pdPASS:工作任務被成功建立,否則回傳錯誤碼;
      錯誤碼定義在 projdefs.h 中。
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char *const pcName,
const uint32_t usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pvCreatedTask,
const BaseType_t xCoreID
)
  • 參數
    • 其他參數同 xTaskCreate;
    • xCoreID:指定要執行此工作任務的核心號碼;
  • 回傳
    • 同 xTaskCreate;

當需要結束當前的工作任務時,使用下面函式來做結束。

void vTaskDelete(TaskHandle_t xTaskToDelete)
  • 參數
    • xTaskToDelete:要刪除的工作任務的控制代碼。
      若此處傳入 NULL,則表示要刪除就是當前的工作任務。


關於其他工作任務建立的函式,請自行參閱下面連結中的說明。

** FreeRTOS - ESP32 - — ESP-IDF Programming Guide latest documentation (espressif.com)


/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
(02-01)FeeRTOS 工作任務範例

建立工作任務之前,首先就是要撰寫一個適合作為工作任務的函式,應該也可叫做 callback 函式。但不同的是,它可被視為一個獨立的 loop () 運行函式,可以單次結束也可以跑無窮迴圈。

撰寫的格式如下所示:

void task1 (void* pvParameters) {
    // Code here...
}

task1 是名稱、pvParametersvoid* 變數名稱,兩者都可以自行變更;

下面的範例建立了兩個工作任務所需要的函式:task1 task2

task1 執行一個無限循環的 LED0 亮滅工作任務。函式裡指定的 GPIO 號碼,剛好就是 ESP32-CAM 的 Flash 燈,低態觸發動作。工作任務執行在 Core 0,優先級別設定為 0。

task2 每隔一秒鐘輸出數字一次,由 0 ~ 9 共 10 次,然後結束該工作任務。

#include <Arduino.h>

#define LED0 4

TaskHandle_t xTask1Handle, xTask2Handle;

void task1 (void* pvParameters) {
    while(1) {
        digitalWrite (LED0, HIGH);
        delay (100);
        digitalWrite (LED0, LOW);
        delay (1000);
    }
}

void task2 (void* pvParameters) {
    for (int i = 0; i < 10; i++) {
        Serial.println (i);
        delay(1000);
    }
    Serial.println ("Ending task2");
    vTaskDelete (NULL);
}

void setup () {
    delay (2000);
    Serial.begin (115200);
    pinMode (LED0, OUTPUT);
    digitalWrite (LED0, HIGH);

    xTaskCreatePinnedToCore (
        task1,          // 本工作任務所對應的函式名稱
        "Task1",        // 工作任務名稱(自行設定)
        10000,          // 所需要的堆疊空間大小 (常用 10000)
        NULL,           // 輸入參數值
        0,              // 0, 優先級別
        &xTask1Handle,  // 對應的工作任務的控制代碼
        0               // 0, 指定工作任務所運行的核心號碼
    );

    xTaskCreatePinnedToCore (
        task2,          // 本工作任務所對應的函式名稱
        "Task2",        // 工作任務名稱(自行設定)
        10000,          // 所需要的堆疊空間大小 (常用 10000)
        NULL,           // 輸入參數值
        1,              // 1, 優先級別
        &xTask2Handle,  // 對應的工作任務的控制代碼
        1               // 1, 指定工作任務所運行的核心號碼
    );
}

void loop () {}

編譯、上傳到 ESP32-CAM 開發板後,底板電源開關一次,開啟串口調試助手並完成通訊,就可以看到 Flash 燈一直重複閃一下就關閉,並在視窗裡輸出 0 ~ 9 然後結束 task2。


【(03)參考程式碼】

簡單說明 FreeRTOS 工作任務的建立和 Watchdog 計時器後,現在可以開始撰寫 x03_ESP32CamWiFiWatchdog.ino 的程式碼了。

x03_ESP32CamWiFiWatchdog.ino 是以 x02_ESP32CamPeripheral.ino 為基礎,移除掉所有 x02 開頭的變數和函式,再加入 WiFi 監控部分的程式碼而寫成,些許原本的程式碼也會因為加入 Watchdog 計時器做細部的改變。


/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* (03-01)監控 WiFi 連線狀態

在檔案開頭處,加入 Watchdog 計時器相關函式需要的標頭檔。

16
#include <esp_task_wdt.h>

WIFI 區域下面,插入 Watchdog 計時器的逾時定義。為了不浪費時間等待,這裡設定為 10(秒)。

37
38
/*================== WATCHDOG =================*/
#define WDT_TIMEOUT     10      // 設定 Watchdog 計時器的時間

lsInterrupt 區域上面,宣告幾個 WiFi 工作任務會用到的全局變數。有些在現在這個程式裡用不到,但後面會用到,所以先加上。

57
58
59
60
61
62
63
64
65
66
67
68
/*================== TASK: WIFIRECONNECT =================*/
enum wifi_reconnect_state {
    UNKNOWN,
    RUNNING,
    CONNECTING,
    CONNECTED,
    DISCONNECTED,
} xWifiReconnectState = UNKNOWN;

bool OTA_status = false;

TaskHandle_t xWiFiReConnectTaskHandle;

因為加入 Watchdog 計時器的關係,setupOTA () 要插入幾行程式碼做些細部修改,不然上傳時會出現錯誤。

.onStart 區塊的最後一行處,插入

259
OTA_status = true;

.onProgress 區塊的最後一行處,插入

266
267
esp_task_wdt_reset ();
OTA_status = true;

.onError 區塊的最後一行處,插入

276
277
OTA_status = false;
esp_restart ();

新增一個工作任務用的函式 wifiReconnectTask

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/**========================================================================
 **                           wifiReconnectTask()
 *?  監測無線連線狀態的任務。若出現問題會造成 WatchDog 重置系統重新連線。
 *@param pvParameter void*  
 *========================================================================**/
void wifiReconnectTask (void *pvParameters) {
    int  wifiStatus = 0;
    int  wifiPrev   = WL_CONNECTED;
    int n = 0;
    unsigned long previousMillis = 0;
    const long interval = 1000;
    esp_task_wdt_add(NULL);         //  添加當前任務到看門狗計時器
    for(;;) {
        wifiStatus = WiFi.status();
        
        #ifdef ENABLE_OTA
            ArduinoOTA.handle();
        #endif
        
        if (OTA_status)             // 如果有 OTA,重置 Watchdog 計時器
            esp_task_wdt_reset();

        /** 確認 WiFi 狀態是否已經改變? */
        if ((wifiStatus == WL_CONNECTED)&&(wifiPrev != WL_CONNECTED)) {
            xWifiReconnectState = CONNECTED;
            esp_task_wdt_reset();   // 重置 Watchdog 計時器
            Serial.printf ("\nConnected to WiFi!\nLocal IP address: %s\n\n", 
                            (WiFi.localIP ().toString ()).c_str ());
        }
        else if ((wifiStatus != WL_CONNECTED)&&(wifiPrev == WL_CONNECTED)) {
            xWifiReconnectState = DISCONNECTED;
            esp_task_wdt_reset();   // 重置 Watchdog 計時器
            Serial.println(F("\nWiFi disconnected!"));
            WiFi.disconnect();
            Serial.println(F("Scanning for WiFi networks"));
            n = WiFi.scanNetworks(false, true);
            Serial.println(F("Scan done"));
            if (n == 0) {
                Serial.println(F("No networks found. Resetting ESP32."));
                esp_restart();
            } else {
                Serial.print(n);
                Serial.println(F(" networks found"));
                for (int i = 0; i < n; ++i) {
                    /** 列出每一個找到的網路的 SSID 和 RSSI */
                    Serial.print(i + 1);        Serial.print(F(": "));
                    Serial.print(WiFi.SSID(i)); Serial.print(F(" ("));
                    Serial.print(WiFi.RSSI(i)); Serial.print(F(")"));
                    Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*");
                    vTaskDelay (pdMS_TO_TICKS(10));
                }
            }
            esp_task_wdt_reset();   // 重置 Watchdog 計時器
            Serial.println();
            Serial.println(F("Please, check if your WiFi network is on the list and check if it's strong enough (greater than -90)."));
            Serial.println("ESP32 will reset itself after "+String(WDT_TIMEOUT)+" seconds if can't connect to the network");
            Serial.println("Connecting to: " + String(SSID));
            xWifiReconnectState = CONNECTING;
            WiFi.reconnect();
        }
        else if ((wifiStatus == WL_CONNECTED)&&(wifiPrev == WL_CONNECTED)) {
            xWifiReconnectState = RUNNING;
            esp_task_wdt_reset();   // 重置 Watchdog 計時器
            vTaskDelay (pdMS_TO_TICKS(1000));
        }
        else {
            /** 不要重置 Watchdoh 計時器,如果計時器到達設定的時間就會重置 ESP32 */
            unsigned long currentMillis = millis();
            if (currentMillis - previousMillis >= interval) {
                static int i = 0;
                previousMillis = currentMillis;
                Serial.print (String(++i) + " ");
                
            }
        }
        wifiPrev = wifiStatus;
    }
}

Line 226 ~ 235 裡面的程式會每秒計時並輸出,到達 10 時就會重置 ESP32。這裡的程式有做過修改,實際應用上 Line 230 和 232 都不需要。


/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* (03-02)完整程式碼

x03_ESP32CamWiFiWatchdog.ino 的 loop () 裡不需要任何程式碼,setup () 裡要把 WiFi.begin 下面等待連線的那 5 行移除掉,最後加入 Watchdog 計時器初始化和建立工作任務的程式碼。

如此就完整整個程式的撰寫。

整個撰寫好的程式功能很簡單,如果在設定的 Watchdog 計時器逾時時間內沒有與無線分享器連線成功,那麼系統就會因為 Watchdog 計時器逾時重置。

編譯、上傳成功後,完整的輸出就如下圖所示。

WDT_TIMEOUT 所設定的時間,是為了更快的看到 ESP32-CAM 因為 Watchdog 計時器逾時而重置,實際使用上至少要設 30 或是 60 以上,不然若是需要較長連線時間的話,還沒連上就會被重置了,使用時這點要特別注意一下!


【(04)結論】

在這一篇,沒有很深入的討論 FreeRTOS 和 Watchdog 計時器,但對於 ESP32QRDoorLock 來說已經夠用了。

接下來利用這篇學到的知識,下一篇來看看 ESP32-CAM 的二維碼辨識。


.

.


<<部落格相關文章>>


.

.

沒有留言:

張貼留言

留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !