1. 1. 系列导读
    1. 1.1. 同系列其它文章
  2. 2. 1. 核心问题解答
    1. 2.1. 1.1 两个芯片,Arduino IDE 烧录到哪个?
    2. 2.2. 1.2 为什么你看不到底层库的编译过程?
  3. 3. 2. Arduino 库的标准结构与 Microchip 方案实例
    1. 3.1. 2.1 Arduino 库的目录约定
    2. 3.2. 2.2 Arduino IDE 的自动发现规则
    3. 3.3. 2.3 示例工程的可见文件数量
  4. 4. 3. Arduino IDE vs Keil 工程对比
    1. 4.1. 3.1 工程文件位置对比
    2. 4.2. 3.2 配置文件层次结构
  5. 5. 4. 内存映射详解
    1. 5.1. 4.1 memory_regions.ld 文件内容
    2. 5.2. 4.2 Flash 布局图
  6. 6. 5. 完整编译链接烧录流程(7 阶段)
    1. 6.1. 5.1 阶段 1:源文件扫描
    2. 6.2. 5.2 阶段 2:编译 (.c/.cpp → .o)
    3. 6.3. 5.3 阶段 3:打包核心库 (.o → core.a)
    4. 6.4. 5.4 阶段 4:链接 (.o + .a → .elf)
    5. 6.5. 5.5 阶段 5:生成二进制 (.elf → .bin)
    6. 6.6. 5.6 阶段 6:计算大小
    7. 6.7. 5.7 阶段 7:烧录到 MCU
  7. 7. 6. 关键产物详解
    1. 7.1. 6.1 .map 文件 — 最重要的调试工具
    2. 7.2. 6.2 三种产物文件
  8. 8. 7. 调试技巧
    1. 8.1. 7.1 启用详细编译输出
    2. 8.2. 7.2 使用 arduino-cli 命令行编译
    3. 8.3. 7.3 反汇编查看生成的代码
    4. 8.4. 7.4 分析 Flash 使用情况
    5. 8.5. 7.5 分析 RAM 使用情况
    6. 8.6. 7.6 各模块 Flash/RAM 占比速查
  9. 9. 8. 常见错误与解决方案
    1. 9.1. 8.1 Flash 空间不足
    2. 9.2. 8.2 RAM 空间不足
    3. 9.3. 8.3 链接错误:undefined reference
    4. 9.4. 8.4 链接错误:multiple definition
    5. 9.5. 8.5 烧录失败
    6. 9.6. 8.6 找不到库
  10. 10. 9. 开发工作流建议
    1. 10.1. 9.1 阶段化集成策略
    2. 10.2. 9.2 阶段 1 模板:仅验证 SPI(不依赖完整库)
    3. 10.3. 9.3 阶段 3 模板:lwIP loopback 测试
    4. 10.4. 9.4 完整工作流 checklist
  11. 11. 10. 推荐的工具链
    1. 11.1. 10.1 VSCode + Arduino 插件
    2. 11.2. 10.2 命令行:arduino-cli
    3. 11.3. 10.3 版本控制
    4. 11.4. 10.4 持续集成(GitHub Actions)
  12. 12. 11. 与 Keil 工程的完整操作对比
  13. 13. 12. 总结
    1. 13.1. 12.1 关键要点
    2. 13.2. 12.2 文件流程总结
    3. 13.3. 12.3 推荐工作流
    4. 13.4. 12.4 何时跳出 Arduino 框架
  14. 14. 13. 参考资源

Arduino IDE 编译架构深度解析:以 Microchip LAN8650/LAN8651 10BASE-T1S 为例

系列导读

本文面向首次接触 Arduino 嵌入式开发链路的传统 MCU 工程师(熟悉 Keil、IAR、STM32CubeIDE),以 Microchip LAN8650/LAN8651 + Arduino UNO R4 Minima + Arduino_10BASE_T1S v0.1.1 为蓝本,把 Arduino IDE 这一”看似点击即上传”工具背后的完整编译架构讲清楚。

同系列其它文章

# 标题
0 系列索引与全局速查
1 应用层 UDP Socket API
2 PHY Interface 与 HAL 层
3 libtc6 协议核心与 lwIP 集成
4 辅助类与 lwIP 配置
- 架构概览
- OA TC6 协议栈深度分析
- 移植实战指南

1. 核心问题解答

1.1 两个芯片,Arduino IDE 烧录到哪个?

