目录
- 前言
- 一、STM32CubeMX 工程结构与代码编写规范
-
- 1.1 理解代码生成的本质与“安全区”
-
- 1.1.1 用户代码区域(User Code Sections)
- 1.2 工程目录结构深度解析
-
- 1.2.1 Application 文件夹
- 1.2.2 Drivers 文件夹
- 1.2.3 固件包仓库(Repository)
- 二、GPIO 输出速度配置详解
-
- 2.1 理想波形与实际波形的差异
- 2.2 三种速度档位的物理含义
-
- 2.2.1 低速模式 (Low Speed)
- 2.2.2 中速模式 (Medium Speed)
- 2.2.3 高速模式 (High Speed)
- 2.3 选型建议与实际应用
- 三、开发环境:Keil MDK 与 VS Code 协同开发
-
- 3.1 为什么要引入 VS Code?
- 3.2 行业现状与技术传承
- 3.3 Vs Code 引入与问题解决
-
- 3.3.1 VS Code 的核心定位与生态模式
- 3.3.2 基础环境配置:中文插件与项目导入
- 3.3.3 核心配置:关联 Keil 编译器路径
- 3.3.4 编译与烧写实战及避坑指南
- 结语
🎬 云泽Q:个人主页
🔥 专栏传送入口: 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法》《AI赋能》《STM32》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、STM32CubeMX 工程结构与代码编写规范
在前面文章的第一个点灯程序的编写后,大家会发现借助 HAL 库,实现功能变得非常快捷。但随之而来的一个核心问题是:如何正确理解和使用 STM32CubeMX 生成的代码?很多初学者在重新生成代码后,发现自己写的代码“消失”了,或者面对复杂的目录结构感到无从下手。本文将深入剖析 CubeMX 的代码生成机制、工程目录结构以及正确的代码编写习惯。
1.1 理解代码生成的本质与“安全区”
STM32CubeMX 所谓的“生成代码”,本质上是一个“拷贝一部分 + 生成一部分”的过程。当你点击 GENERATE CODE 按钮时,工具会从本地的固件包(Repository)中拷贝基础的驱动文件和启动文件,并根据你的图形化配置生成相应的初始化代码。

1.1.1 用户代码区域(User Code Sections)
在 main.c 以及其他由 CubeMX 管理的源文件中,你会看到大量成对出现的注释,例如: 
/* USER CODE BEGIN Header */
...
/* USER CODE END Header */
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
/* USER CODE END PV */
这些注释构成了代码的“安全区”。这是 CubeMX 开发中最重要的一条规则:所有用户自定义的代码(包括头文件包含、宏定义、全局变量声明、函数实现等),必须严格写在这些 BEGIN 和 END 标记之间。
-
为什么必须写在里面? CubeMX 在重新生成代码时,会扫描这些标记。标记之外的内容会被视为“机器生成代码”而被覆盖或重置;只有标记之内的内容会被识别为“用户代码”并予以保留。如果你把代码写在了外面,一旦你修改了 CubeMX 中的配置并再次生成代码,你手写的逻辑就会瞬间丢失。
-
具体的区域划分:
- USER CODE BEGIN Includes / END Includes:用于包含你自己定义的头文件。前期我们可能只用单文件开发,但随着项目变大,需要多文件协作时,这里就是存放 #include "my_driver.h" 的地方。
- USER CODE BEGIN PTD / END PTD:用于定义私有类型定义(Private Type Definitions)。
- USER CODE BEGIN PD / END PD:用于定义私有宏(Private Defines)。
- USER CODE BEGIN PV / END PV:用于定义私有变量(Private Variables),即全局变量。
- USER CODE BEGIN 0 … USER CODE BEGIN WHILE … USER CODE BEGIN 3:这些通常位于 main 函数内部,分别对应初始化后的代码区、主循环前的代码区以及主循环内部的代码区。
1.2 工程目录结构深度解析
打开生成的工程,你会发现文件树非常庞大。理解这个结构对于后续排查问题至关重要。虽然 Keil MDK 等 IDE 显示的逻辑分组与磁盘上的物理文件夹结构可能不完全一致,但核心逻辑如下:
1.2.1 Application 文件夹
- MDK-ARM:这里面存放的是特定于 IDE 的工程文件,最核心的是启动文件(如 startup_stm32f103xe.s)。这个汇编文件负责系统复位后的底层初始化(如设置栈指针、初始化中断向量表),最后跳转到 main() 函数。现阶段我们不需要修改它。
- User/Core:这是我们的主战场。main.c 就在这里,它是用户逻辑的入口。同时还有 stm32f1xx_it.c,这是中断服务函数的模板文件。

