嵌入式C语言宏配置的各种技巧!

李肖遥 2023-03-20 22:01
    关注、星标公众号,直达精彩内容

来源:https://blog.csdn.net/lin_strong/article/details/102626503



前言

在项目中,我们经常会需要针对不同的需求进行不同的配置。

在windows/Linux等大平台下,可能会用到配置文件 ini、xml等。而在嵌入式平台下,可能连文件系统都没有。而且很多时候我们只需要硬编码这些配置进代码里就好,不需要在运行时更改。比如每台设备的设备信息等,在整个生命周期中是不会变的。所以并不需要用那么灵活的配置文件。

下面我就带大家游览一下C语言的宏配置相关技术,其可以实现灵活的代码裁剪定制。基于自己目前的积累,可能有错误或者遗漏,敬请指出。

故事会时间

假设我们在开发一个设备的项目,简单起见,我们只写出其中一小部分。 主函数就长这样就好了:

main.c:

#include "device.h"

int main(){
  Device_printfMsg();
  return 0;
}

设备的方法简单起见就一个函数,打印自身信息:

device.h

#ifndef _DEVICE_H
#define _DEVICE_H

void Device_printfMsg(void);

#endif

device.c

#include "device.h"
#include 
#include 

static const char *devType = "ABS";
static uint32_t devID = 34;

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s_%u.local\r\n" , devType, devID);
}

这样一个简单的设备就完成了:

但这样实在偶合太严重了。要是现在我多了一台设备,需要多维护一个设备,那最朴实的人肯定就屁颠屁颠的一个个去修改值了。要是偶尔修改一下,而且就几个参数还好,但实际中经常会有多个参数,而且会经常要修改,那直接人工修改就很不靠谱了。

而我第一反应可能会这么搞。

device.c

#include "device.h"
#include 
#include 


#if 0
static const char *devType = "ABS";
static uint32_t devID = 34;
#else
static const char *devType = "CBA";
static uint32_t devID = 33435;
#endif

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s_%u.local\r\n" , devType, devID);
}

这是快速切换技术,这样我只要修改#if后面为1或0就能快速切换不同配置:

观察代码发现,冗余的代码有点多,而且比如那个DomainName,很可能代码其他地方还会经常用到,这样把它的格式放在printf的格式字符串里就很不合适了,我们需要单独为它分配个字符串。于是整理之后就变成了这样。

#include "device.h"
#include 
#include 

#if 0
#define DEV_NAME    ABS
#define DEV_ID      34
#else
#define DEV_NAME    CBA
#define DEV_ID      33435
#endif

#define _STR(s)  #s
#define MollocDefineToStr(mal)  _STR(mal)

static const char devType[] = MollocDefineToStr(DEV_NAME);
static uint32_t devID = DEV_ID;
static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local";

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

不用看了,运行结果和上面那个一模一样。

#define 就是宏定义,都在看宏配置技巧了应该其实是不需要解释宏在干什么了。但是要强调的是,宏的作用是文本替换,注意是文本,预处理器并不认得变量不变量的,它只知道见到之前定义过的宏,就直接替换文本。

所以:

static uint32_t devID = DEV_ID;

这句其实经过预处理后就是:

static uint32_t devID = 33435;

我们看到其中MollocDefineToStr这个宏很有意思,这对宏是用于把宏展开后的值作为字符串的。

预处理后,

static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local";

这句就会变成:

static const char devDName[] = "CBA"   "_"   "33435"   ".local";

然后由于C语言里连续的字符串不分割的话会自动合并,上面这就相当于

static const char devDName[] = "CBA_33435.local";

接下来又来了一台设备。我忍,扩充下快速切换,弄成多路分支的那种。

#include "device.h"
#include 
#include 

#define DEV_ABS 1
#define DEV_CBA 2
#define DEV_LOL 3

// 选择当前的设备
#define DEV_SELECT  DEV_LOL

#if (DEV_SELECT == DEV_ABS)
#define DEV_NAME    ABS
#define DEV_ID      34
#elif(DEV_SELECT == DEV_CBA)
#define DEV_NAME    CBA
#define DEV_ID      33435
#elif(DEV_SELECT == DEV_LOL)
#define DEV_NAME    LOL
#define DEV_ID      1234
#else
#error "please select current device by DEV_SELECT"
#endif

#define _STR(s)  #s
#define MollocDefineToStr(mal)  _STR(mal)

static const char devType[] = MollocDefineToStr(DEV_NAME);
static uint32_t devID = DEV_ID;
static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local";

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

这样每次这样在 #define DEV_SELECT 那修改一下对应的设备就好了,其实可读性还不错。