Arduino UNO R4 Minima + 10BASE-T 扩展板系统架构

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
┌─────────────────────────────────────────────────────────────┐
│ Arduino UNO R4 Minima 主板 │
│ │
│ ┌──────────────────────┐ ┌────────────────────┐ │
│ │ Renesas RA4M1 │ │ 你的扩展板 │ │
│ │ (主MCU) │◄───SPI──►│ Microchip LAN8650 │ │
│ │ │ │ /LAN8651 MAC-PHY │ │
│ │ Cortex-M4 48MHz │ └────────────────────┘ │
│ │ 256KB Flash │ │
│ │ 32KB RAM │ ┌────────────────────┐ │
│ │ │ │ 以太网变压器 │ │
│ │ 运行的代码: │ │ + RJ45接口 │ │
│ │ - UDP_Client.ino │ └────────────────────┘ │
│ │ - lwIP协议栈 │ │
│ │ - libtc6 协议 │ │
│ │ - SPI 驱动 │ │
│ │ - MAC-PHY 寄存器配置 │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ USB转串口芯片 │ │
│ │ (用于烧录和串口通信) │ │
│ └──────────┬───────────┘ │
│ │ │
└─────────────┼──────────────────────────────────────────────┘
│ USB-C 线缆

PC (Arduino IDE)

明确答案

  • Arduino IDE 只烧录主 MCU (Renesas RA4M1)
  • Microchip LAN8650/LAN8651 MAC-PHY 芯片不需要烧录,它是 SPI 外设
  • 应用代码运行在 RA4M1 上,通过 SPI + OA TC6 协议控制 MAC-PHY
  • MAC-PHY 内部可能有 ROM 固件(OA TC6 协议栈),但不由 Arduino IDE 烧录

类比

  • RA4M1 主 MCU = STM32 主控芯片(运行你的代码)
  • LAN8650/LAN8651 = 外围的 W5500 网络芯片(通过 SPI 控制),区别是 LAN865x 使用 OA TC6 私有协议而非裸 SPI 寄存器

1.2 为什么你看不到底层库的编译过程?

当你在 Arduino IDE 2 中打开 UDP_Client.ino 并点击”上传”时,背后发生了这些事情(默认在 UI 中不可见):

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
你看到的:
┌─────────────────────────────┐
│ UDP_Client.ino │ <-- 你只编辑这个文件
│ #include <Arduino_10BASE… │
│ void setup() { ... } │
│ void loop() { ... } │
└─────────────────────────────┘
↓ 点击"上传"
[黑盒过程]

┌─────────────────────────────┐
│ 烧录到 Arduino R4 │
└─────────────────────────────┘

实际发生的(隐藏过程):
┌─────────────────────────────────────────────────────────────┐
│ 1. Arduino IDE 扫描 #include 语句 │
│ 发现: Arduino_10BASE_T1S.h │
├─────────────────────────────────────────────────────────────┤
│ 2. 在库目录中查找: │
│ Windows: %USERPROFILE%\Documents\Arduino\libraries\ │
│ 或: %LOCALAPPDATA%\Arduino15\libraries\ │
│ 找到: Arduino_10BASE_T1S/ │
├─────────────────────────────────────────────────────────────┤
│ 3. Arduino-CLI 递归扫描库的 src/ 目录: │
│ src/Arduino_10BASE_T1S_UDP.cpp ✓ 加入编译 │
│ src/MacAddress.cpp ✓ 加入编译 │
│ src/microchip/TC6_Arduino_10BASE_T1S.cpp ✓ 加入编译 │
│ src/microchip/TC6_Io.cpp ✓ 加入编译 │
│ src/microchip/lib/libtc6/src/tc6.cpp ✓ 加入编译 │
│ src/microchip/lib/libtc6/src/tc6-regs.cpp ✓ 加入编译 │
│ src/lib/liblwip/*.c ✓ 全部加入编译 │
├─────────────────────────────────────────────────────────────┤
│ 4. 调用 arm-none-eabi-g++ 编译器: │
│ 编译每个 .cpp/.c 文件 → .o 目标文件 │
│ 链接所有 .o + Arduino 核心库 → .elf 可执行文件 │
│ 转换 .elf → .bin 二进制镜像 │
├─────────────────────────────────────────────────────────────┤
│ 5. 调用 bossac 烧录工具: │
│ 通过 USB 串口上传 .bin 到 Arduino R4 Flash │
└─────────────────────────────────────────────────────────────┘

关键点:Arduino IDE 会自动编译 src/ 目录下的所有 .cpp.c 文件,你不需要手动指定。这就是为什么看起来”魔法”,但本质是 GNU Make + GCC 的工程化封装。


2. Arduino 库的标准结构与 Microchip 方案实例

2.1 Arduino 库的目录约定

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
Arduino_10BASE_T1S/                    # 库根目录

├── library.properties # 库元数据(IDE 读取这个识别库)
│ name=Arduino_10BASE_T1S
│ version=0.1.1
│ architectures=renesas_uno # 支持 UNO R4

├── examples/ # 示例代码(用户可见)
│ ├── UDP_Client/
│ │ └── UDP_Client.ino # ← 用户编辑这个
│ ├── UDP_Server/
│ │ └── UDP_Server.ino
│ └── pingpong/
│ └── pingpong.ino

└── src/ # 库源码(自动编译,用户不可见)
├── Arduino_10BASE_T1S.h # 主头文件
├── Arduino_10BASE_T1S_PHY_Interface.h # PHY 抽象基类
├── Arduino_10BASE_T1S_UDP.h # UDP Socket API
├── Arduino_10BASE_T1S_UDP.cpp # ← IDE 自动编译
├── MacAddress.h/.cpp # MAC 地址工具类
├── T1SPlcaSettings.h/.cpp # PLCA 配置
├── T1SMacSettings.h/.cpp # MAC 配置

├── microchip/ # Microchip 特定代码(vendor 子目录)
│ ├── TC6_Arduino_10BASE_T1S.h/.cpp # TC6 PHY 实现
│ ├── TC6_Io.h/.cpp # SPI HAL
│ └── lib/
│ ├── libtc6/ # Microchip OA TC6 核心(C API)
│ │ ├── inc/tc6.h # 公共 API
│ │ ├── inc/tc6-regs.h # LAN865x 寄存器表
│ │ ├── src/tc6.cpp # 协议状态机
│ │ ├── src/tc6-regs.cpp # 寄存器访问实现
│ │ └── cfg-example/tc6-conf.h # libtc6 配置宏
│ └── liblwip/ # 移植后的 lwIP 协议栈
│ ├── core/ # ~80 个 .c 文件
│ │ ├── ipv4/ip4.c
│ │ ├── udp.c
│ │ ├── tcp.c
│ │ └── ...
│ ├── netif/
│ │ ├── ethernet.c
│ │ └── etharp.c
│ └── cfg/lwipopts.h # lwIP 配置

└── lib/ # 第三方库(可与 vendor 同级)
└── liblwip/ # lwIP 协议栈(本仓库实际位置)
└── ...

2.2 Arduino IDE 的自动发现规则

  1. 任何在 src/ 目录下的 .cpp / .c / .S 文件都会被编译
  2. 包括所有子目录(递归扫描)—— 这是为什么 src/microchip/lib/libtc6/src/ 里的 100+ 文件会自动加入
  3. src/**/include/ 下的头文件自动加入编译器的 include 路径
  4. 用户只需要在 .ino 文件中 #include 主头文件
  5. 链接顺序与文件顺序无关,由符号解析自动完成(除非有 circular reference)

