13
0
0

【嵌入式硬件】4.0 STM32F103 HAL库开发实战——寄存器

2026-10-26
2026-10-26
【嵌入式硬件】4.0 STM32F103 HAL库开发实战——寄存器

什么是寄存器

寄存器(Register)是 CPU 内部的存储单元,位于处理器芯片里,是整个存储层级中速度最快、容量最小的一级。

维度 说明
位置 CPU 芯片内部,不是内存
速度 通常 1 个时钟周期即可访问,比内存快几百倍
容量 极小,一般几十个,每个 32 位或 64 位
用途 CPU 做运算时,操作数必须先装入寄存器

这里的"个"指寄存器的数量——一个寄存器就是一个独立的存储格子,有自己的名字(比如 RAX、RBX)。

说法 含义
“几十个” 寄存器的数量,即 CPU 里有多少个这样的格子
“32 位 / 64 位” 每个格子的宽度,即一个格子能装多少位二进制数据

x86-64 架构有 16 个通用寄存器,名字分别是:

RAX RBX RCX RDX RSI RDI RBP RSP
R8  R9  R10 R11 R12 R13 R14 R15

每个 64 位 = 8 字节,所以通用寄存器总共只有 16 × 8 = 128 字节。加上向量寄存器(16 个 ZMM,每个 512 位 = 64 字节,共 2048 字节)和各类控制寄存器,整个寄存器组也就几 KB。

存储层级从快到慢:寄存器 → L1 缓存 → L2 → L3 → 内存 → 磁盘。

为什么需要它

CPU 的 ALU(算术逻辑单元)不能直接对内存里的数据做加减法。一次 a = b + c 实际是这样:

mov  rax, [b]    ; 把 b 从内存载入寄存器 rax
add  rax, [c]    ; rax = rax + c
mov  [a], rax    ; 把结果写回内存 a

寄存器就是 ALU 的"工作台",数据必须先搬上台面才能加工。

  • 通用寄存器:存放临时数据和地址。x86-64 有 RAX、RBX、RCX、RDX、RSI、RDI、R8-R15;ARM64 有 X0-X30

  • 栈指针 / 帧指针:RSP 指向栈顶,RBP 指向当前函数栈帧

  • 指令指针:RIP(x86)/ PC(ARM),存放下一条要执行的指令地址

  • 标志寄存器:FLAGS,记录上次运算的进位、零、溢出等状态,if 判断就靠它

  • 向量寄存器:XMM/YMM/ZMM(SSE/AVX),用于 SIMD 并行计算

  • 控制寄存器:CR0-CR4 等,操作系统用来配置分页、保护模式、

核心概念

概念 一句话定义
存储器映射 给存储单元分配地址(存储器本身没有地址)
存储器重映射 给同一存储器再分配一个地址
寄存器 已分配地址、有特定功能的内存单元的别名
寄存器映射 给这些内存单元起别名的过程

寄存器本质就是一个固定地址的 32 位内存单元。读写这个地址 = 驱动外设。

F103-指南者开发板

指南者开发板中使用的芯片是100pin(100个引脚)的STM32F103VET6。
ARM应该是表示该芯片使用的是ARM的内核,STM32F103VET6是芯片型号,后面的字应该是跟生产批次相关,最下面的是ST的LOGO。

image1

芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。

开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作。
image2

芯片里面有什么

STM32F103 的内核是 Cortex-M3,由 ARM 设计。ARM 只出售内核技术授权,不生产芯片;ST、TI、NXP 这些厂商在内核之外设计部件并生产整颗芯片,这些内核之外的部件叫片上外设,如 GPIO、USART、I2C、SPI。

image3

内核和外设之间通过总线连接。总线上有 4 个驱动单元(可理解为 CPU 侧,发起访问)和 4 个被动单元(外设侧,被访问)。
image4
我们可以把驱动单元理解成是CPU部分,被动单元都理解成外设。下面我们简单介绍下驱动单元和被动单元的各个部件。

四个驱动单元(四条总线)

