这是最近在做项目的过程中,遇到的一个问题。

        问题背景

        初始化两个esp_timer定时器,并分别注册了周期回调函数,当定时器溢出后被调用。在定时器1回调函数中获取sntp网络时间并打印;在定时器2回调函数中发送https请求获取数据包。此外主函数中还创建了一个跑LVGL的任务。具体代码如下:

#include <stdio.h>
#include <time.h>
#include "incl/myWIFI.h"
#include "incl/Ntp_time.h"
#include "incl/https_reqst.h"
#include "incl/lv_port.h"
#include "lvgl/lvgl.h"
#include "lv_demos.h"

static const char* TAG = "main";

// sntp时间获取完成回调
void mySntp_done_cb(void)
{
    ESP_LOGI(TAG, "mySntp_done, Beijing time was successfully obtained");
}

void lvgl_task_run(void * param)
{
    lv_demo_widgets();
    while (1)
    {
        lv_task_handler();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void app_main(void)
{
    wifi_sta_init();    // wifi设置为sta模式
    mySntp_ready();

    lv_port_init();     // 初始化lvgl
    xTaskCreatePinnedToCore(lvgl_task_run, "lvgl_task", 8192, NULL, 3, NULL, 1);

    while (1)
    {
        mySntp_start(mySntp_done_cb);
        https_request_start();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

        以上是主函数,下面是创建esp_timer的代码:

void mySntp_start(mySntp_done_t f)
{
    if(!get_wifi_status())
        return;

    if(false == mySntp_start_flag)       // 仅当第一次发起sntp同步时创建定时函数和任务
    {
        f_mySntp_done = f;
        ESP_LOGI(TAG, "mySntp start");
        mySntp_start_flag = true;

        static uint32_t timer_period = 60000000ull;     // 单位us, 即定时器周期为1分钟
        const esp_timer_create_args_t mySntp_periodic_timer_args = {
                .arg = NULL,
                .callback = print_now_time,
                .name = "print_time",
        };
        esp_timer_handle_t print_timer_handle;
        esp_timer_create(&mySntp_periodic_timer_args, &print_timer_handle);
        esp_timer_start_periodic(print_timer_handle, timer_period);

        xTaskCreatePinnedToCore(sntp_task, "sntp_task", 4096, NULL, 5, NULL, 1);
    }
}

static void https_request_task(void *pvparameters)
{
    static uint64_t timer_period = 120000000ull;        // 2分钟发送一次https请求

    const esp_timer_create_args_t https_request_timer_args = {
            .callback = send_https_request,
    };
    esp_timer_handle_t https_request_timer;
    ESP_ERROR_CHECK(esp_timer_create(&https_request_timer_args, &https_request_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(https_request_timer, timer_period));
    vTaskDelete(NULL);
}

        两个定时器回调函数

// 获取sntp网络时间回调函数
static void print_now_time(void* args)
{
    time_t now;
    struct tm timeinfo;
    static uint16_t cnt = 0;
    static bool is_fristPrint = true;

    time(&now);
    localtime_r(&now, &timeinfo);
    // Is time set? If not, tm_year will be (1970 - 1900).
    if (is_fristPrint || timeinfo.tm_year < (2016 - 1900)) {
        is_fristPrint = false;
        ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
        if(!sntp_inited_flag)
        {
            if(mySntp_init() == ESP_OK)
                sntp_inited_flag = 1;
        }
        if(obtain_time() != ESP_OK)
        {
            ESP_LOGE(TAG, "obtain time failed!");
        }
        // update 'now' variable with current time
        time(&now);
    }

    char strftime_buf[64];

    // Set timezone to China Standard Time
    setenv("TZ", "CST-8", 1);     
    tzset();
    localtime_r(&now, &timeinfo);

    // 将时间戳写入nvs
    nvs_handle_t my_handle = 0;
    xSemaphoreTakeRecursive(nvs_MuxSem, portMAX_DELAY);     // 获取锁
    if(tim_write_into_nvs(&my_handle, &now) == ESP_OK)
        ESP_LOGI(TAG, "tim_write_into_nvs successful");
    else ESP_LOGE(TAG, "tim_write_into_nvs failed");
    xSemaphoreGiveRecursive(nvs_MuxSem);                    // 释放锁

    // 打印时间
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "%dth: The current date/time in BeiJing is: %s", ++cnt, strftime_buf);
}

// 发送https请求回调函数
static void send_https_request(void* args)
{
    static uint16_t req_cnt = 0;    // 请求次数
    ESP_LOGI(TAG, "Start %dth https_request", ++req_cnt);

    if (esp_reset_reason() == ESP_RST_POWERON) {     // 从nvs中更新系统时间, 用于https验证
        ESP_LOGI(TAG, "Updating time from NVS");
        ESP_ERROR_CHECK(update_time_from_nvs());
    }

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
    https_get_request_using_crt_bundle();
#endif
    ESP_LOGI(TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
    ESP_LOGI(TAG, "Finish https_request");
}

         问题描述

        当定时器2回调函数发送https请求时,其他任务全部阻塞,lvgl界面卡住,待获取数据包完成后恢复正常。

问题分析

        经过调试以及查看相关资料得出:当esp_timer定时器溢出时,会触发一个高优先级的中断,然后在中断上下文中调度回调函数的执行。具体来说这个回调函数不是直接在中断上下文中调用,而是在中断中将回调函数的执行调度到一个高优先级的任务(esp_timer任务)中,意思就是将这个回调函数放在系统级的高优先级任务中执行。

        那么我们知道,在rtos中高优先级的任务会抢占低优先级的任务执行权,如果我们在这个回调函数中执行了过于繁琐的流程,那么其他低优先级任务就会得不到执行,所以导致我们的其它任务都阻塞,UI界面也卡住。

        接下来我们来看上面的定时器2回调函数,在回调函数中是发送https请求。由于HTTPS请求涉及复杂的操作,包括DNS解析、TCP连接、TLS握手、数据发送和接收等,所以这些操作可能会占用较长时间,导致用户创建的应用级任务(低优先级任务)得不到执行,等https请求完成后,又恢复正常。

        还需要注意的是,esp32具有两个CPU核心,分别为Core0和Core1,Core0一般是运行系统协议的任务比如wifi和蓝牙等等,而esp_timer的回调函数默认也是运行在Core0上面的,

这个可以通过将下面这句代码放在回调函数中打印得出,这里自行实验

printf("coreID: %d, line%d\r\n", xPortGetCoreID(), __LINE__);

所以在使用WIFI或蓝牙的情况下,请尽量不要调用核心0来执行任务,不然会影响通讯。

        解决方案 

        既然不能在回调函数中执行过多的操作,那么我们可以创建一个任务来执行发送https请求的相关操作,然后在回调函数中使用信号量、队列或者事件等用来通知任务的执行,这样在回调函数中仅执行一行代码就能执行https请求,具体代码如下:

// 定时器回调函数
static void access_https_request(void* args)
{
    xEventGroupSetBits(reqEventGp_handle, START_REQ_EVENTBIT);
}

// 发送https请求任务
static void send_https_request_task(void* params)
{
    static uint16_t req_cnt = 0;    // 请求次数
    static EventBits_t wait_bit;

    while(1)
    {
        wait_bit = xEventGroupWaitBits(reqEventGp_handle, START_REQ_EVENTBIT, pdTRUE, pdTRUE, portMAX_DELAY);
        if(wait_bit & START_REQ_EVENTBIT)
        {
            ESP_LOGI(TAG, "Start %dth https_request", ++req_cnt);
            if (esp_reset_reason() == ESP_RST_POWERON) {     // 从nvs中更新系统时间, 用于https验证
                ESP_LOGI(TAG, "Updating time from NVS");
                ESP_ERROR_CHECK(update_time_from_nvs());
            }
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
            https_get_request_using_crt_bundle();
#endif
            ESP_LOGI(TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
            ESP_LOGI(TAG, "Finish https_request");
        }
    }
}


// http请求任务和esp_timer的创建、事件标志组的创建
static void https_request_task(void *pvparameters)
{
    static uint64_t timer_period = 120000000ull;        // 2分钟发送一次https请求

    reqEventGp_handle = xEventGroupCreate();
    xEventGroupSetBits(reqEventGp_handle, START_REQ_EVENTBIT);     // 默认启动后发送一次https请求

    const esp_timer_create_args_t https_request_timer_args = {
            .callback = access_https_request,
    };
    esp_timer_handle_t https_request_timer;
    ESP_ERROR_CHECK(esp_timer_create(&https_request_timer_args, &https_request_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(https_request_timer, timer_period));

    xTaskCreatePinnedToCore(send_https_request_task, "send_https_req_task", 4096, NULL, 5, NULL, 1);
    vTaskDelete(NULL);
}

        修改后成功解决卡顿问题。这里由于连接了wifi,  指定Core1创建https请求任务。至于另外一个定时器回调函数print_now_timer同理使用相同的方法即可,尽量不要在esp_timer回调中执行过多的操作。

        总结

        就像中断中的处理一样,不允许在中断中使用延时函数或者执行过多的操作,否则会导致系统的实时性降低。

更多推荐