宏定义X-MACRO的高级应用(高阶版)

1. 结构体序列化问题

通常情况下,对于模块或者设备之间通信,往往是以字节流的方式来传输,而模块内部却要将这些字节流按某种数据结构来处理。这就存在,如何将数据结构(结构体)转换(序列化)成字节流,已经如何将字节流转换(反序列化)成数据结构的方法。

这不是很简单么?

tStrcut s;
unsigned char buff[100];
memcpy(buff, &s, sizeof(s)); // serial
memcpy(%s, buff, sizeof(s)); // de-serial

似乎,这也没什么毛病,非要挑毛病,那也只是结构体对齐的一点点问题。对于我们追求卓越的程序猿,还有什么更好的方法呢?

1.1 使用X-MACRO来构建

之前,我们学习了X-MACRO,何不用一下?

首先,我们定义一个宏:

#define EXPAND_STAR \
    EXPAND_STAR_MEMBER(x, int) \
    EXPAND_STAR_MEMBER(y, int) \
    EXPAND_STAR_MEMBER(z, int) \
    EXPAND_STAR_MEMBER(radius, double)

然后,在用这个宏定义结构体:

typedef struct
{
#define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;
EXPAND_STAR
#undef EXPAND_EXPAND_STAR_MEMBER
} starStruct;

那么这个序列化和反序列化方法可以这样:

void serialize_star(const starStruct *const star, unsigned char *buffer) 
{
  #define EXPAND_STAR_MEMBER(member, type) \
    memcpy(buffer, &(star->member), sizeof(star->member)); \
    buffer += sizeof(star->member);
  EXPAND_STAR
  #undef  EXPAND_STAR_MEMBER
}

void deserialize_star(starStruct *const star, const unsigned char *buffer) 
{
  #define EXPAND_STAR_MEMBER(member, type) \
    memcpy(&(star->member), buffer, sizeof(star->member)); \
    buffer += sizeof(star->member);
  EXPAND_STAR
  #undef  EXPAND_STAR_MEMBER
}

如何打印结构体元素呢?元素中有intdouble,打印方式是不一样的,于是,我们可以这样做:

#define print_int(val) printf("%d", val)
#define print_double(val) printf("%g", val)

void print_star(const starStruct *const star)
{
    /* print_##type will be replaced with print_int or print_double */
#define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    printf("%s: ", #member);                    \
    print_##type(star->member);                 \
    printf("\n");

    EXPAND_STAR
#undef  EXPAND_STAR_MEMBER
}
1.2 用#include来改善

