一、.vscode/tasks.json 是什么

它是 VS Code 的任务声明文件(不是 CMake 的一部分),作用是把一串 shell 命令包装成可在 Cmd+Shift+P → Tasks: Run Task 或 Cmd+Shift+B 里一键执行的操作。它本身不编译任何东西,真正的编译由它调用的 cmake 完成。

1.1 顶层字段

1
2
3
{
"version": "2.0.0",
"tasks": [
  • version: "2.0.0" —— tasks.json 的 schema 版本(注意:与 CmakePresets.json 里的 "version": 3 是两个完全不相干的东西)。
  • tasks[] —— 五个任务,下面逐个拆。

1.2 各字段通用含义

字段 含义
label 任务名,被 launch.json 的 preLaunchTask 引用
type: "shell" 交给系统 shell(zsh)执行
command / args 实际命令行
options.cwd 工作目录,这里统一是 ${workspaceFolder}
group.kind: "build" + isDefault: true 绑定到 Cmd+Shift+B
problemMatcher 把终端输出解析成”问题”面板条目,支持点击跳转
isBackground 长驻进程,配合 beginsPattern/endsPattern 判断启动完成
1
"command": "printf 'device FR8029D\nsi SWD\nspeed auto\nconnect\nloadfile \"${workspaceFolder}/build/debug/ble_hzpc.elf\"\nr\ng\nexit\n' > \"${workspaceFolder}\"/build/debug/flash.jlink && JLinkExe -device FR8029D -if SWD -speed auto -autoconnect 1 -CommanderScript \"${workspaceFolder}\"/build/debug/flash.jlink",

两步:先用 printf 动态拼出一个 J-Link Commander 脚本,再让 JLinkExe 执行它。脚本内容是:

1
2
3
4
5
6
7
8
device FR8029D      # 目标芯片
si SWD # 接口
speed auto
connect # 连上内核
loadfile "...ble_hzpc.elf" # 比较+擦除+编程+校验 一条龙
r # reset
g # go(运行)
exit

关键点:**烧的是 .elf 而不是 .bin**。loadfile 能读 ELF 里的段地址,自动按链接脚本的地址(0x10002000)写入,不需要手工指定偏移。

1.4 任务 ②:Build (Debug)(build 组默认,Cmd+Shift+B)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"label": "Build (Debug)",
"type": "shell",
"command": "cmake",
"args": [
"--build",
"--preset",
"arm-gcc-debug"
],
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "build",
"isDefault": true
},

这是唯一真正产出可执行文件的任务。它把构建完全委托给 CMake + Ninja,--preset arm-gcc-debug 从 CmakePresets.json 取参数。

它的问题匹配器值得注意:

1
2
3
4
5
6
7
8
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error|note):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}

把 GCC 的 文件:行:列: 级别: 信息 抽成可点击条目(捕获组含 note)。

1.5 任务 ③:Build & Flash (Debug)

1
"command": "cmake --build --preset arm-gcc-debug && printf 'device FR8029D\nsi SWD\nspeed auto\nconnect\nloadfile \"${workspaceFolder}/build/debug/ble_hzpc.elf\"\nr\ng\nexit\n' > \"${workspaceFolder}\"/build/debug/flash.jlink && JLinkExe -device FR8029D -if SWD -speed auto -autoconnect 1 -CommanderScript \"${workspaceFolder}\"/build/debug/flash.jlink",

&& 串联 ② 和 ① —— 编译成功才烧录。

1
"command": "printf 'device FR8029D\nsi SWD\nspeed auto\nconnect\nerase 0x10000000 0x10100000\nexit\n' > \"${workspaceFolder}\"/build/erase_chip.jlink && JLinkExe -device FR8029D -if SWD -speed auto -autoconnect 1 -CommanderScript \"${workspaceFolder}\"/build/erase_chip.jlink",

detail 里说明了为什么必须给显式地址范围:J-Link 没有 FR8029D 的 flash loader,只能按地址区间擦。范围 0x10000000~0x10100000 覆盖 bootloader + app 共 1MB,擦完必须重新烧录。

1
2
3
4
5
6
7
8
9
10
11
12
"isBackground": true,
"problemMatcher": {
"owner": "jlink",
"pattern": {
"regexp": "."
},
"background": {
"activeOnStart": true,
"beginsPattern": "^.*J-Link GDB Server.*",
"endsPattern": "^Waiting for GDB connection\\.\\.\\.\\s*"
}
},

