stm32寄存器开发,根据点灯了解寄存器底层逻辑
前言
很多STM32新手直接学HAL库、标准库,只会调用函数,完全不懂底层原理,一旦程序异常、调试报错,完全无从下手,本文从零开始,以点灯为例,让你搞懂寄存器开发
实战硬件:STM32F103C8T6
一、配置寄存器(学会看官方参考手册)
1.1 两个手册的区别
- 数据手册:引脚、电气参数
- 参考手册(重点):所有寄存器地址、功能(写代码只看这个)
1.2 GPIO地址公式
寄存器真实地址 = 外设基地址 + 偏移地址
1.3 怎样看外设基地址
打开《STM32F103 参考手册》,搜索存储器映像,会看到一张外设地址表。
这张表里列出所有外设的基地址:就是这个外设的起始首地址。
为了方便阅读,我举GPIO的例子,把它各个端口基地址的图片贴在这

现在我们知道了寄存器的外设基地址,那么偏移地址在哪呢?
1.4 怎样看偏移地址

搜索GPIO寄存器描述,这里包括的所有寄存器都有一个名为地址偏移(Offset)的地址,这个就是偏移地址。
下面给出总结,这里有一些专有名词,现在不懂没关系,你只需要知道如何使用,我将通过一个练习来说明
| 寄存器 | 偏移Offset | 寄存器作用 | 详细说明 |
|---|---|---|---|
| CRL | 0x00 |
0‑7号引脚模式配置寄存器 | 专门配置 Px0~Px7(0到7号引脚)的工作模式。每个引脚占用4个bit:CNF(2位)设置功能模式,MODE(2位)设置输出速度。可以配置成推挽输出、开漏输出、输入、复用功能。修改某一个引脚时,要先把对应的4bit清零,避免旧配置残留。 |
| CRH | 0x04 |
8‑15号引脚模式配置寄存器 | 专门配置 Px8~Px15(8到15号引脚)。内部结构、位定义和CRL完全一样。只负责高8个引脚,不要用来操作0‑7引脚。 |
| IDR | 0x08 |
输入数据寄存器 | 只读寄存器,不能写。用来读取芯片引脚外部真实电平。某bit为1代表引脚外部是高电平;bit为0代表引脚外部是低电平。做按键输入、外部信号检测时读取这个寄存器。对IDR做写操作没有任何效果。 |
| ODR | 0x0C |
输出数据寄存器 | 可读、可写。用来设置GPIO引脚输出电平。bit位置1,引脚输出高电平;bit位置0,引脚输出低电平。 |
| BSRR | 0x10 |
置位寄存器(置高) | 只写寄存器,读出的数据无意义。对某一位写1,对应GPIO引脚输出高电平;写0不起任何作用。不会影响其他引脚的状态,不需要提前清零,中断中使用很安全。 |
| BRR | 0x14 |
复位寄存器(置低) | 只写寄存器,读出的数据无意义。对某一位写1,对应GPIO引脚输出低电平;写0不起任何作用。不会影响其余引脚,工程上优先推荐BSRR+BRR操作输出。 |
1.5 详细步骤总结
选CRL还是CRH(配置引脚工作模式)
- CRL、CRH专门用来配置引脚模式(输入、输出、速度)。
- 引脚编号 0 ~ 7(Px0~Px7) → 使用 CRL(偏移0x00)
- 引脚编号 8 ~ 15(Px8~Px15) → 使用 CRH(偏移0x04)
根据操作选择电平读写寄存器
| 想要实现的功能 | 寄存器 | 偏移 |
|---|---|---|
| 读取外部引脚电平 | IDR | 0x08 |
| 普通方式读写输出电平 | ODR | 0x0C |
| 设置引脚输出高电平 | BSRR | 0x10 |
| 设置引脚输出低电平 | BRR | 0x14 |
判断逻辑
- 需要读取外部电压、检测按键 → 使用IDR(只读,不能写)
- 普通方式控制输出电平 → 使用ODR
- 拉高引脚 → BSRR,向对应bit写1
- 拉低引脚 → BRR,向对应bit写1
BSRR、BRR为只写寄存器,读取返回值无意义。
常见错误
- BSRR写0不能拉低引脚,拉低引脚要使用BRR写1
- 读取按键电平不要读ODR,外部真实电平看IDR
- CRL、CRH只设置模式,不能控制引脚输出高低电平
口诀:0‑7用CRL,8‑15找CRH;读按键用IDR;ODR普通输出;BSRR拉高,BRR拉低。
二、使用APB2 外设时钟使能寄存器
2.1 什么是APB2总线
STM32内部外设挂载在不同总线上:
- APB2总线:挂载GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、USART1、SPI1等高速外设
- APB1总线:挂载USART2、TIM2‑TIM4、I2C等外设,由
RCC_APB1ENR控制时钟
GPIO端口全部挂载在APB2总线上,因此操作GPIO,就要操作RCC_APB2ENR。
2.2 为什么必须开启时钟
STM32低功耗机制:上电之后所有外设时钟默认关闭。
时钟相当于外设的工作动力,没有时钟,外设内部电路不工作。
即使你给CRL、CRH、ODR寄存器写入配置,硬件也不会生效,引脚没有任何反应。
开启时钟本质:向RCC_APB2ENR对应bit写1,给对应外设提供时钟信号,外设才能正常工作。
2.3 RCC_APB2ENR关键bit位
写1开启时钟,写0关闭时钟