总线 全称 干什么 访问目标
ICode Instruction Code 取指令,几乎时刻在用 内部 FLASH
DCode Data Code 取数据 FLASH 里的常量、SRAM 里的变量
系统总线 System 读写外设寄存器,寄存器编程走这条 片上外设
DMA Direct Memory Access 搬数据,不占内核 外设数据寄存器 / SRAM / FLASH

数据存放位置:

  • const 修饰的常量 → 内部 FLASH
  • 全局变量、局部变量 → 内部 SRAM

DCode 和 DMA 都能访问数据,可能同时取数,由总线矩阵仲裁决定谁先走。

四个被动单元

单元 说明
内部 FLASH 存放程序,内核经 ICode 取指
内部 SRAM 变量、堆栈开销都在这里,内核经 DCode 访问
FSMC 灵活的静态存储器控制器,可外扩 SRAM / NAND FLASH / NOR FLASH。注意 S = Static,SDRAM 这类动态内存扩不了
AHB-APB 桥 从 AHB 延伸出 APB1、APB2,GPIO / 串口 / I2C / SPI 等外设都挂在上面

二、存储器映射

上一节的四个被动单元——FLASH、RAM、FSMC、AHB-APB 桥(即片上外设)——共同排列在同一个 4GB 的地址空间内。编程时就是通过地址找到它们,再用 C 语言对它们读写。

存储器本身不具有地址信息,地址是由芯片厂商或用户分配的。给存储器分配地址的过程就叫存储器映射;如果给同一存储器再分配一个地址,叫存储器重映射。

image5

区域功能划分:8 个 Block

ARM 把这 4GB 空间粗线条地平均分成 8 块,每块 512MB,每块都规定了用途。512MB 很大,芯片厂商在每块范围内设计外设时并不一定用得完,通常只用其中一部分。

Block 用途 地址范围
0 Code(内部 FLASH) 0x0000 0000 ~ 0x1FFF FFFF
1 SRAM 0x2000 0000 ~ 0x3FFF FFFF
2 片上外设 0x4000 0000 ~ 0x5FFF FFFF
3 FSMC bank1~2 0x6000 0000 ~ 0x7FFF FFFF
4 FSMC bank3~4 0x8000 0000 ~ 0x9FFF FFFF
5 FSMC 寄存器 0xA000 0000 ~ 0xBFFF FFFF
6 未使用 0xC000 0000 ~ 0xDFFF FFFF
7 Cortex-M3 内部外设 0xE000 0000 ~ 0xFFFF FFFF

常用三块:Block0 = FLASH(程序)、Block1 = SRAM(变量)、Block2 = 外设(寄存器)。

Block0 关键区段

地址范围 说明
0x0800 0000 ~ 0x0807 FFFF FLASH,程序就放这里(最大 512KB)
0x1FFF F000 ~ 0x1FFF F7FF 系统存储器,出厂 Bootloader,串口下载用
0x1FFF F800 ~ 0x1FFF F80F 选项字节,读写保护 / 看门狗 / 复位配置
0x0000 0000 ~ 0x0007 FFFF 由 BOOT 引脚决定的别名区

Block1 关键区段

0x2000 0000 ~ 0x2000 FFFF 为 SRAM(64KB),其余预留。

三、Block2 三条总线基地址

低速外设挂 APB1,高速挂 APB2 和 AHB。相应总线的最低地址称为该总线的基地址,也是挂在这条总线上首个外设的地址。APB1 的地址最低,片上外设从这里开始,所以 APB1 基地址也叫外设基地址。

总线 基地址 相对外设基地址偏移
APB1 0x4000 0000 0x0(也是「片上外设基地址」)
APB2 0x4001 0000 0x0001 0000
AHB 0x4001 8000 0x0001 8000

AHB 上地址最低的外设是 SDIO(0x4001 8000),所以按「总线基地址 = 该总线首个外设的地址」算,AHB 基地址是这个值。但官方库的 AHBPERIPH_BASE 宏取的是 0x4002 0000,即 DMA1 的位置,往后依次是 DMA2、RCC、FLASH 接口、CRC;SDIO 不在这一段,官方库单独给它定地址:

