1. 为什么现在要认真搭一套 Rust + STM32 + VS Code 的嵌入式开发环境?
Rust 进入嵌入式领域不是赶时髦,而是解决了一类长期被容忍却极其危险的“慢性病”——内存安全漏洞在裸机环境下的不可控蔓延。我从 2015 年开始用 C 写 STM32 项目,踩过太多因指针越界、未初始化变量、中断上下文资源竞争导致的偶发死机:某次温控设备在凌晨三点重启,日志里只留下一句HardFault_Handler;某款工业传感器模组批量返工,最后定位到是memcpy拷贝长度算错两个字节,把控制寄存器给覆写了。这些不是“小问题”,而是嵌入式系统可靠性天花板的直接杀手。Rust 的所有权模型不依赖 GC,也不靠运行时检查,它在编译期就用类型系统把绝大多数内存错误拦在门外——这不是理论优势,是我在 STM32F407 上实测跑满 72MHz 主频、连续 96 小时无异常的硬指标。
你可能听过“Rust 学习曲线陡峭”,但真实情况是:嵌入式 Rust 的入门门槛,其实比现代 C 工程更低。为什么?因为传统 STM32 开发绕不开 Keil/STM32CubeIDE 的图形化配置陷阱:CubeMX 自动生成的 HAL 库代码动辄上千行,HAL_Delay() 里藏着 SysTick 中断优先级配置的隐式依赖,HAL_UART_Transmit() 调用前必须确保 huart->gState 是 HAL_UART_STATE_READY,而这个状态又受 DMA 配置、中断使能、甚至串口线缆接触质量的影响。新手调试时常常陷入“改了 A 参数,B 功能崩了,C 日志又不打印”的无限循环。Rust 生态的cortex-m、stm32f4xx-hal等 crate 采用零成本抽象(zero-cost abstraction),所有外设驱动都通过类型系统强制约束使用顺序——比如Serial::new()返回的串口句柄,必须先调用.configure()设置波特率和数据位,否则编译直接报错;Delay::new()创建的延时器,其滴答周期由系统时钟树在编译期确定,不可能出现运行时才报“时钟未使能”的诡异错误。这种“编译即验证”的体验,对刚脱离大学单片机实验课的新手,反而比面对一堆 HAL 函数手册更友好。
VS Code 在这里不是简单替代 IDE,而是重构工作流。Keil 的工程管理像一个黑盒,.uvprojx文件是二进制格式,无法用 Git 追踪配置变更;CubeIDE 的 Device Configuration Tool 生成的Core/Src/*_hal.c文件,每次重新生成都会覆盖你的手动修改。而 VS Code + Rust 的组合,让整个开发栈回归文本化、可版本化、可复现的本质:Cargo.toml明确声明依赖版本,memory.x链接脚本用纯文本定义 RAM/ROM 分区,openocd.cfg配置文件清晰列出 JTAG 探针参数。我上个月帮一家做医疗设备的客户迁移旧项目,他们原来的 Keil 工程在不同工程师电脑上编译出的固件 CRC 校验值都不一致——查了三天才发现是某个工程师本地安装了新版 ARMCC 编译器,而工程配置里没锁定工具链版本。换成 Rust 后,rust-toolchain.toml一行代码就锁死nightly-2023-10-15,全团队编译结果完全一致。
这套环境真正解决的是“嵌入式开发中的信任危机”:你是否真的相信自己写的每一行代码,在上电那一刻会按预期执行?Rust 提供编译期保证,VS Code 提供透明化工作流,STM32 提供成熟稳定的硬件基座。这不是炫技,是让工程师把精力从“猜硬件状态”转向“设计系统逻辑”。接下来我会带你从零开始,亲手搭起这套环境——不跳过任何一个看似琐碎的步骤,因为每一个被省略的细节,都可能是你三天后深夜抓狂的根源。
2. 整体架构设计与关键决策依据
2.1 为什么选 Rust 而非 C++ 或 Zig?
有人会问:“C++ 也有 RAII 和模板,Zig 声称更简单,为什么非 Rust 不可?”这个问题的答案藏在嵌入式最严苛的约束里:确定性和可预测性。C++ 的异常机制(try/catch)和 RTTI(运行时类型信息)在裸机环境下需要额外的运行时支持,即使禁用也会增加链接器脚本复杂度;Zig 的@panic默认行为是无限循环,但它的标准库尚未提供成熟的中断处理宏或内存池管理器。而 Rust 的no_std生态是为裸机量身定制的:core库不依赖任何操作系统服务,alloc库提供Box和Vec的堆分配(可选),cortex-mcrate 直接映射 ARM Cortex-M 架构的寄存器布局和异常向量表。
最关键的是unsafe的边界控制。Rust 允许在必要时使用unsafe块操作硬件寄存器,但这个块必须显式标注,且其作用域被严格限制。对比 C 语言中随处可见的*(volatile uint32_t*)0x40023800 = 0x1;这种“上帝模式”操作,Rust 要求你先声明const RCC_BASE: *mut u32 = 0x40023800 as *mut u32;,再在unsafe { (*RCC_BASE) = 1; }中调用。这种显式标记强迫开发者思考:“这段代码为什么必须不安全?它的副作用是否可控?”——这正是嵌入式开发中最稀缺的思维习惯。
2.2 为什么坚持用 VS Code 而非专用 IDE?
STM32CubeIDE 确实开箱即用,但它把“硬件抽象”和“软件抽象”混为一谈。CubeMX 生成的初始化代码,把时钟树配置、GPIO 复用、外设使能全部揉进一个函数,而 Rust 的pac(Peripheral Access Crate)和hal(Hardware Abstraction Layer)分层明确:stm32f4xx-pac直接映射寄存器地址,stm32f4xx-hal在其上构建类型安全的 API。VS Code 的优势在于能用插件精准支撑这种分层:
rust-analyzer提供跨 crate 的符号跳转,点进serial.write(b"hello")能一路追溯到pac::usart1::cr1::TE::SET的位操作;Cortex-Debug插件直接读取 OpenOCD 的 GDB 协议,断点可以打在cortex_m::asm::delay的汇编指令上;Error Lens插件在代码行尾实时显示编译错误,不用切到终端看cargo build的滚动日志。
更重要的是,VS Code 的设置是纯 JSON 文本(.vscode/settings.json),可以和源码一起提交到 Git。当新同事克隆仓库,执行git clone && cd project && cargo build,就能获得和你完全一致的开发体验——没有“请安装最新版 CubeIDE”这种模糊指令。
2.3 为什么选择 STM32F4 系列作为入门载体?
网络热词里频繁出现stm32f4、stm32 车载以太网,这不是偶然。F4 系列是 ARM Cortex-M4 内核的标杆,主频 168MHz,带 FPU 和 DSP 指令集,Flash 1MB / RAM 192KB,外设丰富(USB OTG、Ethernet MAC、FSMC)。更重要的是生态成熟:stm32f4xx-halcrate 更新活跃,社区教程多,OpenOCD 对 F4 的调试支持最稳定。相比之下,更新的 H7 系列虽然性能更强,但stm32h7xx-hal的文档碎片化严重;而 F0/F1 系列 RAM 太小(仅 6-32KB),连 Rust 的core::fmt::Writetrait 实现都可能因栈溢出失败。我建议新手从STM32F407VGT6开发板入手(淘宝约 35 元),它有 100 引脚 LQFP 封装,JTAG/SWD 调试接口标准,且配套的bluepill变种板资料极多。
2.4 工具链选型的底层逻辑
整个环境依赖四个核心工具链,每个选择都有明确的工程权衡:
| 工具 | 版本要求 | 选择理由 | 替代方案风险 |
|---|---|---|---|
| Rust toolchain | nightly+cargo-binutils | nightly支持#[panic_handler]和#![no_main]等裸机必需特性;cargo-binutils提供cargo-objdump查看反汇编 | stable无法编译no_std二进制;xargo已被官方弃用 |
| ARM GCC | arm-none-eabi-gcc 12.2 | GNU 工具链对 Cortex-M 支持最完善,ld链接器脚本语法稳定 | LLVM 的clang --target=armv7m对某些外设寄存器别名支持不全 |
| OpenOCD | v0.12.0+ | 支持 STM32F4 的 SWD 调试,stlink-v2探针兼容性好 | J-Link GDB Server 需要商业授权,且配置复杂 |
| VS Code | 1.85+ | rust-analyzer插件对no_std项目的索引准确率在 1.85 版本后显著提升 | Sublime Text 缺乏成熟的 Rust 调试集成 |
特别注意:不要用系统包管理器安装这些工具。Ubuntu 的apt install gcc-arm-none-eabi可能安装过旧版本(如 9.x),导致链接时出现undefined reference to 'memcpy'错误;MacOS 的brew install openocd可能缺少--enable-stlink编译选项。必须从官网下载预编译二进制包,或用rustup/cargo install管理 Rust 工具链。
3. 核心组件安装与配置详解
3.1 Rust 工具链:从 nightly 到裸机支持
第一步永远是安装 Rust。执行以下命令(不要用 root 权限):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env"安装完成后,必须切换到nightly工具链并添加裸机目标:
rustup default nightly rustup target add thumbv7em-none-eabihf rustup component add llvm-tools-preview cargo install cargo-binutils这里的关键点在于thumbv7em-none-eabihf这个目标三元组:
thumbv7em:指定 ARM Thumb-2 指令集(M4 内核使用);none:表示无操作系统(bare metal);eabihf:遵循 ARM EABI(Embedded Application Binary Interface)硬浮点调用约定。
提示:如果你用的是 STM32F429(带 Chrom-ART 加速器)或 F469(带 LCD-TFT 控制器),需额外添加
thumbv7em-none-eabi(软浮点)目标,但 F407 默认用硬浮点,eabihf更高效。
验证安装是否成功:
cargo new --bin hello-stm32 cd hello-stm32 # 修改 Cargo.toml,添加 [dependencies] [dependencies] cortex-m = "0.7" cortex-m-rt = "0.7" panic-halt = "0.2"此时cargo build会失败,因为缺少链接脚本。创建memory.x文件(放在项目根目录):
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K } SECTIONS { .vector_table ORIGIN(FLASH) : { KEEP(*(.vector_table)) } > FLASH .text : { *(.text) *(.rodata) } > FLASH .data : { *(.data) } > RAM AT > FLASH .bss : { *(.bss) *(COMMON) } > RAM }这个脚本定义了 STM32F407 的内存布局:Flash 从0x08000000开始(1MB),RAM 从0x20000000开始(192KB)。AT > FLASH表示.data段的初始值存储在 Flash,启动时由 C runtime 复制到 RAM。
3.2 VS Code 插件配置:从编辑到调试的闭环
打开 VS Code,安装以下插件(按顺序,避免依赖冲突):
- Rust Analyzer(必装):提供语义高亮、自动补全、类型推导。在设置中搜索
rust-analyzer,启用rust-analyzer.cargo.loadOutDirsFromCheck,确保它能正确解析no_std依赖。 - Cortex-Debug(必装):调试核心。安装后需配置
launch.json。在项目根目录创建.vscode/launch.json:
{ "version": "0.2.0", "configurations": [ { "type": "cortex-debug", "request": "launch", "name": "Debug STM32F4", "servertype": "openocd", "cwd": "${workspaceFolder}", "executable": "./target/thumbv7em-none-eabihf/debug/hello-stm32", "configFiles": [ "${workspaceFolder}/openocd.cfg" ], "preLaunchTask": "Build and Flash", "runToMain": true, "showDevDebugOutput": true } ] }- Error Lens(推荐):在代码行尾直接显示编译错误,避免频繁切到终端。
- GitLens(推荐):查看每行代码的 Git 提交历史,对协作开发至关重要。
注意:
Cortex-Debug的executable字段必须指向target/.../debug/下的 ELF 文件,不能是.bin或.hex。Rust 编译默认生成 ELF,这是 GDB 调试必需的格式。
3.3 OpenOCD 配置:让探针听懂你的指令
OpenOCD 是连接 VS Code 和硬件的翻译官。创建openocd.cfg(项目根目录):
# 使用 ST-Link v2 探针 source [find interface/stlink-v2.cfg] # 指定 STM32F407 芯片 source [find target/stm32f4x.cfg] # 重置并 halt CPU reset_config srst_only adapter speed 1000 # 加载固件到 Flash program {{./target/thumbv7em-none-eabihf/debug/hello-stm32}} verify reset exit关键参数解释:
adapter speed 1000:设置 SWD 时钟为 1MHz,过高会导致连接不稳定(尤其廉价 ST-Link v2);reset_config srst_only:仅使用系统复位(SRST),避免误触发调试复位(TRST);program ... verify reset exit:烧录后校验 Flash 内容,复位芯片并退出 OpenOCD。
测试 OpenOCD 是否正常工作:
openocd -f openocd.cfg如果看到Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints,说明探针已识别芯片。
3.4 构建任务自动化:告别手动敲命令
VS Code 的tasks.json可以把cargo build、cargo objdump、openocd串联成一键操作。创建.vscode/tasks.json:
{ "version": "2.0.0", "tasks": [ { "label": "Build and Flash", "type": "shell", "command": "sh", "args": [ "-c", "cargo build --target thumbv7em-none-eabihf && arm-none-eabi-objcopy -O binary ./target/thumbv7em-none-eabihf/debug/hello-stm32 ./target/firmware.bin && echo 'Build success!' || echo 'Build failed!'" ], "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": true }, "problemMatcher": ["$rust"] } ] }这个任务做了三件事:
cargo build --target ...:交叉编译为 ARM 目标;arm-none-eabi-objcopy:将 ELF 转为纯二进制.bin(烧录用);- 输出成功/失败提示。
实操心得:我最初用
cargo objcopy替代arm-none-eabi-objcopy,结果发现cargo-binutils的objcopy对--binary-architecture参数支持不全,导致生成的.bin文件头损坏。务必用 GNU 工具链的objcopy。
4. 从零编写第一个 Rust 嵌入式程序
4.1 初始化框架:main.rs的最小可行结构
创建src/main.rs,这是裸机 Rust 的心脏:
#![no_std] #![no_main] use cortex_m_rt::entry; use panic_halt as _; #[entry] fn main() -> ! { // 获取设备外设访问权限 let mut peripherals = cortex_m_peripheral::Peripherals::take().unwrap(); // 配置系统时钟(HSE=8MHz, PLL=168MHz) let rcc = peripherals.RCC.constrain(); let clocks = rcc.cfgr.sysclk(168.mhz()).freeze(); // 获取 GPIOA 句柄 let mut gpioa = peripherals.GPIOA.split(); // 配置 PA5 为推挽输出(LED 引脚) let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper); // 主循环:翻转 LED loop { led.set_high(); cortex_m::asm::delay(clocks.sysclk().0 / 2); // 约 0.5 秒 led.set_low(); cortex_m::asm::delay(clocks.sysclk().0 / 2); } }逐行解析这个“Hello World”:
#![no_std]:禁用标准库,只使用core;#![no_main]:禁用 C 标准入口main(),改用cortex-m-rt的#[entry]宏;cortex_m_rt::entry:提供Reset异常向量的实现,包含栈初始化、.data复制等;cortex_m_peripheral::Peripherals::take():获取外设单例(Singleton),这是 Rust 防止并发访问的核心机制;rcc.cfgr.sysclk(168.mhz()).freeze():配置 PLL 时钟,freeze()返回Clocks结构体,其中sysclk().0是实际频率数值(Hz);gpioa.pa5.into_push_pull_output():类型安全地将 PA5 配置为推挽输出,编译器会检查该引脚是否支持此模式;cortex_m::asm::delay():基于 SysTick 的精确延时,参数是滴答数,无需配置 SysTick 寄存器。
4.2 链接与启动过程:从代码到芯片的旅程
当你执行cargo build,发生了什么?
- 编译阶段:
rustc将main.rs编译为 LLVM IR,再生成 ARM 汇编; - 链接阶段:
ld读取memory.x,将.vector_table放到 Flash 起始地址0x08000000,.text紧随其后,.data放在 RAM 起始0x20000000; - 启动代码注入:
cortex-m-rt提供_start符号,它在复位后首先执行:- 初始化栈指针(SP)为
0x20020000(RAM 末尾); - 复制
.data段从 Flash 到 RAM; - 清零
.bss段; - 调用
main()函数。
- 初始化栈指针(SP)为
你可以用cargo objdump查看生成的机器码:
cargo objdump --bin hello-stm32 -- -d输出中会看到:
Disassembly of section .vector_table: 08000000 <_vector_table>: 8000000: 20020000 .word 0x20020000 # SP initial value 8000004: 08000141 .word 0x08000141 # Reset handler address这证明向量表已正确定位。
4.3 硬件连接与首次烧录:点亮那颗 LED
物理连接步骤(以常见蓝色 STM32F407 开发板为例):
ST-Link v2 探针接线:
SWDIO→ 板子PA13(或SWDIO标注引脚);SWCLK→ 板子PA14(或SWCLK标注引脚);GND→ 板子GND;3.3V→不接!(开发板自带稳压,接外部电源可能导致损坏)
LED 确认:多数开发板的 D1 LED 接在
PA5,若不亮,用万用表测PA5对地电压是否在 0V/3.3V 间跳变。烧录操作:
- VS Code 中按
Ctrl+Shift+P,输入Cortex-Debug: Start Debugging; - 选择
Debug STM32F4配置; - 如果看到
Info : Listening on port 3333 for gdb connections,说明 OpenOCD 已启动; - 按
F5开始调试,程序会停在main()第一行。
- VS Code 中按
常见问题:首次烧录时 OpenOCD 报错
Error: init mode failed (unable to connect to the target)。90% 是因为:
- ST-Link 固件过旧:用 ST-Link Utility 升级到 V2.J37.S7;
- SWD 线接触不良:换一根杜邦线,或用焊锡加固探针引脚;
- 开发板 BOOT0 引脚未接地:确认
BOOT0=0(正常运行模式)。
4.4 调试技巧:不只是打断点
Cortex-Debug 提供远超传统 IDE 的调试能力:
- 寄存器视图:在调试面板点击
Registers,可实时查看R0-R12、SP、LR、PC,以及NVIC_ISER(中断使能寄存器); - 内存查看:在
Debug Console输入monitor mdw 0x40023800 4,查看 RCC 寄存器值(0x40023800是 RCC_BASE); - 外设寄存器映射:安装
Cortex-Debug后,VS Code 会自动加载stm32f407.svd(SVD 文件描述外设寄存器),在Peripherals视图中展开RCC,点击CR寄存器即可看到每位的含义。
我曾用这个功能快速定位一个 SPI 通信失败问题:在Peripherals中查看SPI1->SR寄存器,发现BSY(忙标志)一直为 1,而OVR(溢出标志)为 0,说明是时钟没配好,而非数据错误——这比用逻辑分析仪抓波形快十倍。
5. 常见问题排查与实战避坑指南
5.1 编译错误高频场景与解决方案
| 错误现象 | 根本原因 | 解决方案 | 经验备注 |
|---|---|---|---|
error[E0463]: can't find crate for std | 忘记#![no_std]或Cargo.toml中未声明std = false | 在Cargo.toml的[profile.dev]下添加panic = "abort",并确保src/main.rs有#![no_std] | panic = "abort"是no_std必需,否则core::panic!会尝试调用std::process::abort |
undefined reference to 'memcpy' | arm-none-eabi-gcc版本过低,未提供memcpy实现 | 升级到arm-none-eabi-gcc 12.2+,或在Cargo.toml中添加compiler_builtins = { version = "0.1.84", features = ["mem"] } | GNU 工具链 12+ 自带memcpy,旧版本需compiler_builtinscrate 补充 |
error: linking with 'arm-none-eabi-gcc' failed | arm-none-eabi-gcc未加入PATH,或路径含空格 | 在终端执行which arm-none-eabi-gcc,确认路径;若含空格,重装到无空格路径(如/opt/gcc-arm) | Windows 用户注意:MinGW 的gcc不能替代arm-none-eabi-gcc,它们是不同目标架构 |
error: could not compile 'cortex-m-rt' | cortex-m-rt版本与cortex-m不匹配 | 查看cortex-m-rt的 GitHub README,选择与cortex-m主版本一致的版本(如cortex-m-rt 0.7对应cortex-m 0.7) | Rust crate 版本强耦合,0.6.x和0.7.x的#[entry]宏签名不同 |
5.2 调试连接失败的系统化排查
当Cortex-Debug启动失败,按以下顺序检查:
物理层:
- 用万用表测
SWDIO和SWCLK对地电阻,应为10kΩ左右(上拉电阻); - 检查
NRST引脚是否悬空,若悬空需外接 10kΩ 上拉电阻。
- 用万用表测
探针层:
# Linux/MacOS 查看 USB 设备 lsusb | grep -i st # Windows 在设备管理器中确认 "STMicroelectronics ST-LINK/V2"OpenOCD 层:
# 手动启动 OpenOCD,观察详细日志 openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c "init" -c "targets"正常输出应包含
TargetName: stm32f4x和State: halted。VS Code 层:
- 检查
.vscode/launch.json中configFiles路径是否正确(相对路径以工作区根目录为基准); - 确认
executable字段指向的 ELF 文件存在且可读。
- 检查
实操心得:我遇到过一次
OpenOCD连接成功但Cortex-Debug无法通信的问题,最终发现是 VS Code 的Cortex-Debug插件版本(0.4.12)与 OpenOCD v0.12.0 不兼容,降级到 v0.4.10 后解决。插件版本必须与 OpenOCD 版本匹配,这个信息在Cortex-Debug的 GitHub Issues 中有详细记录。
5.3 性能优化关键点:让 Rust 跑得比 C 更快
Rust 的零成本抽象不是口号,是实打实的性能优势:
- 编译期计算:
cortex-m的delay函数接受u32参数,但clocks.sysclk().0 / 2在编译期就被计算为常量84_000_000,生成的汇编就是mov r0, #84000000,没有运行时除法开销; - 内联优化:
gpioa.pa5.into_push_pull_output()被完全内联,最终生成的GPIOA->ODR |= 1 << 5指令,和手写 C 一样高效; - 死代码消除:如果
main()中从未调用serial.write(),stm32f4xx-hal的串口驱动代码不会被链接进最终固件。
验证方法:比较相同功能的 C 和 Rust 固件大小:
# C 版本(Keil MDK) arm-none-eabi-size firmware.axf # text data bss dec hex filename # 12456 128 2048 14632 3928 firmware.axf # Rust 版本 arm-none-eabi-size target/thumbv7em-none-eabihf/debug/hello-stm32 # section size addr # .vector_table 512 134217728 # .text 11240 134218240 # .rodata 40 134229480 # .data 128 536870912 # .bss 256 536871040 # Total 12176Rust 版本.text段(代码)比 C 小 1216 字节,因为cortex-m-rt的启动代码比 Keil 的startup_stm32f407.s更精简。
5.4 从入门到进阶:下一步该学什么?
搭建完环境只是起点。根据我的项目经验,建议按此路径深化:
- 外设驱动实践:用
stm32f4xx-hal实现 UART 回环测试,重点理解Serial::new()的Tx/Rx类型分离——发送和接收是独立的资源,编译器会阻止你同时持有两者,这天然避免了中断冲突; - 中断处理:编写
EXTI0外部中断,捕获按键按下。关键点是cortex_m::interrupt::free()闭包,它确保临界区代码不被中断打断; - DMA 集成:用
stm32f4xx-hal的Dma1Channel4实现 ADC 采样,对比轮询方式的 CPU 占用率; - RTOS 尝试:引入
cortex-m-semihosting和heapless,在 Rust 中跑起FreeRTOS的轻量级替代品rtic(Real-Time Interrupt-driven Concurrency)。
最后分享一个小技巧:在
Cargo.toml中添加profile.dev.debug = true,这样cargo build生成的 ELF 文件包含完整调试信息,Cortex-Debug可以单步执行到每一行 Rust 代码,而不是跳转到汇编。很多新手以为 Rust 调试不如 C 直观,其实是没开启调试符号。
这套环境我用了三年,从医疗监护仪到工业 PLC,它让我把更多时间花在解决业务问题上,而不是和工具链搏斗。当你第一次看到 VS Code 的调试器停在led.set_high()这行,并在寄存器窗口里亲眼看到GPIOA->ODR的第 5 位变成 1,那种掌控硬件的踏实感,是任何高级框架都无法替代的。