当保存参数使用结构体时必备的开发技巧方式

1、前言

想必做嵌入式产品开发都遇到过设备需要保存参数,常用的方式就是按照结构体的方式管理参数,保存时将整个结构体数据保存在 Flash 中,方便下次读取。

1.1、目的

本文时分析嵌入式/单片机中参数保存的几种方式的优点和缺点(仅针对单片机/嵌入式开发而言),同时针对以结构体的方式解决一些弊端问题( 重点在第 3 节)。

2、参数保存格式

2.1、结构体格式

该方式是嵌入式/单片机中开发最常用的,将所有的系统参数通过结构体的方式定义,然后保存数据,介绍一下该方式的优缺点。

储存方式:二进制 bin 文件格式

优点:

  1. 管理简单:无需额外的代码直接就能很方便的管理参数
  2. 内存最小:通过结构体的形式保存在Flash中,占用内存最小

缺点:

  1. 扩展性差:
  2. 从产品角度来说,产品需要升级,若是涉及增加参数,则升级后参数通常无法校验通过(通常包含长度校验等),导致参数被恢复默认
  3. 若是每个模块都存在自己的独有结构体参数定义,删除/新增时势必影响到其他的,导致设备升级后参数错乱(结构体中的变量地址在 bin 文件中是固定的)
  4. 阅读性差:若参数需要导出,bin文件没有可读性

改进措施:

结构体增加预留定义,若之后需要新增参数,则在预留空间新增即可,能在一定程度上解决扩展性差的问题,即新增不影响原有的结构体大小和其他成员变量的位置,删除恢复成预留即可。

为啥说只能在一定程度上解决该问题,因为之后的升级某些模块可能很长时间或者从不需要增加新的参数,这种势必就会造成内存的无效占用,或者有些模块频繁增加参数导致预留大小不够等问题,只能在前期设计时多加思考预留的分配情况(毕竟内存只有那么大)

/*****************************
           改进之前
*****************************/

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
} TestParam_t;    /* 某模块参数 */

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
    TestParam_t tTestParam;
} SystemParam_t; /* 系统参数 */

/*****************************
           改进之后
*****************************/

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
    uint8_t reserve[6];    // 预留
} TestParam_t;    /* 某模块参数 */

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
    TestParam_t tTestParam;
    uint8_t reserve[50];   // 预留
} SystemParam_t; /* 系统参数 */

2.2、JSON格式

最近Json格式很是流行使用,特别是数据交换中用的很多,但是它也可以用来保存参数使用,JSON 的是 “{键:值}” 的方式。

储存方式:字符串格式,即文本的形式

优点:

  1. 扩展性好:由于Json的格式,找到对应键值(一般都是该变量的标识),就能找到对应的值
  2. 阅读性好:有标识所以导出参数文件通过普通的文本文件打开都能看懂

缺点:

  1. 管理相对复杂:没有结构体那么简单,不熟还得先学习 JSON 的写法
  2. 内存占用较大:内容不只有值,而且都按照字符串的形式保存的
  3. 使用相关困难:需要解析,C语言虽然有开源库,但是由于语言性质使用不方便,C++ 反而使用简单
{
    "SYS":
    {
        "testParam" : 2,
        "testParam2" : 5,
        "tTestParam":
        {
            "testParam" : 2,
            "testParam2" : 5
        }
    }
}

//压缩字符串为:
{"SYS":{"testParam":2,"testParam2":5,"tTestParam":{"testParam":2,"testParam2":5}}}

2.3、键值格式

和上述的 JSON 格式很类似,都是键值对的格式,但是比JSON简单

储存方式:字符串格式,即文本的形式

优点:

  1. 扩展性好:找到对应键值(一般都是该变量的标识),就能找到对应的值
  2. 阅读性好:有标识所以导出参数文件通过普通的文本文件打开都能看懂

缺点:

  1. 内存占用较大:内容不只有值,而且都按照字符串的形式保存的
  2. 使用稍微困难:需要简单解析处理
  3. 管理不变:不方便按照一定的规则管理各模块的参数
testParam=2
testParam2=5
T_testParam=2
T_testParam2=5

2.4 其他

还有其他,如 xml (类似JSON)等,就不多介绍了

3、编译器检查结构体的大小和成员变量的偏移

在第 2 节中介绍了关于参数保存的三种方式,但是对于嵌入式单片机开发而言,Flash 大小不富裕,所以通常都是通过二进制的形式保存的,所以这节重点解决结构体管理保存参数的扩展性问题。