GDB Server 不会退出,所以标 isBackground。VS Code 靠 endsPattern(等到出现 Waiting for GDB connection...)判断”服务已就绪”,之后才允许依赖它的调试配置启动。端口:GDB 2331 / SWO 2332 / Telnet 2333 / RTT 2334,正好对应 launch.json 里第二个配置的 miDebuggerServerAddress: localhost:2331。

1.8 与 launch.json 的联动

两个调试配置都写了 "preLaunchTask": "Build (Debug)",按 F5 会先跑 ② 再调试:

1
"executable": "${workspaceFolder}/build/debug/ble_hzpc.elf",

二、可执行文件是怎么生成的

tasks.json 只是”按钮”,真正的流水线是 CMake Presets → 交叉编译 → 链接 → 后处理。分 5 个阶段。

阶段 0:配置(configure)

CmakePresets.json 定义了两个预设:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "arm-gcc-debug",
"displayName": "ARM GCC Debug",
"description": "Build with arm-none-eabi-gcc (Debug)",
"generator": "Ninja",
"toolchainFile": "${sourceDir}/cmake/toolchain-arm-none-eabi.cmake",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TRY_COMPILE_TARGET_TYPE": "STATIC_LIBRARY"
}
},
  • generator: Ninja → 增量编译靠 Ninja。
  • binaryDir: build/debug → 所有产物落在这里(实测目录里确实有 ble_hzpc.elf/.bin/.hex/.lst/.map)。
  • CMAKE_TRY_COMPILE_TARGET_TYPE: STATIC_LIBRARY → 让 CMake 的编译器探测只编译不链接(裸机没有可用的链接测试目标)。

工具链文件设定交叉编译器与产物后缀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
set(CMAKE_SYSTEM_NAME               Generic)
set(CMAKE_SYSTEM_PROCESSOR cortex-m33)

set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)

set(CMAKE_EXECUTABLE_SUFFIX .elf)

# Don't run the linker during compiler test
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

CMAKE_EXECUTABLE_SUFFIX = .elf 就是最终文件名带 .elf 的来源。目标名写成 ble_hzpc.elf、后缀也是 .elf,CMake 不会重复追加 —— 目录里的实际产物就是 ble_hzpc.elf。

阶段 1:编译(compile)

CMakeLists.txt 手工枚举了约 90 个源文件,分 8 组汇成 ALL_SOURCES:

1
2
3
4
5
6
7
8
9
10
11
12
13
set(ALL_SOURCES
${APP_SOURCES}
${COMMON_BLE_SOURCES}
${COMMON_OTA_SOURCES}
${COMMON_FLASHDB_SOURCES}
${DEVICE_SOURCES}
${STARTUP_SOURCE}
${PERIPHERAL_SOURCES}
${MODULE_SOURCES}
${FREERTOS_SOURCES}
${FREERTOS_ASM_SOURCES}
${PARTITION_UPGRADE}
)

编译选项:

1
2
3
4
5
set(CPU_FLAGS
-mcpu=cortex-m33
-mthumb
-mfloat-abi=hard
)
1
2
3
4
5
6
7
8
9
10
11
12
target_compile_options(${CMAKE_PROJECT_NAME}.elf PRIVATE
${CPU_FLAGS}
-O2
-fmessage-length=0
-fsigned-char
-ffunction-sections
-fdata-sections
-g3
-std=gnu11
-Wall
-Wextra
)

-ffunction-sections/-fdata-sections 是为链接期的 --gc-sections 做铺垫(每个函数独立成段,未用到的整段丢弃)。

阶段 2:链接(link)

1
2
3
add_executable(${CMAKE_PROJECT_NAME}.elf ${ALL_SOURCES})

