C语言宏列表的奇怪用法
示例的数据结构
嵌入式软件开发中,我们精心设计数据结构,利用结构体高效管理MCU运行时各变量的值。
那么,总是有那么一些数据,数量众多、结构一致,且读写操作也相同,甚至名称上存在规律。
static CalPoint_t Channel1_Table[] =
{
{1000, 23432},
{2000, 34453},
};
static CalPoint_t Channel2_Table[] =
{
{100, 8356},
};
static CalTable_t XXX_CalTable[] =
{
{
Channel1_ADDR,
2,
sizeof(Channel1_Table)/sizeof(CalPoint_t),
Channel1,
Channel1_Table},
{
Channel2_ADDR,
2,
sizeof(Channel2_Table)/sizeof(CalPoint_t),
Channel2,
Channel2_Table},
};
宏替换,进行简单优化
针对“XXX_CalTable”结构体数组,利用宏的特性,以及关键的连接符“##”,提取数组中重复出现的标识符:
{_ch##_ADDR,\
_n,\
__GET_NUM(_ch##_Table),\
_ch,\
_ch##_Table}
接着结构体数组就可以简化了,重复性的工作抛给了编译器:
static CalTable_t XXX_CalTable[] =
{
__TABLE_REG(Channel1, 2),
__TABLE_REG(Channel2, 2),
};
还能不能优化
上面精简了“XXX_CalTable”数组,但是:
“CalPoint_t”结构体未处理,占用篇幅
一些宏,名称上与结构体数组有关联
无法避免人为错误,使得名称不再关联
因而还可以考虑,用宏简化结构,并从根本上杜绝人为错误。
示例,先定义数据结构
根据实际需求,设计三层结构:
typedef struct
{
int32_t actual; /* 实际的目标值 */
uint32_t original; /* 原始值 */
}CalPoint_t;
typedef struct
{
uint16_t start_addr; /* EEPROM上的存储地址 */
uint8_t point_size; /* 校准点的大小 */
uint8_t number; /* 校准点数 */
uint8_t ch; /* 通道 */
uint8_t linkage_ch; /* 联动通道 */
CalPoint_t *point; /* 校准点 */
}CalTable_t;
typedef struct
{
uint8_t ch_number; /* 通道数 */
CalTable_t *table; /* 校准表格 */
}CalObj_t;
定义宏列表
宏列表用来注册结构体、数组、通道枚举等,我们只需要维护它即可:
/* 注册校准表格。每个通道至少要有一个点,否则编译报错 */
__table_name(ADC_CalTable)\
__ch(CH_VOLT_ADC_OVP, 2, CH_VOLT_ADC_OVP)\
__p(500, 100)\
__p(3500, 800)\
__pend()\
__ch(CH_VOLT_ADC_PACK, 2, CH_VOLT_ADC_PACK)\
__p(-100, 10)\
__p(0, 3276)\
__p(200, 5000)\
__pend()\
__end()\
__table_name(DAC_CalTable)\
__ch(CH_SET_CC1_10A, 2, CH_SET_CC1_10A)\
__p(500, 100)\
__p(4000, 1000)\
__pend()\
__end()
自动生成通道参数
依据宏列表的格式,编写宏展开规则
这里通道采用枚举来定义
提取宏列表中的通道信息,定义枚举项
其余宏列表参数,宏展开为空
/* 注册参数全为空 */
/* 注册通道的枚举 */
生成枚举:
/* 定义各个通道的枚举 */
CAL_TABLE(TABLE_NAME_REG, CHANNEL_ENUM_REG, __NULL, __NULL, END_ENUM_CFG);
宏展开如下,该枚举可作为结构体数组的索引:
enum E_ADC_CalTable_t{
CH_VOLT_ADC_OVP,
CH_VOLT_ADC_PACK,
};
enum E_DAC_CalTable_t{
CH_SET_C1_10A,
};
自动生成结构体数组
这里我们要生成多个“CalPoint_t”类型的结构体数组:
/* 注册独立的校准通道 */
static CalPoint_t _ch##_Table[] = {
生成“CalPoint_t”类型的结构体数组:
/* 定义各个通道的校准点 */
CAL_TABLE(__NULL, CHANNEL_POINT_REG, POINT_CFG, POINT_END, __NULL);
自动生成复杂的结构体数组
/* 注册校准表格 */
static CalTable_t _t##Item[] = {
{_ch##_ADDR,\
_n,\
GET_POINT_NUM(_ch##_Table),\
_ch,\
_lch,\
_ch##_Table},
生成"CalTable_t"结构体数组:
/* 定义校准表格 */
CAL_TABLE(TABLE_REG, CHANNEL_REG, __NULL, __NULL, END_CFG);
宏展开如下:
static CalTable_t ADC_CalTableItem[] = {
{
CH_VOLT_ADC_OVP_ADDR, /* 存储地址 */
2, /* 校准点的大小 */
2, /* 校准点数 */
CH_VOLT_ADC_OVP, /* 通道 */
CH_VOLT_ADC_OVP, /* 联动通道 */
CH_VOLT_ADC_OVP_Table}, /* 校准点 */
{
CH_VOLT_ADC_PACK_ADDR,
2,
2,
CH_VOLT_ADC_PACK,
CH_VOLT_ADC_PACK,
CH_VOLT_ADC_PACK_Table},
};
static CalTable_t DAC_CalTableItem[] = {
{
CH_SET_CC1_10A_ADDR,
2,
1,
CH_SET_CC1_10A,
CH_SET_CC1_10A,
CH_SET_CC1_10A_Table},
};
生成结构体数组、索引下标
利用宏列表,生成:
“CalObj_t”类型的结构体数组
该数组的索引下标,枚举形式
/* 注册表格的枚举 */
/* 注册用户校准表格 */
{GET_CH_NUM(_t##Item), _t##Item},
生成表格的枚举:
/* 定义校准表格的通道 */
typedef enum
{
CAL_TABLE(TABLE_ENUM_REG, __NULL, __NULL, __NULL, __NULL)
}E_CalTable_t;
生成用户校准表格:
/* 用户校准表格控制块。结构已固定 */
static CalObj_t __userCalTable[] = {
CAL_TABLE(TABLE_USER_REG, __NULL, __NULL, __NULL, __NULL)
};
宏展开如下:
static CalObj_t __userCalTable[] = {
{2, ADC_CalTableItem},
{1, DAC_CalTableItem},
};
总结
为了满足程序需求,我们定义了许多数据,这些分散且独立的数据非常不好维护。采用宏列表:
接口统一注册
用宏展开自动定义数据
可避免细微误动作埋下的逻辑BUG
后期能快速增加、删减数据,灵活度高
宏列表不好的地方也很明显,框架固化,宏定义设计繁琐。
-END-