1.2.2 Drivers 文件夹
- STM32F1xx_HAL_Driver:这是 ST 提供的 HAL 库源码。你会发现里面有很多 .c 文件,如 stm32f1xx_hal_gpio.c(GPIO驱动)、stm32f1xx_hal_rcc.c(时钟驱动)、stm32f1xx_hal_uart.c(串口驱动)等。我们在代码中调用的 HAL_GPIO_WritePin 等函数,其具体实现就在这些文件中。
- CMSIS:这是 ARM 公司定义的 Cortex-M 软件接口标准(Cortex Microcontroller Software Interface Standard)。它提供了一层访问 Cortex-M 内核功能的接口,比如操作 NVIC(中断控制器)、SysTick(系统滴答定时器)等。它是连接硬件内核与上层软件的桥梁。

1.2.3 固件包仓库(Repository)
在 CubeMX 的设置中(Help -> Manage embedded software packages),你可以看到 Repository Folder 的路径(例如 D:\\STM32CubeMX_Repository)。这就是所有驱动源文件的“老家”。CubeMX 生成工程时,就是从这里把需要的文件“搬运”到你的项目里的。 
二、GPIO 输出速度配置详解
在配置 GPIO 引脚(如控制 LED 的 PF8)时,有一个选项叫 Maximum output speed(最大输出速度),通常有 Low、Medium、High 三个档位。很多人不理解这个参数的物理意义,甚至认为选得越高越好。其实,这涉及到数字电路的信号完整性问题。 
2.1 理想波形与实际波形的差异
在数字电路的理论世界中,我们假设方波是完美的:从 0 变到 1 是瞬间完成的垂直跳变,高电平和低电平保持时间也是完美的矩形。但在物理世界中,由于电路中寄生电容、电感以及驱动能力的限制,电压的变化是需要时间的。 
- 上升时间 (
t
r
t_r
tr):信号从低电平(通常是 10%V
D
D
V_{DD}
VDD)上升到高电平(90%V
D
D
V_{DD}
VDD)所需的时间。 - 下降时间 (
t
f
t_f
tf):信号从高电平下降到低电平所需的时间。
当 GPIO 翻转速度非常快时,如果上升/下降时间过长,波形就会变成梯形甚至三角形。极端情况下,信号还没稳定到高电平就开始下降了,导致接收端无法识别出有效的“1”或“0”。因此,GPIO 的输出速度是有物理上限的。
2.2 三种速度档位的物理含义
STM32 的 GPIO 速度配置实际上是在配置输出驱动器的翻转速率。根据数据手册(以 STM32F1 系列为例),不同配置对应的最大频率和电气特性如下:
2.2.1 低速模式 (Low Speed)
- 配置值:MODEy[1:0] = 10
- 最大频率:2 MHz
- 电气特性:
- 周期
T
=
500
ns
T = 500\\text{ns}
T=500ns。 - 上升时间/下降时间:约 125ns。
- 这意味着在一个周期内,电平转换占据了相当一部分时间。对于点灯这种低频应用,2MHz 绰绰有余,且能有效降低电磁干扰(EMI)和功耗。

- 周期
2.2.2 中速模式 (Medium Speed)
- 配置值:MODEy[1:0] = 01
- 最大频率:10 MHz
- 电气特性:
- 周期
T
=
100
ns
T = 100\\text{ns}
T=100ns。 - 上升时间/下降时间:约 25ns (在
C
L
=
50
pF
,
V
D
D
=
2
∼
3.6
V
C_L=50\\text{pF}, V_{DD}=2\\sim3.6\\text{V}
CL=50pF,VDD=2∼3.6V 条件下)。 - 相比低速,翻转速度快了 5 倍,适用于一般的通信接口或中等速度的控制信号。

- 周期
2.2.3 高速模式 (High Speed)
- 配置值:MODEy[1:0] = 11
- 最大频率:50 MHz
- 电气特性:
- 周期
T
=
20
ns
T = 20\\text{ns}
T=20ns。 - 上升时间/下降时间:极快,约 5ns ~ 8ns (取决于负载电容和电压)。
- 这种模式下,边沿非常陡峭,高频分量丰富,容易产生电磁干扰。通常只有在驱动高速总线(如 FSMC 驱动 LCD 屏幕、高速 SPI 等)时才需要使用。

