任务看门狗被触发 - 任务没有及时重置看门狗。

9
我正在尝试编写一个小的异步Web服务器。让我简要描述一下情况:
我的ESP32也是一个路由器。因此,如果我用我的手机连接到ESP32传播的WiFi,并使用浏览器调用IP地址和特殊路径,就会提供一个网站。这里显示一个按钮。到这一点为止,它运作得很好。现在,如果我点击该按钮,将发送一个HTTPS Web请求(方法:GET)到特殊机器。该机器回答并返回JSON。这可能需要几秒钟时间。从JSON字符串中提取一个值后,应该显示该值。
为了实现这一点,我正在使用以下库:

我知道(通过另一个草图),最后三个可以正常工作。

不幸的是,当我点击按钮时,以下输出会显示在我的串行监视器上:

开始连接服务器...
[HTTPS] 开始... 路径: https://192.168.4.101/api/unlock/generate_pin
[HTTPS] GET...
E (137906) task_wdt: 任务看门狗被触发。以下任务未能及时重置看门狗:
E (137906) task_wdt: - async_tcp (CPU 0/1)
E (137906) task_wdt: 当前正在运行的任务:
E (137906) task_wdt: CPU 0: IDLE0
E (137906) task_wdt: CPU 1: loopTask
E (137906) task_wdt: 中止。
PC 0x400e08af 上的 abort() 已被调用,核心 0

回溯:0x4008cc18:0x3ffbe170 0x4008ce49:0x3ffbe190 0x400e08af:0x3ffbe1b0 0x40084f21:0x3ffbe1d0 0x4016581b:0x3ffbc120 0x400e1c66:0x3ffbc140 0x4008ab21:0x3ffbc160 0x4008932d:0x3ffbc180

正在重新启动...
ets Jun 8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0018,len:4
load:0x3fff001c,len:1044
load:0x40078000,len:8896
load:0x40080400,len:5816
entry 0x400806ac
串行初始化完成

有人知道正在发生什么以及如何解决这个问题吗?使GET请求得到正确发送/接收答案?

我正在使用Heltec WiFi Kit 32

非常感谢每一个回答,提前致谢。

最好的问候

P.S .:请允许我最后添加我的代码:

#include <heltec.h>
#include "WiFi.h"
#include "ESPAsyncWebServer.h"

#include <WiFiClientSecure.h>
#include <HTTPClient.h>
 
const char* ssid = "MyWiFiSSID";
const char* password =  "MyWiFiPW";
 
AsyncWebServer server(80);

void setup() {

  Heltec.begin(true, false, true, true, 470E6);

  WiFi.softAP(ssid, password);
  
  IPAddress IP = WiFi.softAPIP();
  Serial.print("AccessPoint IP address: ");
  Serial.println(IP);
  
  server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
    
    request->send(200, "text/html", "<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" charset=\"UTF-8\"><link rel=\"icon\" href=\"data:,\"><style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}.button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}</style></head><body><h1>Welcome to the Landing Page of the Web Server</h1><p><a href=\"/get_unlock_pin\"><button class=\"button\">Click Me</button></a></p></body></html>");
  });

  server.on("/get_unlock_pin", HTTP_GET, [](AsyncWebServerRequest *request){

    String firstpartofrawhtmlcode = "<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" charset=\"UTF-8\"><link rel=\"icon\" href=\"data:,\"><style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}.button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}</style></head><body><h2>Received Pin: </h2><h2 style=\"color: #FF0000\">";
    String receivedPin = getPin("192.168.4.101");
    String secondpartofrawhtmlcode = "</h2></body></html>";
    String fullrawhtmlcode;
    firstpartofrawhtmlcode = firstpartofrawhtmlcode.concat(receivedPin);
    fullrawhtmlcode = firstpartofrawhtmlcode.concat(secondpartofrawhtmlcode);
    request->send(200, "text/html", fullrawhtmlcode);
  });
 
  server.begin();
}

void loop() {

}

String getPin(String ip){
    Serial.println("\nStarting connection to server...");  
    WiFiClientSecure *wificlient = new WiFiClientSecure;

    HTTPClient https;
    https.setAuthorization("MyUserName", "MyPassword");

    String path = "https://" + ip + "/api/unlock/generate_pin";
      
    Serial.print("[HTTPS] begin... Path: " + path + "\n");
    if (https.begin(*wificlient, path)) { 
        Serial.print("[HTTPS] GET...\n");
        int httpCode = https.GET();
        if (httpCode > 0) {
          Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
  
          if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
            String payload = https.getString();
            Serial.println(payload);
            //Extract Pin from JSON
            String tmp = payload.substring(payload.indexOf(':'), payload.indexOf('}'));
            String tmp2 = tmp.substring(tmp.indexOf('"')+1,tmp.lastIndexOf('"')); 
            if(tmp2.substring(0,1) == "-"){
               return "-";
            }else{
               return tmp2;
            }              
          }
        } else {
               Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
        }  
        https.end();
    } else {
        Serial.printf("[HTTPS] Unable to connect\n");
    }
}
3个回答

31

从问题的表述来看,我认为您不知道什么是看门狗以及为什么它会触发。因此:

针对ESP8266的Arduino是基于ESP-IDF构建的,而ESP-IDF本身则是围绕FreeRTOS构建的。FreeRTOS会为每个内核创建一个IDLE任务,并在这些任务上设置看门狗定时器。这意味着如果这些任务被执行时间饥饿,那么在超时期后,看门狗会被触发并重置芯片。IDLE任务会在后台执行一些重要的FreeRTOS“家庭”工作,因此您必须给它们时间。它们还具有最低可能的优先级。因此,如果任何任务具有更高优先级(例如您的回调函数),则这些任务将始终首先运行,而IDLE任务将不得不等待。因此,所有具有更高优先级的任务都必须足够短,以避免触发看门狗。如果这不可行,则必须在足够的间隔内插入延迟,例如通过调用vTaskDelay(...)或执行一些长时间阻塞的IO函数。在任一情况下,当前任务都将进入睡眠状态,FreeRTOS将启动另一个任务,并且如果没有其他等待更高优先级任务,则最终允许IDLE任务执行。所有这些意味着通常不应该编写会占用CPU 100%的代码,任何时候都不应该持续超过看门狗超时期。此外,在Arduino中无法关闭此功能。它只能在ESP-IDF中进行配置,这意味着您必须在该框架内编码或自行重新编译Arduino。而且无论如何关闭它都是一个不好的主意。

在调用vTaskDelay(...)时,必须使用至少1的值。由于整数运算和传递的各种表达式的影响,它可能最终相当于0,即根本没有延迟。

如果确实需要无暂停运行任务,则必须自己创建任务,并将其优先级设置为tskIDLE_PRIORITY,例如:

xTaskCreate(someFunction, "HumanReadableNameofTask", 4096, NULL, tskIDLE_PRIORITY, NULL);

其中,您的函数具有以下签名:

void someFunction(void* arg) {...}

您也可以在Arduino中执行此操作。因此,要么将在回调中运行的代码最小化,要么将繁重工作转移到单独的任务中,并使回调仅将任何相关信息转发到该任务(例如使用volatile变量和信号量,即并行处理中的通常同步方法。这超出了本答案的范围)。

请注意,我不确定在Arduino中调用delay(...)是否具有与vTaskDelay相同的效果,但它可能会有一些微妙之处。另外,有一种正式的方法可以让任务“yield”给另一个较低的任务,但我不确定详细信息。

重要提示:在回调函数中使用延迟函数(如vTaskDelay(...)), 特别是定时器回调函数,是一个不好的做法,因为它们会阻塞其他回调函数(在同一任务中)的执行。因此,最好的选择是将信息传递给在空闲优先级(即优先级0)下运行的独立任务。运行在0优先级下的任务之所以不会触发看门狗,是因为空闲任务也具有该优先级,FreeRTOS会轮询这些相同优先级的任务,即以交错的时间片并行地执行它们。但是当您有两个优先级不同的任务时,较高优先级的任务总是先执行直到完成或停滞/睡眠,然后才会运行较低优先级的任务,直到它自己完成或者更高优先级的任务再次唤醒并要求执行时间。
更新: 保持 Arduino 的 loop() 函数为空肯定会触发看门狗,因为它并不是真正的“空”,因为 loop() 函数内部被包装在一个无限循环中,所以CPU利用率实际上会飙升到100%,但没有任何有用的作用。这就是触发看门狗的原因。GET 进程似乎是异步进行的,因此不会阻塞 loop() 函数的执行。

好的解释,谢谢。然而,即使我将任务优先级设置为0,它仍然会触发看门狗。到目前为止,我唯一的解决方案是使用vTaskDelay(10)。10是否足够不触发看门狗定时器? - Albert Tobing
非常感谢您。我正准备吃涂有焊接油脂的面包板。一些在线教程建议可以用任务替换Arduino代码中的loop()函数,并将loop函数留空。这会导致恰好出现这种行为。 - drdeath
很高兴帮助,看到更新。 - mo FEAR
1
@AlbertTobing vTaskDelay(1) 肯定足够了。 如果不是这样,那么就有一些微妙的地方在表现自己。 如果使用vTaskDelay(pdMS_TO_TICKS(1)),如果配置的时钟节拍率为100Hz,则会评估为0。 或者例如将其设置为1可能会导致没有更多的空闲时间可用,如果其他任务正在运行并且还消耗CPU时间。 wdt并不仅因一个单独的任务而触发,而是当整个cpu不再得到时间来呼吸时触发。 此外,任务看门狗和中断看门狗之间有差异(后者在300ms后触发)。 - mo FEAR
@AlbertTobing,然而,只有在任务优先级不为0时才需要调用vTaskDelay。所以你确定你设置正确了吗?即使你创建另一个任务并将所有逻辑移到那里,主循环也不能是空的。如果没有其他代码会导致它足够频繁/长时间地阻塞,主循环始终需要一个vTaskDelay。要更改Arduino循环的优先级,您必须在setup()期间调用vTaskPrioritySet(NULL,newPriority),但甚至可能在循环内部调用(尽管这很有问题,因为它会不断被调用)。 - mo FEAR

3

ESPAsyncWebServer 回调函数在运行时会阻止看门狗定时器被重置。这意味着它们不适用于执行任何实际处理。请将请求注册并推迟处理到主循环(或其他线程)。有关详细信息,请查看此问题


3
在深入研究这个问题后,我找到了一个相当简单的解决方案;
只需将 #include "soc/rtc_wdt.h" 添加到库中即可。
然后执行:
rtc_wdt_protect_off();    // Turns off the automatic wdt service
rtc_wdt_enable();         // Turn it on manually
rtc_wdt_set_time(RTC_WDT_STAGE0, 20000);  // Define how long you desire to let dog wait.

然后你需要执行rtc_wdt_feed();,在一些你认为比狗等待时间更快的地方喂狗。

ESP-IDF配置有一组CONFIG_ESP_TASK_WDT_xxx定义。您还可以使用这些定义来控制所有的逻辑。 - Alex D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接