Adafruit_nRF52_Bootloader 使用 uf2
烧录Adafruit_nRF52_Bootloader
openocd -f interface/stlink.cfg -f target/nrf52.cfg -c "gdb_flash_program enable" -c "gdb_breakpoint_override hard" -c "init" -c "reset halt" -c "flash write_image erase ./nice_nano_bootloader-0.9.2_s140_6.1.1.hex"
制作 uf2
[env:adafruit_feather_nrf52840]
platform = nordicnrf52
framework = zephyr
board = adafruit_feather_nrf52840extra_scripts = post:utils/generate_bin_uf2.py
# 在 platformio.ini 或 SConscript 中
from SCons.Script import DefaultEnvironmentenv = DefaultEnvironment()# 定义 Post Action
def add_post_action(target, source, env):import osimport subprocessbuild_dir = env.subst("$BUILD_DIR")prog_name = env.subst("$PROGNAME")elf_file = os.path.join(build_dir, prog_name + ".elf")bin_file = os.path.join(build_dir, prog_name + ".bin")uf2_file = os.path.join(build_dir, prog_name + ".uf2")# 调用你的现有 Python 逻辑print(f"Generating BIN from {elf_file}...")subprocess.run([env.subst("$OBJCOPY"),"-O", "binary",elf_file,bin_file], check=True)print(f"Generating UF2 from {bin_file}...")subprocess.run(["python3","utils/uf2conv.py",bin_file,"-c","-b", "0x26000","-f", "0xADA52840","-o", uf2_file], check=True)print(f"---------------- UF2 generated: {uf2_file}")# 添加 Post Action 到程序构建完成事件
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", add_post_action)
检查
python ./utils/uf2conv.py -i /home/link/Code/nrf52840/zephyr-blink/.pio/build/adafruit_feather_nrf52840/firmware.uf2
--- UF2 File Header Info ---
Family ID is NRF52840, hex value is 0xada52840
Target Address is 0x00026000
All block flag values consistent, 0x2000
根据你贴的输出,UF2 文件本身头信息是:
- Family ID:
0xADA52840
✅(和你的 MCU 匹配) - Target Address:
0x00026000
- Block size:
0x2000
从这个信息来看,UF2 文件被正确生成了,但你说刷入后 运行不起来,这通常意味着 程序的实际启动地址和 UF2 文件写入的地址不匹配。
核心问题
Target Address = 0x26000
表示 UF2 的固件会被写入 MCU 的 0x26000 地址。
但是:
- MCU 的 **向量表(Vector Table)**必须在复位后 MCU 能正确找到中断入口。
- 如果
.elf
文件的 link address 不匹配 0x26000,MCU 启动时就找不到正确的入口,导致死机或闪烁不了。
换句话说,UF2 写的地址和程序编译链接的地址必须一致。
排查方法
- 查看 ELF 文件的起始地址:
arm-none-eabi-readelf -l .pio/build/adafruit_feather_nrf52840/firmware.elf | grep "LOAD"
- 看看哪个地址是
.text
段的起始地址。 - 如果不是
0x26000
,那 UF2 写入的地址就错了。
- 确认 PlatformIO 或 Zephyr 的 linker script:
- Zephyr 默认可能把程序链接在 0x0 或 0x1000,而不是 0x26000。
- 如果是 Adafruit Feather nRF52840,它的 bootloader 占用 0x0–0x25FFF,所以程序应该从 0x26000 开始。
- 需要保证
.elf
文件 链接地址和 UF2 target address 一致。
解决方案
- 调整 linker script,把程序链接到
0x26000
(对应 UF2 的 target address)。 - 或者 改变 UF2 转换的
-b
参数,使其和 ELF 的 link address 匹配。
💡 建议步骤:
- 用
readelf
看 ELF 起始地址。 - 对照 UF2
-b
参数,确保它们完全一致。 - 再刷入板子测试。
是的,如果你想 UF2 文件刷入后能正常运行,必须保证 ELF 文件的链接地址和 UF2 target address 一致。在你的情况里,你的 UF2 target 是 0x26000
,但是 ELF 链接的是 0x0
,所以 MCU 启动时找不到正确的向量表。
在 PlatformIO + Zephyr 中修改链接地址的方法如下:
1️⃣ 使用 board_build.ldscript
PlatformIO 允许通过 board_build.ldscript
指定自定义 linker script:
-
找到你当前板子的 linker script。通常在:
~/.platformio/packages/framework-zephyr/boards/arm/<board_name>/linker.ld
或者
.pio/build/<board>/zephyr/linker.ld
-
拷贝到工程目录,比如:
boards/feather_nrf52840.ld
-
修改 flash 起始地址:
MEMORY {FLASH (rx) : ORIGIN = 0x26000, LENGTH = 1024K - 0x26000RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K }
-
在
platformio.ini
中指定:[env:adafruit_feather_nrf52840] platform = nordicnrf52 board = adafruit_feather_nrf52840 framework = zephyr board_build.ldscript = boards/feather_nrf52840.ld
-
重新编译:
pio run -t build
这时生成的 ELF 文件会从 0x26000 起始,和你 UF2 target address 一致。
2️⃣ 注意事项
- 不要只改 UF2 的
-b
参数,否则会覆盖 bootloader。 - 保证 flash 起始地址 ≥ bootloader 占用地址(Feather nRF52840 bootloader 一般占到 0x25FFF)。
- 编译后的 ELF 和 UF2 target address 一致,刷入后就能正常运行。
看了你贴的 platformio
SConscript 文件,关键点分析如下:
1️⃣ 为什么你修改 board_build.ldscript
没生效
- 你当前使用的是 Zephyr 框架:
if "zephyr" in env.get("PIOFRAMEWORK", []):env.SConscript(join(platform.get_package_dir("framework-zephyr"), "scripts", "platformio", "platformio-build-pre.py"),exports={"env": env})
- Zephyr 自己的构建系统会生成 linker script(
.ld
),覆盖 PlatformIO 默认的board_build.ldscript
。 - 所以即便你在
platformio.ini
里指定了:
board_build.ldscript = boards/feather_nrf52840.ld
也不会生效。ELF 仍然从 0x0 地址链接。
2️⃣ 正确修改 ELF 链接地址的方法(Zephyr + PlatformIO)
方法 A:通过 Kconfig 覆盖 flash 起始地址
- 在工程根目录创建或修改
prj.conf
:
CONFIG_FLASH_BASE_ADDRESS=0x26000
- Zephyr 会根据这个配置在生成的 linker script 里调整 flash 起始地址。
- 重新编译:
pio run -t clean
pio run
方法 B:修改 Zephyr board YAML 文件
- 找到板子配置:
~/.platformio/packages/framework-zephyr/boards/arm/adafruit_feather_nrf52840/adafruit_feather_nrf52840.yaml
- 修改 flash 起始地址:
flash:origin: 0x26000size: 0x100000
- 重新编译即可。
⚠️ 注意:
- 起始地址必须大于 bootloader 占用地址(Feather nRF52840 bootloader 占到 0x25FFF)。
- 编译后的 ELF 文件和 UF2 target address 必须一致。
3️⃣ UF2 转换
- 保持你的
uf2conv.py
命令:
python utils/uf2conv.py -b 0x26000 -f 0xADA52840 firmware.bin -o firmware.uf2
- 因为 ELF 现在从
0x26000
链接,UF2 target address 与 ELF 地址一致,刷入后 MCU 可以正常启动。
💡 总结:
- PlatformIO 的
board_build.ldscript
对 Zephyr 无效。 - Zephyr 链接地址必须通过
CONFIG_FLASH_BASE_ADDRESS
或 board YAML 修改。 - 修改后重新编译,生成 ELF 就可以和 UF2 target address 对齐。
总结一下你遇到的问题的核心原因:
1️⃣ 链接地址 vs UF2/加载地址
-
链接地址(Link Address):这是编译器/链接器把程序各段(
.text
、.data
、.bss
等)在内存中的 逻辑起始位置。- 编译出来的 ELF 文件里的指令和数据都假设自己从这个地址开始。
- MCU 启动时,向量表和中断表也基于这个地址。
-
UF2 target/load address:这是 UF2 文件刷入 MCU flash 的 物理地址。
- Bootloader 会把 UF2 写到这个地址。
⚠️ 如果二者不一致:
- MCU 会在启动时跳到向量表地址,但实际程序代码不在那个位置。
- 即使 UF2 文件写入了 flash,程序也会无法运行或死机。
2️⃣ 为什么直接改 UF2 的 -b
参数无效
- 改
uf2conv.py -b <address>
只是改变了 UF2 写入 flash 的地址。 - 但是 ELF 文件的指令和数据仍然假设自己从 原来的链接地址(0x0) 开始。
- MCU 启动时会跳到
0x26000
(UF2 target),但代码内部的绝对地址、向量表、函数指针都仍然指向原来的地址(0x0),程序无法正常运行。
✅ 简单理解:UF2 写错地方,程序本身不“知道”它被搬到哪里。
3️⃣ 正确做法
-
修改 ELF 链接地址:
-
确保程序在编译/链接阶段就知道它的起始地址是
0x26000
。 -
在 Zephyr + PlatformIO 下,可以通过:
prj.conf
添加:CONFIG_FLASH_BASE_ADDRESS=0x26000
- 或修改 board YAML 文件中的 flash 起始地址。
-
-
UF2 target address 与 ELF 链接地址一致:
uf2conv.py -b 0x26000
- 这样 MCU 启动时跳到向量表,代码就位于正确地址,能正常运行。
💡 核心总结:
程序本身的逻辑地址(链接地址)和刷入 flash 的物理地址必须一致,否则 MCU 启动时找不到正确的入口。
直接修改 UF2 写入地址无法修正程序内部的指针和向量表,因此程序无法运行。
好的,我帮你画一个简单的 ASCII 图,展示 ELF 链接地址、BIN 内存布局和 UF2 刷入地址的关系:
MCU FLASH
+------------------------+
| 0x00000 - 0x25FFF | <- Bootloader 占用区
+------------------------+
| 0x26000 | <- 程序起始地址 (Link/UF2 Target)
| |
| .text (代码段) | <- ELF 链接地址也从 0x26000
| .rodata (只读数据) |
| |
| .data (初始化数据) |
| .bss (未初始化数据) |
+------------------------+
| 0xFFFFF ... | <- FLASH 末尾
+------------------------+
解释:
-
ELF 链接地址:
- 编译时假设
.text
、.data
等段从 0x26000 开始。 - 向量表(ISR)也在 0x26000。
- 编译时假设
-
BIN 文件:
objcopy
生成的二进制文件不包含物理地址信息,它只是顺序存放各段数据。- 运行时,程序依赖 ELF 链接地址来确定实际内存位置。
-
UF2 文件:
- UF2 的
-b
参数指定写入 flash 的物理地址。 - 必须与 ELF 链接地址一致,否则 MCU 跳到向量表时找不到代码。
- UF2 的
💡 关键点:
- 如果 ELF 链接地址是 0x0,但 UF2 写入 0x26000 → MCU 启动会找不到向量表 → 程序不跑。
- 修改 UF2 target 地址不能解决问题,必须让 ELF 链接地址和 UF2 target 一致。
Arduino 平台和 Zephyr 在链接器脚本和内存管理上有明显区别:
1. 入口地址和启动方式
-
Arduino (nRF5 / UF2 bootloader)
- Bootloader 固定在 FLASH 前端(比如 0x0000–0x2000),应用从 bootloader 之后的地址开始。
- 链接器脚本明确指定应用起始地址 (
_estart
/.text
段)。 - 编译生成的
.elf
就是直接放在应用区,UF2 或 MSC bootloader 负责写入 FLASH。
-
Zephyr
- Zephyr 使用自己的 Bootloader & Vector Table 管理。
- 可以配置 MCUBOOT 或 MCUboot-compatible bootloader,支持 OTA 升级。
- 链接器脚本通常由 Zephyr Kconfig 自动生成,入口地址、段布局都是根据 DTS 和配置文件动态生成。
2. 链接器脚本
-
Arduino
- 手动提供
.ld
文件,用户可以在 Board JSON 或 PlatformIO/IDE 中指定。 - 通常比较简单,只划分
.text
,.data
,.bss
,.heap
,.stack
。
- 手动提供
-
Zephyr
- 使用 CMake + Kconfig + Zephyr linker scripts 生成完整
.ld
,自动管理应用区、bootloader区、闪存分区表、RAM分区等。 - 修改入口或段布局不直接改
.ld
文件,而是通过 DTS / Kconfig 配置。
- 使用 CMake + Kconfig + Zephyr linker scripts 生成完整
3. Bootloader 协议
- Arduino UF2:直接通过 USB MSC 写入应用区,简单高效,入口地址固定。
- Zephyr / MCUBOOT:支持多分区、OTA、签名验证,更灵活但复杂。
简单说:
- Arduino 平台是固定 bootloader + 应用区,链接器脚本简单,可手动改。
- Zephyr 是完整 RTOS + bootloader/分区管理,入口、内存布局高度自动化,用户改动要通过配置,而不是直接改
.ld
。