简介:面向STM32F103ZET6嵌入式开发者的Keil空工程模板,适合刚接触ARM单片机或需要快速搭建项目骨架的工程师使用,基于标准外设库整理,可省去从零配置工程的繁琐过程。模板集成了启动文件、系统时钟初始化、GPIO、定时器、串口、ADC、I2C、CAN等常用外设驱动源文件,在MDK中打开即可添加业务代码并直接编译验证。压缩包共178个文件,核心为C源文件与H头文件,另含汇编启动文件、Keil工程配置与编译产物(axf、hex、map等),整体仅6.63MB,轻量清晰、便于分发备份。已有1086人浏览学习,资源对标准外设库的覆盖较全,从预览可见涉及定时器、复位时钟、模数转换、总线通信及串口等模块,适合对照参考手册理解外设初始化流程。这份工程模板可大幅缩短环境搭建时间,也能作为课程实验、毕业设计或产品原型的基础框架,帮助团队统一开发环境、降低入门门槛。
1. STM32F103ZET6空工程模板:从零新建工程的时代该结束了
很多第一次接触 STM32F103ZET6 的开发者,开局不是被逻辑代码难住,而是被新建工程这件事本身卡到怀疑人生:芯片型号选不对、启动文件选错、下载器不识别、编译报错一片红。尤其 ZET6 这颗 144 脚的大容量芯片,Flash 512KB、RAM 64KB,一旦选错配置文件,整个工程直接废掉。我之前带过一个做嵌入式入门项目的团队,四个人里三个人卡在“工程建不起来”这一步,真正写逻辑代码的时间不到三分之一。所以当看到这份 STM32F103ZET6 空工程模板时,第一反应是:这东西就该早点存在。它不是一个教学 Demo,而是一个已经配置好芯片型号、启动文件、时钟树、下载参数的空壳工程,打开就能编译、烧录、跑通,适合所有想跳过重复配置直接进入功能开发的人。这套模板解决的不是“怎么写代码”,而是“怎么写代码最省时间”。
2. 模板内部解剖:大容量芯片的启动文件与内存模型
2.1 为什么 ZET6 必须配 startup_stm32f10x_hd.s
打开模板的工程目录,最先要确认的就是启动文件。很多从标准库教程起步的人会直接复制小容量工程的启动文件,结果烧录后程序根本不运行。F103 系列根据 Flash 容量分为三个档:LD(小容量,Flash ≤ 32KB)、MD(中容量,Flash ≤ 128KB)、HD(大容量,Flash ≤ 512KB)。ZET6 的 Flash 是 512KB,属于 HD 档,对应启动文件必须是startup_stm32f10x_hd.s。
这个选择不是随便改个名字那么简单,启动文件里定义了初始堆栈大小、中断向量表位置、以及上电后跳转到 SystemInit 和 main 的启动流程。如果用了 MD 的启动文件,芯片型号虽然能识别,但中断向量表偏移和堆栈初始化参数不匹配,最常见的结果是程序下载成功但一运行就进 HardFault,或者干脆不跑。
模板里的启动文件位置通常在Libraries/CMSIS/Device/ST/STM32F10x/Startup/下,这个路径和标准库的默认结构一致,不用手动调整。
2.2 RAM 与 Flash 的地址边界:ZET6 的 512KB + 64KB 到底怎么分配
ZET6 的内存映射在编程时不需要手动配置,因为启动文件和链接脚本已经把地址边界算好了。但你需要知道边界在哪,否则写程序时容易踩到资源不足的坑。
- Flash 地址范围:0x08000000 到 0x0807FFFF,共 512KB
- SRAM 地址范围:0x20000000 到 0x2000FFFF,共 64KB
- 片上外设寄存器区在 0x40000000 起始
模板的链接脚本(.sct 文件或 .ld 文件,取决于你用的是 Keil 还是 GCC 工具链)已经按这个边界分配了 RO、RW、ZI 段。在 Keil 里打开 Options for Target,会看到IRAM1范围是0x20000000,大小0x10000;IROM1范围是0x08000000,大小0x80000。我见过有人把 RAM 大小手滑改成 0x20000,编译能过,但运行时随机崩溃,这种玄学问题排查一次能折腾一下午。
使用模板时最实用的做法是在这一层确认内存足够之后,就不要随便动了,模板给的就是 F103ZET6 的标准边界,改动它十有八九是给自己埋雷。
2.3 下载配置:ST-Link 的时钟频率与连接状态判断
模板自带的下载配置一般是 ST-Link,因为 ST-Link 是 ST 官方工具里成本最低、兼容性最好的选择。打开 Keil 的魔术棒(Options for Target),在 Debug 页签下确认:
- Debugger 选择 ST-Link Debugger
- 点击 Settings,查看 SW Device 是否识别出目标芯片
# 常见识别结果 IDCODE: 0x3BA00477 # 对应 STM32F103-HD 系列的 SWD 识别码 Device Name: ARM CoreSight SW-DP如果这里显示No Target Connected,不要急着怪模板。检查顺序一般是:
- SWD 的四根线:SWDIO、SWCLK、GND、VCC 是否接对
- 目标板是否独立供电,或者由 ST-Link 供电时跳帽是否放对位置
- ST-Link
Settings里的 Max Clock 是否设成 4MHz 或 1.8MHz(ZET6 的 SWD 最高支持 10MHz,但线长超过 15cm 时降速更稳)
模板默认把 Max Clock 设置成 4MHz,这个参数在板上直接调试时完全够用。如果你用的是非官方 ST-Link 克隆版,烧录时偶尔会报RDDI-DAP Error,把速度降到 1.8MHz 就消停了,这是硬件克隆版的老毛病,不是模板的问题。
3. 初始化链路:从复位向量到你的第一行 main 代码
3.1 SystemInit:8MHz 外部晶振如何跑到 72MHz 主频
空工程模板的价值之一,就是里面已经跑通了一套从复位到 main 的完整初始化链。上电后,启动文件里的复位向量会把控制权交给SystemInit,这个函数定义在system_stm32f10x.c中。ZET6 的外部高速晶振(HSE)通常接的是 8MHz,而芯片主频要跑到 72MHz,中间经过的倍频链路是:
// 简化后的时钟树配置逻辑 // HSE = 8MHz // PLL 倍频系数 = 9 // SYSCLK = 8MHz * 9 = 72MHz // AHB 分频 = 1 -> HCLK = 72MHz // APB1 分频 = 2 -> PCLK1 = 36MHz // APB2 分频 = 1 -> PCLK2 = 72MHz void SystemInit(void) { // 复位 RCC 配置 RCC->CR |= 0x00000001; // 使能 HSI RCC->CFGR = 0x00000000; // 清零时钟配置 // ... // 模板调试代码:检查 HSE 是否就绪 while (!(RCC->CR & RCC_CR_HSERDY)) { // 等待外部晶振稳定,如果一直卡在这里说明晶振 // 没起振,优先检查 8MHz 晶振的两个负载电容 } }这段代码的逻辑重点是等待 HSE 就绪。如果你手里的板子用的不是 8MHz 晶振,而是一个有源晶振或者其他频率的无源晶振,模板启动后时钟会乱,所有外设的波特率、定时器频率全都会偏。常见做法是先确认板子的丝印或者原理图,再看模板里stm32f10x.h的HSE_VALUE宏是否匹配。
3.2 中断分组:模板里预留的 NVIC 优先级分配
ZET6 是 Cortex-M3 内核,NVIC 支持中断优先级分组,这是很多人容易忽略但其实必须提前决策的配置。中断分组如果改晚了,后面所有使能了中断的外设都要跟着重新配置。模板里在main.c或Init.c中给了一个默认分组:
void NVIC_Config(void) { // 分组 2:2 位抢占优先级 + 2 位子优先级 // 取值范围均为 0-3 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 示例:配置 USART1 中断 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }这个分组的实际意义是:抢占优先级高的中断可以打断低的,同优先级的靠子优先级排队。如果你计划往工程里移植 RTOS(比如 FreeRTOS),常见做法是用分组 4(4 位抢占优先级、无子优先级),因为 RTOS 通常要求 PendSV 和 SysTick 优先级是固定的,分组 2 的情况下可调空间不足。模板默认分组 2 适合裸机开发,如果你后面确定要上 RTOS,趁早切。
3.3 GPIO 点灯链路:验证模板是否“活”了
拿到模板之后,第一件事不是跑例程,而是用最小代码确认整个工程链路通了。点灯是最直观的验证方式,同时也顺带把 GPIO 外设的启动流程走了一遍。
#include "stm32f10x.h" // PE5 接 LED(正极) // 根据原理图改引脚号,不要照抄 void LED_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 打开 GPIOE 的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); // 配置 PE5 为推挽输出,最大翻转速度 50MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_SetBits(GPIOE, GPIO_Pin_5); // 初始状态:灭(取决于 LED 接法) } int main(void) { SystemInit(); // 如果是模板工程,SystemInit 在启动文件里已经调过, // 再次调用不会出问题, 但建议注释掉以保持模板原状 LED_GPIO_Config(); while (1) { GPIO_SetBits(GPIOE, GPIO_Pin_5); // LED 灭 GPIO_ResetBits(GPIOE, GPIO_Pin_5); // LED 亮 // 延时用简单循环代替, 模板里若有 SysTick 可用其延时函数 uint32_t i = 0x4FFFFF; while (i--); } }这段代码的逻辑是打开外设时钟、配置引脚模式、再读写引脚电平。所有外设操作之前必须先开时钟,否则写寄存器没有任何反应,这是 F103 最容易踩的坑之一。模板工程的main.c里默认是个空 while 循环,你只需要复制这个 GPIO 配置加进去即可。烧录后如果 LED 按预期翻转,说明启动文件、链接脚本、下载配置全部正常。
4. 模板落地实战:串口工程从零到通的四个步骤
4.1 确认模板时钟与外设时钟开关的配合关系
串口是调试嵌入式工程的核心通道,几乎所有空工程模板都会把串口配置留成空白,让使用者自己填,原因是波特率和引脚不通用。先明确一个关键前提:在 F103 上配置串口前,必须确认 RCC 里 USART 的时钟源和 APB2/APB1 总线的频率关系。
ZET6 上有多个串口,USART1 挂在 APB2 上(最高 72MHz),USART2 和 USART3 挂在 APB1 上(最高 36MHz)。模板的时钟配置已经让 APB1 = 36MHz、APB2 = 72MHz,所以下面的波特率计算是用标准库函数直接写的,不需要手动改预分频。
void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 开启 GPIOA 和 USART1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // USART1_TX = PA9, USART1_RX = PA10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // 波特率 115200, 8 数据位, 1 停止位, 无校验 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); }这里的逻辑关键在GPIO_Mode_AF_PP:TX 引脚必须设为复用推挽,否则数据从串口模块输出不到引脚上;RX 用浮空输入还是上拉输入,取决于外部电路是否有上拉。模块自带的串口芯片通常不需要额外上拉,直接浮空输入即可。
4.2 printf 重定向到串口的模板代码
空工程模板通常默认没有启用 printf 重定向,因为编译环境不同(Keil 和 GCC)处理方式有差异。以 Keil + 标准库为例,重定向的核心是重写fputc:
#include <stdio.h> // 重定向 printf 到 USART1 // 需要勾选 Keil 魔术棒 Target 页的 "Use MicroLIB" int fputc(int ch, FILE *f) { // 等待发送缓冲区为空 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) ; // 发送一个字节 USART_SendData(USART1, (uint8_t)ch); return ch; }注意细节:如果不启用 MicroLIB,F103 的标准库会引入__stdout等符号,某些情况下链接报错。勾选 MicroLIB 之后体积也更小,对 Flash 只有 512KB 的 ZET6 来说空间完全不是问题。但 MicroLIB 的 printf 默认不支持浮点,所以如果你在代码里打印浮点数,要么改用sprintf把浮点转成字符串再输出,要么放弃 MicroLIB、改用__io_putchar的完整实现。这是入门阶段非常容易碰到的“printf 打印整数正常、打印浮点全零”的元凶。
4.3 验证串口链路:回环测试比直接调试省事得多
配好串口后,不要急着接传感器或者和上位机做复杂协议,先做回环验证。模板里加上下面这段代码,用串口助手发送任意数据,收到的内容应当原样返回:
// 主循环中做回环处理 // 收到一个字节就原样返回 while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET) // 判断接收非空 { uint16_t ch = USART_ReceiveData(USART1); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) ; // 等待发送就绪 USART_SendData(USART1, ch); } }这一步通过之后,再挂传感器或者协议解析,出问题至少能区分是硬件链路还是软件逻辑。因为我见过太多人一上来就写完整协议栈,最后调不通在串口线上乱猜,浪费的时间足够把整个模板重新搭一遍。
4.4 模板中下载算法与 Flash 编程的选择
烧录 ZET6 时还有一层隐蔽配置藏在 Keil 的 Utilities 页签。模板里默认勾选了Update Target before Debugging,使用 ST-Link 时 Flash 下载算法选择的是:
STM32F10x High-density Flash (512KB)如果你在烧录时看到No Algorithm found for Address之类的报错,去这里手动添加即可。选错算法可能直接烧不进,或者烧进去了复位后程序不执行。有些教程会让你直接选STM32F10x Med-density Flash这样能通过编译但烧录就报错的选项,所以这里特别强调一下看准。
5. 避坑:空工程模板高频踩到的五个问题
5.1 启动文件不匹配导致程序“不跑”
现象:烧录成功、复位后没有任何反应,LED 不闪、串口无输出。点全速运行,程序卡在某处,单步也跳不到 main。
原因:MD 容量的启动文件在 ZET6 上初始化堆栈和中断向量时,断点映射到了不存在的地址。尤其很多是从别的代码上拷来启动文件,没有注意是 ld.s 还是 hd.s。
解决:确认启动文件是startup_stm32f10x_hd.s。在 Keil 里删除工程中原有的启动文件,然后把模板自带的Library/Device/.../Startup/下的文件重新加进去,注意不要重复添加。排除启动文件后,再检查.sct分散加载文件是否正确。
5.2 Keil 编译报 missing device 或芯片型号无法识别
现象:编译时提示Cannot load device description file,或者 Device 列表里选不到 STM32F103ZE。
原因:Keil 安装包不完整,缺少对应器件的 Pack 包;或者是安装时改了默认路径,MDK 找不到 PACK 文件夹。
解决:重新安装Keil.STM32F1xx_DFP器件包,版本 2.x 以上支持 F103 系列全型号。安装时保持默认路径。模板本身不依赖第三方库,只要 DFP 装好,理论上有别的机器打开也能直接编译。
5.3 串口输出乱码或全是 0xFF
现象:串口助手打开后,收到的数据是乱码,或者固定输出0xFF。
原因:三处最可能。一是时钟树里 HSE_VALUE 写错,比如模板按 8MHz 配置,实际板子上是 25MHz 有源晶振;二是串口助手的波特率不是 115200;三是电平转换芯片的 TX、RX 接反了。
解决:先把波特率、晶振频率都按板子的原理图对准;如果还是乱码,用一个 LED 翻转程序对比闪烁节奏,确认程序里的时间基准是否正常。物理接线反了就只能看原理图改跳线,无软件解法。
5.4 下载时报硬件复位失败或 No Target Connected
现象:点击下载后报Cannot access target,或者RDDI-DAP Error。最常见于刚开始接线时。
原因:目标板在上电瞬间和 ST-Link 没有建立稳定通信,通常是供电不稳、SWD 两条信号线接触不良,或者 ST-Link 固件版本过旧。ZET6 的 BOOT0、BOOT1 引脚状态也会影响(如果 BOOT0 被拉高,芯片会从系统存储器启动,不是用户 Flash 区)。
解决:先把模板对应的下载频率降到 1.8MHz;按住板子复位键,点击 Download 后松开复位键,这种方式能解决绝大多数连接不上的问题。模板默认烧录后自动复位并运行,如果没勾选,手动复位一次也是正常现象。
5.5 中断未打开导致外设功能“没反应”
现象:初始化了串口或者定时器,代码看着没问题,但中断回调函数永不执行。这在模板工程里最常见的原因是复制代码时没有把 NVIC 配置一起拷过来。
原因:模板只初始化了空的外设时钟和引脚,并不会帮你使能所有外设的中断源。F103 的外设中断除了在外设_ITConfig里使能外,还要在 NVIC 里打开对应通道,两者缺一不可。
解决:在初始化函数末尾添加 NVIC 配置,参考 3.2 节的代码模板。如果中断不触发,先查外设_ITConfig(..., ENABLE)是否执行,再查 NVIC 的 IRQChannel 是否写的对应中断号。模板提供的中断分组是 Group 2,如果你改成了别的分组,所有中断优先级都要按新分组重新取值。
6. 模板进阶:直接往里面塞 FreeRTOS 的三种姿势
模板基础链路调通之后,下一步大概率是上系统或者上复杂协议。针对 F103ZET6,最常用的进阶动作是移植 FreeRTOS,模板把时钟、中断、堆栈这些底层配好之后,FreeRTOS 的接入其实非常机械。
移植 FreeRTOS 只需要关注三件事:第一个是准备FreeRTOSConfig.h,这个是 FreeRTOS 的配置文件,里面的核心参数之一是configTOTAL_HEAP_SIZE。F103ZET6 虽然有 64KB RAM,但 FreeRTOS 任务栈、内核对象全从这里分配,裸机下的局部变量也会占一块 RAM。我一般建议这块宏设成 20KB,也就是20 * 1024,既能容纳几个常规任务,又给主程序的局部变量留足空间。第二个是中端处理方式,FreeRTOS 官方针对 Cortex-M3 的移植层里必须实现vPortSVCHandler和xPortPendSVHandler,这两个函数名在port.c里已经写好宏映射,你只需要在自己的工程里确保启动文件的中断向量表没有占用SVC_Handler和PendSV_Handler这两个入口。模板的启动文件是标准件,天然满足要求。第三个是 SysTick 的使用,FreeRTOS 需要系统节拍,通常会用 SysTick 产生 1ms 中断,模板里如果有自己的延时函数占用了 SysTick,需要改成用xTaskGetTickCount或者干脆删掉,避免冲突。
// FreeRTOSConfig.h 核心参数 #define configCPU_CLOCK_HZ 72000000U // 必须和 SystemInit 的 72MHz 一致 #define configTICK_RATE_HZ 1000U // 1000Hz, 即 1ms 一个 tick #define configTOTAL_HEAP_SIZE (20 * 1024) #define configMINIMAL_STACK_SIZE 128 #define configUSE_PREEMPTION 1 // 抢占式调度 #define configUSE_TIME_SLICING 1 // 同优先级时间片轮转需要注意的一点是configCPU_CLOCK_HZ必须与模板时钟树的最终 SYSCLK 一致,如果模板里把主频改到 56MHz 或 108MHz,而这里仍写 72MHz,FreeRTOS 的延时函数和 vTaskDelay 时间全部会比实际时长偏长,调试时看起来就是“任务间切换变慢”。
还有一种进阶姿势是 C 和 C++ 混合编译。模板默认是纯 C 工程,如果你有某些算法模块想用 C++ 写,比如写一个类封装 PWM 输出,Keil 里把对应.cpp文件加入工程后,需要在C/C++编译选项里加上--cpp11,同时启动文件里的main入口不能换。C++ 的全局对象构造在 F103 的启动文件里默认是不执行的,所以如果你的 C++ 代码里有非平凡构造函数,需要在main函数开头手动调用__cpp_initialize(不同编译器名字有差异),否则类成员变量初始值是不可预期的。这是我踩过的真坑,某次移植一个开源的 C++ 状态机库,逻辑看着没问题,就是输出全是错的,折腾了一晚上最后查出来是全局对象没有初始化。从那以后我每次新建工程都用这套模板,先跑点灯和串口回环,再进行任何进阶操作,中间环节少了,问题定位快得多,希望帮到你。
本文还有配套的精品资源,点击获取