STM32零基础入门实战:从寄存器到标准库函数控制LED流水灯(基于STM32F103C8T6)
本文介绍了基于STM32F103C8T6的LED流水灯控制实战教程,适合嵌入式初学者。通过Keil5开发环境,使用寄存器和标准库两种方式实现LED控制。硬件方面,开发板自带PC13引脚LED,外接红绿蓝LED分别连接至PA0、PB1、PC14引脚,采用共阳极接法。教程详细讲解了工程建立、寄存器地址计算(如RCC_APB2ENR、GPIOx_CRL/CRH等)和配置步骤,并提供了相关资料下载链接。通
文章目录
STM32零基础入门实战:从寄存器到标准库函数控制LED流水灯(基于STM32F103C8T6)
前言
适合人群:刚接触STM32的电子爱好者、嵌入式初学者
开发环境:Keil MDK-ARM(Keil5)、STM32F103C8T6最小系统板
目标成果:实现多个LED轮流闪烁,掌握寄存器和标准外设库两种编程方式
开发工具:电脑、STM32F103C8T6、面包板、红绿蓝LED灯、杜邦线、stlink
电脑需安装好keil5和stlink驱动,本文不过多赘述,教程可参考其他博主的
keil5安装与配置
实验任务1:使用寄存器方式控制LED流水灯
1. 硬件准备与原理图分析
我们使用的开发板是 STM32F103C8T6 最小系统板,它已经自带一个LED连接在 PC13 引脚上。

这意味着:
当 PC13 输出 低电平(0) → LED亮(导通)
当 PC13 输出 高电平(1) → LED灭(截止)
✅ 所以我们要让这个灯亮,就得把 PC13 设为输出,并写入 0!
此外,我们外接三只LED(红、绿、蓝)分别连接到:
- PA0 → 红色LED(共阳极)
- PB1 → 绿色LED(共阳极)
- PC14 → 蓝色LED(共阳极)
所有LED均为共阳极接法,因此控制逻辑统一为:输出低电平点亮,高电平熄灭。
2. 新建工程
(1)建立存放工程的文件夹
新建文件夹,文件名为“STM32Project”,以后的工程都存放在该文件夹下,这样方便工程管理。STM32工程可参照以下目录创建:
(2)打开Keil5软件新建并保存工程
点击Project—New uVision Project—工程存放路径存放至刚新建的文件夹下。在此路径需要再新建一个文件夹用于存放本次工程。—工程文件名推荐使用通用名称:Project。工程具体的作用可以在文件夹名称中进行说明,比如“2-1 STM32工程模板”。文件夹的名称是很方便改的,但项目名称不容易更改。


(3)选择器件型号Software Packs—STMicroelectronics—STM32F1 Series—STM32F103C8。
点击OK后会弹出一个Manage Run-Time Environment的窗口,这是Keil软件的一个新建工程小助手,可以帮助快速新建工程,暂时不需要该助手,关闭即可。

(4)从固件库中选取必要的工程文件添加到项目文件夹下
相关资料下载链接:资料下载
选择“STM32入门教程资料”资料进行下载,该文件中有下述步骤用到的固件库


将上述文件全部复制到工程文件中新建的Start文件夹下
将文件夹名称改为Start
右键
打开Start文件夹,添加所有后缀为md.s``.c``.h的文件
(5)在工程选项中添加第4步新建文件夹的头文件路径
需要在工程选项中添加新建文件夹的头文件路径,否则软件无法找到.h文件的。

在C/C++中include paths中添加头文件路径

在工程目录下新建User文件夹来存放main函数
右键Target 1
将新建组组名改为User,右键添加main函数


