【嵌入式硬件】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。

芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。
开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作。

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

内核和外设之间通过总线连接。总线上有 4 个驱动单元(可理解为 CPU 侧,发起访问)和 4 个被动单元(外设侧,被访问)。

我们可以把驱动单元理解成是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 语言对它们读写。
存储器本身不具有地址信息,地址是由芯片厂商或用户分配的。给存储器分配地址的过程就叫存储器映射;如果给同一存储器再分配一个地址,叫存储器重映射。

区域功能划分: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 |

五、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。