2.3 示例工程的可见文件数量

一个完整的 UDP_Client 编译大约涉及:

来源 文件数 说明
Sketch (UDP_Client.ino) 1 用户编辑
Arduino_10BASE_T1S src/ ~10 库本身
libtc6 src/ ~10 协议状态机、寄存器访问
liblwip core/ ~80 完整 TCP/IP 协议栈(仅用到的部分编译进去)
Arduino Core ~30 main.cpp, wiring.c, HardwareSerial.cpp
Renesas FSP 静态库 1 个 .a 链接形式提供,不可见
合计编译单元 ~150 个 .o

3. Arduino IDE vs Keil 工程对比

3.1 工程文件位置对比

特性 Keil uVision Arduino IDE
工程文件 .uvproj (单一 XML 文件) 分散在多个 .txt 文件
位置 项目目录下 %LOCALAPPDATA%\Arduino15\packages\
源文件列表 手动添加到工程树 自动扫描 src/ 目录
编译选项 Project Options 对话框 platform.txt 文本配置
链接脚本 .sct (Scatter file) .ld (GNU Linker script)
板卡配置 Device 数据库 boards.txt
输出文件 .axf + .hex .elf + .bin + .map
内存视图 Memory Map 窗口 .map 文件
烧录工具 ULINK/J-Link dfu-util / bossac
调试 硬件调试器 (SWD/JTAG) 无硬件调试,仅串口

3.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
%LOCALAPPDATA%\Arduino15\packages\arduino\
└── hardware\renesas_uno\1.6.0\ ✓ 已安装

├── boards.txt ← 板卡定义(选择哪个板子)
│ 定义内容:
│ - minima.name=Arduino UNO R4 Minima
│ - minima.build.mcu=cortex-m4
│ - minima.upload.maximum_size=262144 (256KB Flash)
│ - minima.upload.maximum_data_size=32768 (32KB RAM)

├── platform.txt ← 编译规则(如何编译)
│ 定义内容:
│ - 编译器路径和参数
│ - 链接器参数
│ - 如何生成 .bin 文件
│ - 烧录工具配置

└── variants\MINIMA\ ← 板卡特定文件 ✓ 已安装
├── memory_regions.ld ← 内存映射(重要!)✓
├── fsp.ld ← 主链接脚本 ✓
├── pins_arduino.h ← 引脚定义 ✓
└── libs\libfsp.a ← Renesas FSP 静态库

编译工具链 (%LOCALAPPDATA%\Arduino15\packages\arduino\tools\):
├── arm-none-eabi-gcc\7-2017q4\ ← GCC 编译器 ✓
├── dfu-util\0.11.0-arduino5\ ← DFU 烧录工具 ✓
├── bossac\1.9.1-arduino5\ ← BOSSA 烧录工具 ✓
└── openocd\0.11.0-arduino2\ ← 调试工具(仅 IDE 内部用)✓

4. 内存映射详解

4.1 memory_regions.ld 文件内容