在main文件中右键插入头文件
写入简单的C语言程序,编译0错误,0警告后进行后续步骤
3. 程序设计思路与寄存器原理理解
STM32不是像51单片机那样直接操作P1=0x00,而是通过一系列内存映射的寄存器来配置和控制IO口。
我们需要操作以下几类寄存器:
| 寄存器名称 | 功能说明 | 地址偏移 |
|---|---|---|
| RCC_APB2ENR | 使能GPIOA/B/C时钟 | 0x18 |
| GPIOx_CRL / GPIOx_CRH | 配置IO口模式和速度(低8位/高8位) | CRL: 0x00, CRH: 0x04 |
| GPIOx_ODR | 输出数据寄存器(读写输出状态) | 0x0C |
📌 地址计算公式:
基地址 + 偏移量 = 实际寄存器地址
常用基地址来自
STM32F103参考手册:
- RCC基地址:
0x40021000 - GPIOA基地址:
0x40010800 - GPIOB基地址:
0x40010C00 - GPIOC基地址:
0x40011000
我们正常使用的寄存器的地址 = 基地址+偏移地址
例如:
RCC_APB2ENR地址 =0x40021000 + 0x18 = 0x40021018GPIOC_CRH地址 =0x40011000 + 0x04 = 0x40011004
(1)总线基地址
片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设, APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。APB1 总线的地址最低,因此片上外设就从这这个地址开始,也称外设基地址。
从上图可以看到 APB1 总线基地址是 0x4000 0000,相对外设基地址的偏移量是 0,所以此总线也是外设的基地址。
(2)外设基地址
每条总线上都会挂接着很多的外设,这些外设也会有自己的地址范围, 外设的最低地址就是外设的基地址,也称作边界地址。以 GPIO 外设来讲解外设基地址。其他的外设也是同样分析。
从图可以知道,外设 GPIOx 都是挂接在 APB2 总线上,属于高速的外设,而 APB2 总线的基地址是 0x4001 0000,故 GPIOA 的相对 APB2 总线的地址偏移是 800。
(3)外设寄存器地址
外设的寄存器就分布在其对应的外设地址范围内。这里我们以 GPIO 外设为例,GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,这些寄存器都是按顺序依次排列在外设的基地址上。寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOB 端口为例,来说明 GPIOB都有哪些寄存器,如所示。
以一下代码为例
//GPIOB.5端口输出高电平
GPIOB->ODR|=1<<5; //PB.5 输出高
//GPIOB端口全部输出高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
0x4001 0C0C 也就是GPIOB 端口ODR寄存器,一共32位,其中低16位控制这端口的输出(output data) 所以我们对这个地址写FFFF 也就是 1111 1111 1111 1111 就是让GPIOB的所有端口输出高电平
总结
每个寄存器都有一个访问地址,每个外设中的所有寄存器的位置都是固定的,每组寄存器的起始地址在《STM32参考手册》的表1中列明;
寄存器的地址 = 基地址+偏移地址 比如:
整个外设的基地址 = AHB1 的偏移+GPIOB 寄存器组的偏移+GPIOB_OSPEEDR 寄存器的偏移
0x4002 0410 = 0x4000 0000 + 0x0002 0000 + 0x0C00 + 0x10
既然理解了原理,那么我们就可一开始我们的代码编写啦!
4. C语言寄存器编程实现
#include "stm32f10x.h" // 包含基本类型定义(如uint32_t等)
// ==================== 定义寄存器地址指针 ====================
// 使用volatile确保每次访问都从内存读取,防止编译器优化
// RCC寄存器:用于开启GPIO时钟
#define RCC_BASE 0x40021000 // RCC外设基地址
#define RCC_APB2ENR (*(volatile unsigned long*)(RCC_BASE + 0x18)) // 时钟使能寄存器
// GPIOA寄存器
#define GPIOA_BASE 0x40010800 // GPIOA基地址
#define GPIOA_CRL (*(volatile unsigned long*)(GPIOA_BASE + 0x00)) // 低8位配置寄存器
#define GPIOA_ODR (*(volatile unsigned long*)(GPIOA_BASE + 0x0C)) // 输出数据寄存器
// GPIOB寄存器
#define GPIOB_BASE 0x40010C00 // GPIOB基地址
#define GPIOB_CRL (*(volatile unsigned long*)(GPIOB_BASE + 0x00))
#define GPIOB_ODR (*(volatile unsigned long*)(GPIOB_BASE + 0x0C))
// GPIOC寄存器(PC13和PC14属于高8位)
#define GPIOC_BASE 0x40011000 // GPIOC基地址
#define GPIOC_CRH (*(volatile unsigned long*)(GPIOC_BASE + 0x04)) // 高8位配置寄存器
#define GPIOC_ODR (*(volatile unsigned long*)(GPIOC_BASE + 0x0C))
// ==================== LED控制宏定义 ====================
// 因为LED是共阳极,低电平点亮,高电平熄灭
#define LED_RED_ON() GPIOA_ODR |= (1 << 0) // PA0 输出低电平 → 红灯亮
#define LED_RED_OFF() GPIOA_ODR &= ~(1 << 0) // PA0 输出高电平 → 红灯灭
#define LED_GREEN_ON() GPIOB_ODR |= (1 << 1) // PB1 输出低电平 → 绿灯亮
#define LED_GREEN_OFF() GPIOB_ODR &= ~(1 << 1) // PB1 输出高电平 → 绿灯灭
#define LED_BLUE_ON() GPIOC_ODR |= (1 << 14) // PC14 输出低电平 → 蓝灯亮
#define LED_BLUE_OFF() GPIOC_ODR &= ~(1 << 14) // PC14 输出高电平 → 蓝灯灭
#define LED_PC13_ON() GPIOC_ODR |= (1 << 13) // PC13 输出低电平 → 板载灯亮
#define LED_PC13_OFF() GPIOC_ODR &= ~(1 << 13) // PC13 输出高电平 → 板载灯灭
// ==================== 延时函数 ====================
// 简易软件延时,约1毫秒(基于72MHz主频)
void delay_ms(unsigned int time) {
unsigned int i, j;
for(i = 0; i < time; i++)
for(j = 0; j < 8000; j++); // 经验值,可根据实际微调
}
// ==================== 主函数 ====================
int main(void)
{
// 第一步:开启GPIOA、GPIOB、GPIOC的时钟
// APB2总线负责GPIOA/B/C等外设,需先使能时钟才能配置IO
RCC_APB2ENR |= (1 << 2) | (1 << 3) | (1 << 4); // 使能PA(2)、PB(3)、PC(4)时钟
// 第二步:配置PA0为推挽输出模式(控制红灯)
GPIOA_CRL &= ~(0xF << (4*0)); // 清除PA0的模式位(每4位控制一个引脚)
GPIOA_CRL |= (0x1 << (4*0)); // 设置为通用推挽输出,最大速度10MHz
// 第三步:配置PB1为推挽输出模式(控制绿灯)
GPIOB_CRL &= ~(0xF << (4*1)); // 清除PB1的模式位
GPIOB_CRL |= (0x1 << (4*1)); // 设置为推挽输出
// 第四步:配置PC13和PC14为推挽输出模式(控制蓝灯和板载灯)
// 注意:PC13和PC14属于高8位,使用CRH寄存器
GPIOC_CRH &= ~((0xF << (4*5)) | (0xF << (4*6))); // 清除PC13(Pin13=bit5)和PC14(Pin14=bit6)的配置
GPIOC_CRH |= ((0x1 << (4*5)) | (0x1 << (4*6))); // 设置为推挽输出模式
// 第五步:初始化所有LED为熄灭状态
LED_RED_OFF(); // PA0 = 1
LED_GREEN_OFF(); // PB1 = 1
LED_BLUE_OFF(); // PC14 = 1
LED_PC13_OFF(); // PC13 = 1
// 第六步:主循环 —— 实现4个LED轮流点亮(每次只亮一个)
while(1)
{
// 3. 点亮蓝灯(PC14),其他灯灭
LED_RED_OFF();
LED_GREEN_OFF();
LED_BLUE_ON(); // PC14 = 0 → 蓝灯亮
LED_PC13_OFF();
delay_ms(1000);
// 1. 点亮红灯(PA0),其他灯灭
LED_BLUE_OFF(); // PC14 = 1 → 蓝灯灭
LED_RED_ON(); // PA0 = 0 → 红灯亮
LED_GREEN_OFF(); // PB1 = 1 → 绿灯灭
LED_PC13_OFF(); // PC13 = 1 → 板载灯灭
delay_ms(1000); // 延时1000ms = 1秒
// 2. 点亮绿灯(PB1),其他灯灭
LED_RED_OFF(); // 红灯灭
LED_GREEN_ON(); // PB1 = 0 → 绿灯亮
LED_BLUE_OFF(); // 蓝灯灭
LED_PC13_OFF(); // 板载灯灭
delay_ms(1000);
// // 4. 点亮板载灯(PC13),其他灯灭
// LED_RED_OFF();
// LED_GREEN_OFF();
// LED_BLUE_OFF();
// LED_PC13_ON(); // PC13 = 0 → 板载灯亮
// delay_ms(1000);
// 循环回到第一步
}
}
✅ 代码解释:
volatile确保每次访问都是真实读写内存。- 每个GPIO的CRL/CRH寄存器中,每4位控制一个引脚。
- 延时函数中的
8000是经验值,在72MHz主频下大约对应1ms。
✅ 硬件连接:
- 将stlink与stm32连接。