target_include_directories(${CMAKE_PROJECT_NAME}.elf PRIVATE ${INCLUDE_DIRS})
1
2
3
4
5
6
7
8
9
10
11
12
13
target_link_options(${CMAKE_PROJECT_NAME}.elf PRIVATE
${CPU_FLAGS}
-O2
...
-Wl,--gc-sections
-specs=nano.specs
-specs=nosys.specs
-T${LD_SCRIPT}
-T${LD_SYSCALLS}
-Wl,-Map,${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.map
)

target_link_libraries(${CMAKE_PROJECT_NAME}.elf PRIVATE ${LINK_LIBS})

要点:

  • 两个链接脚本:-T ldscript.ld(主)+ -T syscall_gcc.txt(BLE 库的 syscall 表)。
  • nano.specs / nosys.specs:用精简 newlib-nano,省体积。
  • 静态库:libbtdm_host.a、libbtdm_controller.a(闭源 BLE 协议栈)。
  • 内存布局决定一切:
1
2
3
4
5
MEMORY
{
FLASH (rx) : ORIGIN = 0x10002000, LENGTH = 1016K /* 1016K*/
RAM (xrw) : ORIGIN = 0x20001818, LENGTH = 96K /* 57K*/
}

前 8K(0x10000000~0x10002000)留给镜像头 + 标志区,所以 app 从 0x10002000 起。链接脚本还埋了两个链接期断言,防止 RAM 静默踩踏:

1
2
3
4
5
    ASSERT(ABSOLUTE(_estack) - ABSOLUTE(_sstack) >= _Min_Stack_Size,
"RAM: MSP stack (.data.stack) smaller than _Min_Stack_Size")
...
ASSERT(ABSOLUTE(_heap_end) + _Rom_Reserved <= ORIGIN(RAM) + LENGTH(RAM),
"RAM: heap/bss too large, no room left at RAM tail")

一个容易忽略但很重要的修复:

1
2
3
# 改了链接脚本必须重新链接(ninja 默认不把 -T 的脚本当依赖,否则 ASSERT 之类
# 的改动不会生效,只会一直用旧的 elf)
set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY LINK_DEPENDS ${LD_SCRIPT} ${LD_SYSCALLS})

阶段 3:后处理(POST_BUILD)→ 生成 .bin / .hex / .lst + 体积校验

