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条件与循环知识体系 条件判断 if/elseif/else/endif DEFINED / STREQUAL EQUAL / IN_LIST EXISTS / TARGET NOT 逻辑非 foreach循环 遍历列表元素 RANGE 数字范围 遍历索引 嵌套循环 break/continue list操作 APPEND / INSERT REMOVE_ITEM / REMOVE_AT FIND / LENGTH GET / SORT REVERSE 数学表达式 + - * / % | & ~ 位运算 << >> 移位 整数运算 EXPR 表达式 综合实战应用 典型应用场景 平台选择 | 功能开关 | 源文件管理 | 编译选项配置 | 版本控制

好了,这一章的内容就到这里。条件判断和循环是CMake脚本的骨架,掌握了它们,你就能写出灵活、可维护的构建系统。下一章我们会继续深入,聊聊CMake的函数和宏定义——那才是真正的高级玩法。


公众号:蓝海资料掘金营,微信deep3321