- 周期
2.3 选型建议与实际应用
注意:最高速度
≠
\\neq
= 实际工作速度。 数据手册中提到的最大频率是指在特定负载(如 50pF)下,占空比能维持在 45%~55% 的极限情况。 
在实际工程中,我们遵循**“够用原则”**:
- 点灯、按键检测、继电器控制:这些应用频率极低(几 Hz 到几 kHz),选择 Low (2MHz) 即可。这不仅能满足需求,还能减少电源波动和对外辐射干扰。
- 普通 UART/SPI 通信:如果波特率在几 Mbps 以内,Medium (10MHz) 通常是最佳选择。
- 高速并行接口:只有当你的信号频率接近或超过 10MHz 时,才考虑开启 High (50MHz)。
我们在之前的文章中配置 PF8 引脚时选择了 Low,就是基于上述考虑。既然我们已经搞清楚了这一点,以后在所有类似的低速 GPIO 配置中,大家都可以统一设置为 Low。
三、开发环境:Keil MDK 与 VS Code 协同开发
随着代码量的增加,很多兄弟会感觉到 Keil MDK 自带的编辑器体验不佳:代码提示弱、界面古老、缺乏现代化的重构功能。为了解决这个问题,我们需要引入更先进的编辑工具,但同时又要兼顾行业现状。
3.1 为什么要引入 VS Code?
Keil MDK 作为传统的嵌入式 IDE,其优势在于编译、调试和仿真的一体化,但在代码编辑体验上确实落后于时代。相比之下,VS Code 拥有强大的插件生态、智能的代码补全(IntelliSense)、实时的语法检查以及极其舒适的 UI 交互。
我们希望将 “写代码” 和 “编译/调试” 这两个动作分离开来:
- 写代码:主要在 VS Code 中进行,享受高效的编辑体验。
- 编译/下载/调试:依然依赖 Keil MDK 或命令行工具链,保证工程的兼容性和稳定性。
3.2 行业现状与技术传承
可能有人会问:“既然 VS Code 这么好用,为什么不彻底抛弃 Keil?”
这就涉及到了嵌入式行业的特殊性。硬件产品的生命周期往往很长,许多公司(尤其是工业控制、汽车电子领域)维护着十年前的老产品。这些老项目的构建系统、调试脚本都是基于 Keil 或 IAR 建立的。由于硬件底层的稳定性要求,企业很难为了换个编辑器而重构整个构建流程。因此,Keil 在行业内依然占据主流地位。
所以在现阶段为了以后发展,我们需要掌握两套技能:
3.3 Vs Code 引入与问题解决
接下来我们将演示如何将 VS Code 引入到现有的 STM32 工程中。这并不是要替换掉 Keil,而是建立一种协同机制:
- 我们依然在 Keil 中管理工程文件(.uvprojx)、配置编译器路径、设置仿真器(ST-Link/J-Link)。
- 但在日常编码时,我们会打开 VS Code,通过插件关联到当前的工程目录,利用 VS Code 的强大功能进行代码编写。
接下来我们将具体演示如何配置这一环境。
3.3.1 VS Code 的核心定位与生态模式
我们要引入 VS Code,首先得搞清楚它到底是什么。VS Code 本质上是一个基于插件架构的开发工具,它的主要功能其实就像一个高级记事本,核心能力仅仅是编辑文本。但是,它强大的地方在于可以通过插件来无限扩展自己的能力。如果你有兴趣,完全可以给它配上编译器、调试器,让它直接用来开发 C 语言、C++ 甚至前端代码。只要是代码,它都能写得很好。
在我们的 STM32 开发场景中,我们主要利用的是 VS Code 卓越的编辑功能。通过安装特定的插件,它可以完美支持 Python、Java、C++ 等多种语言的开发。这种模式背后是微软的开放生态商业策略:它变得足够开放,吸引了大量周边开发者为其编写插件,从而实现了能力的快速迭代。
3.3.2 基础环境配置:中文插件与项目导入
在正式写代码前,我们需要对 VS Code 进行一些基础配置。
安装中文语言包 打开 VS Code,点击左侧侧边栏像积木一样的“扩展”图标。在搜索框输入 Chinese,找到第一个由 Microsoft 官方提供的 Chinese (Simplified) (简体中文) Language Pack,点击安装。安装完成后,如果界面没有自动变回中文,请务必重启 VS Code,这一步非常关键。 
导入 Keil 工程 为了实现 VS Code 与 Keil 的协同,我们需要安装 Keil Assistant 插件。安装后,侧边栏会出现 KEIL UVISION PROJECT 面板。点击面板上的加号 +,浏览并选择你的 Keil 工程文件(例如位于 D:\\code_project\\LED_test_08_07\\MDK-ARM 下的 .uvprojx 文件)。导入成功后,VS Code 会以树状结构展示代码目录,包括 Application、Drivers 等文件夹,这与 Keil 中的结构是完全一致的。