- 使用跳线将stm32的
GND引脚引出到面包板负端。 - 外接LED正极(长脚)(红:PA0,绿:PB1,蓝:PC14)。
5. 程序烧录与验证
-
点击魔术棒按钮

-
更改设置
- Debug → 选择 ST-Link Debugger
- Flash Download → 勾选 “Reset and Run”

-
编译

-
0错误0警告


-
观察LED是否轮流闪烁!现象如下
流水灯
6.修改代码点亮PC13,现象如下
寄存器流水灯2
实验任务2:使用标准外设库方式控制LED流水灯
1. 创建工程并添加标准外设库文件

将固件库中inc``src中的文件全部复制到新建的Library文件夹下


右键Target1``Add Group新建组,并更改组名为Library
重复Start组的操作
然后复制下面路径中的文件到User中

在keil中将文件添加到User组中
打开stm32f10x.h文件夹,复制选中的字符串
复制在Debug的Define中
配置好User和Library文件路径
2. 用标准库重写LED控制程序
// main.c - 使用标准外设库实现4个LED轮流闪烁(每次只亮一个)
#include "stm32f10x.h" // 核心头文件
#include "stm32f10x_gpio.h" // GPIO库函数头文件
#include "stm32f10x_rcc.h" // RCC时钟控制头文件
// 延时函数声明
void Delay_ms(uint32_t nTime);
int main(void)
{
// 初始化系统时钟(默认72MHz,由SystemInit()完成)
SystemInit();
// 第一步:开启GPIOA、GPIOB、GPIOC的时钟
// 必须先使能时钟,才能配置对应GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
// 第二步:定义GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitStruct;
// 第三步:配置PA0为推挽输出(红灯)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; // 选择PA0
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStruct); // 应用配置
// 第四步:配置PB1为推挽输出(绿灯)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1; // 选择PB1
GPIO_Init(GPIOB, &GPIO_InitStruct); // 应用到GPIOB
// 第五步:配置PC13和PC14为推挽输出(蓝灯和板载灯)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14; // 同时配置PC13和PC14
GPIO_Init(GPIOC, &GPIO_InitStruct); // 应用到GPIOC
// 第六步:初始化所有LED为熄灭状态
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // PA0 = 0→ 红灯灭
GPIO_ResetBits(GPIOB, GPIO_Pin_1); // PB1 = 0 → 绿灯灭
GPIO_ResetBits(GPIOC, GPIO_Pin_14); // 蓝灯灯灭
GPIO_SetBits(GPIOC, GPIO_Pin_13); //板载灯灭
// 第七步:主循环 —— 实现4个LED轮流点亮(每次只亮一个)
while(1)
{
// 1. 点亮红灯(PA0),其他灯灭
GPIO_SetBits(GPIOA, GPIO_Pin_0); // PA0 = 1 → 红灯亮
GPIO_ResetBits(GPIOB, GPIO_Pin_1); // PB1 = 0 → 绿灯灭
GPIO_ResetBits(GPIOC, GPIO_Pin_14); // PC13/14 = 0 → 其他灯灭
Delay_ms(1000);
// 2. 点亮绿灯(PB1),其他灯灭
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 红灯灭
GPIO_SetBits(GPIOB, GPIO_Pin_1); // PB1 = 1 → 绿灯亮
GPIO_ResetBits(GPIOC, GPIO_Pin_14); // 其他灯灭
Delay_ms(1000);
// 3. 点亮蓝灯(PC14),其他灯灭
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
// GPIO_SetBits(GPIOC, GPIO_Pin_13); // PC13灭
GPIO_SetBits(GPIOC, GPIO_Pin_14); // PC14 = 0 → 蓝灯亮
Delay_ms(1000);
}
}
// 简易延时函数,nTime单位为毫秒
void Delay_ms(uint32_t nTime)
{
uint32_t i, j;
for(i = 0; i < nTime; i++)
for(j = 0; j < 8000; j++); // 72MHz主频下约1ms
}
3.烧录后运行结果:
标准库流水灯
4. 使用Keil仿真验证
- Debug → 选择 左上角
Use Simualator 