先说一下痛点(虽然对扩展性问题做了改进措施,除了前面讲到的问题,还有其他痛点,虽不算问题,但是一旦出现往往最要命)

  1. 在原来的预留空间中新增参数,要确保新增后结构体的大小不变,否则会导致后面的其他参数偏移,最后升级设备后参数出现异常(如果客户升级那就是要命啊)
  2. 确保第一点,就必须在每次新增参数都要计算检查一下结构体的大小有没有发生变化,而且有没有对结构体中的其他成员也产生影响

每次新增参数,手动计算和校验 99% 可以检查出来,但是人总有粗心的时候(加班多了,状态不好…),且结构体存在填充,一不留神就以为没问题,提交代码,出版本(测试不一定能发现),给客户,升级后异常,客户投诉、扣工资(难啊….)

遇到这种问题后:难道编译器就不能在编译的时候检查这个大小或者结构体成员的偏移吗,每次手动计算校验好麻烦啊,一不留神还容易算错 # _ #

按照正常情况,编译器可不知道你写的结构体大小和你想要的多大,所以检查不出来(天啊,崩溃了0.0….)

别急,有另类的方式可以达到这种功能,在编译时让编译器为你检查,而且准确性 100%(当然,这个添加新参数时你还得简单根据新增的参数大小减少预留的大小,这个是必须要的)

见代码:

/**
  * @brief 检查结构体大小是否符合
  *        在编译时会进行检查
  * @param type 结构体类型
  * @param size 结构体检查大小
  */
#define TYPE_CHECK_SIZE(type, size) extern int sizeof_##type##_is_error [!!(sizeof(type)==(size_t)(size)) - 1]

/**
  * @brief 结构体成员
  * @param type   结构体类型
  * @param member 成员变量
  */
#define TYPE_MEMBER(type, member) (((type *)0)->member)

/**
  * @brief 检查结构体成员大小是否符合
  *        在编译时会进行检查
  * @param type 结构体类型
  * @param member 结构体类型
  * @param size 结构体检查大小
  */
#define TYPE_MEMBER_CHECK_SIZE(type, member, size) extern int sizeof_##type##_##member##_is_error \
    [!!(sizeof(TYPE_MEMBER(type, member))==(size_t)(size)) - 1]

/**
  * @brief 检查结构体中结构体成员大小是否符合
  *        在编译时会进行检查
  * @param type 结构体类型
  * @param member 结构体类型
  * @param size 结构体检查大小
  */
#define TYPE_CHILDTYPE_MEMBER_CHECK_SIZE(type, childtype, member, size) extern int sizeof_##type##_##childtype##_##member##_is_error \
    [!!(sizeof(TYPE_MEMBER(type, childtype.member))==(size_t)(size)) - 1]

/**
  * @brief 检查结构体成员偏移位置是否符合
  *        在编译时会进行检查
  * @param type 结构体类型
  * @param member 结构体成员
  * @param value 成员偏移
  */
#define TYPE_MEMBER_CHECK_OFFSET(type, member, value) \
         extern int offset_of_##member##_in_##type##_is_error \
        [!!(__builtin_offsetof(type, member)==((size_t)(value))) - 1]

/**
  * @brief 检查结构体成员偏移位置是否符合
  *        在编译时会进行检查
  * @param type 结构体类型
  * @param member 结构体成员
  * @param value 成员偏移
  */
#define TYPE_CHILDTYPE_MEMBER_CHECK_OFFSET(type, childtype, member, value) \
         extern int offset_of_##member##_in_##type##_##childtype##_is_error \
        [!!(__builtin_offsetof(type, childtype.member)==((size_t)(value))) - 1]

通过以上代码,就能解决这个问题, 这个写法只占用文本大小,编译后不占内存!!!

用法:

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
    uint8_t reserve[6];    // 预留
} TestParam_t;    /* 某模块参数 */

TYPE_CHECK_SIZE(TestParam_t, 8); // 检查结构体的大小是否符合预期

typedef struct
{
    uint8_t testParam;
    uint8_t testParam2;
    TestParam_t tTestParam;
    uint8_t reserve[54];   // 预留
} SystemParam_t; /* 系统参数 */

TYPE_CHECK_SIZE(SystemParam_t, 64); // 检查结构体的大小是否符合预期
TYPE_MEMBER_CHECK_OFFSET(SystemParam_t, tTestParam, 2); // 检查结构体成员tTestParam偏移是否符合预期

