ESP-IDF的NVS在项目中发现的诡异问题

笔者前边的系列文章中介绍和讲解了ESP-IDF中关于NVS的概念和相关操作函数,并且也澄清了笔者之前对于NVS的误区(参见笔者的“ESP32-C3模组上跑通NVS”系列文章)。然而,就在笔者信心满满地认为完全能够驾驭NVS的时候,一个十分诡异的问题出现了,这个问题到现在笔者也仍然没有找到根本原因,当前只能采取Work Around(规避、绕过)的方法。在此将这个问题出现的始末缘由原原本本地交代清楚,后面如果有遇到相同问题的人,可以留言;如果你更进一步,找到了问题的原因及解决方法,那么更加欢迎在评论区给出原因和解决方法,不胜感激!

问题的完整描述

笔者一开始,按照乐鑫的官网例程编写了NVS的读写代码,如下所示:

  • NVS写入相应的键值对

代码如下:

int upgrade_flag_set(bool flag)
{
    esp_err_t ret;
    nvs_handle_t nvs_handle;
    char upd_flg_val[10] = {0};
    size_t length = 0;

    ret = nvs_open("storage", NVS_READWRITE, &nvs_handle);
    if (ret != ESP_OK)
    {
        printf("nvs_open_from_partition error, ret is: %d\n", ret);
        return ESP_FAIL;
    }

    if (flag == true)
        ret = nvs_set_str(nvs_handle, "upgrade_flag", "1");
    else
        ret = nvs_set_str(nvs_handle, "upgrade_flag", "0");
    if (ret != ESP_OK)
    {
        printf("nvs_set_str error\n");
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }

    ret = nvs_commit(nvs_handle);
    if (ret != ESP_OK)
    {
        printf("nvs_commit error\n");
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }

    ret = nvs_get_str(nvs_handle, "upgrade_flag", NULL, &length);
    if (ret != ESP_OK)
    {
        printf("nvs_get_str error, ret is: %d\n", ret);
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }
    ret = nvs_get_str(nvs_handle, "upgrade_flag", upd_flg_val, &length);
    if (ret != ESP_OK)
    {
        printf("nvs_get_str error, ret is: %d\n", ret);
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }
    printf("upgrade_flag is %s", upd_flg_val);

    if(flag == true)
    {
        if (strncmp(upd_flg_val, "1", 1))
        {
            printf("value read is not equal to that write\n");
            nvs_close(nvs_handle);
            return ESP_FAIL;
        }
    }
    else
    {
        if (strncmp(upd_flg_val, "0", 1))
        {
            printf("value read is not equal to that write\n");
            nvs_close(nvs_handle);
            return ESP_FAIL;
        }
    }

    nvs_close(nvs_handle);

    return ESP_OK;
}

这段代码别看不算短,但功能其实很简单,就是打开系统默认的nvs分区中的名称为storage的命名空间,并且根据函数参数(true或false),写入key为"upgrade_flag"所对应的value("1"或“0”)。写入完成后,通过nvs_commit函数提交,并在之后通过nvs_get_str函数读取其值,看看是否写入成功,确保写入的值是正确的。

  • NVS读取相应的键值对

代码如下:

int upgrade_flag_get(char *flag_val)
{
    esp_err_t ret;
    nvs_handle_t nvs_handle;
    size_t length = 0;

    if (!flag_val)
    {
        printf("parameter can't be null\n");
        return ESP_FAIL;
    }

    ret = nvs_open("storage", NVS_READONLY, &nvs_handle);
    if (ret != ESP_OK)
    {
        printf("nvs_open_from_partition error, ret is: %d\n", ret);
        return ESP_FAIL;
    }

    ret = nvs_get_str(nvs_handle, "upgrade_flag", NULL, &length);
    if (ret != ESP_OK)
    {
        printf("nvs_get_str error, ret is: %d\n", ret);
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }
    ret = nvs_get_str(nvs_handle, "upgrade_flag", flag_val, &length);
    if (ret != ESP_OK)
    {
        printf("nvs_get_str error, ret is: %d\n", ret);
        nvs_close(nvs_handle);
        return ESP_FAIL;
    }

    nvs_close(nvs_handle);

    return ESP_OK;
}

这段代码就更简单了,就是打开nvs分区的storage命名空间,得到其中key为"updgrade_value"所对应的value(之后关闭句柄)。返回给函数入参flag_val。

  • 检查标志位是否设置

代码如下:

bool is_updflg_set(void)
{
    esp_err_t ret;
    char upd_flg_val[10] = {0};

    ret = upgrade_flag_get(upd_flg_val);
    if (ret != ESP_OK)
    {
        printf("upgrade_flag_get failed\n");
        return false;
    }
    printf("strlen(upd_flg_val) is %d\n", strlen(upd_flg_val));
    printf("upd_flg_val is %s\n", upd_flg_val);
    if (strncmp(upd_flg_val, "1", 1))
        return false;
    else
        return true;
}

这段代码就是获取"upgrade_flag"的值,并检查其是否被设置为了"1",如果是,返回true;否则返回false。

