ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32裸机开发:用CMake与Renode构建不依赖IDE的C++工程

STM32裸机开发:用CMake与Renode构建不依赖IDE的C++工程 1. 为什么第四篇了还在讲环境如果你是从这个系列的第一篇一路追过来的看到这个标题大概会心一笑——看了三篇了一行都没让我写呢。这个吐槽太真实了我自己当年学 STM32 的时候也是这个心态买板子、装软件、看教程折腾了两三天LED 还没亮起来心里那个急啊。但我要说的是前三篇不让你写代码恰恰是对你负责。嵌入式开发和纯软件开发最大的区别在于你的代码跑在一颗没有操作系统兜底的芯片上任何环境配置的疏漏都会以编译通过但板子没反应这种最让人抓狂的形式表现出来。PC 上写 Python环境错了顶多报个 ImportErrorSTM32 上环境错了你面对的是沉默的板子和闪烁不定的调试器连个错误信息都不给你。所以这一篇我们终于要动手了。但在动手之前我得先把工具链的最后一环补齐——CMake 构建系统和Renode 仿真验证。这两个东西是让你从跟着教程点鼠标进化到自己搭工程的关键。我见过太多人 Keil 用得很溜但一旦让他脱离 IDE 从命令行构建一个 STM32 工程就完全懵了。这不是能力问题是从来没接触过底层构建流程。这篇文章的目标很明确让你在不依赖任何商业 IDE的前提下用 CMake arm-none-eabi-gcc 把代码编译出来用 Renode 在 PC 上先把逻辑跑通最后再烧到真实板子上。整套流程走完你对一个 STM32 工程是怎么从源码变成可执行文件这件事的理解会超过 80% 只会点 Keil 编译按钮的人。适合谁看如果你已经跟着前三篇把工具链装好了这篇是必经之路。如果你是从别的地方跳过来的只要你会基本的 C/C 语法能看懂#include和函数调用也能跟上。我会把每个配置项为什么这么写讲清楚而不是甩给你一个 CMakeLists.txt 让你复制粘贴。2. 从 Keil 到 CMake构建系统的思维切换2.1 Keil 帮你隐藏了什么大部分人的 STM32 入门路径是这样的装 Keil MDK装芯片包STM32F1xx_DFP 之类的新建工程勾选需要的库写代码点编译点下载。整个过程行云流水你甚至不需要知道编译器叫什么名字。但 Keil 在背后做了大量工作只是没告诉你它自动帮你选了编译器ARMCC 或 ARMCLANG并配置了一大堆编译选项它自动帮你写了链接脚本分散加载文件 .sct告诉链接器代码放哪、数据放哪它自动帮你生成了启动文件配置了中断向量表它自动帮你调用了 fromelf 把 elf 转成 hex 或 bin这些自动在初学阶段是好事降低了门槛。但当你需要定制构建流程、做持续集成、在团队里统一构建环境、或者用 VSCode 写代码但用命令行编译的时候Keil 的黑盒就成了障碍。CMake 的价值就在于它把这些自动变成了显式的、可读的、可版本控制的配置。你的构建逻辑写在 CMakeLists.txt 里谁都能看懂谁都能改放进 Git 里一目了然。换一台电脑装好工具链cmake .. make就能出结果不需要重新在 IDE 里点一遍配置向导。2.2 交叉编译到底在交叉什么在讲 CMake 配置之前必须先把这个概念讲透否则后面看到CMAKE_C_COMPILER那一堆设置你会一头雾水。你平时在 PC 上写 C 代码用的编译器是 gcc 或 clang它生成的是x86-64 指令集的机器码能在你的 Intel/AMD CPU 上直接跑。但 STM32 用的是ARM Cortex-M 内核指令集完全不同。x86 的机器码丢给 STM32它一个字节都看不懂。所谓交叉编译就是在 A 平台上编译出能在 B 平台上运行的代码。这里 A 是你的 PCx86-64B 是 STM32ARM Cortex-M。实现这件事的工具叫交叉编译工具链我们用的是arm-none-eabi-gcc。这个名字拆开看很有意思arm目标架构是 ARMnone没有操作系统裸机eabi嵌入式应用二进制接口Embedded Application Binary Interface所以arm-none-eabi-gcc就是给裸机 ARM 用的 GCC。它和你在 Linux 上用的 gcc 是同一套代码库编译出来的只是目标架构不同。这意味着 GCC 的绝大多数选项、语法支持都是一致的你学到的知识可以迁移。2.3 工具链里到底有哪几个关键工具装好gcc-arm-none-eabi之后你的 PATH 里会多出一堆以arm-none-eabi-开头的命令。常用的就这几个我列个表让你心里有数工具作用类比 PC 上的arm-none-eabi-gccC 编译器gccarm-none-eabi-gC 编译器garm-none-eabi-as汇编器asarm-none-eabi-ld链接器ldarm-none-eabi-objcopy格式转换elf→bin/hexobjcopyarm-none-eabi-objdump反汇编、查看段信息objdumparm-none-eabi-size查看代码/数据占用sizearm-none-eabi-gdb调试器gdb实际构建时你主要跟 gcc/g 打交道它们会自动调用 as 和 ld。objcopy 和 size 是构建后处理阶段用的后面会讲。提示如果你在 Windows 上建议用 MSYS2 或 WSL 来获得类 Unix 的构建环境CMake 和 Make 的体验会顺畅很多。纯 Windows 命令行下用 MinGW 也能做但路径分隔符和 shell 语法的坑会多一些。3. 手写一个能编译的 CMakeLists.txt3.1 最小可编译工程需要哪些文件在写 CMakeLists.txt 之前先明确一个 STM32 裸机工程最少需要哪些源文件。很多人以为只要有 main.c 就行其实不然启动文件startup_stm32f103xb.s这是芯片上电后执行的第一段代码负责初始化栈指针、设置中断向量表、调用 SystemInit最后跳到 main。没有它芯片上电后不知道从哪开始执行。链接脚本STM32F103C8Tx_FLASH.ld告诉链接器 Flash 从 0x08000000 开始、大小 64KRAM 从 0x20000000 开始、大小 20K以及各个段.text/.data/.bss怎么摆放。系统初始化文件system_stm32f1xx.c配置系统时钟比如把 8MHz 外部晶振倍频到 72MHz。HAL 库或标准库提供 GPIO、USART 等外设的驱动函数。main.c你的业务代码。这五样缺一不可。启动文件和链接脚本通常从 ST 官方的 CubeMX 或者芯片包例程里拿不用自己写。HAL 库可以从 GitHub 上的 STM32CubeF1 仓库获取。3.2 CMakeLists.txt 逐段拆解下面这份 CMakeLists.txt 是我在实际项目中反复打磨过的版本去掉了不必要的复杂度但保留了工程化需要的结构。我按段落讲每段都告诉你为什么这么写。cmake_minimum_required(VERSION 3.20) # 关键必须在 project() 之前设置否则 CMake 会先用主机编译器探测 set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(TOOLCHAIN_PREFIX arm-none-eabi-) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g) set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) # 告诉 CMake 不要尝试链接可执行文件来测试编译器 set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) project(stm32_cpp_journey LANGUAGES C CXX ASM)第一段的核心是CMAKE_SYSTEM_NAME Generic。如果不设这个CMake 会认为你在做本机编译然后尝试用 arm-none-eabi-gcc 去链接一个能在 x86 上跑的程序结果必然失败。设成 Generic 就是告诉 CMake这是裸机目标别做那些本机编译的假设。CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY这一行是很多人的坑。CMake 默认会编译一个测试程序并尝试链接成可执行文件来验证编译器可用性。但裸机环境下没有标准库的_start链接会失败CMake 就误判编译器不可用。设成 STATIC_LIBRARY 后它只编译成静态库不链接就能通过检测。3.3 编译选项每一个都有存在的理由set(CPU_FLAGS -mcpucortex-m3 -mthumb) set(COMMON_FLAGS ${CPU_FLAGS} -Wall -Wextra -fdata-sections -ffunction-sections -ffreestanding -fno-builtin ) set(CMAKE_C_FLAGS ${COMMON_FLAGS} -stdgnu11) set(CMAKE_CXX_FLAGS ${COMMON_FLAGS} -stdgnu17 -fno-exceptions -fno-rtti -fno-threadsafe-statics)-mcpucortex-m3 -mthumbSTM32F103 是 Cortex-M3 内核只支持 Thumb 指令集不支持 ARM 指令集。这两个选项必须成对出现缺了 -mthumb 编译器会生成 ARM 指令芯片跑不了。-fdata-sections -ffunction-sections把每个函数和每个数据都放到独立的段里。配合链接选项--gc-sections可以把没被引用的函数和数据从最终固件里剔除显著减小体积。对于 Flash 只有 64K 的 F103C8 来说这个优化很关键。-ffreestanding -fno-builtin告诉编译器我运行在裸机环境没有完整的标准库这样它不会把printf优化成puts也不会假设memcpy的行为符合标准库规范。嵌入式里这两个选项能避免很多诡异问题。C 的三个-fno-选项值得单独说-fno-exceptions异常机制需要运行时支持栈展开、类型信息裸机上没有关掉能省不少空间。-fno-rtti运行时类型识别同样需要额外数据裸机上基本用不到关掉。-fno-threadsafe-staticsC 局部静态变量的线程安全初始化需要__cxa_guard_acquire之类的运行时函数裸机上没有多线程关掉避免链接错误。注意关掉异常和 RTTI 意味着你不能用 try/catch 和 dynamic_cast。在嵌入式 C 里这是标准做法用错误码和模板替代这些特性。3.4 链接脚本与 gc-sectionsset(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld) set(CMAKE_EXE_LINKER_FLAGS ${CPU_FLAGS} -T${LINKER_SCRIPT} \ -Wl,--gc-sections \ -Wl,-Map${PROJECT_NAME}.map \ --specsnano.specs --specsnosys.specs )-T${LINKER_SCRIPT}指定链接脚本。这个文件决定了你的代码在 Flash 和 RAM 里的布局是裸机工程的核心配置。-Wl,--gc-sections开启段回收。配合前面的-ffunction-sections -fdata-sections把没用到的代码从固件里删掉。我实测过一个 HAL 工程不加这个选项固件 28K加了之后降到 12K效果非常明显。-Wl,-Mapxxx.map生成 map 文件。这个文件记录了每个符号被链接到了哪个地址、占多大空间。当你的固件超出 Flash 容量或者想搞清楚某个函数到底占了多少空间时map 文件是唯一的排查依据。--specsnano.specs使用 newlib-nano这是为嵌入式精简过的 C 标准库比完整版小很多。--specsnosys.specs提供一堆系统调用的空实现_write、_sbrk等。裸机上没有文件系统、没有进程管理这些系统调用本来就不存在但 newlib 里有些函数会引用它们不提供就会链接报错。3.5 把源文件组织起来file(GLOB_RECURSE HAL_SOURCES ${CMAKE_SOURCE_DIR}/Drivers/STM32F1xx_HAL_Driver/Src/*.c ) set(APP_SOURCES ${CMAKE_SOURCE_DIR}/Core/Src/main.c ${CMAKE_SOURCE_DIR}/Core/Src/system_stm32f1xx.c ${CMAKE_SOURCE_DIR}/Core/Src/stm32f1xx_it.c ${CMAKE_SOURCE_DIR}/Core/Startup/startup_stm32f103xb.s ) add_executable(${PROJECT_NAME}.elf ${APP_SOURCES} ${HAL_SOURCES}) target_include_directories(${PROJECT_NAME}.elf PRIVATE ${CMAKE_SOURCE_DIR}/Core/Inc ${CMAKE_SOURCE_DIR}/Drivers/STM32F1xx_HAL_Driver/Inc ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F1xx/Include ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include )这里有个细节HAL 库的源文件我用GLOB_RECURSE一次性收集但应用代码我一个个列出来。为什么区别对待因为 HAL 库是第三方代码文件多且稳定用 GLOB 省事而应用代码是你自己要频繁增删的显式列出能让你清楚知道工程里到底有哪些文件也避免 GLOB 在新增文件后不重新配置 CMake 导致文件没被编译的坑。提示file(GLOB)有个众所周知的缺点——新增文件后 CMake 不会自动感知需要重新运行 cmake 配置。CMake 官方甚至不推荐用 GLOB。但在 HAL 这种一次引入、长期不动的场景下它的便利性大于缺点。应用代码还是老实列出来。3.6 生成 bin 和 hex 的后处理add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $TARGET_FILE:${PROJECT_NAME}.elf ${PROJECT_NAME}.bin COMMAND ${CMAKE_OBJCOPY} -O ihex $TARGET_FILE:${PROJECT_NAME}.elf ${PROJECT_NAME}.hex COMMAND ${CMAKE_SIZE} $TARGET_FILE:${PROJECT_NAME}.elf )编译产物是 elf 格式包含调试信息和段信息但烧录工具通常需要 bin 或 hex。这段 POST_BUILD 命令在每次链接完成后自动生成这两种格式并调用 size 打印固件占用情况。$TARGET_FILE:...是 CMake 的生成器表达式会自动展开成目标文件的完整路径比手写路径可靠。size 的输出长这样text data bss dec hex filename 12340 108 1564 14012 36bc stm32_cpp_journey.elftext 是代码只读数据data 是已初始化的全局变量存在 Flash运行时拷贝到 RAMbss 是未初始化的全局变量只占 RAM。Flash 占用 text dataRAM 占用 data bss。这个账要会算不然固件超了都不知道超在哪。4. Renode不插板子也能验证逻辑4.1 为什么要在 PC 上仿真你可能会问板子就在手边为什么不直接烧进去看效果还要搞什么仿真原因有几个都是实际开发中会遇到的第一硬件不一定随时可用。我经常在通勤路上或者出差途中想改点代码板子不在身边。这时候 Renode 能让我在笔记本上把逻辑跑通回去再烧板子验证。第二调试信息更丰富。真板子上跑飞了你只能看到 LED 不亮或者串口没输出。Renode 里你可以直接看寄存器、看内存、看每条指令的执行定位问题快得多。第三CI 集成。团队协作时你不可能给每个 CI runner 都插一块 STM32。Renode 可以在纯软件环境里跑自动化测试每次提交代码自动验证逻辑没跑偏。第四学习成本低。初学阶段你还没搞懂时钟配置、引脚复用这些硬件细节直接上板子容易一头雾水。Renode 里先把纯逻辑比如算法、状态机跑通再上硬件心理负担小很多。4.2 Renode 模拟 STM32F103 的基本配置Renode 用.resc脚本描述要模拟的平台。下面这份是针对 STM32F103 的最小配置# stm32f103.resc mach create stm32f103 machine LoadPlatformDescription platforms/cpus/stm32f103.repl sysbus LoadELF ${CURDIR}/../build/stm32_cpp_journey.elf showAnalyzer sysbus.uart1 start逐行解释mach create创建一个新的机器实例名字随便取。LoadPlatformDescription加载平台描述文件。Renode 自带了很多常见芯片的.repl文件STM32F103 的在platforms/cpus/stm32f103.repl。这个文件描述了芯片有哪些外设、寄存器地址在哪、中断怎么连。LoadELF把你的固件加载进去。Renode 会解析 elf 里的段信息把代码放到对应的 Flash 地址数据放到 RAM 地址。showAnalyzer sysbus.uart1打开 UART1 的分析窗口。这是 Renode 最实用的功能之一——你的代码往串口发数据分析窗口里能直接看到不需要真实的 USB 转串口模块。start开始执行。4.3 用 Renode 验证一个 GPIO 闪烁逻辑光说不练假把式。我们写一段最简单的代码让 PC13 上的 LED 闪烁然后在 Renode 里验证。// main.c #include stm32f1xx_hal.h void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(500); } } static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_13; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); }在 Renode 里跑起来后你可以在 Renode 的监视器里输入命令查看 GPIOC 的 ODR 寄存器(machine) sysbus.gpioc.ODR你会看到 bit 13 在 0 和 1 之间周期性变化说明闪烁逻辑正常。这个过程不需要真实板子也不需要示波器。4.4 Renode 的局限与真机验证的不可替代性Renode 虽好但必须清楚它的边界时序不精确。Renode 模拟的是功能不是精确时序。你的代码在 Renode 里跑得好好的真机上可能因为时钟配置错误、中断优先级冲突、外设初始化顺序问题而跑飞。特别是涉及精确延时的场景比如软件模拟的 WS2812 驱动Renode 完全无法验证。外设覆盖不全。STM32F103 的.repl文件覆盖了 GPIO、USART、SPI、I2C、定时器等常用外设但一些冷门外设或者特定型号的差异可能没模拟到。用到这些外设时Renode 里可能直接报未实现的寄存器访问。电气特性无法验证。上拉电阻够不够、驱动电流足不足、信号完整性好不好这些只有真机能告诉你。所以我的建议是Renode 用来验证逻辑和算法真机用来验证硬件交互。两者互补不是替代关系。5. 从零到点灯完整流程走一遍5.1 目录结构规划在动手之前先把目录结构定下来。一个好的结构能让你后面少折腾stm32_cpp_journey/ ├── CMakeLists.txt ├── STM32F103C8Tx_FLASH.ld ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ └── stm32f1xx_hal_conf.h │ ├── Src/ │ │ ├── main.c │ │ ├── system_stm32f1xx.c │ │ └── stm32f1xx_it.c │ └── Startup/ │ └── startup_stm32f103xb.s ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── build/ └── renode/ └── stm32f103.rescCore放你自己的代码Drivers放 ST 官方库build是 CMake 的输出目录不纳入版本控制renode放仿真脚本。这个结构基本是 CubeMX 生成工程的翻版但去掉了 IDE 相关的文件更干净。5.2 构建命令与常见报错mkdir -p build cd build cmake -DCMAKE_BUILD_TYPEDebug .. make -j4如果一切顺利你会在 build 目录下看到stm32_cpp_journey.elf、.bin、.hex和.map四个文件。但第一次构建大概率不会顺利。我列几个最常见的报错和排查思路报错一arm-none-eabi-gcc: command not found工具链没装或者没加到 PATH。Linux 下用sudo apt install gcc-arm-none-eabiWindows 下装完记得把bin目录加到环境变量。验证方法arm-none-eabi-gcc --version能打印版本号就对了。报错二undefined reference to _exit或_sbrk这是 newlib 的系统调用没提供。检查链接选项里有没有--specsnosys.specs。如果加了还报错可能是某个库函数引用了更冷门的系统调用需要自己实现一个空函数。报错三region FLASH overflowed by XXXX bytes固件超出 Flash 容量。先看 size 输出确认是 text 还是 data 太大。如果是 HAL 库引入太多检查stm32f1xx_hal_conf.h里是不是把所有模块都打开了关掉用不到的模块能省不少空间。报错四cannot find -lnosys工具链安装不完整缺少 newlib 的 nosys 库。重新安装libnewlib-arm-none-eabi包。5.3 烧录与验证构建出 bin 文件后烧录方式取决于你手上的工具ST-Link用st-flash write stm32_cpp_journey.bin 0x08000000OpenOCD配置好 interface 和 target 后program stm32_cpp_journey.elf verify reset exit串口 ISP用stm32flash工具需要把 BOOT0 拉高进入 bootloader 模式烧录后如果 LED 没反应按这个顺序排查用万用表量 PC13 对地电压看有没有在 0V 和 3.3V 之间跳变。如果有跳变但 LED 不亮是 LED 电路问题限流电阻太大、LED 极性接反。如果没有跳变用调试器连上去看 PC 卡在哪。常见的是卡在HAL_Delay里说明 SysTick 中断没配好。如果连调试器都连不上检查 BOOT0/BOOT1 引脚状态以及复位电路。5.4 把 C 特性用起来既然是C 编程之旅总得用点 C 的东西。在裸机上最实用的 C 特性是模板和编译期计算因为它们零运行时开销。举个例子用模板封装 GPIO 操作templateuint32_t PortBase, uint16_t Pin class Led { public: static void init() { // 使能对应端口的时钟 if constexpr (PortBase GPIOC_BASE) { __HAL_RCC_GPIOC_CLK_ENABLE(); } GPIO_InitTypeDef cfg {0}; cfg.Pin Pin; cfg.Mode GPIO_MODE_OUTPUT_PP; cfg.Pull GPIO_NOPULL; cfg.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(reinterpret_castGPIO_TypeDef*(PortBase), cfg); } static void toggle() { HAL_GPIO_TogglePin(reinterpret_castGPIO_TypeDef*(PortBase), Pin); } }; using StatusLed LedGPIOC_BASE, GPIO_PIN_13;用的时候StatusLed::init(); while (1) { StatusLed::toggle(); HAL_Delay(500); }if constexpr是 C17 的特性编译期求值不满足条件的分支根本不会生成代码。整个Led类没有任何虚函数、没有运行时开销编译出来的汇编和直接调 HAL 函数一模一样。这就是嵌入式 C 的正确打开方式——用抽象换可读性但不换性能。6. 那些教程不会告诉你的坑6.1 启动文件选错型号STM32F103 有好几个型号启动文件也分startup_stm32f103xb.s中容量64K/128K Flash、startup_stm32f103xe.s大容量256K以上等。选错了会怎样编译能过但中断向量表可能对不上表现为中断进不去或者进错中断。判断方法看你的芯片型号后缀。C8T6 是 64K Flash对应xbCBT6 是 128K也是xbZET6 是 512K对应xe。拿不准就查 ST 的参考手册别猜。6.2 链接脚本的 RAM 大小写错F103C8T6 的 RAM 是 20K但很多人从别的工程复制链接脚本时忘了改写成 64K。结果就是编译链接都通过但运行时栈溢出程序随机跑飞。这种问题最难查因为现象不固定。链接脚本里这两行必须和芯片实际匹配RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K6.3 HAL_Delay 在中断里调用会死锁HAL_Delay依赖 SysTick 中断来更新计时变量。如果你在某个中断服务函数里调用HAL_Delay而该中断优先级高于或等于 SysTick 的优先级SysTick 中断进不来计时变量不更新HAL_Delay就永远等下去整个系统卡死。正确做法是在中断里用简单的忙等待循环或者用硬件定时器做精确延时绝对不要在中断里调HAL_Delay。6.4 CMake 的 build 目录不要提交到 Gitbuild目录里全是生成的文件路径还跟你的机器绑定提交上去只会污染仓库。在.gitignore里加上build/ *.elf *.bin *.hex *.map6.5 Renode 加载 ELF 时找不到符号Renode 加载 ELF 需要符号信息来定位入口点。如果你编译时加了-s或者 strip 掉了符号Renode 可能加载失败。Debug 构建不要 stripRelease 构建如果要给 Renode 用保留一份带符号的 elf。7. 下一步从点灯到真正的项目走到这里你已经有了一个完全自主可控的 STM32 C 工程不依赖任何商业 IDE构建流程透明能在 PC 上仿真验证也能烧到真机运行。这个基础打好了后面学什么外设都是在这个框架里加代码的事。接下来可以尝试的方向把串口用起来。在 Renode 里showAnalyzer看输出在真机上用 USB 转串口模块看输出。串口是嵌入式开发最重要的调试手段比点灯有用得多。实现一个printf重定向到 UART后面调试会轻松很多。引入状态机框架。裸机程序写复杂了就是一堆if-else和标志位用 C 写个简单的状态机模板代码会清晰很多。这是 C 在嵌入式里真正发挥价值的地方。试试单元测试。把纯逻辑代码不依赖硬件的部分抽出来在 PC 上用 Google Test 跑单元测试硬件相关的部分用 mock 替代。这样大部分 bug 在 PC 上就能发现不用反复烧板子。深入链接脚本。把变量放到特定段、把函数放到 RAM 里执行、配置栈和堆的大小这些都需要改链接脚本。搞懂了链接脚本你对程序在芯片里怎么布局就有了完整的掌控。我在实际项目里最大的体会是构建系统搭一次受益整个项目周期。前期花两天把 CMake 和工具链理顺后面每次改代码、加文件、做 CI 都是几分钟的事。反过来如果一直依赖 IDE 的图形界面换个环境就要重新折腾一遍团队协作时更是灾难。这笔时间投资绝对值。
返回列表