前言

很多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

判断逻辑

  1. 需要读取外部电压、检测按键 → 使用IDR(只读,不能写)
  2. 普通方式控制输出电平 → 使用ODR
  3. 拉高引脚 → BSRR,向对应bit写1
  4. 拉低引脚 → BRR,向对应bit写1

BSRR、BRR为只写寄存器,读取返回值无意义。

常见错误

  1. BSRR写0不能拉低引脚,拉低引脚要使用BRR写1
  2. 读取按键电平不要读ODR,外部真实电平看IDR
  3. 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点亮条件:阳极电压 > 阴极电压,形成电流

  1. PC13输出低电平(0V),对应寄存器bit写0
    阳极3.3V,阴极0V,存在电压差。
    电流路径:VCC3V3 →LED →R5 →PC13。
    有电流,LED点亮。

  2. 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 + 0x04 PC8~PC15模式配置寄存器
  • GPIOC_ODR:0x40011000 + 0x0C IO输出电平寄存器
  • 寄存器访问模板:*((volatile unsigned int *)(基地址 + 偏移))
1.开启GPIOC时钟
    //1.开启GPIOC时钟
    *((volatile unsigned int*)(0x40021000 + 0x18)) |= 1 << 4;
  1. 0x40021000 + 0x18:RCC_APB2ENR寄存器硬件物理地址,STM32寄存器地址固定写死。
  2. (volatile unsigned int *):把数字地址强制转为无符号int类型指针。
    • volatile关键字:告诉编译器这是硬件寄存器,禁止编译器做优化,每次读写都直接访问硬件,去掉会出现莫名其妙bug。
  3. 最前面的*:指针解引用,代表读写这个地址对应的硬件寄存器。
  4. 1 << 4:数字1二进制0001,向左移动4位,得到0001 0000bit4等于1,其余bit全部为0

小技巧:借助电脑的计算器:Win+R → 输入 calc → 回车
这个代表偏移
在这里插入图片描述

2.清空PC13旧配置
    //2.清空PC13旧配置
    *((volatile unsigned int*)(0x40011000 + 0x04)) &= ~(0xf << 20);
  1. 0x40011000 + 0x04:GPIOC_CRH寄存器地址,PC8~PC15引脚全部由CRH寄存器配置。

    手册:PC13占用CRH的bit20、bit21、bit22、bit23,一个GPIO引脚占用4bit配置模式

  2. 0xf:十六进制,二进制1111,4个连续的1,正好对应PC13的4个配置位。
  3. 0xf << 20:将1111整体向左移动20位,对齐bit20~bit23。
  4. ~(0xf << 20)~按位取反运算符,bit20‑23变成0000,寄存器剩下所有bit全部变成1。
  5. &=:按位与赋值,等价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;
  1. 3二进制=0011
  2. 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;
}
  1. 0x40011000 + 0x0C:GPIOC_ODR输出寄存器地址。ODR每1bit对应一个IO引脚输出电平。bit13对应PC13。
    • bit13=1:引脚输出高电平3.3V
    • bit13=0:引脚输出低电平0V
  2. 1 <<13:bit13位置置1,其余bit为0。
  3. |=:把ODR寄存器bit13写1。
5.led_on() 点亮LED函数
//点亮LED
void led_on(void)
{
    *((volatile unsigned int*)(0x40011000 + 0x0C)) &= ~(1 << 13);
}
  1. 操作ODR输出寄存器。
  2. 1 <<13:bit13=1。
  3. ~(1<<13):按位取反,bit13变为0,其他所有bit全部为1。
  4. &=:只把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;

读者一定要自己写出完整代码检验一下

总结

  1. 开启外设时钟永远是第一步,不开时钟GPIO完全无效。
  2. CRH寄存器:每个引脚占4bit,设置引脚工作模式
    • 清空旧值:&= ~(0xf << n)
    • 设置新模式:|= 数值 << n
  3. ODR寄存器:1bit控制1个引脚输出电平
    • 输出高电平(置1):|= 1 << n
    • 输出低电平(置0):&= ~(1 << n)
  4. volatile关键字操作硬件寄存器不能省略。
  5. 灯亮灭不只看寄存器0/1,必须看原理图;PC13电路是写0亮,写1灭。

如果这篇 STM32 寄存器点灯对你有帮助,欢迎点赞、收藏、关注支持一下。
后续我会继续更新寄存器系列:时钟配置、GPIO 原理、外部中断、定时器、串口通信等,全部基于寄存器实现。
关注我,更新后第一时间收到,不错过下一篇。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