ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式构建系统演变史

嵌入式构建系统演变史 嵌入式构建系统演变史为什么需要构建系统为什么需要构建系统在嵌入式开发中构建系统决定了你的代码从写出来到跑起来之间发生了什么。这一节我们从最底层的逻辑出发——CPU 只认识机器码而你要把 C 源码变成机器码——这中间的每一步就是构建。 本课目标学完你将会说清楚构建系统到底在构建什么——源码 → 目标文件 → 可执行文件的全过程走一遍手动 gcc 编译的完整流程——体会为什么要自动化理解Makefile 解决了什么、又留下了什么遗憾看懂CMake 为什么是嵌入式领域的事实标准——以及 LKS32 项目为什么要用 CMake本讲思维导图构建系统的 4 个演化阶段手动命令 →Makefile第一次自动化→CMake声明式革命→Presets配置即代码。每个阶段都解决上一个阶段的痛点你可以用这 4 个阶段串起整个系列课程的主线。 先问一个灵魂问题CPU 认识 C 语言吗答案显然是否定的。CPU比如 LKS32MC033 内部的 Cortex-M0 核只认识二进制机器码——也就是一系列 0 和 1 组成的指令。而你的 C 源码长这样// 这是人写的——你、你的同事都能读懂 int main(void) { GPIOB-ODR ^ GPIO_PIN_5; // 翻转 LED while (1); }// 这是机器翻译后的——只有 CPU 能读懂 0x0000023A: ldr r0, [pc, #20] ; 0x40010C14 → 载入 GPIOB 地址 0x0000023C: ldr r1, [r0, #20] ; 读取 GPIOB-ODR 当前值 0x0000023E: eors r1, r1, #32 ; 异或 32第5位取反 翻转 0x00000240: str r1, [r0, #20] ; 写回 GPIOB-ODR那么问题来了从 .c 到 .hex最终烧进芯片的文件中间要经历几步每一步又是谁在做什么——这就是本课要拆解的构建系统。 嵌入式构建的完整流水线一个嵌入式项目的构建本质是一条4 段流水线。任何一个环节出错最终都不会得到能跑的固件 从一个文件到一个项目——规模带来的复杂度单独编译main.c只需 1 条命令。但一个真实的嵌入式项目远不止一个文件——LKS32 项目的完整结构是这样的这也是第 27 课要展开的七层架构 一句话总结本页构建系统 自动完成「编译 链接 格式转换」的流水线管理员。它管理几十个源文件、几百个头文件依赖、几十条编译参数——把人肉执行变成一键执行。下一页我们就看看人肉执行到底有多痛苦。史前时代手动命令行编译史前时代手动命令行编译在 Makefile 出现之前程序员是这样构建项目的——打开终端把每个 .c 文件编译成 .o再手动链接成 .elf最后手动转换格式。听起来简单当你有 50 个源文件时就完全是灾难了。本页大纲手动编译的完整解剖① 一条完整的 gcc 命令长什么样 → ② 为什么需要几十条这样的命令 → ③ 手动编译的 5 大痛点 → ④ 交互体验假装你是 1970 年代的工程师① 一条完整的 ARM 交叉编译命令在嵌入式领域编译器不止是 gcc——它是一整套工具链arm-none-eabi-gcc。下面这条命令是 LKS32 项目中每一个 .c 文件都要经历的arm-none-eabi-gcc \ -mcpucortex-m0 -mthumb -mfloat-abisoft ← CPU 架构参数 -Og -g3 ← 优化等级 调试信息 -I drivers/CMSIS/Include ← 头文件搜索路径 -I drivers/Device/Inc -I drivers/HAL/Inc -I bsp/Inc -I app/Inc -DLKS32MC03X -DUSE_HAL_DRIVER ← 预定义宏 -ffunction-sections -fdata-sections ← 段级优化配合裁剪 -Wall -Wextra -Wshadow ← 警告开关 -c app/main.c ← 只编译不链接 -o build/debug/main.o ← 输出目标文件请注意这只是一个文件的命令。如果你有30 个 .c 文件main、bsp_led、delay、hal_gpio、hal_uart、hal_tim、system_lks32mc03x、startup…就要敲30 条几乎一样、但文件名不同的命令。而且只要头文件路径或宏定义变了一个30 条全得改。② 链接 —— 把 30 个 .o 合成 1 个 .elf编译完所有 .o 文件后还需要最后一步链接link。链接器要把 30 个目标文件拼成一个完整的可执行文件并解决符号引用比如 main.o 调用了 HAL_GPIO_TogglePin链接器要从 hal_gpio.o 里找到它。arm-none-eabi-gcc \ -T drivers/Device/lks32mc03x_flash.ld ← 链接脚本内存布局 -Wl,--gc-sections ← 丢弃未用段省 Flash -Wl,-Mapbuild/debug/lks32.map ← 生成内存映射报告 --specsnano.specs --specsnosys.specs ← 微型标准库 build/debug/main.o build/debug/bsp_led.o build/debug/delay.o \ build/debug/hal_gpio.o ... ← 手动列上全部 30 个 .o -o build/debug/lks32.elf再之后还有objcopy.elf → .hex和size查看 Flash/RAM 占用。整条流程下来30 条编译 1 条链接 1 条 objcopy 1 条 size 33 条命令——每条都要手动敲。③ 手动编译的 5 大痛点#痛点具体场景后果1命令太长一条命令 8 行参数——换行排版、复制粘贴时丢一个反斜杠就废拼写错误频发2重复劳动改了 1 个头文件30 个 .c 全部要重新编译——但你不知道哪几个受影响要么全编慢要么漏编错3无法增量没有哪些文件变了的概念——每次全量重编编译 30 个文件 × 2 秒 1 分钟起步4参数散落编译参数写在终端历史里、记事本里、同事的脑子里换个环境就无法复现5无法协作新同事加入你只能口述先敲这个再敲那个团队协作效率为 0④ 为什么能跑不等于可维护——构建的另一个维度初学者常有一个误区“编译能通过、烧录能跑构建系统不是可有可无吗”——错。构建系统还负责另外三件看不见的事 核心领悟手动编译不是不会用工具而是*“把宝贵的脑力浪费在重复劳动上”*。真正的问题不是命令本身而是没有一套机制来记录依赖关系、管理增量、复现构建。下一页的 Makefile 就是第一代解决方案。Makefile第一次自动化革命Makefile第一次自动化革命1976 年Stuart Feldman 在贝尔实验室写出了make——构建自动化第一次成为可能。它的核心思想至今仍在使用目标target、依赖prerequisite、规则recipe三位一体。本页大纲Makefile 的解剖① make 的三要素目标/依赖/规则 → ② 一个真实的嵌入式 Makefile → ③ make 如何做到增量编译 → ④ Makefile 的 3 个致命缺陷为什么被 CMake 取代① make 的三要素 —— 用大白话解释Makefile 的每一行规则都是这样一句话「如果 目标 比 依赖 旧就执行 规则 来重新生成 目标」。听起来绕看例子这就是 make 的魔法通过比较文件时间戳实现增量编译——只有比目标新的依赖才会触发重新编译。相比手动全量编译这是革命性的进步。② 一个真实嵌入式项目的 Makefile这是 LKS32 项目如果只用 Makefile 会写成的样子简化版——注意每个 .c 都要写一行规则、每层都要手动维护# 编译参数 MCU -mcpucortex-m0 -mthumb -mfloat-abisoft OPT -Og -g3 INCLUDE -Idrivers/CMSIS/Include -Idrivers/Device/Inc \ -Idrivers/HAL/Inc -Ibsp/Inc -Iapp/Inc DEFS -DLKS32MC03X -DUSE_HAL_DRIVER # 所有源文件手动维护加一个文件改一行 SRCS app/main.c \ bsp/bsp_led.c bsp/delay.c \ drivers/HAL/hal_gpio.c drivers/HAL/hal_uart.c \ drivers/Device/system_lks32mc03x.c OBJS $(SRCS:.c.o) # 链接 lks32.elf: $(OBJS) arm-none-eabi-gcc -T drivers/Device/lks32mc03x_flash.ld \ $(OBJS) -o $ --specsnano.specs # 每条编译规则靠 .c.o 隐式规则简化了 %.o: %.c arm-none-eabi-gcc $(MCU) $(OPT) $(INCLUDE) $(DEFS) -c $ -o $ # 常用目标 clean: rm -f $(OBJS) lks32.elf flash: JLink.exe -CommanderScript flash.jlink Makefile 的功劳它解决了手动编译的 5 大痛点中的 4 个命令太长 ✓写一次、重复劳动 ✓增量编译、无法增量 ✓、参数散落 ✓写在文件里。但还差最后一个没解决——跨平台。而这正是它的致命伤。③ Makefile 的 3 个致命缺陷缺陷问题描述对嵌入式项目的影响1. 平台绑定Makefile 用 shell 语法rm -f / mkdir -p——Windows 的 cmd 不认识Windows Linux 无法共享同一份 Makefile——团队被迫分裂2. 手工依赖main.o: main.c bsp_led.h里哪个 .h 是依赖你得手写漏一个就改了头文件却不重编最常见的隐蔽 bug——改了配置头文件烧进去的还是旧固件3. 无库抽象没有库的概念——每个 Makefile 都要重复写一遍公共代码多层架构app/bsp/HAL之间无法表达传播头文件路径④ Makefile 依赖图的手工维护噩梦Makefile 增量编译的前提是你正确地写出了每个目标的所有依赖。但这几乎是不可能完成的任务——看下面的例子 关键结论Makefile 是命令式构建——你描述怎么做每步执行什么命令机器照着做。而 CMake 是声明式构建——你描述要什么哪些源文件、依赖谁CMake 帮你决定怎么做。下一页揭晓这场革命的细节。CMake声明式构建革命CMake声明式构建革命CMake 于 2000 年由 Kitware 公司发布——它不是又一个 make而是生成 make 的工具。你写一份 CMakeLists.txtCMake 会根据你的平台自动生成 Makefile 或 Ninja 文件——真正的一次编写处处构建。本页大纲CMake 的颠覆① CMake 是构建系统的构建系统 → ② 声明式 vs 命令式一张图看懂 → ③ LKS32 项目的最小 CMakeLists.txt → ④ 为什么嵌入式行业全面拥抱 CMake① CMake 构建系统的编译器理解 CMake 最重要的一句话CMake 本身不编译代码它生成用于编译代码的构建脚本。流程是这样的② 命令式 vs 声明式 —— 一张图看懂本质区别# 你要告诉机器每一步怎么做 %.o: %.c arm-none-eabi-gcc -mcpucortex-m0 \ -I... -D... -c $ -o $ # 隐含的假设编译参数、平台、 # 头文件依赖——全部要你操心# 你只要说要什么——怎么做交给 CMake add_executable(lks32.elf app/main.c bsp/bsp_led.c drivers/HAL/hal_gpio.c ) target_include_directories(lks32.elf PUBLIC drivers/CMSIS/Include ) target_compile_definitions(lks32.elf PUBLIC LKS32MC03X ) 类比Makefile 像点菜的厨师跟着菜谱做菜谱写死了步骤CMake 像你跟服务员说我想吃什么后厨自己决定怎么做、用什么锅。菜谱依赖具体厨房点餐则通用。③ LKS32 项目的最小 CMakeLists.txt —— 提前预习别看它短——这 10 行就是整个 LKS32 项目构建的核心。每一行都将在本系列后续课程中展开cmake_minimum_required(VERSION 3.20) ← 版本要求第 07 课 project(lks32 C ASM) ← 项目名 语言第 09 课 # 编译选项从 toolchain.cmake 注入第 19-22 课 add_executable(lks32.elf app/main.c bsp/bsp_led.c bsp/delay.c drivers/Device/system_lks32mc03x.c drivers/Device/startup_lks32mc03x.S ) # 头文件路径PUBLIC 自动传播给链接它的目标第 17 课 target_include_directories(lks32.elf PUBLIC drivers/CMSIS/Include drivers/Device/Inc drivers/HAL/Inc bsp/Inc app/Inc ) # 编译宏第 10 课 if() 章节 target_compile_definitions(lks32.elf PUBLIC LKS32MC03X USE_HAL_DRIVER ) # 链接脚本 产 hex/bin第 18 课 POST_BUILD target_link_options(lks32.elf PRIVATE -T drivers/Device/lks32mc03x_flash.ld )④ 为什么嵌入式行业全面拥抱 CMake芯片厂商官方工具链CMake 支持情况STSTM32STM32CubeMX原生生成 CMake 工程已放弃自家 SW4STM32EspressifESP32ESP-IDF100% 基于 CMakeNordicnRFnRF Connect SDK100% 基于 CMakeZephyr RTOSZephyr RTOSzephyr-build100% 基于 CMake凌鸥LKS32Keil / GCC本系列教程教你用 CMake 管理⑤ CMake 生态不止是一个工具而是一个体系CMake 之所以成为事实标准还因为它周围长出了一个完整的生态体系——每个部分都在本系列有对应课程 大势所趋2024 年后主流 MCU 厂商的新 SDK 几乎全部转向 CMake。学会 CMake 不是多一个选项而是掌握嵌入式行业的通用语言——换芯片、换厂商、换 RTOS构建逻辑都通用。三代对比与时间线三代构建系统对比与时间线把前三页放在一起对比你会清晰地看到每一代构建系统都解决了上一代最痛的问题同时引入了新一代的权衡。这张全景图就是整个系列课程的地图。本页大纲对比与定位① 三代系统 7 维度大对比 → ② 交互式对比面板点一点看细节→ ③ 构建系统完整时间线 → ④ LKS32 项目在整个演进中的位置① 三代构建系统 7 维度对比对比维度手动命令行MakefileCMake ★语法风格无就是命令命令式HOW声明式WHAT增量编译无时间戳比对自动生成依赖跨平台看 shell弱强生成器抽象头文件依赖无概念手写易漏自动-MD 扫描库/模块抽象无无Target PUBLIC/PRIVATEIDE 集成无弱VS Code / CLion 原生团队协作口口相传文件可共享但平台分裂一份配置全队通用③ 构建系统完整时间线④ 用哪种构建系统取决于什么——选择模型不是每个项目都该用 CMake——选择构建系统要考虑项目规模、团队、平台。这张决策图帮你快速判断 本系列课程的路线图接下来的 57 课将沿着这条时间线展开第 05-08 课搭环境 → 第 09-14 课学 CMake 语法 → 第 15-18 课理解构建模型 → 第 19-22 课搞懂交叉编译 → … → 第 52-55 课实战进阶。你现在站在这张地图的起点。CMake 三层架构深入CMake 三层架构深入前面我们把 CMake 比作构建系统的构建系统。这一节把这个比喻落到实处——CMake 的内部由三层组成脚本层、模块层、生成层。理解这三层你就能看懂 CMake 报的每一个错误。本页大纲CMake 三层架构① 三层架构全景图 → ② 每一层的职责与代表文件 → ③ 一次完整的 configure 内部发生了什么 → ④ 关键文件速查CMakeCache / build.ninja / compile_commands.json① CMake 三层架构全景② 一次 configure 内部发生了什么当你执行cmake --preset debug第 33-35 课时CMake 在内部依次做这些事读取预设解析 CMakePresets.json拿到生成器Ninja、构建目录build/debug、缓存变量CMAKE_BUILD_TYPEDebug 等加载工具链读取 toolchain.cmake——确定编译器是 arm-none-eabi-gcc、CPU 是 cortex-m0解析脚本从根目录 CMakeLists.txt 开始逐行执行 set()/add_executable()/target_include_directories()…遇到 add_subdirectory() 就递归进入子目录第 29 课检查依赖构建 target 依赖图谁链接谁、头文件路径怎么传播写入缓存把关键变量存进 build/debug/CMakeCache.txt生成脚本输出 build/debug/build.ninja compile_commands.json 关键概念configure 和 build 是两步configure 生成 build.ninjabuild 执行 ninja 真正调用 gcc 编译。改 CMakeLists.txt → 必须重新 configure改 .c 源码 → 直接 build 即可。这是第 15 课的两阶段模型。③ 三个关键生成文件速查文件位置内容与用途build.ninjabuild/debug/Ninja 的实际构建规则——每一条从哪个 .c 编译出哪个 .o的命令CMakeCache.txtbuild/debug/本次 configure 的所有变量快照——排查为什么编译参数没生效的第一现场compile_commands.jsonbuild/debug/每个 .c 的完整编译命令——VSCode 智能提示第 38 课直接读取它
返回列表