STM32F407存储器映射和寄存器映射

1 存储器映射

在说明存储器映射之前,先说明单片机寻址和存储器存储的知识。

例:有一款存储器,它的地址线有19根,数据线有16根,该存储器能够存储多少数据?

分析:地址线有19根,每根地址线有0和1两种状态,这19根地址线能表示2^{19}种状态,所以能够找寻的地址有2^{19}个;那么每个地址里放着几个位呢,这就与数据线的根数有关系了,数据线有16根,说明了该存储器每个地址下存放16个位,即两个字节。2^{19}表示有512K个地址,每个地址有两个字节,所以存储器能够存储1MB的数据。

同样的,对于STM32单片机,它有32根地址线,它的存储单元是按照字节编址的,即数据线有8根,每个地址下存放着一个字节。所以STM32能够寻的地址有_{^{}}^{}2^{32}个,即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个寄存器的地址是排在一起的相对应,这是能够用结构体变量来进行寄存器映射的条件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值