【STM32】Proteus仿真STM32教程(寄存器)1--手把手教你用寄存器点亮流水灯,彻底搞懂GPIO底层原理

💡写在前面

大家好!

新手学STM32,第一个实战案例几乎都是LED流水灯——但很多人一上来就用库函数敲代码,灯亮了却一脸懵:
GPIO_SetBits()到底做了什么?为什么配置GPIO要先开时钟?代码改个数字灯就不亮了?”

库函数把底层逻辑全封装了,看似简单,实则让你错过最核心的「寄存器操作」。今天我们抛开所有库函数,纯手工用寄存器编程实现LED流水灯,从“总电源开关”到“GPIO电平控制”,一步步拆解GPIO寄存器的底层应用,让你不仅能让灯“流起来”,更能明白“为什么能流起来”!

本文以最常用的STM32F103C8T6(核心板)为例,全程通俗易懂,新手跟着做就能上手,看完彻底搞懂GPIO寄存器的核心用法~

准备好了吗?坐稳扶好,我们要发车了!🏎️
请添加图片描述


一、为什么要学寄存器编程?🤔

很多小伙伴会问:“现在都有HAL库了,为什么还要学寄存器?”

三个理由说服你:

  1. 知其然,知其所以然 - 只有懂寄存器,才能真正理解STM32的工作原理
  2. 代码效率更高 - 直接操作寄存器,执行速度快,占用资源少
  3. 调试能力提升 - 遇到棘手问题时,寄存器级调试是终极武器

就像学车,自动挡简单,但懂手动挡才是真正的老司机!


二、前期准备

在开始之前,请确保你已经准备好:

  1. 提前安装目标 IDE(如 Keil MDK V5),方便后续验证生成的工程,本文以「Keil MDK V5」为例进行演示。(安装教程关注本文最下面的芦苇电子微信公众号,私信回复0008
  2. 提前安装仿真软件(如 proteus9.0),方便后续验证生成的工程,本文以「proteus9.0」为例进行演示。(安装教程关注本文最下面的芦苇电子公众号,私信回复 0005
  3. 便于理解本文内容建议提前了解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])功能
引脚01:03:2• 00:输入模式(默认)
• 01:输出10MHz
• 10:输出2MHz
• 11:输出50MHz
输入模式(MODE=00):
• 00=模拟输入
• 01=浮空输入
• 10=上拉/下拉输入
• 11=保留
输出模式(MODE≠00):
• 00=通用推挽
• 01=通用开漏
• 10=复用推挽
• 11=复用开漏
引脚15:47:6同引脚0同引脚0
引脚29:811:10同引脚0同引脚0
引脚313:1215:14同引脚0同引脚0
引脚417:1619:18同引脚0同引脚0
引脚521:2023:22同引脚0同引脚0
引脚625:2427:26同引脚0同引脚0
引脚729:2831: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 位运算:底层开发的“手术刀” 🔪

在写寄存器时,我们不能破坏其他位的状态,所以必须掌握位掩码操作:

  1. 置位 (Set Bit):使用 |=
    • GPIOB_ODR |= (1 << 0); // 将第 0 位置 1
  2. 清零 (Clear Bit):使用 &= ~
    • GPIOB_CRL &= ~(0xF << 0); // 将低 4 位清零
  3. 取反 (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)
{
	
}

代码关键解析(新手必看)

  1. 寄存器地址定义:用#define简化操作,避免重复写冗长的地址;
  2. 延时函数:空循环实现简单延时,time值越大,流水速度越慢(可自行调整);
  3. 主循环while(1)是死循环,保证流水灯一直运行;
  4. ODR赋值:采用先清后置(&=0 | |=)操作,不破坏其他位的状态,每次只让一个引脚为1(亮),其余为0(灭),延时后切换引脚。

六、 免费获取资料:

关注下面的 芦苇电子 微信公众号,

在公众号内 私信回复

028

收到后自动发送该仿真资料


如果你能坚持看到这里,说明你已经具备了成为嵌入式大神的潜质!💪 别光看,赶紧打开你的 Keil,新建一个工程,把这段代码跑起来试试吧!

下一篇讲应用位操作寄存器对寄存器版流水灯程序进行优化

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!——你的认可,是我持续输出嵌入式硬核干货的最大动力!我们下期再见! 🌟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芦苇电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值