运行
仿真结果与实际结果相符
📚 总结与进阶建议
📌 两种开发方式的对比
| 对比项 | 寄存器方式 | 标准库方式 |
|---|---|---|
| 难不难写 | 难 ❌ 要懂寄存器、地址、位操作 |
简单 ✅ 调函数就行 |
| 好不好懂 | 难懂 ❌ 代码像“天书” |
好懂 ✅ 名字清楚,如 GPIO_Init |
| 会不会出错 | 容易错 ❌ 比如忘了开时钟 |
不易错 ✅ 结构化配置 |
| 换芯片能不能用 | 不能 ❌ 要重写 |
能用 ✅ 改几行就行 |
| 适合谁用 | 学习原理的人 📘 | 做项目的人 🛠 |
📌 一句话总结
- 寄存器方式:难写难懂,但能真正理解STM32是怎么工作的。适合学习。
- 标准库方式:简单好用,开发快、少出错。适合做项目。
💡 四、建议
- 初学者:先用寄存器写一遍,知道底层原理。
- 做项目:直接用标准库,省时间。
- 想更准:不要用
delay_ms()循环,改用定时器才能真正准到1秒。 - 下一步:学 HAL库(ST官方推荐),比标准库更现代、更方便。
🎉 你已经掌握了STM32控制LED的两种核心方法,很棒!
📌 如本文对你有帮助,欢迎点赞 ✅、收藏 ⭐、关注 💡,你的支持是我持续创作的最大动力!
💬 有任何问题或建议,欢迎在评论区留言,我会一一回复!
参考文献
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)