在学习STM32的时候,我们看到很多的寄存器编程,
比方说LED灯:
//GPIOB.5端口输出高电平
GPIOB->ODR|=1<<5; //PB.5 输出高
GPIOE->ODR|=1<<5; //PE.5输出高
//GPIOB端口全部输出高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
就用到了寄存器,为什么对(0x4001 0C0C) 这个地址写0xFFFF,GPIOB就能输出高电平呢,这些寄存器的本质是什么,比方说GPIO,我们查看下GPIOB
和GPIOE
的定义
在stm32f10x.h里面,可以看到GPIOx
都是由GPIOX_BASE
,宏定义组成
再来看下GPIOX_BASE
分别代表什么:
可以看到GPIOX_BASE
都是由APB2PERIPH_BASE
这个定义加上一定的值组成,APB2我们知道是总线,学过STM32的应该都知道初始化一个外设之前,要先使能外设对应总线的时钟,通过这里我们知道GPIO是在APB2总线上的
再来看一下APB2PERIPH_BASE
是由PERIPH_BASE
加上0x10000 的宏定义,而PERIPH_BASE
的宏定义是0x40000000 也就是说,我们所操作的寄存器,本质就是对不同的地址写值,就可以实现想要的功能。
那么这些地址定义都是代表这什么呢,都是怎么来的呢,这就引出了我们今天的问题----STM32外设寄存器地址-基地址-偏移
STM32芯片基于ARM公司的Cortex-M3内核由ST公司设计生产,ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。内核与总线矩阵之间有ICode System (系统) 、DCode (数据) 三条信号线。内核通过总线矩阵与FLASH、SRAM、外设连接。而外设包括GPIO、USART、12C、SPI等。
STM32F的内核CPU是cortex-M3
指令总线、数据总线、系统总线
ICode 着重传输指令,DCode(Data 数据) 和 System 着重传输数据,至于更详细的区分,不用关心。实际上 ICode、DCode 和 System 内部都包含三个部分,即地址总线、控制总线、数据总线。
DMA Direct Memory Access缩写 直接存储器访问,可将数据从一个地址空间传输到另外一个地址空间,地址空间可以是外设到寄存器或者寄存器到寄存器。访问的数据可以是寄存器,也可以是SRAM,亦可是FLASH;数据可以被DCode和DMA同时访问到,因此为了避免访问冲突,需要总线来仲裁来决定哪个总线访问。
内部的闪存存取器,即FLASH,程序存储在FLASH中,内核通过ICode读取指令。
内部的SRAM,即数据存储器RAM,程序的变量和堆栈开销在SRAM中。
AHB和APB桥:AHB是高性能的系统总线,APB是外设总线。二者分别适用于高速和低速的设备连接。
AHB总线:全称Advanved High Performance Bus 高级高性能系统总线 简写:AHB
APB总线:全称Adanvced Peripheral Bus 高级外设总线 简写:APB
F1分为APB1和APB2 APB1=36MHZ APB2=72MHZ
我们这里以M3内核为例,STM32F1系列以Cortex-M3作为内核, 由于STM32系列芯片地址总线有32根,也就是一次最大可以访问00000000 00000000 00000000 00000000 — 11111111 11111111 11111111 11111111的2^32个地址,范围刚好为4G,可寻址空间为2^32=4GB;
也就是STM32系列最大有4GB的使用空间,下面我们先来看下M3内核的内存映射图。
如图这是M3内核的存储器映射图,从0x00000000到0xFFFFFFFF,一共4G的空间
这个图是ARM官方给出的M3内核图,所有使用Cortex-M3内核的芯片,内部的地址空间基本都是这样用的(ARM官方推荐厂商根据这个设计,厂商也可以自己设计。当然基本上芯片厂商是根据这个设计的)。
再来看下中文的图:
cortex-m3内核把4G空间分成了各个部分
用于存放和运行代码,存储用户下载的代码,还有出厂时固化的一些底层代码
SRAM是运行时临时存放代码的地方。不同类型的STM32单片机的SRAM大小是不一样的,但是他们的起始地址都是0x2000 0000,终止地址都是0x2000 0000+其固定的容量大小。SRAM的理解比较简单,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据,用于程序运行的堆栈开销。设备断电后,SRAM中存储的数据就会丢失。
用于设计片内外设,根据总线速度的不同,被分为了APB和AHB。在stm32中,有三大总线,AHB总线,APB1总线以及APB2总线。不同的外设挂载在不同的总线上边。比如GPIO,串口1,ADC以及部分定时器挂载在APB2总线上。我们通过操作这些外设对应的地址,便能控制这些外设寄存器。
下面看一下STM32官方给出的内存映射图,这个图可以详细的看出来STM32各个部分的映射,注意这个图是STM32F4的图,也就是M4内核的图
可以看到各个部分详细的分类,
映射其实就是对应的意思。事实上存储器本身不具备地址,所以把芯片内核所预先设定好的地址分配给寄存器,就是存储器映射。因为stm32把这个4G的虚拟存储空间和芯片内部外设进行一一对应,每个外设和其对应的寄存器都有一个确定的地址,也就是给存储器分配地址,即存储器映射。
也就是说,ARM给M3内核的使用空间,设计了一个规范,而其本身就是一个4GB的空间,然后其中的一部分用于存储数据(RAM) ,一部分用来运行代码(code)还有一部分被赋予了某种意义(Peripherals),比方说GPIO,TIM定时器,ADC,IIC等等,就成为了片上外设,也就是寄存器 只是理论上的4G范围远远大与实际的存储器空间,也就说实际的存储器空间并没有4G。
我们可以看到在M4内核中,片上外设分配的地址是0x40000000-0x5FFFFFFF 而在上面的例子中,可以看到STM32官方的外设基地址PERIPH_BASE
的宏定义就是是0x40000000,在这部分空间中,不同的外设对应不同的地址。
STM32F10x.h这个头文件正是吧STM32的所有寄存器进行地址映射,此文件通过宏定义的方式,将各个寄存器的地址转换为相应的符号名称,
寄存器以四个字节为一个单元,也就是32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针
的操作方式来访问这些地址单元,对这些地址写入数据,就是对寄存器所对应的外设进行操作。
但是如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,你可以想象一下一个项目几千行地址赋值调用,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
而我们正常使用的寄存器的地址 = 基地址+偏移地址
(1)总线基地址
片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设, APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。APB1 总线的地址最低,因此片上外设就从这这个地址开始,也称外设基地址。
从上图可以看到 APB1 总线基地址是 0x4000 0000,相对外设基地址的偏移量是 0,所以此总线也是外设的基地址。
(2)外设基地址
每条总线上都会挂接着很多的外设,这些外设也会有自己的地址范围, 外设的最低地址就是外设的基地址,也称作边界地址。以 GPIO 外设来讲解外设基地址。其他的外设也是同样分析。
从图可以知道,外设 GPIOx 都是挂接在 APB2 总线上,属于高速的外设,而 APB2 总线的基地址是 0x4001 0000,故 GPIOA 的相对 APB2 总线的地址偏移是 800。
(3)外设寄存器地址
外设的寄存器就分布在其对应的外设地址范围内。这里我们以 GPIO 外设为例,GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,这些寄存器都是按顺序依次排列在外设的基地址上。寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOB 端口为例,来说明 GPIOB都有哪些寄存器,如所示。
这个就回到了我们一开始的那个问题:
```c
//GPIOB.5端口输出高电平
GPIOB->ODR|=1<<5; //PB.5 输出高
//GPIOB端口全部输出高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
``0x4001 0C0C 也就是GPIOB 端口ODR寄存器,一共32位,其中低16位控制这端口的输出(output data) 所以我们对这个地址写FFFF 也就是 1111 1111 1111 1111 就是让GPIOB的所有端口输出高电平
每个寄存器都有一个访问地址,每个外设中的所有寄存器的位置都是固定的,每组寄存器的起始地址在《STM32参考手册》的表1中列明;
寄存器的地址 = 基地址+偏移地址 比如:
整个外设的基地址 = AHB1 的偏移+GPIOB 寄存器组的偏移+GPIOB_OSPEEDR 寄存器的偏移
0x4002 0410 = 0x4000 0000 + 0x0002 0000 + 0x0C00 + 0x10