💡写在前面
大家好!
新手学STM32,第一个实战案例几乎都是LED流水灯——但很多人一上来就用库函数敲代码,灯亮了却一脸懵:
“GPIO_SetBits()到底做了什么?为什么配置GPIO要先开时钟?代码改个数字灯就不亮了?”
库函数把底层逻辑全封装了,看似简单,实则让你错过最核心的「寄存器操作」。今天我们抛开所有库函数,纯手工用寄存器编程实现LED流水灯,从“总电源开关”到“GPIO电平控制”,一步步拆解GPIO寄存器的底层应用,让你不仅能让灯“流起来”,更能明白“为什么能流起来”!
本文以最常用的STM32F103C8T6(核心板)为例,全程通俗易懂,新手跟着做就能上手,看完彻底搞懂GPIO寄存器的核心用法~
准备好了吗?坐稳扶好,我们要发车了!🏎️

目录
一、为什么要学寄存器编程?🤔
很多小伙伴会问:“现在都有HAL库了,为什么还要学寄存器?”
三个理由说服你:
- 知其然,知其所以然 - 只有懂寄存器,才能真正理解STM32的工作原理
- 代码效率更高 - 直接操作寄存器,执行速度快,占用资源少
- 调试能力提升 - 遇到棘手问题时,寄存器级调试是终极武器
就像学车,自动挡简单,但懂手动挡才是真正的老司机!
二、前期准备
在开始之前,请确保你已经准备好:
- 提前安装目标 IDE(如 Keil MDK V5),方便后续验证生成的工程,本文以「Keil MDK V5」为例进行演示。(安装教程关注本文最下面的芦苇电子微信公众号,私信回复0008)
- 提前安装仿真软件(如 proteus9.0),方便后续验证生成的工程,本文以「proteus9.0」为例进行演示。(安装教程关注本文最下面的芦苇电子公众号,私信回复 0005)
- 便于理解本文内容建议提前了解STM32的GPIO口基础知识。(STM32F1系列GPIO口深度解析----关注本文最下面的芦苇电子公众号,私信回复 027 )
三、LED流水灯硬件准备 🛠️
我们设计8路LED流水灯,引脚选择PB0~PB7,电路原理如下:

- 点亮逻辑:GPIO引脚输出高电平(3.3V) → LED两端有3.3V电压差,点亮;GPIO输出低电平(0V) → 电压差为0,熄灭。
- 核心目标:通过操作GPIO寄存器,让PB0~PB7轮流输出高电平,循环往复,实现“流水灯”效果:
四、 寄存器深度分析 🔍
STM32的GPIO操作本质是“操作寄存器”——寄存器就是芯片内部的“开关面板”,每一位对应一个功能。流水灯只需要用到4个核心寄存器,我们逐个拆解:
4.1 第一步:开时钟!RCC_APB2ENR(时钟使能寄存器)
核心逻辑:STM32所有外设(包括GPIO)默认是“断电”的,必须先开启对应时钟,否则后续配置全无效(新手最容易踩的坑!)。
4.1.1 寄存器基础
- 寄存器:
RCC_APB2ENR - 地址:
0x40021000(AHB_RCC基地址)+0x18(RCC_APB2ENR寄存器偏移地址); - 作用:控制APB2总线上的外设时钟,GPIOB属于APB2外设;
- 关键位:第3位(GPIOB时钟使能位),置1=开启,置0=关闭。

4.1.2 操作逻辑(流水灯场景)
要操作GPIOB,必须把RCC_APB2ENR的第3位设为1,代码层面就是:
#define GPIOB_BASE (unsigned int)0x40010c00
#define RCC_APB2ENR *(unsigned int *)(AHB_RCC_BASE+0x18)
RCC_APB2ENR |= (1<<3);
💡 新手解惑:为什么用
|=?
|=是"按位或等于"。
它的作用是:只把第3位置1,而不改变其他位的状态。
如果你直接用=,可能会不小心把别人的时钟给关了!
4.2 第二步:配模式!GPIOB_CRL(GPIO配置寄存器低8位)
核心逻辑:GPIO引脚默认是输入模式,要控制LED必须配置为推挽输出模式(驱动能力强,适合LED)。
STM32F1的GPIO配置寄存器分两个:
- CRL (Config Register Low):管理低8位引脚 (Pin 0 ~ Pin 7)
- CRH (Config Register High):管理高8位引脚 (Pin 8 ~ Pin 15)
我们要配置PB0~PB7,所以操作 GPIOB_CRL 寄存器。
4.2.1 寄存器基础
- 寄存器:
GPIOB_CRL - 地址:
0x40010C00(GPIO_B口基地址)+0x00(GPIOX_CRL偏移地址); - 作用:配置GPIOB的低8位(Pin0~Pin7)的工作模式和输出速度;
- 关键规则:每个引脚占4位(2位CNF+2位MODE),格式如下:

| 引脚编号(y) | MODEy位位置 | CNFy位位置 | MODE位(MODEy[1:0])功能 | CNF位(CNFy[1:0])功能 |
|---|---|---|---|---|
| 引脚0 | 1:0 | 3:2 | • 00:输入模式(默认) • 01:输出10MHz • 10:输出2MHz • 11:输出50MHz | 输入模式(MODE=00): • 00=模拟输入 • 01=浮空输入 • 10=上拉/下拉输入 • 11=保留 输出模式(MODE≠00): • 00=通用推挽 • 01=通用开漏 • 10=复用推挽 • 11=复用开漏 |
| 引脚1 | 5:4 | 7:6 | 同引脚0 | 同引脚0 |
| 引脚2 | 9:8 | 11:10 | 同引脚0 | 同引脚0 |
| 引脚3 | 13:12 | 15:14 | 同引脚0 | 同引脚0 |
| 引脚4 | 17:16 | 19:18 | 同引脚0 | 同引脚0 |
| 引脚5 | 21:20 | 23:22 | 同引脚0 | 同引脚0 |
| 引脚6 | 25:24 | 27:26 | 同引脚0 | 同引脚0 |
| 引脚7 | 29:28 | 31:30 | 同引脚0 | 同引脚0 |
4.2.2 操作逻辑(流水灯场景)
我们需要把PB0~PB7都配置为“50MHz推挽输出”,对应每4位的取值都是0011(CNF=00,MODE=11):
Pin7 Pin6 Pin5 Pin4 Pin3 Pin2 Pin1 Pin0
0011 0011 0011 0011 0011 0011 0011 0011 (二进制)
3 3 3 3 3 3 3 3 (十六进制)
#define GPIOB_BASE (unsigned int)0x40010c00
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00)
GPIOB_CRL = 0x33333333; // 配置PB0~PA7为推挽输出(0011=3,8个3就是33333333)
⚠️ 注意: GPIO端口复位后的默认值是
0x44444444(浮空输入)。如果你不是一次性配置所有引脚,建议先清零再赋值,防止配置混乱。
例如:
/* 详细展开的写法:*/
GPIOB_CRL &= ~(0x0f<<(4*0)); //PB0配置清零
GPIOB_CRL |= (0x3 << (4*0)); // PB0
GPIOB_CRL &= ~(0x0f<<(4*1)); //PB1配置清零
GPIOB_CRL |= (0x3 << (4*1)); // PB1
4.3 第三步:控电平!GPIOB_ODR(输出数据寄存器)
核心逻辑:配置好模式后,通过ODR寄存器直接控制引脚电平——位值=0→低电平(LED灭),位值=1→高电平(LED亮)。
4.3.1 寄存器基础
- 寄存器:
GPIOB_ODR - 地址:
0x40010C00(GPIO_B口基地址)+0x0C(GPIOX_ODR偏移地址) - 作用:低16位对应GPIOB的16个引脚,每一位控制一个引脚的输出电平;
- 关键规则:读/写均可,写0=低电平,写1=高电平。

