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

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/581946/

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

(0)

大家都在看

  • 面试题:Java序列化与反序列化

    序列化和反序列化的概念 应用场景? 序列化实现的方式 继承Serializable接口,普通序列化 继承Externalizable接口,强制自定义序列化 serialVersio…

    Linux 2023年6月6日
    0114
  • Snap Build Your Own Blocks输入中文解决办法

    Snap Build Your Own Blocks 输入中文解决办法 Snap! (formerly BYOB) is a visual, drag-and-drop progr…

    Linux 2023年6月6日
    085
  • centos系统和Ubuntu系统命令区别以及常见操作

    一.前言 二.系统环境 三.命令区别 3.1 使用习惯和命令区别 3.2 服务管理的区别 3.3 软件包信息区别 四.Ubuntu系统常见操作 4.1 Ubuntu系统apt和ap…

    Linux 2023年6月7日
    0216
  • Java刷题笔记7.25

    一个类构造方法的作用是什么? 主要是完成对&am…

    Linux 2023年6月7日
    0106
  • MySQL启动报:[ERROR] The server quit without updating PID file

    修改配置后 MySQL启动不了,报错: 看见这个不要惊慌,先把刚才修改的配置注释掉,看是不是配置有误!大部分是手误造成。 如果不行,再尝试一下方法: 解决方法 : 给予权限,执行 …

    Linux 2023年6月13日
    074
  • Android:hook很“危险”,使用需谨慎。

    前言 上篇文章《Android安卓进阶技术分享之AGP工作原理》和大家分析了 AGP(Android Gradle Plugin) 做了哪些事,了解到 AGP 就是为打包这个过程服…

    Linux 2023年6月13日
    076
  • JavaScript事件处理(三)

    上机三 JavaScript事件处理 目的: 熟练掌握JavaScript事件处理机制 重点理解面向对象编程思想,并构建程序。 要求: 定义一个按钮,动态生成DIV,可以生成多个D…

    Linux 2023年6月13日
    086
  • Java — 面向对象

    简介:类是对事物的一种描述,对象则为具体存在的事物。 类的定义: public class 类名 { // 成员变量 访问修饰符 数据类型 变量名; … // 成员方法 访问修…

    Linux 2023年6月8日
    084
  • JDK8以上提高开发效率

    1 接口的默认方法和静态方法 1.1 接口中可以定义默认方法和静态方法。 默认方法使用default修饰,静态方法和默认方法可以多个; 静态方法通过接口直接调用,默认方法通过接口实…

    Linux 2023年6月13日
    089
  • OrchardCore Headless建站

    说到CMS系统,可能大家都能想起 WordPress和 Drupal之类的框架,作为.NET爱好者,一般也是知道一些基于.NET的CMS框架的,典型的比如 DNN、 Umbraco…

    Linux 2023年6月6日
    094
  • 单片机 MCU 固件打包脚本软件

    ​ 1 前言 开发完 MCU 软件后,通常都会生成 hex 文件或者 bin 文件,用来做固件烧录或者升级,如果用来做产品开发,就涉及到固件版本的问题,初学者通常采用固件文件重命名…

    Linux 2023年6月7日
    091
  • CentOS 7.6 安装 MySQL-5.7.31(RPM方式安装)

    准备工作: 注:5.7.31版本安装步骤及初始化和之前版本有较大区别 CentOS 7.6 系统: 带GUI的服务器 默认安装 MySQL 5.7.31 安装包: 1.RPM安装包…

    Linux 2023年6月8日
    076
  • kafka能做什么?kafka集群配置 (卡夫卡 大数据)

    什么是Kafka 官网介绍: 几个概念: 详细介绍 : 操作kafka: kafka集群 消息测试 问题检测 什么是Kafka 官网介绍: ApacheKafka®是一个分布式流媒…

    Linux 2023年6月7日
    0116
  • CAP 5.1 版本发布通告 你期待的 Redis 来了

    前言 今天,我们很高兴宣布 CAP 发布 5.1 版本正式版,在这个版本里我们同样引入了更多令人激动的新特性和改进,同时也得到越来越多人的喜爱。 得益于社区的反馈和贡献者的支持,在…

    Linux 2023年5月28日
    0103
  • MySQL日志管理之二进制日志

    二进制日志采用二进制格式(01)来存储日志信息(导致数据发生变化的SQL语句),二进制日志记录整个数据库的修改过程。 MySQL的二进制日志主要就是起一个备份的功能。二进制日志≈备…

    Linux 2023年6月7日
    098
  • 【电子取证:镜像仿真篇】Windows Server镜像仿真、vmdk镜像仿真

    Windows Server镜像仿真、vmdk镜像仿真 时间过得真快呀!–【suy999】 一、qemu-img镜像转换工具 qemu-img(v2.3.0.0)镜像转…

    Linux 2023年6月13日
    099
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球