1. 为什么我建议新手用Keil 5.23加固件包的方式入门STM32
最近后台经常收到类似的私信:“刚买了开发板,装了Keil 5,但打开软件之后一脸懵——新建工程要选设备、要添加启动文件、要配置Flash算法,网上的教程十个有九个是Keil 4时代的截图,文件结构和Keil 5完全对不上,到底该怎么下手?”
这个问题我太有感触了。当年从51单片机转过来学STM32,卡得最久的不是C语言语法,也不是寄存器操作,而是“怎么把一个空工程建起来”。那时候用的还是Keil 4,标准外设库需要自己从官网下载,下载之后解压、复制、在工程里依次添加CMSIS核心文件、启动文件、外设库源文件……哪一步漏了,编译直接一堆红色错误,连芯片型号都对不上号,别提多崩溃了。
现在情况已经大不一样了。Keil 5引入了Pack(固件包)机制,芯片支持包、CMSIS组件、HAL库这些全部可以通过包管理器一键安装。只要在新建工程时选对了芯片型号,启动文件、系统初始化文件、核心头文件这些“地基”由固件包自带,不再需要手工搬运。这篇文章就用Keil 5.23版本,带着你从零创建一个基于固件包的STM32工程,并且成功编译出Hex文件。
先说清楚这篇文章适合谁:
- 刚接触STM32,不知道工程里那堆文件是干什么用的纯新手;
- 已经照着老教程建过工程,但被各种“找不到文件”“重复定义”“头文件路径错误”折磨过的自学者;
- 想搞清楚固件包、CMSIS、启动文件这几个概念到底是什么关系的人。
我会尽量把每一步的背后逻辑讲透,不是为了让你“照着做就行”,而是让你做完这一步之后,知道这一步解决的是什么问题。这样以后换芯片型号、换开发环境,你也能自己举一反三。
2. 准备工作:Keil 5.23安装与固件包安装的细节
2.1 版本选择:为什么是5.23而不是更新的MDK 5.3x
先明确一个概念:Keil 5.x版本号一直在更新,现在官网上最新版已经到了MDK 5.38甚至更高。但很多学习资料、开发板的配套例程、视频教程仍然停留在5.23、5.24这样相对“老”的版本。这是因为STM32的入门流程和HAL库 API几乎没有变化,用5.23完全够学,而且5.23对第三方烧录工具、老式ST-Link仿真器的兼容性也经过了大量用户验证,翻车概率低。
我自己的主力机装的也是5.23,用到现在几年了,跑STM32F1、F4的工程没有遇到解决不了的问题。唯一要注意的是,5.23默认的ARM编译器是ARMCC(即Arm Compiler 5),和后面高版本Keil默认的AC6编译器在C语言语法支持上有些区别。如果你用的是5.23,编译老工程基本不会碰到“__CC_ARM”这类宏定义找不到的奇葩问题,这对于新手来说反而少踩一个坑。
安装包就不放链接了,官网注册一个账号就能下。下载文件一般是MDK523.EXE这样的名称,双击运行,一路Next。安装路径不要放在带中文或空格的目录,默认的C:\Keil_v5就挺好。
2.2 关键一步:安装芯片固件包(DFP Pack)
这一步是Keil 5和Keil 4最大的不同,也是最容易被新手忽略的地方。Keil 4的时候,芯片文件是集成在安装包里的;Keil 5把芯片支持单独拆成了Pack包,需要用户自己装。
打开Keil 5.23之后,点击工具栏上的“Pack Installer”图标(一个绿色小包裹的图标),打开后可以看到左边是器件列表,右边是已安装的包。我们要装ST的芯片包,在左侧的“Software Packs”区域找到“Keil::STM32F1xx_DFP”,点击右边的“Install”按钮。
这个DFP包里包含什么东西呢?简单说就是:
- 芯片型号列表(你在新建工程时能选到STM32F103C8T6、STM32F407ZGT6这些型号,全靠它);
- Flash烧写算法文件;
- 启动文件模板;
- SVD调试描述文件。
如果你用的是STM32F407、F429这类M4内核的芯片,装“Keil::STM32F4xx_DFP”就行。F1和F4两个系列的内核不同,固件包不通用,这一点要记住。
安装过程需要联网,包大概几百MB,根据网速等一会儿。装完之后可以在“Device”标签下看到STMicroelectronics展开后列出了STM32F1系列的所有型号。我在实际安装中还遇到过一个情况:Pack Installer提示“Cannot connect to keil.download.…”,公司网络代理拦了。换成手机热点之后就正常了。如果你也卡在下载环节,先排查网络。
2.3 关于“固件包自带版本”的解读
这里要特别解释一下标题里的“固件包自带版本”。它指的是:你通过Pack Installer安装的DFP包里,已经内置了CMSIS核心组件、启动文件、系统初始化文件,以及可供RTE(Run-Time Environment)机制调用的软件组件。这意味着你不需要再去ST官网单独下载“标准外设库”或者“HAL库”的压缩包并手动添加到工程里。
对新手来说,这意味着新建工程的方式发生了根本变化:
- Keil 4老方式:手动下载标准库 → 复制stm32f10x_conf.h等文件到工程目录 → 手动添加一堆.c和.h到工程树 → 手动配置头文件路径 → 编译。
- Keil 5新方式:Pack包里自带 → 新建工程时勾选“CMSIS:CORE”和“Device:Startup” → 写代码 → 编译。
高版本的Keil还支持直接在RTE窗口中勾选HAL库的各个驱动模块,Keil 5.23这个版本虽然没有新版那么图形化,但核心机制是一样的。文章后面我会演示如何在不手动加库文件的情况下完成一个能点灯的工程。
3. 新建工程完整实操:从选芯片到跑通第一个点灯程序
3.1 新建工程与芯片型号选择
打开Keil 5.23,点击菜单栏Project → New uVision Project,输入工程名字,比如LED_Test,选择保存路径。这里再强调一次:路径千万不要有中文,否则后边编译时会出现奇怪的编码错误或者文件找不到。
点击保存后,会弹出“Select Device for Target”窗口,这就是让你选芯片型号。我在左侧导航依次展开STMicroelectronics → STM32F1 Series → STM32F103,然后选中我用的一款经典芯片——STM32F103C8T6。右侧可以看到这颗芯片的基本信息:ARM Cortex-M3内核、64KB Flash、20KB SRAM、工作频率72MHz。
选好芯片点OK,这时候会弹出一个对话框:“Copy STM32 Startup File to the Project Folder?”,问你要不要把启动文件复制到当前工程文件夹。这就是固件包机制的第一个体现——你要是选“是”,Keil会直接从DFP包里把对应的startup_stm32f103xe.s文件复制到你的工程目录。选“否”也没问题,它仍然会通过固件包路径引用这个文件,不影响编译。
接下来弹出的“Manage Run-Time Environment”窗口是重头戏。左侧是一堆组件列表,右侧是版本号。对于5.23版本,我们只需要勾选两项:
- CMSIS → CORE(这个提供内核寄存器定义,类似core_cm3.h这样的头文件);
- Device → Startup(启动文件,跟裸机芯片初始化有关)。
勾完之后点OK,此时在Project窗口左侧能看到工程树里已经有了Target 1,在Target 1下面会自动生成一个“CMSIS”文件夹和一个“Device”文件夹,展开后能看到core_cm3.h、startup_stm32f103xe.s、system_stm32f103xe.c这几个文件。看到这些文件,说明你的工程地基已经打好了。
3.2 编写第一个main.c:用寄存器点亮一个LED
有了地基,接下来就是往上盖楼。右键Source Group 1 → Add New Item to Group,选择C File,把文件名命名为main,点Add。然后在编辑区输入下面的代码:
#include "stm32f10x.h" void delay(void) { volatile unsigned int i; for (i = 0; i < 1000000; i++) ; } int main(void) { /* 使能GPIOC时钟 */ RCC->APB2ENR |= (1 << 4); /* 配置PC13为推挽输出,这里以开发板上的LED为例 */ GPIOC->CRH &= ~(0xF << 20); GPIOC->CRH |= (0x2 << 20); while (1) { GPIOC->ODR ^= (1 << 13); delay(); } }花十秒钟解释一下这段代码在干什么。STM32的所有外设都是挂载在总线上的,使用任何外设的第一步是打开它的时钟。RCC->APB2ENR是APB2总线的时钟使能寄存器,第4位对应GPIOC;然后我们要把PC13这个引脚配置成通用推挽输出模式,配置引脚模式的寄存器是CRH,因为PC13属于高8位引脚,所以操作CRH寄存器的bit20到bit23;最后通过翻转ODR寄存器的第13位,就可以控制LED亮灭。
先不用深究这些寄存器具体每一个位代表什么,你的目标只是先跑通编译链路。这里面体现的核心知识点是:芯片手册里看到的寄存器名字和地址,已经在stm32f10x.h头文件里定义好了结构体,可以通过GPIO这个指针来访问。
写完代码,先别急着编译。右击Target 1 → Options for Target,进入到工程配置界面,这一层有四个地方必须要设置,具体看下一节。
3.3 四个必须设置的工程配置项
配置界面的英文界面,默认打开的是Target选项卡。第一行Xtal(MHz)指的是外部晶振频率,默认改不改其实影响不大,因为后面调试器和程序都用不到这个频率值做定时运算。但为了规范,我习惯把这里改成8.0,对应开发板上的8MHz晶振。
接着点Output选项卡,这里要勾选“Create HEX File”。如果不勾这个选项,编译成功只会生成一个AXF格式的调试文件,烧录软件(比如ST-LINK Utility、FlyMcu)不能直接识别Hex格式就不行。勾上之后,编译完成后会额外生成一个.hex文件,可以直接用于烧录。
然后是C/C++选项卡。这里有两个地方需要关注:第一个是“Define”输入框。如果你用的是标准外设库,需要定义USE_STDPERIPH_DRIVER;如果像我们这样直接操作寄存器,这个Define就空着不填。第二个是“Include Paths”——头文件路径。正常情况下勾选了CMSIS组件后,Keil会自动把固件包路径配置好,不需要手动添加。但如果你在编译时遇到“file not found”之类的错误,第一反应就去这里看看,把RTE文件夹、Device文件夹路径添加进去。我在5.23版本上实测,勾选组件后路径是自动配好的,无需手动操作。
最后是Debug与Utilities选项卡。Debug里右上角选择当前使用的调试器,常用的是ST-Link Debugger,选完之后点Settings确认一下是否能识别到设备。左侧Utilities选项卡里的“Flash Download”设置一般保持默认即可,如果烧录报错“Flash Download failed - Cortex-M3”,回来检查两个东西:一是编程算法列表里有没有添加STM32F10x高密度Flash算法,二是芯片型号有没有选对。
配置好这四项,点击OK。回到编辑界面,按F7或者点工具栏的“Build”按钮,开始编译。
4. 编译输出里的门道:那些数字和警告到底意味着什么
4.1 第一次编译成功是什么样的
如果你的操作和我一致,编译完成后,Build Output窗口会显示类似下面的信息:
Build target 'Target 1' compiling main.c... linking... Program Size: Code=292 RO-data=252 RW-data=4 ZI-data=1024 FromELF: creating hex file... ".\Objects\LED_Test.axf" - 0 Error(s), 0 Warning(s).看到了吗,这就是一个合格的编译结果:0 Error, 0 Warning。很多人看到这个窗口刷一下就过了,从来不看里面的信息。但其实这里每一行都很有用。
compiling main.c表示C语言源文件被编译器翻译成机器指令;linking表示把所有目标文件和你调用的库函数打包成一个可执行文件;Program Size那一行的四个数据,Code指的是你写的程序占用了多少Flash空间,RO-data是只读数据(比如字符串常量),RW-data是已初始化的全局变量所占的空间,ZI-data是未初始化或零初始化的全局变量所占空间,如果后面学FreeRTOS,会特别关注ZI-data和栈大小之间的关系。
从ELF这个格式可以看到,Keil其实在中间环节生成的是标准ELF可执行文件,.hex只是在它基础上做了一次格式转换。这也是为什么编译成功不一定有.hex,必须勾了Create HEX File才会调用FromELF工具做转换。
4.2 从哪里看自己的固件包版本生效了
有些同学会在工程里手动添加了一个启动文件,同时又开了RTE里的Startup组件,结果编译报“duplicate symbol”之类的错误。如果你是用固件包自带版本的方式建的工程,右键一下Device文件夹里的startup_stm32f103xe.s,选择Properties,看到的文件路径是类似这样的:
C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\2.4.1\Device\Source\ARM\startup_stm32f103xe.s这就是固件包自带的文件,说明Keil直接引用了Pack目录里的源文件,而不是复制一份到你的工程目录。这种引用方式的优点是:多个工程共用一份芯片支持文件,不占磁盘空间,更新固件包版本时所有工程自动生效。缺点是:你要是把Keil安装目录删了或者搬家了,这些文件会失效。所以不要随便移动Keil安装目录。
5. 固件包工程的常见报错与查错逻辑
这部分是我最想让新手仔细看的。因为学会看报错,比学会点上那几个按钮更重要。我用几个高频报错来复盘一下排查的思路。
5.1 经典报错:cannot open source input file "core_cm3.h"
这个错误相信很多人眼熟。它出现在你用了CMSIS库函数(比如NVIC_Config、SysTick_Config这些)时,编译器找不到内核头文件。为什么会找不到?因为core_cm3.h在固件包的CMSIS目录里,而你的工程并没有把这个目录加到Include Paths。
排查路径:Options for Target → C/C++选项卡 → Include Paths。勾选了CMSIS:CORE组件之后,Keil应该自动配置好,但如果你的Keil版本比较老,或者你手动删除了工程树里的CMSIS文件夹,这个路径就会丢。解决办法有两个:点右上角的魔术棒,在Include Paths里手动添加固件包CMSIS路径,通常是:
C:\Keil_v5\ARM\PACK\ARM\CMSIS\5.0.1\CMSIS\Include或者更简单:回到Manage Run-Time Environment窗口,重新勾选CMSIS:CORE,让Keil自动恢复路径配置。我推荐第二种,因为手动配置容易配错路径导致下一次又出幺蛾子。
5.2 经典报错:stm32f10x.h(89): error: #5: cannot open source input file "stm32f10x_conf.h"
这个报错一看就是用了标准外设库的工程才可能出现。stm32f10x.h这个文件在最底部会根据STM32F10X_HD这样的宏来决定要不要包含stm32f10x_conf.h,而这个配置文件只有标准库才有,HAL库和固件包RTE机制根本不存在这个文件。如果你在C/C++的Define里填了USE_STDPERIPH_DRIVER,Keil就会去找标准库的头文件,找不到就报这个错。
对策:要么清空Define,用寄存器写代码;要么自己去下载旧版标准外设库,手动添加stm32f10x_conf.h等一整套文件。对于5.23固件包自带版本的学习路线,我的建议是放弃标准库,直接学习HAL库,或者干脆用寄存器打基础。原因后面专门说。
5.3 经典报错:error: L6200E: Symbol xxx multiply defined
这个报错的意思是符号重复定义。最典型的场景是:你听说要添加一个“delay.c”文件,于是从某个老教程里复制了一份,又自己在Source Group里添加了一个同名函数;或者你添加了标准库的stm32f10x_gpio.c,同时RTE里又自动引用了HAL库的gpio驱动。编译时链接器发现有名字一样的函数,就分不清该用哪个。
排查思路很简单:看error信息里具体列出的symbol是谁,然后在整个工程里用Ctrl+Shift+F搜索这个函数名,看看它出现在哪些文件里。把不是你自己写的那份从工程树里移除,问题解决。记住:同一个功能模块,只保留一份源代码。
5.4 关于编码与中文字符的坑
还有一个很多人问过的问题:“为什么我在Keil里注释写中文,编译之后在调试界面看全是乱码?为什么从别人那里拷贝的代码,注释显示成一堆问号?”
原因是编码方式不一致。Keil 5.23默认以ANSI编码打开文件,而很多现代编辑器(比如VS Code、Notepad++)默认用UTF-8。你在VS Code里写的中文注释,用Keil打开时就会乱码;反过来也一样。有个比较绕的坑是:编译本身不会报错,但你自己看着难受,而且有时候字符串里的中文会变成乱码影响串口输出。
我的习惯是:所有工程文件和代码文件统一使用GBK编码(Keil的ANSI在简体中文系统下就是GBK),所有中文注释都在Keil里写,不在外部编辑器里改文件编码。如果你从网上拷贝代码片段,粘贴到Keil之前,先用一个纯文本编辑器把它转成ANSI编码,粘贴完再第一时间保存。
5.5 编译木乃伊:Build Output窗口卡住不动?
有同学会问:“为什么我点编译,右下角状态栏显示rebuilding all target files,然后半个小时都没反应?”这种情况十有八九是你把整个工程目录的自动备份文件夹、或者非工程文件(比如.rar压缩包、浏览器缓存)也加进了工程树,或者Source Group里误添加了一个体积巨大的文件。编译器会老老实实处理每一个源文件,只要它在工程树里。
解决办法:在Project窗口里展开所有文件夹,仔细看看有没有不是自己添加的、大得不正常的文件。尤其是从别人电脑上拷贝的工程,很可能把旧电脑上的Local、List、Objects这些编译临时文件夹一起拷过来了。这些不要添加到工程树。
6. 固件包、标准库、HAL库到底怎么选——给自学者的一条主线建议
这一节算是我个人的一点总结和路线推荐,不一定所有人都认同,但至少是走过弯路之后觉得最省心的一条路。
很多新手被网上杂乱的资料搞得很迷茫:这教程教标准外设库,那视频用HAL库,还有人上来让你手写寄存器。这三个东西到底什么关系?
一句话概括:
- 寄存器是最底层的操作方式——直接跟芯片手册对话,看得见每一个bit。
- 标准外设库(SPL)是官方对寄存器的C语言封装,用函数名替代位操作,比寄存器好读一些,但ST早已停止维护F1系列标准库的更新。
- HAL库是今天ST主推的库,抽象层级更高,带来的好处是代码在不同芯片之间移植方便,坏处是有些细节被藏起来了,初学者容易“知其然不知其所以然”。
Keil 5.23固件包自带的方式,默认指向的是CMSIS和HAL库这条线。你在Manage Run-Time Environment窗口里勾选设备组件时,会发现Device里除了Startup,还有很多类似“STM32Cube HAL”的子组件,勾上它们,Keil会把相应HAL驱动以软件组件的方式加进工程。
我的主线路建议是这样的:先用寄存器把GPIO、时钟、中断这三个最基础的东西跑通,建立一个“芯片手册上的寄存器名 ↔ 内存地址 ↔ C语言指针”的心智模型,然后再切HAL库去完成复杂功能。标准外设库作为这个时代的过渡产物,除非你的教材硬性要求,否则不必刻意学习。
7. 把这个工程真正变成一个能烧录的固件
写到这里,工程编译已经通过了,但如果你用的是开发板,还差最后一公里——烧录。我用蓝桥杯嵌入式开发板(CT117E)上的STM32F103RBT6为例,说一下烧录中的几个关键点。
板载的ST-Link通过USB连接电脑之后,确认设备管理器里能看到一个“STMicroelectronics STLink dongle”设备。然后在Keil的Options for Target → Debug → 右侧下拉选择ST-Link Debugger,点Settings,如果能看到SW Device列表里出现一个ARM CoreSight SW-DP设备,说明调试器连接正常。接下来点Load按钮,编译器会先用当前工程的配置调用烧写算法,把生成的.hex文件通过ST-Link写入芯片Flash。
烧录完成后,开发板上LED应该开始闪烁。如果LED不亮,先别急着怀疑芯片坏了,按这个顺序排查:先看PC13引脚有没有接触不良,再用万用表量LED两端电压是否在3.3V左右跳变,最后用调试器进入仿真模式,单步执行到GPIO->ODR那行代码,看看寄存器的值是不是在翻转。硬件问题用软件手段也能定位,这是嵌入式调试一个很有意思的地方。
如果没有ST-Link,用串口ISP方式烧录也可以。STM32F103的Boot0引脚拉高并复位后,芯片进入ISP模式,用USB转TTL模块连接PA9(TX)、PA10(RX)和GND,打开FlyMcu软件,选择生成的.hex文件,设置波特率115200(也可以设为更低提高成功率),点开始编程。这种方式适合没有仿真器的低配玩法,缺点是调试功能用不了,定位代码问题只能靠串口打印或者LED指示。
8. 最后,把我踩过的坑集中说一遍
新手学STM32,最不缺的就是“道理我都懂,但就是编译不过”。以下三个坑,都是我自己当初踩过、或者陪别人调项目时反复见过的,写出来希望你避开。
坑一:把Keil工程整个目录直接拷贝到另一台电脑上编译报错。原因是工程文件里保存了本机的绝对路径,换电脑后路径失效。解决办法是找到目标文件里的“.uvprojx”文件,用文本编辑器打开,把里面的“..\..\”这类相对路径检查一遍,或者干脆在另一台电脑上重新用Pack机制新建一个工程,把main.c和头文件复制过去。稳定可靠,永不翻车。
坑二:选芯片时错选了Flash体积不匹配的型号。比如你的芯片是STM32F103C8T6,只有64KB Flash,但你在Keil里选了STM32F103RCT6(256KB Flash),编译出来的hex也能烧进去,但烧录算法不匹配,可能导致程序跑飞。虽然C8T6和RCT6内核一样、引脚兼容,但烧录算法文件不一样,这点一定不要搞错。
坑三:因为贪图“最新版”去装MDK 5.38并混用AC6编译器,结果老教程里的代码编译出一堆警告。不是说新版不好,而是5.23 + 固件包这条链路,有海量的教程、帖子、社区回答验证过,你遇到任何问题都能搜到现成的答案。自学阶段,稳定比“新”更重要。
写到这里,一个能用固件包自带的芯片支持文件编译通过的STM32工程就已经建立起来了。从选芯片、勾选组件、写点灯代码、配置工程选项到成功烧录,整个过程再走一遍可能只需要五分钟。以后新建其他项目,不需要再从零看教程,只需要重复这一套流程,换一下芯片型号,你就会发现,Keil 5加固件包的方式,确实是目前对自学者最友好的一套环境。如果过程中碰到具体的报错信息,欢迎留言,我会尽量针对性地给出排查思路。