#define SDIO_BASE        (PERIPH_BASE + 0x18000)     /* 0x4001 8000 */  
#define AHBPERIPH_BASE   (PERIPH_BASE + 0x20000)     /* 0x4002 0000 */  
#define DMA1_BASE        (AHBPERIPH_BASE + 0x0000)  

写代码用 AHBPERIPH_BASE(0x4002 0000),访问 SDIO 用 SDIO_BASE。

偏移列是干什么的:它是该总线基地址与外设基地址 0x4000 0000 的差值,用途只有一个——写进宏定义里,让所有地址都从同一个基准推算出来,而不是每条总线各写一个绝对地址。

#define PERIPH_BASE         ((unsigned int)0x40000000)   /* 唯一的硬编码值 */  
#define APB1PERIPH_BASE     PERIPH_BASE                  /* 偏移 0 */#define APB2PERIPH_BASE     (PERIPH_BASE + 0x00010000)   /* 用的就是偏移列 */

这样做的好处是地址的层级关系直接写在代码里:看到 GPIOB_BASE (APB2PERIPH_BASE + 0x0C00) 就知道 GPIOB 挂在 APB2 上、在该总线内偏移 0xC00。

四、寄存器映射与地址三级定位

Block2 这块区域设计的是片上外设,它们以四个字节为一个单元,共 32bit,每个单元对应不同功能,控制这些单元就能驱动外设工作。

可以找到每个单元的起始地址,用 C 语言指针去访问。但每次都用地址访问不好记也容易出错,于是按功能给这个内存单元取个别名——这个别名就是寄存器,而这个「给已分配好地址、有特定功能的内存单元取别名」的过程就叫寄存器映射。

地址按三级累加得到:

寄存器地址 = 总线基地址 + 外设偏移 + 寄存器偏移  

总线上挂载各种外设,每个外设有自己的地址范围,特定外设的首个地址称为「XX 外设基地址」,也叫该外设的边界地址。

以 GPIO(挂 APB2)为例,偏移列同样是为了写宏——GPIOB_BASE (APB2PERIPH_BASE + 0x0C00) 里的 0x0C00 就取自这里:

外设 基地址 相对 APB2 偏移
GPIOA 0x4001 0800 0x800
GPIOB 0x4001 0C00 0xC00
GPIOC 0x4001 1000 0x1000
GPIOD 0x4001 1400 0x1400
GPIOE 0x4001 1800 0x1800
GPIOF 0x4001 1C00 0x1C00
GPIOG 0x4001 2000 0x2000

在某外设的地址范围内,分布着的就是该外设的寄存器。每个寄存器 32bit、占 4 字节,在外设基地址上按顺序排列,位置以相对外设基地址的偏移来描述。这一级偏移有两个用处:写宏定义(GPIOB_ODR (GPIOB_BASE + 0x0C)),以及按同样顺序排列结构体成员

寄存器 偏移 GPIOB 实际地址
CRL 0x00 0x4001 0C00
CRH 0x04 0x4001 0C04
IDR 0x08 0x4001 0C08
ODR 0x0C 0x4001 0C0C
BSRR 0x10 0x4001 0C10
BRR 0x14 0x4001 0C14
LCKR 0x18 0x4001 0C18

image6

五、C 语言对寄存器的封装

第一步:裸地址。编译器把 0x40010C0C 当普通立即数,必须强转成指针才能解引用。

/* GPIOB 全部 IO 输出高电平 */
*(unsigned int *)(0x40010C0C) = 0xFFFF;  

第二步:抽出基地址 + 偏移,起别名。

#define GPIOB_ODR    (unsigned int *)(GPIOB_BASE + 0x0C)  
*GPIOB_ODR = 0xFFFF;  

第三步:把 * 一并包进宏,像普通变量一样赋值。

#define GPIOB_ODR    *(unsigned int *)(GPIOB_BASE + 0x0C)  
GPIOB_ODR = 0xFFFF;  

第四步:地址宏按层级串起来,和三级定位一一对应。

/* 片上外设基地址 */
#define PERIPH_BASE         ((unsigned int)0x40000000)  
  