那句#error确保了你不会遗忘去配置它,因为如果你配置了个错误的值,预处理器会直接报错。

img

这时候,一般来说我会把配置相关的移到头文件中,就变成了这样:

device.h

#ifndef _DEVICE_H
#define _DEVICE_H

#define DEV_ABS 1
#define DEV_CBA 2
#define DEV_LOL 3

#ifndef DEV_SELECT
#define DEV_SELECT DEV_ABS
#endif

#if (DEV_SELECT == DEV_ABS)
#define DEV_NAME    ABS
#define DEV_ID      34
#elif(DEV_SELECT == DEV_CBA)
#define DEV_NAME    CBA
#define DEV_ID      33435
#elif(DEV_SELECT == DEV_LOL)
#define DEV_NAME    LOL
#define DEV_ID      1234
#else
#error "please select current device by DEV_SELECT"
#endif

void Device_printfMsg(void);

#endif

device.c

#include "device.h"
#include 
#include 

#define _STR(s)  #s
#define MollocDefineToStr(mal)  _STR(mal)

static const char devType[] = MollocDefineToStr(DEV_NAME);
static uint32_t devID = DEV_ID;
static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local";

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

这样,这些配置参数就对其他include了这个头文件的文件是可见的了。

至于那句

#ifndef DEV_SELECT
#define DEV_SELECT DEV_ABS
#endif

这句可有个大好处,所有你想要拥有默认参数且想要在不同工程中都可以定制的地方都可以这么写。这样,在编译器选项中定义宏,就可以用同一套源码为不同项目生成项目定制代码。

比如在VS中可以在解决方案资源管理器中的项目条目上右键->属性,打开项目的属性页,在 C/C++ ->预处理器->预处理器定义 中定义宏

CodeWarrior中则是在Edit->Standard Settings里

当然,有一点点问题就是这样搞没法使用像前面类枚举那种方法来给宏赋值宏,得直接赋值数字、字符串等。

接下来。what!?还要加设备,这样下去不行!一堆#if#else会搞死人的。要是我几十W个设备,难道一个.h文件就几十万行么?我得把配置信息独立出来!

建立一个随便什么名字,甚至随便什么扩展名的文件,扔进工程文件夹,就随便起个名字叫DEVINFO.txt得了。

DEVINFO.txt

// 设备配置信息模板,根据具体设备配置

// 设备名,字符串
#define DEV_NAME    DEFAULT
// 设备ID,U32
#define DEV_ID      0

然后修改device模块:

device.h

#ifndef _DEVICE_H
#define _DEVICE_H

#ifndef DEVINFO_FILENAME
#define DEVINFO_FILENAME DEVINFO.txt
#endif

void Device_printfMsg(void);

#endif

device.c

#include "device.h"
#include 
#include 

#define _STR(s)  #s
#define MollocDefineToStr(mal)  _STR(mal)

#include MollocDefineToStr(DEVINFO_FILENAME)

static const char devType[] = MollocDefineToStr(DEV_NAME);
static uint32_t devID = DEV_ID;
static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local";

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

完美,设备相关信息全部都从外面的txt文件中读出来了,而且这个文件的文件名还是由刚刚才提到的可工程定制的宏配置的方式给出的。我们可以把其他几个设备的配置信息文件都补上。

// 设备名,字符串
#define DEV_NAME    ABS
// 设备ID,U32
#define DEV_ID      34
// 设备名,字符串
#define DEV_NAME    CBA
// 设备ID,U32
#define DEV_ID      33435
// 设备名,字符串
#define DEV_NAME    LOL
// 设备ID,U32
#define DEV_ID      1234

好了,这样我们只要为所有设备各建立一个TXT的信息表,然后当需要切换不同的设备时就用前述方法改一下宏配置切换不同的文件名就好了。

要明白这个方法为什么能起作用,关键是要理解这一句:

#include MollocDefineToStr(DEVINFO_FILENAME)

我们知道,经过预处理器后,这一句就会变为

#include "DEVINFO.txt"

也许你会想:这是什么鬼,还可以include txt文件?我之前见得怎么都是include .h文件呀。 这是一个大大的误区。其实include从来没规定说一定要.h文件,其实可以是任何名字的,这个预处理器指令干的事情就是把include的文件不断递归的文本展开而已。

所以其实上面这句在经过预处理器后会被直接文本替换为对应的文件的内容,一字不差的那种。可能前后会加点注释信息。

所以这种成组绑定、十分固定的配置信息就很适合用这种方式解耦到不同的配置文件中去,按需导入即可。更进一步的,应该要专门为这些配置文件建一个文件夹进行管理。

