15、CMake条件与循环:if/elseif/else/endif条件判断,foreach循环,list操作,数学表达式
各位同学,欢迎来到第十五讲。今天我们要聊的是CMake里的控制流——条件判断和循环。说实话,很多嵌入式工程师写CMakeLists.txt时,都是平铺直叙地写,项目一复杂就抓瞎。我个人觉得,掌握好条件与循环,你的构建脚本才能真正“活”起来。
15.1 条件判断:if/elseif/else/endif
条件判断是CMake里最基础的控制结构。说白了,就是根据不同的情况,执行不同的构建逻辑。比如,针对不同的编译器、不同的硬件平台、不同的构建类型(Debug/Release),我们需要做不同的处理。
15.1.1 基本语法
if(<condition>)
# 条件为真时执行
elseif(<condition>)
# 另一个条件
else()
# 以上都不满足时执行
endif()
嗯,这里要注意:endif()后面可以加注释,比如endif(),但我不建议你这么做——容易看花眼。我习惯在endif()后面什么都不加,保持干净。
15.1.2 常用条件表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
if(DEFINED <var>) |
变量是否已定义 | 检查用户是否设置了某个选项 |
if(<var>) |
变量是否为真(非空、非0、非OFF、非NO等) | 判断开关选项 |
if(NOT <condition>) |
逻辑非 | 取反 |
if(<var1> STREQUAL <var2>) |
字符串相等比较 | 比较编译器名称 |
if(<var1> EQUAL <var2>) |
数值相等比较 | 比较版本号数字 |
if(<var1> IN_LIST <list_var>) |
判断变量是否在列表中 | 检查某个源文件是否在源文件列表中 |
if(EXISTS <path>) |
文件或目录是否存在 | 检查头文件或库是否存在 |
if(TARGET <name>) |
目标是否已定义 | 检查某个库目标是否已创建 |
避坑指南:我曾经在项目里犯过一个低级错误——用if(${VAR} STREQUAL "ON"),结果VAR没定义,展开后变成了if( STREQUAL "ON"),直接报错。正确的写法是if(VAR STREQUAL "ON"),不要加${}包裹变量名。
15.1.3 实战:根据编译器选择编译选项
# 根据编译器设置不同的编译选项
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
message(STATUS "使用 GCC 编译器")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
message(STATUS "使用 Clang 编译器")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Weverything")
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
message(STATUS "使用 MSVC 编译器")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4")
else()
message(WARNING "未知编译器: ${CMAKE_C_COMPILER_ID}")
endif()
你想想看,如果没有条件判断,你得维护三套不同的CMakeLists.txt,那得多痛苦?
15.2 foreach循环:遍历列表的利器
foreach循环在CMake里用得非常多。我个人的经验是,只要遇到重复性的操作,比如给多个源文件设置相同的编译选项、生成多个可执行文件,第一时间想到foreach。
15.2.1 基本语法
foreach(<loop_var> <item1> [<item2> ...])
# 循环体
endforeach()
15.2.2 遍历列表
set(SOURCES main.c uart.c spi.c i2c.c)
foreach(src ${SOURCES})
message(STATUS "处理源文件: ${src}")
# 可以在这里对每个源文件做特殊处理
endforeach()
15.2.3 遍历数字范围
# 从1到5
foreach(i RANGE 1 5)
message(STATUS "迭代次数: ${i}")
endforeach()
# 从0到10,步长为2
foreach(i RANGE 0 10 2)
message(STATUS "偶数: ${i}")
endforeach()
15.2.4 遍历列表的索引
set(SOURCES main.c uart.c spi.c i2c.c)
# 获取列表长度
list(LENGTH SOURCES src_count)
math(EXPR last_index "${src_count} - 1")
foreach(i RANGE 0 ${last_index})
list(GET SOURCES ${i} src)
message(STATUS "索引 ${i}: ${src}")
endforeach()
小技巧:如果你需要同时遍历两个列表,可以用foreach配合list操作来实现。我曾在项目中需要将源文件列表和对应的编译选项列表一一对应,就是用这种方式处理的。
15.3 list操作:列表的增删改查
CMake里的列表,说白了就是一个用分号分隔的字符串。但你别小看它,配合list命令,能干不少事。
15.3.1 常用list命令
| 命令 | 说明 | 示例 |
|---|---|---|
list(APPEND <list> <item>...) |
追加元素 | list(APPEND SOURCES new_file.c) |
list(INSERT <list> <index> <item>...) |
在指定位置插入 | list(INSERT SOURCES 0 header.c) |
list(REMOVE_ITEM <list> <item>...) |
删除指定元素 | list(REMOVE_ITEM SOURCES test.c) |
list(REMOVE_AT <list> <index>...) |
删除指定索引的元素 | list(REMOVE_AT SOURCES 2) |
list(FIND <list> <value> <output_var>) |
查找元素索引 | list(FIND SOURCES main.c idx) |
list(LENGTH <list> <output_var>) |
获取列表长度 | list(LENGTH SOURCES count) |
list(GET <list> <index>... <output_var>) |
获取指定索引的元素 | list(GET SOURCES 0 first_src) |
list(SORT <list>) |
排序 | list(SORT SOURCES) |
list(REVERSE <list>) |
反转 | list(REVERSE SOURCES) |
15.3.2 实战:动态管理源文件列表
# 初始源文件列表
set(SOURCES main.c uart.c spi.c)
# 根据条件添加文件
if(ENABLE_I2C)
list(APPEND SOURCES i2c.c)
endif()
if(ENABLE_CAN)
list(APPEND SOURCES can.c)
endif()
# 排除测试文件
list(REMOVE_ITEM SOURCES test_main.c)
# 排序,保持一致性
list(SORT SOURCES)
# 创建可执行文件
add_executable(my_firmware ${SOURCES})
注意:list操作是就地修改的,也就是说它会直接改变原列表变量的值。如果你需要保留原始列表,记得先复制一份。
15.4 数学表达式:math命令
CMake本身不是一门编程语言,但有时候我们确实需要做一些简单的数学运算。比如计算缓冲区大小、计算数组索引等。这时候math命令就派上用场了。
15.4.1 基本语法
math(EXPR <output_var> <math_expression>)
15.4.2 支持的运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
+ |
加法 | math(EXPR result "3 + 5") |
- |
减法 | math(EXPR result "10 - 3") |
* |
乘法 | math(EXPR result "4 * 5") |
/ |
整数除法 | math(EXPR result "10 / 3") 结果为3 |
% |
取模 | math(EXPR result "10 % 3") 结果为1 |
| |
按位或 | math(EXPR result "0x01 | 0x02") |
& |
按位与 | math(EXPR result "0x03 & 0x01") |
~ |
按位取反 | math(EXPR result "~0xFF") |
<< |
左移 | math(EXPR result "1 << 3") |
>> |
右移 | math(EXPR result "8 >> 2") |
15.4.3 实战:计算缓冲区大小
# 定义基本参数
set(BUFFER_SIZE 256)
set(NUM_CHANNELS 8)
# 计算总缓冲区大小
math(EXPR TOTAL_SIZE "${BUFFER_SIZE} * ${NUM_CHANNELS}")
message(STATUS "总缓冲区大小: ${TOTAL_SIZE} 字节")
# 计算对齐后的地址
set(BASE_ADDR 0x20000000)
math(EXPR ALIGNED_ADDR "${BASE_ADDR} + 0x100")
message(STATUS "对齐后地址: 0x${ALIGNED_ADDR}")
小提示:math命令只支持整数运算。如果你需要浮点运算,那就得用其他方式了——不过说实话,在构建系统里做浮点运算的场景极少。
15.5 综合实战:一个完整的嵌入式项目构建脚本
好了,我们把今天学的东西串起来,写一个完整的例子。这个例子模拟了一个嵌入式项目,根据不同的硬件平台和功能选项,动态构建固件。
# 项目配置
project(EmbeddedFirmware C CXX ASM)
# 硬件平台选择
set(SUPPORTED_PLATFORMS stm32f4 stm32f7 nrf52840)
set(PLATFORM "stm32f4" CACHE STRING "目标硬件平台")
# 验证平台是否支持
if(NOT PLATFORM IN_LIST SUPPORTED_PLATFORMS)
message(FATAL_ERROR "不支持的平台: ${PLATFORM},支持的平台: ${SUPPORTED_PLATFORMS}")
endif()
# 功能选项
option(ENABLE_BLUETOOTH "启用蓝牙功能" ON)
option(ENABLE_WIFI "启用WiFi功能" OFF)
option(ENABLE_SENSORS "启用传感器" ON)
# 基础源文件
set(BASE_SOURCES
main.c
system_clock.c
gpio.c
uart.c
)
# 平台相关源文件
set(PLATFORM_SOURCES
platform/${PLATFORM}/startup_${PLATFORM}.c
platform/${PLATFORM}/hal_${PLATFORM}.c
)
# 根据功能选项添加源文件
if(ENABLE_BLUETOOTH)
list(APPEND PLATFORM_SOURCES
platform/${PLATFORM}/bluetooth_${PLATFORM}.c
)
add_definitions(-DENABLE_BLUETOOTH)
endif()
if(ENABLE_WIFI)
list(APPEND PLATFORM_SOURCES
platform/${PLATFORM}/wifi_${PLATFORM}.c
)
add_definitions(-DENABLE_WIFI)
endif()
if(ENABLE_SENSORS)
list(APPEND PLATFORM_SOURCES
platform/${PLATFORM}/sensor_${PLATFORM}.c
)
add_definitions(-DENABLE_SENSORS)
endif()
# 合并所有源文件
set(ALL_SOURCES ${BASE_SOURCES} ${PLATFORM_SOURCES})
# 排序,保持构建一致性
list(SORT ALL_SOURCES)
# 计算源文件数量
list(LENGTH ALL_SOURCES src_count)
math(EXPR src_count "${src_count} - 1")
message(STATUS "共 ${src_count} 个源文件参与构建")
# 创建可执行文件
add_executable(${PROJECT_NAME} ${ALL_SOURCES})
# 根据平台设置编译选项
if(PLATFORM STREQUAL "stm32f4")
target_compile_options(${PROJECT_NAME} PRIVATE -mcpu=cortex-m4 -mthumb)
elseif(PLATFORM STREQUAL "stm32f7")
target_compile_options(${PROJECT_NAME} PRIVATE -mcpu=cortex-m7 -mthumb)
elseif(PLATFORM STREQUAL "nrf52840")
target_compile_options(${PROJECT_NAME} PRIVATE -mcpu=cortex-m4 -mthumb -DNRF52840)
endif()
这个例子你看懂了吗?它用到了我们今天讲的所有知识点:条件判断、foreach(虽然没有显式写,但list操作本质上就是为循环服务的)、list操作、数学表达式。我个人觉得,这种写法才是嵌入式项目该有的样子——灵活、可配置、好维护。
15.6 本章知识体系
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考。
好了,这一章的内容就到这里。条件判断和循环是CMake脚本的骨架,掌握了它们,你就能写出灵活、可维护的构建系统。下一章我们会继续深入,聊聊CMake的函数和宏定义——那才是真正的高级玩法。
公众号:蓝海资料掘金营,微信deep3321