零基础玩转STM32:深入理解ARM Cortex-M内核与寄存器编程
1. 什么是 STM32
STM32 是 ST(意法半导体,STMicroelectronics)公司推出的 32 位微控制器。
其内核基于 ARM Cortex-M 系列(如 M0、M3、M4、M7),性能强大、功耗低、外设丰富。凭借高性价比和完善的生态,STM32 在嵌入式开发和教学中得到了广泛应用。
相比传统的 8 位单片机(如 51 系列),STM32 提供了:
- 更强的运算能力(32 位,主频更高)
- 更丰富的接口(USART、I2C、SPI、CAN、USB 等)
- 更低功耗和更高集成度
这使得 STM32 成为现代嵌入式开发的主流选择。
2. STM32 能做什么
STM32 自带了丰富的外设接口(USART、I2C、SPI、CAN、USB…),能连接各种传感器和模块,被广泛应用在:
- 智能手环:采集加速度 / 陀螺仪数据,蓝牙 / WiFi 通信,OLED 显示。
- 微型四轴飞行器:电机控制、姿态解算、无线通信。
- 智能家电:电饭锅、空气净化器、智能门锁。
- 工业控制:传感器采集、通讯模块、运动控制。
这些应用展示了 STM32 在物联网设备、可穿戴设备、智能家电和机器人等领域的潜力。可以说,几乎所有带有‘智能化’功能的电子产品中,都可能藏着一颗 STM32 芯片。
3. STM32 的选型
3.1 系列分类与适用场景
根据内核和定位,STM32 主要分为以下几类:
内核 | 代表系列 | 特点 | 场景 |
---|---|---|---|
Cortex-M0/M0+ | STM32F0/L0 | 超低功耗、低成本 | 简单设备、低功耗应用 |
Cortex-M3 | STM32F1 | 主流型,资料丰富 | 入门学习、常规控制 |
Cortex-M4 | STM32F4 | 高性能,带 DSP/FPU | 图像处理、大数据计算 |
Cortex-M7 | STM32F7/H7 | 更高性能 | 需要复杂运算、大屏显示 |
学习入门推荐:初学者先用 F1 系列入门,等熟悉后再上手 F4 系列。
3.2 命名规则
以 STM32F103VET6 为例:
更详细的命名方法
理解命名规则有助于快速找到合适的芯片型号。
3.3 如何选择合适的 MCU
在选 STM32 的时候,一般从以下几个方面来权衡:
3.3.1. 内核和性能
- 如果只是点亮 LED、读写按键、做一些简单的通信实验 → STM32F103C8T6(常见的“最小系统板”)就够了。它是 Cortex-M3 内核,主频 72MHz,Flash 64KB,性价比高,资料也非常丰富。
- 如果需要浮点运算 / DSP 运算(比如滤波、FFT、姿态解算) → 选 STM32F4 系列。例如:STM32F407VGT6:Cortex-M4 内核,带硬件 FPU,主频 168MHz,1MB Flash,192KB RAM。常用于飞控、数据处理。
- 如果要驱动大屏幕(TFT LCD、触摸屏)或复杂 GUI → 建议用 STM32F429 或 STM32F746,它们自带 LCD 控制器 LTDC,带 DMA2D 图形加速,适合做界面。
3.3.2. 引脚数量与封装
- 小型项目(传感器采集、UART 调试) → 48/64 引脚的芯片即可,比如 STM32F103C8T6(LQFP48 封装)。
- 如果需要更多接口(CAN 总线、以太网、多个 SPI/I2C) → 就要选择 100 引脚以上的型号,比如 STM32F407ZGT6(LQFP144 封装)。
一般来说:引脚越多 → 外设资源越全 → PCB 更复杂 → 成本更高。
3.3.3. Flash 与 RAM 要求
- 学习和基础控制项目 → 64KB Flash / 20KB RAM 已足够(如 F103C8T6)。
- 如果要跑 RTOS(如 FreeRTOS)+ 多任务管理 + 通信协议栈(如 TCP/IP、USB Host) → 至少要 256KB Flash / 64KB RAM。推荐 STM32F407VGT6(1MB Flash / 192KB RAM),完全够用。
- 做音频处理 / 图形界面 → 需要 大于 512KB Flash + 192KB RAM,否则容易“内存告急”。
3.3.4. 功耗
- 电池供电、需要长期待机 → 用 STM32L 系列(低功耗系列),比如 STM32L072CZT6。它有超低功耗模式,待机电流可低至几微安,适合智能手表、传感器节点。
- 如果是 USB 供电或者不用太在意耗电 → 直接选择 F1/F4 系列即可。
4. STM32 的引脚分类
对 MCU 的引脚进按照功能进行分类,以 STM32F103VET6 引脚分类(LQFP100 封装)为例:
分类 | 典型引脚 | 功能说明 | 应用场景 |
---|---|---|---|
电源类 | VDD, VSS, VDDA, VSSA, VBAT | 数字电源、模拟电源、RTC 电源 | MCU 供电、ADC 模拟精度、RTC 备用电源 |
时钟类 | OSC_IN, OSC_OUT, PC14, PC15 | 外部高速晶振、低速晶振输入/输出 | 系统主时钟、RTC 时钟 |
复位类 | NRST | 外部复位输入 | 外接复位电路,保证上电可靠 |
通用 IO | PAx, PBx, PCx, PDx | 通用数字 IO | 控制 LED、按键输入 |
ADC/DAC | PA0–PA7, PC0–PC5 | 模拟输入,部分支持 DAC | 传感器采样、电压检测、DAC 输出波形 |
定时器 | PA8–PA11, PB0–PB1, PC6–PC9 等 | 高级定时器/通用定时器通道 | PWM 输出、电机控制 |
串口 (USART/UART) | PA9, PA10, PB10, PB11, PD8–PD12 等 | 串行通信接口 | 调试串口、外设通信 |
I²C | PB6, PB7, PB10, PB11 | I²C1/I²C2 通信 | 连接 EEPROM、传感器 |
SPI | PA5–PA7, PB12–PB15 | SPI1/SPI2 总线接口 | 显示屏、存储器 |
CAN/USB | PA11, PA12, PB8, PB9 | CAN 总线,USB DM/DP | 工业通信、USB 设备 |
调试接口 | PA13 (SWDIO), PA14 (SWCLK), PB3 (JTDO), PB4 (NJTRST) | SWD/JTAG 调试接口 | 下载程序、在线调试 |
5. 芯片内部组成
从外部看, STM32 芯片只是一个封装好的成品,而实际上,它的内部主要由 内核 (Core) 和 片上外设 (Peripherals) 两大部分组成。类比于电脑,内核相当于CPU,而片上外设则类似于主板上的内存、显卡、硬盘等组件。
以 STM32F103 为例,它采用 ARM 公司设计的 Cortex-M3 内核,这是芯片的“大脑”。值得注意的是,ARM 公司仅提供芯片技术授权,并不直接生产芯片。具体的芯片制造由 ST、TI、Freescale 等半导体厂商(SOC)完成,这些厂商在内核基础上设计外围功能模块,并生产完整芯片。其中 内核之外的功能模块 被称为 核外外设(或片上外设)。常见的片上外设包括 GPIO、USART、I²C、SPI 等。具体架构可参考图 STM32 芯片架构简图。

