简介:本资源是STM32F10x系列微控制器系统级初始化核心组件,面向嵌入式初学者与中级开发者,解决工程启动阶段时钟配置、中断向量设置及系统底层初始化等关键问题。包内共6个文件(3个C源文件+3个H头文件),涵盖system_stm32f10x.c/.h、sys.c/.h、delay.c/.h等典型模块,分别承担系统时钟树配置、基础寄存器封装、延时函数实现等职责;10KB精简体积便于快速集成到Keil、IAR或STM32CubeIDE工程中。已有671人学习下载,说明其在入门实践与项目复用中具备较高参考价值。读者可直接获取符合ST官方固件库规范的可编译代码结构,掌握HSE/HSI时钟源切换、PLL倍频配置、SysTick初始化等实操要点,并基于该框架快速扩展USART、GPIO等外设功能,显著降低STM32F10x裸机开发门槛。
1. STM32F10x SYSTEM 文件夹:不是“系统盘”,而是启动与外设配置的硬核控制台
你刚拿到一块 STM32F103C8T6 最小系统板,烧进官方固件能点灯,但一换自己写的裸机工程——串口不发数据、SysTick 不进中断、甚至SystemInit()后主频还是默认的 8MHz?别急着怀疑晶振或 BOOT 引脚,大概率是漏掉了STM32F10x_SYSTEM文件夹里那几份不起眼却决定硬件生死的源码。它不是操作系统,也不是 SDK 封装层,而是 ST 官方为 F10x 系列(Cortex-M3 内核)定制的底层硬件初始化中枢:从时钟树配置(RCC)、向量表重定向(SCB)、系统滴答定时器(SysTick)到中断向量入口(startup_*.s),全在这里闭环。它专为裸机开发和轻量级 RTOS 移植设计,不依赖 HAL 或 LL 库,代码行数少、可读性强、修改自由度高——某高校嵌入式课程要求学生必须手改system_stm32f10x.c中的PLL_MULL和HSE_VALUE才算通过实验验收。如果你正在做低功耗唤醒、精准定时控制或 Bootloader 开发,这个文件夹就是你绕不开的“硬件宪法”。
2. 拆解 SYSTEM 文件夹:四个核心文件的功能边界与协作逻辑
STM32F10x_SYSTEM并非一个黑匣子,而是一组高度解耦、职责明确的 C 和汇编文件组合。我一般会先用tree -L 2快速确认结构(Windows 下可用dir /s /b):
STM32F10x_SYSTEM/ ├── startup_stm32f10x_md.s # 中断向量表 + 复位处理入口 ├── system_stm32f10x.c # 主时钟配置 + 系统时钟频率计算 ├── core_cm3.h # Cortex-M3 内核寄存器定义(ST 提供) └── stm32f10x.h # F10x 系列外设寄存器映射头文件注意:这里没有main.c,也没有led.c——它只管“让芯片活过来”,不管“让 LED 亮起来”。下面逐个讲清每个文件在真实项目中的不可替代性。
2.1startup_stm32f10x_md.s:复位后第一行执行的汇编,决定程序从哪开始
这是芯片上电/复位后 CPU 取指的第一段代码。很多人以为只要main()编译进去就行,但忽略了一个致命前提:栈指针 SP 必须在第一条 C 语句执行前就指向合法 RAM 地址。startup_stm32f10x_md.s正是干这事的:
; startup_stm32f10x_md.s 片段(基于 MD 型:Medium Density,64KB Flash) Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp ; 复位向量入口 Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 ; 先调用 SystemInit() LDR R0, =__main BX R0 ; 再跳转到 C 运行时入口 ENDP关键参数说明:
Stack_Size = 0x400(1KB)是默认栈大小,若你的项目用了大量局部数组或递归函数,此处不足会导致 HardFault;IMPORT SystemInit表明它依赖system_stm32f10x.c中的初始化函数,二者必须同时存在;__main是 ARM C 库的初始化入口(设置.data段复制、.bss清零等),不是用户main()—— 这是新手最常混淆的点。
2.2system_stm32f10x.c:时钟树的“总开关”,所有外设速度的源头
SystemInit()函数是整个 SYSTEM 文件夹的灵魂。它不直接操作外设寄存器,而是配置 RCC(Reset and Clock Control)模块,决定 CPU、APB1、APB2 总线的最终频率。以最常见的 HSE+PLL 配置为例(外部 8MHz 晶振 → 72MHz 系统时钟):
// system_stm32f10x.c 片段(精简版) void SystemInit (void) { /* 1. 使能 HSE(外部高速晶振) */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); while((RCC->CR & RCC_CR_HSERDY) == 0) { } // 等待稳定 /* 2. 配置 PLL:HSE * 9 = 72MHz */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9); /* 3. 使能 PLL 并等待锁定 */ RCC->CR |= RCC_CR_PLLON; while((RCC->CR & RCC_CR_PLLRDY) == 0) { } /* 4. 切换系统时钟源为 PLL */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; /* 5. 等待切换完成 */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)RCC_CFGR_SWS_PLL) { } }为什么不能跳过这一步?
- 若未调用
SystemInit(),系统默认使用内部 8MHz RC 振荡器(HSI),且 PLL 关闭 → CPU 频率仅 8MHz,串口波特率误差超 10%,无法通信;RCC_CFGR_PLLMULL9是关键倍频系数,F103 最大支持 ×9(72MHz),F100 系列则最大 ×6(48MHz),选错会触发 PLL 锁定失败(PLL_RDY=0);RCC->CFGR寄存器需按位操作(&=清除旧值,|=设置新值),直接=覆盖会误关其他时钟门控(如 USB、ADC),导致外设失能。
2.3stm32f10x.h与core_cm3.h:寄存器定义的“字典”,不是可有可无的头文件
有人觉得“我直接写*(volatile uint32_t*)0x40021000 = 0x1;不也一样?”,但这样写既难维护又易出错。stm32f10x.h提供了结构化、可读性强的寄存器映射:
// stm32f10x.h 中对 RCC 的定义(节选) typedef struct { __IO uint32_t CR; /*!< RCC clock control register, Address offset: 0x00 */ __IO uint32_t CFGR; /*!< RCC clock configuration register, Address offset: 0x04 */ __IO uint32_t CIR; /*!< RCC clock interrupt register, Address offset: 0x08 */ // ... 其他寄存器 } RCC_TypeDef; #define RCC ((RCC_TypeDef *) RCC_BASE) // RCC_BASE = 0x40021000而core_cm3.h则定义了 Cortex-M3 内核寄存器(如SCB->VTOR,SysTick->LOAD),这些在裸机中必须手动配置。例如重定向中断向量表到 SRAM(用于 Bootloader 动态加载应用):
// 在 main() 开头添加(假设向量表放在 0x20000000) SCB->VTOR = 0x20000000;没有core_cm3.h,你就得查 ARM 官方文档找VTOR偏移地址,效率低且易错。
3. 集成到工程:Keil、IAR、GCC 三大工具链的链接与配置要点
SYSTEM 文件夹本身不编译,必须被正确加入工程并参与链接。不同工具链对启动文件、内存布局、符号解析的处理差异极大,稍有不慎就报undefined reference to 'SystemInit'或vector table not found。以下是我实测有效的三套配置方案,全部基于真实项目(某工业传感器节点,Keil v5.37 / IAR EWARM v8.50 / GCC arm-none-eabi-gcc 10.3)。
3.1 Keil MDK:.s文件必须设为“Use MicroLIB”且禁用分散加载
Keil 对汇编启动文件有特殊要求。若你在Options for Target → C/C++ → Use MicroLIB未勾选,__main将无法解析__user_initial_stackheap符号,导致链接失败:
提示:MicroLIB 是 ARM 提供的轻量级 C 库,专为嵌入式设计,不包含
printf等浮点依赖,栈空间占用比标准库小 60%。F10x 资源紧张,必须启用。
同时,startup_stm32f10x_md.s的属性必须设为:
Options for File → Generate assembler listing✅Options for File → Always build✅Options for File → Use MicroLIB✅(全局设置也要开)
内存布局(Options for Target → Linker → Use Memory Layout from Target)中,确保:
IRAM1起始地址0x20000000,大小0x5000(20KB)IROM1起始地址0x08000000,大小0x10000(64KB)- 禁止勾选
Use Memory Layout from Target下的Use Scatter File—— 否则 Keil 会自动生成 scatter 文件覆盖你的向量表位置。
3.2 IAR EWARM:.s文件需手动指定为“Assembly file”,且__vector_table必须显式导出
IAR 默认将.s当作 C 文件处理,必须右键文件 →Options→Language→Assembly。更关键的是,IAR 不像 Keil 那样自动识别__vector_table符号,需在startup_stm32f10x_md.s末尾强制导出:
; 在 startup_stm32f10x_md.s 末尾添加 PUBLIC __vector_table EXTERN Reset_Handler AREA |.text|, CODE, READONLY __vector_table DCD sfe(CSTACK) ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler ; ... 后续中断向量(共 60 项)否则链接器报错Error[e16]: segment __vector_table has no base address。此外,IAR 的icf链接脚本中必须明确定义向量表位置:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0800FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x20004FFF; place at address mem:__ICFEDIT_region_ROM_start__ { readonly section .intvec };3.3 GCC(arm-none-eabi-gcc):.ld脚本必须重定向.isr_vector段
GCC 工具链下,startup_stm32f10x_md.s中的向量表会被编译为.isr_vector段,而非.text。若STM32F103C8Tx_FLASH.ld链接脚本未显式放置该段,向量表将随机落址,复位必失败:
/* STM32F103C8Tx_FLASH.ld 片段 */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* <-- 关键:强制保留向量表在起始位置 */ . = ALIGN(4); } > FLASH .text : { . = ALIGN(4); *(.text) *(.rodata) . = ALIGN(4); } > FLASH }编译命令中还需加-Wl,--entry=Reset_Handler显式指定入口,否则链接器默认用main,跳过Reset_Handler直接进 C 运行时,SystemInit()永远不会执行。
4. 避坑指南:五个血泪经验总结的 SYSTEM 层常见问题与排查路径
SYSTEM 文件夹的问题往往表现为“程序不运行”“串口没输出”“调试器连不上”,现象隐蔽、原因分散。以下是我在某跨平台固件升级项目中踩过的坑,每一条都附带现场日志和定位方法。
4.1 现象:下载程序后 LED 不闪,J-Link 报Cannot connect to target
原因:startup_stm32f10x_md.s中栈顶地址(__initial_sp)超出 SRAM 范围,复位时写入非法地址触发 HardFault,CPU 锁死。
解决:检查Stack_Size是否大于实际 RAM(F103C8T6 为 20KB)。用 J-Link Commander 执行mem32 0x20000000 1查看栈顶附近是否为0xFFFFFFFF(未初始化),若是则说明栈溢出。将Stack_Size改为0x00000200(512 字节)再试。
4.2 现象:SystemCoreClock变量始终为8000000,SysTick_Config(SystemCoreClock/1000)返回 0
原因:system_stm32f10x.c中SystemInit()被优化掉。GCC 下若未在main()前加__attribute__((constructor))或 Keil 下未关闭Optimization Level,编译器可能剔除“未被显式调用”的函数。
解决:Keil 中Options for Target → C/C++ → Optimization → Level = 0;GCC 下在SystemInit()声明前加__attribute__((used))。验证方法:在SystemInit()第一行加__NOP(),用调试器单步确认是否执行。
4.3 现象:串口 115200 波特率收发乱码,示波器测 TX 引脚波形周期不对
原因:RCC_CFGR配置错误导致 APB2 总线频率非预期值。F103 默认 APB2 为 HCLK(72MHz),但若RCC_CFGR中PPRE2位被误设为/2,则 APB2 实际为 36MHz → USARTDIV 计算错误。
解决:用调试器查看RCC->CFGR寄存器值,重点检查 bit13~bit11(PPRE2):000=HCLK,100=HCLK/2。应为000。修改system_stm32f10x.c中RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;。
4.4 现象:NVIC_EnableIRQ(USART1_IRQn)后中断不触发,NVIC->ISER[0]对应位为 0
原因:system_stm32f10x.c未使能对应外设时钟。NVIC本身由内核提供,但USART1时钟需RCC_APB2ENR显式开启。
解决:在SystemInit()结尾或main()开头添加:
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // 使能 USART1 时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能 GPIOA 时钟(USART1_TX/RX 所在端口)缺一不可,否则寄存器写无效。
4.5 现象:Bootloader 跳转到应用区后立即 HardFault,SCB->VTOR显示为0x08000000
原因:应用区的startup_stm32f10x_md.s未重定向向量表。Bootloader 跳转后 CPU 仍从0x08000000取向量,但该地址是 Bootloader 的向量表,地址错乱。
解决:在应用区main()第一行强制设置:
SCB->VTOR = 0x08002000; // 假设应用区起始为 0x08002000,向量表在此处 __DSB(); __ISB(); // 数据/指令同步屏障,确保 VTOR 生效且应用区的startup_stm32f10x_md.s中__vector_table必须放在0x08002000,需修改链接脚本。
5. 进阶技巧:动态时钟切换与低功耗模式下的 SYSTEM 层适配
当项目进入量产阶段,单纯“上电初始化一次”已不够用。比如某电池供电的环境监测节点,需白天用 72MHz 处理传感器数据,夜间切至 1MHz HSI+Stop 模式省电;又比如固件升级时需临时关闭 PLL 以降低功耗。此时system_stm32f10x.c必须支持运行时重配,而不仅是复位时一次性初始化。
5.1 实现SystemClockSwitchToHSI():安全关闭 PLL 并切回内部 RC
直接RCC->CR &= ~RCC_CR_PLLON会引发严重后果——若当前系统时钟源是 PLL,关闭瞬间 CPU 失去时钟,直接锁死。正确做法是先切回 HSI,再关 PLL:
// 在 system_stm32f10x.c 中新增函数 ErrorStatus SystemClockSwitchToHSI(void) { uint32_t timeout = 0; // 1. 切换系统时钟源为 HSI(8MHz) RCC->CFGR &= (uint32_t)~(RCC_CFGR_SW); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSI; while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)RCC_CFGR_SWS_HSI) { if (++timeout == HSI_TIMEOUT_VALUE) return ERROR; // 自定义超时宏 } // 2. 关闭 PLL(此时已不依赖 PLL) RCC->CR &= (uint32_t)~(RCC_CR_PLLON); while ((RCC->CR & RCC_CR_PLLRDY) != 0) { } // 3. 更新 SystemCoreClock 全局变量 SystemCoreClock = HSI_VALUE; return SUCCESS; }关键细节:
HSI_TIMEOUT_VALUE建议设为0x1000(4096 次循环),避免无限等待;SystemCoreClock必须手动更新,否则SysTick_Config()仍按旧值计算;- 切换后所有 APB 总线频率同步变为 8MHz,需重新配置 UART 波特率、TIM 重装载值等。
5.2 Stop Mode 下唤醒后恢复时钟:PWR_EnterSTOPMode()后的必要补救
F10x 进入 Stop 模式时,HSI/PLL/HSE 全部关闭,唤醒后需手动重启时钟。但SystemInit()不能直接重入——它假设所有时钟源初始为关闭状态,而 Stop 唤醒后 HSI 是自动开启的。因此需定制SystemClockRestoreAfterStop():
// 唤醒后立即调用(在 EXTI 唤醒中断服务函数末尾) void SystemClockRestoreAfterStop(void) { // 1. 等待 HSI 稳定(唤醒后 HSI 已开,但仍需确认 RDY) while((RCC->CR & RCC_CR_HSIRDY) == 0) { } // 2. 重新配置 PLL(若需要 72MHz) RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9); RCC->CR |= RCC_CR_PLLON; while((RCC->CR & RCC_CR_PLLRDY) == 0) { } // 3. 切回 PLL 作为系统时钟 RCC->CFGR &= (uint32_t)~(RCC_CFGR_SW); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)RCC_CFGR_SWS_PLL) { } SystemCoreClock = 72000000; }5.3 验证时钟是否真正生效:三重交叉校验法
光看SystemCoreClock变量不准,必须硬件实测。我习惯用以下组合验证:
| 方法 | 操作步骤 | 判定标准 |
|---|---|---|
| SysTick 溢出计时 | SysTick_Config(72000000/10)→ 每 100ns 进一次中断,用逻辑分析仪测SysTick->VAL重载周期 | 周期应为 100ns ± 1%(示波器实测) |
| GPIO 翻转测频 | 在main()循环中GPIOA->ODR ^= GPIO_ODR_ODR0,用示波器测 PA0 频率 | 若SystemCoreClock=72MHz,理论最高翻转频率 36MHz(指令周期限制) |
| UART 回环测试 | 发送固定字符串,用 USB-TTL 模块接收,用串口助手看是否无乱码、无丢帧 | 115200 波特率下误码率 < 1e-6 即达标 |
从那以后我每次新建 F10x 工程,都会在main()开头强制插入while(1) { __NOP(); },用调试器单步确认SystemInit()执行完毕、SystemCoreClock正确、RCC->CFGR寄存器值符合预期,再删掉这行。这 30 秒的停顿,省去了后面 3 小时的 HardFault 排查。希望帮到你。
本文还有配套的精品资源,点击获取