/* 总线基地址 = 片上外设基地址 + 总线偏移 */#define APB1PERIPH_BASE     PERIPH_BASE  
#define APB2PERIPH_BASE     (PERIPH_BASE + 0x00010000)  
#define AHBPERIPH_BASE      (PERIPH_BASE + 0x00020000)  
  
/* GPIO 外设基地址 = 总线基地址 + 外设偏移 */#define GPIOA_BASE          (APB2PERIPH_BASE + 0x0800)  
#define GPIOB_BASE          (APB2PERIPH_BASE + 0x0C00)  
#define GPIOC_BASE          (APB2PERIPH_BASE + 0x1000)  
#define GPIOD_BASE          (APB2PERIPH_BASE + 0x1400)  
#define GPIOE_BASE          (APB2PERIPH_BASE + 0x1800)  
#define GPIOF_BASE          (APB2PERIPH_BASE + 0x1C00)  
#define GPIOG_BASE          (APB2PERIPH_BASE + 0x2000)  
  
/* 寄存器地址 = 外设基地址 + 寄存器偏移,以 GPIOB 为例 */#define GPIOB_CRL           (GPIOB_BASE + 0x00)  
#define GPIOB_CRH           (GPIOB_BASE + 0x04)  
#define GPIOB_IDR           (GPIOB_BASE + 0x08)  
#define GPIOB_ODR           (GPIOB_BASE + 0x0C)  
#define GPIOB_BSRR          (GPIOB_BASE + 0x10)  
#define GPIOB_BRR           (GPIOB_BASE + 0x14)  
#define GPIOB_LCKR          (GPIOB_BASE + 0x18)

有了具体地址就能用指针读写。用 (unsigned int *) 把宏的数值强制转换成地址,再用 * 做取指针操作:

/* 控制 GPIOB 引脚 0 输出低电平(BSRR 寄存器的 BR0 置 1) */*(unsigned int *)GPIOB_BSRR = (0x01 << (16 + 0));  
  
/* 控制 GPIOB 引脚 0 输出高电平(BSRR 寄存器的 BS0 置 1) */*(unsigned int *)GPIOB_BSRR = 0x01 << 0;  
  
unsigned int temp;  
/* 读取 GPIOB 端口所有引脚的电平(读 IDR 寄存器) */temp = *(unsigned int *)GPIOB_IDR;  

第五步:用结构体打包寄存器列表。上面那样逐个定义还是繁琐——GPIOA~GPIOG 各有一组功能相同的寄存器(GPIOA_ODR/GPIOB_ODR/GPIOC_ODR……),只是地址不一样,却要为每个都定义地址。改用结构体封装:

typedef unsigned       int uint32_t;    /* 无符号 32 位变量 */
typedef unsigned short int uint16_t;    /* 无符号 16 位变量 */  
/* GPIO 寄存器列表 */
typedef struct {  
 uint32_t CRL;      /* GPIO 端口配置低寄存器    地址偏移: 0x00 */  
 uint32_t CRH;      /* GPIO 端口配置高寄存器    地址偏移: 0x04 */  
 uint32_t IDR;      /* GPIO 数据输入寄存器      地址偏移: 0x08 */  
 uint32_t ODR;      /* GPIO 数据输出寄存器      地址偏移: 0x0C */  
 uint32_t BSRR;     /* GPIO 位设置/清除寄存器   地址偏移: 0x10 */  
 uint32_t BRR;      /* GPIO 端口位清除寄存器    地址偏移: 0x14 */  
 uint16_t LCKR;     /* GPIO 端口配置锁定寄存器  地址偏移: 0x18 */  
} GPIO_TypeDef;

七个成员变量名正好对应寄存器名。C 语法规定结构体内变量的存储空间是连续的,32 位变量占 4 字节、16 位占 2 字节。所以假如结构体首地址是 0x4001 0C00(也就是第一个成员 CRL 的地址),第二个成员 CRH 的地址就是 0x4001 0C00 + 0x04,这个 0x04 正是 CRL 占用的 4 字节。