以上3个函数代码,笔者是看了又看、查了又查,又找同事帮忙检查,最终都认为并无认识上的和逻辑上的错误(当然,就更没有语法上的错误了)。

把这3个函数串起来,放到主函数中,实现一个完整的功能。代码如下:

void app_main(void)
{
    // Initialize NVS
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // NVS partition was truncated and needs to be erased
        // Retry nvs_flash_init
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK( err );

    if (is_updflg_set())
    {
        upgrade_flag_set(false);
        updres_send_flag = true;
    }

    upgrade_flag_set(true);

}

主函数的逻辑也很简单:先初始化nvs,然后检查nvs分区中的storage命名空间中的"upgrade_flag"这个key的值是否为"1",如果为"1",则将其写为"0",在下边再次将其写为"1"。

在此先不用纠结笔者为什么要这么做(写完"0",接着又写"1"),先往下看。

实际上,笔者原本的逻辑是,在上一次固件升级成功、准备重启之前,将"upgrade_flag"的值写为1。这样重启后通过判断此值,就能够知道本次是正常启动还是升级重启。之后如果"upgrade_flag"的值为"1",则将它写为“0”。这是笔者设计以上这些代码的初衷。

如果只是以上这些代码,那么执行起来毫无问题,逻辑和功能完全是对的。第1次执行的时候,在执行到is_updflg_set函数判断的时候会报错,因为此时nvs分区的storage命名空间中还没有"upgrade_flag"这个key,之前并没有写过。之后就执行带主函数最后的upgrade_flag_set(true),将"upgrade_flag"设置为"1"。之后无论是再次重启还是重新烧录运行,都在执行到is_updflg_set()时不再提示报错,因为此时nvs的命名空间storage中已经有了"upgrade_flag"这个key,并且无论是重启还是重新烧录,都不会改变nvs分区中的这个"upgrade_flag"所对应的值。

然而,当笔者引入了同事的配网代码之后(这里出于保密原因,不贴出同事的配网代码),情况就不一样了。合入其配网代码后,发现他的代码中所操作的nvs分区storage命名空间中的另外一个key每次逻辑都正常,但是笔者上边的代码就有些问题了。

具体什么问题呢?笔者实测发现:加入同事的配网代码后,编译并烧录,之后如果是重启,不管多少次,逻辑都是正确的,和上边仍然一致;但如果是每次重新烧录后再运行,则极大概率(几乎是100%)nvs中的那个"update_flag"会变回"0",即使上一次已经写为了"1"(这就是笔者上边的主函数代码中,每次最后都重新将"upgrade_flag"设置为"1"的原因)。

这就很奇怪了,按理说甭管是重启也好、重新烧录后启动也罢,nvs中相应key的value不应该在没有专门性操作的情况下被改变,即使烧录也是擦除和烧录的app分区,与nvs分区不相干。

笔者将这个问题反馈给了乐鑫技术支持,他们反馈说一般不会帮助客户定位其自身的业务逻辑代码,但如果能够更为精准地定位,即将业务逻辑代码刨出去,只剩下系统代码,那么他们可以帮忙定位原因,并且一旦复现,也好找相关部门进行反馈。人家说得也有道理,于是笔者针对于同事的配网代码进一步缩小范围、精准定位,最终定位到了引起问题的地方 —— xEventGroupWaitBits函数的调用。

于是笔者对于主函数进行了改造。改造后的app_main函数代码如下:

void app_main(void)
{
    // Initialize NVS
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // NVS partition was truncated and needs to be erased
        // Retry nvs_flash_init
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK( err );

    printf("\n3333333333\n");
    if (is_updflg_set())
    {
        printf("\n555555555555\n");
        upgrade_flag_set(false);
        updres_send_flag = true;
        printf("\n666666666666\n");
    }
    printf("\n4444444444\n");

    s_wifi_event_group = xEventGroupCreate();
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,
                                           //portMAX_DELAY);
                                           //20);
                                           500);

    printf("\n111111111\n");
    upgrade_flag_set(true);
    printf("\n222222222\n");
}

在这里说明一下改造后的app_main函数中的逻辑:主体功能还是和上边一样,只是加入了打印以及事件相关的代码段。第1次完整执行app_main()后,"upgrade_flag"的值必定是"1",这样下次启动(重启或重新烧录后启动),在执行到is_updflg_set()代码段时,正确的情况一定会打印出"55555……"和"66666……"。

但是实际测试时,就和上边说的一样,如果是重启,甭管多少次,从第2次开始,每次都会打印出"55555……"和"66666……";而如果每次是重新烧录代码后再启动,则几乎每次都出现不了"55555……"和"66666……",也即进入不了is_updflg_set()代码段,也即"upgrade_flag"的值不是"1"而是"0"。

