☰
STM32F103C8T6 最小系统与 LED 流水灯实验报告
2026/9/30 4:42:09 网站建设 项目流程

一、STM32 最小系统核心板认识

1.1 核心板概述

STM32F103C8T6 最小系统板在国际上通常被称为"Blue Pill"开发板,因其蓝色 PCB 和口香糖大小的外形而得名。其核心参数如下:

一、STM32 最小系统核心板认识

1.1 核心板概述

STM32F103C8T6 最小系统板在国际上通常被称为"Blue Pill"(蓝色药丸)开发板,因其蓝色 PCB 和口香糖大小的外形而得名。其核心参数如下:

项目参数
主控芯片STM32F103C8T6(LQFP48 封装)
内核ARM Cortex-M3,最高主频 72 MHz
Flash64 KB(实际多数芯片可解锁 128 KB)
SRAM20 KB
供电5V(USB / 排针)或 3.3V,板载 AMS1117-3.3 稳压
时钟8 MHz 外部高速晶振(HSE)+ 32.768 kHz 低速晶振(LSE)
GPIO3 组端口 PA/PB/PC,共 37 个可用 IO
调试接口SWD(PA13=SWDIO,PA14=SWCLK)
板载 LED接在PC13,低电平点亮

1.2 最小系统的组成

一个 STM32 最小系统要正常工作,必须包含以下几个部分:

  1. 电源电路:3.3V 供电,VDD/VSS、VDDA/VSSA 均需连接,每个电源引脚就近放置 0.1µF 去耦电容;板上由 AMS1117 将 5V 降压为 3.3V。

  2. 时钟电路:8 MHz 无源晶振 + 两个负载电容(约 20 pF)构成 HSE;32.768 kHz 晶振为 RTC 提供时钟(LSE)。若不使用外部晶振,也可使用内部 8 MHz HSI RC 振荡器。

  3. 复位电路:NRST 引脚通过 10 kΩ 上拉至 3.3V,并接 0.1µF 电容到地,配合复位按键实现上电复位与手动复位。

  4. 启动配置电路:BOOT0、BOOT1 跳线决定启动方式:

    • BOOT0=0:从主 Flash 启动(正常运行模式);

    • BOOT0=1、BOOT1=0:从系统存储器启动(串口 ISP 下载模式);

    • BOOT0=1、BOOT1=1:从内置 SRAM 启动(调试用)。

  5. 调试/下载接口:SWD 接口(SWDIO、SWCLK、GND、3.3V 四线),连接 ST-Link 烧录调试。

1.3 引脚图分析(实验所用端口)

由核心板引脚图可知(见题图):

  • GPIOA:PA0~PA15 基本全部引出,PA9/PA10 为 USART1,PA11/PA12 为 USB,PA13/PA14 为 SWD 调试口(作 GPIO 使用需谨慎)。

  • GPIOB:PB0~PB15 大部分引出,PB2 与 BOOT1 复用。

  • GPIOC:该芯片为 48 引脚封装,只引出了PC13、PC14、PC15三个引脚。其中 PC14/PC15 默认接 32.768 kHz 晶振,驱动能力有限(数据手册注明吸入电流不超过 3 mA,且不能作为电流源);PC13 上已焊接板载 LED。

  • 板载 LED 电路:3.3V → 限流电阻 → LED →PC13,因此PC13 输出低电平时 LED 点亮,输出高电平时熄灭(灌电流接法)。

1.4 实验前准备步骤

  1. 用万用表检查核心板 5V、3.3V 与 GND 之间有无短路;

  2. 确认 BOOT0 跳线置于 0(Flash 启动);

  3. 将 ST-Link 的 SWDIO、SWCLK、GND、3.3V 分别接到核心板对应排针(核心板底部 4 针即为 SWD 接口,对应 PA13/PA14);

  4. Keil 中安装 STM32F1xx_DFP 器件支持包,Debug 选项选择 ST-Link Debugger,能识别到芯片 IDCODE(0x1BA01477)即连接正常;

  5. 先烧录一个 PC13 闪烁的小程序,验证"下载—运行"链路通畅。


二、实验任务 1:寄存器方式实现流水灯

2.1 电路搭建

在面包板上搭建 3 只 LED(红、绿、蓝),采用低电平点亮(灌电流)接法:

3.3V ──[ 220Ω 限流电阻 ]──▶|── PA0(红) LED 阳极接电阻,阴极接引脚 3.3V ──[ 220Ω 限流电阻 ]──▶|── PB0(绿) 3.3V ──[ 220Ω 限流电阻 ]──▶|── PC14(蓝)

说明:STM32 IO 灌电流能力(最大 25 mA)强于拉电流,故 LED 阳极接 3.3V、阴极经限流电阻接 IO 口,引脚输出 0 灯亮、输出 1 灯灭。限流电阻按 (3.3V − 2V) / 5~10mA ≈ 150~220Ω 选取。 任务 1 第 3 步再把板载 PC13 LED 加入流水序列。

2.2 程序设计思路与寄存器地址

(1)总线与寄存器映射关系

STM32F103 的 GPIO 全部挂在APB2 总线上。操作 GPIO 之前必须先在 RCC(复位与时钟控制器)中使能对应端口的时钟,否则对 GPIO 寄存器的写操作无效。相关外设基地址(参考《STM32F10x 参考手册》存储器映像表):

寄存器地址说明
RCC 基地址0x4002 1000复位与时钟控制
RCC_APB2ENR0x4002 1018APB2 外设时钟使能寄存器(基地址+0x18)
GPIOA 基地址0x4001 0800端口 A
GPIOB 基地址0x4001 0C00端口 B
GPIOC 基地址0x4001 1000端口 C

RCC_APB2ENR 与本实验相关的位:

位名称功能
bit2IOPAEN1 = 使能 GPIOA 时钟
bit3IOPBEN1 = 使能 GPIOB 时钟
bit4IOPCEN1 = 使能 GPIOC 时钟
(2)GPIO 端口内部寄存器(偏移地址)
寄存器名偏移地址示例(以 GPIOA 为例)功能
GPIOx_CRL0x000x4001 0800端口配置低寄存器(引脚 0~7)
GPIOx_CRH0x040x4001 0804端口配置高寄存器(引脚 8~15)
GPIOx_IDR0x080x4001 0808输入数据寄存器(只读)
GPIOx_ODR0x0C0x4001 080C输出数据寄存器
GPIOx_BSRR0x100x4001 0810位设置/清除寄存器(低 16 位置 1,高 16 位清 0)
GPIOx_BRR0x140x4001 0814位清除寄存器
GPIOx_LCKR0x180x4001 0818配置锁定寄存器
(3)CRL/CRH 的位配置规则

每个引脚占 4 个配置位:CNF[1:0] + MODE[1:0]。本实验将引脚配置为通用推挽输出,最大速度 50 MHz,即:

  • MODE[1:0] = 11(输出模式,50 MHz)

  • CNF[1:0] = 00(通用推挽输出)

组合值 =0b0011 = 0x3。引脚 n 在 CRL 中的位段为[4n+3 : 4n],例如:

  • PA0 → GPIOA_CRL 的 bit[3:0] 写入 0x3;

  • PB0 → GPIOB_CRL 的 bit[3:0] 写入 0x3;

  • PC14 → GPIOC_CRH 的 bit[27:24](引脚 14 在 CRH 中的位置为 (14−8)×4 = 24)写入 0x3。

(4)程序流程
开始 ├─ 使能 GPIOA / GPIOB / GPIOC 时钟(RCC_APB2ENR) ├─ 配置 PA0、PB0、PC14 为推挽输出 50MHz └─ while(1) 循环: ├─ PA0 亮(其余灭)→ 软件延时 1 秒 ├─ PB0 亮(其余灭)→ 软件延时 1 秒 └─ PC14 亮(其余灭)→ 软件延时 1 秒

软件延时估算:系统默认 72 MHz(若未配置时钟树,复位后使用 HSI 8 MHz)。一条空循环约消耗 3~4 个时钟周期,72 MHz 下延时 1 秒约需循环72000000/3.4 ≈ 20000000次,代码中取0x4C4B40(5,000,000)×内层若干次做近似,实际精确值以任务 2 中逻辑分析仪实测为准。

2.3加入板载 PC13 LED 的新程序

查阅最小系统板原理图可知:板载 LED 接在3.3V → 限流电阻 → LED → PC13,属于灌电流接法,PC13 输出低电平点亮、高电平熄灭(这点从题图中 "PC13 LED" 标注也可印证)。PC13 是验证代码是否正常烧录运行的最便捷指示灯。

新程序流水顺序改为:PA0(红)→ PB0(绿)→ PC14(蓝)→ PC13(板载灯),共 4 个状态循环。只需在上一个程序基础上增加 PC13 的配置与控制:

/** * @file main.c (加入板载 PC13 LED 的流水灯版本) * @brief PA0 -> PB0 -> PC14 -> PC13(板载) 循环点亮,间隔约 1 秒 * PC13 同样是低电平点亮 */ #define RCC_APB2ENR (*(volatile unsigned int *)0x40021018) #define GPIOA_CRL (*(volatile unsigned int *)0x40010800) #define GPIOA_BSRR (*(volatile unsigned int *)0x40010810) #define GPIOB_CRL (*(volatile unsigned int *)0x40010C00) #define GPIOB_BSRR (*(volatile unsigned int *)0x40010C10) #define GPIOC_CRH (*(volatile unsigned int *)0x40011004) #define GPIOC_BSRR (*(volatile unsigned int *)0x40011010) void Delay_1s(unsigned int n) { unsigned int i, j; for (j = 0; j < n; j++) for (i = 0; i < 5000000; i++); } int main(void) { /* 使能 GPIOA、GPIOB、GPIOC 时钟 */ RCC_APB2ENR |= (1 << 2) | (1 << 3) | (1 << 4); /* PA0、PB0 推挽输出 50MHz */ GPIOA_CRL &= ~(0xF << 0); GPIOA_CRL |= (0x3 << 0); GPIOB_CRL &= ~(0xF << 0); GPIOB_CRL |= (0x3 << 0); /* PC13、PC14 都在 CRH: PC13 -> bit[23:20]((13-8)*4=20),PC14 -> bit[27:24] */ GPIOC_CRH &= ~((0xF << 20) | (0xF << 24)); GPIOC_CRH |= ((0x3 << 20) | (0x3 << 24)); /* 全部熄灭 */ GPIOA_BSRR = (1 << 0); GPIOB_BSRR = (1 << 0); GPIOC_BSRR = (1 << 13) | (1 << 14); while (1) { /* 状态1:PA0 亮 */ GPIOA_BSRR = (1 << 16); /* PA0=0 亮 */ GPIOB_BSRR = (1 << 0); GPIOC_BSRR = (1 << 13) | (1 << 14); Delay_1s(1); /* 状态2:PB0 亮 */ GPIOA_BSRR = (1 << 0); GPIOB_BSRR = (1 << 16); /* PB0=0 亮 */ GPIOC_BSRR = (1 << 13) | (1 << 14); Delay_1s(1); /* 状态3:PC14 亮 */ GPIOA_BSRR = (1 << 0); GPIOB_BSRR = (1 << 0); GPIOC_BSRR = (1 << 13) | (1 << 30); /* PC14=0 亮(30=14+16) */ Delay_1s(1); /* 状态4:板载 PC13 亮 */ GPIOA_BSRR = (1 << 0); GPIOB_BSRR = (1 << 0); GPIOC_BSRR = (1 << 14) | (1 << 29); /* PC13=0 亮(29=13+16) */ Delay_1s(1); } }

三、实验任务 2:标准外设库(SPL)方式 + Keil 逻辑分析仪

3.1 标准外设库工程创建详细过程

STM32 标准外设库(Standard Peripheral Library,SPL)官方下载地址:https://www.st.com/zh/embedded-software/stm32-standard-peripheral-libraries.html下载STM32F10x_StdPeriph_Lib_V3.5.0(或 V3.6.0),解压后建立如下工程目录结构:

LED_SPL/ ├── User/ ← 用户代码 │ ├── main.c ← 主程序(自己编写) │ ├── stm32f10x_conf.h ← 库配置文件(从库 Project 模板复制) │ ├── stm32f10x_it.c/.h ← 中断服务函数(从模板复制) │ └── system_stm32f10x.c ← 系统时钟配置(库自带) ├── Core/ ← 内核与启动文件 │ ├── core_cm3.c / core_cm3.h ← Cortex-M3 内核支持(Libraries/CMSIS) │ ├── stm32f10x.h ← 器件寄存器总头文件 │ └── startup_stm32f10x_md.s ← 启动汇编(md=中容量,C8T6 用此文件) ├── Library/ ← 标准外设库源文件 │ ├── src/ ← stm32f10x_gpio.c, stm32f10x_rcc.c, misc.c 等(Libraries/STM32F10x_StdPeriph_Driver/src) │ └── inc/ ← 对应头文件 ├── Output/ ← 编译输出(.hex/.axf,在 Keil 中指定) └── LED_SPL.uvprojx ← Keil 工程文件

Keil 中的具体操作步骤:

  1. 新建工程:Project → New µVision Project,芯片选择STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8;

  2. 添加文件分组:在工程目录树中建立User、Core、Library三个 Group,把上面对应的.c文件和startup_stm32f10x_md.s添加进各分组(C8T6 是 64KB Flash 中容量器件,必须选_md.s,选错会导致启动异常);

  3. 设置头文件路径:Options for Target → C/C++ → Include Paths,加入User、Core、Library/inc三个目录;

  4. 添加全局宏定义:在 C/C++ → Define 中填写STM32F10X_MD,USE_STDPERIPH_DRIVER(前者告诉库芯片是中容量型号,后者启用标准外设库);

  5. 配置输出与下载器:Output 页勾选 Create HEX File,输出目录选Output;Debug 页选择 ST-Link Debugger,Settings 中确认 SW 模式能识别芯片,Flash Download 页勾选 Reset and Run;

  6. 编译(F7)0 Error 0 Warning 后,按 F8/Download 烧录。

3.2 标准外设库流水灯代码

SPL 方式不再直接写地址,而是调用库函数。库内部仍是对上述寄存器的封装:RCC_APB2PeriphClockCmd()最终就是写RCC->APB2ENR,GPIO_Init()最终就是写CRL/CRH,GPIO_SetBits()/GPIO_ResetBits()最终就是写BSRR。

/** * @file main.c * @brief 标准外设库(SPL)方式流水灯 * PA0 -> PB0 -> PC13(板载) 轮流点亮,间隔约 1 秒,低电平点亮 */ #include "stm32f10x.h" /* 粗略软件延时:72MHz 下 n≈8000 时约 1ms */ void Delay_ms(uint32_t nms) { uint32_t i; while (nms--) for (i = 0; i < 8000; i++); } void LED_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; /* GPIO 初始化结构体 */ /* 1. 使能 GPIOA、GPIOB、GPIOC 时钟(APB2 总线) */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); /* 2. 配置 PA0、PB0、PC13 为推挽输出 50MHz */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; /* 通用推挽输出 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; /* 输出速度 50MHz */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOA, &GPIO_InitStructure); /* PA0 */ GPIO_Init(GPIOB, &GPIO_InitStructure); /* PB0 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_Init(GPIOC, &GPIO_InitStructure); /* PC13 板载灯 */ /* 3. 初始全灭(低电平点亮,故全置 1) */ GPIO_SetBits(GPIOA, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOC, GPIO_Pin_13); } int main(void) { LED_GPIO_Init(); while (1) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); /* PA0 = 0,红灯亮 */ Delay_ms(1000); GPIO_SetBits(GPIOA, GPIO_Pin_0); /* PA0 = 1,灭 */ GPIO_ResetBits(GPIOB, GPIO_Pin_0); /* PB0 亮 */ Delay_ms(1000); GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOC, GPIO_Pin_13); /* 板载 PC13 亮 */ Delay_ms(1000); GPIO_SetBits(GPIOC, GPIO_Pin_13); } }

3.3 软件延时的准确周期分析

Delay_ms(1000)是"空转"循环延时,其精确耗时取决于:

  • 系统主频(72 MHz 还是默认 8 MHz HSI);

  • 编译器优化等级(-O0/-O2 生成的循环指令数不同);

  • Flash 等待周期与流水线效应。

按 72 MHz、-O0 估算:内层一次空循环约 4 个时钟周期,for(i=0;i<8000;i++)≈ 8000×4/72MHz ≈0.44 ms,即Delay_ms(1000)理论值只有约0.44 秒,并不准——这说明纯软件循环延时只能定性演示,不能精确定时(精确定时应使用 SysTick 或定时器)。因此需要用仪器实测。

四、实验任务 3:HAL 库 + 按键中断控制暂停/恢复

4.1 设计思路

