README_条件编译笔记
条件编译笔记1. 这篇笔记讲什么这篇笔记不是泛泛而谈 C 语言预处理器而是结合你当前 STM32 工程里的真实代码来讲清楚什么是条件编译它和普通if判断有什么本质区别你的项目里哪些地方正在使用条件编译这些写法分别解决什么问题后面你自己改工程时哪些宏可以改哪些不要随便动工程路径D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试2. 什么是条件编译条件编译属于C 预处理器preprocessor的工作。一个.c文件在真正编译成目标代码之前会先经过“预处理”阶段。这个阶段会先处理#include#define#undef#if#ifdef#ifndef#elif#else#endif所以条件编译的本质是在“编译之前”先决定某一段代码到底保不保留。也就是说编译器真正看到的代码已经是“预处理之后”的结果了。3. 条件编译和普通if的区别这是最容易混淆的点。3.1 运行时ifif(flag){func();}特点这段代码一定会参与编译程序运行时才判断flag会产生运行时分支开销3.2 编译时#if#ifLOG_LEVEL1UsartPrintf(...);#endif特点预处理阶段就决定保不保留条件不成立时这段代码会直接从源文件里“消失”不产生运行时判断开销一句话总结if运行时选路径#if编译前选代码这也是为什么单片机项目特别喜欢用条件编译节省 Flash 空间减少串口打印去掉不必要的模块控制不同芯片、不同板型、不同编译选项下的代码分支4. 你这个项目里条件编译主要分哪几类结合工程里的实际代码可以大致分为 5 类日志等级控制C / C 兼容包装HAL 模块开关与默认参数兜底芯片型号、硬件能力、启动方式相关分支头文件防重复包含这里我把“头文件防重复包含”故意放到后面讲因为它虽然最常见但如果一上来就讲它反而容易把条件编译的核心原理冲淡。我们先把“真的会改变功能路径”的几类理解透再回来看头文件保护会更顺。5. 日志等级控制这一部分我按你第一次给我那段代码时的讲法重新写得更“拆解式”一点。对应文件D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\bsp_usart.h原代码#ifndefLOG_LEVEL#defineLOG_LEVEL1#endif#ifLOG_LEVEL2#defineLOG_DEBUG(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#else#defineLOG_DEBUG(...)#endif#ifLOG_LEVEL1#defineLOG_INFO(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#else#defineLOG_INFO(...)#endif#defineLOG_WARN(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#defineLOG_ERROR(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)这段代码的目的很简单用LOG_LEVEL控制日志输出等级在编译阶段决定哪些日志代码保留哪些日志代码直接删掉也就是说它不是“运行时判断要不要打印”而是“编译前就决定这行代码存不存在”。5.1#ifndef LOG_LEVEL是什么意思#ifndefLOG_LEVEL#defineLOG_LEVEL1#endif等价理解如果LOG_LEVEL没有被定义那就给它一个默认值1这里的作用是设置默认日志等级。比如如果你在别处没有写#define LOG_LEVEL 2那这里就自动使用1所以它是一种“默认配置兜底”。5.2#if LOG_LEVEL 2是什么意思#ifLOG_LEVEL2#defineLOG_DEBUG(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#else#defineLOG_DEBUG(...)#endif等价理解如果日志等级大于等于2那LOG_DEBUG(...)就真的展开成UsartPrintf(...)否则LOG_DEBUG(...)展开成空这里最关键的是“展开成空”。也就是LOG_DEBUG(value%d\r\n,x);当LOG_LEVEL 2时预处理后会直接变成;你也可以粗略理解成这句日志根本不存在了编译器后面看到的代码里已经没有这条调试打印5.3...和__VA_ARGS__是什么这是你这次最关心的点我们把它拆细一点。例如#defineLOG_INFO(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)这里的...表示“参数数量不固定”__VA_ARGS__表示“把传进来的参数原样代入”这叫可变参数宏。也就是说LOG_INFO后面可以接任意数量的参数。例如LOG_INFO(hello\r\n);LOG_INFO(x%d\r\n,x);LOG_INFO(id%d name%s\r\n,id,name);这三种都合法。它们的展开结果分别类似于UsartPrintf(USART_DEBUG,hello\r\n);UsartPrintf(USART_DEBUG,x%d\r\n,x);UsartPrintf(USART_DEBUG,id%d name%s\r\n,id,name);所以你可以把它记成...负责把所有参数“收起来”__VA_ARGS__负责把收起来的参数“原样倒出去”这个理解非常重要。5.4 为什么日志宏这里必须用...因为日志打印本来就像printf有时候只打一段固定字符串有时候打一段格式串加一个变量有时候打一段格式串加多个变量如果不用可变参数宏你就得写很多版本#defineLOG_INFO0(str)...#defineLOG_INFO1(fmt,a)...#defineLOG_INFO2(fmt,a,b)...这样既丑也难维护。而用了#defineLOG_INFO(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)你就能一直保持和printf一样的调用风格。5.5 这段代码在不同LOG_LEVEL下的效果当LOG_LEVEL 0LOG_DEBUG(...)被展开为空LOG_INFO(...)被展开为空LOG_WARN(...)仍然打印LOG_ERROR(...)仍然打印效果只保留警告和错误日志当LOG_LEVEL 1LOG_DEBUG(...)被展开为空LOG_INFO(...)会打印LOG_WARN(...)会打印LOG_ERROR(...)会打印效果打印普通信息、警告、错误不打印调试细节当LOG_LEVEL 2LOG_DEBUG(...)会打印LOG_INFO(...)会打印LOG_WARN(...)会打印LOG_ERROR(...)会打印效果所有日志都打印5.6 用预处理器展开后的直观结果原代码LOG_DEBUG(debug: %d\r\n,a);LOG_INFO(info: %d\r\n,b);LOG_WARN(warn: %d\r\n,c);当LOG_LEVEL 1预处理后大致变成;UsartPrintf(USART_DEBUG,info: %d\r\n,b);UsartPrintf(USART_DEBUG,warn: %d\r\n,c);当LOG_LEVEL 2预处理后大致变成UsartPrintf(USART_DEBUG,debug: %d\r\n,a);UsartPrintf(USART_DEBUG,info: %d\r\n,b);UsartPrintf(USART_DEBUG,warn: %d\r\n,c);所以它的本质就是编译前替换编译前裁剪代码5.7 这种写法的优点5.7.1 没有运行时开销如果写成if(log_level2){UsartPrintf(...);}那么即使不打印程序运行时也还要判断一次。而条件编译是不满足条件时直接不生成这部分代码所以运行时更省。5.7.2 适合单片机单片机项目通常很关心Flash 空间RAM 空间串口打印耗时实时性日志太多会影响速度条件编译正好适合做裁剪。5.7.3 管理方便只改一处#defineLOG_LEVEL2就能控制整个工程的日志详细程度。5.8LOG_WARN和LOG_ERROR为什么没做条件编译#defineLOG_WARN(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#defineLOG_ERROR(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)说明作者当前的设计意图是WARN和ERROR永远保留不受LOG_LEVEL限制因为这两类信息通常更重要。如果你以后也想让它们受等级控制也可以改成#ifLOG_LEVEL1#defineLOG_WARN(...)UsartPrintf(USART_DEBUG,__VA_ARGS__)#else#defineLOG_WARN(...)#endif5.9 这种写法和函数有什么区别例如你可能会想voidLOG_INFO(constchar*fmt,...){...}宏和函数的区别宏预处理阶段替换可以被条件编译直接裁掉没有函数调用开销函数运行时调用即使想关闭日志函数本身通常还在更容易调试类型检查也更清晰所以在嵌入式里高频日志调试开关通常更喜欢用宏。5.10 这一节你最好记住的三句话你可以把日志等级控制先记成下面这三句#ifndef LOG_LEVEL如果没配置日志等级就给一个默认值。...表示这个宏可以接收不定数量参数。__VA_ARGS__把这些参数原样传给UsartPrintf。再加最后一句总纲这套日志宏不是在运行时决定“打不打印”而是在编译前决定“这行代码还在不在”。6. C / C 兼容包装在这些文件里很常见D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\cJSON.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\usart.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\gpio.h典型写法#ifdef__cplusplusexternC{#endif/* C 函数声明 */#ifdef__cplusplus}#endif6.1 作用如果这个头文件被 C 工程包含C 编译器会自动做名字修饰name mangling。这样会导致C 里实现的函数名和 C 编译器理解的函数名不一致最终链接失败。extern C的作用就是告诉 C这些函数按 C 的方式导出名字不要改名。6.2 为什么要用#ifdef __cplusplus因为在 C 编译器里__cplusplus根本不存在只有 C 编译器才会定义它所以这个条件编译的意思是如果当前是 C 编译就加extern C如果当前是 C 编译就什么都不加这是一种很典型的“跨语言兼容条件编译”。7. HAL 默认参数兜底位于D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\stm32f4xx_hal_conf.h典型写法#if!defined(HSE_VALUE)#defineHSE_VALUE25000000U#endif类似的还有HSI_VALUELSE_VALUELSI_VALUEHSE_STARTUP_TIMEOUTEXTERNAL_CLOCK_VALUE7.1 这类写法在干什么它是在做“有定义就用你的没有就给默认值”。也就是说如果你在工程选项或别的头文件里已经定义了HSE_VALUE那这里就不会重新定义如果你没定义这里才补一个默认值7.2 好处这种写法很适合做“通用配置模板”同一份 HAL 配置文件可以适应不同板子也允许上层工程覆盖默认值7.3 这和普通#define的区别普通#define HSE_VALUE ...是强制赋值。而#if!defined(HSE_VALUE)#defineHSE_VALUE...#endif是“未定义才赋值”更灵活也更安全。8. HAL 模块开关同样主要位于D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\stm32f4xx_hal_conf.h典型形式#ifdefHAL_GPIO_MODULE_ENABLED#includestm32f4xx_hal_gpio.h#endif还有很多类似HAL_RCC_MODULE_ENABLEDHAL_DMA_MODULE_ENABLEDHAL_UART_MODULE_ENABLEDHAL_TIM_MODULE_ENABLEDHAL_SPI_MODULE_ENABLEDHAL_I2C_MODULE_ENABLED8.1 作用这类条件编译是在控制某个 HAL 模块要不要参与编译某个头文件要不要被包含8.2 为什么这么做STM32 HAL 很大如果所有模块都打开编译更慢代码更大依赖更多所以 ST 的设计思路是先定义“启用哪些模块”再按宏去包含对应头文件和功能8.3 对你项目的意义你当前项目主要用到GPIOUARTTIMSPII2C所以 HAL 配置里这些启用宏直接决定底层库参与程度。9. 芯片型号和硬件能力判断这类多见于D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Src\system_stm32f4xx.cCMSIS 头文件和驱动头文件例如常见会看到#ifdefined(STM32F407xx)...#endif或#if(__FPU_PRESENT1)(__FPU_USED1)...#endif9.1 作用根据不同芯片、不同硬件能力决定启动文件怎么配时钟树怎么配向量表位置怎么配FPU 要不要打开某些寄存器功能是否存在9.2 为什么必须用条件编译因为这些差异是“编译前就确定”的不是运行时才知道。比如你用的是STM32F407ZGT6它支持哪些外设、寄存器、FPU这些都取决于芯片型号这不是程序跑起来以后才能判断的事情所以必须用条件编译。10.defined(...)和#ifdef的关系你项目里两种都能看到。10.1#ifdef#ifdef__cplusplus...#endif等价于#ifdefined(__cplusplus)...#endif10.2#ifndef#ifndefLOG_LEVEL...#endif等价于#if!defined(LOG_LEVEL)...#endif10.3 什么时候用哪种经验上判断一个宏是否存在#ifdef/#ifndef更直观多条件组合判断#if defined(...) !defined(...)更灵活所以你会看到简单判断用#ifdef复杂判断用#if defined(...)11. 你的项目里“不是条件编译但容易误以为是”的内容比如很多地方有#definePASSWORD_MAX_COUNT12#defineSCREEN_MODE_FACE_RFID1#defineSERVO_UNLOCK_US2000这些不是条件编译。它们只是普通宏常量用来在编译前做文本替换。区别在于普通宏替换值条件编译决定代码保不保留所以#defineA10不是条件编译。而#ifA5...#endif才是条件编译。12. 头文件防重复包含现在再回头看这类写法就会更容易理解它为什么也是条件编译。例如D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\bsp_usart.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\serial_screen.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\main.h典型写法#ifndefBSP_USART_H#defineBSP_USART_H/* 头文件内容 */#endif12.1 它的作用防止同一个头文件被重复包含两次以上。比如main.c包含了bsp_system.hbsp_system.h又包含了bsp_usart.h另一个头文件也可能再次包含bsp_usart.h如果没有这层保护编译器就可能看到重复的类型定义重复的函数声明重复的宏定义然后报错。12.2 它为什么属于条件编译因为这里本质是在问BSP_USART_H这个宏定义过了吗如果没定义就编译头文件内容如果已经定义过就跳过整份头文件所以它虽然看起来像模板但本质仍然是条件编译。12.3 为什么这部分放到后面讲因为头文件保护更像一种“工程卫生习惯”。它当然很重要但它没有前面那些例子那么直接地体现“功能裁剪”和“编译前选代码”的味道。先理解日志等级、模块开关、芯片差异再回来看头文件保护会更容易把它放进整体框架里理解而不是只把它背成一个固定模板。13. 为什么单片机项目特别依赖条件编译你的这个毕设项目里条件编译非常合理原因包括13.1 资源有限STM32 的FlashRAM串口带宽调试时间都比较宝贵。所以像调试日志开关模块按需启用芯片特性差异裁剪这些都适合在编译阶段处理掉。13.2 同一份代码要兼容不同环境例如调试版 / 发布版C 编译 / C 编译不同晶振参数不同芯片型号这些差异最适合靠条件编译管理。13.3 比运行时判断更干净对于很多底层功能如果这个模块不用最好连代码都不要编进去而不是留着代码到运行时再判断不用。14. 你现在这个项目里最值得你自己掌握的条件编译点如果从“后面你自己维护项目”的角度看最值得掌握的是这 4 类14.1 日志等级文件D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\bsp_usart.h你最可能自己改的是#defineLOG_LEVEL1改成0只保留警告和错误1显示普通信息2连调试日志都显示14.2 HAL 模块开关文件D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\stm32f4xx_hal_conf.h这决定哪些 HAL 模块参与编译。14.3 晶振和超时参数默认值还是D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\stm32f4xx_hal_conf.h如果你以后换板子外部晶振不是当前值这一类就可能要跟着变。14.4 芯片型号宏一般由工程编译选项或 CMSIS 头文件体系决定比如STM32F407xx这类不要随便手改除非你真的换了芯片。15. 改条件编译时要特别小心什么15.1 不要把头文件保护删掉像#ifndefXXX_H#defineXXX_H...#endif这种看起来像模板但它是必须的。15.2 不要随便关 HAL 模块如果你把HAL_UART_MODULE_ENABLED之类的宏关掉可能会导致编译不过某些 HAL 头文件没包含某些底层函数声明消失15.3 不要误把条件编译当运行时逻辑例如日志宏改LOG_LEVEL会改变“编译出来的代码”不是程序运行时动态切换也就是说改了LOG_LEVEL后需要重新编译效果才会生效。15.4 芯片相关宏不要乱改像STM32F407xx__FPU_PRESENT__FPU_USED这类宏跟芯片和工具链配置绑定乱改会导致系统初始化异常甚至启动都不正常。16. 一个简单总结如果把你这个项目里的条件编译压缩成一句话可以这样理解条件编译就是“在编译之前用宏决定哪些代码存在哪些代码不存在”。在你的项目里它主要承担了这些职责控制日志输出等级做 C / C 兼容给 HAL 配置默认值按芯片能力启用或裁剪底层代码防止头文件重复包含所以条件编译不是“高级小技巧”而是嵌入式工程非常核心的一部分。17. 你后面可以怎么继续学如果你想把这一块真正吃透建议按这个顺序继续先彻底吃透#define、#ifdef、#ifndef、#if defined(...)再理解可变参数宏比如__VA_ARGS__然后学会区分编译时配置运行时状态最后再去看更复杂的跨平台头文件和 HAL / CMSIS 配置头18. 本项目里建议你重点查看的文件如果你想结合实际代码再读一遍最推荐这几个D:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\bsp_usart.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\APP\cJSON.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\stm32f4xx_hal_conf.hD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Src\system_stm32f4xx.cD:\Desktop\STM32_Project_Collection\f407zgt6(hal)\Graduate_Project(物联网rfid正常通讯) -测试\Core\Inc\usart.h19. 最后一句最好记的话你可以先把条件编译记成下面这句话if是程序运行时做选择#if是编译器在编译前帮你删代码。这句话一旦真正理解了你后面再看 STM32、HAL、CMSIS 里的宏配置就会顺很多。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2591889.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!