寄存器的详解
STM32芯片实物
丝印:就是芯片实物上的标注信息
- ST:生产的厂商。
- STM32F103ZET6:此芯片的芯片类型。
- ARM:内核采用ARM公司的内核结构。
- HPAAM93 KOR78528:芯片在哪个地方生产的代表。
芯片引脚的辨别:
芯片的引脚是有方向的,为了便于识别,生产厂家一般都会留有标记,比如芯片上的半圆缺口、圆形凹口、切角或者斜面都是为了识别方向和脚位而设,只要根据标记找出第一脚,然后按照逆时针方向,脚位依次为1、2、3……N脚。
比如此STM32的芯片上的小圆点为1脚,紧接着逆时针转动,脚位依次为1、2、3……144脚。
STM32芯片内部结构
开发板上的STM32 芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内 核与外设就如同电脑上的 CPU 与主板、内存、显卡、硬盘的关系。
STM32F103 采用的是 Cortex-M3 内核,内核即 CPU,由 ARM 公司设计。ARM 公司并不生产芯 片,而是出售其芯片技术授权。芯片生产厂商 (SOC) 如 ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、USART(串 口)、I2C、SPI 等都叫做片上外设。具体见下图 。
芯片(这里指内核,或者叫 CPU)和外设之间通过各种总线连接,其中驱动单元有 4 个,被动单元也有4个。可以把驱动单元理解成是 CPU 部分,被动单元可以理解成外设。STM32的系统框图如下。
● 四个驱动单元:
─ Cortex™-M3内核DCode总线(D-bus),和系统总线(S-bus)
─ 通用DMA1和通用DMA2
● 四个被动单元:
─ 内部SRAM
─ 内部闪存存储器
─ FSMC
─ AHB到APB的桥(AHB2APBx),它连接所有的APB设备
ICode总线:
ICode 中的 I 表示 Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在 FLASH 中,内核要读取这些指令来执行程序就必须通过 ICode 总线,它几乎每时每刻都需要被使用,它是专门用来取FLASH中的指令的。
驱动单元
DCode 总线:
DCode 中的 D 表示 Data,即数据,那说明这条总线是用来取数据的。我们在写程序的时候,数据有常量和变量两种,常量就是固定不变的,用 C 语言中的 const 关键字修饰,是放到内部的 FLASH当中的。变量是可变的,不管是全局变量还是局部变量都放在内部的 SRAM。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数据。
系统总线:
系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
DMA 总线:
DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在 SRAM,也可以在内部的 FLASH。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突, 在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数据。
被动单元
内部的闪存存储器(FLASH) :
内部的闪存存储器即 FLASH,我们编写好的程序就放在这个地方。内核通过 ICode 总线来取FLASH里面的指令。
内部的 SRAM:
内部的 SRAM,即我们通常说的 RAM,程序的变量,堆栈等都是基于内部的SRAM。内核通过DCode 总线来访问它。
FSMC:
FSMC 的英文全称是 Flexible static memory controller,叫灵活的静态的存储器控制器,是 STM32F10xx 中一个很有特色的外设,通过 FSMC,我们可以扩展内存,如外部的 SRAM,NANDFLASH 和 NORFLASH。
但有一点我们要注意的是,FSMC 只能扩展静态的内存,即名称里面的 S:static,不能是动态的内存,比如 SDRAM 就不能扩展。
AHB 到 APB 的桥:
从 AHB 总线延伸出来的两条 APB2 和 APB1 总线,上面挂载着 STM32 各种各样的特色外设。我们经常说的 GPIO、串口、I2C、SPI 这些外设就挂载在这两条总线上。
STM32存储器映射
在STM32系统框图中,被控单元的 FLASH,RAM,FSMC 和 AHB 到 APB 的桥(即片上外设),这些功能部件共同排列在一个 4GB 的地址空间内。在编程的时候,可以通过它们的地址就能够找到他们,然后来操作他们(通过 C 语言对它们进行数据的读和写)。
存储器本身并不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,如果给存储器再分配一个地址就叫存储器重映射。存储器的映射关系如下图。
STM32存储器区域功能划分
存储器本身没有地址,给存储器分配地址的过程叫存储器映射。
在这 4GB 的地址空间中,ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途。每个块的大小都有 512MB,这个存储器的大小已经是很大了,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分 而已。
在这 8 个 Block 里面,有 3 个Block非常重要,也是我们最关心的三个Block。Block0 用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设,下面我们简单的介绍下 这三个 Block 里面的具体区域的功能划分。
存储器 Block0 内部区域功能划分
Block0 主要用于设计片内的 FLASH。FLASH的大小是 512KB ,Block 内部区域的功能划分具体见表格存储器 Block0 内部区域功能划分。
存储器 Block1 内部区域功能划分
Block1 用于设计片内的 SRAM。SRAM 大小是 64KB,Block1内部区域的功能划分具体见表格存储器 Block1 内部区域功能划分 。
存储器 Block2内部区域功能划分
Block2 用于设计片内的外设,根据外设的总线速度不同,Block 被分成了 APB 和 AHB 两部分, 其中 APB 又被分为 APB1 和 APB2,具体见表格存储器 Block2 内部区域功能划分 。
STM32寄存器映射
在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个 单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错。
我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能 的内存单元取别名的过程就叫寄存器映射。
我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x40010C0C(至于这个地址如何 找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个 外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB 的 16 个 IO 都输出高电平。
// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
0x4001 0C0C 在我们看来是 GPIOB 端口 ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针, 即 (unsigned int *)0x4001 0C0C,然后再对这个指针进行 * 操作。
可以给强制类型转换的地址取一个别名,更好的记忆和操作寄存器。
// GPIOB 端口全部输出 高电平
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;
如果把取地址的操作放在寄存器的别名里,则更加容易编程。
// GPIOB 端口全部输出 高电平
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;
TM32外设地址映射
总线基地址:
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。
相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
表格总线基地址的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000 的差值。
外设基地址:
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫 XX 外设的边界地址。
例如GPIO外设是属于高速的外设,挂载到APB2总线 上,外设 GPIO 基地址如表格所示 。
外设寄存器:
在 某一个外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例,GPIO 是通用输入\输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极,LED 灯的阳极接电源,然后通 过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。
GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOB 端口为例,来说明 GPIO 都有哪些寄存器,具体见表格 GPIOB 端口的寄存器地址列表 。
C 语言对寄存器的封装
所谓C语言对寄存器的封装就是在编程上为了方便理解和记忆,把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名。例如下图中的将宏定义对寄存器取别名的操作。
总线和外设基地址宏定义:
/* 外设基地址 */
#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,接着在 PERIPH_BASE 上加入 各个总线的地址偏移,得到 APB1、APB2 总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE, 在其之上加入外设地址的偏移,得到 GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写。
使用指针控制 BSRR 寄存器:
/* 控制 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;
该代码使用 (unsigned int *) 把 GPIOB_BSRR 宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态。
用结构体来封装寄存器的列表:
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;
用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量, 变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节。
定义的这个 GPIO_TypeDef结构体 ,假如这个结构体的首地址为 0x4001 0C00(这也是第一个成员变量 CRL 的地址),那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04 ,加上的这个 0x04,正是代表 CRL 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器。
以下的操作就是通过结构体指针来实现访问寄存器。
GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
GPIOx = GPIOB_BASE; //把指针地址设置为宏 GPIOB_BASE 地址
GPIOx->IDR = 0xFFFF;
GPIOx->ODR = 0xFFFF;
uint32_t temp;
temp = GPIOx->IDR; //读取 GPIOB_IDR 寄存器的值到变量 temp 中
以此类推,可以利用结构体和指针来定义其他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)
#define GPIOH ((GPIO_TypeDef *) GPIOH_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;
uint32_t temp;
temp = GPIOA->IDR; //读取 GPIOA_IDR 寄存器的值到变量 temp 中
如何利用寄存器来进行位操作:
如果使用 C 语言对寄存器赋值时,要求只修改该寄存器的某几位的值,且其它的寄存器位 不变,那么就需要用到C语言的位操作。
1、把变量的某位清零。
以变量 a 代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量 a 的某一位 清零,且其它位不变。
//定义一个变量 a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;
//对 bit2 清零
a &= ~(1<<2);
//括号中的 1 左移两位,(1<<2) 得二进制数:0000 0100 b
//按位取反,~(1<<2) 得 1111 1011 b
//假如 a 中原来的值为二进制数: a = 1001 1111 b
//所得的数与 a 作”位与&”运算,a = (1001 1111 b)&(1111 1011 b),
//经过运算后,a 的值 a=1001 1011 b
// a 的 bit2 位被被零,而其它位不变
2、把变量的某几个连续位清零
寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几 个连续位清零,且其它位不变。
//若把 a 中的二进制位分成 2 个一组
//即 bit0、bit1 为第 0 组,bit2、bit3 为第 1 组,
// bit4、bit5 为第 2 组,bit6、bit7 为第 3 组
//要对第 1 组的 bit2、bit3 清零
a &= ~(3<<2*1);
//括号中的 3 左移两位,(3<<2*1) 得二进制数:0000 1100 b
//按位取反,~(3<<2*1) 得 1111 0011 b
//假如 a 中原来的值为二进制数: a = 1001 1111 b
//所得的数与 a 作”位与&”运算,a = (1001 1111 b)&(1111 0011 b),
//经过运算后,a 的值 a=1001 0011 b
// a 的第 1 组的 bit2、bit3 被清零,而其它位不变。
//上述 (~(3<<2*1)) 中的 (1) 即为组编号; 如清零第 3 组 bit6、bit7 此处应为 3
//括号中的 (2) 为每组的位数,每组有 2 个二进制位; 若分成 4 个一组,此处即为 4
//括号中的 (3) 是组内所有位都为 1 时的值; 若分成 4 个一组,此处即为二进制数“1111 b”
//例如对第 2 组 bit4、bit5 清零 a &= ~(3<<2*2);
3、对变量的某几位进行赋值
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变。
//a = 1000 0011 b
//此时对清零后的第 2 组 bit4、bit5 设置成二进制数“01 b ”
a |= (1<<2*2)
//a = 1001 0011 b,成功设置了第 2 组的值,其它组不变
4、变量的某位取反。
对寄存器的某个位进行取反操作,即 1 变 0 ,0 变 1。
//a = 1001 0011 b
//把 bit6 取反,其它位不变
a ^=(1<<6);
//a = 1101 0011 b
例如让GPIOB的PB0输出低电平,应该进行一下操作。
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOB_ODR *(unsignedint*)(GPIOB_BASE+0x0C)
// PB0输出输出低电平
GPIOB_ODR &= ~(1<<0);
// PB0输出输出高电平
GPIOB_ODR |= (1<<0);