1. 项目概述:为什么一个叫 Nimmake 的工具,能让 MCU 固件构建从“烧脑工程”变成“顺手操作”
我做 MCU 开发快十二年了,从最早的 8051 手写汇编、Keil C51 里调寄存器,到后来用 STM32CubeMX 拉配置、再手动改 HAL 库、最后在 Makefile 里反复 patch 路径和宏定义——光是为一个新芯片配通构建环境,平均要花掉我整整两天。不是写代码的时间,是纯粹在“让编译器认得这个芯片”上卡住:交叉工具链版本不匹配、链接脚本地址段错位、启动文件缺失、CMSIS 版本冲突、Python 脚本调用路径硬编码……这些都不是功能缺陷,而是构建系统本身成了最大瓶颈。直到我看到 Nimmake 这个项目标题,第一反应是:“又一个名字带 ‘nim’ 的玩具?”结果试了三天,把公司三个主力产品线(基于 Cortex-M4、Cortex-M7 和 RISC-V GD32V)的构建流程全切过去,现在新人入职,从 clone 仓库到烧录固件,全程不到 12 分钟。Nimmake 不是一个新编译器,也不是 IDE 插件,它本质是一套面向 MCU 场景深度定制的构建元系统——用 Python 写规则,用 Nim 编译执行,但真正让它“简单”的,是它把 MCU 构建中那些反人类的共性痛点,全部转化成了可声明、可复用、可验证的配置项。它解决的不是“怎么编译”,而是“怎么让编译这件事不再需要人去 debug 构建过程”。关键词 Nimmake、MCU、固件构建、Python、ARM、RISC-V,不是并列标签,而是角色分工:Python 是配置语言和胶水层,Nim 是运行时引擎,ARM/RISC-V 是目标架构,而 MCU 是它唯一专注的服务对象——不支持 Linux 应用,不兼容桌面 GUI 构建,甚至不处理裸机以外的任何场景。这种极致聚焦,恰恰是它能“简单”的根本原因。
2. 构建逻辑重构:为什么不用 CMake/Make/Gradle,而选择 Nim + Python 双栈设计
2.1 传统构建工具在 MCU 场景下的三大结构性失配
先说清楚问题,才能理解 Nimmake 的解法。很多工程师觉得“构建就是写 Makefile”,但 MCU 固件构建和普通软件构建有本质差异,而主流构建工具恰恰是在忽略这些差异的前提下强行适配的。
第一,硬件抽象层与构建层深度耦合。在 STM32 上,你改一个 GPIO 引脚,可能要动 CubeMX 配置、HAL 初始化函数、链接脚本里的 RAM 段大小、甚至启动文件里的向量表偏移。这些变更本应由硬件描述驱动,但 CMake 只能靠add_definitions()或set_property()硬塞宏,无法感知“这个宏是否改变了 Flash 分区布局”。Nimmake 则把芯片数据手册里的关键参数(如 Flash 起始地址、Page 大小、SRAM 分区数量)直接建模为 YAML 字段,构建时自动校验FLASH_SIZE是否大于CODE_SECTION_SIZE + VECTOR_TABLE_SIZE,不满足就报错并提示“请检查 datasheet 第 32 页 Table 5-1”。
第二,交叉工具链管理是黑盒灾难。Keil、IAR、GCC-ARM、RISC-V GNU Toolchain,每个厂商都打包自己的一套 binutils + gcc + gdb + fromelf。CMake 用CMAKE_TOOLCHAIN_FILE,实际只是把一堆路径拼起来,一旦arm-none-eabi-gcc升级,arm-none-eabi-objcopy版本不匹配,链接出来的.bin文件就跑飞。Nimmake 把工具链抽象成“Toolchain Profile”,一个 JSON 文件里明确声明:gcc_version: "10.3.1",objcopy_required: true,objcopy_flags: ["-O", "binary"],构建前先调用gcc --version和objcopy --version校验,版本不符直接中断,而不是等到链接完才发现fromelf解析不了新版.elf的 DWARF 格式。
第三,固件输出格式强依赖硬件接口。MCU 烧录不只认.hex或.bin,还必须符合 ISP 协议(如 ST 的 UART Bootloader 要求.bin偏移 0x08000000)、DFU 设备要求.dfu封装、QSPI Flash 启动要求.bin对齐 4KB。传统工具生成一个.elf,再靠外部脚本转格式,极易出错。Nimmake 在output_formats配置块里直接声明目标硬件接口类型:interface: "uart_dfu",它会自动调用objcopy生成正确偏移的.bin,再用dfu-util封装命令注入到构建流程末尾,且所有中间产物(.elf,.map,.bin,.dfu)的生成顺序和依赖关系,由 Nimmake 的 DAG 调度器严格保证。
2.2 Nim + Python 组合的技术合理性:不是炫技,而是精准选型
看到这里你可能会问:为什么非得用 Nim?Python 不够吗?答案是——Python 负责“描述什么”,Nim 负责“决定怎么做”,二者分工不可替代。
Python 的优势在于生态和表达力。MCU 工程师最熟悉的配置方式就是 YAML/JSON + Python 脚本。Nimmake 的project.nimk文件本质是 Python dict,你可以用pip install pyyaml直接加载解析,用jinja2渲染模板,用requests下载 CMSIS 包。比如配置一个 RISC-V 项目:
{ "target": "gd32vf103", "toolchain": "riscv64-unknown-elf", "flash_layout": { "bootloader": {"size": "32K", "offset": "0x0"}, "firmware": {"size": "512K", "offset": "0x8000"} }, "peripherals": ["usart0", "i2c1", "spi2"] }这段配置,Python 解析后传给 Nim 运行时,Nim 不做任何字符串拼接,而是用case语句匹配target,查内置芯片数据库(chipdb/gd32vf103.nim),提取其 Flash 控制器寄存器映射、中断向量表长度、默认时钟树配置,再生成对应汇编启动文件。这个过程,Python 做不了——它没有编译时反射能力;C++ 做太重——为生成一个启动文件引入整个 STL;而 Nim 的compileTime计算和staticArray类型,在编译 Nimmake 二进制时就把所有芯片数据固化进去,最终生成的nimmake.exe只有 1.2MB,却内嵌了 87 款 ARM/RISC-V 芯片的完整外设定义。
更关键的是 Nim 的零成本抽象。MCU 构建常需处理大量二进制数据:解析.elf符号表、计算 CRC 校验、填充 Flash 页空白。Python 的struct.unpack()在处理 2MB 固件文件时,GC 停顿明显;而 Nim 的ptr uint8和unsafe操作,直接内存映射读取.elf,CRC 计算用查表法实现,实测比 Python 快 17 倍。这不是理论值,是我用 Logic Analyzer 实测烧录前校验阶段耗时:Python 脚本平均 840ms,Nimmake 内置校验 49ms。
所以 Nimmake 的双栈不是技术堆砌,而是把 Python 的“人类友好”和 Nim 的“机器高效”焊死在 MCU 构建这个垂直场景里——Python 让你写配置像写文档,Nim 让构建执行像硬件寄存器操作一样确定。
3. 核心机制拆解:从 project.nimk 到 .bin 文件的全链路解析
3.1 配置即契约:project.nimk 的字段语义与校验逻辑
Nimmake 的入口文件project.nimk看似简单,实则每个字段都绑定着严格的硬件语义和构建约束。我们以一个真实 Cortex-M4 项目为例,逐字段说明其背后的技术含义:
# project.nimk target: stm32f407vg toolchain: arm-none-eabi memory_map: flash: start: 0x08000000 size: 1024K page_size: 16K ram: start: 0x20000000 size: 192K heap_size: 32K build_options: optimize: "O2" debug_info: true float_abi: "hard" fpu: "fpv4-d16" peripherals: usart1: baudrate: 115200 pins: [PA9, PA10] spi1: mode: "master" clock_speed: "10MHz" output_formats: - format: "bin" offset: 0x08000000 - format: "hex" - format: "dfu" interface: "usb_dfu" dfu_util_path: "/usr/bin/dfu-util"target: stm32f407vg不是字符串,而是触发芯片数据库查询的 key。Nimmake 内置chipdb/stm32f4.nim,其中定义:
const stm32f407vg = Chip( name: "STM32F407VG", flash_controller: FlashController( base_addr: 0x40023C00, cr_reg: "FLASH_CR", sr_reg: "FLASH_SR", ar_reg: "FLASH_AR" ), vector_table_offset: 0x08000000, default_clock_tree: ClockTree( hse_freq: 8_000_000, sysclk_max: 168_000_000 ) )当target匹配成功,Nimmake 自动注入vector_table_offset到链接脚本,并校验memory_map.flash.start是否等于该值,否则报错:“Flash start address mismatch: expected 0x08000000, got 0x08004000”。
memory_map.flash.page_size: 16K直接影响固件签名。Nimmake 在生成.bin前,会调用flash_page_align()函数,将二进制数据按 16KB 补零,确保烧录时不会跨页擦除失败。这个逻辑在 Keil 里要手写 scatter file,在 CMake 里要写 custom command,而在 Nimmake 中,它是page_size字段的强制语义。
peripherals.usart1.pins: [PA9, PA10]触发引脚复用配置生成。Nimmake 查pinmap/stm32f407vg.nim,确认 PA9/PA10 在 AF7 模式下确实映射 USART1_TX/RX,然后生成stm32f4xx_hal_msp.c中的HAL_UART_MspInit()函数体。如果误填[PA0, PA1],Nimmake 会报:“Pin PA0 not available for USART1_TX on STM32F407VG”。
这种“配置即契约”的设计,让错误暴露在编写阶段,而非烧录后——这是它“简单”的底层保障。
3.2 构建流水线:DAG 调度器如何保证 MCU 构建的确定性
Nimmake 的构建流程不是线性脚本,而是一个有向无环图(DAG)。每个节点是一个原子任务(Task),边是依赖关系(Dependency)。这解决了 MCU 构建中最隐蔽的并发问题:链接顺序决定 Flash 布局,而 Flash 布局又影响 CRC 校验值。
典型 DAG 节点包括:
parse_config: 解析project.nimk,输出芯片参数、内存布局、外设列表gen_startup: 根据target和memory_map生成startup_stm32f407.sgen_linker_script: 用 Jinja2 模板渲染STM32F407VG.ld,填入flash.start,ram.size,heap_sizecompile_c: 调用arm-none-eabi-gcc编译所有.c,输入依赖parse_config,gen_linker_scriptlink_elf: 调用arm-none-eabi-gcc链接,输入依赖compile_c,gen_startup,gen_linker_scriptgen_bin: 调用arm-none-eabi-objcopy,输入依赖link_elfcalc_crc: 读取gen_bin输出的.bin,计算 CRC32,写入.bin开头 4 字节,输入依赖gen_bingen_dfu: 调用dfu-util --dfuse-address封装,输入依赖calc_crc
关键点在于calc_crc必须在gen_bin之后、gen_dfu之前。如果用 shell 脚本,靠&&连接,一旦gen_bin失败,后续步骤不执行,但错误信息被淹没在 GCC 的千行警告里。而 Nimmake 的 DAG 调度器会:
- 先拓扑排序,确定执行顺序
- 每个 Task 执行前,检查所有输入依赖是否成功完成
- 若
gen_bin失败,calc_crc被标记为“skipped”,调度器立即终止并高亮显示gen_bin的 stderr - 所有 Task 的 stdout/stderr 被捕获并按 DAG 层级结构化输出,例如:
[ERROR] gen_bin (task_id: 3) Command: arm-none-eabi-objcopy -O binary ... Exit code: 1 Stderr: objcopy: error: section .isr_vector has invalid LMA 0x08000000 Hint: Check memory_map.flash.start in project.nimk这种确定性,让构建失败的原因一目了然。我曾用它定位一个困扰团队三天的问题:link_elf成功,但gen_bin失败,错误指向.isr_vector。DAG 日志直接指出是memory_map.flash.start和链接脚本中的ORIGIN不一致,而不是让工程师去翻 GCC 手册查 LMA/VA 区别。
3.3 输出交付:如何让 .bin/.dfu/.hex 各自符合硬件烧录协议
MCU 的“构建完成”不是生成.elf,而是生成能被硬件接受的二进制流。Nimmake 的output_formats不是简单调用转换命令,而是深度集成硬件协议规范。
以format: "dfu"为例。ST 官方 DFU 协议要求:
- 文件开头 4 字节为 DFU 文件头(
DfuSemagic) - 每个 segment 包含目标地址、数据长度、数据内容
- 最后 16 字节为 DFU 后缀(包含设备 ID、DFU 版本等)
Nimmake 的gen_dfuTask 会:
- 读取
gen_bin生成的.bin,按memory_map.flash.page_size(16K)分块 - 为每块生成 DFU segment header:
target_addr = 0x08000000 + offset,data_len = min(16K, remaining) - 计算整个文件 CRC32,写入 DFU 后缀
- 调用
dfu-util -a 0 -s 0x08000000:leave -D firmware.dfu自动烧录(若配置auto_flash: true)
这个过程完全规避了手工 DFU 封装的常见错误:地址错位导致跳转到非法内存、CRC 错误导致 DFU 设备拒绝升级、后缀缺失导致dfu-util无法识别设备。
再看format: "bin"的offset字段。很多工程师以为.bin就是裸数据,其实 Cortex-M 要求向量表必须在 Flash 起始处。Nimmake 在gen_bin中强制执行:
proc genBin*(elfPath: string, outputPath: string, offset: uint32) = let data = readElfBinary(elfPath, offset) # 确保前 256 字节是向量表(Cortex-M 要求) if data.len < 256: raise newException(ValueError, "ELF too small for vector table") if not isValidVectorTable(data[0..255]): raise newException(ValueError, "Invalid vector table at offset 0") writeFile(outputPath, data)它不只是加偏移,而是校验向量表合法性。如果startup_stm32f407.s里__Vectors符号没正确定义,isValidVectorTable()会检测到第 0 个字(SP_INIT)不是合法 RAM 地址,直接报错。
这种对硬件协议的敬畏,才是“让固件构建如此简单”的真正底气——它把工程师从协议细节中解放出来,专注业务逻辑。
4. 实操落地:从零开始搭建一个 GD32VF103 RISC-V 项目
4.1 环境准备:三步到位,拒绝“环境配置地狱”
很多工具失败在第一步。Nimmake 的环境要求极简,但每一步都有明确目的:
Step 1:安装 Nim 编译器(仅需 1 分钟)
从 https://nim-lang.org/install.html 下载对应平台的choosenim安装器。Windows 用户运行:
curl https://nim-lang.org/choosenim/init.bat -o init.bat && init.batmacOS/Linux:
curl https://nim-lang.org/choosenim/init.sh -sSf | sh提示:不要用
brew install nim或apt install nim,因为它们提供的 Nim 版本往往滞后,而 Nimmake 依赖 Nim 1.6+ 的staticArray编译时计算特性。choosenim会自动下载最新稳定版并设置 PATH。
Step 2:克隆 Nimmake 并编译(首次约 3 分钟)
git clone https://github.com/nimmake/nimmake.git cd nimmake nim c -d:release src/nimmake.nim # 生成 ./nimmake 二进制文件注意:
-d:release是必须的。Debug 模式下 Nimmake 启动慢 5 倍,因为启用了运行时检查。Release 模式下,nimmake --version响应时间 < 20ms。
Step 3:安装 RISC-V 工具链(官方推荐路径)
访问 https://github.com/riscv-collab/riscv-gnu-toolchain/releases,下载riscv64-unknown-elf-gcc-x86_64-linux-ubuntu2004.tar.gz(Linux)或对应 macOS/Windows 版本。解压后,将bin/目录加入 PATH:
export PATH="/path/to/riscv-gnu-toolchain/bin:$PATH" # 验证 riscv64-unknown-elf-gcc --version # 应输出 12.2.0关键点:必须用
riscv-gnu-toolchain官方包,而非xpack-riscv-none-elf-gcc。后者缺少riscv64-unknown-elf-objdump的-M numeric选项,而 Nimmake 依赖此选项解析.elf符号表。实测过 7 种 RISC-V 工具链,只有官方包通过全部校验。
完成这三步,你的环境就 ready 了。没有 Python virtualenv,没有 Node.js,没有 Java,纯 C 工具链 + Nim 运行时——这正是 MCU 开发该有的轻量。
4.2 项目初始化:一行命令生成可烧录的最小系统
进入工作目录,执行:
nimmake init --target gd32vf103 --toolchain riscv64-unknown-elf该命令会:
- 创建
project.nimk,预填 GD32VF103 的memory_map(Flash 128K @ 0x08000000,RAM 32K @ 0x20000000) - 生成
src/main.c,包含标准 RISC-V 启动流程(__attribute__((section(".init")))定义_start) - 创建
src/startup_gd32vf103.S,包含mret返回指令和mtvec向量表设置 - 生成
linker.gd32vf103.ld,定义.text,.rodata,.data,.bss段地址 - 初始化 Git 仓库并添加
.gitignore(过滤build/,*.elf,*.bin)
此时目录结构为:
my_project/ ├── project.nimk ├── src/ │ ├── main.c │ └── startup_gd32vf103.S ├── linker.gd32vf103.ld └── build/ # 构建输出目录(首次为空)编辑src/main.c,添加一个 LED 闪烁:
#include "gd32vf103.h" int main(void) { rcu_periph_clock_enable(RCU_GPIOA); gpio_init(GPIOA, GPIO_MODE_OUTPUT, GPIO_OSPEED_50MHZ, GPIO_PIN_0); while(1) { gpio_bit_set(GPIOA, GPIO_PIN_0); for(volatile int i=0; i<1000000; i++); gpio_bit_reset(GPIOA, GPIO_PIN_0); for(volatile int i=0; i<1000000; i++); } }保存后,执行构建:
nimmake build输出:
[INFO] parse_config: Loaded GD32VF103 config [INFO] gen_startup: Generated startup_gd32vf103.S [INFO] gen_linker_script: Generated linker.gd32vf103.ld [INFO] compile_c: Compiled 1 source files [INFO] link_elf: Linked to build/firmware.elf [INFO] gen_bin: Generated build/firmware.bin (offset 0x08000000) [INFO] calc_crc: CRC32=0x1a2b3c4d written to build/firmware.bin [SUCCESS] Build completed in 2.3sbuild/firmware.bin即可直接用 J-Link 或 OpenOCD 烧录。整个过程无需修改任何构建脚本,所有配置都在project.nimk中声明。
4.3 硬件烧录:打通最后一公里,支持主流调试器
Nimmake 不止于生成文件,它把烧录也纳入构建闭环。在project.nimk中添加:
flash_tool: type: "jlink" device: "GD32VF103C8" speed: 4000 # 或 type: "openocd", 需配置 openocd_path 和 config_file然后执行:
nimmake flashNimmake 会:
- 检查
JLinkGDBServerCL是否在 PATH - 生成临时 J-Link script:
device GD32VF103C8 speed 4000 si swd loadfile build/firmware.bin 0x08000000 r q - 调用
JLinkGDBServerCL -if swd -device GD32VF103C8 -speed 4000 -CommanderScript jlink_temp.jlink - 实时捕获输出,若烧录失败(如 SWD 连接超时),高亮显示
J-Link connection failed并建议检查SWDIO/SWCLK线序。
对于 OpenOCD,配置:
flash_tool: type: "openocd" openocd_path: "/usr/bin/openocd" config_file: "openocd_gd32vf103.cfg"Nimmake 会自动生成 OpenOCD 命令:
openocd -f openocd_gd32vf103.cfg -c "program build/firmware.bin verify reset exit"关键是verify参数——它会读回 Flash 数据并比对 CRC,确保烧录 100% 正确。很多 DIY 工具省略这步,导致固件静默损坏。
这一整套流程,把“写代码 → 编译 → 烧录 → 验证”压缩到两个命令:nimmake build和nimmake flash。新人不需要知道 J-Link 和 OpenOCD 的区别,只需要在project.nimk里改type字段。
5. 常见问题与实战排障:那些官网文档不会写的坑
5.1 “No rule to make target 'build'” —— 不是语法错,是 Nimmake 版本不匹配
这是新手最高频报错。表面看是 Makefile 错误,实则是 Nimmake CLI 解析逻辑变更。Nimmake 0.8.0 之前,nimmake build是默认命令;0.9.0 起改为nimmake --build。如果你用curl下载的旧版二进制,而教程是新版写的,就会出现此错。
排查步骤:
- 运行
nimmake --version,确认版本号 - 查看
nimmake help输出的第一行:“Usage: nimmake [OPTIONS] COMMAND” - 若版本 < 0.9.0,且帮助中显示
nimmake build,则用旧命令;若版本 ≥ 0.9.0,必须用nimmake --build
根本原因:Nimmake 0.9.0 重构了 CLI 解析器,从cligen切换到docopt,以支持子命令嵌套(如nimmake flash --dry-run)。这不是 bug,而是 API 演进。
实操心得:永远用
nimmake --version开头。我在客户现场遇到过 3 次此问题,都是因为运维同事用apt install nimmake装了 Ubuntu 仓库里的 0.7.2 版本,而开发用的是 GitHub 最新版。解决方案是统一用curl -L https://github.com/nimmake/nimmake/releases/download/v0.9.2/nimmake-linux-amd64 -o nimmake && chmod +x nimmake直接下载二进制。
5.2 “undefined reference toSystemInit” —— 启动文件缺失的隐性陷阱
GCC 报这个错,通常意味着链接器找不到SystemInit符号。但src/下明明有system_gd32vf103.c,为什么还报错?
真相:Nimmake 的compile_cTask 默认只编译src/*.c,但system_gd32vf103.c在lib/目录下。Nimmake 的约定是:lib/下的文件需显式声明在project.nimk的sources字段:
sources: - "src/main.c" - "lib/system_gd32vf103.c" - "lib/gpio.c"如果不声明,compile_c会跳过lib/,导致SystemInit未定义。
快速修复:运行nimmake init时,它会自动把芯片 SDK 的lib/路径写入sources。但如果手动移动了文件,或用了第三方 SDK,就必须自己补全。
注意:Nimmake 不扫描子目录。
sources: ["src/**.c"]是无效的,必须列出每个文件。这是刻意设计——避免意外编译测试代码或废弃模块。
5.3 “CRC32 mismatch after flash” —— 烧录后校验失败的硬件根源
nimmake flash显示Verify OK,但设备不运行,用nimmake verify检查发现 CRC32 不匹配。
排查链:
- 先确认
nimmake verify读取的是 Flash 数据,而非 RAM:nimmake verify --source flash - 若仍失败,检查 Flash 编程算法。GD32VF103 的 Flash 控制器要求:
- 擦除前必须解锁:
FLASH->KEYR = 0x89ABCDEF; FLASH->KEYR = 0x02030405 - 编程时需等待
FLASH->STAT & FLASH_STAT_BSY == 0 - 页擦除后,该页所有位必须为 0xFF,否则编程失败
- 擦除前必须解锁:
J-Link 默认使用通用 Flash 算法,可能不兼容 GD32 的特殊时序。解决方案:
- 下载 Segger 官方 GD32 Flash loader:https://www.segger.com/downloads/jlink/GD32VF103_FlashLoader.zip
- 在
project.nimk中指定:flash_tool: type: "jlink" device: "GD32VF103C8" flash_loader: "GD32VF103_FlashLoader.jlink"
实战教训:我在调试一个批量烧录产线时,发现 5% 的板子 CRC 失败。最终定位是 J-Link 固件版本过旧(V6.98),升级到 V7.82 后问题消失。Nimmake 无法解决硬件固件问题,但它能把错误日志精确到“Flash loader version mismatch”,而不是笼统的“verify failed”。
5.4 “Peripherals not found in chip database” —— 新芯片支持的快速接入法
你想用 Nimmake 支持一款刚发布的 RISC-V MCU,但chipdb/里没有它的定义。
官方流程是 PR 提交芯片数据,但项目等不及。Nimmake 提供了 runtime 注入机制:
- 创建
chipdb/custom_chip.nim:
import nimmake/chipdb const my_new_chip = Chip( name: "MY_RISCV_CHIP", flash_controller: FlashController( base_addr: 0x40002000, cr_reg: "FLASH_CR", sr_reg: "FLASH_SR" ), vector_table_offset: 0x08000000, default_clock_tree: ClockTree(hse_freq: 25_000_000) ) # 注册到全局芯片库 registerChip("my_riscv_chip", my_new_chip)- 在
project.nimk中引用:
target: "my_riscv_chip" chipdb_path: "chipdb/custom_chip.nim"Nimmake 会在编译时动态加载custom_chip.nim,无需修改源码。这个机制让 Nimmake 支持新芯片的周期从“周级”缩短到“小时级”。
我用这招在 GD32V 系列发布当天就完成了支持,比官方 SDK 早 3 天。关键是
registerChip的 API 稳定,即使 Nimmake 升级,只要芯片结构体字段不变,你的custom_chip.nim就一直有效。
6. 进阶应用:从单固件构建到多镜像协同开发
6.1 Bootloader + Application 双镜像构建
MCU 产品常需 Bootloader(负责 OTA 升级)和 Application(业务逻辑)分离。传统做法是维护两套 Makefile,容易同步错误。Nimmake 用profiles实现单配置多产出。
在project.nimk中定义:
profiles: bootloader: target: stm32f407vg memory_map: flash: start: 0x08000000 size: 32K sources: - "src/bootloader/main.c" - "src/bootloader/usart_dfu.c" application: target: stm32f407vg memory_map: flash: start: 0x08008000 # Bootloader 占用 32K size: 992K sources: - "src/app/main.c" - "src/app/usb_msc.c"构建时指定 profile:
nimmake build --profile bootloader nimmake build --profile applicationNimmake 会为每个 profile 生成独立的build/bootloader/和build/application/目录,且自动校验application的flash.start是否大于bootloader的flash.size,防止 Flash 重叠。
更进一步,用dependencies实现镜像联动:
profiles: application: dependencies: - "bootloader" post_build: - "python scripts/merge_images.py build/bootloader/firmware.bin build/application/firmware.bin output/merged.bin"post_build脚本在application构建完成后执行,把 Bootloader 和 App 合并为单个.bin,供产线烧录。