Vscode + PlatformIO 开发 STM32:从环境搭建到串口调试全指南
用 Keil 写 STM32 的老哥估计都经历过这些工程模板配到怀疑人生、代码补全约等于没有、串口调试全靠串口助手反复拔线、想用个 Git 管理代码还得小心翼翼不把 Keil 的临时文件提交上去。最开始我也是这么过来的直到换了 Vscode PlatformIO 这套组合才感觉 STM32 开发总算有点现代 IDE 的样子了。这篇文章就把我从环境搭建到串口调试的完整过程以及踩过的各种坑一次性说清楚。这个方案到底强在哪首先是项目管理清爽了PlatformIO 用 platformio.ini 一个文件搞定板卡、框架、烧录器配置工程迁移特别方便其次是代码体验确实好Vscode 的补全、跳转、格式化、Git 集成都是碾压级体验最重要的一点是跨平台Windows、Linux、macOS 下同一套流程比如在 Linux 服务器上跑个 Docker 顺便编固件这套流程也能无缝衔接。不管你是刚买了一块 STM32 最小系统板的入门玩家还是被 Keil 模板和编译速度折磨的进阶选手这篇文章都能帮你少走很多弯路。1. Vscode PlatformIO 环境搭建与工程创建1.1 Vscode 安装没有太多花活但有三个细节值得注意Vscode 的安装本身没什么幺蛾子官网下载安装包一路下一步就行。但有几个细节直接影响后面的体验。第一点是建议在安装时勾选“添加到 PATH”和“将 Code 注册为受支持文件的编辑器”前者方便后面在终端里直接敲code .打开项目后者解决双击工程文件时系统不知道用什么打开的问题。第二点是扩展插件的安装。Vscode 市场里的插件质量参差不齐但 PlatformIO IDE 这个插件是核心直接在插件市场搜索 “PlatformIO IDE” 安装即可。安装过程需要下载 PlatformIO Core这一步在国内网络环境下经常处于“转圈圈”的状态很多人的第一道坎就在这里。第三点是我个人的习惯建议在设置里把files.eol设置为\n把editor.renderWhitespace设置为all。前者避免在 Windows 上代码行尾混入\r\n导致编译告警或 Git 差异混乱后者让你一眼看出每行是不是多了多余空格。这些细节在后期排查问题时会省不少时间。1.2 PlatformIO 创建工程慢这是第一次用几乎必踩的坑装好插件后新建工程时选完板卡和框架你会发现进度条卡在 “Downloading Platform ...” 这一步一动不动等个十几分钟算正常的偶尔直接失败退出。这不是你的操作有问题而是 PlatformIO 在拉取平台包和工具链时访问的是国外的 CDN网络稍不稳定就会卡死。解决办法是给 PlatformIO 配置国内镜像源。在用户目录下找到.platformio文件夹里面有个platformio.ini配置文件添加下面的内容[platformio] packages_dir C:/.platformio/packages [env] platform_packages platformio/framework-stm32cubef1^1.8.0在platformio.ini中加上这一行核心配置[platformio] registry_url https://pio.registry.kulesz.pl实际上 PlatformIO 在 6.x 版本以后镜像配置方式变成了在用户目录的.platformio目录下放一个platformio.ini里面写上[platformio] registry_url https://kulesz.pl但我这里要补充一点不同版本的 PlatformIO 配置方式略有差异最简单的办法其实是“多试几次”。反正 PlatformIO 支持断点续传多触发几次下载通常能把它磨下来。另外一个有效的办法是开代理但代理这块我们就不展开讲了自己体会。1.3 板卡和框架怎么选STM32F103C8T6 的标准答案新建工程时Boards 搜索框里输入 STM32F103C8T6能看到很多相似选项比如BluePill F103C8、STM32F103C8、generic STM32F103C8T6等。很多人在这里开始纠结导致选错板卡后面编译出来的代码烧进去跑不起来。我的建议是选generic STM32F103C8T6。原因很简单这个板卡定义是最通用的它对应的是芯片本身而不是某个商家的开发板。如果你选了某个品牌的开发板定义可能会有厂商自定义的 LED 引脚、自定义时钟配置等反而不通用。板上带的 LED 引脚每个板子不同自己用ld脚本或代码定义就好。Framework 这里有两个选项STM32Cube和Arduino。如果你之前用过 HAL 库选STM32Cube用起来和 STM32CubeMX 生成的代码风格一致。如果你只想要上手快、最快看到点灯效果选ArduinoPlatformIO 会帮你自动处理底层初始化但后面写稍微复杂一点的工程时会觉得被框架束缚住了。这篇文章我们以STM32Cube为主这也是绝大多数转过来的老铁实际使用的框架。选完之后PlatformIO 会生成一个基础的工程结构。默认目录如下src/main.cpp—— 主程序文件platformio.ini—— 工程配置文件include/—— 用户头文件目录lib/—— 用户库目录.vscode/—— Vscode 自动生成的配置1.4 platformio.ini 配置模板整个工程的核心就是platformio.ini我贴一份实测没毛病的配置[env:genericSTM32F103C8] platform ststm32 board genericSTM32F103C8 framework stm32cube upload_protocol stlink monitor_speed 115200 build_flags -D HSE_VALUE8000000 -D VDD_VALUE3300这里解释一下关键项。platform ststm32代表使用意法半导体官方平台包。board指定板卡。upload_protocol stlink指定烧录用 ST-Link这是最常用也最便宜的调试器。monitor_speed设置串口监视器波特率如果你后面用串口调试这一步千万别忘了两边不一致读出来的全是乱码。build_flags里的HSE_VALUE是一个隐藏大坑后面我会专门讲。很多人的串口乱码、定时器时间不对都是这个值不正确导致的。2. 点灯工程GPIO 输出背后的原理2.1 时钟是个大前提点灯看着简单但 Step 1 往往是“系统跑不起来”或者“下载不进去”。写点灯代码之前我建议你先理清这个逻辑STM32 内部几乎所有外设都要在时钟开启之后才能工作。GPIO 属于挂在 APB 总线上的外设所以要先用__HAL_RCC_GPIOB_CLK_ENABLE()之类的方式打开对应 GPIO 端口的时钟。PlatformIO 用 STM32Cube 框架时系统时钟树的初始化会在SystemClock_Config()函数里完成。比如 F103 默认外部晶振是 8MHz通过 PLL 倍频到 72MHz 作为系统主频。所以在主函数最开始你要调用HAL_Init(); SystemClock_Config();这两行不写后面的点灯代码烧进去基本没反应。2.2 点灯代码直接可抄的版本在src/main.cpp里写测试代码#include stm32f1xx_hal.h void SystemClock_Config(void); int main(void) { HAL_Init(); SystemClock_Config(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); while (1) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1); HAL_Delay(500); } }注意引脚号这里我写的是GPIOB, GPIO_PIN_1对应的是我手上的最小系统板载 LED 所接的 PB1。你的板子接哪个引脚要查你自己的原理图。这是新手最容易想当然的地方以为板载 LED 一定是 PC13结果烧上去没反应折腾半天还以为是代码问题。2.3 为什么 SystemClock_Config 里的 HSE_VALUE 会导致串口乱码和延时不准这是我踩过最深的坑之一。F103 的 HAL 库在计算系统时钟时会用到HSE_VALUE这个宏它默认值是 8MHz。如果你的板子上焊的是 8MHz 晶振恭喜你没问题。但现在市面上很多 STM32F103 核心板用的是 8MHz 晶振但也有相当一部分用 12MHz 晶振还有一些模块用的是内部 HSI 时钟。如果你板子上实际是 8MHz 晶振但platformio.ini里没有定义HSE_VALUE默认就是 8MHz一切正常。如果板子实际是 12MHz 晶振而HSE_VALUE仍旧是默认的 8MHzHAL 计算 PLL 分频倍频参数时就错了主频不是你以为的 72MHz而是 48MHz 或者别的值。后果就是 HAL_Delay 的时间不对、串口波特率对不上因为波特率是按照实际时钟算的、定时器溢出不準。所以配置里我写了build_flags -D HSE_VALUE8000000这块一定要根据你自己的晶振频率调整。不确定的话可以先接上 ST-Link 用 STM32CubeProgrammer 读一下芯片的 RCC 配置或者直接用示波器量 MCO 引脚输出。最笨的办法是先用默认值编译点灯测一下 HAL_Delay(1000) 实际间隔是不是一秒不对再调整。2.4 烧录失败的几个常见原因代码写好了编译通过结果烧录时报Cannot connect to target或者No ST-Link detected这种问题我前后被折磨了一个多星期。排查方向大致如下优先检查 ST-Link 驱动。Windows 下 ST-Link 驱动装不上或者版本不对最常见现象是设备管理器里能看到一个带感叹号的未知设备。解决方法是去意法半导体官网下载最新的 ST-Link 驱动注意别下载成 ST-Link Utility 的旧驱动。然后检查接线。ST-Link 那边虽然有防呆但很多淘宝的 ST-Link 是非官方版本引脚定义五花八门。我推荐至少接四根线SWDIO、SWCLK、GND、3.3V。部分山寨 ST-Link 的 SWDIO 和 SWCLK 丝印反了出现这种情况只能换线序测试。再检查目标板供电。部分核心板只靠 ST-Link 供电时如果板上还有其他外设模块耗电电压会跌落导致连接不稳定。这种情况下单独给板子接上一根 USB 供电多半就能正常烧录了。还有一点容易忽略的是upload_protocol配置。如果你手里是 J-Link那就要写upload_protocol jlink如果你用的又是串口 ISP 下载那用stlink肯定连不上。每次换调试器之前先检查platformio.ini里的协议是不是和硬件对得上。3. 串口调试printf 重定向与串口打印实战3.1 UART 初始化的基本套路串口调试的核心就两步第一步让 UART 外设工作起来第二步把 printf 的数据流重定向到 UART 发送接口。UART 的初始化代码和 GPIO 类似先开时钟再配置引脚复用最后配置 UART 参数。以最常用的 USART1 为例代码框架如下void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1); }同时需要注意 GPIO 复用配置PA9 作为 TXPA10 作为 RX__HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);注意GPIO_MODE_AF_PP这个配置F103 的 USART_TX 必须配成复用推挽输出RX 配成浮空输入或复用开漏不然收发都有问题。3.2 printf 重定向比 Keil 里省心但也没省到哪去Keil 里用微库可以轻松实现 printf 重定向PlatformIO 里 GCC 环境下做法不同。坑在于如果你直接裸写一个fputc就调用printf可能会遇到“编译过了但运行时硬 fault”的情况。原因在于 GCC 的printf默认实现会做一些内存分配相关的操作而 STM32 的默认堆栈设置可能不够用尤其是用 HAL 库之后。最稳妥的做法是#include stdio.h int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; } int fputc(int ch, FILE *f) { return __io_putchar(ch); }同时需要在platformio.ini的build_flags里加上build_flags -D HSE_VALUE8000000 -u _printf_float --specsnano.specs --specsnosys.specs--specsnano.specs是必须的它告诉链接器用精简版 C 库这个库对内存和栈的要求低很多。-u _printf_float是为了支持%f格式化浮点数不加的话打印浮点数会输出?或者不输出。--specsnosys.specs提供了一些系统级的 stub 函数避免链接器报_sbrk之类的未定义符号错误。如果这些配置都加了printf 还是不好使检查一下huart1这个全局变量是不是在你 printf 调用之前已经初始化完成。如果是在只定义未初始化的时候调用HAL_UART_Transmit 会卡死在超时等待里。3.3 用 PlatformIO 自带 Serial Monitor 还是用第三方串口助手PlatformIO 在 Vscode 底部有一个串口监视器按钮它本质上是调用了pio device monitor命令。好处是没有任何驱动安装的额外步骤而且和monitor_speed配置联动。点击底部小插头图标或者终端里执行pio device monitor就能看到串口输出。需要注意的是如果此时屏幕上有乱码大概率是波特率不匹配检查一下platformio.ini里的monitor_speed是不是和代码里huart1.Init.BaudRate一致。我个人的习惯是简单调试用 Vscode 内置的串口监视器采集数据、保存日志或需要发送特殊协议时就外接一个串口调试助手。市面上常用的有 SSCOM、XCOM、MobaXterm 自带串口等。选串口助手时要注意一点部分软件发送数据时会自动附加回车换行这个在调试 AT 指令或 Modbus 协议时会多出字节导致协议解析失败。建议先在一个文本编辑器里打两行十六进制数据看看发送效果再决定要不要用。3.4 串口乱码和丢数据的排查套路串口打印出错时先别急着换线换芯片按照这个顺序排查第一确认波特率。代码里的BaudRate和监视器里的波特率一致。这一点我和朋友联调时翻车过很多次很多时候就是一个人用了 9600另一个人用 115200输出全是乱码。第二确定晶振频率。这其实是乱码的隐藏元凶HSE_VALUE 不对UART 实际波特率偏差超出容忍范围115200 就变成 104857 这种奇怪值了。这就是为什么我在前面强调必须在 build_flags 里显式声明 HSE_VALUE。第三检查 TX/RX 交叉接线。串口通信要交叉连接开发板的 TX 接 USB 转 TTL 的 RX开发板的 RX 接 USB 转 TTL 的 TX。同向连接是新手最常见的接线错误之一。第四排除供电问题。USB 转 TTL 的 3.3V 和外部电源同时供电时如果两边电压有微小差异可能导致部分电平长期处于不确定状态。用万用表量一下开发板供电电压 3.3V 是否稳定。4. PlatformIO 常见编译问题和稳定性优化4.1 编译慢PlatformIO 的编译缓存够用吗第一次编译 STM32Cube 框架的工程会感觉像是在看 PPT。原因是 HAL 库编译量巨大CPU 和磁盘 IO 都被拉满。但从第二次开始PlatformIO 会启用编译缓存未修改的源文件不会重新编译速度会快很多。如果你觉得还是慢可以做两件事。第一件事是在platformio.ini里启用build_cache参数build_cache .pio/build_cache这个参数会启用共享缓存目录多个工程之间可以复用同一份编译产物。对于 STM32F103 这种 HAL 库是公共依赖的场景效果非常明显。第二件事是在 Vscode 里把 PlatformIO 的编译任务并行数调高这个在Advanced设置里找pio builder threads相关选项。另外尽量把工程放在 SSD 上.pio目录的 IO 压力很大。把工程直接放在机械硬盘上首编是很多人没有怀疑到的性能瓶颈。4.2 编译报错这些错误不用慌在 PlatformIO 从 Keil 迁移过来时常见编译报错基本都是 C 语言层面的习惯差异。比如 Keil 里默认支持#include stm32f1xx_hal.h直接引用但在 PlatformIO 中偶尔会报找不到文件这时检查 include 路径是否写对了。PlatformIO 的 include 搜索路径不会自动包含 Keil 里常见的那些自定义文件夹你可以通过build_flags -I lib/xxx手动添加。再比如Keil 的 ARMCC 编译器对 C 语言的语法检查相对宽松但 GCC 编译器会更严格。像函数声明缺失、隐式转换、结构体未初始化等在 Keil 里可能只是警告但在 GCC 下直接就是 error。解决办法就是按标准 C 语法来写函数先声明再使用结构体变量用{0}初始化。4.3 工程目录与版本管理建议我强烈建议一创建工程就把目录纳入 Git 管理。.pio目录和.vscode目录中有一部分是平台自动生成的不建议提交到版本控制中。需要提交的关键文件是platformio.ini、src/、include/、lib/和test/。在项目根目录创建.gitignore写入.pio/ .vscode/如果后续你用 STM32CubeMX 生成过初始化代码注意 CubeMX 的工程文件也和 PlatformIO 的工程结构不同建议 CubeMX 单独建一个目录把生成的 Core、Drivers 等拷贝到 PlatformIO 的 include 和 src 结构里并统一维护。这样后面升级 HAL 库或者换芯片型号会省很多事。4.4 从 PlatformIO 的配置层面避开的一堆小问题有人会把build_flags里的-D STM32F103xB写错。PlatformIO 的 ststm32 平台包会基于板卡定义自动添加正确的芯片宏但如果你手动覆盖了可能导致 HAL 库内部的某些条件编译分支出错常见的现象是#error Please select first the target STM32F1xx device used in your application。遇到这种错误先检查自己有没有在 build_flags 里乱加-D STM32F103xE之类的宏。除非你确实在做芯片型号迁移否则让 PlatformIO 自己管理即可。还有一个常见问题是framework stm32cube下默认生成的链接脚本是基于板卡定义自动选择的。如果你改用外部内存或者要自定义内存布局记得创建自定义.ld文件并写入board_build.ldscript。5. 实战经验完整跑通一个“点灯 串口打印”的例程5.1 一个完整的流程参考说了这么多原理和坑最后我分享一下我实际跑通的最小工程流程每一步都经过了反复验证。第一步创建工程板卡选generic STM32F103C8T6框架选stm32cube。第二步在platformio.ini里写入以下配置[env:genericSTM32F103C8] platform ststm32 board genericSTM32F103C8 framework stm32cube upload_protocol stlink monitor_speed 115200 build_flags -D HSE_VALUE8000000 --specsnano.specs --specsnosys.specs -u _printf_float第三步编写主程序实现 PB1 引脚 LED 翻转和 USART1 初始化后的 printf 输出。第四步用 ST-Link 连接 SWDIO、SWCLK、GND、3.3V点击底部工具栏的上传箭头。第五步编译成功后打开串口监视器看到板子周期性输出日志同时 LED 熄灭点亮交替。这套流程整个跑下来如果顺利大约十分钟以内就能完成。如果中间任何一步卡住请对照前面几章的排查点逐个检查。5.2 一个实用的日志输出技巧调试时我习惯封装一个简单的日志函数避免到处直接调用HAL_UART_Transmit#include stdarg.h #include stdio.h void log_printf(const char *fmt, ...) { char buf[256]; va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), HAL_MAX_DELAY); }这样写有个好处调试时可以直接控制开关。在代码里加一个编译宏发布版本时直接把整个函数体替换成空操作不用逐个删改调用点。5.3 串口接收中断的坑回调函数里及时搬运数据串口发送搞定了很多人开始研究串口接收。F103 的 HAL 库接收中断有几个容易犯错的地方。开启接收中断的接口是HAL_UART_Receive_IT(huart1, rx_buffer, 1);注意这是“接收单个字节后触发一次回调”的机制。第一次启动后如果不在回调函数里重新调用这个接口接收完一个字节之后后续数据就都不会再触发中断了。回调函数如下void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { received_data rx_buffer; HAL_UART_Receive_IT(huart1, rx_buffer, 1); } }很多人在回调函数里做了数据处理但忘了重新调用HAL_UART_Receive_IT导致的现象是“第一个字节能收到后面的全丢”。这个问题排查起来还挺隐蔽我一度以为是 DMA 配置出了问题。另外在回调函数里尽量不要做耗时操作比如调用HAL_Delay或者 printf 大量数据。中断回调里做重活会阻塞其他中断时序要求高的场景很容易翻车。正确做法是把数据放到环形缓冲区里主循环再统一处理。5.4 项目管理不要把所有代码堆在一个 main.cpp 里新手很容易把所有代码堆进src/main.cpp这在上手阶段问题不大但工程稍微一复杂就变成一锅粥。PlatformIO 本身建议的目录划分是驱动程序放lib/目录下的独立子文件夹每层驱动包含自己的.h和.c文件。比如写一个 OLED 驱动就新建lib/OLED/目录在下面放OLED.h和OLED.cpp。PlatformIO 会自动递归扫描lib/目录下的所有库并组织编译这么管理的好处是代码复用率高、模块边界清晰。到了后期维护阶段你只需要关注某个外设的驱动源码而不需要在全局文件里搜索一个函数的实现。6. STM32F103 之外的扩展PlatformIO 还能玩什么6.1 从 F103 到 ESP32、树莓派 PicoPlatformIO 的价值不只是 STM32 这一家。它的核心设计理念是“一套工具链多种平台”同一个 Vscode 窗口下你可以同时管理 STM32F103、ESP32、树莓派 Pico、Arduino UNO 等多个目标平台的工程。比如你在做 ROS 2 相关的机器人项目时经常会遇到需要同时开发 ESP32 和 STM32 的情况。STM32 负责电机闭环控制ESP32 负责网络通信和 ROS 2 节点。PlatformIO 可以在同一个工作区里管理这两个工程编译下载方式统一日志输出方式统一比来回切换 IDE 舒服太多。6.2 配合 Docker 进行命令行编译PlatformIO 提供完整的命令行接口意味着它可以在无图形界面的 Linux 服务器中通过 Docker 容器完成编译。在docker环境中使用microros和ros2 humble时经常需要在容器内部交叉编译 STM32 或 ESP32 的固件。PlatformIO 在这方面非常友好一条命令docker run -v $(pwd):/project -it platformio/platformio pio run就能把当前目录的 PlatformIO 工程丢到 Docker 容器里编译宿主机不用安装任何编译工具链。这在团队协作、CI/CD 场景下面特别好用也是我要提一嘴的原因。6.3 单元测试支持PlatformIO 原生支持嵌入式单元测试框架 Unity你可以在test/目录下写测试用例用pio test -e native先在 PC 上跑一轮纯逻辑测试再烧到板子上跑硬件相关的测试。这一套下来代码质量明显会往上走一个台阶尤其适合做电机控制、滤波算法这类需要反复调试数值逻辑的场景。7. 我的个人体会总结从 Keil 换到 Vscode PlatformIO最直观的感受是心态变好了。Keil 的工程管理和代码编辑体验停留在 2010 年代而 PlatformIO 给出的是一个接近现代后端开发体验的嵌入式工作流。它没有牺牲 STM32 工程本身的灵活性只是把模板化、重复性的工作压缩到了极致。在踩过无数坑之后我个人的总结是这套组合最怕的不是技术问题而是“以为自己配置错了”然后反复折腾配置实际只是晶振频率写错了。老实说我开发 STM32 这么久百分之三十的时间花在调时钟、调波特率、查接线这些基础问题上剩下的时间才是真正的业务逻辑开发。希望这份指南能帮你安全绕过一大半我刚入坑时踩过的雷。最后再分享一个小技巧工程规模变大以后编译前先看一眼 Vscode 左下角的 PlatformIO 状态栏如果显示的小电脑图标不是绿色多半是环境有问题这时候点它重新加载一下能避免很多莫名其妙的编译错误。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →