[{"content":"本文只覆盖 编辑 + 自动补全 + 函数跳转。编译 / 烧录 / 断点调试的完整硬件链路见 Windows 下 openvela 的编译、烧录与调试；任务入口已在 .vscode/tasks.json / launch.json 预留为 scaffold。\n正确架构（必须遵守） 1 2 3 4 5 6 7 8 9 10 11 12 ┌──────────────────────────────────────────────────────────────┐ │ VS Code Remote - WSL（唯一推荐的写代码窗口） │ │ │ │ 编辑 / 补全 / 跳转 → WSL 内 ms-vscode.cpptools │ │ + Linux arm-none-eabi-gcc │ │ + NuttX/openvela 真实头文件 │ │ │ │ 编译（后续） → 仍在 WSL 跑 openvela 工具链 │ │ 烧录（后续） → Windows CubeProgrammer + ST-LINK │ │ 调试（后续） → Windows OpenOCD + Cortex-Debug │ │ request: attach, loadFiles: [] │ └──────────────────────────────────────────────────────────────┘ 职责 跑在哪 为什么 源码编辑、补全、跳转 WSL NuttX 大量 Linux 绝对路径符号链接；Windows 本地 C/C++ 扩展会跟丢，并混用 Windows newlib 与 NuttX libc 固件编译 WSL openvela 官方工具链与 build.sh 是 Linux 路径 QSPI 烧录 Windows STM32H750 超 128 KiB 片内 Flash 时，用 CubeProgrammer External Loader 写外部 QSPI；OpenOCD 不负责下载 断点调试 Windows OpenOCD ST-LINK 由 Windows 访问；GDB 只 attach 符号，不再 load flash 不要把下面的 UNC 路径当普通 Windows 文件夹长期写代码：\n1 \\\\wsl.localhost\\Debian\\home\\\u0026lt;user\u0026gt;\\openvela\\contest2026_004_TeamFalcons 一次性准备 Windows 侧 安装 WSL（本仓默认发行版名 Debian，可用环境变量 OPENVELA_WSL_DISTRO 覆盖）。 安装 VS Code，并启用 “Add to PATH”。 安装扩展： ms-vscode-remote.remote-wsl （进入 WSL 窗口后再装）ms-vscode.cpptools （后续调试再装）marus25.cortex-debug openvela 整树放在 WSL 文件系统里，例如： /home/\u0026lt;user\u0026gt;/openvela/，本仓是其子目录 contest2026_004_TeamFalcons/。 打开正确窗口 任选其一：\n1 2 3 # 在 Windows PowerShell 中 cd \\\\wsl.localhost\\Debian\\home\\\u0026lt;user\u0026gt;\\openvela\\contest2026_004_TeamFalcons .\\scripts\\windows_open_vscode_wsl.ps1 或在已经误开的 UNC 窗口里：Ctrl+Shift+P → Tasks: Run Task →\n1 openvela: reopen workspace in WSL for IntelliSense 左下角必须显示 WSL: Debian（或你的发行版名）。\n生成 IntelliSense 头文件 NuttX 的 include/arch 符号链接和 include/nuttx/config.h 在 configure 之后才存在。 在 Remote - WSL 窗口执行默认构建任务（Ctrl+Shift+B）：\n1 openvela: prepare IntelliSense headers (WSL) 等价命令：\n1 2 ./scripts/prepare_wsl_intellisense.sh # 可选：BOARD_CONFIG=stm32h750b-dk:lvgl FORCE_CONFIGURE=1 ./scripts/prepare_wsl_intellisense.sh 然后：\n扩展面板确认 C/C++ 已安装到 WSL C/C++: Reset IntelliSense Database Developer: Reload Window 验证补全与跳转 打开 app/hello_app/hello_app_main.c：\n把光标放在 printf 上，按 F12（Go to Definition）应进入 NuttX/stdio 相关声明。 输入 prin 应出现补全。 状态栏 C/C++ 配置名应为 openvela STM32H750 (WSL)。 若红色波浪很多且无法跳转，按顺序检查：\n是否 Remote - WSL（不是 UNC / 本地 Windows 文件夹） 是否已跑 prepare_wsl_intellisense.sh，且存在： ../nuttx/include/nuttx/config.h ../nuttx/include/arch → ../nuttx/arch/arm/include ../prebuilts/gcc/linux-x86_64/arm-none-eabi/bin/arm-none-eabi-gcc 是否可执行 是否 Reset 过 IntelliSense Database 与后续编译 / 烧录的衔接 当前默认 Ctrl+Shift+B 只准备 IntelliSense，不烧板。\n已预留的 scaffold 任务（脚本在、产品 app 选择后续再对齐）：\nTask 作用 openvela: build firmware via Windows host script (scaffold) 经 PowerShell 调 WSL 编译，产物进 .debug/ openvela: flash firmware via CubeProgrammer (scaffold) Windows CubeProgrammer 写 QSPI + boot stub openvela: flash current artifacts only (scaffold) 只烧已有 .debug 产物 Debug: openvela: attach only (scaffold) Cortex-Debug attach，loadFiles: [] 硬件工具路径在 .vscode/settings.json 的 cortex-debug.* 项；本机安装位置不同时只改该文件，或设：\n环境变量 用途 OPENVELA_WSL_DISTRO WSL 发行版，默认 Debian OPENVELA_ROOT_WSL openvela 根目录 OPENVELA_OUT_DIR 产物目录，默认仓内 .debug STM32_PROGRAMMER_CLI CubeProgrammer CLI STM32_EXTERNAL_LOADER MT25TL01G_STM32H750B-DISCO.stldr 仓库内相关文件 1 2 3 4 5 6 7 8 9 .vscode/c_cpp_properties.json # 补全 / 跳转（本环境核心） .vscode/extensions.json # 推荐扩展 .vscode/settings.json # C/C++ + 后续调试工具路径 .vscode/tasks.json # prepare + 后续 build/flash scaffold .vscode/launch.json # 后续 attach scaffold scripts/prepare_wsl_intellisense.sh scripts/windows_open_vscode_wsl.ps1 scripts/windows_build_openvela.ps1 # 后续 scripts/windows_flash_cube.ps1 # 后续 ","permalink":"https://www.19y.cc/p/openvela-windows-vscode-setup/","summary":"\u003cp\u003e本文只覆盖 \u003cstrong\u003e编辑 + 自动补全 + 函数跳转\u003c/strong\u003e。编译 / 烧录 / 断点调试的完整硬件链路见\n\u003ca href=\"/p/openvela-windows-build-debug/\"\u003eWindows 下 openvela 的编译、烧录与调试\u003c/a\u003e；任务入口已在\n\u003ccode\u003e.vscode/tasks.json\u003c/code\u003e / \u003ccode\u003elaunch.json\u003c/code\u003e 预留为 scaffold。\u003c/p\u003e\n\u003ch2 id=\"正确架构必须遵守\"\u003e正确架构（必须遵守）\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e┌──────────────────────────────────────────────────────────────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ VS Code Remote - WSL（唯一推荐的写代码窗口）                  │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│                                                              │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│  编辑 / 补全 / 跳转  →  WSL 内 ms-vscode.cpptools            │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│                        + Linux arm-none-eabi-gcc             │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│                        + NuttX/openvela 真实头文件           │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│                                                              │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│  编译（后续）        →  仍在 WSL 跑 openvela 工具链          │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│  烧录（后续）        →  Windows CubeProgrammer + ST-LINK     │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│  调试（后续）        →  Windows OpenOCD + Cortex-Debug       │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│                        request: attach, loadFiles: []        │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└──────────────────────────────────────────────────────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e职责\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e跑在哪\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e为什么\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e源码编辑、补全、跳转\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWSL\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNuttX 大量 Linux 绝对路径符号链接；Windows 本地 C/C++ 扩展会跟丢，并混用 Windows newlib 与 NuttX libc\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e固件编译\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWSL\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eopenvela 官方工具链与 \u003ccode\u003ebuild.sh\u003c/code\u003e 是 Linux 路径\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eQSPI 烧录\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWindows\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSTM32H750 超 128 KiB 片内 Flash 时，用 CubeProgrammer External Loader 写外部 QSPI；OpenOCD 不负责下载\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e断点调试\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWindows OpenOCD\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eST-LINK 由 Windows 访问；GDB 只 attach 符号，不再 load flash\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e不要\u003c/strong\u003e把下面的 UNC 路径当普通 Windows 文件夹长期写代码：\u003c/p\u003e","title":"Windows VS Code 写代码环境：openvela 的补全与跳转"},{"content":" 写代码（补全 / 跳转）请先看 Windows VS Code 写代码环境。 本文侧重编译、QSPI 烧录与 Cortex-Debug；编辑窗口必须是 Remote - WSL。\n方案边界 STM32H750XBH6 只有 128 KiB 片内 Flash。超过该容量的 openvela 镜像采用：\n1 2 0x08000000 片内 Flash：QSPI boot stub 0x90000000 外部 QSPI：NuttX/openvela XIP 主镜像 本项目固定以下职责，避免再次把不存在的片内空间声明给 OpenOCD：\nWSL：编译 Linux 工具链下的 openvela。 STM32CubeProgrammer：使用 MT25TL01G_STM32H750B-DISCO.stldr 写入外部 QSPI，再写入片内 boot stub。 OpenOCD：只作为 Cortex-Debug 的 GDB server，不负责 QSPI 下载。 历史提交 5eb5070 已在本板验证：xPack OpenOCD 的 stmqspi 无法稳定 JEDEC probe/write 双 MT25TL01G，而上述 CubeProgrammer External Loader 可以 正常擦写和校验。\n1. 环境 在 Windows 安装：\nVS Code，以及 ms-vscode-remote.remote-wsl、ms-vscode.cpptools、 marus25.cortex-debug 扩展； WSL，完整 openvela 工作区位于 WSL 文件系统； STM32CubeCLT/STM32CubeProgrammer； Windows xPack OpenOCD。 开发板通过 ST-LINK USB 连接到 Windows。即使源码和编译环境位于 WSL， CubeProgrammer、OpenOCD 和 ST-LINK 都由 Windows 进程访问，不需要把 USB 设备转发给 WSL。\n源码编辑、自动补全和函数跳转必须在 Remote - WSL 窗口中进行。不要把下面 的 UNC 路径当作普通 Windows 文件夹长期开发：\n1 \\\\wsl.localhost\\Debian\\home\\\u0026lt;user\u0026gt;\\openvela\\contest2026_004_TeamFalcons Windows 本地 C/C++ 扩展无法正确跟随 NuttX 的 Linux 绝对符号链接，还会把 Windows newlib 与 NuttX libc 混用，表现为 velaguard.c 没有可靠补全、 不能跳转或出现大量错误类型提示。若当前已经打开 UNC 窗口，按 Ctrl+Shift+P 运行 Tasks: Run Task，选择：\n1 openvela: reopen workspace in WSL for IntelliSense 新窗口左下角必须显示 WSL: Debian。首次进入时按扩展面板提示，把 ms-vscode.cpptools 安装到 WSL；然后运行 C/C++: Reset IntelliSense Database 和 Developer: Reload Window。项目配置会使用 WSL 内的真实 ARM GCC、NuttX 头文件与 Linux 符号链接。\n当前硬件任务仍由 Windows 进程访问 CubeProgrammer、OpenOCD 与 ST-LINK。 因此编辑/跳转使用 Remote - WSL 窗口；执行现有编译、烧录和 Cortex-Debug 任务时保留原 Windows UNC 窗口。不要把 ST-LINK 转发到 WSL，也不要在尚未 配置 Windows 工具桥接的 Remote 窗口直接启动 Cortex-Debug。Windows 工具路径 位于 .vscode/settings.json；安装位置不同时只需修改该文件。\nPowerShell 脚本也支持环境变量：\n变量 用途 OPENVELA_WSL_DISTRO WSL 发行版，默认 Debian OPENVELA_ROOT_WSL WSL 中的 openvela 根目录；默认从参赛仓父目录推导 OPENVELA_OUT_DIR Windows 产物目录，默认参赛仓 .debug STM32_PROGRAMMER_CLI STM32_Programmer_CLI.exe 完整路径 STM32_EXTERNAL_LOADER MT25TL01G_STM32H750B-DISCO.stldr 完整路径 2. 编译 按 Ctrl+Shift+B，运行默认测试版本：\n1 openvela: build VelaGuard test QSPI firmware 构建脚本会：\n将团队仓内的 QSPI 补丁幂等应用到当前 NuttX checkout； 维护 manifest 声明的 VelaGuard 生成态 linkfile； 以 stm32h750b-dk:lvgl 作为板级驱动基线，但禁用官方 lvgldemo 和开发期 VS Code Lab； 启用 CONFIG_STM32H750B_DK_QSPI_BOOT； 启用项目自有 VelaGuard 并将 velaguard_app_main 设置为启动入口 （由 app/velaguard/Makefile 的 PROGNAME 决定，改名时同步修改脚本）； 构建主镜像和内部 boot stub； 复制以下文件到参赛仓 .debug： 1 2 3 nuttx.elf nuttx.hex nuttx.bin qspi_bootstub.elf qspi_bootstub.hex qspi_bootstub.bin nuttx.config build-info.txt 调试构建使用任务 openvela: build VelaGuard test debug QSPI firmware，它额外 启用 -g3 和 -Og 调试优化构建。生产身份验证使用 openvela: build VelaGuard production QSPI firmware。产品模式 test/production 与编译模式 debug/release 相互独立。\n增量 vs 全量 默认 incremental（日常改 app 后 flash 应走这条）：\n已有 nuttx/.config 时 跳过 configure.sh； 仅在 kconfig-tweak 后 .config 真的变化 时才 make clean； 否则直接 make -j，只重编改动的目标。 需要全量时用 -Rebuild full 或任务 openvela: FULL clean+build production QSPI firmware / openvela: FULL clean+flash production (CubeProgrammer)（会重新 configure.sh 并强制 make clean）。\n1 2 3 4 5 6 7 # 默认增量 scripts\\windows_flash_cube.ps1 -VelaGuardMode production # 强制全量 scripts\\windows_flash_cube.ps1 -VelaGuardMode production -Rebuild full # 或 scripts\\windows_build_openvela.ps1 -VelaGuardMode production -FullClean 从 test 切到 production（或改 debug 符号）时，.config 会变，仍会 clean 一次，属正常。\n判断编译正确不要只看任务退出码，还应确认 .debug 中六个产物都存在，且 日志显示主镜像位于 0x9000xxxx、boot stub 位于 0x0800xxxx。\n3. 下载 运行：\n1 openvela: flash VelaGuard test QSPI firmware (CubeProgrammer) 脚本在连接硬件前检查：\n主 HEX 全部位于 0x90000000..0x97ffffff； boot stub 全部位于 0x08000000..0x0801ffff； CubeProgrammer CLI 与 External Loader 存在。 随后依次执行：\n1 2 3 Cube + External Loader → 写入并校验 QSPI 主镜像 Cube → 写入并校验片内 boot stub Cube → 复位 已有产物时可使用 openvela: flash current artifacts only (CubeProgrammer)。生产版本使用 openvela: flash VelaGuard production firmware (CubeProgrammer)。\n没有连接开发板时，可在 Windows PowerShell 只验证工具和镜像布局：\n1 scripts\\windows_flash_cube.ps1 -NoBuild -ValidateOnly 4. 断点调试 选择：\n1 openvela: VelaGuard test debug after Cube flash 按 F5 后会先构建并用 CubeProgrammer 下载 debug 镜像，再由 OpenOCD 启动 GDB server。配置使用 request: attach 和空 loadFiles，因此 GDB 只 加载 ${workspaceFolder}\\.debug\\nuttx.elf 的符号，不会再次写 Flash。\n若固件已经下载，使用 openvela: VelaGuard attach only。\n推荐的日常操作顺序是：\n普通运行验证：执行 openvela: flash VelaGuard test QSPI firmware (CubeProgrammer)，它会自动编译、烧录并复位。 调试代码：在“运行和调试”中选择 openvela: VelaGuard test debug after Cube flash 后按 F5；该配置会先生成并烧录 debug 固件，再 attach。 只增加或移动断点、不改固件：选择 attach only，避免重复擦写 QSPI。 改过代码后不要直接使用 attach only，否则板上代码与 ELF 符号可能不一致。 连接后若程序正在运行，可先暂停，再在应用源码中下断点并继续。不要用 VS Code 或 GDB 的 Download/Load 命令；本项目的 loadFiles: [] 正是为了阻止这条错误 路径。\n单窗口方案（可选）：Remote - WSL 里直接 F5 如果不想在两个窗口之间切换，可以把编译 / 烧录 / 调试全部留在 Remote - WSL 窗口完成，Windows 只充当 ST-LINK 的硬件宿主。这是仓库默认的调试姿势， 完整支持断点、单步和源码高亮：\nGDB：WSL 内的原生 Linux gdb-multiarch（已装到 ~/tools/gdb-multiarch，免 sudo），源码路径原生，高亮正常。 OpenOCD / 烧录：仍是 Windows 进程（ST-LINK USB 由 Windows 访问），由 WSL 侧的 Cortex-Debug 经 WSL interop 拉起；OpenOCD 的 Linux 路径参数由 scripts/wsl_openocd_bridge.sh 转成 Windows 路径。 通信：Linux GDB 通过 localhost:3333 连 Windows OpenOCD，依赖 WSL2 mirrored 网络模式（共享回环）。.wslconfig 已写入 networkingMode=mirrored，需要一次性重启 WSL 生效。 仓库已配置好，无需改 launch.json。一次性启用步骤（只需做一次）：\n确认 C:\\Users\\\u0026lt;user\u0026gt;\\.wslconfig 内容为： 1 2 [wsl2] networkingMode=mirrored 在 Windows PowerShell 执行 wsl --shutdown，然后重新打开 VS Code Remote - WSL 窗口（注意先保存其他终端的工作）。 在 Remote - WSL 窗口直接按 F5 选 openvela: VelaGuard test debug after Cube flash，构建 → CubeProgrammer 烧录 → OpenOCD attach 全在一个窗口 完成，断点、单步、源码高亮均正常。 相关配置在 .vscode/settings.json 的 cortex-debug.*.linux 三项：\ncortex-debug.gdbPath.linux → scripts/wsl_gdb_launcher.sh（原生 Linux GDB）。 cortex-debug.armToolchainPath.linux → WSL 内 Linux objdump/nm。 cortex-debug.openocdPath.linux → scripts/wsl_openocd_bridge.sh。 回退：若不想启用 mirrored 网络（或不支持，需 Windows 11 22H2+ / WSL 2.0+），把 cortex-debug.gdbPath.linux 指回 /mnt/d/Develop/STM32CubeCLT/GNU-tools-for-STM32/bin/arm-none-eabi-gdb.exe 即可退回\u0026quot;Windows GDB 桥接\u0026quot;模式：功能正常，但 GDB 上报 UNC 源码路径， Linux 侧无法自动打开源码，高亮不可用。原 UNC 窗口调试流程也不受影响。\n5. VelaGuard 运行验证 ST-LINK 虚拟串口使用 115200 8N1。烧录后，VelaGuard 工业首页会自动显示， 不需要运行 openvela 的 lvgldemo。界面显示 Device ID、test/production 模式、固件版本、存储启动状态，以及 Acquisition、Alarm、Network、Audio、 Time 五类状态占位。\n在 app/velaguard/velaguard.c（VelaGuard 应用源码）中设置断点，按 F5 attach。应用主循环每秒都会 运行，断点会稳定命中，可检查模块级状态变量。\nNSH 仍在实际枚举的 ST-LINK COM 口（本机当前为 COM7）以 115200 8N1 可用。触摸初始化成功时串口会出现 /dev/input0 open success, maxpoint 1， 应用启动成功后还会输出 [velaguard] UI ready。ISSUE1 首页没有虚构的交互 按钮，触摸仅作为平台连续性验证。\n6. 故障恢复 Main QSPI image address range is invalid：构建未启用 QSPI linker，禁止下载。 找不到 External Loader：检查 STM32CubeProgrammer 安装，或设置 STM32_EXTERNAL_LOADER。 OpenOCD 无法 attach：关闭 CubeProgrammer GUI 和其他 OpenOCD 进程，确认 ST-LINK 未被占用。 QSPI 下载成功但不启动：先检查 boot stub 是否写入片内 Flash，再检查复位后 PC 是否进入 0x9000xxxx。若停在 0x080002aa，检查 boot stub 是否错误要求 NuttX 初始 MSP 8 字节对齐；当前实现只要求合法 SRAM 范围和 4 字节对齐。 屏幕只有背光：查看串口是否出现 [velaguard] UI ready；若没有，检查更早的 板级/LVGL 初始化错误，确认烧录的是刚生成的 .debug 产物。 LVGL 报 get touch maxpoints failed (errno=25)：当前 NuttX checkout 未应用 团队补丁，重新执行项目构建任务，不要只烧录旧 .debug 产物。 能显示但触摸无效：先看串口是否出现 /dev/input0 open success，再检查触摸 读事件；若点击位置方向错误，再核对 CONFIG_FT5X06_SWAPXY 和屏幕旋转配置。 GDB 中 lv_nuttx_init() 显示 disp=NULL 但 indev!=NULL：不要硬编码 /dev/lcd0；当前 framebuffer 配置应保留 lv_nuttx_dsc_init() 选择的 /dev/fb0。 芯片保护或连接异常：在 CubeProgrammer GUI 中检查 Option Bytes；记录原值后 再处理。正常流程不会自动修改 Option Bytes 或执行 mass erase。 ","permalink":"https://www.19y.cc/p/openvela-windows-build-debug/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e写代码（补全 / 跳转）请先看\u003c/strong\u003e\n\u003ca href=\"/p/openvela-windows-vscode-setup/\"\u003eWindows VS Code 写代码环境\u003c/a\u003e。\n本文侧重编译、QSPI 烧录与 Cortex-Debug；编辑窗口必须是 Remote - WSL。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"方案边界\"\u003e方案边界\u003c/h2\u003e\n\u003cp\u003eSTM32H750XBH6 只有 128 KiB 片内 Flash。超过该容量的 openvela 镜像采用：\u003c/p\u003e","title":"Windows 下 openvela 的编译、烧录与调试（STM32H750B-DK）"},{"content":"本文记录我们这一路从 openvela 竞赛模板走到 STM32H750B-DK 点亮第一颗 LED 的完整过程。\n它不是只告诉你“改哪一行”，而是帮你建立一套迁移思维：你已经有 STM32HAL、FreeRTOS、LwIP、FatFs、Modbus 经验，那么学习 openvela/NuttX 时，最重要的是把已有经验映射到 NuttX 的概念上。\n0. 你应该先建立的总图 在 STM32HAL 工程里，你大概会这样点灯：\n1 2 3 4 HAL_GPIO_WritePin(GPIOJ, GPIO_PIN_2, GPIO_PIN_RESET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOJ, GPIO_PIN_2, GPIO_PIN_SET); HAL_Delay(500); 在 openvela/NuttX 里，我们最终希望应用层这样写：\n1 2 3 4 board_userled(BOARD_LED_GREEN, true); usleep(500 * 1000); board_userled(BOARD_LED_GREEN, false); usleep(500 * 1000); 两者最大的区别是：\n1 2 3 4 5 STM32HAL 写法： 应用直接知道 GPIO 端口、引脚、电平极性。 NuttX/openvela 推荐写法： 应用只表达“我要控制逻辑 LED”，具体 GPIO、极性、板子差异交给 BSP。 所以这条链路是：\n1 2 3 4 5 6 7 contest app -\u0026gt; board_userled_initialize() -\u0026gt; board_userled(BOARD_LED_GREEN, true/false) -\u0026gt; nuttx/boards/.../stm32_userleds.c -\u0026gt; stm32_gpiowrite() -\u0026gt; STM32 GPIO -\u0026gt; LED 亮灭 这就是我们这一路真正打通的东西。\n1. 竞赛目录规则 你的当前竞赛仓库是：\n1 /home/\u0026lt;user\u0026gt;/openvela/contest2026_004_TeamFalcons openvela 全量源码在外层：\n1 /home/\u0026lt;user\u0026gt;/openvela 关键目录关系：\n1 2 3 4 5 /home/\u0026lt;user\u0026gt;/openvela/ nuttx/ 公共 NuttX 内核和 BSP 仓 apps/ openvela/NuttX app 框架 packages/ package 聚合目录 contest2026_004_TeamFalcons/ 你的比赛仓 比赛规范的核心是：\n1 2 3 4 5 6 应用赛道代码： 放在 contest2026_004_TeamFalcons/app/hello_app/ BSP 修复： 可以本地验证，但正式提交不能混在你的应用仓 PR 里。 应该单独 fork 公共 nuttx 仓，向 dev-ai-contest-2026 分支提 PR。 你的应用实际路径是：\n1 contest2026_004_TeamFalcons/app/hello_app/ openvela 编译树里通过软链接映射到：\n1 packages/demos/contest2026_004_hello_app 这就是为什么你不需要手动 copy 文件。比赛 manifest 会通过 \u0026lt;linkfile\u0026gt; 处理。\n2. 我们从模板工程改了什么 模板最开始是 team 000 hello app。\n我们把它改成了你队伍的 app：\n1 2 team 000 -\u0026gt; team 004 hello_app -\u0026gt; first_led 涉及这些文件：\n1 2 3 4 5 app/hello_app/Kconfig app/hello_app/Make.defs app/hello_app/Makefile app/hello_app/CMakeLists.txt app/hello_app/hello_app_main.c 2.1 Kconfig 做什么 Kconfig 的作用类似你在 STM32HAL 工程里的各种宏开关，例如：\n1 2 #define HAL_TIM_MODULE_ENABLED #define HAL_GPIO_MODULE_ENABLED 只不过 NuttX 用 Kconfig 统一管理。\n我们的 app Kconfig 符号是：\n1 CONFIG_LVX_USE_DEMO_CONTEST2026_004_HELLO_APP 它的含义是：\n1 是否把你的 first_led app 编进系统。 如果这个没打开，你的代码即使写对了，也不会进最终固件。\n2.2 Make.defs 做什么 Make.defs 告诉 NuttX：\n1 2 如果 CONFIG_LVX_USE_DEMO_CONTEST2026_004_HELLO_APP=y， 就把 packages/demos/contest2026_004_hello_app 加入编译。 这一步相当于 STM32HAL 工程里把某个 .c 文件加入工程。\n2.3 Makefile 做什么 Makefile 里最关键的是：\n1 2 PROGNAME = first_led MAINSRC = hello_app_main.c 含义是：\n1 2 这个应用叫 first_led。 主源文件是 hello_app_main.c。 在 NuttX 内置应用模式下，源码里仍然写：\n1 int main(int argc, char *argv[]) 但是构建系统会根据 PROGNAME = first_led 处理入口符号，所以 .config 里启动入口使用：\n1 2 CONFIG_INIT_ENTRYPOINT=\u0026#34;first_led_main\u0026#34; CONFIG_INIT_ENTRYNAME=\u0026#34;first_led\u0026#34; 你可以把它理解成：\n1 2 3 4 源码 main() -\u0026gt; 构建系统包装/重命名 -\u0026gt; first_led_main() -\u0026gt; 系统启动后直接运行 first_led 3. 当前 app 的最终代码 当前文件：\n1 app/hello_app/hello_app_main.c 核心代码是：\n1 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 \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;arch/board/board.h\u0026gt; #include \u0026lt;nuttx/board.h\u0026gt; /* Blink the board\u0026#39;s first user LED through the NuttX board LED API. */ #define FIRST_LED BOARD_LED_GREEN int main(int argc, char *argv[]) { uint32_t nleds = board_userled_initialize(); if (nleds == 0) { printf(\u0026#34;first_led: no user LEDs available\\n\u0026#34;); return 1; } for (; ; ) { board_userled(FIRST_LED, true); usleep(500 * 1000); board_userled(FIRST_LED, false); usleep(500 * 1000); } } 逐行解释：\n1 #include \u0026lt;arch/board/board.h\u0026gt; 这个头文件提供板级符号，比如：\n1 2 3 BOARD_LED_GREEN BOARD_LED_RED BOARD_NLEDS 也就是“这块板子有哪些逻辑 LED”。\n1 #include \u0026lt;nuttx/board.h\u0026gt; 这个头文件提供通用板级 API，例如：\n1 2 3 board_userled_initialize() board_userled() board_userled_all() 也就是“应用如何通过 NuttX 标准方式操作 LED”。\n1 #define FIRST_LED BOARD_LED_GREEN 这行很重要。它避免了硬编码：\n1 #define FIRST_LED 0 为什么不要硬编码 0？\n因为 0 只是数组下标。今天 index 0 是 PJ2，明天 BSP 修了映射，index 0 也许仍是绿灯，但具体管脚可能变。应用层应该依赖语义符号，而不是依赖下标。\n1 uint32_t nleds = board_userled_initialize(); 这相当于 HAL 里的 GPIO 初始化：\n1 HAL_GPIO_Init(...) 只是 NuttX 帮你封装在 BSP 里。\n1 board_userled(FIRST_LED, true); 含义是：\n1 点亮 FIRST_LED。 注意，应用层不应该关心它到底写高电平还是低电平。极性应该由 BSP 处理。\n1 usleep(500 * 1000); 类似 FreeRTOS 的：\n1 vTaskDelay(pdMS_TO_TICKS(500)); 只是这里用 POSIX 风格的 usleep()，单位是微秒。\n4. 三个 LED 配置开关一定要分清 这部分是我们一路上最关键的知识点。\n4.1 CONFIG_ARCH_HAVE_LEDS 1 CONFIG_ARCH_HAVE_LEDS=y 含义：\n1 这个板子的 BSP 声明：我提供标准 LED API。 它不是“内核接管 LED”，只是“这个板子有 LED 能力”。\n有了它，\u0026lt;nuttx/board.h\u0026gt; 才会暴露这些函数声明：\n1 2 3 uint32_t board_userled_initialize(void); void board_userled(int led, bool ledon); void board_userled_all(uint32_t ledset); 如果没有它，你就会遇到这种 warning：\n1 2 warning: implicit declaration of function \u0026#39;board_userled_initialize\u0026#39; warning: implicit declaration of function \u0026#39;board_userled\u0026#39; 这就是为什么我们不应该在 app 里手动写：\n1 2 uint32_t board_userled_initialize(void); void board_userled(int led, bool ledon); 手动声明只是绕过编译器 warning，不是根治问题。\n根治问题是让 BSP 正确：\n1 2 3 4 config ARCH_BOARD_STM32H750B_DK bool \u0026#34;STM32H750B-DK board\u0026#34; depends on ARCH_CHIP_STM32H750B select ARCH_HAVE_LEDS 4.2 CONFIG_ARCH_LEDS 1 # CONFIG_ARCH_LEDS is not set 含义：\n1 不要让 NuttX 内核把 LED 当成系统状态灯使用。 如果打开它：\n1 CONFIG_ARCH_LEDS=y 内核会拿 LED 表示启动、异常、panic、idle 等状态。\n这时应用再调用 board_userled() 控制 LED，就会出现所有权冲突：\n1 2 3 内核也在改 LED 你的 app 也在改 LED 最后现象会变得很难判断 所以应用赛道点灯时，应该关闭它。\n4.3 CONFIG_USERLED 1 # CONFIG_USERLED is not set 含义：\n1 不注册 /dev/userleds 这个字符设备。 如果你以后想用文件设备方式控制 LED，例如：\n1 2 open(\u0026#34;/dev/userleds\u0026#34;) ioctl(...) 那才考虑打开它。\n现在我们直接调用 board_userled()，所以不需要它。\n4.4 当前正确组合 我们当前希望的组合是：\n1 2 3 CONFIG_ARCH_HAVE_LEDS=y # CONFIG_ARCH_LEDS is not set # CONFIG_USERLED is not set 翻译成人话：\n1 2 3 4 板子提供 LED API。 内核不抢 LED。 /dev/userleds 不抢 LED。 应用通过 board_userled() 控制 LED。 5. 为什么之前会有 warning 之前编译时出现：\n1 2 warning: implicit declaration of function \u0026#39;board_userled_initialize\u0026#39; warning: implicit declaration of function \u0026#39;board_userled\u0026#39; 它的意思不是“链接不到函数”，而是：\n1 编译 hello_app_main.c 时，编译器没有看到函数声明。 原因是：\n1 2 3 STM32H750B-DK BSP 实际实现了 board_userled_initialize() 和 board_userled()， 但 Kconfig 没有 select ARCH_HAVE_LEDS， 所以 \u0026lt;nuttx/board.h\u0026gt; 把这些函数声明藏起来了。 我们一开始临时手动声明，能让编译通过，但不优雅。\n正确做法是：\n1 2 3 BSP 声明自己 HAVE_LEDS。 应用 include 标准头文件。 应用不手写外部函数声明。 6. 为什么 LED 现象一度很奇怪 你观察到过：\n1 2 3 LD6 闪烁 LD7 熄灭 LD8 常亮 这不是烧录失败，而是 BSP 的 LED 表和真实硬件不一致。\n原本 openvela/NuttX 这个板级代码里大致是：\n1 2 3 index 0 -\u0026gt; PI13 index 1 -\u0026gt; PJ2 index 2 -\u0026gt; PD3 而你的 app 当时写的是：\n1 #define FIRST_LED 0 所以：\n1 2 3 4 FIRST_LED 0 -\u0026gt; index 0 -\u0026gt; PI13 -\u0026gt; 你看到 LD6 闪烁 至于 LD8 常亮，是因为 BSP 把 PD3 也混进 user LED 表里，并且我们当时把所有 LED 都按 active-low 处理。PD3/LD8 不应该简单套入和 PJ2/PI13 一样的逻辑。\n最后我们按更合理的 BSP 语义修成：\n1 2 3 BOARD_NLEDS = 2 BOARD_LED_GREEN -\u0026gt; PJ2 BOARD_LED_RED -\u0026gt; PI13 并且不再把 PD3 纳入 board_userled() 管理。\n7. LED 极性为什么要放在 BSP 修 STM32H750B-DK 上用户 LED 是 active-low。\n含义：\n1 2 GPIO 写 0，LED 亮。 GPIO 写 1，LED 灭。 如果应用层自己写：\n1 board_userled(FIRST_LED, !ledon); 这就错了。\n因为应用层不应该知道极性。\n正确分层是：\n1 2 3 4 5 应用层： board_userled(led, true) 表示我要点亮。 BSP 层： 发现硬件 active-low，于是内部写 !ledon。 BSP 里的关键逻辑是：\n1 2 3 4 5 6 7 void board_userled(int led, bool ledon) { if ((unsigned)led \u0026lt; ARRAYSIZE(g_ledcfg)) { stm32_gpiowrite(g_ledcfg[led], !ledon); } } 这和你熟悉的 HAL BSP 类似：\n1 2 3 4 void BSP_LED_On(Led_TypeDef Led) { HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET); } 应用只调用：\n1 BSP_LED_On(LED_GREEN); 不应该在应用层到处写 GPIO_PIN_RESET。\n8. BSP 我们实际修了什么 这些属于公共 nuttx 仓的修复：\n1 2 3 4 5 /home/\u0026lt;user\u0026gt;/openvela/nuttx/boards/Kconfig /home/\u0026lt;user\u0026gt;/openvela/nuttx/boards/arm/stm32h7/stm32h750b-dk/include/board.h /home/\u0026lt;user\u0026gt;/openvela/nuttx/boards/arm/stm32h7/stm32h750b-dk/src/stm32h750b-dk.h /home/\u0026lt;user\u0026gt;/openvela/nuttx/boards/arm/stm32h7/stm32h750b-dk/src/stm32_userleds.c /home/\u0026lt;user\u0026gt;/openvela/nuttx/boards/arm/stm32h7/stm32h750b-dk/src/stm32_autoleds.c 8.1 boards/Kconfig 增加：\n1 select ARCH_HAVE_LEDS 目的：\n1 让 STM32H750B-DK 声明自己有标准 LED API。 8.2 stm32h750b-dk.h 把 LED GPIO 定义整理成：\n1 2 GPIO_LD1 -\u0026gt; PJ2 GPIO_LD2 -\u0026gt; PI13 并且默认输出高电平：\n1 GPIO_OUTPUT_SET 因为 active-low LED 在高电平时是熄灭的。\n8.3 board.h 把逻辑 LED 数量改成：\n1 #define BOARD_NLEDS 2 并定义：\n1 2 #define BOARD_LED_GREEN BOARD_LED1 #define BOARD_LED_RED BOARD_LED2 8.4 stm32_userleds.c 修正用户态 LED 控制逻辑：\n1 stm32_gpiowrite(g_ledcfg[led], !ledon); 以及：\n1 stm32_gpiowrite(g_ledcfg[i], (ledset \u0026amp; (1 \u0026lt;\u0026lt; i)) == 0); 这让：\n1 2 board_userled(..., true) -\u0026gt; 点亮 board_userled(..., false) -\u0026gt; 熄灭 语义恢复正常。\n8.5 stm32_autoleds.c 虽然当前我们关闭了：\n1 # CONFIG_ARCH_LEDS is not set 所以 stm32_autoleds.c 当前不会参与编译。\n但 BSP PR 应该把它也修掉，否则以后别人打开 CONFIG_ARCH_LEDS 又会遇到 active-high/active-low 反向问题。\n9. 从空白工程复现一遍 下面是你以后从零复现的流程。\n9.1 确认当前目录 1 2 cd /home/\u0026lt;user\u0026gt;/openvela/contest2026_004_TeamFalcons pwd 应该看到：\n1 /home/\u0026lt;user\u0026gt;/openvela/contest2026_004_TeamFalcons 9.2 确认 app 被软链进 packages 1 ls -l /home/\u0026lt;user\u0026gt;/openvela/packages/demos | grep contest2026_004 应该看到类似：\n1 contest2026_004_hello_app -\u0026gt; ../../contest2026_004_TeamFalcons/app/hello_app 如果没有这个软链，说明 manifest/linkfile 没生效，编译树看不到你的 app。\n9.3 配置板子 1 2 cd /home/\u0026lt;user\u0026gt;/openvela/nuttx ./tools/configure.sh stm32h750b-dk:lvgl 如果输出：\n1 No configuration change. 这不是错误，只是说明当前已经是这个配置。\n9.4 设置构建环境变量 如果你直接 make menuconfig，可能会遇到：\n1 2 arm-none-eabi-gcc: command not found kconfig-mconf: command not found 原因是 openvela 自带 prebuilts 没进 PATH。\n手动设置：\n1 2 3 export OPENVELA_DIR=/home/\u0026lt;user\u0026gt;/openvela export PATH=\u0026#34;$OPENVELA_DIR/prebuilts/tools/python/bin:$OPENVELA_DIR/prebuilts/gcc/linux-x86_64/arm-none-eabi/bin:$OPENVELA_DIR/prebuilts/build-tools/linux-x86_64/bin:$PATH\u0026#34; export PYTHONPATH=\u0026#34;$OPENVELA_DIR/prebuilts/tools/python/dist-packages/kconfiglib:$OPENVELA_DIR/prebuilts/tools/python/dist-packages:${PYTHONPATH:-}\u0026#34; 之后再执行：\n1 make menuconfig 9.5 在 menuconfig 里确认关键配置 进入 make menuconfig 后，不建议一层层找菜单。\n更稳的方式是按 / 搜索符号。\n需要确认：\n1 2 3 4 5 6 ARCH_HAVE_LEDS = y ARCH_LEDS = n USERLED = n LVX_USE_DEMO_CONTEST2026_004_HELLO_APP = y INIT_ENTRYPOINT = first_led_main INIT_ENTRYNAME = first_led 最终 .config 应该包含：\n1 2 3 4 5 6 CONFIG_ARCH_HAVE_LEDS=y # CONFIG_ARCH_LEDS is not set # CONFIG_USERLED is not set CONFIG_LVX_USE_DEMO_CONTEST2026_004_HELLO_APP=y CONFIG_INIT_ENTRYPOINT=\u0026#34;first_led_main\u0026#34; CONFIG_INIT_ENTRYNAME=\u0026#34;first_led\u0026#34; 9.6 编译 1 2 cd /home/\u0026lt;user\u0026gt;/openvela/nuttx make -j$(nproc) 成功时会看到：\n1 2 3 LD: nuttx CP: nuttx.hex CP: nuttx.bin 最终固件在：\n1 2 /home/\u0026lt;user\u0026gt;/openvela/nuttx/nuttx.hex /home/\u0026lt;user\u0026gt;/openvela/nuttx/nuttx.bin 9.7 烧录 我们写了一个脚本：\n1 scripts/flash_openocd.sh 它做两件事：\n1 2 1. 先编译。 2. 编译成功后，再烧录 nuttx.hex。 执行：\n1 2 cd /home/\u0026lt;user\u0026gt;/openvela/contest2026_004_TeamFalcons ./scripts/flash_openocd.sh 脚本内部固定烧：\n1 /home/\u0026lt;user\u0026gt;/openvela/nuttx/nuttx.hex 使用的是 Windows 侧 OpenOCD：\n1 /mnt/d/Develop/xpack-openocd-0.12.0-7/bin/openocd.exe 并把 hex 复制到 Windows 临时目录：\n1 C:/Users/\u0026lt;user\u0026gt;/AppData/Local/Temp/openvela_nuttx.hex 这是为了避免 Windows OpenOCD 处理 WSL 路径时踩坑。\n10. VSCode 自动补全为什么一开始不工作 你是通过 VSCode WSL 插件打开目录的。\n自动补全和跳转依赖几个东西：\n1 2 3 4 1. C/C++ 扩展或 clangd。 2. 正确的 includePath。 3. 最好有 compile_commands.json。 4. openvela configure/build 后生成的软链接和配置头文件。 如果没有先 configure/build，很多文件不存在或者路径没展开：\n1 2 3 4 include/arch include/arch/board include/arch/chip generated config 那么 VSCode 不知道 \u0026lt;nuttx/board.h\u0026gt;、\u0026lt;arch/board/board.h\u0026gt; 从哪里来。\n我们仓里后来补了 .vscode 配置，例如：\n1 2 3 4 .vscode/c_cpp_properties.json .vscode/compile_commands.json .vscode/settings.json .vscode/extensions.json 但你要记住：\n1 2 3 IDE 补全不是编译本身。 编译通过才是第一标准。 补全依赖编译配置。 11. 常见错误和判断方法 11.1 arm-none-eabi-gcc 找不到 现象：\n1 arm-none-eabi-gcc: command not found 原因：\n1 交叉编译器没进 PATH。 修法：\n1 2 export OPENVELA_DIR=/home/\u0026lt;user\u0026gt;/openvela export PATH=\u0026#34;$OPENVELA_DIR/prebuilts/gcc/linux-x86_64/arm-none-eabi/bin:$PATH\u0026#34; 我们的烧录脚本已经自动做了这件事。\n11.2 kconfig-mconf 找不到 现象：\n1 kconfig-mconf: command not found 原因：\n1 Kconfig menu 工具没进 PATH。 修法：\n1 export PATH=\u0026#34;/home/\u0026lt;user\u0026gt;/openvela/prebuilts/build-tools/linux-x86_64/bin:$PATH\u0026#34; 11.3 Kconfig 一堆 syntax error 如果看到很多：\n1 2 3 unknown option \u0026#34;--help--\u0026#34; unknown option \u0026#34;osource\u0026#34; syntax error 通常说明：\n1 2 你用了系统里的普通 kconfig 工具， 而不是 openvela prebuilts 里的工具。 先检查：\n1 which kconfig-mconf 应该指向：\n1 /home/\u0026lt;user\u0026gt;/openvela/prebuilts/build-tools/linux-x86_64/bin/kconfig-mconf 11.4 编译通过但 LED 不对 不要先怀疑烧录。\n按这个顺序查：\n1 2 3 4 5 6 7 1. app 里控制的是哪个逻辑 LED？ 2. BOARD_LED_GREEN 在 board.h 里等于几？ 3. stm32_userleds.c 的数组 index 映射到哪个 GPIO？ 4. 这个 GPIO 对应开发板上哪颗 LED？ 5. LED 是 active-high 还是 active-low？ 6. CONFIG_ARCH_LEDS 是否打开导致内核抢 LED？ 7. CONFIG_USERLED 是否打开导致 /dev/userleds 也参与？ 这就是我们排查出 LD6 闪烁、LD7 熄灭、LD8 常亮 的方法。\n11.5 烧录成功但现象没变 检查：\n1 2 3 4 5 1. 你烧的是不是 /home/\u0026lt;user\u0026gt;/openvela/nuttx/nuttx.hex？ 2. 编译是否真的重新生成了 nuttx.hex？ 3. OpenOCD 是否连接到当前这块板？ 4. reset 后程序是否真的从 flash 启动？ 5. app 是否作为 init entry 自动运行？ 可以确认 .config：\n1 grep -n \u0026#34;INIT_ENTRY\\\\|CONTEST2026_004\u0026#34; /home/\u0026lt;user\u0026gt;/openvela/nuttx/.config 期望：\n1 2 3 CONFIG_INIT_ENTRYPOINT=\u0026#34;first_led_main\u0026#34; CONFIG_INIT_ENTRYNAME=\u0026#34;first_led\u0026#34; CONFIG_LVX_USE_DEMO_CONTEST2026_004_HELLO_APP=y 12. 你已有经验如何迁移 12.1 STM32HAL 经验 你已经熟悉：\n1 2 3 4 GPIO 初始化 GPIO 输出高低电平 active-high / active-low HAL_GPIO_WritePin() 迁移到 NuttX 时：\n1 2 3 4 GPIO 初始化逻辑放 BSP。 极性放 BSP。 应用不直接碰 GPIO。 应用调用 board_userled()。 12.2 FreeRTOS 经验 你熟悉：\n1 2 3 4 task vTaskDelay() 优先级 栈大小 迁移到 NuttX 时：\n1 2 3 NuttX app 可以像 POSIX 程序一样写 main()。 delay 可以先用 usleep()。 栈大小在 Makefile 或 Kconfig 里配置。 例如：\n1 STACKSIZE = 2048 类似 FreeRTOS task stack size。\n12.3 LwIP 经验 你熟悉 socket、IP、网卡初始化。\nNuttX 里网络更偏 POSIX 风格：\n1 2 3 4 5 socket() bind() listen() send() recv() 底层网卡和协议栈由 Kconfig 和驱动控制。\n12.4 FatFs 经验 你熟悉：\n1 2 3 f_open() f_read() f_write() NuttX 里更常见的是：\n1 2 3 4 5 open() read() write() close() mount() 文件系统通过 Kconfig 开关和驱动挂载。\n12.5 Modbus 经验 你熟悉串口、帧、超时、CRC。\nNuttX 里串口一般走设备文件：\n1 2 /dev/ttyS0 /dev/ttyS1 应用层用：\n1 2 3 4 open() read() write() ioctl() 你的 Modbus 经验会很有用，只是底层驱动入口从 HAL UART 变成了 POSIX device file。\n13. 你现在最应该掌握的五个能力 13.1 会看 Kconfig 你要知道：\n1 2 3 某个功能有没有编进去？ 某个宏在哪里定义？ 某个选项由谁 select？ 常用方法：\n1 2 grep -RIn \u0026#34;CONFIG_ARCH_HAVE_LEDS\u0026#34; /home/\u0026lt;user\u0026gt;/openvela/nuttx grep -RIn \u0026#34;LVX_USE_DEMO_CONTEST2026_004_HELLO_APP\u0026#34; /home/\u0026lt;user\u0026gt;/openvela 如果机器上没有 rg，用 grep -RIn。\n13.2 会看 .config .config 是当前构建结果的开关集合。\n查 LED：\n1 grep -n \u0026#34;ARCH_HAVE_LEDS\\\\|ARCH_LEDS\\\\|USERLED\u0026#34; /home/\u0026lt;user\u0026gt;/openvela/nuttx/.config 查 app：\n1 grep -n \u0026#34;CONTEST2026_004\\\\|INIT_ENTRY\u0026#34; /home/\u0026lt;user\u0026gt;/openvela/nuttx/.config 13.3 会顺着调用链找代码 从 app 往底层找：\n1 2 3 4 5 hello_app_main.c -\u0026gt; board_userled() -\u0026gt; include/nuttx/board.h -\u0026gt; boards/arm/stm32h7/stm32h750b-dk/src/stm32_userleds.c -\u0026gt; stm32_gpiowrite() 这和你在 HAL 工程里从业务代码追到 HAL driver 是一个思路。\n13.4 会判断所有权 硬件资源最怕多个 owner。\nLED 这里的 owner 可能有：\n1 2 3 4 你的 app NuttX auto LED /dev/userleds bootloader 或调试器遗留状态 我们要的状态是：\n1 只有你的 app 通过 board_userled() 控制 LED。 13.5 会分清应用修复和 BSP 修复 应用问题：\n1 2 3 app 写错 LED 名字。 app 没启用 Kconfig。 app 没作为 init entry。 BSP 问题：\n1 2 3 4 GPIO 映射错。 LED 极性错。 board.h 注释和实际硬件不一致。 Kconfig 没 select ARCH_HAVE_LEDS。 比赛提交时，这两类修复的归属不一样。\n14. 比赛规范下该怎么提交 你的应用仓可以提交：\n1 2 3 4 5 6 app/hello_app/ scripts/flash_openocd.sh docs/openvela_stm32h750b_dk_first_led_babysitter_guide.md logs/ README.md .vscode/ 如果你想提交 IDE 配置 你的应用仓不应该正式提交：\n1 2 3 ../nuttx/boards/Kconfig ../nuttx/boards/arm/stm32h7/stm32h750b-dk/... ../nuttx/drivers/... 这些属于公共仓。\n如果要正式修 BSP，应该：\n1 2 3 4 5 1. fork 公共 nuttx 仓。 2. 切到 dev-ai-contest-2026 分支。 3. 提交 STM32H750B-DK LED BSP 修复。 4. 发 PR 给公共 nuttx 仓。 5. 在你的应用仓 README 里说明依赖这个 BSP PR。 这样才符合比赛规范。\n15. 建议你做的练习 15.1 改闪烁频率 把：\n1 usleep(500 * 1000); 改成：\n1 usleep(100 * 1000); 观察 LED 是否快速闪烁。\n你会学到：\n1 应用改动 -\u0026gt; 编译 -\u0026gt; 烧录 -\u0026gt; 现象验证 15.2 改闪第二颗 LED 把：\n1 #define FIRST_LED BOARD_LED_GREEN 改成：\n1 #define FIRST_LED BOARD_LED_RED 观察第二颗 user LED。\n你会学到：\n1 应用只改逻辑 LED，不改 GPIO。 15.3 打印 LED 数量 加一行：\n1 printf(\u0026#34;first_led: board reports %lu LEDs\\n\u0026#34;, (unsigned long)nleds); 你会学到：\n1 board_userled_initialize() 返回 BSP 暴露的 LED 数量。 15.4 用 board_userled_all() 尝试：\n1 2 3 4 board_userled_all(BOARD_LED1_BIT | BOARD_LED2_BIT); usleep(500 * 1000); board_userled_all(0); usleep(500 * 1000); 你会学到：\n1 单灯控制和批量控制的区别。 16. 以后遇到硬件现象不对时的排查模板 把下面这段当成 checklist：\n1 2 3 4 5 6 7 8 9 10 1. 先确认当前烧录的是最新 hex。 2. 确认 app 是否真的编进系统。 3. 确认 app 是否真的作为 init entry 运行。 4. 确认 Kconfig 没有别的 owner 抢硬件。 5. 确认应用用的是逻辑名字，不是硬编码 index。 6. 确认 BSP 的 board.h 逻辑名字和数组一致。 7. 确认 BSP 的 GPIO 映射和原理图/ST BSP 一致。 8. 确认 active-high / active-low 极性。 9. 确认初始化默认态不会误点亮。 10. 最后再怀疑调试器、烧录器、硬件连接。 这次我们就是按这条思路找到问题的。\n17. 这一路我们具体做了什么 按时间顺序总结：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 1. 确认比赛目录规则，明确应用代码应该放在 contest 仓。 2. 确认 manifest/linkfile 会把 app 软链到 packages/demos。 3. 把 team 000 模板改成 team 004。 4. 把应用名改成 first_led。 5. 配置 .config，让 first_led app 编入系统并作为启动入口。 6. 解决 arm-none-eabi-gcc 找不到的问题。 7. 解决 kconfig-mconf 找不到和 Kconfig 解析工具不对的问题。 8. 写第一版 board_userled() blink app。 9. 发现 warning，定位到 CONFIG_ARCH_HAVE_LEDS 没打开。 10. 解释为什么不应该在 app 里手动声明 BSP 函数。 11. 打开 CONFIG_ARCH_HAVE_LEDS。 12. 确认 CONFIG_ARCH_LEDS 应该关闭，避免内核抢 LED。 13. 确认 CONFIG_USERLED 应该关闭，避免 /dev/userleds 参与。 14. 写 build + flash 脚本，固定编译后烧录 nuttx.hex。 15. 用 OpenOCD 烧录并观察真实硬件现象。 16. 发现 LED 极性相反，定位到 active-low。 17. 修 board_userled()，让 true 表示亮，false 表示灭。 18. 发现 LD6/LD7/LD8 现象仍不符合预期。 19. 继续 review BSP LED 映射，发现 PI13/PJ2/PD3 混在一个 LED 表里。 20. 按 STM32H750B-DK BSP 语义整理为 PJ2 和 PI13 两个 user LED。 21. app 改为使用 BOARD_LED_GREEN，而不是硬编码 index。 22. 编译验证通过。 18. 你现在应该记住的核心原则 第一条：\n1 应用层不要直接写 STM32 寄存器，也不要直接写具体 GPIO。 第二条：\n1 应用层表达意图，BSP 层处理硬件细节。 第三条：\n1 2 3 CONFIG_ARCH_HAVE_LEDS 是能力声明。 CONFIG_ARCH_LEDS 是内核接管。 CONFIG_USERLED 是字符设备接管。 第四条：\n1 LED 极性必须在 BSP 修，不应该在 app 里到处取反。 第五条：\n1 比赛应用仓和公共 nuttx 仓要分开提交。 如果你把这五条吃透，后面从 LED 扩展到按键、串口、I2C、SPI、屏幕、文件系统，思路都是一样的。\n","permalink":"https://www.19y.cc/p/openvela-first-led/","summary":"\u003cp\u003e本文记录我们这一路从 openvela 竞赛模板走到 STM32H750B-DK 点亮第一颗 LED 的完整过程。\u003c/p\u003e\n\u003cp\u003e它不是只告诉你“改哪一行”，而是帮你建立一套迁移思维：你已经有 STM32HAL、FreeRTOS、LwIP、FatFs、Modbus 经验，那么学习 openvela/NuttX 时，最重要的是把已有经验映射到 NuttX 的概念上。\u003c/p\u003e","title":"从空白工程到点亮 STM32H750B-DK 第一颗 LED"},{"content":"本文解释本项目在 STM32H750B-DK 上使用外部 QSPI 作为主镜像、片内 Flash 作为启动 stub 的完整链路。重点不是“执行哪条命令”，而是说明：\n地址为什么是 0x08000000 和 0x90000000 两套空间； Kconfig 如何影响链接脚本，链接脚本如何影响 ELF、HEX 和 BIN； 为什么板上需要两个持久化镜像； STM32CubeProgrammer 的 External Loader 到底是什么； boot stub 如何把 QSPI 变成 CPU 可以直接取指的 XIP 地址； 烧录成功但不能启动时，应该从哪里开始定位。 本文对应我们在 openvela 项目中的实际实现，主要参考资料包括项目仓库里的 windows_build_openvela.ps1、windows_flash_cube.ps1、QSPI 补丁 openvela-qspi-boot-stm32h750b-dk.patch、boot stub 源码 stm32h750b_qspi_bootstub.c 与链接脚本 stm32h750b_qspi_bootstub.ld， 以及配套的 Windows 编译/烧录/调试指南。\n1. 先记住整体结论 板上有两个持久化镜像，PC 上还有一个只在烧录期间使用的临时算法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 PC / Windows └─ STM32_Programmer_CLI.exe └─ MT25TL01G_STM32H750B-DISCO.stldr └─ 只加载到 CubeProgrammer/目标 RAM 中执行 不属于用户固件，不负责上电启动 板上 ├─ 内部 Flash 0x08000000 │ └─ qspi_bootstub.hex │ └─ 复位后首先执行 │ └─ 外部 QSPI 的 memory-mapped 窗口 0x90000000 └─ nuttx.hex └─ VelaGuard/NuttX 主镜像，最终从 QSPI XIP 执行 复位后的实际链路是：\n1 2 3 4 5 6 7 8 9 10 11 12 复位 → Cortex-M 从 0x08000000 取 boot stub 向量表 → 执行 boot stub Reset_Handler → 初始化时钟、QSPI GPIO 和 QUADSPI 控制器 → 将 QSPI 配置为 memory-mapped read 模式 → 从 0x90000000 读取主镜像初始 MSP → 从 0x90000004 读取主镜像 Reset_Handler → VTOR = 0x90000000 → MSP/PSP = 主镜像初始 MSP → BX 跳转到主镜像 Reset_Handler → NuttX 启动代码清零 .bss、复制 .data、初始化系统 → VelaGuard 从 QSPI XIP 运行 这里的 XIP 是 Execute In Place：代码仍然存放在外部 Flash 中，CPU 通过 0x90000000 地址窗口直接读取指令，不需要先把整个 .text 复制到 SRAM。\n2. 地址空间：0x08000000、0x09000000 和 0x90000000 这是本项目最容易误读的地方。\n2.1 实际使用的地址范围 CPU 地址范围 大小 对应对象 是否存放板载持久化镜像 0x08000000 到 0x08020000 128 KiB STM32H750 片内 Flash 是，boot stub 0x90000000 到 0x98000000 128 MiB 地址窗口 外部双 QSPI memory-mapped 空间 是，主镜像 0x24000000 到 0x24080000 512 KiB AXI SRAM 否，运行时 .data、.bss、堆栈 0x08000000 是一个地址，不是“128 MB 的容量”。片内 Flash 的容量由 链接脚本中的 LENGTH = 128K 表示：\n1 2 0x08000000 + 0x20000 = 0x08020000 0x20000 = 131072 bytes = 128 KiB 当前 boot stub 只有约 720 bytes，因此它完全放得进这 128 KiB。\n2.2 0x08000000 到 0x09000000 不是一段连续 Flash 如果把两个地址相减：\n1 0x09000000 - 0x08000000 = 0x01000000 = 16 MiB 这只说明 CPU 地址编号之间隔着 16 MiB 的地址空间，不能说明板上存在 16 MiB 的片内 Flash。当前实现没有使用从 0x08000000 延伸到 0x09000000 的连续存储区。\n而且主镜像的地址是：\n1 0x90000000 不是：\n1 0x09000000 两者少了一个十六进制数字，含义完全不同。0x90000000 是 STM32H7 为外部 QSPI memory-mapped 访问保留的 CPU 地址窗口起点。\n因此，片内 Flash 和外部 QSPI 之间存在很大的地址空洞是正常的：\n1 2 3 4 5 6 7 8 9 CPU 地址空间 0x08000000 ┌─────────────────────────────┐ │ 片内 Flash，实际只有 128 KiB │ 0x08020000 └─────────────────────────────┘ │ 未使用的地址空间 │ 0x90000000 ┌─────────────────────────────┐ │ 外部 QSPI 映射窗口，128 MiB │ 0x98000000 └─────────────────────────────┘ 链接器只关心 CPU 运行时看到的地址。它不要求两个物理存储器在地址上 连续，也不要求 0x08020000 后面必须紧接着出现下一个 Flash 区域。\n2.3 为什么链接器声明 QSPI 为 128 MiB 当前 QSPI 链接脚本中有：\n1 flash (rx) : ORIGIN = 0x90000000, LENGTH = 128M 这表示“允许主镜像使用的 CPU 地址窗口上限”，不是说每次构建都会生成 128 MiB 文件，也不是说当前 HEX 会填满整段地址空间。\n当前示例主镜像的有效代码大约只有 160 KiB，实际只占用：\n1 0x90000000 ... 0x90027b38 其余 QSPI 地址没有出现在 HEX 数据记录中，不会被写入。烧录脚本只校验 镜像是否落在 0x90000000 到 0x98000000 的合法窗口内。\n3. 为什么必须有两个板载镜像 STM32 复位时，Cortex-M 会立即从当前可执行的启动地址读取向量表。 此时外部 QSPI 还没有被配置成 memory-mapped 模式，CPU 不能直接把 0x90000000 当作普通指令地址使用。\n所以不能把完整 NuttX 镜像只放到 QSPI 后直接复位。必须先有一小段能在 片内 Flash 执行的代码完成硬件准备：\n镜像 执行位置 主要职责 qspi_bootstub 片内 Flash 0x08000000 初始化时钟、QSPI 引脚、QUADSPI 控制器，检查并跳转主镜像 nuttx 外部 QSPI 映射到 0x90000000 NuttX、VelaGuard、驱动、应用和只读数据 boot stub 不是完整的 BootROM，也不是第二套操作系统。它只做“让主镜像 可执行并把控制权交出去”这件事。\n4. 构建链路：从 PowerShell 到两个 ELF 执行：\n1 .\\scripts\\windows_build_openvela.ps1 -VelaGuardMode test -DebugBuild -Rebuild full 脚本本身运行在 Windows PowerShell，但真正的 openvela 编译在 WSL 中完成。 大致链路如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 windows_build_openvela.ps1 → 将临时 shell 脚本写入输出目录 → wsl.exe 启动 Debian → 设置 ARM GCC、Kconfig、Python 和构建工具 PATH → ensure-openvela-links.sh → 重新生成 packages/demos/Kconfig → 幂等应用 QSPI 补丁 → configure.sh -e stm32h750b-dk:lvgl → kconfig-tweak 修改 .config → make olddefconfig → 必要时 make clean → make -j → 单独编译 qspi_bootstub → 复制产物到 .debug 4.1 QSPI 补丁做了什么 apply-openvela-qspi-patch.sh 会把 openvela-qspi-boot-stm32h750b-dk.patch 应用到当前 NuttX checkout。 它会先检查反向补丁和关键标记，因此重复构建不会反复应用同一补丁。\n补丁包含几类改动：\n在板级 Kconfig 增加 CONFIG_STM32H750B_DK_QSPI_BOOT； 在 Make.defs 和 CMakeLists 中根据该配置选择 qspi_flash.ld； 在 STM32H7 MPU 初始化中把 0x90000000 到 0x98000000 标记为可执行的 Flash 区域； 增加 qspi_flash.ld； 保留项目当前需要的 FT5X06 触摸能力补丁。 如果补丁没有应用，可能出现两种典型问题：\nKconfig 中没有 QSPI 选项； 主镜像仍使用普通 flash.ld，链接地址落在 0x0800xxxx。 4.2 Kconfig 如何切换链接脚本 PowerShell 生成的 WSL 构建脚本会强制启用：\n1 CONFIG_STM32H750B_DK_QSPI_BOOT=y 随后板级构建文件做选择：\n1 2 3 4 5 CONFIG_STM32H750B_DK_QSPI_BOOT = y → LDSCRIPT = qspi_flash.ld CONFIG_STM32H750B_DK_QSPI_BOOT != y → LDSCRIPT = flash.ld 这一步是整个机制的分水岭。它不是在烧录阶段把一个普通 BIN “搬到 QSPI”，而是在链接阶段就让所有代码地址、函数指针、向量表和 只读数据地址变成 0x9000xxxx。\n4.3 qspi_flash.ld 的关键含义 简化后的核心内容是：\n1 2 3 4 5 6 7 8 9 MEMORY { flash (rx) : ORIGIN = 0x90000000, LENGTH = 128M sram (rwx): ORIGIN = 0x24000000, LENGTH = 512K } .text : { ... } \u0026gt; flash .data : { ... } \u0026gt; sram AT \u0026gt; flash .bss : { ... } \u0026gt; sram 这里的 flash 只是链接脚本里的区域名字。在 boot stub 的链接脚本里， 同名的 flash 指的是 0x08000000 的片内 Flash；在主镜像链接脚本里， 同名的 flash 指的是 0x90000000 的 QSPI 映射窗口。区域名字相同， 地址和物理对象并不相同。\nVMA 与 LMA AT \u0026gt; flash 是理解 QSPI 主镜像的关键：\nVMA（Virtual Memory Address）：程序运行时访问变量的地址； LMA（Load Memory Address）：初始数据在镜像中保存的位置。 主镜像的典型布局如下：\n段 运行地址 VMA 镜像保存地址 LMA 运行方式 .text、.rodata、向量表 0x9000xxxx 0x9000xxxx 直接从 QSPI XIP .data 0x2400xxxx .text/.rodata 之后的 QSPI 地址 启动时复制到 SRAM .bss 0x2400xxxx 无初始化数据 启动时清零 链接脚本定义的 _eronly 是只读区域末尾，也是 .data 初始值在 镜像中的起点。STM32H7 启动代码会把：\n1 2 [_eronly, _eronly + sizeof(.data)) → [_sdata, _edata) 复制到 SRAM，然后把 [_sbss, _ebss) 清零。这个复制动作发生在 boot stub 已经把 QSPI 设为 memory-mapped 之后，所以 _eronly 可以直接指向 0x9000xxxx。\n当前 .debug/nuttx.elf 的实际符号可以用来验证这个关系：\n1 2 3 4 5 6 _stext = 0x90000000 _eronly = 0x90027b38 _sdata = 0x24000000 _edata = 0x24000794 _sbss = 0x240007a0 _ebss = 0x2400bd34 因此，.data 的“初始值”在 QSPI，但程序运行时访问的变量地址仍然是 0x24000000 附近的 SRAM 地址。\n4.4 MPU 为什么也必须修改 NuttX 启动阶段会启用 Cortex-M7 MPU。如果 MPU 没有把 QSPI 映射窗口 设置为可执行区域，即使 boot stub 已经成功跳到 0x9000xxxx，主程序 也可能在取指时产生 MemManage/BusFault。\n补丁中的逻辑等价于：\n1 mpu_priv_flash(0x90000000, 128 * 1024 * 1024) 因此，QSPI 机制需要同时满足：\n链接地址位于 0x9000xxxx； boot stub 把 QSPI 配成 memory-mapped； VTOR 指向外部向量表； MPU 允许该窗口执行； .data 的初始值能够从 QSPI 读取。 5. ELF、HEX、BIN：同一份程序的三种表现 5.1 ELF 是分析和调试的主文件 nuttx.elf 保存：\nsection 和 segment； 绝对地址； 符号表； 调试信息； ELF entry point。 GDB 通过 ELF 找到源文件、函数和变量。ELF 不一定直接拿来给 CubeProgrammer 烧录，但它是判断链接是否正确的最佳文件。\n5.2 HEX 保存了地址 arm-none-eabi-objcopy -O ihex 会把 ELF 的可加载内容转换为 Intel HEX。 HEX 通过扩展线性地址记录携带高位地址。\n当前主镜像开头是：\n1 :0200000490006A 记录类型 04 表示 Extended Linear Address，数据 9000 表示后续数据 的高 16 位为 0x9000，因此后面的 offset 0x0000 实际落在：\n1 0x9000 \u0026lt;\u0026lt; 16 | 0x0000 = 0x90000000 boot stub 的 HEX 开头是：\n1 :020000040800F2 所以它的后续数据落在 0x08000000。\n这就是为什么当前烧录脚本直接使用 HEX：文件自身已经携带目标地址， CubeProgrammer 不需要猜测它应该写到片内 Flash 还是外部 QSPI。\n5.3 BIN 是无地址裸数据 arm-none-eabi-objcopy -O binary 会删除 ELF/HEX 的地址记录，只留下 连续字节。nuttx.bin 和 qspi_bootstub.bin 适合需要“文件加起始地址” 的工具，但当前 windows_flash_cube.ps1 不使用它们。\n不要把 nuttx.bin 当成可以直接替代 nuttx.hex 的文件。若使用 BIN， 烧录工具必须另外知道目标地址和对应的 External Loader 参数。\n6. 从当前 ELF 检查主镜像布局 当前仓库已有 .debug/nuttx.elf 时，可以执行：\n1 2 3 readelf -h .debug/nuttx.elf readelf -S .debug/nuttx.elf readelf -s .debug/nuttx.elf 在 WSL 中若 ARM 工具链没有进入 PATH，可以使用 openvela 自带工具链下的 arm-none-eabi-readelf。\n当前示例结果：\nsection 地址 大小 含义 .text 0x90000000 0x27b30 向量表、代码、只读数据 .ARM.exidx 0x90027b30 0x8 ARM 异常索引信息 .data 0x24000000 0x794 SRAM 运行区，初始值来自 QSPI .bss 0x240007a0 0xb594 SRAM 清零区 主镜像向量表前 8 个字节当前为：\n1 34 c1 00 24 19 0b 00 90 按小端序解释：\n1 2 *(uint32_t *)0x90000000 = 0x2400c134 初始 MSP *(uint32_t *)0x90000004 = 0x90000b19 Reset_Handler | Thumb bit 0x90000b19 的最低位为 1，表示 Cortex-M Thumb 状态。最低位不是代码 实际对齐地址的一部分，而是异常/分支入口的状态标记。\n当前 ELF 的 entry point 是 0x90000299。调试时不要只看 ELF header 的 entry point；Cortex-M 复位跳转的权威来源是向量表第二个 word，即 0x90000004。两者都应位于 QSPI 代码区，且向量表中的入口必须带 Thumb bit。\n7. boot stub 是如何初始化 QSPI 并跳转的 7.1 boot stub 的构建 build_bootstub.sh 使用独立的裸机编译命令：\n1 2 3 4 5 6 7 8 9 10 11 12 arm-none-eabi-gcc -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -Os -ffreestanding -nostartfiles -nostdlib -Wl,--gc-sections -T stm32h750b_qspi_bootstub.ld stm32h750b_qspi_bootstub.c 它不链接 NuttX，也不依赖 C 运行库。这样做的目的就是让它足够小、 足够早、足够独立。\nboot stub 链接脚本声明：\n1 2 flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K ram (rwx): ORIGIN = 0x24000000, LENGTH = 512K 当前 boot stub 的关键布局：\n1 2 3 g_vectors = 0x08000000 Reset_Handler = 0x08000090 附近 _estack = 0x24080000 由于 Cortex-M 函数入口需要 Thumb 状态，向量表中保存的 Reset_Handler 地址会是奇地址，例如当前产物中的 0x08000091。\n7.2 向量表 boot stub 的向量表位于片内 Flash 起点：\n1 2 *(uint32_t *)0x08000000 = 0x24080000 boot stub 初始 MSP *(uint32_t *)0x08000004 = 0x08000091 boot stub Reset_Handler | Thumb bit 向量表由 g_vectors 定义，并按 512 字节对齐。对当前启动链来说，最重要 的是前两个 word；其余异常入口暂时都指向 Default_Handler。\n7.3 时钟和 GPIO clock_init() 做最小的系统时钟准备，包括：\n设置 Flash 访问等待周期； 配置 RCC 时钟分频； 配置 PLL1； 等待 PLL1 ready； 切换系统时钟源。 qspi_gpio_init() 打开 GPIO 和 QUADSPI 外设时钟，并为 D、F、G、H 端口设置：\nAlternate Function； 输出速度； 复用选择； QSPI 相关片选、时钟、数据线功能。 这些寄存器是直接按芯片地址访问的，不经过 HAL，原因是 boot stub 要 尽量小，并且必须在 NuttX 尚未启动时工作。\n7.4 QUADSPI memory-mapped 配置 qspi_enter_memory_mapped() 的高层顺序是：\n1 2 3 4 5 6 7 8 1. 初始化 QSPI GPIO 2. 配置 QSPI_CR / QSPI_DCR / QSPI_LPTR 3. 配置一次命令模式并使能 QSPI 4. ABORT，等待 BUSY 清零 5. 发送退出 QPI 的命令，避免上一次镜像留下异常协议状态 6. 再次 ABORT 7. 将 CCR 设置为 memory-mapped READ 8. 从此 CPU 访问 0x90000000 就会触发 QSPI 读事务 当前实现使用的是双 QSPI 芯片的 SPI 访问路径，而不是 QPI 路径。源码 注释记录了本板验证结果：\nCCR = 0x0d003513 的 SPI memory-mapped 路径能够在 0x90000000 读到有效向量表； QPI 试验值 0x0f283fec 读出的结果无效，不能启动； 该序列与 OpenOCD 板级配置中的 qspi_init 0 路径保持一致。 这里的关键不是记住一个“魔法常量”，而是理解 CCR 配置必须与板上 MT25TL01G 的连接方式、SPI/QPI 协议、地址宽度、dummy cycles 和 memory-mapped 模式相匹配。更换板型、Flash 器件或接线后，不能直接照搬 这些数值。\n7.5 检查主镜像并跳转 boot stub 从 QSPI 取出两个 word：\n1 2 app_stack = *(uint32_t *)0x90000000; app_entry = *(uint32_t *)0x90000004; 然后检查：\napp_stack 位于合法 SRAM 地址范围； app_stack 至少 4 字节对齐； app_entry 位于 0x90000000 到 0x98000000； app_entry 的最低位为 1，表示 Thumb。 如果检查失败，boot stub 进入 Default_Handler()，只执行 WFI，不会 跳到一个随机地址。\n检查成功后，代码做四件事：\n1 2 3 4 5 6 SCB-\u0026gt;VTOR = 0x90000000 DSB / ISB MSP = app_stack PSP = app_stack CONTROL = 0 BX app_entry 设置 VTOR 很重要。即使 PC 已经跳到 QSPI，如果 VTOR 仍然指向 0x08000000，主程序产生异常时仍可能使用 boot stub 的异常向量表。\n8. 烧录脚本到底做了什么 执行：\n1 .\\scripts\\windows_flash_cube.ps1 -DebugBuild -Rebuild full 不想重新构建、只烧录已有 .debug 产物时：\n1 .\\scripts\\windows_flash_cube.ps1 -NoBuild 8.1 烧录前的地址验证 windows_flash_cube.ps1 中的 Get-IntelHexAddressRange 会解析每个 HEX 数据记录，并处理：\n类型 00：实际数据； 类型 02：Extended Segment Address； 类型 04：Extended Linear Address。 它维护一个当前高位基地址，把每个数据记录的 offset 加上基地址，计算 整个 HEX 的最小地址和最大结束地址。\n随后执行两个范围检查：\n1 2 3 4 5 主 QSPI 镜像： 0x90000000 \u0026lt;= address \u0026lt; 0x98000000 片内 boot stub： 0x08000000 \u0026lt;= address \u0026lt; 0x08020000 如果主镜像仍然落在 0x0800xxxx，脚本会在接触硬件前拒绝烧录。这是 一个很有价值的安全闸门：它避免把普通内部 Flash 镜像误当成 QSPI 镜像。\n8.2 External Loader 的真实角色 第一条 CubeProgrammer 命令等价于：\n1 2 3 4 5 STM32_Programmer_CLI.exe -c port=SWD mode=UR -el MT25TL01G_STM32H750B-DISCO.stldr -d .debug/nuttx.hex -v -el 指定的是 STM32CubeProgrammer 的外部存储器编程算法。它通常会被 下载/运行在目标 RAM 或由 Programmer 控制执行，用来完成：\n识别外部 Flash； 解锁； 擦除； 写入； 读取回校验。 它不会在复位后留在板上，也不会替代 boot stub。即使外部 Loader 文件 叫 .stldr，它也不是第三份需要长期保存的固件。\n8.3 两阶段写入和复位 脚本实际顺序是：\n1 2 3 1/3 External Loader 写入并校验 nuttx.hex 到外部 QSPI 2/3 普通 SWD 写入并校验 qspi_bootstub.hex 到片内 Flash 3/3 复位目标 先写主镜像、后写 boot stub 是有意的。这样直到 QSPI 主镜像准备好之前， 不会让新 boot stub 在复位后立即跳到一个尚未写完的外部地址。\n连接参数是 port=SWD，因此当前流程使用的是 SWD，不要求 JTAG。 ST-LINK 负责通过 SWD 访问片内 Flash 和控制外部 Loader；串口 CTS、 RS485、ESP-01S 等业务引脚不参与 QSPI 烧录链路。\n8.4 为什么当前流程不使用 OpenOCD 下载 QSPI 本项目已经验证：当前板上双 MT25TL01G 的 OpenOCD stmqspi 路径无法 稳定完成 JEDEC probe/write。CubeProgrammer 的 MT25TL01G_STM32H750B-DISCO.stldr 可以正常擦写和校验，因此职责被 明确分开：\n1 2 3 CubeProgrammer + External Loader → QSPI 下载 CubeProgrammer 普通 SWD → 片内 boot stub 下载 OpenOCD → 仅提供 GDB server 调试 9. 推荐的验证步骤 9.1 构建 1 .\\scripts\\windows_build_openvela.ps1 -VelaGuardMode test -DebugBuild -Rebuild full 确认 .debug 至少包含：\n1 2 3 4 5 6 7 8 nuttx.elf nuttx.hex nuttx.bin qspi_bootstub.elf qspi_bootstub.hex qspi_bootstub.bin nuttx.config build-info.txt 9.2 不接板只检查地址 1 .\\scripts\\windows_flash_cube.ps1 -NoBuild -ValidateOnly 期望日志类似：\n1 2 3 Main QSPI image : 0x90000000..0x900xxxxx Internal stub : 0x08000000..0x08000xxx Validation completed; hardware was not accessed. 9.3 烧录 1 .\\scripts\\windows_flash_cube.ps1 -NoBuild 烧录结束后脚本会自动复位。若只使用现有产物，必须确认 .debug 中的 ELF 与 HEX 来自同一次构建，避免 GDB 符号与板上代码不一致。\n9.4 GDB/调试 OpenOCD attach 时应加载与板上镜像匹配的 nuttx.elf 符号，但不要让 GDB 再次执行 load。当前工程的调试配置使用空的 loadFiles，原因是：\n1 2 CubeProgrammer 负责真实烧录 GDB 只负责符号、断点、单步和寄存器观察 进入调试后优先观察：\n1 2 3 4 5 PC MSP SCB-\u0026gt;VTOR 0x90000000 0x90000004 如果能访问目标内存，可以检查：\n1 2 3 x/8wx 0x90000000 x/8wx 0x08000000 info registers pc msp 期望看到：\n1 2 3 4 0x90000000: 0x2400xxxx 0x90000004: 0x9000xxxx 且最低位为 1 PC: 0x9000xxxx VTOR: 0x90000000 10. 故障定位树 现象 先观察 常见原因 处理 构建成功但主 HEX 地址是 0x0800xxxx readelf -S nuttx.elf QSPI Kconfig 没开、补丁未应用、仍用了 flash.ld 重新执行完整构建，确认 CONFIG_STM32H750B_DK_QSPI_BOOT=y Main QSPI image address range is invalid windows_flash_cube.ps1 -NoBuild -ValidateOnly 主镜像没有链接到 0x90000000 不要强行烧录，先修复链接配置 QSPI 写入/校验失败 CubeProgrammer 输出、External Loader 路径 Loader 不存在、型号不匹配、外部 Flash 连接或供电异常 检查 .stldr、板型和供电，关闭占用 ST-LINK 的程序 复位后 PC 仍在 0x0800xxxx PC、片内 Flash 前 8 字节 boot stub 未写入、没有复位、烧录的是旧 stub 重新烧录 stub 并复位 PC 停在 boot stub，主镜像不运行 0x90000000 和 0x90000004 QSPI 未进入映射模式、主 HEX 未正确写入、向量无效 先检查 External Loader 和 HEX 前两项 boot stub 进入 WFI 主向量两个 word MSP 不在 SRAM、入口不在 QSPI、Thumb bit 丢失 检查主镜像向量表和地址范围 PC 已到 0x9000xxxx 后立即 Fault VTOR、MPU、Fault 状态寄存器 MPU 不允许执行、QSPI 读配置错误、VTOR 未切换 确认 QSPI 补丁、VTOR 和 CCR 配置 主程序启动但全局变量异常 _eronly、_sdata、_edata .data 的 LMA/VMA 或启动复制错误 检查 qspi_flash.ld 与 STM32H7 启动代码 能运行但断点位置不对 ELF 与 build-info.txt 板上固件和 GDB 使用了不同构建产物 重新构建、Cube 烧录并加载同一份 ELF 10.1 最小判断顺序 遇到“烧录成功但不启动”，按下面顺序排查，效率最高：\n1 2 3 4 5 6 7 8 1. 主 HEX 是否真的包含 0x9000 的扩展线性地址记录 2. boot stub HEX 是否真的包含 0x0800 的扩展线性地址记录 3. QSPI External Loader 是否完成 verify 4. 片内 Flash 前两个 word 是否是 0x2400xxxx / 0x0800xxxx 5. 外部 QSPI 前两个 word 是否是合法 MSP / 0x9000xxxx|1 6. 复位后 PC 是否离开 0x0800xxxx 7. VTOR 是否为 0x90000000 8. PC 到 0x9000xxxx 后是否发生 MPU/BusFault 11. 当前产物的可复核样例 当前仓库 .debug 中的一组产物可以作为地址 sanity check：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 主镜像： ELF entry point 0x90000299 .text 0x90000000, size 0x27b30 .data VMA 0x24000000, size 0x794 .bss 0x240007a0, size 0xb594 初始 MSP 0x2400c134 Reset_Handler vector 0x90000b19 boot stub： ELF entry point 0x08000091 向量表 0x08000000 初始 MSP 0x24080000 Reset_Handler vector 0x08000091 qspi_bootstub.bin 720 bytes 这些数值会随代码和配置改变；稳定不变的是布局不变量：\n1 2 3 4 主镜像向量表和代码 → 0x90000000 地址空间 boot stub 向量表和代码 → 0x08000000 地址空间 主镜像运行时 .data/.bss → SRAM boot stub 先初始化 QSPI → 主镜像才能执行 12. 一句话理解整个机制 链接器先把 NuttX 主程序“写成运行在 0x90000000 的程序”， CubeProgrammer 再按 HEX 地址把它写入外部 QSPI；复位时，片内 0x08000000 的 boot stub 把外部 Flash 映射到这个地址、切换 VTOR、 装载主镜像栈指针，然后跳到 0x9000xxxx，从而实现 QSPI XIP。\n","permalink":"https://www.19y.cc/p/stm32h750-qspi-xip-deep-dive/","summary":"\u003cp\u003e本文解释本项目在 STM32H750B-DK 上使用外部 QSPI 作为主镜像、片内\nFlash 作为启动 stub 的完整链路。重点不是“执行哪条命令”，而是说明：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e地址为什么是 0x08000000 和 0x90000000 两套空间；\u003c/li\u003e\n\u003cli\u003eKconfig 如何影响链接脚本，链接脚本如何影响 ELF、HEX 和 BIN；\u003c/li\u003e\n\u003cli\u003e为什么板上需要两个持久化镜像；\u003c/li\u003e\n\u003cli\u003eSTM32CubeProgrammer 的 External Loader 到底是什么；\u003c/li\u003e\n\u003cli\u003eboot stub 如何把 QSPI 变成 CPU 可以直接取指的 XIP 地址；\u003c/li\u003e\n\u003cli\u003e烧录成功但不能启动时，应该从哪里开始定位。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e本文对应我们在 openvela 项目中的实际实现，主要参考资料包括项目仓库里的\n\u003ccode\u003ewindows_build_openvela.ps1\u003c/code\u003e、\u003ccode\u003ewindows_flash_cube.ps1\u003c/code\u003e、QSPI 补丁\n\u003ccode\u003eopenvela-qspi-boot-stm32h750b-dk.patch\u003c/code\u003e、boot stub 源码\n\u003ccode\u003estm32h750b_qspi_bootstub.c\u003c/code\u003e 与链接脚本 \u003ccode\u003estm32h750b_qspi_bootstub.ld\u003c/code\u003e，\n以及配套的 \u003ca href=\"/p/openvela-windows-build-debug/\"\u003eWindows 编译/烧录/调试指南\u003c/a\u003e。\u003c/p\u003e","title":"STM32H750B-DK 的 QSPI-XIP 构建、烧录与启动机制"},{"content":"VelaGuard 是我们在 openvela（NuttX）上为 STM32H750B-DK 开发的一款工业边缘网关：它通过 Modbus 采集现场数据，在本地完成规则判断与告警，再经由 MQTT 把关键信息上送云端，并借助 AI 助手完成诊断与配置建议。\n在项目演进的过程中，我们陆续记录下五项架构决策（ADR，Architecture Decision Record）。它们看起来是零散的技术选型，背后却贯穿着一条一致的思路：\n现场可靠性优先，云端只是增强，绝不成为依赖。\n本文把这五条决策整理成文，并说明每一条背后的权衡。\n一、独立网关，本地安全回路优先（ADR 0001） 决策：VelaGuard 是一台独立的 STM32H750B-DK 网关，而不是挂在 PC 上的“边车”式演示。\n背景：不少物联网演示项目把采集、判断与展示都放在 PC 或云端，开发板只是传感器前端。一旦网络或云端服务不可用，整套系统便随之瘫痪。\n理由：VelaGuard 的工业价值恰恰在于“现场依然有用”。因此，Modbus 采集、规则评估、界面告警、本地日志、本地告警音——这一整条本地安全回路——必须在没有网络、没有 MQTT、没有 AI Bridge、没有 MiMo/TTS/ASR、也没有连接电脑的情况下照常运转。云端能力失效时，网关仍然守在现场。\n二、经 MQTT Broker 与 AI Bridge 接入云端，板端不直连 MiMo（ADR 0002） 决策：板端只与 MQTT Broker 通信；MiMo、TTS、ASR 与手册解析等 AI 能力，统一由独立的 AI Bridge 服务通过 HTTPS 调用。\n背景：如果让板子直接调用大模型与语音服务的 HTTPS API，重试、鉴权、凭据管理以及高流量的 AI 工作流，都会被压在一块嵌入式板卡上。\n理由：多引入一个轻量云组件，换来的是把重量级的 HTTPS/API 处理、重试与凭据逻辑、大型 AI 工作流全部移出 H750B-DK。网关保持“独立网关”的形态，云侧的复杂度被关在云侧。\n三、生产固件的设备 ID 稳定且不可在运行时修改（ADR 0003） 决策：生产固件从 STM32 UID 派生 device_id，并且不提供任何运行时修改接口；测试固件可以通过代码或编译期配置覆盖。\n理由：设备身份是 MQTT Topic、ACL、鉴权、日志与云端路由的共同基础。如果它能在 UI、串口、HTTP 或 MQTT 上被意外改动，整条云链路的可靠性与安全性都会动摇。身份稳定，是其余一切云侧机制成立的前提。\n四、AI 生成的配置必须经过设备端确认（ADR 0004） 决策：AI 可以生成候选配置，但 VelaGuard 只有在设备端完成 schema 校验、风险评估、试读验证与本地确认之后，才会真正激活它。\n背景：直接采纳 AI 输出是最省事的路径——但一个错误的 Modbus 寄存器、阈值或规则，即使 AI 的输出看起来头头是道，也可能在工业现场制造出误导性的告警。\n理由：在“AI 看起来很合理”与“现场数据真实可靠”之间，我们把最后一道闸门放在设备端：AI 提供建议，本地负责确认。\n五、OTA 只走 MQTT 拉取，不加板端 HTTPS 下载器（ADR 0005） 决策：固件升级的控制与分片传输统一走 MQTT/TLS；不在板端实现 HTTPS 下载器。\n背景：大文件固件交付在工业产品中通常选择 HTTPS，这本身是常见且合理的选择。\n理由：本项目更看重一条单一的鉴权通路：经过认证的 MQTT 链路、更简单的嵌入式网络栈、直接复用 Broker 的 ACL，并能与本地确认、进度上报、失败回滚紧密集成。少一条协议通路，就少一类攻击面，也少一类调试成本。\n结语 回头看，这五条决策可以压缩成一句话：把可靠性留在现场，把复杂性留在云侧，把最终决定权留在设备端。\n本地安全回路（ADR 0001）保证离线可用； Broker + AI Bridge（ADR 0002）保证板端轻盈； 稳定身份（ADR 0003）保证云侧链路可信； 设备端确认（ADR 0004）保证 AI 不越权； MQTT 统一 OTA（ADR 0005）保证升级路径收敛。 对一个要在现场长期运行的工业网关来说，这些选择的价值，往往要等到云端真正出故障的那一天才会显现。\n本文整理自团队仓库 docs/adr/ 中的 0001–0005 决策记录。\n","permalink":"https://www.19y.cc/p/velaguard-architecture-decisions/","summary":"\u003cp\u003eVelaGuard 是我们在 openvela（NuttX）上为 STM32H750B-DK 开发的一款工业边缘网关：它通过 Modbus 采集现场数据，在本地完成规则判断与告警，再经由 MQTT 把关键信息上送云端，并借助 AI 助手完成诊断与配置建议。\u003c/p\u003e","title":"VelaGuard 架构决策记录：边缘网关的五条可靠性设计原则"},{"content":"STM32CUBEMX在配置定时器PWM输出的刹车输入引脚时\n要注意配置的刹车极性和对应引脚的上下拉模式是否相反\n若不相反，则会导致复位启动时无法产生对应的波形。\n经排查，是CUBEMX没有对刹车输入引脚进行正确的初始化，需要手动更改\n","permalink":"https://www.19y.cc/p/cubemx%E5%85%B3%E4%BA%8E%E5%88%B9%E8%BD%A6%E8%BE%93%E5%85%A5%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8Fbug/","summary":"\u003cp\u003eSTM32CUBEMX在配置定时器PWM输出的刹车输入引脚时\u003c/p\u003e\n\u003cp\u003e要注意配置的刹车极性和对应引脚的上下拉模式是否相反\u003c/p\u003e\n\u003cp\u003e若不相反，则会导致复位启动时无法产生对应的波形。\u003c/p\u003e","title":"CubeMX关于刹车输入的一个小BUG"},{"content":" [!NOTE]\n关键字 inline 的作用 inline 是 C/C++ 中的关键字，用于建议编译器将函数内联展开（即在调用处直接插入函数代码，而非生成函数调用指令）。它的主要影响包括：\n选项 描述 是否正确？ 原因 A：降低栈内存的消耗 ✅ 正确 ✔️ 内联函数避免了函数调用的栈帧开销（如参数压栈、返回地址保存等），从而减少栈内存使用。 B：可以提高代码的运行效率 ✅ 正确 ✔️ 内联消除了函数调用的开销（跳转、返回等），可能提升运行效率（但过度内联可能导致代码膨胀，反而降低缓存命中率）。 C：可以提高微控制器访问内部寄存器的速度 ❌ 错误 ✖️ inline 与寄存器访问速度无关，这是由硬件和编译器优化决定的，而非内联函数的功能。 D：程序中大量使用，会增大代码编译后的可执行文件的大小 ✅ 正确 ✔️ 内联会导致函数代码被多次复制到调用处，若滥用会显著增加二进制文件大小。 [!NOTE] 答案:D，最高位为1，为负数，负数的补码等于反码取反+1，负数取反符号位不变，得11010100，+1得11010101\n[!NOTE] D：AHB2 在 STM32G4 系列中，GPIO 外设是挂载在 AHB2 总线 上的，因此需要在 RCC 的 AHB2 外设时钟寄存器 (RCC_AHB2ENR) 中使能对应的 GPIO 端口时钟。\n总结：选项解析 选项 内容 正确性 原因 ​A 具有电流放大作用 ✅ 三极管的核心功能（基极电流控制集电极电流）。 ​B 内部有2个PN结 ✅ 发射结+集电结，缺一不可。 ​C 具有单向导电性 ❌ 这是二极管的特性，三极管工作模式复杂，无单向性。 ​D 有集电区、基区、发射区 ✅ 三极管的基本结构组成。 [!NOTE] 三极管由三个半导体区（发射极、基极、集电极）组成，内部包含两个PN结（BE结和BC结）。\n[!NOTE]\n各选项电路的常见应用场景 A. 同相比例电路 • 功能：对输入信号进行线性放大（增益由电阻决定）。\n• 典型应用：\n• 信号放大（如传感器信号调理）。\n• 阻抗匹配（高输入阻抗，低输出阻抗）。\n• 不适用场景：波形转换（仅放大幅度，不改变形状）。\nB. 同相求和电路 • 功能：将多个输入信号加权相加。\n• 典型应用：\n• 音频混频（混合多路信号）。\n• 传感器信号融合（如温度+压力信号叠加）。\n• 不适用场景：波形转换（仅叠加信号，不改变单个波形特性）。\nC. 微分电路 • 功能：输出信号与输入信号的变化率（导数）成正比。\n• 典型应用：\n• 三角波→方波转换（如本题）。\n• 边缘检测（脉冲信号生成）。\n• 控制系统中的误差微分补偿（PID控制器中的D项）。\n• 注意事项：对高频噪声敏感，需配合低通滤波。\nD. 积分电路 • 功能：输出信号与输入信号的积分（时间累积）成正比。\n• 典型应用：\n• 方波→三角波转换（与微分电路相反）。\n• 模拟计算（如求解微分方程）。\n• 电源中的PWM滤波（转换为直流电平）。\n• 注意事项：需防止运放饱和（添加复位电路）。\n总结：如何选择电路？ 需求 适用电路 示例 放大信号幅度 同相比例（A） 放大麦克风信号 混合多路信号 同相求和（B） 音频混音器 三角波→方波转换 微分（C） 信号发生器、触发电路 方波→三角波转换 积分（D） PWM转模拟电压 [!NOTE]\n关键区别：\n• 微分（C）和积分（D） 是波形转换的核心电路，但作用相反。\n• 比例（A）和求和（B） 仅处理信号幅度或叠加，不改变波形本质。\n建议结合具体需求（如是否需要波形变换、信号放大或混合）选择电路类型。\n有源元件 vs. 无源元件 ​特性 ​有源元件 ​无源元件 ​能量需求 必须外接电源供电 无需外部电源 ​功能 放大、开关、振荡、信号控制 消耗、存储或滤波能量（无增益） ​典型例子 晶体管、运放、逻辑芯片 电阻、电容、电感 [!NOTE] 此类题目去手册memory map找\n[!NOTE]\n1. 同步串行（Synchronous Serial）​ ​定义：数据按单一位流依次传输，发送方和接收方通过共享时钟信号同步时序。 ​核心特点： ​单数据线：逐位传输（如I²C、SPI、UART*）。 ​时钟同步：发送和接收端共用时钟（CLK）信号，确保时序一致。 ​低引脚数：节省硬件资源（适合远距离或引脚受限场景）。 ​典型应用： 传感器通信（如温度传感器通过I²C传输数据）。 存储器读写（如SPI接口的Flash芯片）。 *注：UART本质是异步串行，但可通过外部时钟同步化。\n​2. 同步并行（Synchronous Parallel）​ ​定义：数据通过多根数据线并行传输，所有位在同一时钟周期内同步发送。 ​核心特点： ​多数据线：每位占用一条物理线（如8位数据需8根线）。 ​时钟同步：时钟信号控制所有线路的同步采样。 ​高速传输：单时钟周期完成多比特传输（理论速度更快）。 ​典型应用： 高速内存接口（如DDR SDRAM）。 早期CPU与外围芯片通信（如ISA总线）。 [!NOTE] 专用引脚加起来有大约12个（比如4个VDD，4个VSS，1个NRST，2个OSC，2个SWD，可能还有其他），那么剩下的IO数目大概是64-12=52个\n[!NOTE] ==指数部分1.2是小数，违反指数必须为整数的规则。==\n==- 错误：2e1.2（指数非整数）、1.2e3.4（指数非整数）。== ==正确：2e+3（正指数符号可省略）、1e-5（负整数指数）。== [!NOTE]\n​RS触发器的约束口诀：​​“RS=11是禁忌，输出混乱要规避”​。 ​JK触发器的优势：​​“JK无约束，11可翻转”​。 [!NOTE] Title 注意得用变化的集电极电流除以基极电流 β定义为三极管放大倍数\n[!NOTE] 这道题目考察的是数字电路中组合逻辑电路和时序逻辑电路的分类理解。我们可以通过以下逻辑进行分析：\n​核心概念区分： ​组合逻辑电路：输出仅由当前输入决定（无记忆功能），如编码器、译码器、数据选择器、加法器。 ​时序逻辑电路：输出不仅取决于当前输入，还与电路原来的状态有关（有存储功能），如计数器、寄存器、移位寄存器。 ​选项解析：\n​A. 编码器​（组合逻辑）：将特定输入信号转换为二进制编码，无状态存储。 ​B. 计数器​（时序逻辑）：通过触发器记录脉冲个数，具有状态存储和更新功能。 ​C. 译码器​（组合逻辑）：将二进制编码转换为特定输出信号，无状态依赖。 ​D. 数据选择器​（组合逻辑）：根据选择信号输出对应输入通道的数据，无记忆。 ​关键判断依据：\n时序电路必须包含存储元件（如触发器），而计数器内部由触发器构成，能够通过时钟信号实现计数状态的保存和递进。其他三个选项均不涉及状态存储。\n记忆技巧：\n联想\u0026quot;时序\u0026quot;与\u0026quot;时间顺序\u0026quot;，需要记录状态变化的电路（如计数、存储）属于时序逻辑。 常见时序电路：计数器、寄存器、序列检测器。 常见组合电路：编码/译码器、数据选择/分配器、加法器、比较器。 通过理解电路是否具备\u0026quot;记忆能力\u0026quot;，可以快速判断此类题型。\n[!NOTE]\n核心概念解析 穿透电流ICEO：指基极开路时（IB=0），集电极-发射极之间的漏电流。它由少数载流子的漂移运动形成，是衡量晶体管性能的重要参数。\n​选项逐项分析 ​A：温度稳定性（正确）​\n​原因：ICEO对温度极为敏感。温度升高 → 本征激发增强 → 少数载流子浓度增加 → ICEO显著增大。 ​意义：ICEO的大小直接反映晶体管在不同温度下的稳定性。若ICEO过大，高温下可能导致晶体管热失控。 ​B：最大电流极限参数（争议性正确）​\n​争议点：传统上，晶体管的最大电流极限参数是ICM​（集电极最大电流），而非ICEO。ICEO是微安级漏电流，远小于极限电流。 ​可能的出题逻辑：若题目将ICEO视为“设计时需限制的参数”（如过高的ICEO可能影响可靠性），则B可视为正确。但需注意，严格来说ICEO不属于“允许通过的最大电流”。 ​C：放大能力（错误）​\n​原因：放大能力由电流放大系数β（或hFE）决定，与ICEO无直接关系。 ​D：频率特性（错误）​\n​原因：频率特性与结电容、载流子渡越时间相关，而ICEO反映的是漏电流特性。 ​用户得分0分的原因 ​正确答案需同时选A和B：用户仅选择B，漏选A导致未得分。 ​关键误区：可能误将ICEO与极限电流参数（如ICM）关联，但未意识到其温度敏感性的核心作用。 ​记忆技巧 ​穿透电流的两面性： ​温度稳定性​（A）：联想“温度升高→漏电流飙升→稳定性差”。 ​极限参数（若题目接受B）​：需注意题目可能的隐含设定，但实际应用中需区分ICEO与ICM。 ​排除法： 放大能力（C）→ 直接排除（与β相关）。 频率特性（D）→ 排除（与结电容相关）。 ​总结 ​核心考点：穿透电流ICEO的双重特性（温度稳定性 + 设计限制参数）。 [!NOTE]\n核心解题思路 ​运算放大器的两种工作状态：\n​线性区：输出信号与输入信号成比例关系（实现放大功能）。 ​非线性区​（饱和区）：输出信号为电源电压的正/负极限值（用作比较器或开关）。 ​关键条件：负反馈\n运算放大器要工作在线性区，​必须引入负反馈​（将输出信号通过反馈网络送回反相输入端）。 ​作用： 抑制开环增益的极高不稳定性（典型开环增益为 105∼106）。 通过负反馈调整闭环增益，使输出与输入保持线性关系。 ​选项排除分析：\n​B. 正反馈：会导致输出迅速饱和（进入非线性区），无法线性放大（例：振荡电路）。 ​C. 开环：无反馈时运放开环增益极大，输入微小差异即饱和，无法稳定放大。 ​D. 振荡：属于非线性应用（需正反馈），与线性放大无关。 ​记忆技巧 ​联想公式：线性放大 → Vout​=ACL​⋅(V+​−V−​)（ACL​ 为闭环增益，由负反馈决定）。 ​对比应用场景： ​负反馈：放大器、滤波器、积分电路（线性应用）。 ​正反馈/开环：比较器、施密特触发器、振荡器（非线性应用）。 ​常见误区提醒 ​误区：“运放必须开环才能放大信号”。 ​纠正：开环时运放极易饱和，实际放大电路必须通过负反馈闭环工作。 ​误区：“正反馈可以稳定放大信号”。 ​纠正：正反馈会加速输出进入饱和状态，破坏线性关系。 ​总结 ​核心结论：负反馈是运放工作在线性区的必要条件。 ​答题关键：直接关联“线性区”与“负反馈”，排除其他非线性工作模式。 [!NOTE]\n​ROM（Read-Only Memory，只读存储器）​：通常指Flash Memory，用于存储程序代码和常量数据（如代码固化后不可修改）。 ​RAM（Random Access Memory，随机存取存储器）​：用于存储临时变量和运行时数据（易失性，断电丢失）。 ​寄存器：CPU内部的存储单元，用于指令执行和临时操作，不存储程序代码。 ​E2PROM（Electrically Erasable Programmable ROM）​：可擦写的非易失性存储器，用于存储配置参数等需长期保存的数据。 [!NOTE]\nA. RCC（正确）​ ​中断类型：RCC（Reset and Clock Control）相关中断，如时钟安全系统（CSS）中断、PLL就绪中断等。 ​优先级配置： 属于可屏蔽中断，优先级通过NVIC（嵌套向量中断控制器）配置。 例如，CSS中断的优先级可设置为低于其他关键中断（如NMI），但高于普通外设中断。 ​用户可能误解：误认为RCC是系统核心功能（如时钟配置）的中断优先级不可调，但实际优先级是可配置的。 ​B. NMI（错误）​ ​中断类型：NMI（Non-Maskable Interrupt，不可屏蔽中断），用于处理严重硬件错误（如电源故障）。 ​优先级配置： ​优先级固定为最高​（逻辑优先级最高，不可修改）。 无法通过NVIC或其他方式调整优先级。 ​用户错误原因：可能混淆了“不可屏蔽”与“优先级不可配置”的关系，误认为NMI优先级可调。 ​C. HardFault（错误）​ ​中断类型：HardFault（硬件错误异常），由非法操作（如访问未定义内存）触发。 ​优先级配置： ​优先级固定为-1（最高优先级）​，属于系统级异常，不可配置。 任何优先级配置操作对HardFault无效。 ​关键区别：HardFault是异常（Exception），而非普通中断，优先级机制与中断不同。 ​D. Systick（正确）​ ​中断类型：Systick（系统定时器中断），用于操作系统任务调度或定时触发。 ​优先级配置： 优先级通过NVIC配置，与普通外设中断相同。 例如，在RTOS中可降低Systick优先级以避免影响高实时性任务。 ​用户可能忽略：误认为Systick是内核功能优先级固定，但其优先级可自由设置。 [!NOTE]\n许多同学一眼看见有“+3 V”和“+9 V”两条支路，就会直觉认为输出可能会是 3 V 或 9 V，结果往往忽略了那条最关键的“通向 0 V”支路。事实上，因为“通向 0 V” 的二极管最先导通，输出会被它死死地钳到 0 V，根本“上不去”3 V 或 9 V。 [!NOTE]\nD: 可以通过软件控制SysTick定时器启动和停止 ​正确\n通过设置控制寄存器（CTRL）的ENABLE位，可直接用软件启动或停止SysTick计数器。例如：\n1 2 SysTick-\u0026gt;CTRL |= SysTick_CTRL_ENABLE_Msk; // 启动 SysTick-\u0026gt;CTRL \u0026amp;= ~SysTick_CTRL_ENABLE_Msk; // 停止 [!NOTE]\n“跨平台可移植（D）”并非嵌入式系统的普遍特征 嵌入式系统通常“紧耦合”硬件\n对特定硬件平台进行了定制优化，大量驱动、库函数都与目标硬件的寄存器、外设紧密关联。\n要想“跨平台移植”，往往要重新适配甚至重写底层驱动、硬件抽象层，远没有通用 PC 软件那样“轻松”移植。\n跨平台可移植更多是通用软件的特点\n例如使用高级语言编写的桌面应用、跨平台库（Qt、Java 虚拟机等）等，编译或运行时只要目标平台能提供兼容环境，就能跑起来。\n而嵌入式软件因其“贴近底层硬件、资源有限、功能定制”等特点，移植并非轻而易举。\n虽然部分嵌入式 RTOS 或应用层可以做到一定程度的可移植性，但这并不是所有嵌入式系统的共性。不少教材或考试题更倾向于强调嵌入式的“专用、定制、不可随意跨平台”的一面，因此D 往往被排除。\n[!NOTE] 在电子技术里，当题目说“电压增益为 -20 dB”时，指的是用分贝（dB）来度量“电压增益（Voltage Gain）”这一物理量。关键公式是：\nAv(dB)=20×log⁡10(Av)A_{v}(dB) = 20 \\times \\log_{10} \\bigl(A_{v}\\bigr)\n其中 AvA_{v} 是线性电压增益（即“倍数”形式的增益），Av(dB)A_{v}(dB) 是分贝形式的电压增益。根据题意，已知\nAv(dB)=−20 dBA_{v}(dB) = -20 \\text{ dB}\n那么就可以列方程：\n−20=20×log⁡10(Av)-20 = 20 \\times \\log_{10}\\bigl(A_{v}\\bigr)\n接下来一步步求解：\n把 20 移到左边\nlog⁡10(Av)=−2020=−1\\log_{10}\\bigl(A_{v}\\bigr) = \\frac{-20}{20} = -1\n对数换为线性量\nAv=10−1=0.1A_{v} = 10^{-1} = 0.1\n所以当电压增益为 -20 dB 时，对应的线性电压增益是 0.10.1 倍（也可以理解为衰减 10 倍）。因此题目中的正确答案是 0.1 倍。\n[!NOTE] 从题目与选项来看，这道题考察的是多级放大电路的通频带(带宽)随级数增加而变化的规律。题目给出的选项是：\nA：变宽\nB：变窄\nC：不变\nD：无关\n而“正确答案：B”，说明多级放大电路的通频带会变窄。下面是推理过程：\n1. 多级放大电路的频率特性叠加 一个单级放大电路在其高频端和低频端都会受到各种因素（如电容、电感、寄生参数等）的影响，从而在某个频段内保持较好的增益。当把若干单级放大电路串联（多级放大）时，总的通频带（带宽）会受到各级放大电路频率特性的共同制约。\n在低频端：耦合电容、旁路电容以及输入/输出耦合电容等，都会对低频响应产生影响；多级串联后，低频端的截止点可能会上移。\n在高频端：器件本身的寄生电容、电感等导致的高频滚降，随着级数的增加也会更明显；多级串联后，高频端的截止点会更早下降。\n因此，从整体来看，多级放大电路的通频带往往比单级放大电路变窄。\n2. 理论依据：极点与零点的叠加 在“网络理论”或“控制理论”的角度，每级放大电路都可以用相应的极点和零点来描述它的频率响应。串联多级放大器时，其传递函数相当于各级传递函数的连乘：\nHtotal(s)=H1(s)×H2(s)×⋯×Hn(s)H_{\\text{total}}(s) = H_1(s) \\times H_2(s) \\times \\cdots \\times H_n(s)\n每个 Hi(s)H_i(s) 都有自己的一组极点和零点；\n连乘后，极点和零点会叠加到一起，通常会使得总带宽变得更窄。\n3. 结论 由于多级放大器的总通频带是各级通频带综合作用的结果，而每级的高低频衰减特性相叠加后，往往导致整体频率范围（带宽）变窄，这是理论与实际中都能观察到的普遍规律。所以正确答案为 B：变窄。\n[!NOTE]\n1. 7 位地址模式 最常见、最普及的地址模式就是 7 位地址模式。\n7 位地址占据总传输字节中的高 7 位，第 8 位则通常用来表示读/写位（R/W）。\n2. 10 位地址模式 在某些场合下，需要更多设备地址时会采用 10 位地址模式。\n10 位地址模式时，会使用一个特定的前缀（如 11110XX）来表示这是 10 位地址，然后再后续的字节里传送剩余的地址位。\n3. “8 位”与“4 位”的误解 有人将“7 位地址 + 1 位读写标志”误以为是“8 位地址”，但在 I²C 协议定义中，读写标志并不计入地址位数，因此**不存在“8 ","permalink":"https://www.19y.cc/p/blue-bridge-cup-choose/","summary":"\u003cp\u003e\u003cimg alt=\"蓝桥杯嵌入式选择题错题集截图 1\" loading=\"lazy\" src=\"Pasted%20image%2020250217104715.png\"\u003e\n\u003cimg alt=\"蓝桥杯嵌入式选择题错题集截图 2\" loading=\"lazy\" src=\"Pasted%20image%2020250217104803.png\"\u003e\n\u003cimg alt=\"蓝桥杯嵌入式选择题错题集截图 3\" loading=\"lazy\" src=\"Pasted%20image%2020250217115425.png\"\u003e\n\u003cimg alt=\"蓝桥杯嵌入式选择题错题集截图 4\" loading=\"lazy\" src=\"Pasted%20image%2020250217151824.png\"\u003e\n\u003cimg alt=\"蓝桥杯嵌入式选择题错题集截图 5\" loading=\"lazy\" src=\"Pasted%20image%2020250217152223.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!NOTE]\u003c/p\u003e\n\u003ch4 id=\"关键字\"\u003e\u003cstrong\u003e关键字 \u003ccode\u003einline\u003c/code\u003e 的作用\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003e\u003ccode\u003einline\u003c/code\u003e 是 C/C++ 中的关键字，用于建议编译器将函数\u003cstrong\u003e内联展开\u003c/strong\u003e（即在调用处直接插入函数代码，而非生成函数调用指令）。它的主要影响包括：\u003c/p\u003e","title":"蓝桥杯嵌入式选择题部分错题集"},{"content":"Markdown 全格式测试文档 1. 标题与段落 这是普通段落，展示加粗、斜体、删除线和行内代码。\n换行需在行尾添加两个空格或使用\u0026lt;br\u0026gt;标签。\n2. 列表与任务 无序列表 苹果 香蕉 进口香蕉 本地香蕉 有序列表 打开IDE 创建项目 编写代码 任务列表 学习Markdown基础 掌握高级表格技巧 3. 代码与语法高亮 行内代码：print(\u0026quot;Hello Markdown\u0026quot;)\n1 2 3 4 5 6 # Python代码块 def fibonacci(n): if n \u0026lt;= 1: return n else: return fibonacci(n-1) + fibonacci(n-2) 1 2 // JavaScript代码块 const greet = (name) =\u0026gt; console.log(`Hello ${name}!`); 4. 表格与引用 语言 热度 特性 Python ★★★★ 简洁易读 JavaScript ★★★★☆ 全栈开发 Rust ★★★★ 内存安全 引用块嵌套示例\n二级引用：代码是诗，注释是散文\n三级引用：好代码会自己说话\n5. 数学公式（KaTeX） 行内公式：$\\frac{\\partial f}{\\partial t} = \\nabla \\cdot (D \\nabla f)$\n块级公式： $$ \\begin{bmatrix} a \u0026amp; b \\ c \u0026amp; d \\end{bmatrix} \\begin{bmatrix} x \\ y \\end{bmatrix} \\begin{bmatrix} \\alpha \\ \\beta \\end{bmatrix} $$\n6. 扩展功能 折叠内容 点击查看配置说明\r1 2 3 4 # 服务器配置 server: port: 8080 ssl: true 脚注示例 这是一个带有脚注的句子 这是第一个脚注的内容\n引用自Markdown教程 表格技巧详见速优物联指南 1 2 3 4 5 6 7 8 9 --- **关键语法说明**： 1. 代码块使用三个反引号包裹并指定语言类型（如`python`）可实现语法高亮 2. 数学公式通过`$...$`（行内）和`$$...$$`（块级）实现科技文档排版 3. 表格对齐使用冒号标记（`:---`左对齐、`:---:`居中、`---:`右对齐） 4. 折叠区块需配合HTML的`\u0026lt;details\u0026gt;`标签实现交互效果 5. 任务列表通过短横线+方括号（`- [ ]`）创建待办事项 ","permalink":"https://www.19y.cc/p/slugtest/","summary":"\u003ch1 id=\"markdown-全格式测试文档\"\u003eMarkdown 全格式测试文档\u003c/h1\u003e\n\u003ch2 id=\"1-标题与段落\"\u003e1. 标题与段落\u003c/h2\u003e\n\u003cp\u003e这是普通段落，展示\u003cstrong\u003e加粗\u003c/strong\u003e、\u003cem\u003e斜体\u003c/em\u003e、\u003cdel\u003e删除线\u003c/del\u003e和\u003ccode\u003e行内代码\u003c/code\u003e。\u003cbr\u003e\n换行需在行尾添加两个空格或使用\u003ccode\u003e\u0026lt;br\u0026gt;\u003c/code\u003e标签。\u003c/p\u003e\n\u003ch2 id=\"2-列表与任务\"\u003e2. 列表与任务\u003c/h2\u003e\n\u003ch3 id=\"无序列表\"\u003e无序列表\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e苹果\u003c/li\u003e\n\u003cli\u003e香蕉\n\u003cul\u003e\n\u003cli\u003e进口香蕉\u003c/li\u003e\n\u003cli\u003e本地香蕉\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"有序列表\"\u003e有序列表\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e打开IDE\u003c/li\u003e\n\u003cli\u003e创建项目\u003c/li\u003e\n\u003cli\u003e编写代码\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"任务列表\"\u003e任务列表\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 学习Markdown基础\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 掌握高级表格技巧\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"3-代码与语法高亮\"\u003e3. 代码与语法高亮\u003c/h2\u003e\n\u003cp\u003e行内代码：\u003ccode\u003eprint(\u0026quot;Hello Markdown\u0026quot;)\u003c/code\u003e\u003c/p\u003e","title":"全场景md测试"},{"content":"Markdown 全格式测试文档 1. 标题与段落 这是普通段落，展示加粗、斜体、删除线和行内代码。\n换行需在行尾添加两个空格或使用\u0026lt;br\u0026gt;标签。\n2. 列表与任务 无序列表 苹果 香蕉 进口香蕉 本地香蕉 有序列表 打开IDE 创建项目 编写代码 任务列表 学习Markdown基础 掌握高级表格技巧 3. 代码与语法高亮 行内代码：print(\u0026quot;Hello Markdown\u0026quot;)\n1 2 3 4 5 6 # Python代码块 def fibonacci(n): if n \u0026lt;= 1: return n else: return fibonacci(n-1) + fibonacci(n-2) 1 2 // JavaScript代码块 const greet = (name) =\u0026gt; console.log(`Hello ${name}!`); 4. 表格与引用 语言 热度 特性 Python ★★★★ 简洁易读 JavaScript ★★★★☆ 全栈开发 Rust ★★★★ 内存安全 引用块嵌套示例\n二级引用：代码是诗，注释是散文\n三级引用：好代码会自己说话\n5. 数学公式（KaTeX） 行内公式：$\\frac{\\partial f}{\\partial t} = \\nabla \\cdot (D \\nabla f)$\n块级公式： $$ \\begin{bmatrix} a \u0026amp; b \\ c \u0026amp; d \\end{bmatrix} \\begin{bmatrix} x \\ y \\end{bmatrix} \\begin{bmatrix} \\alpha \\ \\beta \\end{bmatrix} $$\n6. 扩展功能 折叠内容 点击查看配置说明\r1 2 3 4 # 服务器配置 server: port: 8080 ssl: true 脚注示例 这是一个带有脚注的句子 这是第一个脚注的内容\n引用自Markdown教程 表格技巧详见速优物联指南 1 2 3 4 5 6 7 8 9 --- **关键语法说明**： 1. 代码块使用三个反引号包裹并指定语言类型（如`python`）可实现语法高亮 2. 数学公式通过`$...$`（行内）和`$$...$$`（块级）实现科技文档排版 3. 表格对齐使用冒号标记（`:---`左对齐、`:---:`居中、`---:`右对齐） 4. 折叠区块需配合HTML的`\u0026lt;details\u0026gt;`标签实现交互效果 5. 任务列表通过短横线+方括号（`- [ ]`）创建待办事项 ","permalink":"https://www.19y.cc/p/%E5%85%A8%E6%A0%BC%E5%BC%8F%E6%B5%8B%E8%AF%95/","summary":"\u003ch1 id=\"markdown-全格式测试文档\"\u003eMarkdown 全格式测试文档\u003c/h1\u003e\n\u003ch2 id=\"1-标题与段落\"\u003e1. 标题与段落\u003c/h2\u003e\n\u003cp\u003e这是普通段落，展示\u003cstrong\u003e加粗\u003c/strong\u003e、\u003cem\u003e斜体\u003c/em\u003e、\u003cdel\u003e删除线\u003c/del\u003e和\u003ccode\u003e行内代码\u003c/code\u003e。\u003cbr\u003e\n换行需在行尾添加两个空格或使用\u003ccode\u003e\u0026lt;br\u0026gt;\u003c/code\u003e标签。\u003c/p\u003e\n\u003ch2 id=\"2-列表与任务\"\u003e2. 列表与任务\u003c/h2\u003e\n\u003ch3 id=\"无序列表\"\u003e无序列表\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e苹果\u003c/li\u003e\n\u003cli\u003e香蕉\n\u003cul\u003e\n\u003cli\u003e进口香蕉\u003c/li\u003e\n\u003cli\u003e本地香蕉\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"有序列表\"\u003e有序列表\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e打开IDE\u003c/li\u003e\n\u003cli\u003e创建项目\u003c/li\u003e\n\u003cli\u003e编写代码\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"任务列表\"\u003e任务列表\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 学习Markdown基础\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 掌握高级表格技巧\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"3-代码与语法高亮\"\u003e3. 代码与语法高亮\u003c/h2\u003e\n\u003cp\u003e行内代码：\u003ccode\u003eprint(\u0026quot;Hello Markdown\u0026quot;)\u003c/code\u003e\u003c/p\u003e","title":"全格式测试"},{"content":"","permalink":"https://www.19y.cc/p/test/","summary":"","title":"Test"},{"content":"常用链接。\n","permalink":"https://www.19y.cc/links/","summary":"\u003cp\u003e常用链接。\u003c/p\u003e","title":"Links"},{"content":"你好，我是 19y。这个博客记录我在嵌入式、编程与学习过程中的笔记和踩坑经历，也会整理一些竞赛与开源项目的实践心得。\n","permalink":"https://www.19y.cc/%E5%85%B3%E4%BA%8E/","summary":"\u003cp\u003e你好，我是 \u003cstrong\u003e19y\u003c/strong\u003e。这个博客记录我在嵌入式、编程与学习过程中的笔记和踩坑经历，也会整理一些竞赛与开源项目的实践心得。\u003c/p\u003e","title":"关于"}]