1. 动手之前先想清楚:你真的需要一份超详细建工程指南吗
网上关于Keil建工程的教程多到数不清,但你会发现一个现象:大部分教程要么是"下一步、下一步、下一步"的截图流水账,要么直接甩给你一个现成工程模板让你改。真到了自己从零开始建一个裸机工程时,新人照样一头雾水——芯片型号选哪个?启动文件是什么?为什么别人代码里能include "stm32f10x.h"而我这边报错?为什么点了下载按钮却提示找不到设备?这些问题截图流水账根本不会告诉你。
我写这篇指南的出发点很简单:把ARM Cortex-M系列芯片在Keil MDK下从安装环境、创建工程、配置调试器到跑通点灯的全过程,按我自己实际操作的顺序完整走一遍,并且把每一步背后"为什么这么做"也讲清楚。这篇内容主要适合三类人:刚接触嵌入式开发、准备从教程视频转向自己建工程的学生;从其他IDE(比如IAR、PlatformIO)转过来想快速上手Keil的开发者;以及建过工程但一直靠复制模板、出了问题不会排查的工程师。看完之后你应该能独立完成一个真实可用的工程骨架,而不是只会照着截图点鼠标。
我默认你用的是Windows系统,目标芯片以STM32F103系列为主,同时会说第三方芯片(比如GD32、瑞萨RA)和Cortex-M内核通用的部分。Linux下通过Wine跑Keil不是常规操作,真要在Linux下做Cortex-M开发,原生方案我更推荐直接用命令行工具链加CMake,这个话题展开又是一篇长文,这里不赘述。
2. 装对环境:MDK版本、器件支持包和编译器三件事一次说清
2.1 MDK版本怎么选:5.37是个风水岭
Keil MDK的版本选择不像装个微信那么简单,里面有个关键变量叫ARM Compiler(ARM编译器)。MDK 5.36及之前的版本,你可以在工程里同时使用AC5(ARM Compiler 5)和AC6(ARM Compiler 6);从MDK 5.37开始,官方默认不再打包AC5,虽然还能通过单独安装的方式补回来,但官方主推AC6的意图已经非常明显了。
为什么这个区别重要?因为AC5和AC6的C语言标准支持、语法检查严格程度、编译优化策略差异很大。很多老工程、老教程、第三方开源代码(尤其是早期标准库项目)是拿AC5写的,你用新版MDK默认的AC6去编译,常常冒出一堆warning甚至error——最常见的比如"#pragma pack"的用法差异、某个隐式类型转换的告警。反过来说,AC6对C99和C11的支持更好、编译速度更快、优化后的代码密度也更好,新项目没有历史包袱我都是直接用AC6。
我的个人建议是:全新安装直接上MDK 5.37及以上版本,新工程一律用AC6;如果公司项目或学校实验要求沿用老工程,再考虑装5.36双编译器版本。这个选型直接影响后面工程配置里的编译器切换,所以放在第一步说。
2.2 许可证:先搞清楚评估版限制再谈其他
Keil MDK的许可证问题绕不开,我直接说结论:不注册的评估版(Evaluation Mode)代码容量限制在4KB以内,超过4KB程序就无法编译通过。4KB是什么概念?一个稍微完整点的点灯程序加上初始化代码大概一两KB,还能跑;一旦你开始用标准外设库、HAL库或者FreeRTOS,轻松突破4KB,后编译就直接报"code size limit"错误。
处理许可证的方式我见过很多弯路,有人找注册机去破解,且不说安全风险,现在的MDK授权验证逻辑已经比以前复杂,很容易把安装环境搞坏。正规途径其实没那么难:学生和老师可以通过学校邮箱申请教育授权,一年一续,完全免费;商业用途建议直接买MDK-Plus或MDK-Professional,价格不算便宜但对公司来说属于正常研发工具成本。还有一种临时方案是使用MDK自带的30天全功能试用,到时间会退回评估模式。
提示:无论用哪种授权方式,装完MDK后第一时间在IDE里确认License状态,路径是File → License Management,里面能看到当前授权类型和到期时间。别等编译报错了才回来查这个。
2.3 Device Pack:工程创建前的隐藏前置条件
很多新手装了MDK后立刻新建工程,结果在芯片选择界面找不到自己的MCU型号,第一反应是"我的MDK坏了"。其实不是,MDK 5之后的架构把芯片支持从IDE主体里拆出来了,变成了独立的Device Pack(器件支持包)机制。MDK安装包本身只包含调试器和少量ARM官方评估板的支持,你要用的具体芯片型号——比如STM32F103C8T6——需要单独从Pack Installer里下载对应的DFP(Device Family Pack)。
打开Pack Installer的方式很简单:在MDK的工具栏点击绿色魔方图标,或者在项目里选择Project → Manage → Pack Installer。在里面按芯片厂商筛选,找到"Keil::STM32F1xx_DFP"(这是ST官方在Keil体系下的包名),点击Install即可。安装完成后,新建工程时的芯片选择列表里才会出现完整的STM32F103系列型号。
GD32用户注意:兆易创新的GD32在Keil里也有独立Pack,不包含在ST的包里。你需要在Pack Installer里搜索"GD32"或去GD官网下载对应的DFP文件手动安装。第三方国产芯片的Pack安装逻辑都一样:要么在Pack Installer在线搜索,要么在芯片厂商官网下载.pack文件后,双击打开或者用Pack Installer的File → Import菜单导入。
3. 空工程到点灯:一次完整的工程创建全流程
3.1 新建工程时的两个关键弹窗怎么填
打开Keil MDK,选择Project → New uVision Project,把工程文件(.uvprojx)放在你规划好的项目根目录里,比如D:\WorkSpace\LED_Demo。这个时候会弹出两个极其关键的对话框,第一个是芯片选择界面。我见过很多人在这里随便选一个型号就往下走,结果后面发现Flash容量、SRAM大小跟实际芯片对不上。Cortex-M内核的芯片选型必须先看丝印或原理图,确认具体的子型号——以STM32F103C8T6为例,你要在STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8下面选中它,而不是选成C6、CB或者别的密度版本,Flash和SRAM配置直接从启动文件就开始依赖这个选择。
第二个弹窗是"Copy STM32 Startup Code to Project Folder?"——询问是否把启动文件复制到工程目录。这里的建议是选择"Yes"。启动文件(.s后缀文件)负责设置初始栈指针、调用SystemInit、执行中断向量表和C运行时环境初始化,是整个工程能够正确运行的起点。选择复制到工程目录的好处是你后面可以直接查看和修改它,也不会因为MDK更新导致行为不一致。
如果你不幸选了"No",后面就得自己手动把启动文件(.s)和分散加载文件(.sct)资产到工程里,对新手来说完全没有必要给自己加这个难度。
3.2 最小工程的三件套:main.c、启动文件和链接脚本
新建工程后,MDK会自动生成一个目标文件夹(默认叫Target 1),里面只有一个空的Source Group。你需要手动添加源文件。右键Source Group → Add New Item to Group,选择C File(.c),命名main.c,这样第一个源文件就进工程了。
然后打开main.c,敲一段最简代码:
#include "stm32f10x.h" int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); while (1) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); for (volatile int i = 0; i < 1000000; i++); GPIO_SetBits(GPIOC, GPIO_Pin_13); for (volatile int i = 0; i < 1000000; i++); } }写完直接编译,你会发现报错:无法打开"stm32f10x.h"。这里就引出了标准外设库和寄存器开发的选择问题了。如果你打算用寄存器操作,这个头文件可以是你自己写的寄存器定义文件;但绝大多数人做STM32F103会倾向于用ST的标准外设库(STD Periph Library)或HAL库。标准库的方式是把库源码文件一并加入工程,并在C/C++编译器选项里添加宏定义:STM32F10X_MD(对应中等密度芯片)和USE_STDPERIPH_DRIVER,同时把库的头文件路径加入Include Paths。
这里我不展开整个库函数的移植过程,但你要理解一个核心逻辑:所谓"建工程",本质上是把——启动文件 + 链接脚本 + 芯片头文件/外设库源文件 + 应用代码——四样东西正确地组织进同一个工程里,并让编译器知道去哪里找它们。这个理解了,后面用HAL库、LL库、甚至FreeRTOS,都是同一套组织方法的变体。
3.3 工程选项里的必改项:Output、C/C++、Debug和Utilities
新建工程后千万不要急着写代码,先把工程选项(魔术棒按钮)里的几个关键页签设置好。我按修改顺序过一遍:
Output页签:勾选"Create HEX File",这一步对应热搜词里"keil uvision5怎么烧录hex文件"的问题。生成Hex后,你可以用任意第三方烧录工具(如ST-LINK Utility、J-Flash)烧录,不依赖Keil的下载按钮。
C/C++页签:这里是新手报错重灾区。Preprocessor Symbols里要添加正确的宏定义,比如标准库工程要加STM32F10X_MD和USE_STDPERIPH_DRIVER;Include Paths里必须包含所有头文件所在的目录,比如标准库的inc目录和你自己写的头文件目录。路径用相对路径还是绝对路径,行业惯例是用相对路径,这样整个工程复制到别的电脑不会失效。操作时点开Include Paths后面的"..."按钮,逐条添加即可。
Debug页签:默认是Use Simulator(软件仿真),这没什么问题,但你接上调试器后想下载和单步调试,就必须改成Use UlLink Pro等对应调试器选项。这里具体怎么选,我放到第五章详细讲。Utilities页签:默认勾选的"Use Debug Driver"可以保持,这样点下载按钮时会自动调用于Debug页签配置的调试器进行Flash烧录。
提示:很多教程让你加一堆Define、加一堆Include路径,你不理解就在那机械复制。这里可以验证你是否真的懂了——如果你写代码时include了一个头文件,编译报"cannot open source input file",绝大多数情况就是头文件所在目录没加入Include Paths,而不是头文件不存在。想通这一点,编译错误里很大一类问题你就能自己解决了。
4. 工程目录和启动文件:一个耐用的工程模板该怎么搭
4.1 按功能拆分目录而不是一锅炖
以我现在的工程习惯为例,一个稍微正式点的Cortex-M裸机工程我会这样组织目录:
LED_Demo/ ├── Doc/ // 项目文档、数据手册、原理图 ├── Core/ // 启动文件、系统初始化文件 │ ├── startup_stm32f103c8tx.s │ ├── system_stm32f10x.c │ └── core_cm3.h ├── Driver/ // 自己写的外设驱动 │ ├── bsp_led.c │ └── bsp_led.h ├── Device/ // 芯片头文件,标准库或HAL库的核心头文件 │ └── stm32f10x.h ├── StdPeriph/ // 标准外设库的src和inc(如果用HAL库这里对应HAL驱动) ├── App/ // 应用层代码 │ └── main.c └── Output/ // 编译产物的输出目录我见过不少人直接把所有.c文件堆在一个文件夹里,头文件路径混乱,改一个芯片型号时整个工程跟着乱。这个目录结构不是强制的,但"核、驱动、应用、输出分开"的原则建议尽早养成,尤其在团队协作或者一个工程要维护三五年的场景下,好处会在后期集中体现。
4.2 启动文件到底做了什么
新手最不理解的就是启动文件(.s)为什么存在。用一句话概括:Cortex-M处理器上电后从向量表取出复位向量,跳转到复位处理函数,复位处理函数里要完成栈初始化、数据段和BSS段搬运、调用SystemInit初始化时钟,最后才跳到C的main函数。这一整套流程,在老式8位单片机上是编译器环境自动处理的,在Cortex-M上则显式由启动文件和系统初始化文件完成。
以STM32F103的启动文件为例,除了设置堆栈大小、定义中断向量表,还会调用SystemInit()函数(如果你没有用汇编方式在启动文件里强制调用,那么SystemInit通常由system_stm32f10x.c提供,启动文件中通常有一段注释提醒你需要自己调用)。这就是为什么ARM官方CMSIS包里一定包含system_xxx.c和core_cmx.h——前者负责系统时钟初始化,后者是Cortex-M内核的寄存器定义和访问接口。两者缺一,工程跑不起来或者跑起来时钟频率不对。
4.3 用CMSIS还是用标准库还是HAL库
这三个选择是STM32开发绕不开的一个三岔路口。CMSIS是ARM官方定义的Cortex-M软件接口标准,它提供的是内核寄存器操作,不直接管外设;标准库(StdPeriph)是ST官方早期为STM32F1/F2/F3等系列推出的外设驱动库,直接操作寄存器封装的函数,代码执行效率高、学习曲线陡一点;HAL库是ST后来主推的抽象层库,代码可移植性好、配备图形化配置工具CubeMX,但代码体积大、间接层多。新项目我一般推荐HAL库+CubeMX生成代码骨架,但如果你想真正搞懂芯片内部工作机理,花时间读标准库源码比读HAL库源码值得多。我写这篇指南之所以偏标准库,是因为它的源码就是最好的寄存器中文教材,而且创建工程时涉及到的文件组织方式,比CubeMX全自动生成更接近"理解工程本质"的路径。
5. 调试器配置与烧录:让程序真正跑起来
5.1 ST-Link、J-Link、CMSIS-DAP和ULINK到底怎么选
热搜词里有"keil 5 报no ulink devivc found"和"keil uvision 5中debug配置stlink时闪退"这类问题,其实都出在对调试器类型和连接方式的设置上。
Debug页签里,选择调试器时你首先要确认手里调试器的协议族:ST-Link是ST自家调试器,支持全系列STM32和部分其他ST芯片,J-Link是SEGGER出的通用调试器,支持绝大多数Cortex-M芯片,CMSIS-DAP则是一大波第三方调试器(比如DAPLink、离线烧录器、某些开发板板载调试器)遵循的CMSIS标准调试协议,ULINK是Keil自家认证的调试器,兼容性和稳定性都不错但价格和生态已经不如前几者普及。
选择之后,还要点开右侧的Settings按钮,确认两件事:一是调试器是否被正确识别,二是SW设备列表里是否能扫到目标芯片。SW设备列表空白,九成原因是物理连接问题——SWDIO、SWCLK、GND三根线接了吗?目标板供电了吗?或者目标板处于复位状态导致扫描不到内核。别急着怀疑调试器坏了,先用万用表量一遍连线是排查这类问题最优先的步骤。
5.2 ST-Link烧录配置与常见失败原因
在Debug页签选择ST-Link Debugger后,点Settings,在Debug子页右下角确认Port为SW(不是JTAG),Max Clock可以拉低到1MHz试稳定,尤其你手头面包板飞线一长串的时候,高速SWD容易信号反射导致连接不稳定。然后切到Flash Download页签,这里要配置烧录算法(Programming Algorithm)。STM32F103C8T6要添加"STM32F10x Med-density 128K Flash",并且勾选"Reset and Run"——如果不勾,程序烧录后不会自动复位运行,你还要手动按板子上的复位键才能看到效果。
"RDDI-DAP Error"、"Flash Download failed - Cortex-M3"这类错误我见过很多次,绝大多数原因就是烧录算法选错(选了Low-density或者High-density)、芯片Flash读保护没有解除、或者接线接触不良。对应排查步骤:检查算法是否匹配、用ST-LINK Utility或CubeProgrammer尝试连接并去除读保护、换更短的杜邦线或重新插拔。
5.3 调试窗口的进阶技巧:结构体变量、Watchdog和查看外设寄存器
热搜词里"keil调试助手里面的debug模式如何显示结构体变量"是个高频问题。其实答案很简单:在Debug模式下,左边Registers窗口下方有个Watch窗口(视图 → Watch / Call Stack Window),在Watch 1里右键添加变量名,直接输入结构体变量名然后回车,它就会以树形展开显示成员。如果你希望永远只观察某个结构体的特定成员,可以手动展开后右键"Add to Watch"。还有一点很多人不知道:结构体指针变量在Watch窗口里默认只显示地址,你需要用"(类型名)变量名"的语法才能强制解引用查看内容。
还有一个在调试时非常容易踩的坑:你用硬件调试器跑程序,断点打在主循环里,但程序一跑就复位或者跑到HardFault_Handler里去了。常见原因之一是看门狗(IWDG/WWDG)在调试模式下没有被冻结——Cortex-M内核调试寄存器里有个DBGMCU_CR寄存器的DBG_IWDG_STOP位可以设置在调试时暂停看门狗计数。MDK在连接调试器时默认不会自动配置这个位(除非你在初始化代码里设置),所以你在单步调试时看门狗照样计数,时间一长溢出就复位了。这类问题很隐蔽,排查起来费时间,知道原理后可以在初始化代码里主动配置。
6. 编译报错与运行异常:我见过最多的几种坑
6.1 经典编译错误:"cannot open source input file"与"undefined symbol"
先说不只是新手、老手也常遇到的"cannot open source input file"的报错。这个报错信息基本已经把问题告诉你——某个头文件打不开。但具体是哪一个?双击报错会跳到引用该头文件的那一行代码。大部分情况就是Include路径没配好,配好之后问题立刻消失。少数情况是头文件本身确实不存在,比如用错库版本、文件没放进工程目录。
另一类高频问题:"undefined symbol"或者链接时的"L6218E: Undefined symbol"。这表示一个函数或者全局变量声明了、使用了,但链接器最终没有找到它的定义。典型原因:源文件没有加入工程(你声明了一个.c文件里的函数,但没有把那个.c文件添加到Source Group里);库文件路径不对;或者函数名拼写错了(编译器最容易被大小写不同的函数名绕进去)。这类问题的排查思路:先双击报错确认是哪个符号,然后在工程里全局搜索该符号的函数定义体是否存在,加了对的.c文件后重新编译。
6.2 编译通过但运行乱跑:启动文件和芯片型号不匹配
编译零错误零警告,烧录成功后芯片却没反应,或者运行到一半进HardFault,这个问题比编译错误难查得多。第一排查对象就是启动文件与芯片型号的匹配关系。不同Flash容量的STM32F1芯片,中断向量表里的外设中断数量不同,如果小容量芯片配了大容量的启动文件,启动文件引用了不存在的外设中断向量,程序运行到异常时会跳到一个无效地址。另外,SysTick、NVIC优先级分组这些初始化如果配置错了,也会让程序看起来在跑但行为完全不对。排查方式是把启动文件换成芯片对应的原版,确认system_stm32f10x.c里的时钟配置与你板子的晶振一致(常见8MHz外部晶振,但有些板子是25MHz,这个数值直接决定系统主频是不是你预期值)。
6.3 界面、注释和编码:中文乱码问题
"keil uvision5怎么改成中文"这个热搜我看到过很多次,可能包含两种诉求:一是想把IDE界面汉化,二是代码里中文注释变成乱码。界面汉化我不推荐,因为大部分中文教程、技术文档里的菜单名都是以英文界面为准的,汉化反而增加沟通成本。乱码问题倒很实际:Keil 5默认用ANSI编码保存文件,而很多现代编辑器用UTF-8,如果你用VS Code写了带中文注释的文件再拿到Keil里打开,中文就会变成乱码。反之亦然。解决办法是统一编码:建议在Keil里把Encoding设为UTF-8(Edit → Configuration → Editor → Encoding → UTF-8),然后所有代码文件统一用UTF-8保存。如果你接手的老工程一直用GB2312/ANSI,那就别改成UTF-8了,保持工程内编码统一比什么都重要。
6.4 把工程文件复制到别处后出现的玄学问题
还有一类问题跟工程文件本身有关。.uvprojx工程文件里保存了绝对路径和相对路径,如果你把整个工程目录从一台电脑复制到另一台电脑,或者改了目录名,启动文件、库文件、头文件路径全可能失效。这时最常见的错误是"file not found"一类的编译错误。解决办法是新建工程时全用相对路径,并且把工程目录完整复制(不要只复制.uvprojx,连带Core、Device、Driver等整个结构都复制过去)。还有一个隐藏坑:.uvguix文件是界面布局配置,不同版本MDK可能不兼容,复制时也不用管它,MDK会自动重建。
7. 让Keil更好用的几个补充工具与技巧
7.1 代码格式化、静态检查和Git忽略
写代码不用格式化工具的人,迟早会碰到一个风格乱七八糟的工程,要么是自己一周前写的,要么是同事写的。Keil本身没有内置自动格式化,但可以通过External Tools方式集成Astyle,这也是热搜词里"astyle (keil代码自动对齐工具)"的来源。用法不复杂:在Keil的Tools → Customize Tools Menu里添加一条命令,Command指向astyle.exe,Arguments填--style=allman -s4 -f -S -N -L -m0 -p -H -j -c -k3 -U -W1 -xn -xc -xl -xL -xC80 %E,然后你要做的就是在每个源文件里点一下自定义工具菜单,代码风格立刻统一。Astyle的参数非常多,"allman"风格是嵌入式和Linux内核最常见的风格,缩进4空格,我自己真是离不开它。
Cppcheck是另一个我从项目里受益很多的工具:静态代码分析,不实际执行程序就能发现很多编译器注意不到的warning——内存泄漏、数组越界、变量误用、逻辑漏洞。在Keil里同样通过Custom Tools Menu集成,命令行大致是cppcheck --enable=warning,style,performance,portability --std=c99 --language=c --suppress=missingIncludeSystem -I"路径" *.c。它和编译器互补,编译器告诉你"这个语法对不对",Cppcheck告诉你"这段逻辑是不是有隐患"。
用Git管理工程代码的人,记得在.gitignore里加上这几项:.uvguix、.uvopt、Listings/、Objects/、*.crf、*.d、*.o、*.axf、*.htm。工程文件.uvprojx要进仓库,但构建产物和界面布局文件没意义,别一股脑全提交。
7.2 从Keil到VS Code:白天VS Code写代码,晚上Keil编译调试
如果你觉得Keil的编辑器写代码太痛苦——补全慢、不智能、界面老旧——可以试试"VS Code写代码+Keil编译调试"的工作流。Keil从5.25版本开始在工程文件中支持名为CMSIS-View的机制,而社区里有几个开源插件(比如Keil Assistant、Embedded IDE)可以直接读取和修改.uvprojx工程文件,在VS Code里实现代码补全、语法检查、一键调用Keil命令行工具编译,报错还能直接在VS Code里定位跳转。我在几个跨平台项目里用了这套组合,体验比在Keil里敲代码舒服得多。不过要注意:单步调试和查看外设寄存器还是得回到Keil里,VS Code侧目前大多是把Keil的UV4命令行编译和烧录功能代理出来。
7.3 为什么有人转PlatformIO、有人又转回来
最后聊一下我自己的工具体会。PlatformIO这几年很火,尤其做ESP32、STM32的一些开发者喜欢用,因为它的包管理和跨平台体验确实优秀。但热词里还有个"platformio创建工程慢",这个我深有体会:PlatformIO第一次创建工程时需要下载若干GB的工具链、框架源码和编译器,网络不好时体验极差。相比之下Keil创建一个新工程,只要Device Pack装了,点几下就出来了。我的态度是:工具没有绝对好坏,取决于你的场景——做产品原型、稍微复杂点的裸机/RTOOS项目,Keil的调试体验依然是嵌入式IDE第一梯队;做跨平台、持续集成、开源项目,PlatformIO的工具链抽象和解耦价值更明显。两条路都值得走一遍,最终落脚点还是你对Cortex-M本身的掌握程度。
最后分享一个小技巧。我建每个新工程时,会在工程根目录放一个README.md,里面写着这个工程用的IDE版本、编译器版本、芯片型号、时钟频率、调试器型号、关键外设初始化位置。三个月后回头维护或者交接给同事时,这个文件比任何代码注释都省时间。Keil工程这种"谁建谁清楚"的项目,信息沉淀比工具本身更值钱。