这样的成员偏移与 GPIO 外设寄存器的地址偏移一一对应,只要给结构体设置好首地址,成员地址就全部确定,然后就能以结构体形式访问寄存器:

GPIO_TypeDef *GPIOx;                      /* 定义一个结构体指针 */
GPIOx = (GPIO_TypeDef *)GPIOB_BASE;       /* 指向 GPIOB_BASE (0x4001 0C00) */  
GPIOx->IDR = 0xFFFF;  
GPIOx->ODR = 0xFFFF;  
  
uint32_t temp;  
temp = GPIOx->IDR;                        /* 读 GPIOB_IDR 的值到 temp */

GPIOB_BASE 展开后是整数而非指针,必须加 (GPIO_TypeDef *) 强制转换,否则编译器报类型不匹配。

第六步:直接用宏定义好结构体指针,让指针指向各个 GPIO 端口的首地址,使用时直接用宏访问寄存器。这就是标准库的最终形态。

/* 使用 GPIO_TypeDef 把地址强制转换成指针 */
#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)  
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)  
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)  
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)  
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)  
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)  
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)  

此后访问寄存器就像访问普通结构体字段:

/* 访问 GPIOB 端口的寄存器 */
GPIOB->BSRR = 0xFFFF;        /* 修改 GPIOB_BSRR 寄存器 */
GPIOB->CRL  = 0xFFFF;        /* 修改 GPIOB_CRL 寄存器 */GPIOB->
ODR  = 0xFFFF;        /* 修改 GPIOB_ODR 寄存器 */  
uint32_t temp;  
temp = GPIOB->IDR;           /* 读取 GPIOB_IDR 的值到 temp */  
/* 访问 GPIOA 端口的寄存器,只需换宏名 */
GPIOA->BSRR = 0xFFFF;  
GPIOA->CRL  = 0xFFFF;  
GPIOA->ODR  = 0xFFFF;  
temp = GPIOA->IDR;  

其他外设同样用这套方法封装,这部分工作固件库已经完成。

六、修改寄存器的位操作

只改某几位、其余位保持不变。以 8 位变量 a 示意(STM32 实际 32 位)。

1. 清零:按位与 & + 取反掩码

/* 对某位清零 */unsigned char a = 0x9f;        /* a = 1001 1111 b */  
  
a &= ~(1 << 2);                /* 对 bit2 清零 */
/* (1<<2)      = 0000 0100 b   ← bit2 的掩码  
 ~(1<<2)     = 1111 1011 b 1001 1111 & 1111 1011 = 1001 1011 b   bit2 清零,其它位不变 */  
  
/* 对某几个连续位清零 */unsigned char a = 0x9f;        /* a = 1001 1111 b */  
  
/* 若把 8 位分成 2 位一组:bit1:0 为第 0 组,bit3:2 为第 1 组,  
 bit5:4 为第 2 组,bit7:6 为第 3 组。现在清零第 1 组 */
 a &= ~(3 << 2 * 1);  
/* (3<<2*1)    = 0000 1100 b   ← bit3、bit2 的掩码  
 ~(3<<2*1)   = 1111 0011 b 1001 1111 & 1111 0011 = 1001 0011 b 第 1 组清零,其它位不变 */  
a &= ~(3 << 2 * 2);            /* 清零第 2 组 bit5:4,3 左移 4 位 */

掩码写法 ~(mask << n * group) 的三个数:

位置 含义 举例
mask 组内所有位都为 1 时的值 2 位一组 → 3;4 位一组 → 0xF(即 1111 b)
n 每组的位数 2 位一组 → 2;4 位一组 → 4
group 组编号,从 0 数 清第 3 组 bit7:6 → 3,即左移 6 位

2. 赋值:按位或 |

清零之后写入参数值。

/* 对某位赋值 */
/* 假设 a = 1000 0011 b */
a |= (1 << 4);                 /* bit4 置 1,得 a = 1001 0011 b */  
  
/* 对某几位赋值 */
/* 假设 a = 1000 0011 b */
a |= (1 << 2 * 2);             /* 给已清零的第 2 组 bit5:4 写入 01 b */
/* 即把 01 b 左移 4 位,得 a = 1001 0011 b,其它组不变 */

