1. 先搞懂这套组合:CubeMX 与 Keil µVision 各管什么
刚上手 STM32 的朋友,最容易犯的一个错是把 STM32CubeMX 和 Keil µVision 当成两个能互相替代的东西。不是的。这两个工具在整条开发链路里扮演的角色完全不同,一旦这个概念没理顺,后面出现的绝大多数"玄学问题"你都会找不到北。我见过有人拿着 CubeMX 生成的代码在 Keil 里改了半天,结果一动 .ioc 文件重新生成,代码全被冲掉,白干一晚上。根子就在于没搞清楚谁生成谁、谁覆盖谁。
我自己的习惯是把 STM32CubeMX 理解为"硬件描述 + 初始化代码生成器",而 Keil µVision(准确说是 MDK-ARM)是"编辑 + 编译 + 下载 + 调试"的集成环境。CubeMX 负责把芯片的外设、时钟、引脚分配这些"板级信息"翻译成一段可编译的 C 代码,其中 HAL 库或者 LL 库的初始化函数是它的主要产出;Keil 负责把这段代码连同你的业务逻辑一起编译成 bin/hex,然后通过调试器烧进芯片,并在运行时让你看变量、打断点、单步执行。
1.1 两个工具的分工与数据流
把数据流捋一遍就清楚了:你在 CubeMX 里选芯片型号 → 配置时钟树 → 配置外设(GPIO、USART、SPI、ADC……)→ 分配引脚 → 设置工程名称和工具链类型 → 点击 GENERATE CODE。这时候 CubeMX 会在你指定的目录下产出一整套工程:Core/里放着main.c、stm32f1xx_it.c这些用户代码,Drivers/里放着 HAL 库和 CMSIS,根目录下有一个.ioc文件记录你所有的图形化配置,还有一个.uvprojx文件——这就是 Keil 的工程文件。
Keil 打开.uvprojx之后,它并不"认识"CubeMX,它只是按照 XML 里描述的源文件列表、头文件路径、宏定义去编译。所以你后面如果手动在 Keil 里加了一个.c文件,又在 CubeMX 里点了重新生成,CubeMX 用自己记录的文件列表去覆盖.uvprojx,你手动加的那个文件就从工程列表里消失了。文件还在磁盘上,但 Keil 不编译它了——这就是新手最常见的"我代码明明写了,怎么没执行"的来源。
理解这个分工之后,一个很重要的实践原则就出来了:外设初始化永远在 CubeMX 里改,业务逻辑永远写在USER CODE BEGIN和USER CODE END之间。CubeMX 生成代码时会扫描这些标记,标记之间的内容它原样保留,标记之外的内容它会重写。这条规则你今天记住,能省下未来几十个小时的返工。
1.2 版本搭配与授权选择
版本搭配这件事,网上的资料杂乱得厉害,我不想给你一个"必须用某某版本"的绝对结论,因为芯片系列和项目需求不同,最优解不一样。但有几条经验值得分享。
CubeMX 的版本和 Keil 的工具链版本之间存在耦合。CubeMX 在 Project Manager 里让你选的 Toolchain 选项,决定了它生成的是.uvprojx(对应 MDK-ARM V5)还是.uvproj(老版本 V4)。现在主流都是选 MDK-ARM V5,这个不要选错,选错了 Keil 直接打不开。
另外一个坑是Arm Compiler 版本。Keil MDK 从 5.37 版本开始,默认安装的编译器是 Arm Compiler 6(AC6,基于 Clang/LLVM),而老工程和历史教程基本都是 Arm Compiler 5(AC5,基于 ARMCC)。AC6 对代码规范更严格,STM32 早期的一些 HAL 库在 AC6 下会冒出警告甚至报错,典型的就是__weak符号重复定义、内联汇编语法不兼容这类。如果你拿到的工程模板是几年前写的,编译报一堆错,先别怀疑代码,去Project → Options for Target → Target选项卡里看看编译器版本,切到 AC5 往往就通了。反过来,AC6 的编译速度确实快不少,新项目我建议直接用 AC6,然后在 C/C++ 选项卡的 Misc Controls 里按需加-Wno-...抑制部分警告。
关于授权,这里必须说清楚:Keil MDK 有面向个人学习、非商业用途的免费版本(MDK-Community),功能和商业版基本一致,只是授权范围不同;商业项目请走正规采购渠道。另外一条完全免费的路线是 ST 官方的 STM32CubeIDE,本身就是 Eclipse 内核,集成了 CubeMX 的图形化配置功能,缺点是国内用户反馈启动慢、偶尔卡顿。选哪个看你团队习惯,别用来源不明的安装包,出了编译产物异常、链接脚本被篡改这种事,排查起来得不偿失。
2. CubeMX 侧:从选型到生成工程的关键配置
这一段是整篇的核心。我按操作顺序把每一步为什么这么做讲透,你照着做能避开九成的返工。
2.1 芯片选型与时钟树配置的算式
新建工程时,CubeMX 提供三种入口:按 MCU 型号选、按开发板选、按外设需求交叉筛选。新手直接用型号搜索最快,比如你手上是 STM32F103C8T6 最小系统板,就在搜索框敲STM32F103C8,选中带Tx的封装那一行。注意封装要和实物对上,C8T6 是 LQFP48,选成 LQFP64 虽然也能编译,但引脚编号和你板子上的丝印对不上,接线全乱。
选完芯片进入主界面,第一件要配置的就是时钟。这一步很多人直接点"OK"跳过去,结果后面串口波特率不对、定时器定时不准,全是时钟惹的祸。时钟树的逻辑其实不复杂,就是把外部晶振经过 PLL 倍频,分配到各个总线上。
以 STM32F103 系列为例,典型配置是:外部晶振 HSE 选 8MHz(有些板子焊的是 12MHz,务必看原理图)→ PLL 源选 HSE → PLL 倍频系数选 ×9 → 得到 72MHz 的系统时钟 SYSCLK → AHB 预分频器选 /1,所以 HCLK = 72MHz → APB1 预分频器选 /2,PCLK1 = 36MHz(这是 APB1 的上限,超了就挂)→ APB2 预分频器选 /1,PCLK2 = 72MHz。同时 Flash 延迟要设成 2 个等待周期(Latency 2WS),因为 72MHz 超过了 48MHz 这个分界点,不设等待周期读 Flash 会出错。
换成 STM32F407 这类 F4 芯片,PLL 的结构变成了 M/N/P 三个参数,配置逻辑是:VCO 输入频率 = HSE / M,要求落在 1~2MHz 区间;VCO 输出频率 = VCO 输入 × N,要求落在 192~432MHz;最终 SYSCLK = VCO 输出 / P。常见的一组是 HSE=8MHz、M=8、N=336、P=2,算下来 VCO 输入 = 1MHz,VCO 输出 = 336MHz,SYSCLK = 168MHz。再配 APB1 = /4 得 42MHz,APB2 = /2 得 84MHz,Flash 延迟设 5WS。这组参数在 F407 上非常经典,很多教程都用它。
CubeMX 的时钟树界面有个很贴心的设计:它会实时显示你当前的配置有没有超出芯片规格。如果某个总线频率那一栏显示成红色或者有警告图标,说明你配超了,赶紧调预分频。我个人的习惯是配完之后截图存一份,写在项目 README 里,后期别人接手一看就懂,免得对着时钟树界面猜半天。
2.2 SYS/Debug 与外设初始化的几个必改项
时钟配好之后,有几个默认配置我强烈建议你动手改。
第一个是 SYS 里的 Debug 选项。CubeMX 新建工程时这个默认是Disable,如果你不改,第一次烧录完程序之后,芯片的 SWD 引脚就被你的程序接管了,第二次想下载的时候调试器连不上,报 "No target connected" 或者直接 flash 识别失败。解决办法是把 SYS → Debug 改成Serial Wire,这样 CubeMX 会在初始化代码里保留 SWD 引脚的功能,后续可以反复下载。万一已经踩坑了,按住复位键点下载、或者用 BOOT0 拉高的方式进系统存储器启动模式擦除,能救回来,但何必呢。
第二个是 Timebase Source。默认是 SysTick。如果你打算用 FreeRTOS,这个必须改成某个通用定时器,比如 TIM1 或者 TIM6。原因很实在:FreeRTOS 自己要拿 SysTick 当时基,HAL 库的HAL_Delay()和超时判断也依赖 SysTick,两边抢同一个中断,轻则延时不准,重则任务调度直接乱掉。换成 TIM 之后,HAL 用定时器做时基,FreeRTOS 独占 SysTick,井水不犯河水。这个坑我当年踩过,现象是任务能跑但周期完全不对,查了两天才定位到时基冲突。
第三个是外设的细节参数。以 USART 为例,CubeMX 会自动根据你选的波特率反推寄存器值,但你得确认时钟源频率填对了。F1 系列的 USART1 挂在 APB2 上,默认 72MHz;USART2 挂在 APB1 上,36MHz。如果前面时钟树配错了,这里生成的波特率就会有偏差,串口助手收到一堆乱码。这种情况我把经验写死了:串口乱码,第一步查时钟,第二步查波特率,第三步查接线(TX/RX 是不是交叉了),第四步查共地,按这个顺序走,基本五分钟能定位。
GPIO 配置也别嫌麻烦。每个引脚要设模式(输入/输出/复用/模拟)、上下拉(Pull-up/Pull-down/No pull)、输出速度(Low/Medium/High/Very High)、用户标签(User Label)。用户标签这栏看着不起眼,但它会直接变成生成代码里的宏名。比如你把 PA5 标成LED_RUN,生成代码里就会出现LED_RUN_Pin和LED_RUN_GPIO_Port这两个宏,后面写代码直接调,不用记 PA5 对应哪个寄存器位。这个习惯我强烈推荐养成,尤其是引脚超过十个的项目,标签能救命。
2.3 Project Manager:决定 Keil 工程长什么样的几个开关
配置完外设,切到 Project Manager 页面,这一页的选项直接决定生成出来的 Keil 工程长什么样,值得逐项过一遍。
Project 子页里,Project Name 和 Project Location 不用多说,注意路径别带中文和空格,Keil 对中文路径的支持时好时坏,有时候报错信息还特别隐晦,让你完全想不到是路径问题。Toolchain/IDE 选MDK-ARM,版本选 V5。Toolchain Folder Location 建议留空,让它自动放到工程目录下。
Code Generator 子页是重点。Copy only necessary library files这个选项,勾选之后 CubeMX 只把用到的 HAL 源文件拷到你的工程里,工程体积小、编译快;不勾选的话它会把整个 HAL 库都拷进来,几百个文件,看着就头大。我一般勾上。Generate peripheral initialization as a pair of .c/.h files per peripheral这个选项也建议勾上,勾了之后每个外设的初始化代码会独立成文件(比如usart.c、gpio.c),不勾的话全堆在main.c里,几百行挤在一起,改起来很痛苦。
Keep User Code when re-generating这条务必保持勾选,它是前面说的 USER CODE 保护机制的总开关。另外还有一个Backup previously generated files when re-generating,勾上之后每次重新生成都会把旧文件存一份备份,多占点磁盘空间,但关键时刻能救急,我建议勾。
Advanced Settings 子页里可以指定每个外设生成的库是 HAL 还是 LL。LL 库更接近寄存器、代码更精简、执行更快,但可移植性差一些;HAL 库抽象层次高、写业务逻辑方便,缺点是代码体积大、有额外开销。我的经验是:主控逻辑和复杂外设用 HAL,对时序极敏感的场合(比如软件模拟的精确延时、高速 SPI 时序)局部用 LL,两者可以在同一个工程里混用,CubeMX 支持单独指定。
点 GENERATE CODE 之后,CubeMX 会在输出窗口打印一套日志,把生成的文件列表列出来。如果中途报错,常见原因是固件包没下载。CubeMX 第一次用某个系列芯片时,需要下载对应的固件包(比如 F1 系列的STM32Cube FW_F1 V1.8.x),可以从 CubeMX 里的Help → Manage embedded software packages里下,也可以提前从 ST 官网拿到离线包解压到指定目录。国内下载偶尔会慢,耐心等或者换个时间段。
3. Keil µVision 侧:编译、下载、调试的完整落地
工程生成出来了,接下来是 Keil 这一侧的操作。很多人以为双击.uvprojx打开就能一键跑通,实际上一堆默认配置需要检查,尤其是调试器相关的。
3.1 打开工程后的第一轮检查
第一次打开工程,先别急着点编译。花两分钟做三件事。
第一,看左侧 Project 窗口的文件树是不是完整的。正常情况下你会看到Application/User/Core下面有main.c、gpio.c、usart.c这些,Drivers/STM32F1xx_HAL_Driver下面是 HAL 库,Drivers/CMSIS下面是启动文件和内核头文件。如果某一块是空的或者文件前面有个黄色感叹号,说明 Keil 找不到那个文件,通常是路径问题——你可能把工程目录移动过位置,或者从别人那儿拷过来时只拷了部分目录。解决办法是在 Options for Target → C/C++ → Include Paths 里检查路径是否有效。
第二,看一眼Target选项卡里选中的芯片型号对不对。CubeMX 生成工程时会自动写入,但如果你是从别处拿的工程,型号可能不匹配。型号错了最直接的后果是 Flash 算法不对,下载时报 "Flash Download failed"。
第三,确认编译器的 Target 时钟频率。Target选项卡里有一个Xtal (MHz)输入框,它影响的是 Keil 仿真器(不是硬件)跑的时间。如果你用软件仿真模式,这个值必须和实际晶振一致,否则HAL_Delay的模拟时间会对不上。硬件调试模式下它不影响实际运行,但填对了没坏处。
3.2 Options for Target 逐页配置
Options for Target这个对话框是 Keil 的核心配置中心,我按顺序把关键项过一遍。
Target 页:晶振频率填实际值(比如 8.0);Use MicroLIB这个选项值得说一句。勾上之后 Keil 会用 ARM 的精简 C 库替代标准 C 库,好处是代码体积小、printf重定向到串口更方便;坏处是部分标准库函数被裁剪了,比如浮点格式化支持不全、malloc行为不同。如果你项目里用到浮点转字符串,sprintf("%f", x)出不来小数点,多半就是 MicroLIB 的问题。我的建议是:简单项目勾上省事,复杂项目、用到完整 C 库的,别勾。
Output 页:勾上Create HEX File,方便后面用其他工具烧录;Debug Information必须勾,不勾的话调试时看不了源码级信息,只能看汇编。Browse Information也建议勾,它让 Keil 的代码跳转(右键 Go To Definition)功能正常工作。
Listing 页:里面的.map文件生成建议打开(Linker Listing里勾Memory Map)。编译完之后看map文件能知道每个函数占了多少 Flash、RAM 里各段是怎么分布的。代码写大了 Flash 不够用的时候,这个文件是唯一能告诉你"谁在吃空间"的东西。
C/C++ 页:这里有两个关键点。一是Define里的宏。CubeMX 生成的工程会自带USE_HAL_DRIVER和芯片系列宏(比如STM32F103xB),这个别删,删了直接编译报错。二是Optimization等级。调试阶段强烈建议选Level 0 (-O0),因为高优化等级下编译器会重排指令、合并变量、把某些变量放进寄存器,导致你打断点打不上、单步跳得很怪、watch窗口显示<optimized out>。发布版本再切到-O2或-Os(体积优先)。这个切换动作看着小,但能省下大量"为什么这个变量值不对"的怀疑时间。
Debug 页:这是下载调试的总开关。左边选调试器,ST-Link 就选ST-Link Debugger,J-Link 选J-LINK / J-TRACE Cortex,国产 DAP-Link 一般选CMSIS-DAP Debugger。选完之后点右边的Settings按钮,会弹出调试器设置窗口。在Debug标签页里能看到Port选项,SWD 和 JTAG 二选一,用 SWD 引脚少、速度稳,推荐。如果这里能识别到芯片 ID(比如SW Device框里显示ARM CoreSight SW-DP),说明连接正常;显示一片空白说明线没接好或者调试器驱动没装。
在Flash Download标签页里,要确认Programming Algorithm列表里有对应你芯片 Flash 容量的算法。F103C8 是 64KB,就选STM32F10x Med-density Flash;如果列表是空的,点Add手动加。同页勾上Reset and Run,这样下载完程序自动复位运行,不用手动按板子上的复位键——这个选项不勾,很多人会以为程序没烧进去。
3.3 编译下载与首次上电验证
配置完点Build(F7)。第一次编译会慢一些,因为要编译整个 HAL 库,几十秒到一两分钟不等。编译输出窗口最后一行如果是0 Error(s), 0 Warning(s)或者少量无害警告,就说明成功了。
点Download(F8)烧录。如果前面配置都对,会看到进度条一闪,然后提示Programming Done. Verify OK.,如果勾了 Reset and Run,板子就直接跑起来了。
验证程序有没有真正在跑,最简单的方式是点一个 LED。CubeMX 里把某个 GPIO 配成输出,代码里在while(1)中调用HAL_GPIO_TogglePin()加HAL_Delay(500),烧进去看灯闪不闪。灯闪了,说明时钟、GPIO、Flash 下载这一整条链路都通了,可以开始写业务逻辑了。
如果灯不闪,别慌,按这个顺序查:先用调试器连一下,看能不能读到芯片(能读说明下载链路 OK);然后进调试模式,在main()里打个断点,看程序是不是停在断点处(能停说明程序在跑);然后在HAL_Init()之后单步,看时钟配置函数有没有正常返回。这套流程走下来,问题基本能定位到具体环节。
4. 常见问题与排查技巧实录
这一部分是我这些年攒下来的问题清单,按类型整理,遇到的时候直接对表查。
4.1 下载与连接类问题速查
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
报No ULINK device found | 调试器选错或驱动异常 | Options → Debug 里确认选的是 ST-Link/CMSIS-DAP,不是 ULINK;重装对应驱动 |
报No target connected | SWD 引脚被程序占用、接线松、板子没供电 | 检查 SYS→Debug 是否设为 Serial Wire;用万用表量 SWDIO/SWCLK 通断;确认板子供电正常 |
| 第一次能下载,第二次连不上 | 程序里把 SWD 引脚配成了普通 GPIO | 按住复位键点下载,或者 BOOT0 拉高进 bootloader 擦除,然后在 CubeMX 里把 Debug 改成 Serial Wire |
Flash Download failed - Target DLL has been cancelled | Flash 算法没选或者选错容量 | Options → Debug → Settings → Flash Download 里 Add 正确的算法 |
| 点 Settings 时 Keil 直接卡死闪退 | 调试器固件版本与 Keil 版本不兼容,常见于某些克隆版 ST-Link | 用 ST 官方工具升级调试器固件;换官方调试器;重启 Keil 和电脑 |
| 下载成功但程序不跑 | 没勾 Reset and Run,或者芯片处于某个低功耗状态 | 手动按复位;勾上 Reset and Run;检查__WFI()相关代码 |
"第二次连不上"这个坑我特别想再强调一遍,因为它出现的概率非常高,而且新手完全想不到根因。原理是:芯片复位后,程序执行的第一个动作之一就是配置 GPIO 的复用功能。如果你的 SWDIO 和 SWCLK 引脚(F103 上是 PA13、PA14)被配成了普通输出,那调试器就再也抓不住它们了。CubeMX 的 SYS→Debug 选项本质上就是在时钟初始化和 GPIO 初始化之间插一段特殊代码,把这两个引脚锁在调试复用状态。所以这个选项不是"可改可不改",是必须改。
另外关于烧录,补充一个实用技巧:如果手头没有调试器,只有串口,STM32 的 ROM 里自带系统存储器 bootloader,可以用串口工具配合 BOOT0 拉高来烧录 hex/bin 文件。这个方法在调试器坏了、板子又急用的时候能顶一阵,缺点是速度慢、不支持在线调试。
4.2 编译与代码类问题
AC5/AC6 切换导致的报错。前面提过,报错信息里如果出现__asm、__weak、#pragma相关的语法错误,先怀疑编译器版本,去 Target 页切到另一个版本试试,比一上来就改代码高效得多。
重新生成代码后出现重复定义。CubeMX 重新生成时,可能会把你之前手动加进main.c的某些初始化代码又生成一遍。解决办法还是那句话,把自定义代码严格放进USER CODE BEGIN xxx标记里。标记外的代码,CubeMX 有权力重写。
头文件找不到。报fatal error: xxx.h: No such file or directory,去 C/C++ 页的 Include Paths 里加路径。注意路径要精确到包含.h文件的那一层目录,多加一层少加一层都不行。
工程整体迁移后编译报错。多半是绝对路径问题。CubeMX 生成的工程默认用相对路径,但如果你在 Keil 里手动加过源文件,它记录的可能是绝对路径。解决办法是把所有源文件改用相对路径,或者在 Options → C/C++ 里统一配置。
代码体积超过 Flash 容量。去看 map 文件,看哪个函数最大。常见元凶是printf/sprintf这类的格式化函数,它们在标准库里会拖进来一大坨。换成 MicroLIB 能省不少,或者改用轻量的自定义输出函数。
4.3 调试窗口里的实用技巧
Keil 的调试功能比很多人以为的强,这里分享几个我自己常用的。
看结构体变量。很多人困惑的一点是:调试时Watch窗口里加了个结构体指针,只显示一个地址,看不到成员。这是因为指针需要先解引用。在 Watch 窗口里输入(*变量名)或者变量名->成员名,就能展开。如果是结构体变量本身(不是指针),直接输入变量名,左边会有个+号点击展开。更省事的做法是右键变量选Add to Watch Window,Keil 会自己处理解引用。
逻辑分析仪(Logic Analyzer)。这是 Keil 一个被严重低估的功能。在调试模式下,菜单View → Analysis Windows → Logic Analyzer,可以添加要观察的变量或者引脚。比如你想看某个 PWM 波形对不对,把对应的变量加进去,就能在时间轴上看到电平变化。前提是这些变量得在 RAM 里能读到、编译器没把它们优化掉(所以调试时用 -O0 很重要)。
实时变量更新(Live Watch)。普通的 Watch 窗口只在程序暂停时更新,想看运行时的值变化很麻烦。Keil 的View → Watch Windows → Watch 2加进变量后,配合调试器的实时读写能力可以做到不停机刷新。不过这依赖调试器支持、也要注意别让它拖慢运行速度。
内存窗口看数组和缓冲区。串口接收、DMA 传输这类场景,数据都在缓冲区里,用View → Memory Windows直接输入数组名或者地址,就能以十六进制、十进制、ASCII 等多种格式查看内存内容。判断一帧数据收全了没有,比打日志快得多。
断点的高级用法。除了普通断点,Keil 支持条件断点(右键断点 →Breakpoint Properties→ Condition),比如i == 100时才停。还有数据断点(访问某个内存地址时触发),查野指针、数组越界特别好用。这两个功能在 ST-Link 上都可以用,别浪费。
5. 工程维护与后续扩展的一些经验
工具跑通了,接下来是工程怎么长期维护的问题。这一段是给打算把这个项目做下去的人看的。
5.1 .ioc 的版本管理与重新生成策略
.ioc文件本质上是一个纯文本的配置文件,记录了你所有的图形化配置。它必须纳入版本管理(git 之类),而且必须和生成出来的代码一起提交。原因很简单:三个月后你想改个引脚功能,打开 CubeMX 加载.ioc,所有配置原样恢复,改完重新生成,前后一致。如果你把.ioc丢了,只能对着现有的代码反推配置,那工作量比重新做一遍少不了多少。
重新生成的频率我建议这样控制:硬件相关的配置(引脚、时钟、外设开关)改得差不多定型之后,尽量减少重新生成。业务逻辑写完一层就提交一次代码,重新生成前先 commit,生成后 diff 一下,看看 CubeMX 动了什么。养成这个习惯,即使某次生成把东西搞乱了,回滚也只是几秒钟的事。
还有一个细节:Keil 工程文件.uvprojx会被 CubeMX 覆盖。如果你在 Keil 里手动添加过源文件、改过分组结构,重新生成后会丢。解决办法有两个,一是把这些自定义文件通过 CubeMX 的外设配置"间接"纳入工程(比如把自定义代码放进已有的文件里),二是在 CubeMX 的 Project Manager 里手动维护"额外源文件"的路径。前者更优雅,后者更灵活,看项目规模定。
5.2 从裸机到 FreeRTOS 与图形库的接续
CubeMX 最大的价值不只是生成初始化代码,还在于它能帮你把中间件也一起配好。FreeRTOS 是最典型的例子:在 Middleware 分类里找到FREERTOS,选接口版本(CMSIS-V1 还是 CMSIS-V2,新项目建议 V2,API 更清晰),然后配置任务、队列、信号量、定时器。CubeMX 生成出来的代码里会有MX_FREERTOS_Init(),你的任务函数骨架都建好了,填业务逻辑就行。
这里再重复一遍前面提过的关键点:用 FreeRTOS 时,SYS → Timebase Source 一定要改成非 SysTick 的定时器。另外要注意中断优先级分组(NVIC Priority Group),FreeRTOS 要求所有用到内核 API 的中断优先级数值(逻辑优先级)要低于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则会出现诡异的行为。CubeMX 在 NVIC 配置界面里有提示,但很多人不看,直接按默认值走,然后被一个随机的死机现象折磨。
再往上一层,如果你要做界面,LVGL 是现在比较主流的选择。配合 CubeMX 和 Keil 的流程是:先用 CubeMX 配好 SPI 或者 FSMC(并口屏),生成工程;然后把 LVGL 源码作为外部文件夹加入 Keil 工程,配置lv_conf.h,实现显示刷新和触摸读取两个回调函数;最后跑官方的 demo 验证。这条路走通之后,STM32 做中小型 HMI 项目就没什么门槛了。要注意的是 LVGL 比较吃 RAM,F103C8 这种 20KB RAM 的芯片跑完整 LVGL 会很吃力,至少得上 F4 系列才有舒服的体验。
最后说一个我自己的习惯。每做完一个用 CubeMX + Keil 的项目,我会在项目根目录写一个简短的 README,把芯片型号、晶振频率、时钟树参数、CubeMX 版本、Keil 版本、固件包版本这几项记录下来。看着多余,但这些信息在你半年后回来加功能、或者同事接手的时候,价值极高。我吃过这个亏——有个项目放了两年,想改个串口波特率,光是回忆当时时钟怎么配的就花了半天,从那以后这个 README 我再也没省过。工程维护这件事,投入产出比最高的往往不是写代码,而是把"当时为什么这么配"记下来。