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统软件存在一定问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/760734.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

超详细的 C++中的封装继承和多态的知识总结<1.封装>

引言 小伙伴们都知道C面向对象难,可是大家都知道,这个才是C和C的真正区别的地方,也是C深受所有大厂喜爱的原因,它的原理更接近底层,它的逻辑更好,但是学习难度高,大家一定要坚持下来呀&#xff…

如何做好一个企业家IP:塑造独特的个人品牌

在当今数字化时代,个人品牌的力量愈发凸显,对于企业家而言,一个强大的IP(Intellectual Property,即知识产权或个人品牌)不仅有助于提升个人影响力,还能为企业的发展注入强大动力。那么&#xff…

Flutter【组件】点击类型表单项

简介 flutter 点击表单项组件,适合用户输入表单的场景。 点击表单项组件是一个用户界面元素,通常用于表单或设置界面中,以便用户可以点击它们来选择或更改某些设置或输入内容。这类组件通常由一个标签和一个可点击区域组成,并且…

【后端面试题】【中间件】【NoSQL】ElasticSearch索引机制和高性能的面试思路

Elasticsearch的索引机制 Elasticsearch使用的是倒排索引,所谓的倒排索引是相对于正排索引而言的。 在一般的文件系统中,索引是文档映射到关键字,而倒排索引则相反,是从关键字映射到文档。 如果没有倒排索引的话,想找…

基于51单片机的篮球计时器Proteus仿真

文章目录 一、篮球计时器1.题目要求2.思路3.仿真图3.1 未仿真时3.2 仿真开始3.3 A队进分3.4 B队进分3.5 比赛结束 4.仿真程序4.1 主函数4.2 时间显示4.3 比分显示4.4 按键扫描 二、总结 一、篮球计时器 1.题目要求 以51单片机为核心,设计并制作篮球计时器 基本功…

数据结构:期末考 第六次测试(总复习)

一、 单选题 (共50题,100分) 1、表长为n的顺序存储的线性表,当在任何位置上插入或删除一个元素的概率相等时,插入一个元素所需移动元素的平均个数为( D ).(2.0) A、 &am…

基于matlab的可乐标签模板匹配

1 建模思路 1.图像预处理: 如果目标图像和模板图像是彩色的(即RGB图像),则将它们转换为灰度图像,以便在单通道上进行匹配。使用rgb2gray函数进行灰度化。 2.获取模板大小: 使用size函数获取模板图像的高…

骁龙相机拍照流程分析

和你一起终身学习,这里是程序员Android 经典好文推荐,通过阅读本文,您将收获以下知识点: 1.deliverInputEvent 拍照点击事件处理 2.submitRequestList Camera 提交拍照请求 3.createCaptureRequest 拍照请求帧数 骁龙相机通过binder 数据传输…

2006-2020上市公司研发投入金额数据集

2006-2020上市公司研发投入金额数据集https://download.csdn.net/download/a519573917/89501035 目录 上市公司研发投入与企业绩效的关系研究 一、引言 二、文献综述 三、研究设计 四、实证结果与分析 (一)描述性统计分析 (二&#xf…

人工智能在肿瘤:分子亚型分类领域的最新研究进展|顶刊速递·24-07-01

小罗碎碎念 今日推文主题:人工智能在肿瘤/分子亚型分类中的应用 小罗观点 前两天有一位复旦的师兄私聊问了我一些问题,我看完以后觉得大家可能对于“分类”的概念有点不太熟悉,所以我决定写这篇推文系统的梳理一下“分类”和“回归”。 这俩都…

CleanMyMacX2024免费且强大的mac电脑系统优化工具

如果你的Mac电脑出现了存储空间不足、运行缓慢、电池电量消耗过快等问题,那么CleanMyMacX这款软件或许能为你提供解决方案。作为一款强大的系统优化工具,它能够帮助用户清理垃圾文件、优化内存和电池使用,从而提升Mac的性能表现,让…

09_计算机网络模型

目录 OSI/RM七层模型 OSI/RM七层模型 各层介绍及硬件设备 传输介质 TCP/IP协议簇 网络层协议 传输层协议 应用层协议 完整URL的组成 IP地址表示与计算 分类地址格式 子网划分和超网聚合 无分类编址 特殊含义的IP地址 IPv6协议 过渡技术 OSI/RM七层模型 OSI/RM七…

使用 Vue 实现包含单选框的弹窗功能(附Demo)

目录 前言1. Vue22. Vue3 前言 如果在弹窗中单独增设一些选项或者少部分的数据,可用如下的方式 (不用单独创建专门的表单样式) 如果单纯可以通过基本的按钮传输给后端,可用如下知识点 对于弹窗的基本知识推荐阅读: …

2024年06月CCF-GESP编程能力等级认证Scratch图形化编程四级真题解析

本文收录于《Scratch等级认证CCF-GESP图形化真题解析》专栏,专栏总目录:点这里,订阅后可阅读专栏内所有文章。 一、单选题(共 10 题,每题 2 分,共 30 分) 第1题 小杨父母带他到某培训机构给他报名参加 CCF 组织的 GESP 认证考试的第 1 级,那他可以选择的认证语言有几…

数据资产铸就市场竞争优势:运用先进的数据分析技术,精准把握市场脉搏,构建独特的竞争优势,助力企业实现市场领先地位,赢得持续成功

目录 一、引言 二、数据资产的重要性 三、先进数据分析技术的应用 1、大数据分析技术 2、人工智能与机器学习 3、数据可视化技术 四、精准把握市场脉搏 1、深入了解客户需求 2、预测市场趋势 3、优化资源配置 五、构建独特的竞争优势 1、定制化产品和服务 2、精准营…

zerotier-one自建根服务器方法四

一、简介 前面几篇文章已经写完了安装配置服务器,今天写一下客户端如何连接自建的服务器。 二、准备工作 准备一个有公网IP的云主机。 要稳定性、安全性、不差钱的可以使用阿里、腾讯等大厂的云服务器。 本人穷屌丝一枚,所以我用的是免费的“三丰云…

Firefox 编译指南2024 Windows10-使用Git 管理您的Firefox(五)

1. 引言 在现代软件开发中,版本控制系统(VCS)是不可或缺的工具,它不仅帮助开发者有效管理代码的变化,还支持团队协作与项目管理。Mercurial 是一个高效且易用的分布式版本控制系统,其设计目标是简洁、快速…

【代码随想录】【算法训练营】【第53天】 [739]每日温度 [496]下一个更大元素I [503]下一个更大元素II

前言 思路及算法思维,指路 代码随想录。 题目来自 LeetCode。 day 48,周六,不能再坚持~ 题目详情 [739] 每日温度 题目描述 739 每日温度 解题思路 前提: 思路: 重点: 代码实现 C语言 [496] 下一…

算法题型归类整理及同类题型解法思路总结(持续更新)

1、最优路线 通用思路 1、递归 #案例1-最优路测路线 题目描述 评估一个网络的信号质量,其中一个做法是将网络划分为栅格,然后对每个栅格的信号质量计算。 路测的时候,希望选择一条信号最好的路线(彼此相连的栅格集合&#x…

Unity开箱即用的UGUI面板的拖拽移动功能

文章目录 👉一、背景👉二、效果图👉三、原理👉四、核心代码👉五,总结 👉一、背景 之前做PC项目时常常有面板拖拽移动的需求,今天总结封装一下,做成一个随时随地可复用的…