STM32寄存器编程指南
STM32编程通常有两种方法:一种是寄存器编程,另一种是固件库编程。寄存器编程是基础,固件库编程是在寄存器编程的基础上升级而来,提供了一种易于学习和开发的编程方法。尽管固件库编程对于项目开发更加简单和快速,但从学习的角度出发,掌握寄存器编程也是非常必要的。
在学习寄存器编程时,我们会从STM32芯片的外观开始,逐步深入到寄存器的操作。最终,通过学习,您应该能够用一句话给寄存器下一个定义。
5.1 STM32芯片外观
我们开发板中使用的芯片是100引脚的STM32F103VET6,如图5-1所示。STM32芯片将带领我们进入嵌入式开发的领域。
芯片四周是引脚,左下角的小圆点表示引脚1,然后从引脚1起按照逆时针的顺序排列。开发板中把芯片的引脚引出,连接到各种传感器上,通过编程控制这些引脚输出高电平或者低电平,从而控制各种传感器工作。开发板是一种评估板,板载资源丰富,引脚复用较多,力求在一个板子上验证芯片的全部功能。
5.2 芯片里面有什么
STM32芯片是一个封装好的成品,主要由内核(CPU)和片上外设组成。若与电脑类比,内核与外设的关系类似于电脑上的CPU与主板、内存、显卡、硬盘的关系。
内核与片上外设
STM32F103采用的是Cortex-M3内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如ST、TI、Freescale负责在内核之外设计部件并生产整个芯片。这些内核之外的部件被称为核外外设或片上外设,如GPIO、USART(串口)、I2C、SPI等。具体见下图。
驱动单元和被动单元
芯片(即内核或CPU)和外设之间通过各种总线连接。驱动单元有4个,被动单元也有4个。为了方便理解,可以将驱动单元理解为CPU部分,而将被动单元理解为外设。
驱动单元
-
ICode总线:用于读取指令。编写好的程序编译后变成一条条指令,存放在Flash中,内核通过ICode总线读取这些指令来执行程序。ICode总线几乎每时每刻都在使用,是专门用来取指的。
-
DCode总线:用于读取数据。程序中的数据有常量和变量两种,常量用
const
关键字修饰,放在内部的Flash中;变量放在内部的SRAM中。为了避免数据访问冲突,DCode总线和DMA总线通过总线矩阵来仲裁决定哪个总线取数。 -
System总线:主要用于访问外设的寄存器。我们通常所说的寄存器编程,即读写寄存器,都是通过这根系统总线来完成的。
-
DMA总线:主要用于数据传输。数据可以在某个外设的数据寄存器中,也可以在SRAM中或内部的Flash中。为了避免数据访问冲突,DCode总线和DMA总线通过总线矩阵来仲裁决定哪个总线取数。
被动单元
-
内部闪存存储器:即Flash,存放编写好的程序,内核通过ICode总线来取指令。
-
内部SRAM:即RAM,存放程序的变量和堆栈,内核通过DCode总线来访问。
-
FSMC:即灵活的静态存储器控制器(Flexible Static Memory Controller),是STM32F10xx中一个特色外设。通过FSMC,可以扩展外部的SRAM、NAND Flash和NOR Flash,但不能扩展动态内存如SDRAM。
-
AHB到APB的桥:AHB总线作为一个中央通道连接多个关键组件,从AHB总线延伸出的两条APB2和APB1总线挂载了STM32的各种特色外设,如GPIO、USART、I2C、SPI等。学习STM32的重点是编程这些外设去驱动外部设备。
5.3 存储器映射
在图5-4中,STM32芯片的被动单元(Flash、RAM、FSMC和AHB到APB的桥)这些功能部件共同排列在一个4GB的地址空间内。我们在编程时,可以通过它们的地址找到它们,并操作它们(通过C语言对它们进行数据的读和写)。
存储器本身不具有地址信息,其地址由芯片厂商或用户分配,给存储器分配地址的过程称为存储器映射。如果给存储器再分配一个地址,就叫存储器重映射。
地址空间划分
在这4GB的地址空间中,ARM已经粗略地将其平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见下表:
Block | 起始地址 | 终止地址 | 用途 |
---|---|---|---|
0 | 0x00000000 | 0x1FFFFFFF | 内部Flash |
1 | 0x20000000 | 0x3FFFFFFF | 内部RAM |
2 | 0x40000000 | 0x5FFFFFFF | 片上外设 |
3 | 0x60000000 | 0x7FFFFFFF | 外部RAM |
4 | 0x80000000 | 0x9FFFFFFF | 外部设备 |
5 | 0xA0000000 | 0xBFFFFFFF | 不使用 |
6 | 0xC0000000 | 0xDFFFFFFF | 不使用 |
7 | 0xE0000000 | 0xFFFFFFFF | 系统控制空间 |
在这8个块中,有3个块非常重要,也是我们最关心的3个块:Block0被设计成内部Flash,Block1被设计成内部RAM,Block2被设计成片上的外设。下面我们介绍这3个Block内的具体区域功能划分。
1. 存储器Block0内部区域功能划分
Block0主要用于片内的Flash存储器。我们使用的STM32F103ZET6(霸道)和STM32F103VET6(指南者)的Flash容量都是512KB,属于大容量。内部集成更大的Flash或SRAM意味着芯片成本增加,ST能在追求性价比的同时做到512KB,实属不易。Block0内部区域的功能划分如下表所示:
区域 | 起始地址 | 终止地址 | 大小 | 用途 |
---|---|---|---|---|
主存储区 | 0x08000000 | 0x0807FFFF | 512KB | 用户程序存储 |
系统存储区 | 0x1FFF0000 | 0x1FFF77FF | 30KB | 系统引导加载程序 |
2. 存储器Block1内部区域功能划分
Block1用于片内的SRAM。我们使用的STM32F103ZET6和STM32F103VET6的SRAM容量都是64KB。Block1内部区域的功能划分如下表所示:
区域 | 起始地址 | 终止地址 | 大小 | 用途 |
---|---|---|---|---|
SRAM | 0x20000000 | 0x2000FFFF | 64KB | 数据存储 |
3. 存储器Block2内部区域功能划分
Block2用于片内的外设。根据外设的总线速度不同,Block2被分成了APB和AHB两部分,其中APB又分为APB1和APB2。具体划分如下表所示:
区域 | 起始地址 | 终止地址 | 用途 |
---|---|---|---|
APB1 | 0x40000000 | 0x40007FFF | 低速外设,如定时器、UART |
APB2 | 0x40010000 | 0x40017FFF | 高速外设,如GPIO、SPI |
AHB | 0x40020000 | 0x40023FFF | 高速外设,如DMA控制器、SDIO |
5.4 寄存器映射
存储器映射是给存储器分配地址的过程,那么寄存器映射是给有特定功能的内存单元取别名的过程。寄存器是我们通过编程控制外设的重要接口。
STM32的外设地址映射
片上外设区分为3条总线:APB1、APB2和AHB。APB1挂载低速外设,APB2和AHB挂载高速外设。每条总线的基地址和各外设的基地址如下表所示:
总线 | 基地址 | 说明 |
---|---|---|
APB1 | 0x40000000 | 低速外设 |
APB2 | 0x40010000 | 高速外设 |
AHB | 0x40020000 | 高速外设 |
GPIO外设寄存器
以GPIO外设为例,GPIO是通用输入输出端口,基本功能是控制引脚输出高电平或低电平。以下是GPIOB端口的寄存器列表:
寄存器名 | 地址偏移 | 功能说明 |
---|---|---|
GPIOB_CRL | 0x00 | 配置低8位引脚功能 |
GPIOB_CRH | 0x04 | 配置高8位引脚功能 |
GPIOB_IDR | 0x08 | 输入数据寄存器 |
GPIOB_ODR | 0x0C | 输出数据寄存器 |
GPIOB_BSRR | 0x10 | 位设定/复位寄存器 |
GPIOB_BRR | 0x14 | 位复位寄存器 |
GPIOB_LCKR | 0x18 | 引脚锁定寄存器 |
通过这些寄存器,我们可以控制GPIO引脚的状态,实现对外部设备的控制。了解和熟悉这些寄存器的操作,是进行STM32编程的重要基础。
5.5 C语言对寄存器的封装
为了方便寄存器的操作,我们使用C语言的结构体对寄存器进行封装。
封装总线和外设基地址
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOB_ODR (*(volatile unsigned int *)(GPIOB_BASE + 0x0C))
封装寄存器列表
使用结构体封装GPIO寄存器:
typedef struct {
volatile unsigned int CRL;
volatile unsigned int CRH;
volatile unsigned int IDR;
volatile unsigned int ODR;
volatile unsigned int BSRR;
volatile unsigned int BRR;
volatile unsigned int LCKR;
} GPIO_TypeDef;
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
void set_gpio_high(void) {
GPIOB->ODR = 0xFFFF; // 设置GPIOB所有引脚为高电平
}
5.6 修改寄存器位操作方法
- 清零某一位
a &= ~(1 << n);
- 清零某几个连续位
a &= ~(0xF << n);
- 对某几位进行赋值
a |= (value << n);
- 对某一位取反
a ^= (1 << n);