转自:
https://blog.csdn.net/wjgwrr/article/details/72848506
一、存储器映射与重映射
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给物理存储器分配逻辑地址的过程就称为存储器映射,通过这些逻辑地址就可以访问到相应的存储器的物理存储单元。如果给存储器再分配一个地址就叫存储器重映射。
如STM32,对于片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
二、通过绝对地址访问内存单元
对于外设的访问实际就是对内存地址的访问。在C语言里,即指针操作。为了增加阅读性,往往采用如下的宏定义。
#define MACRO_BASE 0x4001 0C0C
#define MACRO_NAME (*(volatile unsigned int *)MACRO_BASE)
1.首先看MACRO_BASE这仅仅是一个立即数,一数值。
2.(volatile unsigned int *)MACRO_BASE:这里将进行强制类型转换,把它转换成指针,这里即地址。
需要注意的是有两点:
A)用unsigned:禁止算数移位,采用逻辑移位,因为嵌入式编程大量采用移位操作。如果用带符号,会形成算术位移,即最高位符号不参与移位,这是错误的。
B)用volatile:禁止编译器对变量访问优化。即源码中有多少读写操作,编译后生产多少机器操作指令。
3.第一个*的作用,对这个指针的解引用。取这个地址对应的内容。
这一长串就相当于一个变量了。
详细说明volatile关键字:
在 C 语言中该关键字用于表示变量是易变的,确保本条指令不会因C编译器的优化而被省略。且要求每次直接读值。例如用while((unsigned char *)0x20)时,有时系统可能不真正去读0x20的值,而是用第一次读出的值,如果这样,那这个循环可能是个死循环。用了volatile则要求每次都去读0x20的实际值。
volatile 类型是这样的,其数据确实可能在未知的情况下发生变化。比如,硬件设备的终端更改了它,现在硬件设备往往也有自己的私有内存地址,比如显存,他们一般是通过映象的方式,反映到一段特定的内存地址当中,这样,在某些条件下,程序就可以直接访问这些私有内存了。另外,比如共享的内存地址,多个程序都对它操作的时候。你的程序并不知道,这个内存何时被改变了。如果不加这个volatile修饰,程序是利用catch当中的数据,那个可能是过时的了,加了volatile,就在需要用的时候,程序重新去那个地址去提取,保证是最新的。归纳起来如下:
1. volatile变量可变允许除了程序之外的比如硬件来修改他的内容 2.访问该数据任何时候都会直接访问该地址处内容,即通过cache提高访问速度的优化被取消
三、在STM32中的应用
#define __IO volatile
/**
* @brief General Purpose I/O
*/
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;
#define FLASH_BASE ((uint32_t)0x08000000)
#define SRAM_BASE ((uint32_t)0x20000000)
#define PERIPH_BASE ((uint32_t)0x40000000)
#define SRAM_BB_BASE ((uint32_t)0x22000000)
#define PERIPH_BB_BASE ((uint32_t)0x42000000)
#define FSMC_R_BASE ((uint32_t)0xA0000000)
/*!< Peripheral memory map */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define AFIO_BASE (APB2PERIPH_BASE + 0x0000)
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400)
#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)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
详细说明:
外设寄存器结构体定义仅仅是一个定义,要想实现给这个结构体赋值就达到操作寄存器的效果,我们还需要找到该寄存器的地址,就把寄存器地址跟结构体的地址对应起来。
这些结构体内的成员,都代表着寄存器,每个结构体成员前增加了一个“__IO”前缀,它的原型代表了C 语言中的关键字“volatile”,而寄存器很多时候是由外设或STM32 芯片状态修改的,也就是说即使CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求CPU 去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。
定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址强制类型转换成相应的外设寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通过该指针可以直接操作该外设的全部寄存器。
最终通过强制类型转换把外设的基地址转换成 GPIO_TypeDef类型的结构体指针,然后通过宏定义把 GPIOA、GPIOB等定义成外设的结构体指针,通过外设的结构体指针我们就可以达到访问外设的寄存器的目的。
例子:GPIOB->ODR 等价于 (*(volatile unsigned int *)0x40010C0C)