系列导读
本文深入剖析 Arduino IDE 如何在不使用 Makefile 的前提下,完成从源码到 .bin 的完整编译链路。以 Arduino UNO R4 Minima + Microchip LAN8650/LAN8651 + Arduino_10BASE_T1S 库为具体实例,追踪 arduino-builder / arduino-cli 如何自动扫描源文件、解析 #include、填充 platform.txt recipe 中的变量,最终调用真实的 arm-none-eabi-gcc 编译器。
同系列其它文章
1. 核心问题:Arduino 到底有没有 Makefile?
简短回答:Arduino 底层并不是”没有 Makefile”,而是有一个自动生成的隐式 Makefile,它的逻辑被硬编码在 arduino-builder(Java)或 arduino-cli(Go)的源码中。Makefile 的”规则”变成了 platform.txt 中的 recipe,Makefile 的”变量”变成了 boards.txt 中的 build.xxx。
类比:
| 传统 Makefile 世界 |
Arduino 世界 |
Makefile |
platform.txt (recipe 模板) |
Makefile 中的 SRCS := *.c |
arduino-builder 递归扫描 src/ |
Makefile 中的 CFLAGS := -D... |
boards.txt 中的 minima.build.defines=... |
make 执行规则 |
arduino-builder fork+exec GCC |
.d 依赖文件 |
GCC -MMD 生成的 .d 文件 |
make -j8 并行编译 |
arduino-builder 并行执行无依赖的 .c/.cpp 编译 |
2. 整体架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| ┌────────────────────────────────────────────────────────────────────────┐ │ Arduino IDE 2.x (前端 UI) │ │ 点击 "验证" / "上传" │ └─────────────────────────────┬──────────────────────────────────────────┘ │ JSON-RPC / 内部调用 ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ arduino-builder (Java) 或 arduino-cli (Go) │ │ 真正的构建引擎 │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Phase 1: 源码发现 │ │ │ │ • 递归扫描 .c/.cpp/.S 文件(~150 个) │ │ │ │ • 解析 #include,收集所有头文件路径 │ │ │ │ • 读取 library.properties 识别库 │ │ │ │ • 合并同目录 .ino 文件为一个 .cpp │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Phase 2: 变量替换 │ │ │ │ • 从 boards.txt 读取所有 {build.xxx} 变量 │ │ │ │ • 从 platform.txt 读取所有 {compiler.xxx} 变量 │ │ │ │ • 从 variant/*.txt 读取 FSP 编译参数 │ │ │ │ • 替换 recipe 中的所有 {xxx} 占位符 │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Phase 3: 命令执行 │ │ │ │ • recipe.c.o.pattern → arm-none-eabi-gcc × N (并行) │ │ │ │ • recipe.cpp.o.pattern → arm-none-eabi-g++ × N (并行) │ │ │ │ • recipe.ar.pattern → arm-none-eabi-ar (core.a) │ │ │ │ • recipe.c.combine → arm-none-eabi-g++ (链接 .elf) │ │ │ │ • recipe.objcopy.bin → arm-none-eabi-objcopy (.bin) │ │ │ │ • recipe.size.pattern → arm-none-eabi-size (报告大小) │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ 增量编译支持 │ │ │ │ • GCC -MMD 为每个 .o 生成 .d 依赖文件 │ │ │ │ • 下次编译跳过未变化的 .cpp 及其依赖的 .h │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────┬──────────────────────────────────────────┘ ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ GNU Arm Embedded Toolchain (真实编译器) │ │ arm-none-eabi-gcc arm-none-eabi-g++ arm-none-eabi-ar objcopy │ └────────────────────────────────────────────────────────────────────────┘
|
3. Phase 1:源码发现——{includes} 是如何填满的
3.1 扫描路径(4 个来源)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 需要扫描的目录(按优先级排序):
1. Sketch 所在目录 └── UDP_Client.ino ← 用户编辑的入口 ├── .ino 本身(Arduino 会合并为 UDP_Client.cpp) └── 同一目录所有 .cpp/.c/.S
2. 库目录 (libraries/Arduino_10BASE_T1S/src/) └── 递归扫描所有 .c/.cpp/.S ├── Arduino_10BASE_T1S_UDP.cpp ├── MacAddress.cpp ├── T1SPlcaSettings.cpp ├── T1SMacSettings.cpp ├── microchip/TC6_Arduino_10BASE_T1S.cpp ├── microchip/TC6_Io.cpp ├── microchip/lib/libtc6/src/tc6.cpp ├── microchip/lib/libtc6/src/tc6-regs.cpp ├── lib/liblwip/core/udp.c ├── lib/liblwip/core/ipv4/ip4.c ├── lib/liblwip/core/netif/ethernet.c └── ... (~80 个 lwIP .c 文件)
3. Arduino 核心目录 %LOCALAPPDATA%\Arduino15\packages\arduino\hardware\ renesas_uno\1.6.0\cores\arduino\ ├── main.cpp ├── wiring.c ├── HardwareSerial.cpp ├── Print.cpp └── ... (~30 个)
4. Variant 目录(板级特定源码) %LOCALAPPDATA%\...\variants\MINIMA\ └── includes/ ← 仅头文件路径,不含 .c/.cpp
|
3.2 .ino 文件合并(Sketch 预处理)
Arduino 在编译前会把同一目录下所有 .ino 文件按文件名排序后首尾拼接为一个 .cpp,并在其中插入必要的声明。以下是实际发生过程的伪代码表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
#include <Arduino_10BASE_T1S.h>
void setup() { Serial.begin(115200); }
void loop() { }
|
合并规则:
- 多个
.ino 按文件名字母顺序拼接(_01_*.ino 在 _02_*.ino 之前)
- Arduino 在拼接后的文件顶部自动插入
#include <Arduino.h>
- 这就是为什么 sketch 中不需要
#include <Arduino.h> 也能使用 Serial、pinMode 等
3.3 头文件路径收集——{includes} 的填充
这是整个机制最精妙的部分。Arduino 不仅扫描 .c/.cpp,还会:
- 解析每个源文件的
#include 语句(递归解析所有层级)
- 将路径转换为
-I 编译器参数,组装成 {includes} 变量
以 Arduino_10BASE_T1S 库中的关键文件为例:
1 2 3 4 5
| #include "Arduino_10BASE_T1S_PHY_Interface.h" #include "TC6_Io.h" #include "../../lib/liblwip/include/lwip/netif.h" #include "lib/liblwip/include/lwip/ip_addr.h"
|
1 2 3 4
| #include "tc6.h" #include "tc6-conf.h" #include <string.h>
|
会生成对应的 -I 参数:
1 2 3 4 5 6
| -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/" -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/microchip/" -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/microchip/lib/libtc6/inc/" -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/microchip/lib/liblwip/include/" -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/lib/liblwip/include/" -I"%LOCALAPPDATA%/.../cores/arduino/"
|
最终,{includes} 的典型展开包含 约 50 个 -I 参数,覆盖 sketch 层、库层、核心层和 variant FSP 层。
3.4 依赖文件生成(-MMD)
注意 platform.txt 中的编译 flags:
1 2
| compiler.cpp.flags=... -MMD ... compiler.c.flags=... -MMD ...
|
-MMD 告诉 GCC 生成 .d 依赖文件(.d = dependency)。编译 Arduino_10BASE_T1S_UDP.cpp 时会同时生成 Arduino_10BASE_T1S_UDP.d:
1 2 3 4 5 6 7 8 9 10 11 12
|
Arduino_10BASE_T1S_UDP.cpp.o: \ Arduino_10BASE_T1S_UDP.cpp \ Arduino_10BASE_T1S_UDP.h \ ../lib/liblwip/include/lwip/netif.h \ ../lib/liblwip/include/lwip/udp.h \ ../lib/liblwip/include/lwip/pbuf.h \ ../../../cores/arduino/Arduino.h \ ../../../cores/arduino/Print.h \ ...
|
Arduino-builder 读取所有 .d 文件,用于增量编译:如果某个 .h 文件没有变化,则不重新编译依赖它的 .cpp。
4.1 build 变量的来源与优先级
Arduino 按以下优先级合并变量(后面的覆盖前面的同名变量):
1 2 3 4 5 6 7
| platform.txt (基础值,所有板子共享) ↓ boards.txt (minima.xxx 覆盖同名 xxx) ↓ platform.local.txt (用户本地覆盖,不受 IDE 升级影响) ↓ Sketch 的 boards.local.txt
|
4.2 UNO R4 Minima 的关键变量展开
以 UNO R4 Minima 为例,关键 {build.xxx} 和 {compiler.xxx} 变量展开如下:
| 占位符 |
展开值 |
来源 |
{build.mcu} |
cortex-m4 |
boards.txt |
{build.float-abi} |
-mfloat-abi=hard |
boards.txt |
{build.fpu} |
-mfpu=fpv4-sp-d16 |
boards.txt |
{build.crossprefix} |
arm-none-eabi- |
boards.txt |
{build.compiler_path} |
%LOCALAPPDATA%...\arm-none-eabi-gcc\7-2017q4\bin\ |
boards.txt |
{build.variant.path} |
%LOCALAPPDATA%...\variants\MINIMA\ |
boards.txt |
{build.core.path} |
%LOCALAPPDATA%...\cores\arduino\ |
boards.txt |
{build.defines} |
-DF_CPU=48000000 -DARDUINO_UNOR4_MINIMA |
boards.txt |
{build.board} |
MINIMA |
boards.txt |
{compiler.fsp} |
%LOCALAPPDATA%...\variants\MINIMA\libs\libfsp.a |
boards.txt |
{compiler.fsp.cxxflags} |
-mthumb "@{compiler.fsp.defines}" |
boards.txt |
{compiler.fsp.includes} |
@{build.variant.path}/includes.txt |
boards.txt |
{compiler.tinyusb.cxxflags} |
-DCFG_TUSB_MCU=OPT_MCU_RAXXX |
boards.txt |
{build.variant.path}/defines.txt |
-D_RA_CORE=CM4 -D_RENESAS_RA_ |
MINIMA/defines.txt |
{build.variant.path}/includes.txt |
14 行 -iwithprefixbefore/... |
MINIMA/includes.txt |
4.3 命令逐步展开实例
以编译 Arduino_10BASE_T1S_UDP.cpp 为例,展示从 recipe 模板到真实 GCC 命令的每一步展开。
1 2 3 4 5 6 7 8
| {compiler.path}{compiler.cpp.cmd} {compiler.cpp.flags} -DARDUINO={runtime.ide.version} "-DPROJECT_NAME="{build.path}/{build.project_name}"" -DARDUINO_{build.board} -DARDUINO_ARCH_{build.arch} -DARDUINO_ARCH_RENESAS -DARDUINO_FSP -D_XOPEN_SOURCE=700 {compiler.fsp.cxxflags} {compiler.tinyusb.cxxflags} {compiler.cpp.extra_flags} {build.extra_flags} {tinyusb.includes} "-I{build.core.path}/api/deprecated" "-I{build.core.path}/api/deprecated-avr-comp" {includes} "-iprefix{runtime.platform.path}" "@{compiler.fsp.includes}" "{source_file}" -o "{object_file}"
|
Step 1:替换编译器路径和命令名
1 2 3
| "C:\...\arm-none-eabi-gcc\7-2017q4\bin\arm-none-eabi-g++" {compiler.cpp.flags} -DARDUINO=... -DARDUINO_MINIMA -DARDUINO_ARCH_RENESAS ... {includes} "{source_file}" -o "{object_file}"
|
Step 2:替换 {compiler.cpp.flags}(展开 ~15 个编译选项)
1 2 3 4 5 6 7 8
| -w -Os -g3 -fno-use-cxa-atexit -fno-rtti -fno-exceptions -MMD -nostdlib -DF_CPU=48000000 -DARDUINO_UNOR4_MINIMA -std=gnu++17 -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -fsigned-char -ffunction-sections -fdata-sections -fmessage-length=0 -fno-builtin
|
Step 3:替换 FSP 相关变量
1
| -mthumb "@{compiler.fsp.defines}"
|
↓ 读取 MINIMA/defines.txt 内容 -D_RA_CORE=CM4 -D_RENESAS_RA_,展开为:
1
| -mthumb -D_RA_CORE=CM4 -D_RENESAS_RA_
|
Step 4:替换 {includes}(约 50 个 -I 参数)
1 2 3 4 5 6 7 8
| -I"%LOCALAPPDATA%/.../cores/arduino/api/deprecated" -I"%LOCALAPPDATA%/.../cores/arduino/api/deprecated-avr-comp" -ID:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src -I%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc -I%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc/api ... -iprefix%LOCALAPPDATA%/.../renesas_uno/1.6.0/ @%LOCALAPPDATA%/.../variants/MINIMA/includes.txt
|
Step 5:替换源文件和输出路径
1 2
| "D:/.../Arduino_10BASE_T1S/src/Arduino_10BASE_T1S_UDP.cpp" -o "D:/.../build/Arduino_10BASE_T1S_UDP.cpp.o"
|
最终生成的真实命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| "C:\Users\cursorhu\AppData\Local\Arduino15\packages\arduino\tools\ arm-none-eabi-gcc\7-2017q4\bin\arm-none-eabi-g++" \ -c \ -w -Os -g3 \ -fno-use-cxa-atexit \ -fno-rtti \ -fno-exceptions \ -MMD \ -nostdlib \ -DF_CPU=48000000 \ -DARDUINO_UNOR4_MINIMA \ -std=gnu++17 \ -mcpu=cortex-m4 \ -mfloat-abi=hard \ -mfpu=fpv4-sp-d16 \ -fsigned-char \ -ffunction-sections \ -fdata-sections \ -fmessage-length=0 \ -fno-builtin \ -mthumb \ -D_RA_CORE=CM4 -D_RENESAS_RA_ \ -DCFG_TUSB_MCU=OPT_MCU_RAXXX \ -DARDUINO=20300 \ -DARDUINO_MINIMA \ -DARDUINO_ARCH_RENESAS \ -DARDUINO_ARCH_RENESAS \ -DARDUINO_FSP \ -D_XOPEN_SOURCE=700 \ -I"%LOCALAPPDATA%/.../cores/arduino/api/deprecated" \ -I"%LOCALAPPDATA%/.../cores/arduino/api/deprecated-avr-comp" \ -I"D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src" \ -I"%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc" \ -I"%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc/api" \ -I"%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc/instances" \ -I"%LOCALAPPDATA%/.../variants/MINIMA/includes/ra_gen" \ ... \ -iprefix"%LOCALAPPDATA%/.../renesas_uno/1.6.0/" \ @"%LOCALAPPDATA%/.../variants/MINIMA/includes.txt" \ "D:/Documents/Arduino/libraries/Arduino_10BASE_T1S/src/Arduino_10BASE_T1S_UDP.cpp" \ -o "D:/.../AppData/Local/Temp/arduino/sketches/.../build/Arduino_10BASE_T1S_UDP.cpp.o"
|
这就是开启”编译时显示详细输出”时在 Arduino IDE 控制台看到的实际命令。每一行背后都是一个 ProcessBuilder.exec()(Java)或 exec.Command()(Go),对应一个真实的 GCC 进程。
5. Phase 3:编译执行流程(6 条 recipe 的串/并行)
5.1 六个 recipe 总览
| 顺序 |
recipe 名称 |
执行内容 |
并行性 |
| 1 |
recipe.c.o.pattern |
gcc -c *.c → *.o |
并行(~30 个 .c) |
| 2 |
recipe.cpp.o.pattern |
g++ -c *.cpp → *.o |
并行(~120 个 .cpp) |
| 3 |
recipe.S.o.pattern |
g++ -c *.S → *.o |
串行(汇编文件很少) |
| 4 |
recipe.ar.pattern |
ar rcs core.a *.o |
串行(依赖步骤 1-3) |
| 5 |
recipe.c.combine.pattern |
g++ -o *.elf |
串行(依赖步骤 4) |
| 6 |
recipe.objcopy.bin.pattern |
objcopy *.elf → *.bin |
串行(依赖步骤 5) |
5.2 并行编译的细节
arduino-builder 使用线程池并行执行无依赖关系的编译任务:
1 2 3 4 5 6 7
| CPU 核心 1: arm-none-eabi-g++ → Arduino_10BASE_T1S_UDP.cpp.o CPU 核心 2: arm-none-eabi-g++ → TC6_Arduino_10BASE_T1S.cpp.o CPU 核心 3: arm-none-eabi-g++ → TC6_Io.cpp.o CPU 核心 4: arm-none-eabi-g++ → libtc6/tc6.cpp.o CPU 核心 5: arm-none-eabi-g++ → liblwip/core/udp.c.o CPU 核心 6: arm-none-eabi-g++ → liblwip/core/ipv4/ip4.c.o ... (同时运行 4-8 个进程,取决于 CPU 核心数)
|
5.3 各 recipe 详解
1 2 3 4 5
| arm-none-eabi-gcc [flags] -o xxx.o xxx.c
arm-none-eabi-g++ [flags] -o xxx.o xxx.cpp
|
注意 -MMD:每个编译任务同时生成一个 .d 依赖文件。
1 2 3 4 5 6 7
| arm-none-eabi-ar rcs build/core.a \ main.cpp.o \ wiring.c.o \ HardwareSerial.cpp.o \ Print.cpp.o \ Stream.cpp.o \ ... (所有 Arduino 核心层的 .o)
|
这是所有 .o + core.a + libfsp.a 最终合并为 .elf 的步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| arm-none-eabi-g++ \ -Wl,--gc-sections \ --specs=nosys.specs \ -mcpu=cortex-m4 \ -mfloat-abi=hard \ -mfpu=fpv4-sp-d16 \ -o "build/UDP_Client.ino.elf" \ -L"build" \ -L"%LOCALAPPDATA%/.../variants/MINIMA/" \ -T"%LOCALAPPDATA%/.../variants/MINIMA/fsp.ld" \ build/*.o \ -Wl,--whole-archive \ "%LOCALAPPDATA%/.../variants/MINIMA/libs/libfsp.a" \ build/core.a \ -Wl,--no-whole-archive \ --specs=nano.specs \ -lstdc++ -lsupc++ -lm -lc -lgcc -lnosys \ -Wl,--end-group \ -Wl,-Map,build/UDP_Client.ino.map
|
关键参数解读:
| 参数 |
作用 |
-Wl,--gc-sections |
丢弃未引用的函数和数据段(需要 -ffunction-sections 配合) |
--specs=nosys.specs |
不链接系统调用(嵌入式无 OS) |
-T fsp.ld |
指定链接脚本(定义 Flash/RAM 布局) |
-Wl,--whole-archive ... -Wl,--no-whole-archive |
强制链接 libfsp.a 和 core.a 中所有符号(包括未引用的) |
--specs=nano.specs |
使用 newlib-nano(精简版 C 库) |
-Wl,-Map,*.map |
生成符号映射文件 |
Recipe 5 & 6:生成 .bin
1 2 3 4 5
| arm-none-eabi-objcopy -O binary -j .text -j .data \ UDP_Client.ino.elf \ UDP_Client.ino.bin
arm-none-eabi-size -A UDP_Client.ino.elf
|
6. 完整的参数替换链路图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| boards.txt minima.build.mcu=cortex-m4 minima.build.float-abi=-mfloat-abi=hard minima.build.fpu=-mfpu=fpv4-sp-d16 minima.build.defines=-DF_CPU=48000000 -DARDUINO_UNOR4_MINIMA minima.compiler.fsp.cxxflags=-mthumb "@{compiler.fsp.defines}" minima.compiler.fsp={build.variant.path}/libs/libfsp.a minima.compiler.fsp.includes={build.variant.path}/includes.txt │ │ platform.txt │ compiler.cpp.cmd={build.crossprefix}g++ │ compiler.cpp.flags=... -mcpu={build.mcu} {build.float-abi} {build.fpu} ... │ recipe.cpp.o.pattern="{compiler.path}{compiler.cpp.cmd}" {compiler.cpp.flags} ... │ │ │ │ MINIMA/defines.txt │ │ -D_RA_CORE=CM4 │ │ -D_RENESAS_RA_ │ │ │ │ │ │ arduino-builder 源码发现 │ │ │ {includes} = -I... -I... -I... (50+ 个) │ │ │ {source_file} = Arduino_10BASE_T1S_UDP.cpp │ │ │ {object_file} = Arduino_10BASE_T1S_UDP.cpp.o │ │ │ │ │ │ │ ▼ │ │ │ "C:\...\arm-none-eabi-g++" \ │ │ │ -c -Os -mcpu=cortex-m4 \ │ │ │ -DF_CPU=48000000 \ │ │ │ -DARDUINO_UNOR4_MINIMA \ │ │ │ -D_RA_CORE=CM4 \ │ │ │ -mthumb \ │ │ │ -I"D:/.../Arduino_10BASE_T1S/src" \ │ │ │ -I"%LOCALAPPDATA%/.../variants/MINIMA/includes/ra/fsp/inc" \ │ │ │ ... \ │ │ │ "Arduino_10BASE_T1S_UDP.cpp" \ │ │ │ -o "Arduino_10BASE_T1S_UDP.cpp.o" │ │ │ │ │ ▼ │ │ fork+exec → arm-none-eabi-g++ 进程 ← 真实编译器 │ │ │ │ │ ▼ │ │ build/Arduino_10BASE_T1S_UDP.cpp.o (150 个 .o 之一) │ │ │ ▼ │ build/core.a (ar 打包所有核心 .o) │ │ │ ▼ │ arm-none-eabi-g++ (链接) │ + libfsp.a + core.a + 所有 .o │ │ │ ▼ │ build/UDP_Client.ino.elf │ │ │ ▼ │ arm-none-eabi-objcopy │ │ │ ▼ │ build/UDP_Client.ino.bin ← 最终烧录文件
|
7. 如何查看 Arduino 实际生成的完整编译命令
7.1 方法 1:arduino-cli dry-run(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| arduino-cli compile ` --fqbn arduino:renesas_uno:minima ` --verbose ` --only-compilation-database ` --build-path ./build_verbose ` ./examples/UDP_Client/UDP_Client.ino
Get-Content ./build_verbose/compile_commands.json | ConvertFrom-Json | Select-Object -First 3 | ForEach-Object { Write-Host "=== $($_.file) ===" -ForegroundColor Cyan Write-Host $_.command }
|
compile_commands.json 是 JSON 格式的编译数据库,每一项包含完整的编译器命令,可直接用于 Clangd / VSCode C++ Intellisense 或分析工具。
7.2 方法 2:查看 build 目录
1 2 3 4 5 6 7 8 9 10
| ls build/*.o | Measure-Object ls build/*.d | Measure-Object ls build/*.a
Get-Content build/Arduino_10BASE_T1S_UDP.cpp.d
arm-none-eabi-ar -t build/core.a | Select-Object -First 20
|
7.3 方法 3:Arduino IDE 详细输出
1 2 3
| 文件 → 首选项 ☑ 编译时显示详细输出 ☑ 上传时显示详细输出
|
控制台会显示每一条 arm-none-eabi-g++ 命令的全部参数。
7.4 方法 4:用 PowerShell 过滤感兴趣的命令
1 2 3 4 5 6 7 8 9
| arduino-cli compile --verbose ... 2>&1 | Select-String "lwip"
arduino-cli compile --verbose ... 2>&1 | Select-String "libtc6"
arduino-cli compile --verbose ... 2>&1 | Select-String "arm-none-eabi-g\+\+" | Where-Object { $_.ToString().Length -gt 500 }
|
8. 源码分析:arduino-builder 内部做了什么
如果你想深入到代码层面,arduino-builder 的核心逻辑在以下文件中(从 github.com/arduino/arduino-builder 获取):
8.1 源码发现逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
props := loader.LoadHardware(...) boardProps := props.SubTree("minima.")
allFlags := Merge(platformFlags, boardFlags, variantFlags)
sketch := loadSketch(sketchPath) libraries := findLibraries(sketch) sourceFiles := discoverSourceFiles(sketch, libraries, core)
includes := collectIncludes(sourceFiles)
for _, src := range sourceFiles { cmd := buildCommand(recipe, { "{source_file}": src.Path, "{object_file}": objPath(src), "{includes}": includes, "{build.xxx}": allFlags["build.xxx"], }) exec.Command(cmd) }
exec.Command("ar", "rcs", "core.a", coreObjects...)
exec.Command("g++", "-o", "*.elf", allObjects, "core.a", "libfsp.a", ldScript, ...)
|
8.2 增量编译逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if incrementalBuildEnabled { for _, src := range sourceFiles { obj := objPath(src) dep := depPath(src)
if !needsRebuild(src, dep, obj) { continue } compile(src, obj) } }
|
9. 常见误解澄清
误解 1:”Arduino 不用 Makefile 所以很快”
事实:Arduino 的构建速度往往比纯 Makefile 更慢,因为:
- 每次编译都要重新扫描所有源文件(没有预先计算的
compile_commands.json)
- 没有精细的增量依赖跟踪(仅靠
.d 文件,不追踪更复杂的 include 依赖链)
- 并行度受限于线程池,不及
make -j$(nproc) 灵活
误解 2:”Arduino 编译的是 .ino 文件”
事实:.ino 只是 sketch 的入口,真正的编译单元是 Arduino 合并后的 .cpp + 所有 src/*.cpp + cores/*.c。.ino 本身不直接被 GCC 处理。
事实:platform.txt 只定义 recipe 模板(如何调用 GCC),而 build 变量(MCU 型号、FPU 参数、链接脚本路径)全部来自 boards.txt。两者缺一不可。
误解 4:”库只在第一次编译时扫描”
事实:每次编译 Arduino 都会重新扫描 src/ 目录。如果库文件变化,或 sketch 中 #include 了新的库,下次编译会自动包含。Arduino IDE 会缓存扫描结果,但缓存失效时仍需重新扫描。
10. 总结
10.1 三层抽象对照
| 层级 |
机制 |
负责什么 |
| 用户层 |
#include + src/ 自动扫描 |
用户无需声明源文件 |
| 模板层 |
platform.txt recipe 模式 + boards.txt 变量 |
一个 recipe 通用于所有板子 |
| 执行层 |
arduino-builder / arduino-cli |
填充变量 → fork GCC → 并行执行 |
10.2 关键文件的作用
| 文件 |
位置 |
作用 |
platform.txt |
hardware/renesas_uno/1.6.0/ |
定义 recipe 模板和编译器参数 |
boards.txt |
hardware/renesas_uno/1.6.0/ |
定义板级变量(MCU、Flash、RAM、链接脚本) |
fsp.ld |
variants/MINIMA/ |
链接脚本(Flash/RAM 布局、段定义) |
defines.txt |
variants/MINIMA/ |
FSP 专用宏定义 |
includes.txt |
variants/MINIMA/ |
FSP 头文件搜索路径 |
library.properties |
libraries/Arduino_10BASE_T1S/ |
库元数据(版本、支持的架构) |
10.3 核心流程
1 2 3 4 5 6 7 8 9 10
| arduino-builder 启动 │ ├── 1. 递归扫描 src/ + sketch/ + cores/ → 150 个源文件 ├── 2. 解析所有 #include → 50 个 -I 参数 ├── 3. 读取 boards.txt + platform.txt → 替换 {build.xxx} 变量 ├── 4. 对每个 .c/.cpp 执行: arm-none-eabi-gcc/g++ -c (并行) ├── 5. ar rcs core.a (打包核心 .o) ├── 6. arm-none-eabi-g++ 链接 → .elf ├── 7. objcopy → .bin └── 8. size 报告 Flash/RAM 使用
|
本质:Arduino 用 recipe 模板代替 Makefile 的规则,用 自动扫描代替 Makefile 的 SRCS := *.c,用 arduino-builder 代替 make。Makefile 的角色被这个 Java/Go 程序接管了,但它生成的命令仍然是标准 GCC,输出的仍然是 .elf + .bin,链接脚本仍然是 .ld。对于习惯 Makefile 的工程师来说,Arduino 的构建系统本质上是一个带自动依赖管理的 GCC 调用器。
下一步阅读:
实用命令:arduino-cli compile --only-compilation-database --verbose --build-path ./build ... 生成的 compile_commands.json 是理解编译过程的最佳起点。