4.3.2 操作逻辑(流水灯场景)
#define GPIOB_BASE (unsigned int)0x40010c00
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
// 点亮PC0上的LED
GPIOB_ODR |= (1 << 0);
// 熄灭PC0上的LED
GPIOB_ODR &= ~(1 << 0);
4. 4 位运算:底层开发的“手术刀” 🔪
在写寄存器时,我们不能破坏其他位的状态,所以必须掌握位掩码操作:
- 置位 (Set Bit):使用
|=GPIOB_ODR |= (1 << 0);// 将第 0 位置 1
- 清零 (Clear Bit):使用
&= ~GPIOB_CRL &= ~(0xF << 0);// 将低 4 位清零
- 取反 (Toggle):使用
^=GPIOB_ODR ^= (1 << 0);// 翻转 PB0 电平
五、完整代码实现 💻
#define GPIOB_BASE (unsigned int)0x40010c00
#define AHB_RCC_BASE (unsigned int)0x40021000
#define RCC_CR *(unsigned int *)(AHB_RCC_BASE+0x00)
#define RCC_CFGR *(unsigned int *)(AHB_RCC_BASE+0x04)
#define RCC_CIR *(unsigned int *)(AHB_RCC_BASE+0x08)
#define RCC_APB2RSTR *(unsigned int *)(AHB_RCC_BASE+0x0C)
#define RCC_APB1RSTR *(unsigned int *)(AHB_RCC_BASE+0x10)
#define RCC_AHBENR *(unsigned int *)(AHB_RCC_BASE+0x14)
#define RCC_APB2ENR *(unsigned int *)(AHB_RCC_BASE+0x18)
#define RCC_APB1ENR *(unsigned int *)(AHB_RCC_BASE+0x1C)
#define RCC_BDCR *(unsigned int *)(AHB_RCC_BASE+0x20)
#define RCC_CSR *(unsigned int *)(AHB_RCC_BASE+0x24)
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR *(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR *(unsigned int*)(GPIOB_BASE+0x18)
// 延时函数(空循环,新手不用深究,改数值可调整流水速度)
void Delay(unsigned int time)
{
unsigned int i,j;
for(i=0; i<time; i++)
for(j=0; j<1000; j++);
}
int main(void)
{
//开启GPIOB口时钟
RCC_APB2ENR |= (1<<3);
//配置推挽输出 50mhz
GPIOB_CRL = 0x33333333; // 配置PB0~PA7为推挽输出(0011=3,8个3就是33333333)
/* 详细展开的写法:
GPIOB_CRL &= 0x00000000; // 先清零
GPIOB_CRL |= (0x3 << (0*4)); // PB0
GPIOB_CRL |= (0x3 << (1*4)); // PB1
GPIOB_CRL |= (0x3 << (2*4)); // PB2
GPIOB_CRL |= (0x3 << (3*4)); // PB3
GPIOB_CRL |= (0x3 << (4*4)); // PB4
GPIOB_CRL |= (0x3 << (5*4)); // PB5
GPIOB_CRL |= (0x3 << (6*4)); // PB6
GPIOB_CRL |= (0x3 << (7*4)); // PB7
*/
// led off
GPIOB_ODR &= ~(0xFF);
while(1)
{
GPIOB_ODR |= (1<<0); // led oN
GPIOB_ODR &= ~(1<<7); // led oFF
Delay(200);
GPIOB_ODR |= (1<<1); // led oN
GPIOB_ODR &= ~(1<<0); // led oFF
Delay(200);
GPIOB_ODR |= (1<<2); // led oN
GPIOB_ODR &= ~(1<<1); // led oFF
Delay(200);
GPIOB_ODR |= (1<<3); // led oN
GPIOB_ODR &= ~(1<<2); // led oFF
Delay(200);
GPIOB_ODR |= (1<<4); // led oN
GPIOB_ODR &= ~(1<<3); // led oFF
Delay(200);
GPIOB_ODR |= (1<<5); // led oN
GPIOB_ODR &= ~(1<<4); // led oFF
Delay(200);
GPIOB_ODR |= (1<<6); // led oN
GPIOB_ODR &= ~(1<<5); // led oFF
Delay(200);
GPIOB_ODR |= (1<<7); // led oN
GPIOB_ODR &= ~(1<<6); // led oFF
Delay(200);
}
}
void SystemInit(void)
{
}
代码关键解析(新手必看)
- 寄存器地址定义:用
#define简化操作,避免重复写冗长的地址; - 延时函数:空循环实现简单延时,
time值越大,流水速度越慢(可自行调整); - 主循环:
while(1)是死循环,保证流水灯一直运行; - ODR赋值:采用先清后置(&=0 | |=)操作,不破坏其他位的状态,每次只让一个引脚为1(亮),其余为0(灭),延时后切换引脚。
六、 免费获取资料:
关注下面的 芦苇电子 微信公众号,
在公众号内 私信回复
028
收到后自动发送该仿真资料
如果你能坚持看到这里,说明你已经具备了成为嵌入式大神的潜质!💪 别光看,赶紧打开你的 Keil,新建一个工程,把这段代码跑起来试试吧!
下一篇讲应用位操作寄存器对寄存器版流水灯程序进行优化
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!——你的认可,是我持续输出嵌入式硬核干货的最大动力!我们下期再见! 🌟
6353

被折叠的 条评论
为什么被折叠?