也就是
| bit位 | 控制外设 |
|---|---|
| bit2 | GPIOA时钟 |
| bit3 | GPIOB时钟 |
| bit4 | GPIOC时钟 |
| bit5 | GPIOD时钟 |
| bit6 | GPIOE时钟 |
三、练习:点亮LED灯
3.1 LED原理图分析

LED点亮条件:阳极电压 > 阴极电压,形成电流。
-
PC13输出低电平(0V),对应寄存器bit写0
阳极3.3V,阴极0V,存在电压差。
电流路径:VCC3V3 →LED →R5 →PC13。
有电流,LED点亮。 -
PC13输出高电平(3.3V),对应寄存器bit写1
阳极3.3V,阴极3.3V,两端电压几乎相等,没有电流。
LED熄灭。
R5(510Ω)是限流电阻,保护LED,防止电流过大烧坏灯珠。
上方D1:阴极直接通过电阻接GND,上电就常亮,不受单片机控制。
3.2 完整代码及分析
led.h
#ifndef __LED_H
#define __LED_H
void led_init(void);
void led_on(void);
void led_off(void);
#endif
main.c
#include"led.h"
int main (void)
{
led_init();
while(1)
{
led_on();
led_off();
}
}
led.c
#include "led.h"
//PC13 板载LED:高电平灭、低电平亮
void led_init(void)
{
//1.开启GPIOC时钟
*((volatile unsigned int*)(0x40021000 + 0x18)) |= 1 << 4;
//2.清空PC13旧配置
*((volatile unsigned int*)(0x40011000 + 0x04)) &= ~(0xf << 20);
//3.配置PC13 推挽输出 50MHZ 00 11
*((volatile unsigned int*)(0x40011000 + 0x04)) |= 3 << 20;
//4.默认熄灭
*((volatile unsigned int*)(0x40011000 + 0x0C)) |= 1 << 13;
}
//点亮LED
void led_on(void)
{
*((volatile unsigned int*)(0x40011000 + 0x0C)) &= ~(1 << 13);
}
//熄灭LED
void led_off(void)
{
*((volatile unsigned int*)(0x40011000 + 0x0C)) |= 1 << 13;
}
led.c代码解析
硬件背景:板载LED接PC13;PC13输出低电平(0)灯亮,输出高电平(1)灯灭
寄存器地址说明
- RCC_APB2ENR:
0x40021000 + 0x18时钟控制寄存器- GPIOC_CRH:
0x40011000 + 0x04PC8~PC15模式配置寄存器- GPIOC_ODR:
0x40011000 + 0x0CIO输出电平寄存器- 寄存器访问模板:
*((volatile unsigned int *)(基地址 + 偏移))
1.开启GPIOC时钟
//1.开启GPIOC时钟
*((volatile unsigned int*)(0x40021000 + 0x18)) |= 1 << 4;
0x40021000 + 0x18:RCC_APB2ENR寄存器硬件物理地址,STM32寄存器地址固定写死。(volatile unsigned int *):把数字地址强制转为无符号int类型指针。volatile关键字:告诉编译器这是硬件寄存器,禁止编译器做优化,每次读写都直接访问硬件,去掉会出现莫名其妙bug。
- 最前面的
*:指针解引用,代表读写这个地址对应的硬件寄存器。 1 << 4:数字1二进制0001,向左移动4位,得到0001 0000,bit4等于1,其余bit全部为0。
小技巧:借助电脑的计算器:Win+R → 输入 calc → 回车
这个代表偏移
2.清空PC13旧配置
//2.清空PC13旧配置
*((volatile unsigned int*)(0x40011000 + 0x04)) &= ~(0xf << 20);
0x40011000 + 0x04:GPIOC_CRH寄存器地址,PC8~PC15引脚全部由CRH寄存器配置。手册:PC13占用CRH的bit20、bit21、bit22、bit23,一个GPIO引脚占用4bit配置模式。
0xf:十六进制,二进制1111,4个连续的1,正好对应PC13的4个配置位。0xf << 20:将1111整体向左移动20位,对齐bit20~bit23。~(0xf << 20):~按位取反运算符,bit20‑23变成0000,寄存器剩下所有bit全部变成1。&=:按位与赋值,等价a = a & b。运算规则:有0则0,全1才1。- 只把bit20‑bit23强制清零,寄存器其他bit完全不变。
为什么要清零:单片机上电寄存器是随机乱值,不清零旧配置,新引脚模式会出错。
3.配置PC13推挽输出 50MHZ
//3.配置PC13 推挽输出 50MHZ 00 11
*((volatile unsigned int*)(0x40011000 + 0x04)) |= 3 << 20;
3二进制=0011。3 << 20:把二进制0011移动到bit20、bit21。
CRH每个引脚4bit格式:
CNF1 CNF0 MODE1 MODE0
PC13:bit23 bit22 bit21 bit20 =00 11
- MODE[1:0] =
11:输出模式,最大速度50MHz - CNF[1:0] =
00:通用推挽输出,引脚既可以输出0V,也可以输出3.3V。
4.初始化默认熄灭LED
//4.默认熄灭
*((volatile unsigned int*)(0x40011000 + 0x0C)) |= 1 << 13;
}
0x40011000 + 0x0C:GPIOC_ODR输出寄存器地址。ODR每1bit对应一个IO引脚输出电平。bit13对应PC13。- bit13=1:引脚输出高电平3.3V
- bit13=0:引脚输出低电平0V
1 <<13:bit13位置置1,其余bit为0。|=:把ODR寄存器bit13写1。
5.led_on() 点亮LED函数
//点亮LED
void led_on(void)
{
*((volatile unsigned int*)(0x40011000 + 0x0C)) &= ~(1 << 13);
}
- 操作ODR输出寄存器。
1 <<13:bit13=1。~(1<<13):按位取反,bit13变为0,其他所有bit全部为1。&=:只把bit13写0,其余bit保持不变。
ODR.bit13 =0 →PC13输出0V低电平,LED点亮。
6.led_off() 熄灭LED函数
//熄灭LED
void led_off(void)
{
*((volatile unsigned int*)(0x40011000 + 0x0C)) |= 1 << 13;
}
3.3 使用BSRR/BRR寄存器
不需要清除位,不会出错,推荐工程使用:
//亮灯
*((volatile unsigned int*)(0x40011000+0x14)) |= 1<<13;
//灭灯
*((volatile unsigned int*)(0x40011000+0x10)) |= 1<<13;
读者一定要自己写出完整代码检验一下
总结
- 开启外设时钟永远是第一步,不开时钟GPIO完全无效。
- CRH寄存器:每个引脚占4bit,设置引脚工作模式
- 清空旧值:
&= ~(0xf << n) - 设置新模式:
|= 数值 << n
- 清空旧值:
- ODR寄存器:1bit控制1个引脚输出电平
- 输出高电平(置1):
|= 1 << n - 输出低电平(置0):
&= ~(1 << n)
- 输出高电平(置1):
volatile关键字操作硬件寄存器不能省略。- 灯亮灭不只看寄存器0/1,必须看原理图;PC13电路是写0亮,写1灭。
如果这篇 STM32 寄存器点灯对你有帮助,欢迎点赞、收藏、关注支持一下。
后续我会继续更新寄存器系列:时钟配置、GPIO 原理、外部中断、定时器、串口通信等,全部基于寄存器实现。
关注我,更新后第一时间收到,不错过下一篇。
更多推荐


所有评论(0)