架构图
前言
在进行底层开发时,尤其是C语言,我们时常与暂存器打交道,不过到底暂存器的确切定义是甚麽?有时很难确切定义
有些书将暂存器想像成一排书柜中的特定一格,对这些特殊抽屉,可以将抽屉打开拿取裡面的纸条,也可以把新的纸条放进去。我个人蛮喜欢这个比喻法,但也让我思考,到底能不能用更精准的方式去定义暂存器呢
思考重点
- 暂存器与记忆体映射之间的关联
- 暂存器存在的意义
- 如何查找数据手册
- 编写一个点灯案例
暂存器概念
为了釐清暂存器的概念,我特地找了一块32bits的STM32F4型开发版,核心使用STM429IGT6,其实我们编写程式就是在控制这颗CPU的众多引脚来达到特定需求,例如传感器的输入经过运算后,经由GPIO引脚输出控制
我们可以藉由控制引脚的输出以及输入来达到特定目,。而开发版上的引脚都被分配了一组独一无二的地址位置,透过更改这些地址储存的数值,就可以有效的控制引脚要怎麽输出、如何输出。因此我们可以把引脚当作控制的最基本单位,而暂存器就是引脚背后的控制原理
下图是这次使用的STM32开发版引脚图,它拥有176个引脚
记忆体映射
其实记忆体本身是不具有地址概念的,所谓的地址是由芯片厂商或用户自行规划出来的,也就是说地址的概念其实是我们抽象出来的
那重点来了,要如何知道虚拟地址的范围是多大?
为了方便理解,我们从STM官方网站下载相应的data sheet(我的开发版使用STMF429),从下图可以看出记忆体的映射图范围为0x0000 0000
~0xFFFF FFFF
,总共有4294967296个,也就是4G大小的空间。请注意4G大小并不代表核心版的真实储存大小,而是核心版有能力表示这麽大的空间,这两者是有差别的
4G = 4294967296 = 2^32,简单来说就是处理器的位元数的次方数。STM32F429这个开发版的核心处理器为32位元,处理器裡有很多很多负责存储数据的暂存器,而这些暂存器的长度范围恰好是32bits
我们假设一个长度为32bits的暂存器,它储存了一个整数型态的数据,数值为4,将4转换成二进位制等于0000 0000 0000 0000 0000 0000 0000 0100
,我们分别把这一连串二进位数值存放到0x0000 0000
~0x0000 001F
的地址空间中,这一块32bits长度的连续空间就称为暂存器
由此可知每一个暂存器的起始地址之间存在32bits(4bytes)的差距,起始位置是0x0000 0000
,最大值是0xFFFF FFFF
,这个范围建构了4G大小的寻址空间,官方网站的Memory Mapping就是这麽计算出来的。所以我们把这个位记忆体分配空间的行为称为记忆体映射
暂存器映射
为已经记忆体映射完的记忆体地址命名的过程就称为暂存器映射
暂存器映射的目的在于编写程式时可以用定义好的暂存器名进行操作,而不用每次都调用难懂的16进制,例如下面的这段程式
/* GPIOA 16个引脚都输出高电位 */
*(unsigned int*)(0x40020014) = 0xffff; // 单存操作暂存器地址
#define GPIOA_ODR *(unsigned*)0x40020014
GPIOA_ODR = 0xffff; // 使用暂存器映射
其中使用(unsigned int*)
强制转型的作用是为了让编译器知道它是一个地址类型常数。通常我们为暂存器命名会考虑到它的具体意义,比如GPIOA_ODR代表该GPIO A引脚的通用输出暂存器(Output Data Register),命名尽量便于理解为主
C语言结构封装
假设我们想实现GPIOA的暂存器控制,首先必须知道GPIOA的起始地址,于是参考STM官方网站的reference manual手册中的记忆体映射表可以找到GPIOA的地址范围,下图蓝色方框显示GPIOA的起始地址为0X4002 0000
右侧显示GPIOA位于AHB1高速总线区块上(系统的GPIO引脚都位于此处),透过data sheet的查找发现AHB1被分配到名为Block2的分区内(很明显地片上外设都位于Block2),因此我们可以轻易地将GPIOA身处的地址标示出来,这麽做的目的是为了编写程式时可以进行3个层次的地址偏移
这三种偏移分别是:
- 外设基地址偏移
- 总线基地址偏移
- GPIO基地址偏移
透过查找Memory Table表可以查到外设起始地址、GPIO起始地址以及GPIOA的地址,我们使用嵌套的方式映射这些暂存器地址
/*外设基地址*/
#define PERIPH_BASE ((unsigned int)0x40000000) // 外设起始地址
/*总线基地址*/
#define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000) // GPIO的起始地址
/*GPIO基地址*/
#define GPIO_A_BASE (AHB1_PERIPH_BASE + 0x0000) // GPIOA起始地址
定义偏移地址的好处就是更好的扩充性,比如我今天想要define一个GPIOI地址,只需要将AHB1_PERIPH_BASE+0X2000
就好了,不需要从基地址的暂存器地址开始计算偏移
这三种层次由大到小,使用者只须要依照想要使用的引脚范围进行定义,一旦基地址定义完成,开发者只需要选择距离目标引脚地址最小的偏移量基地址开始定义即可,另一方面这种方式也利于开发者阅读
GPIO端口设有10个暂存器,而且连续储存于GPIOA的连续记忆体空间中。因此我们可以透过自定义一个结构体数据类型来模拟内存空间中的暂存器
typedef unsigned int uint32_t;
typedef struct{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDER;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF
typedef GPIO_TYPEDEF* GPIO_Typedef; // 指向GPIO结构体的指标
我们透过将指标指向GPIO_X
的基地址,使结构体内的成员的地址刚好与各个暂存器对应上,所以当我们对结构体成员操作时,事实上是在操作GPIO对应的暂存器:
GPIO_Typedef GPIO_X;
GPIO_X = GPIOX_BASE;
GPIO_X->MODER = 0X0003;
GPIO_X->OTYPER = 0X0001;
GPIO_X->OSPEEDER = 0X0003;
uint32_t tmp;
tmp = GPIO_X->IDR; // 读取暂存器
查找手册
查找完手册上对应的外设地址,然后利用程式编写暂存器映射的阵列指标后,我