1 存储器映射
在说明存储器映射之前,先说明单片机寻址和存储器存储的知识。
例:有一款存储器,它的地址线有19根,数据线有16根,该存储器能够存储多少数据?
分析:地址线有19根,每根地址线有0和1两种状态,这19根地址线能表示种状态,所以能够找寻的地址有个;那么每个地址里放着几个位呢,这就与数据线的根数有关系了,数据线有16根,说明了该存储器每个地址下存放16个位,即两个字节。表示有512K个地址,每个地址有两个字节,所以存储器能够存储1MB的数据。
同样的,对于STM32单片机,它有32根地址线,它的存储单元是按照字节编址的,即数据线有8根,每个地址下存放着一个字节。所以STM32能够寻的地址有个,即4G个,能够寻址的数据有4GB。寻址范围为0x0000 0000~0xFFFF FFFF。
有了上面的知识,下面说明存储器映射的知识。
存储器映射指的是对存储器分配地址。STM32F4中的存储器映射如下图所示。STM32中将4GB的存储平均分成三个块。块0对应FLASH存储器,存放着一些系统数据和用户代码;块1对应SRAM,是内存区;块2对应着总线外设,APB1、APB2、AHB的外设;块3、4、5对应这FSMC的功能;块6是保留区;块7对应着芯片内核的内部外设。块0、1、2是学习的重点,而块2的总线外设则是学习的重中之重。
2 寄存器映射
通过对于存储器的映射,我们对STM32的4GB的寻址空间进行了分配,最重要的是对于块2中总线外设地址的分配。这部分内容之所以重要,是因为通过对这部分存储器进行写1或者写0可以改变外设的工作状态,因为存储器存储1或者0,本质上也是通过电子电路来实现的,存储1的时候,电子电路中某点为高电平,存储0的时候这一点为低电平,如果将这个高低电平进行放大,就可以作为外设的电平,而块2内控制外设工作状态的原理正是如此。例如:我们把某个IO口对应的存储单元置为1,那么该IO口的输出电平就是1。正是由于这种工作特性,块2的存储器又称为寄存器。
上面是对于寄存器的一种简化的认识,实际中在32单片机中,由于单个外设的工作状态不是单一的,例如IO口可以工作在输出输入等模式,所以一个外设的工作模式、工作状态往往由多个寄存器来控制。32单片机的外设很多,而一个外设又要多个寄存器来控制,所以32单片机的寄存器又更多了,达到上千个。
这上千个寄存器在块2中,寄存器通过存储器映射分配了地址,我们在编程中当然可以通过地址访问寄存器来修改寄存器的值,但是这种方式,非常复杂,代码可读性差。我们将这些地址命名成寄存器的名称,这样通过寄存器名称就可以直接访问寄存器 ,就能够降低编程难度,增强代码可读性。这个命名的过程就叫做寄存器映射。
2.1 寄存器地址
要完成寄存器映射,首先要找到寄存器的地址。总线(AHB、APB)下挂着各个外设、而各个外设下挂着各自的寄存器。总线地址可以通过上图来查到,而外设的地址可以通过下面的图片查到。在手册里我们可以在每个寄存器下查到偏移地址,这个偏移地址是相对于自己外设地址的偏移。因此我们要确定某个寄存器的地址,只要找到这个寄存器属于哪个外设以及这个寄存器的偏移地址,就可以通过外设地址加偏移地址得到寄存器的地址。
2.2 寄存器命名
得到了寄存器的地址,就可以对寄存器进行命名了,即寄存器映射。
寄存器映射方式一:
#define GPIOA_ODR *(unsigned int *)(0x4001 080C)
GPIOA_ODR = 0XFFFF;
注释:(unsigned int *)(0x4001 080C)相当于定义了一个指针,这个指针指向的地址是0x4001 080C,相当于unsigned int *p=0x4001 080C
而*(unsigned int *)(0x4001 080C)则是对取指针指向地址里的数据,相当于*p
#define GPIOA_ODR *(unsigned int *)(0x4001 080C)则是把*p宏定义成GPIOA_ODR
因此GPIOA_ODR就可以表示0x4001 080C这一地址里存放的数据,相当于把这个地址定义成变量GPIOA_ODR,变量里的内容就是0x4001 080C地址里的内容。
这种寄存器映射方式的缺点是需要一个一个的映射,而寄存器可以按照所属的外设来分组,同一外设下的寄存器以一个组的形式来统一映射,可以通过结构体来实现。
寄存器映射方式二:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
//GPIOA_BASE: 0X4001 0800
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
/*&GPIOA->CRL: 0X4001 0800
&GPIOA->CRH: 0X4001 0804
&GPIOA->IDR: 0X4001 0808
&GPIOA->ODR: 0X4001 080C*/
实际应用:
GPIOA-> ODR = 0XFFFF;
注释:
typedef struct { } GPIO_TypeDef;//这里的GPIO_TypeDef不是一个结构体变量名。typedef 用于将为基本数据类型定义新的类型名,这里是定义了一个结构体,这个结构体的结构体名是省略的,因此这条语句表示的意思是将 struct+省略的结构体名定义成GPIO_TypeDef。
结构体里的 __IO是由宏定义#define __IO volatile来的volatile就是不让编译器进行优化,即每次读取或者修改值的时候,都直接存取原始内存地址。volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)注释: GPIOA_BASE表示GPIOA的基地址;(GPIO_TypeDef *)GPIOA_BASE相当于GPIO_TypeDef *p=GPIOA_BASE,即定义了一个结构体指针,这个结构体指针指向的地址是GPIOA_BASE,这样GPIOA_BASE的地址就成了一个结构体变量的首地址,这个结构体变量的类型就是GPIO_TypeDef;#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)这个语句的意思是将指向GPIOA_BASE的一个结构体指针定义成GPIOA
GPIOA-> ODR = 0XFFFF;注释:GPIOA是一个结构体指针,这个结构体指针指向的地址是GPIOA_BASE,因此对从GPIOA_BASE开始的结构体变量的访问采用箭头->。而刚好定义的结构体类型中的几个变量都是32位的,这与GPIOA的7个寄存器都是四个字节向符合;同时结构体变量占据一块连续的存储区域也和7个寄存器的地址是排在一起的相对应,这是能够用结构体变量来进行寄存器映射的条件。