1. 别急着新建工程:先把 Keil5 里三层"库"的关系捋直
我见过太多人在这一步折戟:Keil5 装好了,破解也做了,右键 New uVision Project,弹出器件选择框,在搜索栏里敲STM32F103,结果列表空空如也。于是开始怀疑是不是装了个假 Keil,或者以为要去某个论坛下个"Keil5 全器件包"。其实绝大多数情况下,问题不在 Keil 本身,而是器件支持包没有安装,和芯片库文件没有导入这两件事被混成了一件事。这两个动作在流程上一前一后,缺一不可,但它们的职责范围完全不同,先把这一点掰开揉碎,后面所有操作都会顺。
STM32F103 这颗芯片在嵌入式入门领域的地位不用多说,零售几十块的核心板、教程里翻来覆去的流水灯、串口打印、PWM 调光,几乎都绕不开它。围绕它的一套开发环境无非是:Keil MDK5 作为 IDE 和编译器,芯片的器件支持包告诉 IDE 这颗芯片有多少 Flash、多少 RAM、外设寄存器地址在哪、启动文件长什么样,而固件库则是厂商用 C 语言把寄存器操作封装成函数,让你写GPIO_SetBits()而不是直接往0x40010C0C里写位。前者是 IDE 的"认知",后者是代码的"血肉"。把这两个概念搞混,就会出现"器件列表里有芯片,但一编译就提示stm32f10x.h找不到"这种典型症状。
这篇内容会从零开始,把器件支持包的安装、标准外设库的文件筛选、Keil 工程的分组配置、头文件路径与全局宏的对应关系、启动文件与芯片容量的匹配规则,以及编译下载阶段最高频的十几条报错逐个拆开讲。适合三类人:第一次接触 STM32 的学生、从别的平台转过来的工程师、以及带新人时想找一份能直接抄的配置清单的老手。全文的操作路径基于 Keil MDK5 的常规版本,具体菜单文字可能随版本略有差异,但逻辑通用。
1.1 为什么 Keil5 装完还是搜不到 STM32F103
Keil MDK5 从第五代开始做了一件很"现代"的事:把器件信息从 IDE 安装包里剥离出去,做成独立的Device Family Pack(简称 DFP,器件家族支持包)。安装包本体只带 ARM 编译器、调试器驱动、以及少量通用中间件,芯片相关的 ALG 烧录算法、SFR 寄存器定义、启动文件、示例工程全部放在 Pack 里按需下载。这样做的初衷是减小主安装包体积、让芯片厂商能自己维护更新,代价就是新人第一次用会觉得"装了个寂寞"。
所以当你在器件框里搜不到 STM32F103,正确的判断顺序是:先看 Pack Installer 里Keil::STM32F1xx_DFP这一项装了没有;再看装的是不是过老的版本(老版本的 Pack 里器件命名和现在略有差异);最后才怀疑安装路径或权限问题。反过来,如果器件列表里能选到STM32F103C8,但新建工程后编译报一堆头文件找不到,那就跟 Pack 无关了,问题出在固件库文件没加进工程、或者头文件搜索路径没配。这两类问题的分界线就是"器件选择框里有没有东西",记住这一条,排查效率能翻倍。
还有一个隐藏细节:MDK5 允许同时安装多个版本的 Pack,工程属性里可以指定用哪个版本。团队协作时如果一个人用 Pack 2.3.0、另一个人用 2.4.0,理论上寄存器定义是一致的,但如果涉及启动文件差异,偶发编译警告不一致的情况。稳妥做法是在模板工程里把 Pack 版本固定下来,或者在版本变更时统一更新,别各写各的。
1.2 器件支持包、CMSIS、外设库,三者各管什么
把这三层用生活化的比喻讲清楚:器件支持包像是一本"房产证",说明这块地(芯片)的面积、边界、门牌号(Flash 大小、RAM 大小、外设基地址);CMSIS 像是"水电煤标准接口",由 ARM 主导定义,规定内核相关的访问方式(比如NVIC_EnableIRQ()、SysTick_Config()这类函数和core_cm3.h里的内核寄存器结构体);外设库则是"装修公司",厂商基于前两者把 GPIO、USART、TIM 这些外设的操作封装成函数。
实际工程里这三层的物理位置是这样的:core_cm3.c/h来自 CMSIS,管的是 Cortex-M3 内核;system_stm32f10x.c/h和stm32f10x.h来自器件支持层,管的是芯片级时钟和寄存器映射;stm32f10x_gpio.c、stm32f10x_usart.c这些来自外设驱动层,管的是具体外设。stm32f10x.h是整个体系的"总入口",它内部会根据你定义的宏去决定包含哪颗芯片的头文件、要不要把stm32f10x_conf.h拉进来。
理解了这一层,你就明白为什么工程里必须同时加core_cm3.c和system_stm32f10x.c,缺一个都会在链接阶段报未定义符号。也明白了为什么全局宏里必须同时写USE_STDPERIPH_DRIVER和STM32F10X_HD——前者控制stm32f10x.h是否包含外设库的配置文件,后者告诉编译器这颗芯片属于大容量系列,决定了寄存器结构体里某些成员的偏移量。少写一个,不报错才怪。
1.3 标准外设库和 HAL 库,这个项目该选哪个
打开任何一份 STM32F103 的教程,你都会看到两种流派:一种用STM32F10x_StdPeriph_Lib_V3.5.0,另一种用STM32CubeF1里的 HAL 库。这两者在导入方式上有本质区别,选错了会白白折腾半天。
标准外设库是"老派"做法,文件是手动拷贝进工程的,需要自己建目录、加分组、配路径,过程稍繁琐但每一个文件在干什么都清清楚楚,非常适合打基础。它的代码风格是直接操作寄存器封装,函数名短,执行效率高,缺点是厂商早已停止更新,新芯片不支持。HAL 库是"新派"做法,通常配合 STM32CubeMX 图形化工具生成工程骨架,代码由工具自动生成,导入过程被大幅简化,跨系列移植性好,缺点是代码层级多、体积大、初学者容易只知其然。
对于手上这颗 STM32F103,我的建议很直接:如果你是为了搞懂底层、做课程设计或小批量产品,用标准外设库,它逼着你去理解时钟树、理解寄存器,后期排查硬件问题时有底气;如果你要快速出原型、或者后续可能换到 F4/G0 系列,用 CubeMX 加 HAL,别在手动拷贝文件上耗时间。这篇文章的主线走标准库路线,因为"导入库文件"这个动作在标准库场景下才有完整的操作链条;HAL 路线会在后面单独说明差异点。
2. 器件支持包的两种安装方式与验证方法
2.1 在线装:Pack Installer 的正确打开姿势
Keil 主界面工具栏上有一个带绿色小方块的图标,或者走菜单Project → Manage → Pack Installer,都能打开 Pack 管理窗口。窗口左侧是器件厂商树,展开Keil节点能看到STM32F1 Series,再往下就是STM32F1xx_DFP。右侧会列出该 Pack 的所有版本、发布日期、以及当前状态。点Install按钮就开始下载安装,安装完成后按钮会变成Remove或Up to date。
在线安装最常遇到的问题有两个。第一是网络卡顿导致下载中断,表现为进度条走一半停住,或者提示下载失败。这不是 Keil 的问题,换一个网络时段重试往往就好了。第二是权限问题,如果 Keil 安装在C:\Keil_v5这类需要管理员权限的目录,Pack 写入时可能失败,表现是显示安装完成但目录里没有文件。解决办法是以管理员身份运行 Keil,或者把 Pack 的存储路径改到用户目录下——Pack Installer里可以设置Folder选项,指向一个有写权限的路径即可。
安装完成后有一个容易忽略的细节:别忘了点Packs标签页右上角的刷新。有些版本在安装后器件列表不会立刻刷新,导致你以为没装成功。还有一种情况是安装了多个版本的 DFP,工程默认用的是旧版本,这时需要在Options for Target → Device页面确认一下当前选中的 Pack 版本,或者直接在工程里重新选一次芯片型号。
2.2 离线装:.pack 文件的获取与双击安装
带宽紧张、或者目标机器不能联网的时候,离线安装是更靠谱的路子。.pack文件本质上是一个改了后缀的压缩包,内部包含描述文件、器件定义、启动文件、烧录算法等。获取途径通常有三种:从已经装好的同事电脑上,到Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\目录下把整个版本文件夹打包带走;从芯片厂商提供的开发资源页下载;或者从单位内部的软件仓库获取。
拿到.pack文件后,操作极其简单:直接双击。系统会自动调用 Keil 的 Pack 安装程序,弹出确认窗口,点 Next 一路到底即可。如果双击没反应,说明 Pack 文件没有关联到 Keil,可以手动在 Pack Installer 里走File → Import导入。安装路径默认是C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\<版本号>\,进去之后应该能看到Device、Include、Flash、SVD等子目录,这就是后面排查问题的第一手依据。
这里提醒一句:离线安装时不要图省事把不同版本的文件夹直接覆盖。不同版本的 DFP 目录结构可能有调整,覆盖会导致描述文件和实际文件对不上,出现器件列表显示但打开就报错的情况。正确做法是让每个版本各占一个文件夹,Keil 通过工程配置决定用哪个。
2.3 怎么确认装好了:三个可检查的位置
判断 Pack 是否真的安装成功,不要只看安装程序说的"完成",用下面三个位置交叉验证最稳。
第一个位置是新建工程的器件选择框。新建工程,在搜索框输入STM32F103,如果列表里出现STM32F103C8、STM32F103ZE等型号,说明器件描述文件已经被读到,这是最直观的信号。
第二个位置是文件系统里的 Pack 目录。进入Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\,里面应该有一个以版本号命名的文件夹,其下的Device\Include里能找到stm32f10x.h一类文件,Device\Source\ARM里能找到startup_stm32f10x_hd.s这类启动文件。如果目录是空的,说明安装过程其实失败了。
第三个位置是Pack Installer 里的状态标记。找到STM32F1xx_DFP那一行,如果按钮显示Remove或者状态是Up to date,说明已装;如果显示Install,说明没装成功。
| 检查位置 | 正常表现 | 异常含义 |
|---|---|---|
| 新建工程器件列表 | 能搜到 STM32F103 各型号 | Pack 未安装或未刷新 |
| Pack 安装目录 | 版本文件夹内有 Device/Include/Flash | 安装中断或权限不足 |
| Pack Installer 状态 | 显示 Remove 或 Up to date | 仍显示 Install 说明没装上 |
3. 标准外设库导入的完整实操
3.1 先建目录再动鼠标:一套能复用的工程骨架
很多人导入库文件的方式是"边加边想",文件往工程里一拖,路径随手一配,编译报错了再回头改。这种打法在单个工程上还能凑合,一旦要开第二个工程就全乱套。我的习惯是先在磁盘上把目录结构定死,再打开 Keil,让文件系统的物理结构和 Keil 里的逻辑分组一一对应。
推荐的骨架是这样的:工程根目录下建CORE、FWLIB、USER、OBJ、HARDWARE五个文件夹。CORE放内核与启动相关文件(core_cm3.c/h、system_stm32f10x.c/h、stm32f10x.h、启动文件.s);FWLIB下再分inc和src,分别放外设库的头文件和源文件;USER放main.c、stm32f10x_it.c/h、stm32f10x_conf.h;OBJ专门存编译中间文件和 hex;HARDWARE留给后续自己写的板级驱动,比如led.c、key.c、usart.c。
这套结构的好处在于:Keil 里的Target分组名和磁盘目录名完全对应,任何人拿到你的工程,看一眼目录树就知道文件在哪;后续要换芯片或者升级库,只需要替换CORE和FWLIB,USER和HARDWARE几乎不用动;编译产物全在OBJ里,清理工程时删这一个文件夹就够,不会误删源码。
注意:路径里绝对不能出现中文字符和空格。这是血泪教训,Keil 的编译器和链接器对非 ASCII 路径的处理并不一致,轻则生成的
.axf路径错乱,重则直接报cannot open source input file或者链接阶段找不到.o文件。工程放在D:\Work\STM32\F103_Template这种纯英文路径下最省心。
3.2 哪些文件必须拷,哪些纯属占地方
从标准外设库的原始压缩包里往工程里拷文件时,很多人图省事整个Libraries目录连同Project目录一起搬过来,结果是工程里塞了几百个文件,绝大部分根本用不上,还容易因为重复添加导致链接报错。真正需要的其实就那么一批,按下面的清单挑就行。
CMSIS 内核层:core_cm3.c、core_cm3.h。这两个文件是 Cortex-M3 内核的访问接口,任何 M3 内核的工程都要用,放在CORE目录。
器件支持层:system_stm32f10x.c、system_stm32f10x.h、stm32f10x.h,以及startup目录下对应容量的.s启动文件。system_stm32f10x.c里定义了SystemInit()函数,负责在上电时配置时钟(默认是把外部 8MHz 晶振倍频到 72MHz),这个文件不加,链接到SystemInit时会报未定义符号。stm32f10x.h是整颗芯片的寄存器总表,几万行,必须加。
外设驱动层:inc目录下所有.h,src目录下所有.c。这里面包括misc.c(NVIC、SysTick 相关)、stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_usart.c等等。虽然你可能只用得到其中几个,但全加进去最省心,链接器会自己剔除没被引用的函数,不会让固件变大。如果实在在意编译时间,可以先只加misc、rcc、gpio这三个最基础的,后续用到哪个补哪个。
用户层:stm32f10x_conf.h(外设库的配置文件,控制哪些外设的头文件被包含)、stm32f10x_it.c/h(中断服务函数模板)、main.c。stm32f10x_conf.h这个文件容易被漏掉,而它恰恰是stm32f10x.h在USE_STDPERIPH_DRIVER宏打开后要去包含的文件,路径配错就会报cannot open source input file "stm32f10x_conf.h"。
| 目录 | 必加文件 | 作用 | 漏了会怎样 |
|---|---|---|---|
| CORE | core_cm3.c/h | 内核寄存器与 NVIC 接口 | NVIC 相关函数未定义 |
| CORE | system_stm32f10x.c/h | 系统时钟初始化 | SystemInit 未定义 |
| CORE | stm32f10x.h | 芯片寄存器总定义 | 全工程找不到类型定义 |
| CORE | startup_stm32f10x_xx.s | 中断向量表与启动流程 | 链接报复位向量缺失 |
| USER | stm32f10x_conf.h | 外设头文件开关 | 提示找不到该文件 |
| USER | stm32f10x_it.c/h | 中断服务函数模板 | 可省,但中断不好写 |
| FWLIB | inc/src 全套 | 外设操作函数 | 外设函数未定义 |
3.3 Keil 工程里建组、加文件的具体顺序
磁盘结构定好之后,打开 Keil 新建工程,器件选STM32F103C8(以常见的核心板为例),弹出Manage Run-Time Environment窗口时直接点Cancel,因为走标准库路线不需要通过 RTE 自动加文件,手动加更可控。
接着点工具栏的"三色方块"图标打开Manage Project Items。在Groups一栏里依次新建CORE、FWLIB、USER、HARDWARE四个组,然后把对应目录下的文件逐个Add进去。加文件时有个细节:启动文件要按芯片容量选,不是随便挑一个。STM32F103C8 的 Flash 是 64KB,属于中容量,对应startup_stm32f10x_md.s;如果是 ZET6 那种 512KB 的,要用startup_stm32f10x_hd.s。选错的后果后面单独说。
顺序上建议先加CORE,再加FWLIB,最后加USER,这样在工程树里呈现的位置和依赖关系一致,读起来顺。加完之后别忘了把OBJ目录设置成输出目录:Options for Target → Output → Select Folder for Objects,指向OBJ文件夹,同时勾上Create HEX File,方便后续用串口或者离线烧录器下载。
FWLIB组里加文件时不要一次性全选上百个文件拖进去,容易因为鼠标滑脱加重复。可以按住 Ctrl 分批加,加完在组里扫一眼,看有没有同名的.c出现两次。重复添加是后面链接报"重复定义"的头号原因。
3.4 头文件路径、全局宏、启动文件三件套
这三项是编译能否通过的关键,缺一不可,而且必须相互匹配。打开Options for Target → C/C++页面,分三步走。
第一步配Include Paths。把CORE、FWLIB\inc、USER三个目录加进去。如果你的main.c里还包含了HARDWARE下的头文件,那也要加进来。这里的原则是:凡是#include "xxx.h"里用双引号引的头文件所在目录,都要在路径列表里。路径顺序有讲究,如果两个目录下有同名头文件,编译器按顺序取第一个,所以把最具体的目录放前面比较安全。
第二步配Define。在Preprocessor Symbols的Define框里填USE_STDPERIPH_DRIVER, STM32F10X_MD。这两个宏用逗号分隔,注意不要用中文逗号。前一个宏是告诉stm32f10x.h:"我用了外设库,请把stm32f10x_conf.h包含进来";后一个宏是告诉它芯片属于哪个容量等级。C8T6 是 64KB Flash,属于中容量 MD;如果板子是 RCT6(256KB)或 ZET6(512KB),要改成STM32F10X_HD;如果是 C6T6 那种 32KB 的,是STM32F10X_LD。
容量宏选错的典型症状是:编译能过,但中断向量表对不上,一进中断就跑飞,或者定时器、串口的行为莫名其妙。原因在于stm32f10x.h里会根据这个宏定义不同的中断号枚举和寄存器结构体,选错等于拿错了地图。
第三步确认编译器版本。Options for Target → Target页面里有ARM Compiler选项,标准外设库 3.5.0 那套代码是基于 ARM Compiler 5(AC5)写的,用 AC6 编译会出现大量警告甚至错误,比如启动文件里的汇编语法不被识别。如果下拉框里能选Use default compiler version 5,选它最稳妥;如果 MDK 版本较新、装的时候没勾 AC5,需要单独补装编译器组件,否则只能硬着头皮用 AC6 加一堆兼容参数,不推荐新手走这条路。
提示:配完这三项之后,先别急着写业务代码,写一个只有
#include "stm32f10x.h"和空main的测试文件,按一次 F7 编译。能过 0 Error 就说明库导入这条链路通了,再往里添外设代码。这个习惯能帮你把"库配置问题"和"业务代码问题"彻底隔离开。
3.5 写个最小 main.c 把编译跑通
最小的骨架长这样:包含stm32f10x.h,写一个main函数,函数体里先调用一个延时循环,把唯一要做的事放在死循环里。别小看这几行,它能验证的事情不少——头文件路径通不通、宏定义对不对、启动文件和SystemInit能不能链接上,一次编译全暴露。
#include "stm32f10x.h" static void delay(volatile uint32_t n) { while (n--) { __NOP(); } } int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_13; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &gpio); while (1) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); delay(500000); GPIO_SetBits(GPIOC, GPIO_Pin_13); delay(500000); } }这段代码里RCC_APB2PeriphClockCmd来自stm32f10x_rcc.c,GPIO_Init和GPIO_SetBits来自stm32f10x_gpio.c。如果编译时提示这几个函数未定义,说明FWLIB里的.c没加全,或者加进去了但因为路径问题没被编译。如果提示RCC_APB2Periph_GPIOC未定义,那多半是宏没配对,stm32f10x.h走进了一个不包含该枚举的分支。
编译通过后看 Build Output 窗口的提示,正常情况下应该是0 Error(s), 0 Warning(s)或者少量无关紧要的警告。如果出现Warning: #1-D: last line of file ends without a newline,那只是文件末尾没换行,不影响功能,但顺手补上更规范。
3.6 下载配置与第一次点灯验证
编译过了不等于板子能跑,下载配置是另一道坎。打开Options for Target → Debug,在右上角的下拉框里选你手上的调试器,常见的是ST-Link Debugger或者CMSIS-DAP Debugger。选完点旁边的Settings,在Debug标签页里把Port设成SW(不是 JTAG),Max Clock拉到 1.8MHz 或 4MHz,太快反而容易连不上。右侧的SW Device区域如果能看到一串 IDCODE,说明芯片已经被识别到,链路是通的;如果显示No target connected,先查线序:SWDIO、SWCLK、GND 三根线必须接对,板子要独立供电或者由调试器供电。
接着切到Flash Download标签页,勾上Reset and Run,这样下载完程序自动复位运行,不用手动按复位键。下面的Programming Algorithm列表里应该有STM32F10x Med-density Flash之类的条目,如果没有,点Add手动添加,算法文件就来自前面装的器件支持包。这一步如果加错了容量(比如中容量板子选了高容量算法),会出现"能连上但下载失败"的现象,报错通常是Flash Download failed - Target DLL has been cancelled。
下载成功后,板子上的 LED 应该开始闪烁。如果没闪,先确认 LED 接的是不是 PC13(不同核心板丝印不同),再确认晶振有没有焊好——SystemInit()默认会尝试启动外部 8MHz 晶振,如果晶振不起振,时钟会退回到内部 RC,程序跑得慢但一般不致命。真正致命的是BOOT0拉高进了 ISP 模式,程序下进去也不运行,把BOOT0接 GND 就好。
4. 报错现场:导入库文件最高频的几类问题
4.1 头文件找不到:路径和宏的连环套
cannot open source input file "core_cm3.h"和cannot open source input file "stm32f10x.h"是出现频率最高的两条报错,本质上都是头文件搜索路径没覆盖到对应目录。前者的解决方法是把CORE目录加进 Include Paths;后者要么是CORE路径没加,要么是stm32f10x.h根本没拷进工程。
稍微绕一点的是cannot open source input file "stm32f10x_conf.h"。这个文件的特殊之处在于,它并不是被你的main.c直接包含的,而是stm32f10x.h在检测到USE_STDPERIPH_DRIVER宏之后主动去包含的。所以即使你压根没在代码里写#include "stm32f10x_conf.h",它照样会被要求存在。解决思路有两条:把stm32f10x_conf.h放到USER目录并加入路径;或者干脆在stm32f10x.h里把那段#include注释掉(不推荐,会失去外设头文件的统一管理)。
还有一类更隐蔽的:报错指向stm32f10x.h内部的某一行,提示#error directive: "Please select first the target STM32F10x device used in your application (in stm32f10x.h file)"。这不是路径问题,是容量宏没定义或者拼错了。回到Define框检查STM32F10X_MD是不是漏写、是不是写成了STM32F10x_MD(大小写敏感)、是不是用了中文逗号分隔。
| 报错信息 | 根因 | 处理动作 |
|---|---|---|
| cannot open core_cm3.h | CORE 未入路径 | 添加 CORE 到 Include Paths |
| cannot open stm32f10x.h | CORE 未入路径或文件缺失 | 检查文件是否存在并加路径 |
| cannot open stm32f10x_conf.h | USER 未入路径 | 拷贝该文件并加路径 |
| #error Please select first the target | 容量宏缺失或拼错 | 补 STM32F10X_MD/HD/LD |
| Undefined symbol SystemInit | 缺 system_stm32f10x.c | 加入 CORE 组 |
4.2 链接期的重复定义与未定义符号
编译阶段过了,链接阶段报L6200E: Symbol xxx multiply defined,说明同一个符号在多个.o文件里都有定义。最常见的原因是启动文件被加了两次,或者FWLIB里同一个.c在两个组里各加了一遍。Keil 的工程树里同名文件可以重复出现,肉眼不容易发现,去Manage Project Items里逐个组数一遍最快。
另一个高频原因是文件被复制到了两个目录。比如你既把stm32f10x_gpio.c放在FWLIB\src,又在HARDWARE里复制了一份改过的版本,两个都被加进工程,链接器就懵了。这种情况的报错往往会指明具体的符号名,对着符号名去找哪个函数被定义了两次,一般在stm32f10x_xxx.c里能定位到。
反过来,L6218E: Undefined symbol xxx是"找不到定义"。SystemInit未定义说明system_stm32f10x.c没加;NVIC_PriorityGroupConfig未定义说明misc.c没加;assert_failed未定义说明你在stm32f10x_conf.h里打开了USE_FULL_ASSERT但没实现这个函数。最后这种情况解法很简单:要么把USE_FULL_ASSERT注释掉,要么在main.c里补一个空的assert_failed实现。
4.3 启动文件选错芯片容量的典型症状
启动文件的作用是定义中断向量表和复位入口,不同容量芯片的中断源数量不一样,启动文件自然不同。选错的症状分三档:
轻症是编译通过、程序能跑,但某些中断进不去。比如中容量板子误用了小容量的startup_stm32f10x_ld.s,向量表里少了几个中断入口,用到 TIM3、TIM4 这类外设中断时就会跳到错误地址或者直接进 HardFault。
中症是链接报错,提示某个中断处理函数重复定义或者向量表大小不匹配。因为启动文件里对每个中断都写了一个默认的Handler弱符号,如果你的stm32f10x_it.c里也定义了同名函数,链接器会按强弱符号规则处理,但如果启动文件里压根没有这个中断的槽位,就会报未定义。
重症是程序下载后完全没反应,连点灯都不亮。这种情况通常是启动文件里的栈大小、堆大小配置和芯片实际 RAM 不匹配,或者向量表首地址被改错了。
判断用哪个启动文件,看芯片型号里的 Flash 容量代码就够:C8、R8是 64KB 属中容量用md;C6、R6是 32KB 属小容量用ld;RC、ZC、VE是 256KB 以上属大容量用hd;IG、VG是 1MB 属超大容量用xl;带互联型字样的(如STM32F105/107)用cl。拿不准的时候,查一下芯片数据手册里的 Flash 一栏,比对着容量选,别凭型号里的字母猜。
4.4 下载与调试阶段的坑
库文件导入正确、编译链接全绿,仍然可能在下载环节卡住。No target connected除了线序问题,还有一个常见原因是调试器驱动没装或者被系统识别成了别的设备。设备管理器里如果看到黄色感叹号,重新装一遍驱动就行。
Cannot access target或者Error: Flash Download failed多半是芯片被读保护了,或者上一次下载中途断电导致 Flash 状态异常。处理方法是用调试器的Connect under Reset模式连接:在Debug → Settings里把Connect选项改成under Reset,同时按住板子的复位键点下载,连上之后再松开。这一招对付"砖头"状态的核心板特别有效。
还有一类问题出在下载算法和芯片容量不匹配。前面提到过,Flash Download里的Programming Algorithm必须和实际容量对应。C8T6 用的算法名字里带Med-density,ZET6 用High-density,选错了会报地址越界或者校验失败。如果你从别人那里拷来的工程模板,这一项往往带着原作者板子的配置,换板子之后必须重新确认。
注意:
Reset and Run这个勾选项建议一直开着。我遇到过不少人说"下载成功但程序不跑",最后发现每次下载完都要手动按一下复位键,纯粹是忘了勾这一项。
5. 把配好的工程固化成模板
5.1 模板要删掉什么、保留什么
第一次把库导入跑通之后,别急着开始写业务代码,先花十分钟把它做成模板。具体做法是把当前工程另存一份,命名为F103_Template之类,然后清理掉与业务相关的东西:main.c里只留一个空的死循环、HARDWARE目录清空、OBJ里的编译产物全部删除、工程里针对具体板子的引脚定义全部去掉。保留下来的是CORE、FWLIB、USER三件套,加上已经配好的Include Paths、全局宏、启动文件、输出目录、下载器配置。
模板要做干净还有一个理由:Keil 的工程文件是文本格式的.uvprojx,里面记录了每一个文件的绝对路径或者相对路径。如果模板里残留了D:\某次实验\xxx.c这种绝对路径,换一台电脑打开就会报找不到文件。稳妥做法是在Manage Project Items里把所有文件路径改成相对于工程根目录的形式,然后整个文件夹复制到别处打开测试一次,确认路径没有写死。
模板里还有一个值得固化的东西:默认的时钟配置。system_stm32f10x.c里的SystemInit()默认把系统时钟配到 72MHz,如果你板子上的晶振不是 8MHz,或者你想跑在 48MHz,可以在main开头重新调用RCC相关函数覆盖掉。把这套配置写进模板的main.c注释里,下次开新工程直接抄,能省不少查手册的时间。
5.2 多工程共享一份库目录的相对路径玩法
当项目多起来,每个工程里都复制一份FWLIB会造成磁盘浪费,更麻烦的是库文件升级时得挨个工程替换。解决办法是把库目录提到工程外层,用相对路径引用。
具体布局是这样的:在最外层建一个Lib目录,里面放FWLIB和CORE;然后每个具体项目一个文件夹,里面只有USER、HARDWARE、OBJ。在项目的Include Paths里用..\Lib\CORE、..\Lib\FWLIB\inc这样的相对路径指过去。Keil 里添加文件时也走相对路径,加进去之后在工程树里会显示..\Lib\FWLIB\src\stm32f10x_gpio.c这种形式。
这种玩法的收益在团队协作时特别明显:库出现 bug 或者要统一升级版本,只改一处,所有工程跟着生效。代价是工程不能单独拷走,必须连同外层的Lib一起打包。所以交付给别人的时候,要么把Lib一起压缩,要么临时把库文件拷进工程内部再打包,别让人家打开就一堆红叉。
相对路径还有一个隐蔽的坑:Keil 在计算相对路径时,基准是.uvprojx文件所在目录。如果你把工程文件放在USER子目录里,而.uvprojx在项目根目录,那..\Lib的层数就要按.uvprojx的位置算,不是按main.c的位置算。搞混了就会报路径找不到,但用资源管理器看明明文件就在那儿。
最后分享一个我自己用了很久的小习惯:每次新建工程,先不写代码,把main.c写成下面这样,编译一次,下载一次,看灯闪不闪。闪了,说明从器件包、库文件、路径、宏、启动文件、下载算法到硬件链路,整条路都是通的,后面写什么都有底气;不闪,就按前面几节的排查顺序从后往前捋,问题一定出在某个具体环节,而不是玄学。这个"点灯验收"的动作花不了两分钟,但能挡掉后面百分之八十的莫名其妙。
int main(void) { /* 点灯验收:能闪说明整条工具链通了 */ while (1) { } }