内核与外设之间通过多条总线相连,其中包含4个驱动单元和4个被动单元(详见STM32F10xx系统框图)。为便于理解,可将驱动单元视为CPU部分,而被动单元则看作各种外设。接下来我们将简要介绍驱动单元和被动单元的各组成部分。

5.1 ICode 总线(取指通道)
ICode 中的 “I” 表示 Instruction(指令)。我们写好的程序编译之后会生成一条条指令,存放在 FLASH 中。内核要执行程序,就必须通过 ICode 总线 从 FLASH 中读取指令。这条总线几乎始终在工作,是专门的“取指通道”。
5.2 驱动单元
驱动单元主要负责内核与存储器、外设之间的数据与指令交互。在 STM32F10xx 中,驱动单元包括 ICode 总线、DCode 总线、System 总线和 DMA 总线。
5.2.1 DCode(取数通道)
DCode 中的 D 表示 Data(数据),说明这条总线主要用于取数。
在程序运行时,涉及的数据包括两类:
- 常量(const 修饰的数据):存放在 内部 FLASH 中,内容固定不变。
- 变量(全局变量、局部变量、堆栈数据等):存放在 内部 SRAM 中,内容可变。
由于数据既可能被 DCode 总线访问,也可能被 DMA 总线访问,因此在存取数据时,需要通过 总线矩阵(Bus Matrix) 仲裁,避免访问冲突,确保系统运行稳定。
5.2.2 System(外设寄存器访问)
System 总线主要用于访问外设寄存器。我们常说的“寄存器编程”,即对外设寄存器进行读写操作(如 GPIO 控制、USART 配置、SPI 通信设置等),都是通过 System 总线完成的。可以理解为:System 总线是内核与外设交互的桥梁。
5.2.3 DMA(大块数据搬运)
在 STM32 中,CPU 并不是所有数据传输都要亲自参与。为了减轻负担,芯片引入了 DMA 控制器,它能独立完成大块数据搬运。DMA(Direct Memory Access,直接存储器访问)总线的作用是在存储器与外设之间,或存储器与存储器之间进行高速数据传输,而不需要 CPU 逐条指令参与,从而减轻内核负担。
DMA 可以搬运的数据来源或目标包括:
- 外设的数据寄存器(如 ADC 转换结果寄存器、USART 数据寄存器等)
- 内部 SRAM
- 内部 FLASH(仅支持只读访问)
与 DCode 总线类似,DMA 总线在访问 SRAM 和 FLASH 时,也需要通过 总线矩阵 进行仲裁,以避免冲突。由于 DMA 支持批量搬运数据,且在传输过程中内核可以继续执行其他任务,因此它非常适合处理:
- 外设大数据流(如 ADC 连续采样)
- 外设收发缓冲(如 USART 大量数据通信)
- 图像、音频等高带宽数据
在 STM32F103 系列中,DMA 控制器挂载在 AHB 总线上,可以通过不同的 DMA 通道来配置多个数据搬运任务。
5.2.4 总线矩阵(Bus Matrix)
在 STM32 内部,虽然有 ICode、DCode、System 和 DMA 四条总线,但它们在访问存储器或外设时 可能会出现冲突:例如 CPU 正在通过 DCode 总线访问 SRAM,同时 DMA 也要搬运 SRAM 数据。如果没有合理的调度,就可能导致数据错误或访问延迟。
为了解决这个问题,STM32 内部设计了 总线矩阵(Bus Matrix)。总线矩阵可以理解为 多总线之间的交通枢纽,负责仲裁不同总线的访问请求,确保每个总线能够安全、稳定地访问片内存储器或外设。
功能特点
- 多路访问调度:总线矩阵会根据优先级和空闲情况,决定哪条总线优先访问共享资源(如 SRAM 或 FLASH)。
- 避免冲突:当多条总线同时请求访问同一资源时,总线矩阵会排队或分时片处理,保证数据不被破坏。
- 提高效率:CPU、DMA、外设可以并行请求不同资源,通过矩阵分配访问权,从而提升整体吞吐量。
典型应用场景
- DCode 总线 + DMA 总线访问 SRAM:CPU 通过 DCode 总线读取数据,同时 DMA 要搬运数据到外设缓冲区,总线矩阵会仲裁访问,保证两者不会冲突。
- System 总线访问外设 + DMA 总线搬运数据:外设寄存器可能被 CPU 配置和 DMA 同时访问,总线矩阵协调访问,保证外设寄存器读写的正确性。
类比理解,可以把总线矩阵想象成一个多入口、多出口的交通灯路口:
- 各总线是不同的车道(CPU、DMA 等)
- 共享资源(SRAM、FLASH、外设)是交叉路口
- 总线矩阵像交通灯一样分配通行权,避免“车祸”,保证数据安全和系统稳定运行。
5.3 被动单元
被动单元主要是数据与指令的存储器及扩展接口,内核通过驱动单元的总线与它们交互。
5.3.1 内部的 FLASH
内部 FLASH(闪存存储器) 是 STM32 的程序存储器,我们编写并下载到芯片里的程序最终存放在这里。
内核通过 ICode 总线来取这里面的指令,通过 DCode 总线来读取存放在 FLASH 中的常量。
5.3.2 内部的 SRAM
内部 SRAM(静态随机存取存储器)是 STM32 的数据存储器,用于存放:
- 程序运行时的变量(全局变量、局部变量)
- 堆栈数据
- 临时数据缓冲区
内核主要通过 DCode 总线访问 SRAM,而 DMA 总线也能直接访问,从而实现高速数据搬运。
5.3.3 FSMC
FSMC(Flexible Static Memory Controller),叫灵活的静态的存储器控制器,是 STM32F10xx 中一个很有特色的外设。
通过 FSMC,允许芯片外扩静态存储器,如:
- 外部 SRAM
- NAND Flash
- NOR Flash
需要注意:FSMC 仅支持静态存储器(Static Memory),即名称里面的 S:static,不能是动态的内存。因此不能扩展动态存储器(如 SDRAM)。
5.3.4 AHB/APB 架构与外设分布
STM32 采用分层总线架构,由 AHB 总线扩展出 APB1 和 APB2 两条外设总线。这些总线上连接着 STM32 的各类特色外设模块,包括常见的 GPIO、串口、I2C、SPI 等外设接口。掌握这些外设的编程方法,驱动外部设备运行,是学习 STM32 的关键内容。
STM32 内部架构采用总线分级设计:
- AHB 总线:连接高速资源(DMA、FLASH、FSMC 等)
- APB 总线:主要连接各类外设模块
- |__ APB2 总线:挂载高速外设(GPIO、USART1、SPI1、ADC 等)
- |__ APB1 总线:挂载低速外设(USART2/3、I2C、CAN、DAC 等)
因此,我们在学习 STM32 时,重点就是掌握 如何通过寄存器配置 APB 总线上的外设,以实现对外部传感器和设备的驱动控制。
6. 存储器
6.1 存储器映射
STM32 内部资源丰富,包括 Flash、SRAM 和各种外设,这些功能部件共同排列在一个 4GB 的地址空间内,如下图 STM32F10xx 系统框图所示:

在 STM32 中,存储器单元本身并没有地址。为了使 CPU 能够统一访问,芯片在设计时为不同的功能模块(如 Flash、SRAM、外设寄存器等)划定了唯一的地址区间。处理器通过地址总线访问这些区间,从而实现对相应存储器或外设的操作。这种逻辑地址与物理资源之间的对应关系称为 存储器映射。理解存储器映射,有助于我们明确“访问某个地址实际上就是在操作哪个硬件”。
例如,在 STM32 的存储器映射中:
- 0x0800 0000:片内 Flash 的起始地址
- 0x2000 0000:片内 SRAM 的起始地址
- 0x4000 0000:外设寄存器的起始地址
这样,CPU 通过访问不同的地址,就能读写对应的存储器或外设。如图所示:

6.2 存储器重映射
所谓 存储器重映射(Remap),是指通过系统配置,将某一段固定的地址区间重新映射到不同的物理存储器或外设。
常见的应用场景是 启动引导:
- STM32 启动时,CPU 会从地址
0x0000 0000
取指令。 - 默认情况下,这个区域映射到系统 Boot ROM(存放启动代码)。
- 通过配置系统寄存器,这个区域也可以重映射到 SRAM 或片内 Flash。
- 这样,就可以灵活决定 MCU 是从 BootLoader、用户 Flash 还是 SRAM 启动。
因此,重映射并不是“再分配一个新地址”,而是 让同一个逻辑地址段对应到不同的物理存储器。
6.3 存储器区域功能划分
在 4GB 的地址空间中,ARM 将其大致分为 8 个块,每个块的大小为 512MB。每个块有其规定的用途,具体的划分见下表。由于每个块的大小非常大,芯片厂商在每个块的范围内设计的外设并不会用满,通常只使用其中的一部分。
序号 | 用途 | 地址范围 |
---|---|---|
Block 0 | Code (程序代码区) | 0x00000000 ~ 0x1FFFFFFF (512MB) |
Block 1 | SRAM (静态存储器) | 0x20000000 ~ 0x3FFFFFFF (512MB) |
Block 2 | 片上外设 | 0x40000000 ~ 0x5FFFFFFF (512MB) |
Block 3 | FSMC Bank1~Bank2 | 0x60000000 ~ 0x7FFFFFFF (512MB) |
Block 4 | FSMC Bank3~Bank4 | 0x80000000 ~ 0x9FFFFFFF (512MB) |
Block 5 | FSMC 寄存器区 | 0xA0000000 ~ 0xCFFFFFFF (512MB) |
Block 6 | 未使用 | 0xD0000000 ~ 0xDFFFFFFF (512MB) |
Block 7 | Cortex-M3 内部外设 | 0xE0000000 ~ 0xFFFFFFFF (512MB) |
在这 8 个块中,有 3 个块对我们最为重要,且是我们最关注的:
- Block0:用于设计片内 FLASH
- Block1:用于设计片内 RAM
- Block2:用于设计片上外设
下面我们将简要介绍这三个块中的具体区域功能划分。
6.3.1 Block0 内部存储器区域划分
Block0 主要用于片内的 FLASH。STM32F103ZET6 和 STM32F103VET6 的 FLASH 均为 512KB,属于较大容量。Block 0 内部区域的具体功能划分如下表所示:
块 | 用途说明 | 地址范围 |
---|---|---|
Block 0 | 预留 | 0x1FFE C008 ~ 0x1FFF FFFF |
选项字节:用于配置读写保护、BOR级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不小心被锁住之后,我们可以从 RAM 里面启动来修改这部分相应的寄存器位。 | 0x1FFF F800 ~ 0x1FFF F80F | |
系统存储器:里面存的是 ST 出厂时烧写好的 isp 自举程序(即 Bootloader),用户无法改动。串口下载的时候需要用到这部分程序。 | 0x1FFF F000 ~ 0x1FFF F7FF | |
预留 | 0x0808 0000 ~ 0x1FFF EFFF | |
FLASH:我们的程序就放在这里。 | 0x0800 0000 ~ 0x0807 FFFF(512KB) | |
预留 | 0x0008 0000 ~ 0x07FF FFFF | |
取决于BOOT引脚,为FLASH、系统存储器、SRAM的别名。 | 0x0000 0000 ~ 0x0007FFFF |
6.3.2 Block1 内部存储器区域划分
Block1 主要用于设计片内 SRAM。STM32F103ZET6 和 STM32F103VET6 的 SRAM 容量均为 64KB。Block1 内部区域的具体功能划分如下表所示:
块 | 用途说明 | 地址范围 |
Block 1 | 预留 | 0x2001 0000 ~ 0x3FFF FFFF |
SRAM 64KB | 0x2000 0000 ~ 0x2000 FFFF |
6.3.3 Block2 内部存储器区域划分
Block2 专用于片内外设设计。根据总线速度的差异,该区域划分为 APB 和 AHB 两部分,其中 APB 还细分为 APB1 和 APB2。详细的存储器 Block2 内部区域功能划分见下表:
块 | 用途说明 | 地址范围 |
Block 2 | APB1 总线外设 | 0x4000 0000 ~ 0x4000 77FF |
APB2 总线外设 | 0x4001 0000 ~ 0x4001 3FFF | |
AHB 总线外设 | 0x4001 8000 ~ 0x5003 FFFF |
7 STM32 的总线基地址与外设映射
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速 外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地 址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始, 也叫外设基地址。
7.1 总线基地址(APB1/APB2/AHB)
表格中总线基地址的"相对外设基地址偏移"指的是该总线地址与片上外设基地址0x40000000之间的差值。关于地址偏移的概念,在后续内容中详细说明。
总线名称 | 总线基地址 | 相对外设基地址的偏移 |
---|---|---|
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB | 0x4001 8000 | 0x0001 8000 |
7.2 外设基地址(以 GPIO 为示例)
在 STM32 系统中,总线上挂载着各种外设,这些外设各自占有一段固定的地址空间。每个外设的首个地址称为 “外设基地址”,也可称为该外设的边界地址。具体的 STM32F10xx 外设边界地址,可以参考《STM32F10xx 参考手册》的 2.3 小节的存储器映射的表 1:STM32F10xx 寄存器边界地址。
下面以 GPIO 外设 为例说明外设基地址的概念。GPIO 属于高速外设,挂载在 APB2 总线 上,其基地址如下表所示:
外设名称 | 外设基地址 | 相对 APB2 总线的地址偏移 |
---|---|---|
GPIOA | 0x4001 0800 | 0x0000 0800 |
GPIOB | 0x4001 0C00 | 0x0000 0C00 |
GPIOC | 0x4001 1000 | 0x0000 1000 |
GPIOD | 0x4001 1400 | 0x0000 1400 |
GPIOE | 0x4001 1800 | 0x0000 1800 |
GPIOF | 0x4001 1C00 | 0x0000 1C00 |
GPIOG | 0x4001 2000 | 0x0000 2000 |
通过这些基地址,我们可以方便地通过寄存器映射访问 GPIO 的各个功能寄存器,例如 ODR(输出数据寄存器)、IDR(输入数据寄存器)等,实现对 GPIO 端口的控制。
7.3 外设寄存器布局
在某个外设的地址范围内,分布的就是该外设的各个寄存器。以 GPIO 外设为例,GPIO 是 通用输入输出端口(General Purpose Input/Output)的简称,简单来说,就是 STM32 可以控制的引脚。GPIO 的基本功能是控制引脚输出高电平或低电平。最简单的应用场景是将 GPIO 引脚连接到 LED 灯的阴极,LED 灯的阳极接电源,通过 STM32 控制该引脚的电平,就可以实现 LED 灯的亮灭控制。
GPIO 外设包含多个寄存器,每个寄存器都有特定功能。每个寄存器为 32 位(4 字节),在外设的基地址上按顺序排列。寄存器的位置通常以相对于该外设基地址的偏移地址来描述,这样便于查阅和操作。
下面以 GPIOB 端口 为例,列出其主要寄存器及地址信息:
寄存器名称 | 寄存器地址 | 相对于基地址的偏移 | 功能说明 |
---|---|---|---|
GPIOB_CRL | 0x4001 0C00 | 0x00 | 配置寄存器,配置 GPIOB 端口 0~7 引脚的模式(输入/输出、速率、复用功能等) |
GPIOB_CRH | 0x4001 0C04 | 0x04 | 配置寄存器,配置 GPIOB 端口 8~15 引脚的模式(输入/输出、速率、复用功能等) |
GPIOB_IDR | 0x4001 0C08 | 0x08 | 输入数据寄存器,读取 GPIOB 各引脚的当前输入电平状态 |
GPIOB_ODR | 0x4001 0C0C | 0x0C | 输出数据寄存器,用于控制 GPIOB 各引脚的输出高低电平 |
GPIOB_BSRR | 0x4001 0C10 | 0x10 | 位设置/复位寄存器,通过写 1 可以单独置位或复位指定 GPIO 引脚 |
GPIOB_BRR | 0x4001 0C14 | 0x14 | 位复位寄存器,通过写 1 可以单独复位指定 GPIO 引脚 |
GPIOB_LCKR | 0x4001 0C18 | 0x18 | 配置锁定寄存器,可锁定 GPIO 引脚的配置,防止误操作修改引脚模式 |
通过这些寄存器,开发者可以对 GPIO 引脚进行配置和控制,例如设置输入/输出模式、读写引脚电平、锁定引脚配置等。
有关外设寄存器的详细说明请参阅《STM32F10xx参考手册》中具体章节的寄存器描述部分,这些内容在编程过程中需要经常查阅。
下面,我们以STM32F10x-中文参考手册——>8.2 GPIO寄存器描述——>8.2.5 端口位设置/清除寄存器(GPIOx_BSRR) (x=A..E) 为例进行说明,理解寄存器的说明。
7.4 GPIOx_BSRR 寄存器详解(基于STM32F10x-中文参考手册)
打开STM32F10x-中文参考手册,来到 8.2.5 端口位设置/清除寄存器(GPIOx_BSRR) (x=A..E)这一章节,如下所示:
7.4.1 寄存器名称
寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…E)”这段的含义是:寄存器名称为 GPIOx_BSRR,其中的 x 可以是 A~E,表示这个寄存器说明适用于 GPIOA、GPIOB、…至 GPIOE。也就是说,这些 GPIO 端口都有各自的 BSRR 寄存器,功能相同。
7.4.2 偏移地址(基址 + 偏移)
偏移地址是指本寄存器相对于于这个外设的基地址的偏移量。以GPIOA为例,其BSRR寄存器的偏移地址为0x10。根据参考手册,GPIOA的基地址是0x4001 0800,因此就可以算出 GPIOA 的这个 GPIOA_BSRR 寄存器的地址为:
GPIOA_BSRR = 0x4001 0800 + 0x10 = 0x4001 0810
同理,GPIOB的基地址为0x4001 0C00,可算出 GPIOB_BSRR 寄存器的地址:
GPIOB_BSRR = 0x4001 0C00 + 0x10 = 0x4001 0C10
其他GPIO端口的寄存器地址均可按此方法计算得出。
7.4.3 寄存器位表与权限(r/w)
以下是该寄存器的位表,详细列出了0-31位的名称及访问权限。表格顶部的数字为位编号,中间的英文名称为位名称,底部标注的字母为读写权限(其中,w表示只写,r表示只读,rw表示可读写)。需要注意的是,本寄存器所有位均为只写属性,因此读取操作无法保证能够获取其真实内容。部分寄存器位设计为只读,通常是用于反映STM32外设的工作状态,这些状态由硬件自动更新,软件可通过读取来判断外设当前状态。
7.4.4 位功能说明(BSy / BRy)
位功能是寄存器说明的核心部分,阐述了每个位的具体功能。本寄存器(GPIOx_BSRR,端口置位/复位寄存器)的 32 位分为两类:
7.4.4.1 BSy(置位位)
作用:BSy (Bit Set y, y = 0~15) 用于置位 GPIOx_ODR 的第 y 位。
写入规则:
写入 1 → 将 GPIOx_ODR 第 y 位置为 1,导致对应的 GPIOx 第 y 个引脚输出高电平。
写入 0 → 不会影响该位,保持原状态不变。
7.4.4.2 BRy(复位位)
作用:BRy (Bit Reset y, y = 0~15) 用于复位 GPIOx_ODR 的第 y 位。
写入规则:
写入 1 → 将 GPIOx_ODR 第 y 位清零,导致对应的 GPIOx 第 y 个引脚输出 低电平。
写入 0 → 不会影响该位,保持原状态不变。
这里的 y 表示端口引脚编号,例如:
BS0 / BR0 → 控制 GPIOx 的第 0 号引脚(若 x 表示 A,即为 GPIOA 的第 0 引脚)。
BS1 / BR1 → 控制 GPIOx 的第 1 号引脚,依此类推。
注意:GPIOx_BSRR 寄存器仅支持写入操作,无法直接读取其值。实际输出电平状态由 GPIOx_ODR 寄存器存储。对 BSRR 寄存器的 BSy 和 BRy 位进行写操作,实际上会修改 GPIOx_ODR 寄存器中相应的数据位。
总结逻辑
- 写 1 到 BSy → GPIOx_ODR 第 y 位置 1 → GPIOx 对应引脚 y 输出高电平。
- 写 1 到 BRy → GPIOx_ODR 第 y 位清零 → GPIOx 对应引脚 y 输出低电平。
- 写 0 则无效,不改变引脚当前状态。
8. 寄存器与寄存器映射
8.1 什么是寄存器
寄存器是 MCU 内部的一种特殊存储单元,用于存放数据和配置参数,是 程序员与硬件沟通的桥梁。我们在编程中通过读写寄存器来驱动 MCU 的片上外设工作。
可以从两个角度来理解寄存器:
硬件角度:寄存器本质上是 MCU 内部的一块存储电路,通常为 8 位、16 位或 32 位。每个寄存器对应一个固定的物理地址,存储的内容可能是:
- 控制位(用于打开/关闭某个外设功能)
- 状态位(反映外设的运行状态,例如“忙碌/空闲”)
- 数据寄存器(存放输入/输出的数据,如 USART 数据缓冲区)
软件角度:寄存器是程序员访问硬件的接口。我们通过 C 语言或汇编语言对寄存器进行读/写操作,从而间接实现对 MCU 外设的控制。例如:
- 设置 GPIO 的寄存器,决定某个引脚是输入还是输出
- 配置 USART 的寄存器,决定波特率和数据格式
- 读取定时器的寄存器,获取计数值
即,寄存器是软件控制硬件的“开关面板”,也是 MCU 运行机制中最核心的部分之一。
注意:在 STM32 中,大多数寄存器都是 32 位宽,并且按照字对齐(4 字节对齐) 的方式排列在 MCU 的内存映射空间中。程序员可以像访问内存地址一样,直接通过寄存器地址来操作外设。
8.2 什么是寄存器映射
在 STM32 等嵌入式系统中,存储器本身并没有固定的地址。为存储器分配地址的过程称为 存储器映射。
寄存器映射 是存储器映射概念在寄存器层面的具体应用。在 STM32 的 Block2 区域(片上外设区),这些外设单元(即寄存器)都以四个字节(32位)为一个基本单元,每个单元都对应着不同的硬件功能,并拥有一个由芯片厂商分配好的固定物理地址。
为了简化编程操作,我们可以为这些已经分配好地址并具有特定功能的内存单元起一个别名,这个别名就是我们编程时所说的 “寄存器”。这种为特定功能的内存单元(寄存器)赋予一个程序员友好名称的过程,就叫做寄存器映射。
8.3 为何需要及如何实现寄存器映射(编程视角)
这些存储单元具有固定的起始地址,我们可以通过 C 语言的指针操作来访问它们。但是,如果每次都直接使用绝对的物理地址,不仅难以记忆,还容易出错。
8.3.1 直接使用地址操作
以 GPIOB 端口的输出数据寄存器 (ODR) 为例,其地址为 0x4001 0C0C。我们可以通过指针操作来控制它:
// GPIOB 端口全部输出高电平(直接使用地址)
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
这种方式中,0x4001 0C0C
对编译器只是一个立即数,需要强制类型转换 (unsigned int*)
让其成为指针,再通过 *
运算符解引用来读写。
8.3.2 使用宏定义进行映射(寄存器映射)
为了解决直接使用地址的问题,我们采用寄存器映射的方法,为地址定义一个易懂的别名。
// 方法A:将地址定义为指针
#define GPIOB_ODR (unsigned int*)(0x40010C0C)
*GPIOB_ODR = 0xFFFF; // 使用时需要解引用// 方法B(更常用):在定义中直接包含解引用操作
#define GPIOB_ODR *(unsigned int*)(0x40010C0C)
GPIOB_ODR = 0xFFFF; // 使用时可直接赋值,如同操作变量
通过这种简单的 #define
,我们完成了最基础的寄存器映射。它将复杂的底层地址操作封装成了一个易于理解和使用的标识符(GPIOB_ODR
),极大地提高了代码的可读性和可维护性。后续章节将在此基础上,介绍更系统、更工程化的封装方法。
9 C 语言对寄存器的封装
前面关于存储器与寄存器映射的讨论,最终目的都是为了更好地理解:如何通过 C 语言对寄存器进行读写,从而实现对外设的控制。这部分内容是本章的重点。
9.1 封装总线和外设基地址
在编程实践中,如果直接使用寄存器的绝对地址来操作外设,不仅难以记忆,而且可读性差、容易出错。
为了解决这一问题,通常通过 宏定义 的方式,将总线基地址、外设基地址以及寄存器偏移量进行封装,并以外设或寄存器的名称作为宏名。这样既直观又便于维护。
总线与外设基地址的宏定义:
/* 外设基地址 */
#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)
这种封装方式以 PERIPH_BASE(片上外设基地址) 为起点。在 STM32F103 中,PERIPH_BASE 的值为 0x4000 0000。
在此基础上,通过叠加不同的总线偏移量,可以得到各总线的基地址:
// APB1 总线基地址
APB1PERIPH_BASE = PERIPH_BASE + 0x00000000 = 0x40000000 // APB2 总线基地址
APB2PERIPH_BASE = PERIPH_BASE + 0x00010000 = 0x40010000 // AHB 总线基地址
AHBPERIPH_BASE = PERIPH_BASE + 0x00020000 = 0x40020000
接下来,再在总线基地址上加上外设的偏移量,就可以得到具体外设的基地址。例如:
// GPIOA 基地址
GPIOA_BASE = APB2PERIPH_BASE + 0x0800 = 0x40010800 // GPIOB 基地址
GPIOB_BASE = APB2PERIPH_BASE + 0x0C00 = 0x40010C00 // GPIOC 基地址
GPIOC_BASE = APB2PERIPH_BASE + 0x1000 = 0x40011000
最后,在外设基地址上继续加寄存器的偏移量,即可得到具体寄存器的地址。例如:
// 输出数据寄存器
GPIOB_ODR = GPIOB_BASE + 0x0C = 0x4001 0C0C // 位设置/复位寄存器
GPIOB_BSRR = GPIOB_BASE + 0x10 = 0x4001 0C10 // 输入数据寄存器
GPIOB_IDR = GPIOB_BASE + 0x08 = 0x4001 0C08
这种 分层式推导 的方法,使寄存器地址的计算过程清晰可见,方便记忆,也便于后续维护。
一旦寄存器地址确定,就可以通过 C 语言指针操作 来进行访问。例如:使用指针操作GPIOB_BSRR和GPIOB_IDR寄存器:
/* 控制 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;
9.2 封装寄存器列表
前面介绍的用宏定义寄存器地址的方法虽然可行,但仍然显得繁琐。例如,GPIOA ~ GPIOE 各自都有一组功能相同的寄存器(如GPIOA_ODR、GPIOB_ODR、GPIOC_ODR 等)。它们的功能相同,只是基地址不同,却需要逐一为每个寄存器定义地址。为了简化操作、提高代码可维护性,我们可以借助 C 语言的结构体语法 对寄存器进行封装。如下所示:
typedef unsigned int uint32_t; /* 无符号 32 位变量,占用 4 个字节 */
typedef unsigned short int uint16_t;/* 无符号 16 位变量,占用 2 个字节 *//* 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;
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,在这个结构体中,每个成员变量的名字都与寄存器名一一对应。GPIO_TypeDef 结构体成员的地址偏移如下图所示:
C 语言规定,结构体成员在内存中的存储是连续的,因此地址偏移量正好符合 STM32 芯片手册中 GPIO 外设的寄存器映射。例如:
- 假设结构体首地址为 0x4001 0C00(GPIOB 基地址),则 CRL 的地址为 0x4001 0C00,
- CRH 的地址为 0x4001 0C00 + 0x04 = 0x4001 0C04,
- ODR 的地址为 0x4001 0C00 + 0x0C = 0x4001 0C0C,
依此类推。通过这种方式,寄存器映射与结构体偏移完全对应。只需设置好结构体的首地址,即可确定结构体内各成员的地址,从而以结构体形式访问寄存器,使操作更加直观。示例如下,通过结构体指针访问寄存器:
GPIO_TypeDef *GPIOx; // 定义 GPIO_TypeDef 类型的指针
GPIOx = (GPIO_TypeDef *)GPIOB_BASE; // 指针指向 GPIOB 的基地址 (0x40010C00)// 写寄存器
GPIOx->ODR = 0xFFFF; // 设置 GPIOB 所有 IO 输出高电平
GPIOx->BSRR = 0x0001; // 置位 GPIOB 引脚 0 输出高电平,其他位不产生影响// 读寄存器
uint32_t temp;
temp = GPIOx->IDR; // 读取 GPIOB 输入数据寄存器的值
该代码首先定义 GPIO_TypeDef 类型的结构体指针 GPIOx,并将其指向地址: GPIOB_BASE (0x4001 0C00)。然后通过 C 语言结构体访问语法,使用 GPIOx->ODR 和 GPIOx->IDR 等方式进行寄存器读写。
最后,我们进一步优化,直接使用宏,定义指向各 GPIO 端口首地址的 GPIO_TypeDef 类型指针。使用时直接调用宏即可访问寄存器,具体实现见下面代码:
/* 使用 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)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE) /* 直接访问寄存器 */
GPIOB->BSRR = 0xFFFF; // 设置 GPIOB 所有引脚
GPIOB->ODR = 0xFFFF; // 修改输出寄存器
temp = GPIOB->IDR; // 读取输入寄存器GPIOA->BSRR = 0x0001; // 设置 GPIOA 引脚 0
GPIOA->CRL = 0xFFFF; // 修改配置寄存器
这里,我们以 GPIO 外设为例,讲解了 C 语言对寄存器的封装方法。这种方法同样适用于其他外设。值得庆幸的是,固件库已经帮我们完成了这些封装工作。这里我们主要剖析了封装过程,帮助大家既了解实现方式,又理解背后的原理。
10 修改寄存器的位操作方法
在使用 C 语言操作寄存器时,常常需要只修改寄存器的某几位而保持其他位不变。这时可以利用 C 语言的位操作来实现。
10.1 把变量的某位清零
假设我们有一个变量 a 代表寄存器的值,寄存器中已经有一些数值。现在我们想要清零某一位而不影响其他位,例如,对 bit2 清零:1001 1111 → 1001 1011,方法如下:
// 定义一个变量 a,初始值为二进制 1001 1111b(十六进制 0x9F)
unsigned char a = 0x9F;// 对 bit2 清零
a &= ~(1 << 2);// 按步骤解析:
// 括号中的 1 左移两位,(1<<2) 得二进制数:0000 0100 b
// 按位取反,~(1<<2) 得 1111 1011 b
// 假如 a 中原来的值为二进制数:1001 1111 b
// 将取反得到的数与 a 作 & 运算,a = (1001 1111 b)&(1111 1011 b),
// 经过运算后,a 的值 a=1001 1011 b
// a 的 bit2 位被被零,而其它位不变。
左移操作:(1 << 2) 将二进制数 0000 0001b 左移 2 位,得到:
1 << 2 → 0000 0100b
按位取反:对上一步结果取反:
~(1 << 2) → 1111 1011b
按位与运算:将变量 a
与取反后的值进行按位与运算:
a = 1001 1111b & 1111 1011b → 1001 1011b
运算结果:运算后,a
的 bit2 被清零,其他位保持不变。
10.2 把变量的某几个连续位清零
当寄存器中的特定连续位用于控制某个功能时,我们可能需要在不影响其他位的情况下将这些位清零。具体实现方法请参考以下代码:
// 假设寄存器变量 a 已经有值,例如 a = 1001 1111b(0x9F)// 将 a 中的二进制位分组,每组 2 位:
// 第0组 -> bit0、bit1
// 第1组 -> bit2、bit3
// 第2组 -> bit4、bit5
// 第3组 -> bit6、bit7// 对第1组(bit2、bit3)清零
a &= ~(3 << (2 * 1));
/*
位操作解析:
1. (3 << (2 * 1)) -> 0000 1100b// 数字 3(二进制 11b),左移 2*1 位,定位到第1组
2. ~(3 << (2 * 1)) -> 1111 0011b// 按位取反,得到掩码,清零第1组,保留其他位
3. a & ~(3 << (2 * 1)) -> 1001 1111b & 1111 0011b// 运算结果 a = 1001 0011b// 第1组 bit2、bit3 被清零,其他位保持不变
*/// 对第2组(bit4、bit5)清零
a &= ~(3 << (2 * 2));
/*
位操作解析:
1. (3 << (2 * 2)) -> 0011 0000b
2. ~(3 << (2 * 2)) -> 1100 1111b
3. 运算结果 a = 原值 & 1100 1111b// 第2组 bit4、bit5 被清零,其他位保持不变
*/
10.3 对变量的某几位进行赋值
在经过清零操作后,就可以方便地对某几位写入所需数值,而不影响其它位。常见用法是将寄存器的某个参数位设置为特定值。
// 假设变量 a = 1000 0011b// 将清零后的第2组(bit4、bit5)设置为二进制 01b
a |= (1 << (2 * 2));// 运算结果:a = 1001 0011b
// 成功设置了第2组的值,其它组不变
10.4 对变量的某位取反
在某些情况下,需要对寄存器的某个位进行取反操作(1 变 0,0 变 1),可以用异或运算实现:
// 假设变量 a = 1001 0011b// 对 bit6 取反,其它位保持不变
a ^= (1 << 6);// 运算结果:a = 1101 0011b
这个重新组织的版本保持了原文的所有内容,但按照更合理的逻辑顺序进行了排列,应该更适合初学者系统性地学习STM32。
11. 常用参考资料与下载链接
手册类型 | 主要内容 | 说明 | 使用场景 |
---|---|---|---|
参考手册 | 片上每一个外设的功能说明、寄存器详细描述 | 编程时需要反复查阅,了解外设寄存器配置与功能实现 | 编程开发阶段,外设驱动编写、底层调试 |
数据手册 | 功能概览 | 概括芯片功能,选型时首先参考 | 芯片选型阶段,快速了解功能范围 |
引脚说明 | 详细描述每个引脚的功能 | 原理图设计、固件编写,分配 IO 时参考 | |
内存映射 | 各个总线地址及外设分布 | 存储器映射设计,了解外设地址空间 | |
封装特性 | 芯片封装尺寸、引脚排布参数 | PCB 封装设计,绘制封装封图时参考 |
这两个文档都可以从 ST 官方网站 下载。也可下载通过网盘分享的文件:链接: 百度网盘 请输入提取码 提取码: n23p