我是不满足于此的,至少,我对宏定义后面的反斜杠\看着不顺眼,总是有种强迫症要把它删掉。仔细想想,宏定义的各种奇技淫巧,实际上有部分内容是可以放在头文件(或者被一个#include)的文件。如以下内容是可以放在一个叫xmacro.def的文件:

// xmacro.def
    EXPAND_STAR_MEMBER(x, int)
    EXPAND_STAR_MEMBER(y, int)
    EXPAND_STAR_MEMBER(z, int)
    EXPAND_STAR_MEMBER(radius, double)
#undef EXPAND_STAR_MEMBER

什么?.def后缀也行?如果你真的有这样的疑问,你肯定没认真看我的《基于C99规范,最全C语言预处理知识总结》,不仅可以命名成xmacro.def,我还可以将它命名成xmacro.fxxk你信不信?

好了,在使用这些宏定义的.c文件中,这样用:

typedef struct
{
#define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;
#include "xmacro.def"
} starStruct;

其他的EXPAND_STAR#undef EXPAND_STAR_MEMBER都可以替换成#include "xmacro.def"

在这个案例中,这个更改似乎没看出什么好处。别急,后面再举一个例子给你感受下include的威力!

对于序列化的这例子,满意么?No!还有可以优化的地方。

来,从上面的打印元素的函数print_star入手,这次要发挥两个井号##的作用。

#define FORMAT_(type) FORMAT_##type
#define FORMAT_int    "%d"
#define FORMAT_double "%g"

void print_star(const starStruct *const star) 
{
  /* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    printf("%s: " FORMAT_(type) "\n", #member, star->member);

#include "star.def"
}

是不是很好玩?还有呢,继续搞下去。

1.3 用可变参数__VA_ARGS__来改善

你知道宏定义中的不定参数...怎么玩么?

#define EXPAND_STAR \
    EXPAND_STAR_MEMBER(x, int) \
    EXPAND_STAR_MEMBER(y, int) \
    EXPAND_STAR_MEMBER(z, int) \
    EXPAND_STAR_MEMBER(radius, double)

可以改成:

#define EXPAND_STAR(_, ...) \
    _(x, int, __VA_ARGS__) \
    _(y, int, __VA_ARGS__) \
    _(z, int, __VA_ARGS__) \
    _(radius, double, __VA_ARGS__)

这个__VA_ARGS__就是针对...的,详细使用方法见《基于C99规范,最全C语言预处理知识总结》。

1.4 源码

那么以上的代码可以更改成:

#include <stdio.h>
#include <string.h>
/*
 Generic
 */
#define STRUCT_MEMBER(member, type, dummy) type member;

#define SERIALIZE_MEMBER(member, type, obj, buffer)      \
    memcpy(buffer, &(obj->member), sizeof(obj->member)); \
    buffer += sizeof(obj->member);

#define DESERIALIZE_MEMBER(member, type, obj, buffer)    \
    memcpy(&(obj->member), buffer, sizeof(obj->member)); \
    buffer += sizeof(obj->member);

#define FORMAT_(type) FORMAT_##type
#define FORMAT_int "%d"
#define FORMAT_double "%g"

/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
#define PRINT_MEMBER(member, type, obj) \
    printf("%s: " FORMAT_(type) "\n", #member, obj->member);

/*
 starStruct
 */

#define EXPAND_STAR(_, ...) \
    _(x, int, __VA_ARGS__)  \
    _(y, int, __VA_ARGS__)  \
    _(z, int, __VA_ARGS__)  \
    _(radius, double, __VA_ARGS__)

typedef struct
{
    EXPAND_STAR(STRUCT_MEMBER, )
} starStruct;

void serialize_star(const starStruct *const star, unsigned char *buffer)
{
    EXPAND_STAR(SERIALIZE_MEMBER, star, buffer)
}

void deserialize_star(starStruct *const star, const unsigned char *buffer)
{
    EXPAND_STAR(DESERIALIZE_MEMBER, star, buffer)
}

void print_star(const starStruct *const star)
{
    EXPAND_STAR(PRINT_MEMBER, star)
}

2. EEPROM、FLASH等Memory条目结构化访问
2.1 Memory访问问题

像EEPROM、Flash(包含MCU内部的CodeFlahs、DataFlash,以及专门的外设NorFlash、NandFlash)这样的存储空间,有Block、Section等概念。MCU访问其时,需要按照某种约定去访问,也就是说,不能想在任意一个位置写任意长度的数据。例如,如果DataFlash的一次写单位是8个字节的一个block,那么MCU就必须每次写8字节倍数大小的数据内容,而不能随意写内容到一个block中的特定位置。这些就导致在软件设计上诸多不便。

那么,我们是否有方法,将这些内存空间预先规划好,上层软件通过特定接口访问这些分好区域的空间?

2.2 一般想法

遇到这个问题,通常我们惯性地想,将指定内容到指定地址不就可以了?

以一个block为8字节大小的DataFlash举例,其起始地址为0x00008000。

#define DATA1_ADDR	0x00008000
#define DATA2_ADDR	0x00008008
#define DATA3_ADDR	0x00008010
#define DATA4_ADDR	0x00008020

然后再限定每个数据的长度

#define DATA1_LEN	0x00008000
#define DATA2_LEN	0x00008008
#define DATA3_LEN	0x00008010
#define DATA4_LEN	0x00008020

似乎,这也能玩,但是每次改起来不嫌累么?而且也不好维护啊,换一个IC怎么搞?

2.3 使用X-MACRO

按照上面的结构体系列化例子的方法,我们尝试使用X-MACRO将Memory结构化。

以下是一些设想:

  1. 将Memory的物理地址映射到自定义逻辑地址
  2. 逻辑地址按Memory的Block对齐,逻辑地址从0开始
  3. 用户数据按逻辑地址分配
  4. 应用接口按实际内容大小操作
  5. 底层接口根据逻辑地址对齐读写Memory
#define ALL_ENTRIES()		\
	ENTRY(L_ADDR_DATA1, 8)	\
	ENTRY(L_ADDR_DATA2, 8)	\
	ENTRY(L_ADDR_DATA3, 16)	\
	ENTRY(L_ADDR_DATA4, 16)	

然后通过ENTRY定义逻辑地址

#define ENTRY(addr, len)	addr

再定义长度

#define ENTRY(addr, len)	len

呃……好像不对劲,这个逻辑地址还是要人为定义才行。

发散下思维,我们或许可以将ENTRY(addr, len)中的addr定义为enum,当做是条目ID,而逻辑地址通过其他方式实现(结构体?)。

那,我们将上面的内容改一下:

#define ALL_ENTRIES()	\
	ENTRY(ID_DATA1, 8)	\
	ENTRY(ID_DATA2, 8)	\
	ENTRY(ID_DATA3, 16)	\
	ENTRY(ID_DATA4, 16)	

第一步,定义ID

#undef ENTRY
#define ENTRY(id, len)	id,

typedef enum
{
    ALL_ENTRIES()
    MEM_ID_MAX
}MEM_ID;

第二步,定义逻辑地址(定义结构体)

我们可以,用一个结构体来map其内存逻辑地址

#undef ENTRY
#define ENTRY(id, len)	unsigned char mem_##id[len],

struct MemAddrAlign
{
    ALL_ENTRIES()
};

第三步,定义逻辑地址(定义逻辑地址数组)

再通过结构体的元素offset来取其元素的位置

#define offsetof(s,m) ((int)((char*)&((s*)0)->m))

#undef ENTRY
#define ENTRY(id, len)	offsetof(struct MemAddrAlign,mem_##c),

const unsigned int mem_entry_logic_addr[MEM_ID_MAX]= 
{
    ALL_ENTRIES()
};
    

第四步,定义数据长度

#undef ENTRY
#define ENTRY(id, len)	len,

const unsigned int mem_entry_size[MEM_ID_MAX]= 
{
    ALL_ENTRIES()
};
    

最后,我们就可以通过数据块定义的ID(即enum)来对mem_entry_logic_addrmem_entry_size的取下标访问来操作Memory了。

等等,这里好像还有个问题,ALL_ENTRIES()里面定义的长度不是按block 8字节对齐,怎么办?毕竟每个Entry都要求定义成8字节的倍数,似乎并不友好。

2.4 方法改进
2.5 终极完善
2.6 完整源码

部分内容已省略。

请关注公众号“嵌入式软件实战派”获得完整文章和相关源码。

另见:宏的高级用法——X-MACRO

扫码关注

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值