寄存器重映射与固件库封装原理

寄存器重映射与固件库封装原理

我们已经知道,芯片内部有着内核、存储器,存储器又分成8个区,8个区上的Block2分布着四条总线,分别是AHB1、AHB2和APB1、APB2。根据外设所需的速度不同,分别挂载在不同的总线上,然后在外设所分配的地址范围内,又分布着该外设的各种寄存器,那么当我们需要改变外设的状态时,就需要使用这些寄存器。

我们找到 GPIOF 端口的输出数据寄存器 ODR 的地址是 0x4002 1414寄存器重映射与固件库封装原理_第1张图片
通过查阅我们知道,ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。那么我们现在要使GPIOF的16个引脚都输出高电平,我们就要通过绝对地址来访问它。

*(unsigned int*)(0x4002 1414) = 0xFFFF  //使GPIOF的十六个引脚均为高电平

0x4002 1414 在我们看来是 GPIOF 端口数据输出寄存器 ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4002 1414,然后再对这个指针进行 *操作

这种操作其实是非常麻烦的,每次都要写下这个寄存器的绝对地址再操作,而且F4外设这么多,这么多寄存器,都这么写的话,不得疯掉吗?

所以我们需要更简便的方法来使我们的编程效率得到提高
也就是寄存器重映射(就是给寄存器地址起名字)

来点简单点的,还是上面那个例子,我们使GPIOF的16个引脚均为高电平

#define GPIOF_ODR   *(unsigned int*)(0x4002 1414) 
//为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面
GPIOF_ODR=0xFFFF

那么现在我们再需要操作GPIOF的引脚状态时,就可以直接用这个寄存器的别名GPIOF_ODR来操作就行了,这样就省去了我们很多麻烦。

那么,下面我们规范地来进行一下寄存器的重映射。
a.我们知道,每条总线都挂载着不同的外设,那么我们要进行外设的寄存器重映射,就要先找到每条总线的基地址

/* 外设基地址 */
2 #define PERIPH_BASE ((unsigned int)0x40000000)
3 /* 总线基地址 */
4 #define APB1PERIPH_BASE PERIPH_BASE
5 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
6 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
7 #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)

我们知道Block2的首地址就是APB1的地址,那么其它总线的偏移地址加上APB1的偏移地址就是该总线的地址了,偏移地址也就是相对于Block2,该总线偏移了多少。

b.找到总线的基地址后,外设是挂载在总线上的,所以我们只需要用该总线的基地址加上该外设的偏移地址就是这个外设的基地址了

8 /* GPIO 外设基地址 */
9 #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
10 #define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
11 #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
12 #define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
13 #define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
14 #define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
15 #define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
16 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)

c.在外设的分布地址范围内,又分布着该外设的不同寄存器,该外设的基地址加上寄存器的偏移地址就是该寄存器的基地址了

17/* 寄存器基地址,以 GPIOF 为例 */
18 #define GPIOF_MODER (GPIOF_BASE+0x00)
19 #define GPIOF_OTYPER (GPIOF_BASE+0x04)
20 #define GPIOF_OSPEEDR (GPIOF_BASE+0x08)
21 #define GPIOF_PUPDR (GPIOF_BASE+0x0C)
22 #define GPIOF_IDR (GPIOF_BASE+0x10)
23 #define GPIOF_ODR (GPIOF_BASE+0x14)
24 #define GPIOF_BSRR (GPIOF_BASE+0x18)
25 #define GPIOF_LCKR (GPIOF_BASE+0x1C)
26 #define GPIOF_AFRL (GPIOF_BASE+0x20)
27 #define GPIOF_AFRH (GPIOF_BASE+0x24)

现在我们再去使用外设,是不是就很方便了,编程效率提高了很多,但是这样还是会稍显繁琐,例如 GPIOA~GPIOH 都各有一组功能相同的寄存器,GPIOA_MODER/GPIOB_MODER/GPIOC_MODER 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址

为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装。

1 typedef unsigned int uint32_t; /*无符号 32 位变量*/
2 typedef unsigned short int uint16_t; /*无符号 16 位变量*/
3
4 /* GPIO 寄存器列表 */
5 typedef struct {
6 uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */
7 uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */
8 uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */
9 uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */
10 uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */
11 uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */
12 uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */
13 uint16_t BSRRH; /*GPIO 置位/复位寄存器高 16 位部分 地址偏移: 0x1A */
14 uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */
15 uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */
16 } GPIO_TypeDef;

这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 8 个成员变量,变量名正好对应寄存器的名字。 C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节, 16 位的变量占用 2 个字节。寄存器重映射与固件库封装原理_第2张图片
也就是说, 假如我们定义一个 GPIO_TypeDef 类型的结构体, 且结构体的首地址0x40021400(这也是第一个成员变量 MODER 的地址) , 那么结构体中第二个成员变量OTYPER 的地址即0x4002 1400 +0x04 , 加上的这个 0x04 ,正是代表 MODER 所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移。这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移(也是四个字节的偏移)一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器了

1 GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
2 GPIOx = GPIOF_BASE; //把指针地址设置为宏 GPIOF_BASE 地址
3 GPIOx->BSRRL = 0xFFFF; //通过指针访问并修改 GPIOF_BSRRL 寄存器
4 GPIOx->MODER = 0xFFFFFFFF; //修改 GPIOF_MODER 寄存器
5 GPIOx->OTYPER =0xFFFFFFFF; //修改 GPIOF_OTYPER 寄存器
6
7 uint32_t temp;
8 temp = GPIOx->IDR; //读取 GPIOF_IDR 寄存器的值到变量 temp 中

这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指GPIOF_BASE(0x4002 1400),使地址确定下来,然后根据 C 语言访问结构体的语法,用GPIOx->BSRRLGPIOx>MODER 及 GPIOx->IDR 等方式读写寄存器。

最后,我们更进一步,直接使用宏来定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可

1 /*使用 GPIO_TypeDef 把地址强制转换成指针*/
2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
9 #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
10
11
12
13 /*使用定义好的宏直接访问*/
14 /*访问 GPIOF 端口的寄存器*/
15 GPIOF->BSRRL = 0xFFFF; //通过指针访问并修改 GPIOF_BSRRL 寄存器
16 GPIOF->MODER = 0xFFFFFFF; //修改 GPIOF_MODER 寄存器
17 GPIOF->OTYPER =0xFFFFFFF; //修改 GPIOF_OTYPER 寄存器
18
19 uint32_t temp;
20 temp = GPIOF->IDR; //读取 GPIOF_IDR 寄存器的值到变量 temp 中
21
22 /*访问 GPIOA 端口的寄存器*/
23 GPIOA->BSRRL = 0xFFFF; //通过指针访问并修改 GPIOA_BSRRL 寄存器
24 GPIOA->MODER = 0xFFFFFFF; //修改 GPIOA_MODER 寄存器
25 GPIOA->OTYPER =0xFFFFFFF; //修改 GPIOA_OTYPER 寄存器
26
27 uint32_t temp;

这里仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里只是分析了下这个封装的过程,让大家知其然,也只其所以然。

你可能感兴趣的:(STM32F4的个人理解)