3. 取反:按位异或 ^

/* 假设 a = 1001 0011 b,把 bit6 取反,其它位不变 */
a ^= (1 << 6);                 /* 得 a = 1101 0011 b */

标准套路:先 &= ~掩码 清零,再 |= 新值 写入。BSRR 这类"写 0 无操作"的寄存器不用清零,直接写目标位。

七、寄存器位权限标记

标记 含义
r 只读。一般是硬件自动更新的状态位
w 只写。读回的值无意义
rw 可读写

八、GPIO 寄存器位定义速查

七个寄存器构成完整的 GPIO 控制接口,x 为端口号 A~G,y 为引脚号 0~15。

1. GPIOx_CRL / GPIOx_CRH —— 端口配置(偏移 0x00 / 0x04)

权限 rw,复位值 0x4444 4444(所有引脚默认浮空输入)。CRL 配置引脚 0~7,CRH 配置引脚 8~15。

每个引脚占 4 位,低 2 位 MODEy 定模式,高 2 位 CNFy 定具体类型:

位布局(每引脚 4 位): [CNFy:2][MODEy:2]
CRL: 引脚0 占 bit3:0,引脚1 占 bit7:4 …… 引脚7 占 bit31:28
CRH: 引脚8 占 bit3:0,引脚9 占 bit7:4 …… 引脚15 占 bit31:28

MODEy(低 2 位)——定输入/输出及速度

值 含义
00 输入模式(复位默认)
01 输出模式,最大速度 10MHz
10 输出模式,最大速度 2MHz
11 输出模式,最大速度 50MHz

CNFy(高 2 位)——定具体类型

值 MODEy = 00(输入时) MODEy ≠ 00(输出时)
00 模拟输入(ADC 用) 通用推挽输出
01 浮空输入(复位默认) 通用开漏输出
10 上拉/下拉输入 复用推挽输出
11 保留 复用开漏输出

上拉还是下拉由 ODR 决定:配成上拉/下拉输入(CNFy=10、MODEy=00)后,写 ODRy=1 为上拉,ODRy=0 为下拉。

常用配置值(4 位完整值):

用途 4 位值 说明
推挽输出 50MHz 0011 = 0x3 驱动 LED、普通输出
开漏输出 50MHz 0111 = 0x7 I2C、线与场合
复用推挽 50MHz 1011 = 0xB 串口 TX、SPI
浮空输入 0100 = 0x4 复位默认
上拉/下拉输入 1000 = 0x8 按键检测
模拟输入 0000 = 0x0 ADC 采样

配置示例:

/* 把 GPIOB 引脚 0 配成推挽输出 50MHz */
GPIOB->CRL &= ~(0x0F << (4 * 0));    /* 先清掉引脚 0 的 4 位 */GPIOB->CRL |=  (0x03 << (4 * 0));    /* 再写入 0011 */  
/* 把 GPIOB 引脚 10 配成上拉输入(引脚 10 在 CRH,组号 10-8=2) */
GPIOB->CRH &= ~(0x0F << (4 * 2));  
GPIOB->CRH |=  (0x08 << (4 * 2));  
GPIOB->ODR |=  (1 << 10);            /* ODR 置 1 选上拉 */

2. GPIOx_IDR —— 数据输入(偏移 0x08)

位 名称 权限 说明
31:16 保留 — 始终读为 0
15:0 IDRy r(只读) 对应引脚的输入电平,1 = 高、0 = 低

只能以字(32 位)为单位访问。硬件每个 APB2 时钟周期采样一次。

/* 读引脚 5 的电平 */
if (GPIOB->IDR & (1 << 5)) {  
 /* 引脚 5 为高电平 */
 }  

3. GPIOx_ODR —— 数据输出(偏移 0x0C)

位 名称 权限 说明
31:16 保留 — 始终为 0
15:0 ODRy rw 输出模式下决定引脚电平;输入模式下选上拉(1)/下拉(0)

复位值 0x0000 0000。只能以字为单位访问。