而对于那种经常会独立更改的配置呢?

一两个的话可以通过之前说的预处理器宏定义的方式来搞定,但是一个稍微有点规模的项目总会涉及到好多好多的配置参数,这个时候就不适合都写在编译器选项里了。这个时候我会专门建一个工程配置文件,比如就叫app_cfg.h,然后把整个工程中可能用到的宏配置都汇总在这里方便修改,这时之前那种可工程定制的宏写法就特别管用了:

app_cfg.h

#define DEVINFO_FILENAME  DEVINFO_CBA.txt
// 其他宏配置选项
 ...

然后,就需要用到强制包含文件这个技巧了,相当于在所有的.c文件前面都直接加一行

#include "app_cfg.h"

这是VS2012中的:

这是CodeWarrior中的

然后就可以很愉快的在一个文件中操控整个工程了!

那我现在又来需求了,ID是有限制的,不能超过5000。那我就这么改。在

#include MollocDefineToStr(DEVINFO_FILENAME)

下面加一句:

#if(DEV_ID > 5000)
#error "device ID shouldn't bigger than 5000"
#endif

那这样,当我们选取CBA时就没法通过编译了

img

还可以通过

#ifndef DEV_ID
#error "DEV_ID lost"
#endif

检查DEV_ID是否正确进行了宏定义,或如果想要组合的条件:

#if !defined(DEV_NAME) || !defined(DEV_ID)
#error "DEV_NAME or DEV_ID malloc define lost"
#endif

然后比如某个设备需要进行代码定制处理,一种方法是在代码中直接写语句进行判断当前设备的名字之类的然后执行对应特定语句。但为了节约编码出来的代码量,同时也是为了体现宏的威力,我们同样可以用预处理指令,遗憾的是,我们没法在预处理器指令中判断字符串,但是可以判断数字,正好我们有ID可以用,所以比如我们要让设备ABS多输出一行hahaha,那代码就被改成了这样

void Device_printfMsg(void){
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
#if(DEV_ID == 34)
  printf("hahaha\r\n");
#endif
}

记住,这些预处理指令的本质都是在替换文本,所以,只有ABS设备时才有这一行代码,对其他设备来说压根没有见到这行代码。

当然,你可以尝试用之前那个include的方法以及其他宏方法来进一步组合定制代码,这是一项创造性工作。

最后突然又想起来一个妙招。也是我最近代码里一直在用的,

我专门搞了一个DebugMsg.h,大概长这样:

#ifndef _DEBUG_MSG_H
#define _DEBUG_MSG_H
#include 
#ifdef _DEBUG
  #define _dbg_printf0(format)                   ((void)printf(format))
  #define _dbg_printf1(format,p1)                ((void)printf(format,p1))
  ……
#else
  #define _dbg_printf0(format)
  #define _dbg_printf1(format,p1)
  ……
#endif
#endif

这样,所有各个模块中只要引用了这个文件就可以用统一的接口输出调试信息,只要我在主配置文件中定义_DEBUG,所有调试printf就会变成真实的printf,否则就是空语句,无调试信息:

#include "DebugMsg.h"
void Device_printfMsg(void){
  _dbg_printf0("Device_printfMsg called.\r\n");
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

那我想要使用这个接口,却又想要为我的device模块单独设一个开关怎么办呢? 整个逻辑简单来说就是,_DEBUG是主开关,其关了所有模块的调试信息都关了,然后各个模块再有各自的开关,必须和_DEBUG一起都被定义才会使这个模块有调试信息。

那我这个模块就改成了这样。

device.h

#ifndef _DEVICE_H
#define _DEVICE_H

// malloc define _DEVICE_DEBUG to enable debug message
// #define _DEVICE_DEBUG

#ifndef DEVINFO_FILENAME
#define DEVINFO_FILENAME DEVINFO.txt
#endif

void Device_printfMsg(void);

#endif

device.c

……
#ifndef _DEVICE_DEBUG
#undef _DEBUG
#endif
#include "DebugMsg.h"

void Device_printfMsg(void){
  _dbg_printf0("Device_printfMsg called.\r\n");
  printf("Device: %s\r\n" , devType);
  printf("DevID: %u\r\n" , devID);
  printf("DomainName: %s\r\n" , devDName);
}

这样,我只有同时宏定义_ DEVICE_DEBUG和_ DEBUG时_dbg_printf0才会被宏定义为printf,否则会被宏定义为空语句,也就没有调试信息了。

这是怎么回事呢? 当预处理器读到#ifndef _ DEVICE_DEBUG这句发现未宏定义_ DEVICE_DEBUG时,它会在下一句取消_ DEBUG的宏定义,这样不管我实际有没宏定义_ DEBUG,当到了#include "DebugMsg.h"并展开后,预处理器都会认为未定义_ DEBUG,所以就会把_dbg_printf0宏定义为空语句,然后就实现了这个串联的逻辑。

后记

好啦,已经讲够多的了,相信你看得也很过瘾。想要再深一步,可以专门看看C语言宏的一些高阶用法。

比如这个(随便百度的,不是打广告且不负任何责任): https://www.jianshu.com/p/490fed500b00

下次看见哪个库里头到处乱飞的宏配置,不会那么一脸懵逼了吧(# ^ . ^ #)

版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“加群”按规则加入技术交流群。


点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

李肖遥 公众号“技术让梦想更伟大”,作者:李肖遥,专注嵌入式,只推荐适合你的博文,干货,技术心得,与君共勉。
评论 (0)
  • 低温型产品概述:霍尔效应测试仪由电磁铁、电磁铁电源、高精度恒流源、高精度电压表、霍尔效应样品支架、标准样品、高低温杜瓦,控温仪,系统软件组成。为本仪器系统专门研制的JH10效应仪将恒流源,六位半微伏表及霍尔测量复杂的切换继电器——开关组装成一体,大大减化了实验的连线与操作。JH10可单独做恒流源、微伏表使用。用途:用于测量半导体材料的载流子浓度、迁移率、电阻率、霍尔系数等重要参数,而这些参数是了解半导体材料电学特性必须预先掌控的,因此霍尔效应测试系统是理解和研究半导体器件和半导体材料电学特性必*
    锦正茂科技 2023-06-09 13:16 138浏览
  • CS5466支持dsc1.1/12a压缩视频传输,是一款Type-C转HDMI8K30HZ或者4K144HZ方案芯片,Type-C/DP1.4转HDMI2.1的显示协议转换芯片, 内部集成了PD3.0及DSC decoder.CS5466电路原理图参考:CS5466芯片产品参数特性:1. Type-C/DP(2lanes)to HDMI2.1 8K30或者4K144产品。2. 支持HDMI2.1 FRL。3. 集成DSC1.2a decoder。4. DSC支持RGB, YCbCr4:4:4,
    QQ1540182856 2023-06-09 09:52 178浏览
  • 前段时间出了接近一个月的差,没来得及及更新试用报告,有点不好意思,今天抽空过来写一下自己的看书的心得以及对于整个书籍的一些认知和看法,希望对大家能够有一定的帮助,也希望可以和大家一起探讨进步。以前自己都是使用的Altium Designer做开发设计的,大学的时候就开始接触,作为个人爱好延续至今,对于PADS也是有所耳闻,只是一直没有机会来了解,根据我个人的经验来看,按照以前使用Altium 的经验来说,PADS设计指南 无论说是从流程步骤上以及类容的细致程度上都还是很不错的,从设计流程、原理图
    君莫笑啊 2023-06-08 11:21 216浏览
  • 近年来,伴随着智慧化港口的大潮流,经纬恒润L4高级别智能驾驶业务产品也陆续扎根港口自动驾驶多个项目中,帮助港口实现无人水平运输自动化,达到降本增效的效果,助力客户实现智慧化绿色港口。   在整个港口水平运输场景中,经纬恒润提供了端到端的车、路、网、云、图全栈式自研解决方案,包含自动驾驶系统、路侧车路协同、基于5G网络的远程遥控驾驶、车队调度管理平台、数字孪生、仿真系统、高精地图等专业模块,组成了一套完整的智慧港口解决方案。本篇专门介绍其中的自动驾驶系统。  
    hirain 2023-06-09 11:29 189浏览
  • 近期有点全身心投入到了嵌入式驱动的开发意思了,起早贪黑的学习。不过也是,人生的路都是在不断地学习中度过的。对于干了几年的硬件工程师而言,不说硬件是不是很牛了,就是想换换脑子,整天三极管、电阻、电容的,确实让人乏味。思来想去,硬件是软件的基座,驱动是软件沟通硬件的桥梁。倒不如自己整点知识,也方便自己以后调试硬件不是,再说了从软件角度去理解硬件思维,会有很多不同的收获不是。 奋战了一个月,倒是把驱动的基本框架了解七七八八了,兴致使然,图像采集感觉还不错,公司有产品当开发板,也是省下了大部分的学
    二月半 2023-06-08 12:09 692浏览
  • 苹果如何重新定义AR?在如今以智能手机为主的消费电子市场下行阶段,市场急需开辟一个新的领域带来新的增长点,以往被寄予厚望的VR/AR等头显设备在经历了数年发展后,依旧难堪大任,业界都把希望寄托在苹果身上。简单来说,Vision Pro本质上其实还是VR设备,不过所有操作界面可以结合头显摄像头捕捉的外界环境,在头显内部显示出来,即一款数字内容无缝融入真实世界的VR显示设备。同时Vision Pro的操作方式无需手柄,完全通过眼睛、双手和语音,通过苹果为Vision Pro打造的空间操作系统Visi
    华秋商城 2023-06-08 10:32 150浏览
  • 增加电池寿命的秘诀 1.新买的电车要先充满几次吗?把电车电池完全充满这个操作,在专业上叫锂电池化成,是电车在出厂之前激活电池的一道工序,车主完全没必要这样做。《汽车大数据应用研究报告》里明确指出充放电深度是表征电池健康度的重要参数,充放电深度增加,释放电量变大,使电池的健康度衰减非常明显。所以在日常用车的时候我们尽量把电池的电量维持在20%~80%之间,这样能显著提升电池的使用寿命。1. 电池寿命会受温度影响吗?锂电池的理想工作温度为25摄氏度,工作温度过高或者过低都会引发电
    四川英特丽科技有限公司 2023-06-08 10:42 186浏览
  • MSDS中干电池、铅酸蓄电池、锂电池正负极材料介绍191-0751-6775一、干电池干电池也叫锰锌电池,所谓干电池是相对于伏打电池而言,所谓锰锌是指其原材料。针对其它材料的干电池如氧化银电池,镍镉电池而言。锰锌电池的电压是15V。干电池是消耗化学原料产生电能的。它的电压不高,所能产生的持续电流不能超过1安培。锌锰干电池:正极材料:锰、石墨棒负极材料:锌镁锰干电池:正极材料:二氧化锰粉、氯化铵及碳黑组成的一个混合糊状物负极材料:镁筒锌空气电池:正极材料:用活性炭吸附空气中的氧或纯氧作为正极活性物
    陈丽莎 2023-06-09 16:43 150浏览
  • 最近在使用串口读一些数据,但是总会出现些发、送之间的冲突问题,为了弄清楚问题的所在,于是产生了想法,做了一个日志保存。[code]void Widget::SaveLogTxt(QString dat ){ QDateTime currenttime = QDateTime::currentDateTime(); QString strDate = currenttime.toString("yyyy/MM/dd"); QString strTime = currenttime
    E_ARM 2023-06-09 10:31 176浏览
  • 半导体制冷片是电子器件中重要的辅助元件,用于控制器件的温度,从而保证器件的稳定性和可靠性。在半导体制冷片的制造过程中,半导体制冷片的基板材料选择是非常关键的,因为基板材料的性能会直接影响到制冷片的性能。同时作为精密制冷片新型技术,对陶瓷基板的要求也高于普通基板。1.外观要求:严格的铜面平整度,粗糙度要求控制在0.5um以内,铜面上不允许有凹坑、铜颗粒、氧化、任何形式的外观划伤等。2.尺寸要求:完成板厚控制公差在10-20um以内,而陶瓷板材的来料公差就有±30un公差,这就意味着需要挑选公差范围
    斯利通陶瓷电路板 2023-06-08 11:50 199浏览
  • 电源适配器CE认证标准测试项目,电子产品现在用的是相当的广,常见的产品就一大堆,比如说手机电脑等都会使用到电源适配器。电源适配器适用范围很广,不仅在移动设备端,在其它领域也会应用到。电源适配器CE认证,一般会做CE认证中的低电压指令LVD和电磁兼容指令EMC,欧洲能效认证ERP,RoHS等。下面具体来看看认证这么做吧。电源适配器为什么要做CE认证?CE认证制度下的LVD低电压指令涵盖了交流50V-1000V,直流75V-1500V的所有带电产品,EMC指令涵盖了所有有电路板产生电磁辐射的带电产品
    陈丽莎 2023-06-08 14:09 239浏览
  • 在过去的20年,传感器厂商不断研究创新的测量原理和敏感材料,这些成果能让我们用到高集成、低成本的传感器,其中,最成功也是最具颠覆性的,无疑是MEMS技术在传感器制造中的应用。MEMS技术在传感器的大规模应用,让传感器的小型化、低功耗、智能化成为可能,从而推动了传感器在物联网、消费电子、汽车电子等领域的广泛应用,促进了数字经济的发展和智能时代的到来。可以说,在过去20年,MEMS颠覆和扩展了传感器。传感器专家网https://www.sensorexpert.com.cn专注于传感器技术领域,致力
    传感器专家网 2023-06-08 19:28 204浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