在任务 2 基础上做两处改变:

  1. 改用 HAL 库(STM32CubeMX 生成初始化代码);

  2. 增加一个按键,接在PB12(另一端接 GND),配置为外部中断下降沿触发(按键未按时由内部上拉为高,按下接地产生下降沿)。每按一次,翻转一个运行标志g_run:流水灯暂停 ↔ 恢复。

外部中断路径:PB12 → EXTI15_10 中断线(EXTI12)→ NVIC。CubeMX 中配置步骤:

  • PA0、PB0、PC13 设为GPIO_Output;

  • PB12 设为GPIO_EXTI12,Pull-up,触发方式 Falling Edge;

  • NVIC 中使能EXTI15_10 IRQn,优先级保持默认;

  • 时钟树配置为 HSE 8 MHz → PLL ×9 → SYSCLK 72 MHz。

4.2 HAL 库代码(关键部分)

/* main.c 关键代码(CubeMX 生成框架内 USER CODE 区填写) */ /* 用户全局变量:流水灯运行标志,1=运行,0=暂停 */ volatile uint8_t g_run = 1; /* 延时消抖用的时间戳 */ uint32_t last_tick = 0; /** * @brief HAL 外部中断回调函数(所有 EXTI 共用,按引脚区分) * 该函数由 HAL_GPIO_EXTI_IRQHandler() 在清中断标志后调用 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_12) /* 确认是 PB12 触发 */ { /* 软件消抖:距上次触发不足 50ms 则忽略 */ if (HAL_GetTick() - last_tick > 50) { g_run = !g_run; /* 翻转运行标志:暂停/恢复 */ last_tick = HAL_GetTick(); } } } /* 中断服务函数在 stm32f1xx_it.c 中: void EXTI15_10_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12); // 清标志并回调 } */ /* 流水灯状态机(便于暂停时保持现场) */ typedef enum { LED_RED = 0, LED_GREEN, LED_BLUE, LED_MAX } LedState_t; LedState_t state = LED_RED; uint32_t state_tick = 0; int main(void) { HAL_Init(); SystemClock_Config(); /* CubeMX 生成:72MHz */ MX_GPIO_Init(); /* CubeMX 生成:LED 输出 + PB12 EXTI */ state_tick = HAL_GetTick(); while (1) { if (g_run) /* 运行标志为 1 才推进流水 */ { if (HAL_GetTick() - state_tick >= 1000) /* 非阻塞 1 秒 */ { state_tick = HAL_GetTick(); /* 全部熄灭 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); /* 点亮当前状态对应的灯 */ switch (state) { case LED_RED: HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); break; case LED_GREEN: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); break; case LED_BLUE: HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); break; default: break; } state = (state + 1) % LED_MAX; /* 切换到下一状态 */ } } /* g_run == 0 时什么都不做,LED 保持暂停时的亮灭状态 */ } }

要点说明:

  • 用HAL_GetTick()非阻塞延时代替HAL_Delay():否则暂停后程序卡在 Delay 里,按键虽能进中断翻转标志,但恢复时相位会乱;非阻塞方式暂停时 LED 保持当前状态,恢复后接着走,体验正确;

  • volatile修饰g_run:防止编译器把它优化进寄存器,导致主循环读不到中断里的修改;

  • 中断回调里只做"翻转标志"这种极短操作,不在中断里延时,符合中断编程规范;

  • 消抖采用 50 ms 时间窗,避免按键机械抖动造成一次按下翻转多次。

4.3 实验现象

  • 上电后 PA0→PB0→PC13 依次点亮,间隔 1 秒;

  • 按一下 PB12 按键,流水暂停,当前灯保持点亮;

  • 再按一下,流水从暂停处恢复继续运行。


五、三种开发方式对比总结

对比项寄存器方式标准外设库 SPLHAL 库
代码可读性差,满屏地址和位操作较好好,配合 CubeMX 图形化配置
对硬件理解最深,直达寄存器中等较浅,封装最厚
代码体积/效率最小最快中等较大
可移植性无同系列内可移植跨系列移植最好
维护状态—官方已停止更新官方主推
适用场景学习原理、极致优化老项目维护、教学新项目开发

本次实验从寄存器 → SPL → HAL 逐级抽象,既理解了 GPIO 的底层工作机制(时钟使能 → 模式配置 → 数据读写),又掌握了现代工程的开发流程(CubeMX 配置 + 中断回调 + 非阻塞编程)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询