假设新增了参数,预留写错了,导致结构体的大小不符合,则编译时报错,且提示内容也能快速定位问题。

当保存参数使用结构体时必备的开发技巧方式

关于这种方式的检查,你了解或者能理解多少呢?有兴趣的朋友可以留下你的评论。

Original: https://www.cnblogs.com/const-zpc/p/16364429.html
Author: 大橙子疯
Title: 当保存参数使用结构体时必备的开发技巧方式

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/714537/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 查看某个服务的连接数

    4012为服务端口netstat -an |grep ‘ESTABLISHED’ |grep -i ‘4012’ | wc -l 万水千山总是情, 领个红包行不行? 觉得还可以的话…

    技术杂谈 2023年5月31日
    090
  • Linux同时输出到管道和标准输出

    想使用Shell脚本对某文本文件中无序的一列数字排序并输出求和结果,文本如下所示: 421350 一开始使用的命令只能输出求和结果,不能同时给出排序结果: sort -nr t3….

    技术杂谈 2023年7月11日
    097
  • excel表格宏的启用

    这两张图片都可以启用宏,任何一种方式都可以 方式1 方式2 posted @2022-03-26 16:27 LiuYanYGZ 阅读(383 ) 评论() 编辑 Original…

    技术杂谈 2023年5月31日
    0126
  • HDU 4833 Best Financing (DP)

    Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Su…

    技术杂谈 2023年5月30日
    087
  • Maven项目搭建-Eclipse版

    一、Maven简单介绍 Maven是基于Java平台的项目构建(mvn clean install)、依赖管理(中央仓库,Nexus)和项目信息管理的项目管理工具。 Maven是基…

    技术杂谈 2023年5月31日
    0108
  • Log4j 2 日志框架

    Apache Log4j 2 是对 Log4j 的升级,它比其前身 Log4j 1.x 提供了显着改进,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构…

    技术杂谈 2023年7月11日
    077
  • IO复用三种方式

    IO复用技术,简单来说就是同时监听多个描述符。在没有用到IO复用以前,只能是一个线程或一个线程去监听,服务端同时有多个连接的时候,需要创建多个线程或者进程。而且,并不是所有的连接是…

    技术杂谈 2023年6月21日
    0121
  • Playwright简单试用

    距上篇关于playwright文章过去有一年多了,主要是因为加上早期的playwright并不是很成熟,缺少我最常用到的直接通过CDP(chrome dev protocol)来连…

    技术杂谈 2023年5月31日
    0104
  • 解决netch无法访问Google但可访问其他外网问题

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年6月21日
    0101
  • Activiti6 查询由某人发起的流程请求 设置流程发起人

    发起流程时,配置activiti:initiator属性,并且在代码中: Authentication.setAuthenticatedUserId(userId); 其中,use…

    技术杂谈 2023年5月31日
    0108
  • 5个节约生命的Python小技巧

    前言 Python是一种强大且易上手的语言,语法简洁优雅,不像Java那么繁琐废话,并且有一些特殊的函数或语法可以让代码变得更加 简短精悍。根据我的经验,下面介绍常用的5个Pyth…

    技术杂谈 2023年6月21日
    0118
  • RMarkdown进阶操作

    技术背景 Markdown大家都比较熟悉了,特别是在写程序文档和写数学公式时,拥有着无与伦比的便利性。同时在前面的一篇博客中我们介绍了使用RMarkdown去写Latex Beam…

    技术杂谈 2023年7月25日
    070
  • 高通量测序数据的存储、分析和管理

    以后,随着自己课题组建立起来,数据不断积累,良好的管理系统就必不可少了。 关键点: 测序raw data的存储与初步分析 下游分析processed data的存储与分析 数据的深…

    技术杂谈 2023年5月31日
    0105
  • .NET线程池最大线程数的限制-记一次IIS并发瓶颈

    IIS并发瓶颈,有几个地方,IIS线程池的最大队列数,工作进程数,最大并发数。这些这里就不展开。主要是最近因为过度使用Task 导致的线程数占用过多,所以实验了一下 .net线程池…

    技术杂谈 2023年6月21日
    078
  • electron-vue 隐藏顶部菜单 隐藏导航 、自定义导航

    myheader.vue [] X app.vue 首页 新闻 main/icpMain.js //接收渲染进程广播的数据执…

    技术杂谈 2023年5月31日
    0115
  • kubernetes调度

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年7月24日
    085
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球