3.3.3 核心配置:关联 Keil 编译器路径
这是最关键的一步,也是很多初学者容易卡住的地方。VS Code 本身不具备编译 STM32 代码的能力,它必须调用 Keil MDK 的命令行工具来工作。
常见问题现象: 当你尝试在 VS Code 中点击编译或烧写时,底部的终端可能会报错:
'null' 不是内部或外部命令,也不是可运行的程序或批处理文件。

或者显示类似 UV4.exe path: null 的错误。这说明 VS Code 不知道去哪里找 Keil 的编译器。
解决步骤:
打开设置:点击 VS Code 左下角的齿轮图标,选择“设置”,或者直接按快捷键 Ctrl + ,。 
搜索插件配置:在设置搜索框输入 Keil Assistant。 
配置 UV4 Path:你会看到 Keil Assistant.MDK: Uv4 Path 这一项,默认值通常是 null。你需要在这里填入你电脑上 Keil 软件中 UV4.exe 的完整绝对路径。
如何找到 UV4.exe 的路径?
-
方法一(手动查找):进入你的 Keil 安装目录,通常在 D:\\Keil5\\core\\UV4\\ 下(这里是以我的安装路径为例,仅为参考),找到 UV4.exe 应用程序文件,复制其完整路径。

-
方法二(使用 Everything 搜索):如果你找不到安装目录,推荐使用 Everything 这款轻量级文件搜索工具 https://www.voidtools.com/zh-cn/。在搜索框输入 UV4,它能瞬间定位到 UV4.exe 的位置。

配置示例: 假设你的 Keil 安装在 D 盘,那么配置项应该填写为:
D:\\Keil5\\core\\UV4\\UV4.exe

注意:路径中不要包含多余的空格或引号,直接粘贴路径即可。配置完成后,VS Code 就能成功调用 Keil 的编译链了。
3.3.4 编译与烧写实战及避坑指南
配置好路径后,我们就可以在 VS Code 中享受现代化的开发体验了。
1. 编译代码 在左侧 KEIL UVISION PROJECT 面板中,点击项目名称旁边的“锤子”图标(Build)。此时观察底部终端,你会看到 VS Code 正在调用我们刚才配置的 UV4.exe 执行编译命令:
D:\\Keil5\\core\\UV4\\UV4.exe -b d:\\programming_file\\STM32\\...\\LED_test_08_07.uvprojx ...

如果配置正确,终端会输出编译日志,显示 0 Error(s), 0 Warning(s),并生成 .axf 和 .hex 文件。这意味着编译成功!
2. 烧写代码 编译通过后,点击项目名称旁边的“下载”图标(Download to Device,快捷键 Ctrl+Alt+D)。终端会执行烧写命令,显示 Erase Done、Programming Done、Verify OK,最终提示 Flash Load finished。此时按下开发板的 RESET 键,你编写的 LED 灯应该就会亮起。 
3. 常见 Bug 与排查(重要) 在实际操作中,大家可能会遇到一些“玄学”问题,这里给大家总结一下避坑经验:
-
烧写失败/识别不到设备:有时候点击烧写会报错 No ST-LINK detected 或 Flash Download failed。这往往是 Keil Assistant 插件的小 Bug,或者是第一次使用时状态未同步,因为烧写时VsCode还是调用Keil进行烧写的,若是Keil默认使用的仿真器不对,VsCode自然也不会烧写成功,所以这时候最好返回看一下Keil现在默认使用哪种仿真器进行烧写的。

- 解决方案:若Keil中的仿真器配置没有问题,不要慌,尝试关闭 VS Code 重新打开,或者重启几次 VS Code。很多时候,重启一下就好了。
- 备选方案:如果 VS Code 里的烧写功能一直报错,可以直接切回 Keil MDK 软件进行烧写。Keil 原生软件对烧写的支持是最稳定的,VS Code 主要用来写代码和编译,两者协同工作完全没问题。
-
代码不同步:如果你在 VS Code 里改了代码,记得按 Ctrl+S 保存。插件会自动同步代码,Keil也会跳出下面窗口,点击红框标注选项即可同步代码

且手动保存是个好习惯。如果 Keil 那边没反应,可以尝试在 VS Code 中右键项目选择“Reload”或重新导入,一般都会同步成功的,起码我在使用VsCode的时候没有出现过这种问题。
结语







