當儲存引數使用結構體時必備的開發技巧方式

2022-07-06 12:06:38

1、前言

想必做嵌入式產品開發都遇到過裝置需要儲存引數,常用的方式就是按照結構體的方式管理引數,儲存時將整個結構體資料儲存在 Flash 中,方便下次讀取。

1.1、目的

本文時分析嵌入式/微控制器中引數儲存的幾種方式的優點和缺點(僅針對微控制器/嵌入式開發而言),同時針對以結構體的方式解決一些弊端問題(重點在第 3 節)。


2、引數儲存格式

2.1、結構體格式

該方式是嵌入式/微控制器中開發最常用的,將所有的系統引數通過結構體的方式定義,然後儲存資料,介紹一下該方式的優缺點。

儲存方式:二進位制 bin 檔案格式

優點:

  1. 管理簡單:無需額外的程式碼直接就能很方便的管理引數
  2. 記憶體最小:通過結構體的形式儲存在Flash中,佔用記憶體最小

缺點:

  1. 擴充套件性差:
    1. 從產品角度來說,產品需要升級,若是涉及增加引數,則升級後引數通常無法校驗通過(通常包含長度校驗等),導致引數被恢復預設
    2. 若是每個模組都存在自己的獨有結構體引數定義,刪除/新增時勢必影響到其他的,導致裝置升級後引數錯亂(結構體中的變數地址在 bin 檔案中是固定的)
  2. 閱讀性差:若引數需要匯出,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偏移是否符合預期

假設新增了引數,預留寫錯了,導致結構體的大小不符合,則編譯時報錯,且提示內容也能快速定位問題。


關於這種方式的檢查,你瞭解或者能理解多少呢?有興趣的朋友可以留下你的評論。