这是”可执行文件变成可烧录文件”的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add_custom_command(TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary -S
$<TARGET_FILE:${CMAKE_PROJECT_NAME}.elf>
${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
COMMAND ${CMAKE_OBJCOPY} -O ihex -S
$<TARGET_FILE:${CMAKE_PROJECT_NAME}.elf>
${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.hex
COMMAND ${CMAKE_OBJDUMP} --source --all-headers --demangle --line-numbers --wide
$<TARGET_FILE:${CMAKE_PROJECT_NAME}.elf>
> ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.lst
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${CMAKE_PROJECT_NAME}.elf>
COMMAND ${CMAKE_COMMAND}
-DIMAGE_BIN=${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
-DIMAGE_MAX_SIZE=${IMAGE_MAX_SIZE}
-DIMAGE_BASE=${APP_BASE_ADDR}
-DFLASHENV_OFFSET=${FLASHENV_OFFSET}
-P ${TOP_DIR}/cmake/check_image_size.cmake
COMMENT "Generating .bin, .hex, .lst files and checking image size"
)

产物一览:

产物 工具 用途
ble_hzpc.elf 链接器 调试/J-Link loadfile 烧录(自带地址信息)
ble_hzpc.bin objcopy -O binary -S 裸二进制,-S 剥掉符号
ble_hzpc.hex objcopy -O ihex -S Intel HEX,串口 ISP/编程器用
ble_hzpc.lst objdump --source 源码+汇编交叉对照,排查硬 fault
ble_hzpc.map -Wl,-Map 符号地址/段大小分布

最后一步调用独立的 CMake 脚本做硬校验,超限直接 FATAL_ERROR 让编译失败:

1
2
3
4
5
6
7
if(_image_size GREATER _limit)
math(EXPR _overflow "${_image_size} - ${_limit}")
...
message(FATAL_ERROR
"应用镜像体积超限,编译中止!\n"
...
" 继续烧录会覆盖 FlashEnv 分区,导致设备复位后无法启动。\n"

上限来自这里(332K):

1
2
3
4
set(FLASHENV_OFFSET 0x55000)
# 应用镜像可用空间 = FlashEnv 起始偏移 - 应用起始偏移(当前 332K = 339968 字节),
# 由 cmake/check_image_size.cmake 在 POST_BUILD 阶段强制校验。
math(EXPR IMAGE_MAX_SIZE "${FLASHENV_OFFSET} - ${APP_BASE_OFFSET}")

阶段 4:串口烧录文件(可选,产线用)

要串口烧录时,README 给出的命令:

1
2
> cd components/toolchain
> python3 post_process.py ble_hzpc /Users/cailinjun/Desktop/ble_hzpc/build/debug

它在 .bin 前面拼一个 4K 的 image_info_t 头 + 4K 的标志区:

1
2
3
4
5
6
7
8
9
    f_out.write(struct.pack('I', version))
f_out.write(struct.pack('I', store_offset))
f_out.write(struct.pack('I', code_length))
f_out.write(struct.pack('I', exec_offset))
f_out.write(struct.pack('I', copy_unit))
f_out.write(struct.pack('I', copy_flag_store_step))
...
f_out.write(struct.pack('I', image_crc&0xffffffff))
f_out.write(struct.pack('I', 0x51525251))

随后 info_crc + magic + 0x00008020 + 0xFF 填到 4K,再写 4K 全 0x00 的搬运进度区,最后追加原始 bin。产物是 ble_hzpc_burn.bin。默认 exec_offset = 0x2000,与 APP_BASE_OFFSET 一致 —— ROM bootloader 依据这个头把镜像搬进执行区。

阶段 5:资源包(独立于固件,烧外部 SPI Flash)

字模/图片不进固件,由 Python 脚本单独产出 res/res_pack.bin,烧到外部 flash 0x700000。README 强调两者必须同步更新。这条链路完全不经过 CMake。


三、整体流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
flowchart TD
A["用户: Cmd+Shift+B<br/>tasks.json → Build (Debug)"] --> B["cmake --build --preset arm-gcc-debug"]

B --> C["CmakePresets.json<br/>Ninja + toolchain + binaryDir=build/debug"]
C --> D["cmake/toolchain-arm-none-eabi.cmake<br/>arm-none-eabi-gcc / CMAKE_EXECUTABLE_SUFFIX=.elf"]

D --> E["阶段1 编译<br/>~90 个 .c/.S → .obj<br/>-mcpu=cortex-m33 -O2 -g3 -ffunction-sections"]
E --> F["阶段2 链接<br/>ldscript.ld + syscall_gcc.txt<br/>libbtdm_host.a / libbtdm_controller.a<br/>--gc-sections, nano.specs"]
F --> G["ble_hzpc.elf<br/>@ FLASH 0x10002000"]

G --> H["阶段3 POST_BUILD"]
H --> I["objcopy -O binary → ble_hzpc.bin"]
H --> J["objcopy -O ihex → ble_hzpc.hex"]
H --> K["objdump --source → ble_hzpc.lst"]
H --> L["size 打印段占用"]
H --> M["check_image_size.cmake<br/>硬校验 ≤ 332K,超限 FATAL"]

G --> N["tasks.json → J-Link: Flash<br/>loadfile ble_hzpc.elf"]
I --> O["post_process.py<br/>加 8K 头 → ble_hzpc_burn.bin<br/>(串口烧录)"]

style G fill:#1f6feb,color:#ffffff
style M fill:#d73a49,color:#ffffff
style N fill:#2ea043,color:#ffffff

四、几个值得注意的点

  1. 没有 configure 任务。tasks.json 里只有 --build,首次克隆或改了 CMakeLists.txt 结构后需手动跑一次 cmake --preset arm-gcc-debug(或靠 CMake Tools 扩展)。实测 build/debug/ 里已有 CMakeCache.txt 和 build.ninja,说明已配置过。

  2. 预设文件名大小写:工程里叫 CmakePresets.json,而 CMake / VS Code CMake Tools 按惯例查找 CMakePresets.json。macOS 默认分区大小写不敏感所以能work,但换到大小写敏感的环境(或 Linux CI)会找不到预设。

  3. arm-gcc-release 预设没人用:只有 debug 被三个任务引用,release 目前是”定义了但没接按钮”。

  4. Flash 任务假设 build/debug/ 已存在:命令直接往 ${workspaceFolder}/build/debug/flash.jlink 重定向,若目录没建会失败(当前目录已存在,所以没问题)。

  5. build/ 根目录有一批旧产物(ble_hzpc.elf/.bin/.hex/.lst/.map),是早前 binaryDir 直接设为 build 时留下的,与现在的 build/debug 无关,容易混淆。

  6. 332K 上限的三处联动:CMakeLists.txt 的 FLASHENV_OFFSET、Inc/fal_cfg.h 的 FAL_PART_TABLE 偏移、common/ota/ota.c 的 OTA B 槽保底值,改一处必须同步另外两处,否则会静默覆盖参数区。

  7. gcc/Makefile 是历史遗留路径,CMake 才是当前主用;两者源列表已不完全一致,维护时以 CMakeLists.txt 为准。


需要我进一步展开哪一块?例如:

  • 逐行讲 post_process.py 生成的 48 字节镜像头每个字段的含义;
  • 梳理内部 Flash 1MB 的完整分区表(A 镜像 / FlashEnv / OTA B 槽 / 余量);
  • 或者给 tasks.json 补一个 Clean / CMake: Configure 任务(这需要切到 craft 模式才能落盘)。