1
2
3
4
5
6
7
8
9
10
11
12
/* Arduino UNO R4 Minima 内存映射 */
RAM_START = 0x20000000; /* SRAM 起始地址 */
RAM_LENGTH = 0x8000; /* 32KB = 32768 字节 */

FLASH_START = 0x00000000; /* Flash 起始地址 */
FLASH_LENGTH = 0x40000; /* 256KB = 262144 字节 */

FLASH_IMAGE_START = 0x4000; /* 应用程序起始(重要!)*/
/* 16KB 偏移,前面是 Bootloader */

DATA_FLASH_START = 0x40100000; /* 数据 Flash(用于 EEPROM 模拟)*/
DATA_FLASH_LENGTH = 0x2000; /* 8KB */

4.2 Flash 布局图

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
RA4M1 Flash (256KB) 完整布局:

地址范围 大小 用途 烧录方式
┌──────────────────────────────────────────────────────────────┐
│ 0x00000000 - 0x00003FFF 16KB DFU Bootloader 出厂固化 │
│ (Arduino 官方烧录的) │
│ 负责 USB DFU 协议 │
├──────────────────────────────────────────────────────────────┤
│ 0x00004000 - 0x0003FFFF 240KB 你的应用程序 ← IDE 烧录到这里 │
│ - .text (代码段) │
│ - .rodata (常量) │
│ - .data 初始值 │
│ - 中断向量表 │
└──────────────────────────────────────────────────────────────┘

RAM (32KB) 布局:

地址范围 用途
┌──────────────────────────────────────────────────────────────┐
│ 0x20000000 - 0x200007FF .data 段 (初始化全局变量) │
├──────────────────────────────────────────────────────────────┤
│ 0x20000800 - 0x20006FFF .bss 段 (未初始化全局变量) │
│ - lwIP heap │
│ - pbuf pool │
├──────────────────────────────────────────────────────────────┤
│ 0x20007000 - 0x20007BFF Heap (动态分配) │
├──────────────────────────────────────────────────────────────┤
│ 0x20007C00 - 0x20007FFF Stack (函数调用栈) │
└──────────────────────────────────────────────────────────────┘

关键点

  • 用户程序从 0x4000 开始,不是 0x0000
  • 前 16KB 被 Bootloader 占用(不可覆盖)
  • boards.txtupload.address=0x00010000 是 DFU 协议的参数(不是实际烧录偏移)

5. 完整编译链接烧录流程(7 阶段)

5.1 阶段 1:源文件扫描

Arduino IDE 递归扫描以下目录(约 120-150 个源文件):

1
2
3
4
5
6
7
8
9
10
你的 Sketch:
└── UDP_Client.ino

库目录 (自动发现):
└── libraries/Arduino_10BASE_T1S/src/
├── *.cpp/*.c (约 10 个文件)
└── lib/liblwip/ (约 80 个 .c 文件)

Arduino 核心库:
└── cores/arduino/ (约 30 个文件)

5.2 阶段 2:编译 (.c/.cpp → .o)

编译命令示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arm-none-eabi-g++ \
-c -Os -g3 \
-std=gnu++17 \
-mcpu=cortex-m4 \
-mfpu=fpv4-sp-d16 \
-mfloat-abi=hard \
-mthumb \
-ffunction-sections \
-fdata-sections \
-fno-rtti \
-fno-exceptions \
-DARDUINO_UNOR4_MINIMA \
-DF_CPU=48000000 \
-I... \
Arduino_10BASE_T1S_UDP.cpp \
-o Arduino_10BASE_T1S_UDP.cpp.o

关键编译选项

选项 作用 为什么需要
-Os 优化大小 Flash 只有 256KB
-ffunction-sections 每个函数独立段 链接时可以移除未使用函数
-fno-rtti 禁用 RTTI 节省 Flash 空间
-fno-exceptions 禁用异常 节省 Flash 空间
-mcpu=cortex-m4 目标 CPU 生成 Cortex-M4 指令
-mfloat-abi=hard 硬件浮点 使用 FPU 加速浮点运算
-g3 最高级别调试信息 .map.elf 仍可定位符号

5.3 阶段 3:打包核心库 (.o → core.a)

1
2
3
4
5
arm-none-eabi-ar rcs core.a \
main.cpp.o \
wiring.c.o \
HardwareSerial.cpp.o \
... (所有 Arduino 核心 .o 文件)

5.4 阶段 4:链接 (.o + .a → .elf)

链接命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arm-none-eabi-g++ \
-mcpu=cortex-m4 \
-mfpu=fpv4-sp-d16 \
-mfloat-abi=hard \
-Wl,--gc-sections \ # 垃圾回收未使用代码
--specs=nano.specs \ # 使用 newlib-nano
-T fsp.ld \ # 链接脚本
-o UDP_Client.ino.elf \
\
*.o \ # 所有 .o 文件
-Wl,--whole-archive \
libfsp.a \ # Renesas FSP 库
core.a \ # Arduino 核心库
-Wl,--no-whole-archive \
-lstdc++ -lm -lc -lgcc \
-Wl,-Map,UDP_Client.ino.map # 生成 Map 文件

链接脚本 fsp.ld 的作用

  • 定义内存区域 (Flash @ 0x0, RAM @ 0x20000000)
  • 定义段布局 (.text, .data, .bss, .heap, .stack)
  • 设置入口点 (Reset_Handler)
  • 定义堆栈大小

精简版 fsp.ld

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
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 0x40000
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x8000
}

SECTIONS
{
.vectors 0x00004000 : { KEEP(*(.vectors)) } > FLASH

.text : {
*(.text*)
*(.rodata*)
} > FLASH

.data : {
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT > FLASH

.bss : {
_sbss = .;
*(.bss*)
_ebss = .;
} > RAM

.heap : {
_heap_start = .;
. = . + 0x2000; /* 8KB heap */
_heap_end = .;
} > RAM

