當保存參數使用結構體時必備的開發技巧方式
1、前言
想必做嵌入式產品開發都遇到過設備需要保存參數,常用的方式就是按照結構體的方式管理參數,保存時將整個結構體數據保存在 Flash 中,方便下次讀取。
1.1、目的
本文時分析嵌入式/單片機中參數保存的幾種方式的優點和缺點(僅針對單片機/嵌入式開發而言),同時針對以結構體的方式解決一些弊端問題(重點在第 3 節)。
2、參數保存格式
2.1、結構體格式
該方式是嵌入式/單片機中開發最常用的,將所有的系統參數通過結構體的方式定義,然後保存數據,介紹一下該方式的優缺點。
儲存方式:二進位 bin 文件格式
優點:
- 管理簡單:無需額外的程式碼直接就能很方便的管理參數
- 記憶體最小:通過結構體的形式保存在Flash中,佔用記憶體最小
缺點:
- 擴展性差:
- 從產品角度來說,產品需要升級,若是涉及增加參數,則升級後參數通常無法校驗通過(通常包含長度校驗等),導致參數被恢復默認
- 若是每個模組都存在自己的獨有結構體參數定義,刪除/新增時勢必影響到其他的,導致設備升級後參數錯亂(結構體中的變數地址在 bin 文件中是固定的)
- 閱讀性差:若參數需要導出,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 的是 「{鍵:值}」 的方式。
儲存方式:字元串格式,即文本的形式
優點:
- 擴展性好:由於Json的格式,找到對應鍵值(一般都是該變數的標識),就能找到對應的值
- 閱讀性好:有標識所以導出參數文件通過普通的文本文件打開都能看懂
缺點:
- 管理相對複雜:沒有結構體那麼簡單,不熟還得先學習 JSON 的寫法
- 記憶體佔用較大:內容不只有值,而且都按照字元串的形式保存的
- 使用相關困難:需要解析,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簡單
儲存方式:字元串格式,即文本的形式
優點:
- 擴展性好:找到對應鍵值(一般都是該變數的標識),就能找到對應的值
- 閱讀性好:有標識所以導出參數文件通過普通的文本文件打開都能看懂
缺點:
- 記憶體佔用較大:內容不只有值,而且都按照字元串的形式保存的
- 使用稍微困難:需要簡單解析處理
- 管理不變:不方便按照一定的規則管理各模組的參數
testParam=2
testParam2=5
T_testParam=2
T_testParam2=5
2.4 其他
還有其他,如 xml (類似JSON)等,就不多介紹了
3、編譯器檢查結構體的大小和成員變數的偏移
在第 2 節中介紹了關於參數保存的三種方式,但是對於嵌入式單片機開發而言,Flash 大小不富裕,所以通常都是通過二進位的形式保存的,所以這節重點解決結構體管理保存參數的擴展性問題。
先說一下痛點(雖然對擴展性問題做了改進措施,除了前面講到的問題,還有其他痛點,雖不算問題,但是一旦出現往往最要命)
- 在原來的預留空間中新增參數,要確保新增後結構體的大小不變,否則會導致後面的其他參數偏移,最後升級設備後參數出現異常(如果客戶升級那就是要命啊)
- 確保第一點,就必須在每次新增參數都要計算檢查一下結構體的大小有沒有發生變化,而且有沒有對結構體中的其他成員也產生影響
每次新增參數,手動計算和校驗 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偏移是否符合預期
假設新增了參數,預留寫錯了,導致結構體的大小不符合,則編譯時報錯,且提示內容也能快速定位問題。
關於這種方式的檢查,你了解或者能理解多少呢?有興趣的朋友可以留下你的評論。