手把手教你从零开始用Keil搭建Cortex-M项目
你是不是也经历过这样的时刻:手头有一块STM32开发板,下载好了Keil MDK,点开软件却不知道从哪下手?新建工程时面对一堆选项一头雾水,点了“下一步”又怕配错,不点又没法继续——这几乎是每个嵌入式新手必经的坎。
别担心,今天我们不讲大道理,也不堆术语,就像师傅带徒弟一样,一步步带你把一个能跑起来的Cortex-M工程搭出来。过程中你会明白:为什么要有启动文件?链接脚本到底管什么?宏定义为啥非得加?搞懂这些,以后换芯片、换平台都能举一反三。
一、先搞清楚我们要建的是个啥?
在动手之前,得知道你创建的不是一个简单的.c文件,而是一个“嵌入式系统”的起点。它要完成的任务是:
- 上电后CPU第一件事做什么?
- 堆栈放在哪里?
- 代码烧到Flash哪个位置?
main()函数之前发生了什么?
这些问题的答案,都藏在你新建的这个工程里。Keil不是魔法工具,它只是帮你把这些底层配置组织好,最终生成一个可以写进单片机的二进制文件(比如.hex或.bin)。
所以,“新建工程”本质上是在告诉编译器和链接器:
“我的芯片长什么样?内存怎么分布?程序从哪儿开始?请按我说的来打包。”
明白了这一点,你就不会再把它当成“点几个按钮就行”的操作了。
二、第一步:打开Keil,真正的新手第一步
- 打开Keil µVision(版本推荐V5以上)。
- 菜单栏选择
Project → New uVision Project。 - 弹出窗口让你选保存路径,建议专门建个文件夹,比如:
/MyFirstBlink/ ├── Project/ ← 工程放这里 ├── Src/ ← 后面自己建 ├── Inc/
输入工程名,比如Blink_LED,点击保存。
这时候Keil会立刻跳出来一个对话框:Select Device for Target ‘Target 1’。
关键来了:选对芯片型号!
- 展开厂商列表,找到你的MCU厂家,比如
STMicroelectronics。 - 找到具体型号,例如
STM32F103C8(常见于蓝pill开发板)。
⚠️ 小提示:如果你不确定型号,先查开发板资料。选错芯片可能导致后续无法下载程序或时钟配置错误。
选完之后,Keil会自动做两件事:
- 加载该芯片的基本参数(Flash/RAM大小、默认时钟等);
- 准备好对应的Flash编程算法(后面烧录要用)。
三、要不要启动文件?必须要有!
接下来弹窗问你:“Copy STM32F10x Standard Peripherals Library files?”之类的提示(不同版本略有差异),但最关键的是是否添加启动文件(Startup File)。
✅一定要选“Yes”!
Keil会自动给你加入一个类似这样的文件:
startup_stm32f103xb.s这个名字里的xb对应的是中等容量Flash的F1系列芯片(64KB~128KB)。如果你用的是STM32F103C8,它的Flash是64KB,正好匹配。
启动文件到底干了啥?
你可以把它理解为“单片机起床后的洗漱流程”。上电瞬间,CPU做的第一件事就是读取这个文件中的中断向量表:
AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler ...注意前两个:
- 第一项是初始堆栈指针(SP),指向RAM最高地址;
- 第二项是复位处理函数(Reset_Handler),CPU接下来就跳到这里执行。
没有这个文件,或者内容不对,你的程序根本跑不起来——连main()都到不了。
四、工程结构怎么组织?别让文件满天飞
现在工程有了,但我们还没写代码。先别急着敲main(),先把目录理清楚。
右键左侧“Source Group 1”,重命名为Src。然后在项目根目录下手动创建两个文件夹:
-Src/—— 放所有.c源文件
-Inc/—— 放所有.h头文件
接着,在Inc/里新建一个空头文件main.h,在Src/里新建main.c,并将其添加到工程中(拖进去或右键Add)。
此时你的工程结构应该是这样:
Project/ ├── Blink_LED.uvprojx ├── Src/ │ ├── main.c │ └── startup_stm32f103xb.s ├── Inc/ │ └── main.h规范的结构能让后期维护轻松十倍。
五、最关键的一步:配置工程选项(Options for Target)
这是最容易出问题的地方,也是最值得花时间理解的部分。
右键左侧“Target 1” → “Options for Target…”,进入设置界面。我们逐个标签来看:
1. Target 标签页
XTAL (MHz):填写外部晶振频率。比如你的板子接的是8MHz晶振,就填8.0。
这个值会影响后续HAL库中SysTick定时器的计算,务必准确。
Use MicroLIB✅ 勾上!
MicroLIB 是Keil提供的轻量级C库,比标准库小很多,适合资源紧张的MCU。而且它支持半主机(semihosting),调试时可以用printf输出到串口。
2. Output 标签页
✅ 勾选Create HEX File
很多烧录工具(如FlyMCU)只认
.hex文件,勾上方便后期独立烧录。可以修改输出路径为
./Output文件夹,整洁管理。
3. C/C++ 标签页
这里是编译器的大脑所在,三个关键设置:
(1)Define 宏定义
添加芯片标识宏,格式如下:
STM32F103xB注意:不同型号后缀不同。
C8和CB属于xB系列;RCT6则是xE。
为什么要加这个?因为ST的官方库(如HAL)靠这个判断你是哪个芯片,从而包含正确的寄存器定义和初始化代码。
你可以在stm32f1xx.h中看到类似代码:
#if defined(STM32F103xB) #include "stm32f103xb.h" #elif defined(STM32F103xE) #include "stm32f103xe.h" #endif没定义 → 找不到头文件 → 编译报错。
(2)Include Paths
点击右侧图标,添加以下路径(假设你用了STM32Cube生成的库):
.\Inc Drivers\CMSIS\Include Drivers\CMSIS\Device\ST\STM32F1xx\Include Drivers\STM32F1xx_HAL_Driver\Inc这样编译器才能找到#include <stm32f1xx.h>和#include "main.h"。
(3)C99 Mode
✅ 勾选Enable C99
允许使用现代C语法,比如:
for (int i = 0; i < 10; i++) { ... } // C99才支持在for中定义变量不然编译会报错。
4. Debug 标签页
连接调试器(如ST-Link、J-Link)后,选择对应调试器:
- 点击ST-Link Debugger
- 点击 Settings → Debug → Enable “Run to main()”
这个功能非常实用:每次下载程序后,调试器会自动运行到main()函数开头停下,而不是卡在汇编启动代码里。
5. Utilities 标签页
勾选Use Debug Driver,并确保下面选择了正确的Flash编程算法(Keil通常会自动识别)。
如果出现“No Algorithm Found”,说明没加载Flash算法,检查芯片型号是否选对。
六、链接脚本(Scatter File):内存地图谁说了算?
Keil默认使用内部链接规则,但对于复杂项目,最好自己写一个.sct文件来控制内存布局。
比如你的STM32F103C8有:
- Flash:64KB,起始地址0x08000000
- RAM:20KB,起始地址0x20000000
创建一个linker.sct文件,内容如下:
LR_IROM1 0x08000000 0x00010000 { ; 64KB Flash ER_IROM1 0x08000000 0x00010000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; 20KB SRAM .ANY (+RW +ZI) } }然后在Options → Linker → Use Memory Layout from Target Dialog不勾选,改为勾选Use Custom Scatter File,指定这个文件。
作用是什么?
- 确保复位向量表在Flash最前面;
- 把全局变量(.data)复制到RAM;
- 零初始化段(.bss)清零;
- 堆栈空间预留足够。
否则可能出现“变量没初始化”、“malloc失败”等问题。
七、写个最简main函数,验证能不能通
在main.c中写下最基础的LED闪烁代码:
#include "stm32f1xx.h" #include "main.h" void delay(volatile uint32_t count) { while (count--) __NOP(); } int main(void) { // 使能GPIOA时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // PA5设为推挽输出(LED常用引脚) GPIOA->CRL &= ~GPIO_CRL_MODE5; GPIOA->CRL |= GPIO_CRL_MODE5_1; // 输出模式,最大速度10MHz GPIOA->CRL &= ~GPIO_CRL_CNF5; // 推挽输出 while (1) { GPIOA->BSRR = GPIO_BSRR_BR5; // LED off delay(1000000); GPIOA->BSRR = GPIO_BSRR_BS5; // LED on delay(1000000); } }这段代码直接操作寄存器,不依赖HAL库,编译快、体积小,适合验证工程环境是否正常。
八、编译 → 下载 → 看灯闪!
点击顶部的Build按钮(锤子图标)。
如果没有错误(0 Error(s), 0 Warning(s)),恭喜你,工程结构没问题!
接下来:
1. 用ST-Link将开发板连接电脑;
2. 点击Load按钮(向下箭头),程序就会烧录进Flash;
3. 点击Start/Stop Debug Session(虫子图标),进入调试模式;
4. 观察是否停在main()函数开头;
5. 按F5全速运行,看看LED有没有开始闪烁。
💡 如果灯亮了——你已经跨过了嵌入式开发的第一道门槛!
九、那些年我们都踩过的坑:常见问题与应对
| 问题 | 表现 | 解决方案 |
|---|---|---|
编译报错"identifier 'xxx' is undefined" | 提示找不到寄存器名 | 检查是否定义了STM32F103xB宏 |
| 程序下载失败 | 显示“No target connected” | 检查SWD连线、供电、复位电路 |
| LED不闪,程序卡住 | 调试发现停在HardFault_Handler | 大概率堆栈溢出或非法访问,检查Stack_Size |
| 全局变量始终为0 | 数据没从Flash复制到RAM | 检查scatter文件中是否有.data段处理 |
| printf不输出 | 串口无打印信息 | 启用MicroLIB,并实现fputc函数 |
其中最隐蔽的问题之一是HardFault。建议你在工程中保留一份HardFault_Handler的调试版本:
void HardFault_Handler(void) { __disable_irq(); while (1) { // 在这里打断点,查看调用栈 } }配合调试器,能快速定位非法内存访问或栈溢出。
十、给未来的你留条路:最佳实践建议
当你熟练之后,不妨养成这几个习惯:
模板化工程结构
成功跑通一次后,备份成“通用模板”,下次直接复制,省去重复配置时间。纳入Git版本控制
提交.uvprojx,.c/.h,.sct等核心文件,忽略.uvoptx,Objects/,Listings/等临时文件。尽量使用CMSIS标准接口
比如用SystemCoreClock变量代替硬编码时钟值,提升可移植性。命令行构建准备
Keil提供UV4命令行工具,可用于自动化构建:bash UV4 -b Project.uvprojx -o build.log
为将来接入CI/CD流水线打好基础。
最后一句话
你现在搭的不只是一个LED工程,而是通往嵌入式世界的大门钥匙。每一个成功的“新建工程”,都是你对底层机制理解更深一层的结果。
下次当你看到别人几分钟搞定工程搭建时,别羡慕——因为你已经知道,那背后藏着多少看不见的细节。
如果你在实践中遇到其他问题,欢迎留言交流。我们一起把这条路走得更稳、更远。