.stack : {
. = ALIGN(8);
_stack_end = .;
. = . + 0x400; /* 1KB stack */
_stack_start = .;
} > RAM
}

5.5 阶段 5:生成二进制 (.elf → .bin)

1
2
3
4
5
6
arm-none-eabi-objcopy \
-O binary \
-j .text \
-j .data \
UDP_Client.ino.elf \
UDP_Client.ino.bin

5.6 阶段 6:计算大小

1
arm-none-eabi-size -A UDP_Client.ino.elf

典型输出UDP_Client + Arduino_10BASE_T1S v0.1.1):

1
2
3
4
5
6
7
8
section            size      addr
.text 98234 0x4100 ← 代码段
.rodata 12456 0x1c01a ← 只读数据
.data 2048 0x20000000 ← 初始化数据
.bss 26408 0x20000800 ← 未初始化数据

Total Flash: 110690 / 262144 (42%)
Total RAM: 28456 / 32768 (86%) ← 警告:RAM 几乎用完!

5.7 阶段 7:烧录到 MCU

使用 bossac 工具(UNO R4 默认烧录器):

1
bossac --port=COM3 -e -w -v -b UDP_Client.ino.bin -R

烧录过程

  1. PC 打开串口,触发 RA4M1 复位进入 Bootloader
  2. Bootloader 擦除 Flash (0x4000-0x3FFFF)
  3. 逐块写入 .bin 内容
  4. 验证后复位,从 0x4000 开始运行

注意:一些板子(Portenta H7)使用 DFU 而非 BOSSA,烧录命令变为 dfu-util --device ... -D *.bin


6. 关键产物详解

6.1 .map 文件 — 最重要的调试工具

位置build/UDP_Client.ino.map

用途

  • 查看每个函数的 Flash 地址
  • 查看全局变量的 RAM 地址
  • 找出最大的函数(优化 Flash 使用)
  • 定位链接错误(undefined reference / multiple definition)

示例内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text           0x00004100    0x17fda
.text 0x00004100 0x48 main.cpp.o
0x00004100 main
.text 0x00004148 0x128 Arduino_10BASE_T1S_UDP.cpp.o
0x00004148 Arduino_10BASE_T1S_UDP::begin
0x00004198 Arduino_10BASE_T1S_UDP::endPacket
.text 0x00004270 0x234 TC6_Io.cpp.o
0x00004270 TC6_Io::spiTransaction

.data 0x20000000 0x800
.data 0x20000000 0x10 Arduino_10BASE_T1S_UDP.cpp.o
0x20000000 _rx_pkt_list

.bss 0x20000800 0x6738
.bss 0x20000800 0x1000 lwip_mem.c.o
0x20000800 lwip_heap

使用技巧

