1. 先别急着写代码,聊聊这套工具链到底在折腾什么
“看了三篇了,一行都没让我写呢”——这句话我太熟了。几乎每个从Keil或者IAR转过来的STM32开发者,第一次接触这套现代嵌入式C++工具链的时候,都会发出同样的灵魂拷问。前三篇文章大概率在讲环境搭建、工具链选型、CMake配置、VSCode插件安装这些东西,确实一行用户代码都没碰。但我要说的是,这三篇的内容恰恰是整个项目里最容易被低估、也最容易劝退人的部分。
这套方案的核心思路其实很明确:用现代软件工程的方式来做嵌入式开发。传统STM32开发是什么流程?装Keil或者IAR,新建工程,选芯片型号,勾选外设库,然后在一个封闭的IDE里写代码、编译、下载、调试。这套流程能用,但问题也很明显——工程文件是IDE私有的,换一个工具就打不开;依赖管理基本靠手动拷贝;代码补全和重构能力停留在十年前的水平;版本控制里塞满了各种二进制工程文件。
而基于STM32的嵌入式C++编程这套方案,做的事情是把嵌入式开发拉进现代工具链的生态里。用CMake管理构建流程,用VSCode做编辑器,用ARM GCC做编译器,用OpenOCD或者ST-Link Utility做下载调试,用Renode做仿真验证。每一环都是独立可替换的,工程文件是纯文本的,代码补全和静态分析能力直接拉满。
那为什么前三篇不让你写代码?因为这套工具链的搭建本身就是一个完整的工程项目。你得先理解CMake的构建逻辑,知道交叉编译工具链怎么配置,搞清楚VSCode的C/C++插件和CMake Tools插件怎么协同工作,还要把调试器和仿真器接进来。这些东西不打通,你写再多代码也编译不过、下载不了、调试不了。
我个人的经验是,这套环境搭建的时间投入大概在2到4个小时,取决于你对CMake和VSCode的熟悉程度。但一旦搭好,后面所有项目的复用成本几乎为零。你新建一个STM32项目,只需要复制一份CMakeLists.txt,改一下芯片型号和源文件列表,就能直接开工。这个效率提升在长期来看是巨大的。
这篇文章的目标很明确:把前三篇没讲透的“为什么”补上,把工具链搭建过程中最容易踩的坑挖出来,然后给你一套可以直接抄作业的完整配置。不管你是刚接触STM32的新手,还是从Keil转过来的老鸟,都能在这篇文章里找到能直接用的东西。
2. 工具链选型背后的逻辑:为什么是CMake + VSCode + GCC
2.1 为什么不用Keil或者IAR了
Keil和IAR在STM32开发领域的地位不用多说,几乎是最主流的选择。但它们的问题在于封闭性。Keil的工程文件是.uvprojx格式,IAR是.ewp格式,都是私有格式,离开这个IDE就没法用。你想在CI/CD流水线里自动编译?基本不可能。你想用Git做代码审查?工程文件的diff根本没法看。你想用现代的代码补全和重构工具?Keil的编辑器能力还停留在Notepad++的水平。
还有一个很现实的问题:License。Keil MDK的社区版有代码大小限制,商业版价格不菲。IAR更贵。对于个人开发者和小团队来说,这是一笔不小的成本。而ARM GCC是完全开源的,CMake是开源的,VSCode是免费的,整套工具链的软件成本为零。
当然,Keil和IAR也有它们的优势,比如对STM32全系列芯片的支持非常完善,调试器的集成度很高,上手门槛低。如果你只是做一个简单的课程设计,用Keil确实更快。但如果你想长期在嵌入式领域发展,想接触更复杂的项目结构和团队协作流程,现代工具链是绕不过去的。
2.2 CMake在嵌入式项目里到底解决了什么问题
很多人第一次看到CMake的时候会问:我不是已经有Makefile了吗,为什么还要再套一层CMake?这个问题问得好。Makefile确实能完成构建任务,但它的跨平台能力很差,语法也不够直观。CMake的核心价值在于它是构建系统生成器,不是构建系统本身。你写一份CMakeLists.txt,它可以在Linux上生成Makefile,在Windows上生成Visual Studio工程,在macOS上生成Xcode工程。对于嵌入式开发来说,这意味着你的工程配置可以在不同操作系统之间无缝迁移。
更重要的是,CMake有完善的依赖管理机制。你可以用find_package来查找系统库,用add_subdirectory来组织子模块,用target_link_libraries来管理链接关系。这些能力在传统的嵌入式Makefile里实现起来非常繁琐。
对于STM32项目来说,CMake还有一个很实际的好处:它可以很方便地集成STM32CubeMX生成的代码。CubeMX可以生成CMake工程,你只需要在它的基础上添加自己的源文件和编译选项就行。
2.3 VSCode + CMake Tools + Cortex-Debug的组合拳
VSCode本身只是一个编辑器,但它的插件生态让它变成了一个完整的开发环境。在嵌入式开发场景下,有三个插件是必装的:
- CMake Tools:提供CMake工程的配置、构建、调试集成。安装之后,VSCode底部状态栏会出现Configure按钮,点击它会让你选择编译器工具链。
- C/C++:提供代码补全、跳转、静态分析。需要配置c_cpp_properties.json来指定头文件路径和编译器路径。
- Cortex-Debug:提供ARM Cortex-M的调试支持,配合OpenOCD或者J-Link使用。
这里有一个很多人会踩的坑:CMake Tools插件安装之后,底部状态栏没有出现Configure按钮。这通常是因为你的工作区根目录下没有CMakeLists.txt文件,或者CMakeLists.txt的路径不在VSCode的搜索范围内。解决办法是确保你用VSCode打开的是包含CMakeLists.txt的文件夹,而不是单个文件。
另一个常见问题是C/C++插件的IntelliSense不工作,头文件下面全是红色波浪线。这通常是因为c_cpp_properties.json里的includePath没有配置正确。你需要把STM32的HAL库路径、CMSIS路径、C++标准库路径都加进去。一个比较省事的做法是让CMake Tools自动生成compile_commands.json,然后在c_cpp_properties.json里把compileCommands指向这个文件。
2.4 Renode在开发流程里的位置
Renode是一个开源的仿真框架,可以模拟多种嵌入式平台,包括STM32F103。它的价值在于:你不需要真实的硬件就能跑代码。这对于没有开发板的学生、需要做自动化测试的团队、或者想快速验证算法逻辑的开发者来说非常有用。
Renode的安装很简单,官网下载安装包就行。使用的时候需要写一个.resc脚本,描述平台的配置和加载的固件。比如模拟STM32F103的话,你需要指定CPU型号、内存布局、外设地址映射这些信息。Renode自带了很多平台的配置文件,可以直接用。
不过Renode也不是万能的。它的外设模拟精度有限,有些复杂的时序行为可能和真实硬件有差异。所以我的建议是:用Renode做逻辑验证和单元测试,用真实硬件做最终验证。两者结合使用,效率最高。
3. 从零搭建STM32 C++开发环境的完整实操
3.1 工具链安装清单与版本选择
先把需要装的东西列清楚,然后一个个说安装要点。
| 工具 | 推荐版本 | 下载方式 | 备注 |
|---|---|---|---|
| ARM GNU Toolchain | 10.3-2021.10 | ARM官网 | 选arm-none-eabi版本 |
| CMake | 3.22以上 | cmake.org | Windows选安装包,Linux用包管理器 |
| Ninja | 1.10以上 | GitHub Release | 比Make更快的构建工具 |
| OpenOCD | 0.12.0 | 官网或包管理器 | 用于下载和调试 |
| ST-Link Utility | 最新版 | ST官网 | 用于固件烧录 |
| VSCode | 最新版 | 官网 | 编辑器 |
| STM32CubeMX | 最新版 | ST官网 | 用于生成初始化代码 |
| Renode | 1.14以上 | Renode官网 | 仿真验证 |
ARM GNU Toolchain的安装要注意一点:Windows下安装的时候,安装程序会问你要不要添加到PATH,一定要勾选。Linux下解压之后需要手动把bin目录加到PATH里。安装完成后在终端里执行arm-none-eabi-gcc --version,能输出版本号就说明装好了。
CMake在Windows下安装的时候,建议选择“Add CMake to the system PATH for all users”,这样在VSCode的终端里就能直接用cmake命令。Linux下用sudo apt install cmake就行,但要注意Ubuntu 20.04自带的CMake版本可能偏低,建议从官网下载最新版。
Ninja是一个小型的构建工具,速度比Make快很多。Windows下下载ninja.exe放到某个目录,然后把这个目录加到PATH里就行。Linux下sudo apt install ninja-build。
3.2 CMakeLists.txt的完整配置与逐行解读
这是整个项目的核心配置文件。我把它分成几个部分来讲,每一部分都解释为什么这么写。
cmake_minimum_required(VERSION 3.22) project(stm32_cpp_demo CXX C ASM) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 11) set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm)第一行指定CMake的最低版本要求。3.22是一个比较新的版本,支持了很多现代CMake的特性。project命令声明项目名称和使用的语言,这里用了C++、C和汇编,因为STM32的启动文件是汇编写的。
CMAKE_CXX_STANDARD 17指定使用C++17标准。为什么选17而不是11或者14?因为C++17引入了很多对嵌入式开发有用的特性,比如if constexpr、结构化绑定、std::optional这些。当然,如果你的编译器版本比较老,可以降到14或者11。
CMAKE_SYSTEM_NAME Generic和CMAKE_SYSTEM_PROCESSOR arm这两行是告诉CMake这是一个交叉编译项目,目标平台是ARM。不设置这两个变量的话,CMake会尝试用主机的编译器来编译,那就完全跑偏了。
set(TOOLCHAIN_PREFIX arm-none-eabi-) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size)这里指定了交叉编译工具链的各个工具。arm-none-eabi-是前缀,后面跟上具体的工具名。注意ASM编译器用的也是gcc,因为GCC可以处理汇编文件。
set(CPU_FLAGS "-mcpu=cortex-m3 -mthumb -mfloat-abi=soft") set(CMAKE_C_FLAGS "${CPU_FLAGS} -Wall -Wextra -Og -g3") set(CMAKE_CXX_FLAGS "${CPU_FLAGS} -Wall -Wextra -Og -g3 -fno-exceptions -fno-rtti") set(CMAKE_ASM_FLAGS "${CPU_FLAGS} -x assembler-with-cpp") set(CMAKE_EXE_LINKER_FLAGS "${CPU_FLAGS} -T${CMAKE_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld -Wl,--gc-sections -specs=nano.specs -specs=nosys.specs")这部分是编译选项的核心。-mcpu=cortex-m3指定CPU架构,STM32F103是Cortex-M3内核。-mthumb启用Thumb指令集。-mfloat-abi=soft指定浮点ABI为软件浮点,因为F103没有硬件浮点单元。
C++的编译选项里加了-fno-exceptions和-fno-rtti。这两个选项在嵌入式开发里很重要,因为异常处理和运行时类型信息会显著增加代码体积和运行时开销。嵌入式系统通常资源受限,关掉这两个特性是常见做法。
链接选项里的-specs=nano.specs使用newlib-nano,这是一个精简版的C标准库,适合嵌入式使用。-specs=nosys.specs告诉链接器不要链接系统调用相关的代码,因为裸机环境没有操作系统。-Wl,--gc-sections启用垃圾回收,把没有用到的代码段和数据段从最终固件里移除,能显著减小固件体积。
include_directories( ${CMAKE_SOURCE_DIR}/Core/Inc ${CMAKE_SOURCE_DIR}/Drivers/STM32F1xx_HAL_Driver/Inc ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F1xx/Include ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include )头文件路径的配置。Core/Inc是你自己写的头文件,后面三个是STM32 HAL库和CMSIS的头文件路径。这些路径是STM32CubeMX生成的标准目录结构。
file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.cpp" "Drivers/STM32F1xx_HAL_Driver/Src/*.c" "startup_stm32f103xb.s" )用file(GLOB_RECURSE)来自动收集源文件。这样你新增源文件的时候不需要手动修改CMakeLists.txt,重新运行CMake就会自动包含进来。不过要注意,GLOB_RECURSE不会自动检测文件变化,你需要手动重新运行CMake配置。
add_executable(${PROJECT_NAME}.elf ${SOURCES}) target_link_libraries(${PROJECT_NAME}.elf)创建可执行文件目标,并链接必要的库。这里没有额外链接库,因为HAL库的源文件已经直接编译进去了。
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.hex COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.bin COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${PROJECT_NAME}.elf> )编译完成后自动生成hex和bin文件,并打印固件大小信息。这个CMAKE_SIZE的输出会告诉你flash和RAM的使用情况,对于资源受限的STM32项目来说非常有用。
3.3 VSCode配置文件的正确写法
VSCode需要三个配置文件:c_cpp_properties.json、launch.json和tasks.json。这些文件放在.vscode目录下。
c_cpp_properties.json的配置:
{ "configurations": [ { "name": "STM32", "includePath": [ "${workspaceFolder}/Core/Inc", "${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc", "${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32F1xx/Include", "${workspaceFolder}/Drivers/CMSIS/Include" ], "defines": [ "USE_HAL_DRIVER", "STM32F103xB" ], "compilerPath": "C:/arm-gnu-toolchain/bin/arm-none-eabi-gcc.exe", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "gcc-arm", "compileCommands": "${workspaceFolder}/build/compile_commands.json" } ], "version": 4 }关键点是compileCommands这一行。它指向CMake生成的compile_commands.json文件,这样IntelliSense就能获得和实际编译完全一致的宏定义和头文件路径。compilerPath要改成你实际的工具链路径。
launch.json的配置(用于OpenOCD调试):
{ "version": "0.2.0", "configurations": [ { "name": "OpenOCD Debug", "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceFolder}", "executable": "${workspaceFolder}/build/stm32_cpp_demo.elf", "device": "STM32F103C8", "configFiles": [ "interface/stlink.cfg", "target/stm32f1x.cfg" ], "svdFile": "${workspaceFolder}/STM32F103.svd" } ] }svdFile指向STM32的SVD文件,这个文件描述了所有外设寄存器的地址和位定义。有了它,你在调试的时候就能在VSCode里直接查看外设寄存器的值,非常方便。SVD文件可以从ST官网或者Keil的安装目录里找到。
3.4 第一个C++程序的编写与编译验证
环境搭好之后,写一个最简单的程序验证一下。在Core/Src目录下新建main.cpp:
#include "stm32f1xx_hal.h" extern "C" void SystemClock_Config(void); extern "C" void MX_GPIO_Init(void); class LedBlinker { private: GPIO_TypeDef* port; uint16_t pin; uint32_t lastToggle; uint32_t interval; public: LedBlinker(GPIO_TypeDef* p, uint16_t pinNum, uint32_t ms) : port(p), pin(pinNum), lastToggle(0), interval(ms) {} void update(uint32_t currentTick) { if (currentTick - lastToggle >= interval) { HAL_GPIO_TogglePin(port, pin); lastToggle = currentTick; } } }; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); LedBlinker led(GPIOC, GPIO_PIN_13, 500); while (1) { led.update(HAL_GetTick()); } }这个程序用C++的类封装了LED闪烁的逻辑。LedBlinker类把端口、引脚、间隔时间封装在一起,update方法根据当前tick判断是否需要翻转LED。这种写法比传统的全局变量加if判断要清晰得多,而且可以很方便地创建多个LED实例。
编译的时候在VSCode里按F7,或者在终端里执行:
mkdir build && cd build cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug .. ninja如果一切正常,你会看到编译输出,最后打印出固件大小信息。如果有报错,最常见的几个问题是:工具链路径不对、头文件路径缺失、链接脚本路径错误。根据报错信息逐个排查就行。
4. 实操过程中最容易踩的坑与排查方法
4.1 CMake配置阶段的典型报错
报错一:CMake Error at .../CMakeDetermineCompilerId.cmake:9
这个报错通常出现在CMake第一次配置的时候,原因是CMake无法确定编译器的ID。根本原因一般是交叉编译工具链没有正确设置,或者CMAKE_SYSTEM_NAME没有设为Generic。解决办法是检查CMakeLists.txt里是否设置了set(CMAKE_SYSTEM_NAME Generic),以及CMAKE_C_COMPILER是否指向了正确的arm-none-eabi-gcc路径。
报错二:CMake Error at .../Qt5Config.cmake
这个报错和STM32项目本身没关系,通常是因为你的系统里装了Qt,CMake在搜索包的时候误找到了Qt的配置文件。解决办法是在CMakeLists.txt里明确指定find_package的搜索路径,或者用set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)来限制搜索范围。
报错三:No CMAKE_CXX_COMPILER could be found
这个报错说明CMake找不到C++编译器。检查CMAKE_CXX_COMPILER是否设置正确,以及工具链的bin目录是否在PATH里。Windows下还有一个常见原因是路径里有空格,比如Program Files,这种情况需要用短路径或者把工具链移到没有空格的目录下。
4.2 编译链接阶段的常见问题
问题一:undefined reference to _sbrk或者_write
这是链接newlib的时候缺少系统调用实现导致的。解决办法是在链接选项里加上-specs=nosys.specs,或者自己实现_sbrk、_write、_read这些系统调用桩函数。
问题二:固件体积过大,超出Flash容量
STM32F103C8的Flash只有64KB,如果用了C++的异常处理和RTTI,很容易超。解决办法是加上-fno-exceptions -fno-rtti编译选项,链接时加上-Wl,--gc-sections,并且用-Os优化等级来编译。如果还是超,可以考虑用newlib-nano,也就是-specs=nano.specs。
问题三:程序下载后不运行
检查启动文件是否正确包含在源文件列表里,链接脚本里的Flash起始地址和长度是否和芯片匹配,以及中断向量表是否放在了正确的位置。STM32F103C8的Flash起始地址是0x08000000,长度是0x10000。
4.3 Renode仿真STM32F103的配置要点
Renode模拟STM32F103需要写一个resc脚本。最基本的配置如下:
mach create "stm32f103" machine LoadPlatformDescription @platforms/cpus/stm32f103.repl sysbus LoadELF @build/stm32_cpp_demo.elf showAnalyzer sysbus.uart1 startLoadPlatformDescription加载平台描述文件,Renode自带了很多STM32平台的描述文件。LoadELF加载编译好的elf文件。showAnalyzer打开串口分析器,可以看到串口输出。start启动仿真。
Renode的一个常见问题是外设模拟不完整。比如某些定时器功能、DMA传输、ADC采样这些可能和真实硬件有差异。所以Renode适合做逻辑验证,不适合做时序敏感的验证。我的做法是:在Renode里跑通基本逻辑,然后用真实硬件做最终测试。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| CMake配置失败 | 工具链路径错误 | 检查CMAKE_C_COMPILER | 设置正确的工具链路径 |
| 编译报错找不到头文件 | include路径缺失 | 检查include_directories | 添加缺失的头文件路径 |
| 链接报错undefined reference | 源文件未包含 | 检查SOURCES变量 | 添加缺失的源文件 |
| 固件体积过大 | 异常/RTTI未关闭 | 查看size输出 | 加-fno-exceptions -fno-rtti |
| 下载后不运行 | 启动文件或链接脚本错误 | 检查向量表和地址 | 修正链接脚本 |
| IntelliSense不工作 | compile_commands.json缺失 | 检查c_cpp_properties.json | 设置compileCommands路径 |
| Renode仿真卡住 | 平台描述文件不匹配 | 检查resc脚本 | 使用正确的平台描述 |
5. 一些让我少走弯路的实操心得
5.1 关于C++在STM32上的使用边界
C++在嵌入式开发里能用,但要有选择地用。我个人的原则是:用C++的封装能力,但不用它的运行时特性。具体来说,类、模板、命名空间、构造函数这些零开销或者低开销的特性可以放心用。但异常、RTTI、动态多态(虚函数)这些要谨慎,因为它们会带来额外的运行时开销和代码体积。
一个很实用的技巧是用C++的模板来做编译期计算和类型安全的寄存器操作。比如你可以写一个Register<Address>模板类,在编译期就确定寄存器地址,运行时没有任何额外开销。这种用法在嵌入式C++社区里很常见,效果也很好。
5.2 调试技巧:用OpenOCD + GDB做printf调试
在没有串口或者串口被占用的情况下,可以用OpenOCD + GDB的printf功能来做调试输出。具体做法是在OpenOCD的配置里启用rtt或者semihosting,然后在代码里用printf输出调试信息。这些信息会通过调试器传到GDB的控制台,不需要额外的串口硬件。
不过semihosting会影响程序的实时性,因为每次printf都会触发一个断点。所以只建议在调试阶段用,正式发布的时候要去掉。
5.3 工程结构组织建议
一个清晰的项目结构能省很多事。我通常这样组织:
project/ ├── Core/ │ ├── Inc/ # 用户头文件 │ └── Src/ # 用户源文件 ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── Middlewares/ # 中间件(FreeRTOS、FatFS等) ├── build/ # 构建输出目录 ├── .vscode/ # VSCode配置 ├── CMakeLists.txt └── STM32F103C8Tx_FLASH.ldbuild目录不要提交到Git,在.gitignore里加上build/就行。.vscode目录建议提交,这样团队成员的配置能保持一致。
5.4 关于学习路线的建议
如果你刚开始接触嵌入式,我的建议是先搞定C语言和STM32的基础外设(GPIO、UART、定时器、中断),然后再上这套现代工具链。工具链本身不复杂,但如果你对编译链接的过程没有概念,出了问题会很难排查。
另外,不要一上来就追求大而全的框架。先把一个LED闪烁的程序跑通,然后逐步加功能。每加一个功能就验证一次,这样出了问题容易定位。我见过太多人一上来就搭一个包含FreeRTOS、FatFS、LWIP的复杂工程,结果编译都过不了,然后就开始怀疑人生。
5.5 关于Renode自己造内核REPL的补充
Renode的REPL(交互式命令行)是可以自己扩展的。你可以在Renode的脚本里用Python写自定义命令,然后通过REPL调用。具体做法是继承Monitor类,注册自定义命令,然后在resc脚本里加载。这个功能对于做自动化测试很有用,你可以写一个脚本自动加载固件、运行一段时间、检查串口输出、然后退出。不过这个属于进阶用法,新手先把基本的仿真跑通再说。
5.6 最后分享一个CMake的小技巧
如果你觉得每次改CMakeLists.txt都要重新运行CMake很麻烦,可以在VSCode的设置里开启cmake.configureOnEdit,这样保存CMakeLists.txt的时候会自动重新配置。另外,cmake.buildBeforeRun设为true的话,按F5调试之前会自动编译,省得你手动编译。
还有一个很实用的命令:cmake --build build --target clean可以清理构建产物。有时候编译出现莫名其妙的错误,清理一下重新编译就好了。这个操作在嵌入式开发里出现的频率不低,因为工具链的依赖关系有时候会出问题。
我个人在实际操作中的体会是,这套工具链的学习曲线在前两天比较陡,但一旦跨过去,后面的开发效率会比传统IDE高很多。尤其是当你需要管理多个项目、和团队协作、或者做自动化测试的时候,这套方案的优势会非常明显。