寄存器操作基础
# 寄存器映射
以
STM32F103C8Tx
为例
# 回到起点
这张图很早之前就已经见过了:
但是当时不是很懂存储器映射中一些细节相关的东西,所以今天来详细的探讨一下存储器映射。
很明显,ST 官方将我们的 4GB 的地址空间(32 位 -> 4GB 可以回忆一下)分为了8个 Block,每个都有相应的大致的用途:
可以提一下的是,Block0 用于分配了片上的 FLASH,也就是存储我们指令的地方。
而我们最需要关注的就是 Block2,这个地方是我们的片内外设的地址空间,APB1
, APB2
, AHB
三条总线上挂载的设备对应的寄存器地址都被分配在这里。
# 寄存器映射
# 排列方式
首先,来回忆一下一些基础的东西:
- 1 byte = 8 bit
- 1 字节 = 8 比特
- 1 位16进制 = 4 位二进制
在 STM32 上,寄存器都是(大多数)32 位的,也就是 4 个字节的!
ST 对于寄存器的地址的设计逻辑大概是这样:
- 1个单元
包括
4个字节 - 1个字节
<管辖>
8个bit
也就是讲,寄存器内部的位以 4个一组
为最小单位排列在明面上的存储器地址空间上:
┌----------------------------┐
|寄存器1 0x0000 0000 |
| 0x0000 0001 |
| 0x0000 0002 |
| 0x0000 0003 |
├----------------------------┤
|寄存器2 0x0000 0004 |
| 0x0000 0005 |
| 0x0000 0006 |
| 0x0000 0007 |
└----------------------------┘
2
3
4
5
6
7
8
9
10
11
所以把寄存器排列起来,他们的地址看起来是这样,相邻两个地址之差将会是 4:
这个 4 的差,代表的就是 4 个单元的差距、4 个字节的差距、32 位比特的差距
亘古不变的真理
- 1 个寄存器 --> 32 bit --> 4 byte
- 地址空间上相差 1 代表相差 1 个单元,寄存器内部的相差 8 bit 的位
这对于理解之后的一些操作,例如位带操作非常重要!
总之!对于地址的排列,一定要记住一句话:寄存器内的比特以四个字节为一个单元
马后炮:
寄存器操作添加一句话!!就是对于寄存器的操作总是访问他的最低地址,随后使用
八位十六进制
的方法进行操作,如果只能支持十六位一起操作,则为四位十六进制
数进行移位操作之后得出。不支持单个比特操作,除非是位带操作
# 外设地址映射
# 地址偏移
和偏移相对应的概念是基准,在上面我们可以看到不管是每个 Block、每条总线或者是后面的每个特定的外设,他们都会有一个地址的范围,而这个范围的开头就是它的基地址
而偏移就是在基准的基础上加上的值,加上这个值后,得到的就是我们想要的地址。这两个概念很宽泛,从大到小的范围都存在基地址与偏移来确定某个较为精确的地址
# 确定地址
在大的范围,从 Block2 出发,这个下属的每条总线均有有一个基地址:
在小的范围,每个 GPIO 的外设也会有一个基地址:
上面两个图表的第三栏均为每个设备相对于每个大的范围的基地址的偏移量。
# 外设寄存器
看这张图:
以最常用的对 GPIO 置位与复位的寄存器 BSRR
为例看一下 datasheet:
其中标号②也就是我们的相对于 GPIOB 的偏移量;
③为寄存器位表,需要关注的内容是中间的名称与下方的读写权限:
BRy
是从GPIOx_Pin0~GPIOx+Pin15
的复位寄存器;BSy
是从GPIOx_Pin0~GPIOx+Pin15
的置位寄存器- 读写权限:
w
表示只写,r
表示只读,rw
表示可读写
我们主要关注的是标号④的内容:
BRy
引脚的说明是“0:不会对相应的ODRx
位执行任何操作;1:对相应ODRx
位进行复位”。这里的“复位”是将该位设置为 0 的意思,而“置位”表示将该位设置为 1;说明中的ODRx
是另一个寄存器的寄存器位,我们只需要知道ODRx
位为 1 的时候, 对应的引脚 x 输出高电平,为 0 的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器GPIOx_ODR
的说明了解)。所以,如果对
BR0
写入“1”的话,那么GPIOx
的第 0 个引脚就会输出“低电平”,但是对BR0
写入“0”的话,却不会影响ODR0
位,所以引 脚电平不会改变。要想该引脚输出“高电平”,就需要对BS0
位写入“1”,寄存器位BSy
与BRy
是相反的操作。
# 对寄存器赋值
对上面的例子而言,如果我们想要 BPIOB_Pin5
输出高电平,应该先找到这个引脚的 BSRR
寄存器的地址:
0x4001 0C00 + 0x10 = 0x4001 0C10
这个语句就是:
*(unsigned int*)(0x40010C10) = (0x0001<<5)
要点有:
- 首先要看清楚 datasheet,这些寄存器只支持以 16 位的形式进行操作
- 由于
0x40010C10
单单这个数字放在这里谁也不知道这是啥,所以要强制类型转化为数值指针类型的值 - 随后使用
*
来得到这个指针的值并对它赋值
# 对寄存器的封装
# 每个层级的基地址宏定义
/* 外设基地址 */
#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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这样一来,我们就对从最上层的 Block2 到最底层的每个 GPIOB
的寄存器地址进行了封装,从而不必再记忆每个寄存器的地址,而上面控制 GPIOB_Pin5
的代码也就变成了:
/* 控制GPIOB 引脚0 输出低电平(BSRR 寄存器的BR5 置1) */
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0)); // (16+0) 中的 16 是由于 BR 寄存器组在高 16 位
/* 控制GPIOB 引脚0 输出高电平(BSRR 寄存器的BS0 置1) */
*(unsigned int *)GPIOB_BSRR = 0x01<<0;
unsigned int temp;
/* 读取GPIOB 端口所有引脚的电平(读IDR 寄存器) */
temp = *(unsigned int *)GPIOB_IDR;
2
3
4
5
6
7
8
9
但是如果使用的外设一多,这种方法需要将每个具体的外设的具体的寄存器都定义出来,不是很方便,所以经常使用的是以下方法,也就是大名鼎鼎的标准外设库
# 使用结构体封装寄存器列表
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;
2
3
4
5
6
7
8
9
10
11
12
13
C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节,这么一来:
如果定义一个结构体指针量 GPIO_TyprDef *GPIO_Init
,并将它的地址指定为某个 GPIO 的基地址,结构体内部的存储空间大小刚好按照真实的偏移量定义好了这些寄存器的地址就是真实的寄存器地址,随后就只需要对结构体内的元素进行读写即可达到操控寄存器的目的:
GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef 型结构体指针GPIOx
GPIOx = GPIOB_BASE; //把指针地址设置为宏GPIOH_BASE 地址
GPIOx->IDR = 0xFFFF; // 也就是 1111 1111 1111 1111
GPIOx->ODR = 0xFFFF;
uint32_t temp;
temp = GPIOx->IDR; //读取GPIOB_IDR 寄存器的值到变量temp 中
2
3
4
5
6
7
STM32 的固件库实际上就做了这个工作,而且要比这些要复杂许多,但是思想就是这样