更进一步地,笔者测试得到更加诡异的规律:如果xEventGroupWaitBits函数的最后一个参数timeout即超时时间,设置得越大,则越有可能复现笔者的现象;而其值越小越不会出现。比如笔者上边的代码中,在超时时间为300及以上的时候就有出现问题的概率了,在超时时间为代码中的500左右的时候,问题的出现概率就比较大了;而超时时间为1000左右则每次必现;但如果是笔者代码中(注释掉的)的20左右,则(基本)不会出现此问题。

这就很奇怪了,nvs中值的变化碍着事件等待的超时时间什么事了?完全是不搭界、更通俗地说是八竿子打不着的两个东西,怎么会产生影响(不由得想到了量子纠缠……)。

笔者将在自己的环境下测得的这个现象和结果告知了乐鑫技术支持,这次他们同意我把代码发给他们,先在他们那里尝试复现。如果在他们的环境下也能够复现,则他们再找内部开发人员定位;如果他们那里无法复现,则需要笔者排查自身的软件环境设置。

结果在乐鑫技术支持那里,他们按照笔者同样的复现步骤反复试了十几次,都始终无法复现,每次不管是重启还是烧录后重新启动,逻辑和功能都是正常的、正确的。那笔者就只能检查自己电脑中的环境配置了。

笔者一度也怀疑,是不是前段时间在安装ESP-IDF 5.2.2的时候,使得系统中既存在5.2.1又存在5.2.2版本,从而导致了这个问题(毕竟之前系统中只包含5.2.1版本时还没发现这个问题,也就没有测过)。于是笔者找了一位同事,其电脑环境中只有ESP-IDF 5.2.1版本,将代码工程传给他,请他帮忙测试一下,看看能否复现这个问题。如果他那里不能复现,则无疑是笔者自身电脑软件环境的问题;如果他那里也能复现,则就说明至少ESP-IDF 5.2.1版本确实存在此问题。

经过同事的测试,最终也得到了和笔者相同的现象,即复现了笔者相同的问题(一开始的2、3次他那里没有出现,笔者一度认为真的是笔者的环境问题,从第4次开始往后一直出现了,而且也是超时时间越大越容易出现)。

笔者将这一发现反馈给了乐鑫技术支持,并询问他们的ESP-IDF版本是多少,答复说他们用的是5.2.2版本。那么现在基本可以得出结论了,至少ESP-IDF 5.2.1版本中确实存在此问题。至于5.2.2版本中是否也存在这个问题,则需要笔者后续腾出时间来测试并验证了。大家如果有时间和兴趣,或者也遇到了相同问题,也可以帮忙测试验证一下。

另外,多说一点,笔者发现,如果将is_updflg_set()这一段代码挪到xEventGroupWaitBits函数调用的下边,则每都是正常的。即代码是这样:

void app_main(void)
{
    // Initialize NVS
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // NVS partition was truncated and needs to be erased
        // Retry nvs_flash_init
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK( err );

    s_wifi_event_group = xEventGroupCreate();
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,
                                           //portMAX_DELAY);
                                           //20);
                                           500);

    printf("\n3333333333\n");
    if (is_updflg_set())
    {
        printf("\n555555555555\n");
        upgrade_flag_set(false);
        updres_send_flag = true;
        printf("\n666666666666\n");
    }
    printf("\n4444444444\n");

    printf("\n111111111\n");
    upgrade_flag_set(true);
    printf("\n222222222\n");
}

后记

笔者在自己电脑上VSCode中使用ESP-IDF 5.2.2环境测了一下,问题依旧,这一点与乐鑫技术支持并不一致。这个问题还需要进一步查找和定位,目前可以断定不是使用者的问题,而就是乐鑫的系ESP-IDF统软件存在一定问题。

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
你可以按照以下步骤在 ESP-IDF 新建项目: 1. 首先,确保已经安装了 ESP-IDF 开发环境。你可以从官方网站下载并按照指南进行安装:https://docs.espressif.***/index.html 2. 打开终端或命令行界面,并进入一个你想要创建项目的目录。 3. 运行 `idf.py create-project <project-name>` 命令,其 `<project-name>` 是你自己的项目名称。这将会在当前目录下创建一个名为 `<project-name>` 的文件夹,并生成默认的项目文件结构。 4. 进入项目文件夹,运行 `idf.py menuconfig` 命令来配置项目。在配置界面,你可以设置串口通信参数、WiFi 设置、任务调度器等等。完成配置后,保存并退出。 5. 编写你的代码和应用程序。在 `<project-name>` 文件夹,你将会找到 `main` 文件夹,其包含 `main.c` 文件,你可以在这里编写你的应用程序代码。 6. 构建和烧录项目。在项目文件夹下运行 `idf.py build` 命令来构建项目。如果一切顺利,你将会得到一个可执行文件。然后,使用 `idf.py flash` 命令将可执行文件烧录到你的 ESP32 开发板上。 7. 最后,可以使用 `idf.py monitor` 命令来查看串口输出,并与你的应用程序进行交互。 这样,你就成功创建了一个 ESP-IDF 项目,并可以开始开发你的应用程序了。记得在开发过程参考 ESP-IDF 官方文档和示例代码,以便更深入地了解 ESP-IDF 的功能和特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝天居士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值