1
2
3
4
5
6
7
8
9
10
11
# 查找某个函数的地址
Select-String -Path build/*.map -Pattern "TC6_Io::spiTransaction"

# 找出最大的 10 个函数
Get-Content build/*.map | Select-String "\.text\." |
ForEach-Object { $_.ToString().Split() } |
Where-Object { $_ -match "^0x[0-9a-f]{4,}$" } |
Sort-Object -Descending | Select-Object -First 10

# 查看 lwIP 占用的 Flash
Select-String -Path build/*.map -Pattern "lwip" | Measure-Object

6.2 三种产物文件

文件 大小 用途
UDP_Client.ino.elf ~400KB 完整 ELF,含调试符号、.map 信息、.bin 内容
UDP_Client.ino.bin ~110KB 纯二进制,烧录用
UDP_Client.ino.map ~200KB 文本格式符号表,调试用

7. 调试技巧

7.1 启用详细编译输出

Arduino IDE

1
2
3
文件 → 首选项
☑ 编译时显示详细输出
☑ 上传时显示详细输出

你将看到完整的编译命令,例如:

1
2
3
4
5
6
7
"C:\...\arm-none-eabi-g++" -c -Os -g3 -mcpu=cortex-m4 ...
-o "Arduino_10BASE_T1S_UDP.cpp.o"
"Arduino_10BASE_T1S_UDP.cpp"

编译完成后:
Sketch uses 110834 bytes (42%) of program storage space.
Global variables use 28456 bytes (86%) of dynamic memory.

7.2 使用 arduino-cli 命令行编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 编译
arduino-cli compile --fqbn arduino:renesas_uno:minima `
--verbose `
--build-path ./build `
UDP_Client.ino

# 查看生成的文件
ls build/
# UDP_Client.ino.elf
# UDP_Client.ino.bin
# UDP_Client.ino.map ← 最重要

# 烧录
arduino-cli upload --fqbn arduino:renesas_uno:minima `
--port COM3 `
--input-dir build

7.3 反汇编查看生成的代码

1
2
3
4
5
6
7
8
# 反汇编整个 .elf 文件
arm-none-eabi-objdump -D build/UDP_Client.ino.elf > disassembly.txt

# 只反汇编代码段(带源码混合)
arm-none-eabi-objdump -S build/UDP_Client.ino.elf > disassembly_with_source.txt

# 查看某个函数的汇编
arm-none-eabi-objdump -D build/UDP_Client.ino.elf | Select-String -Pattern "TC6_Io::spiTransaction" -Context 0,50

输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00004270 <TC6_Io::spiTransaction>:
4270: b580 push {r7, lr}
4272: b084 sub sp, #16
4274: af00 add r7, sp, #0
4276: 60f8 str r0, [r7, #12]
4278: 60b9 str r1, [r7, #8]
427a: 607a str r2, [r7, #4]
427c: 803b strh r3, [r7, #0]
427e: 68fb ldr r3, [r7, #12]
4280: 68db ldr r3, [r3, #12] # _cs_pin
4282: 2200 movs r2, #0
4284: 4619 mov r1, r3
4286: 2000 movs r0, #0
4288: f7ff fffe bl digitalWrite
...

7.4 分析 Flash 使用情况

1
2
3
4
5
6
7
8
# 方法 1: 查看 .map 文件中的 section 大小
Get-Content build/UDP_Client.ino.map | Select-String "^\.text"

# 方法 2: 使用 size 工具的详细模式
arm-none-eabi-size -A -d build/UDP_Client.ino.elf

# 方法 3: 找出最大的符号
arm-none-eabi-nm --size-sort -C -r build/UDP_Client.ino.elf | Select-Object -First 20

7.5 分析 RAM 使用情况

1
2
3
4
5
6
# 查看 .data 和 .bss 段
arm-none-eabi-size -A build/UDP_Client.ino.elf | Select-String "data|bss"

# 找出最大的全局变量
arm-none-eabi-nm --size-sort -C -r build/UDP_Client.ino.elf |
Select-String " B | b " | Select-Object -First 20

7.6 各模块 Flash/RAM 占比速查

依据 Arduino_10BASE_T1S v0.1.1 实测:

模块 Flash 占比 备注
lwIP (liblwip/) ~45 KB UDP + ARP + ICMP 全开约 45 KB;禁 TCP 后降至 ~38 KB
libtc6 (libtc6/) ~15 KB OA TC6 协议状态机 + 寄存器访问
库包装层(TC6_Arduino_10BASE_T1S 等) ~8 KB 包括 lwIP 集成回调
Arduino Core ~30 KB main.cpp, HardwareSerial, Print, Stream
FSP HAL(Renesas 静态库) ~12 KB libfsp.a 中实际被链入的部分

8. 常见错误与解决方案

8.1 Flash 空间不足

错误信息

1
2
Sketch uses 268000 bytes (102%) of program storage space.
Maximum is 262144 bytes.

根因分析:通常因为开启了 TCP / DHCP / DNS 等未用功能。

解决方案

1. 禁用 TCP(如果只用 UDP)

编辑 src/lib/liblwip/cfg/lwipopts.h

1
2
#define LWIP_TCP 0
#define MEMP_NUM_TCP_PCB 0

2. 减少 pbuf pool

1
2
#define PBUF_POOL_SIZE 8    // 原来 16
#define MEMP_NUM_PBUF 8 // 原来 16

3. 移除未使用的功能

1
2
3
4
#define LWIP_DHCP 0       // 如果不用 DHCP
#define LWIP_AUTOIP 0 // 如果不用 AutoIP
#define LWIP_IGMP 0 // 如果不用多播
#define LWIP_DNS 0 // 如果不用 DNS

8.2 RAM 空间不足

错误信息

1
2
Global variables use 33000 bytes (101%) of dynamic memory,
leaving -768 bytes for local variables.

根因:UNO R4 Minima 只有 32KB RAM,lwIP heap + pbuf pool + 你的全局变量很容易突破。

解决方案

1. 减少 lwIP 内存池

1
2
#define MEM_SIZE (12*1024)   // 原来 16KB,改为 12KB
#define PBUF_POOL_SIZE 6 // 原来 16

2. 减少 TCP 缓冲区(如果仍启用 TCP):

1
2
#define TCP_SND_BUF (2*TCP_MSS)  // 原来 4*TCP_MSS
#define TCP_WND (2*TCP_MSS) // 原来 4*TCP_MSS

3. 使用动态内存(小心内存碎片):

将大的静态缓冲区改为 malloc/free 或 C++ new/delete

8.3 链接错误:undefined reference

错误示例

1
undefined reference to `TC6_Service'

原因

  • 函数声明了但没有定义
  • 库文件没有正确链接(被 IDE 自动排除)

解决

  1. 确认源文件在 src/ 目录下(含子目录)
  2. 检查函数名是否正确(C++ 注意命名空间 Mchp::TC6::
  3. 重启 Arduino IDE 刷新库缓存
  4. arduino-cli compile --verbose 看哪些 .cpp 被加入编译列表

8.4 链接错误:multiple definition

错误示例

1
multiple definition of `TC6_Service'

原因:通常是把同一库复制了多份,或多个库包含同名符号。

解决

  1. 检查 ~/Documents/Arduino/libraries/ 下是否有多份相同库
  2. 检查 IDE 内置库与用户库是否冲突
  3. 必要时给某个库的符号加前缀(例如 mchp_tc6_service vs vendor_tc6_service

8.5 烧录失败

错误信息

1
2
3
No DFU capable USB device found  (dfu-util)

Could not find device on COM3 (bossac)

解决

  1. 双击 Arduino 板上的 RESET 按钮,让 Bootloader 接管
  2. 检查 USB 线缆是否支持数据传输(部分便宜线只供电)
  3. 更新 USB 驱动(CH340 / RA4M1 内置)
  4. 检查串口是否被其他程序占用(关闭串口监视器再烧录)
  5. UNO R4 Minima 默认用 BOSSA,要确保 Board 选择正确

8.6 找不到库

错误信息

1
2
3
fatal error: Arduino_10BASE_T1S.h: No such file or directory
#include <Arduino_10BASE_T1S.h>
^~~~~~~~~~~~~~~~~~~~~~~~~

原因:库没有安装或路径不对。

解决

  1. 检查库目录是否存在:~/Documents/Arduino/libraries/Arduino_10BASE_T1S/
  2. 确认 library.properties 文件存在
  3. 重启 Arduino IDE(IDE 启动时扫描库目录)
  4. arduino-cli 用户:arduino-cli lib install Arduino_10BASE_T1S

9. 开发工作流建议

9.1 阶段化集成策略

强烈推荐自底向上逐步集成,每一步可独立验证:

1
2
3
4
5
6
7
8
9
10
11
阶段 1: SPI 通信        ─── 不依赖 lwIP 与协议栈

阶段 2: libtc6 协议 ─── 验证 SPI 帧收发、寄存器读写

阶段 3: lwIP 初始化 ─── 仅 netif + IP 配置,不发包

阶段 4: ARP / ICMP ─── 用 ping 验证链路

阶段 5: UDP ─── UDP echo / 简单数据收发

阶段 6: 完整应用 ─── 集成到 sketch 中

9.2 阶段 1 模板:仅验证 SPI(不依赖完整库)

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
#include <SPI.h>

#define CS_PIN 9
#define RESET_PIN 6

void setup() {
Serial.begin(115200);
while (!Serial);

pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);

pinMode(RESET_PIN, OUTPUT);
digitalWrite(RESET_PIN, LOW);
delay(100);
digitalWrite(RESET_PIN, HIGH);
delay(100);

SPI.begin();

// 通过 TC6 协议读取 chip_id
uint32_t chip_id = readReg(0x00000001);
Serial.print("Chip ID: 0x");
Serial.println(chip_id, HEX);
}

void loop() {}

uint32_t readReg(uint32_t addr) {
// OA TC6 控制帧的读寄存器协议(详见协议栈深度分析文章)
// ...
}

优势:快速验证硬件连接,不需要完整的网络栈,单个文件即可调试。

9.3 阶段 3 模板:lwIP loopback 测试

1
2
3
4
5
6
7
8
// 配置 lwIP 使用 loopback
netif_add(&netif, &ipaddr, &netmask, &gw, NULL, netif_init_loopback, ethernet_input);
netif_set_link_up(&netif);

// 测试 UDP echo 到 127.0.0.1
udp.beginPacket(IPAddress(127, 0, 0, 1), 8888);
udp.write("test");
udp.endPacket();

此阶段不接 MAC-PHY,验证 lwIP 协议栈本身的配置是否正确。

9.4 完整工作流 checklist

  • Step 1:运行 Microchip 官方 UDP_Client 示例通过
  • Step 2:读 .map 文件确认 lwIP / libtc6 各占多少 Flash
  • Step 3:按需裁剪 lwipopts.h
  • Step 4:用 arduino-cli 集成到自己的构建脚本
  • Step 5:配置 CI 自动编译(GitHub Actions / GitLab CI)
  • Step 6:用 objdump 反汇编关键函数,验证性能假设

10. 推荐的工具链

10.1 VSCode + Arduino 插件

1
2
3
4
5
6
7
8
9
// .vscode/settings.json
{
"arduino.path": "C:/Program Files/Arduino IDE",
"arduino.useArduinoCli": true,
"C_Cpp.default.includePath": [
"C:/Users/你的用户名/AppData/Local/Arduino15/packages/arduino/hardware/renesas_uno/1.x.x/cores/arduino",
"C:/Users/你的用户名/Documents/Arduino/libraries/**"
]
}

优势

  • 代码补全(IntelliSense)
  • 定义跳转(F12)
  • Git 集成
  • 多文件编辑友好

10.2 命令行:arduino-cli

安装:

1
2
3
4
# Windows (用 Scoop)
scoop install arduino-cli

# 或下载 zip: https://github.com/arduino/arduino-cli/releases

完整编译+烧录+监视串口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 配置核心
arduino-cli core update-index
arduino-cli core install arduino:renesas_uno

# 编译
arduino-cli compile --fqbn arduino:renesas_uno:minima `
--output-dir build `
.\examples\UDP_Client\UDP_Client.ino

# 烧录
arduino-cli upload --fqbn arduino:renesas_uno:minima `
--port COM3 `
--input-dir build

# 监视串口
arduino-cli monitor --port COM3 --config baudrate=115200

10.3 版本控制

.gitignore(至少包含):

1
2
3
4
5
6
build/
*.bin
*.elf
*.hex
*.map
.DS_Store

10.4 持续集成(GitHub Actions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# .github/workflows/build.yml
name: Arduino CI

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-arduino-cli@v1
- run: |
arduino-cli core update-index
arduino-cli core install arduino:renesas_uno
arduino-cli compile --fqbn arduino:renesas_uno:minima examples/UDP_Client
arduino-cli compile --fqbn arduino:renesas_uno:minima examples/UDP_Server
arduino-cli compile --fqbn arduino:renesas_uno:minima examples/pingpong

11. 与 Keil 工程的完整操作对比

任务 Keil uVision Arduino IDE
新建工程 New Project → 选择 Device 新建 Sketch,IDE 自动配置
添加源文件 右键工程 → Add Files 放到 src/ 目录,自动扫描
配置编译选项 Project Options → C/C++ 编辑 platform.txt(高级)
配置链接器 Project Options → Linker 编辑 fsp.ld(高级)
编译 Build 按钮 (F7) 验证按钮 (Ctrl+R)
查看输出 Build Output 窗口 控制台
查看内存使用 Linker → Memory Map .map 文件
烧录 Download 按钮 (F8) 上传按钮 (Ctrl+U)
调试 Debug → Start (F5) 无硬件调试器
断点调试 支持 不支持,只能 Serial.print()
查看寄存器 Peripherals 窗口 不支持
查看变量 Watch 窗口 不支持

12. 总结

12.1 关键要点

  1. 烧录目标明确:Arduino IDE 只烧录 RA4M1 主 MCU,不烧录 LAN865x MAC-PHY
  2. 自动化程度高:源文件自动扫描(递归 src/),无需手动管理工程文件
  3. 配置三件套boards.txt(板卡)、platform.txt(工具链)、fsp.ld(链接脚本)
  4. Flash 布局:0x0000-0x3FFF 是 Bootloader,用户程序从 0x4000 开始
  5. 调试受限:无硬件调试器,只能用 Serial.print().map 文件

12.2 文件流程总结

1
2
3
4
5
6
7
8
9
10
11
源文件 (.ino/.cpp/.c)                       ← Arduino_10BASE_T1S/src/ + Sketch
↓ arm-none-eabi-gcc/g++ -c
目标文件 (.o) × 约 150 个
↓ arm-none-eabi-ar
静态库 (core.a, libfsp.a)
↓ arm-none-eabi-g++ -T fsp.ld
ELF 可执行文件 (.elf) + Map 文件 (.map)
↓ arm-none-eabi-objcopy
纯二进制文件 (.bin)
↓ bossac (UNO R4) 或 dfu-util (Portenta H7)
RA4M1 Flash @ 0x4000

12.3 推荐工作流

  1. 开发阶段:使用 Arduino IDE,利用自动化
  2. 调试阶段:读 .map 文件,用 Serial.print() + 串口监视器
  3. 优化阶段:分析 .map、反汇编 .elf、裁剪 lwipopts.h
  4. 生产阶段:使用 arduino-cli 自动化编译烧录,集成 CI

12.4 何时跳出 Arduino 框架

以下场景应考虑切到 PlatformIO + 自定义 platform.txt,或直接用 CMake + GNU Arm 工具链:

  • 需要精细控制链接脚本(如多段 Flash 分配)
  • 需要硬件调试器(GDB + OpenOCD)
  • 需要集成 RTOS(FreeRTOS 已有 Arduino 绑定但配置受限)
  • 库需要跨 MCU 平台(不只是 renesas_uno)

13. 参考资源


文档结束

下一步阅读

调试建议:使用 arduino-cli compile --verbose 查看完整编译过程。