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

2023-05-16

来源: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 <stdint.h>
#include <stdio.h>

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 <stdint.h>
#include <stdio.h>


#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 <stdint.h>
#include <stdio.h>

#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 <stdint.h>
#include <stdio.h>

#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 <stdint.h>
#include <stdio.h>

#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 <stdint.h>
#include <stdio.h>

#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 <stdio.h>
#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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式 C 语言宏配置的各种技巧 的相关文章

  • 动态内存管理及防御性编程

    概述 xff1a C语言的优势是可以直接访问内存地址 xff0c 也就是指针操作 xff0c 但其缺陷也是因为直接内存访问 如何通过防御性编程提前发现问题 xff0c 尽可能减少内存异常产生的后果 xff0c 就是本文的重点 1 内存划分
  • 基于RTOS的软件开发理论

    文章目录 1 RTOS的特点2 任务设计2 1 任务的特性2 2 任务划分的方法2 2 1 设备依赖性任务2 2 2 关键任务2 2 3 紧迫任务2 2 4 数据处理任务2 2 5 触发条件相同的任务2 2 6 运行周期相同的任务2 2 7
  • 面向对象类之间主要的几种关系

    已剪辑自 https mp weixin qq com s ClBuraVUIPhnWceI7m78Xg 嵌入式开发虽然平时C语言用的比较多 xff0c 但面向对象的思维应该是每一位嵌入式软件工程师必备的知识 之前给大家分享过用C语言实现面
  • 世界上最健康的程序员作息表!

    文章目录 7 307 30 8 008 00 8 308 30 9 009 3010 3011 0013 0014 30 15 3016 0017 00 19 0019 3021 4523 0023 30时间 健康的小常识 已剪辑自 htt
  • 30岁了,冒死说几句大实话!

    已剪辑自 https mp weixin qq com s j0yzonrhPPcemDRF6QBVkw 是的 xff0c 我 30 岁了 xff0c 还是周岁 就在这上个周末 xff0c 我度过了自己 30 岁的生日 都说三十而立 xff
  • QT使用QAxObject读取Excel教程-全网最全

    文章目录 一 背景二 介绍基本操作方法获取对象调用动态方法设置和获取属性更多相关 三 使用要求添加模块与excel com连接的方法Excel基本操作 四 具体使用说明五 项目实战实战项目1实战项目2实战项目3实战项目4实战项目5 封装好的
  • 超越内卷-认知差、信息差、时间差

    已剪辑自 https mp weixin qq com s 9pzMQJJnp9ZbkTCVe ao7w 内卷的话题曾经聊过 xff0c 当大家的努力都上不了层次 xff0c 只是原水平重复竞争 xff0c 那么内卷就开始了 最近对这个问题
  • 数十种嵌入式 C 语言代码优化的经验和方法

    文章目录 简介声明哪里需要使用这些方法 xff1f 整形数除法和取余数合并除法和取余数通过2的幂次进行除法和取余数取模的一种替代方法使用数组下标全局变量使用别名变量的生命周期分割变量类型局部变量指针指针链条件执行布尔表达式和范围检查布尔表达
  • 汽车电子国际标准现状与趋势

    已剪辑自 https mp weixin qq com s vLgnrFPtDPglwde1TZUHSQ 在汽车电子系统发展的早期 xff0c 汽车电子基础软件是没有统一标准的 xff0c 各个 OEM Tier1 Tier2 等厂商针对不
  • Linux多线程服务器编程(陈硕)学习总结

    这本书确实是学习多核时代采用现代C 43 43 编写多线程程序的好书 xff0c 下面是学习总结 xff1a 第一章 线程安全的对象生命期管理 对象的创建很简单 xff0c 但是不要在构造期间泄漏this指针 xff0c 比如不要在构造函数
  • 详解 Modbus 通信协议(清晰易懂)

    文章目录 已剪辑自 https mp weixin qq com s dvo1l1GgJ2DtIHnPK5E1tA 本文总结关于 Modbus 相关的知识 xff0c 浅显易懂 xff0c 旨在对 Modbus 有一个很直观的了解 如有错误
  • RTOS应用中的几种调度策略

    从前后台架构的软件开发过渡到使用实时操作系统 RTOS 可能是一项困难的工作 但使用RTOS有许多优势 xff0c 例如简化应用集成 xff0c 支持任务抢占调度 xff0c 当开发人员使用复杂的32位微控制器 xff0c 且可以获取足够的
  • 几款非常棒的使用文本来进行图形化注释的工具

    https mp weixin qq com s NX8feH UPE7oegM7U9W4GA 说明 xff1a 1 程序代码里面非常好的注释方式 2 相关网站 xff1a xff08 1 xff09 https metacpan org
  • 解决Excel打开UTF-8编码CSV文件乱码的问题

    最近在用QT读写CSV文件 xff0c 发现将数据写入到CSV文件中 xff0c 使用记事本打开文件是正常的 xff0c 使用Excel打开 xff0c 中文是乱码的 xff0c 下面把原因和解决方法记录一下 问题产生的原因 为什么exce
  • Windows下查看端口占用情况

    编程的时候经常发现我们需要使用的端口被别的程序占用 xff0c 这个时候需要清楚查看是哪个程序占用了端口 xff0c 用且清除了这个进程 xff01 1 开始 gt 运行 gt cmd xff0c 或者是window 43 R组合键 xff
  • 【C进阶】同事用void把我给秀翻了!

    2 简单认识一下void 今天跟大家介绍的知识是C语言中的void关键字的用法 xff0c void在大部分小伙伴的程序中都只是用于函数无参数传入 xff0c 或者无类型返回 然而我们平时所定义的变量都会有具体的类型 xff0c int x
  • 如何降低代码圈复杂度

    已剪辑自 https mp weixin qq com s biz 61 MzI2MTE4Nzk5MA 3D 3D amp mid 61 2247483685 amp idx 61 1 amp sn 61 26072d6a41ed9abef
  • 嵌入式开发:周期调度和代码执行时间理解

    已剪辑自 https mp weixin qq com s gaT7D1IgkBxxEOj DNaLPw 汽车嵌入开发中 xff0c 我们常常听到这样的名词 xff1a 1ms Task 5ms Task 10ms Task 试问 xff1
  • C语言中,实现函数宏的三种方式

    已剪辑自 https blog csdn net qq 35692077 article details 102994959 1 函数宏介绍 函数宏 xff0c 即包含多条语句的宏定义 xff0c 其通常为某一被频繁调用的功能的语句封装 x
  • 代码是如何控制硬件的?

    已剪辑自 https mp weixin qq com s UDbxTfAMLAWE8LjUiqGUBQ 先说代码 xff1a 我们是用电脑的键盘来输入的指令 xff0c 每一个指令都对应一个ASCII码 xff0c 而这里的ASCII码就

随机推荐