GPIOB->ODR |=  (1 << 0);     /* 引脚 0 输出高 —— 读-改-写,非原子 */
GPIOB->ODR &= ~(1 << 0);     /* 引脚 0 输出低 */
GPIOB->ODR  = 0xFFFF;        /* 16 个引脚全部输出高 */

|= 和 &= 都是读-改-写三步操作,中断里改同一端口会冲突。操作单个引脚用下面的 BSRR。

4. GPIOx_BSRR —— 位设置/清除(偏移 0x10)

位 名称 权限 写 1 的效果 写 0 的效果
31:16 BRy w(只写) 复位 ODRy → 引脚输出低 无操作
15:0 BSy w(只写) 置位 ODRy → 引脚输出高 无操作

复位值 0x0000 0000,读回的值无意义。

写 0 无操作,所以一条赋值语句就能精确改动目标引脚,不影响其他引脚,天然原子,无需读-改-写。推荐用这个操作引脚。

GPIOB->BSRR = (1 << 0);            /* 引脚 0 输出高(低 16 位 = BS0) */
GPIOB->BSRR = (1 << (16 + 0));     /* 引脚 0 输出低(高 16 位 = BR0) */
GPIOB->BSRR = (1 << 3) | (1 << (16 + 5));   /* 引脚3 置高、引脚5 拉低,一次完成 */

若同一引脚的 BSy 和 BRy 同时写 1,BSy 优先,结果是置位。

5. GPIOx_BRR —— 位清除(偏移 0x14)

位 名称 权限 说明
31:16 保留 — —
15:0 BRy w(只写) 写 1 复位 ODRy → 引脚输出低;写 0 无操作

功能与 BSRR 的高 16 位重复,只拉低引脚时用它更直观。

GPIOB->BRR = (1 << 0);     /* 引脚 0 输出低,等价于 BSRR = 1<<16 */

6. GPIOx_LCKR —— 配置锁定(偏移 0x18)

位 名称 权限 说明
31:17 保留 — —
16 LCKK rw 锁定键。写入特定序列后置 1,锁定生效
15:0 LCKy rw 对应引脚锁定位,1 = 锁定该引脚配置

锁定后该引脚的 CRL/CRH/ODR 配置直到下次系统复位都无法修改。防止误操作改动关键引脚。

激活锁定要按固定序列操作 LCKK 位:写 1 → 写 0 → 写 1 → 读 0 → 读 1。

/* 锁定 GPIOB 引脚 0 和引脚 1 的配置 */
uint32_t pins = (1 << 0) | (1 << 1);  
GPIOB->LCKR = (1 << 16) | pins;    /* LCKK 写 1 */
GPIOB->LCKR = pins;                /* LCKK 写 0 */
GPIOB->LCKR = (1 << 16) | pins;    /* LCKK 写 1 */(void)
GPIOB->LCKR;                 /* 读一次(此时 LCKK 读为 0) */(void)
GPIOB->LCKR;                 /* 再读一次确认(LCKK 读为 1,锁定成功) */

注意 LCKR 需要 32 位访问才能写到 bit16,结构体里该成员必须是 uint32_t,声明成 uint16_t 写不进去。

7. 完整点灯示例

/* 假设 LED 阳极接 VCC,阴极接 PB0,引脚输出低电平时灯亮 */  
/* 第一步:开 GPIOB 时钟。外设不开时钟,寄存器写不进去 */
#define RCC_APB2ENR    (*(unsigned int *)(0x40021000 + 0x18))  
RCC_APB2ENR |= (1 << 3);             /* bit3 = IOPBEN,使能 GPIOB 时钟 */  

/* 第二步:把 PB0 配成推挽输出 50MHz */
GPIOB->CRL &= ~(0x0F << (4 * 0));  
GPIOB->CRL |=  (0x03 << (4 * 0));  
  
/* 第三步:输出低电平点亮 */GPIOB->BSRR = (1 << (16 + 0));       /* 灭:GPIOB->BSRR = (1 << 0); */  

开时钟最容易漏。STM32 外设默认关闭时钟省电,不使能就写寄存器,表现为写入无效、读回全 0。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论