5.1. 寄存器的概念
寄存器(Register)是单片机内部一种特殊的内存,它可以实现对单片机各个功能的控制,简单的来说可以把寄存器当成一些控制开关,控制包括内核及外设的各种状态。无论是 51单片机还是 STM32单片机,都需要用寄存器来实现各种控制,以完成不同的功能。寄存器是连接软件和硬件的桥梁。
寄存器资源非常宝贵,一般都是一个位或者几个位控制一个功能,对于 STM32 来说,其寄存器是 32 位的,一个 32 位的寄存器,可能会有 32 个控制功能,相当于 32 个开关,由于STM32的复杂性,它内部有几百个寄存器,所以整体来说 STM32 的寄存器还是比较复杂的。 STM32 是由于内部有很多外设,所以导致寄存器很多,实际上我们把它分好类,每个外设也就那么几个或者几十个寄存器。
寄存器可以分为两类,内核寄存器和外设寄存器。如图5.1-1.
图5.1-1 STM32寄存器分类
其中,内核寄存器,我们一般只需要关心中断控制寄存器和 SysTick 寄存器即可,其他三大类,我们一般很少直接接触。而外设寄存器,则是学到哪个外设,就了解哪个外设相关寄存器即可,所以整体来说,我们需要关心的寄存器并不是很多,而且很多都是有共性的,我们只需要学习了其中一个的相关寄存器,其他个基本都是一样。
给大家举个简单的例子,我们知道寄存器的本质是一个特殊的内存,对于STM32 来说,以 GPIOB 的 ODR 寄存器为例,其寄存器地址为:0X40010C0C,所以我们对其赋值可以写成:
(*(unsigned int *))(0X40010C0C) = 0XFFFF;
|
这样我们就完成了对 GPIOB->ODR 寄存器的赋值,0XFFFF表示GPIOB所有I/O口(16个I/O口)都输出高电平,0X40010C0C 就是一个寄存器的特殊地址。
5.2. 存储器映射
STM32是一个32位单片机,他可以访问(2^32 = 4GB)4GB以内的存储空间。在前面章节介绍过STM32F10xx 系统框图,如下图5.2-1.被控单元有FLASH,RAM,FSMC 和AHB 到APB 的桥(即片上外设),这些功能部件共同排列在一个4GB 的地址空间内。我们可以通过C语言来访问这些地址空间,从而操作相关外设(读/写)。数据字节以小端格式(小端模式)存放在存储器中,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
图5.2-1-STM32F1系列系统结构图
存储器本身是没有地址信息的,我们对存储器分配地址的过程就叫存储器映射。这个分配一般由芯片厂商做好了,ST将所有的存储器及外设资源都映射在一个4GB的地址空间上(8个块),从而可以通过访问对应的地址,访问具体的外设。其映射关系如图5.2-2所示:
图5.2-2 STM32存储器映射图
如果给存储器再分配一个地址就叫存储器重映射。
存储器重映射是将已经映射过的存储器再次映射的过程。它可以使同一物理存储单元映射多个不同的逻辑地址。在这个过程中,存储单元会被再分配一个地址,这样存储单元就有了两个地址,用户可以通过这两个地址来访问该存储单元。存储器重映射的目的是为了快速响应中断或者快速完成某个任务,可以将同一地址段映射到不同速度的两个存储块,然后将低速存储块中的代码段复制到高速存储块中,对低速存储块的访问将被重映射为对高速存储块的访问。这种技术通过改变中断向量映射关系,使得系统能够更高效地处理数据和任务。
存储器本身不具有地址信息,其地址是由芯片厂商或用户分配的,给存储器分配地址的过程就称为存储器映射。对于具体的某款嵌入式芯片,它包含的各种存储器的大小、地址分布都是确定的。存储器映射是对各种存储器的大小和地址分布的规划,而存储器重映射则是在此基础上进行的进一步操作。
存储器重映射是一种优化系统性能的技术手段,通过对存储器的重新映射,可以提高系统的响应速度和任务处理效率。
5.2.1 存储器区域功能划分
ST将4GB空间分成8个块,每个块512MB,如下图5.2-3所示,从图中我们可以看出有很多保留区域(Reserved),这是因为一般的芯片制造厂家是不可能把4GB空间用完的,同时,为了方便后续型号升级,会将一些空间预留(Reserved)。
图5.2-3 STM32 存储块功能及地址范围
在这8 个Block 里面,有3 个块非常重要,也是我们最关心的三个块。Block0 用来设计成内部FLASH,Block1 用来设计成内部RAM,Block2 用来设计成片上的外设,下面我们简单的介绍下这三个Block 里面的具体区域的功能划分。
5.2.2 存储器Block0 内部区域功能划分
图5.2-4 存储块0的功能划分
Block 0,用于存储代码,即FLASH空间,其功能划分如上图5.2-4所示。图示用户FLASH大小是512KB,这是属于大容量的STM32F103x,如STM32F103ZET6,其他型号,如32F103C8T6则远没有这么多。理论上ST也可以推出更大容量的STM32F103单片机,因为这里保留了一大块地址空间。STM32的出厂固化BootLoader非常精简,整个BootLoder只占了2KB FLASH空间。
BootLoader:嵌入式系统的BootLoader,也称为引导加载程序,是嵌入式系统在加电后执行的第一段代码。它负责在系统启动时初始化硬件设备,并将操作系统映像或固化的嵌入式应用程序装载到内存中,然后跳转到操作系统所在的空间,启动操作系统运行。BootLoader的主要功能包括初始化处理器和周边电路、建立内存空间映射图、加载操作系统映像或用户应用程序等,为最终调用操作系统内核或执行用户应用程序准备好正确的环境。
在嵌入式系统中,BootLoader的代码通常是由开发人员编写,并针对特定的硬件平台进行优化。它通常存储在系统的非易失性存储器中,如闪存或EEPROM,以确保在系统上电后能够被访问和执行。
总之,嵌入式系统的BootLoader是确保系统能够正常启动和运行的关键组件,它负责初始化硬件、加载操作系统或应用程序,并为系统的正常运行提供必要的环境。
5.2.3 储存器Block1 内部区域功能划分
图5.2-5 STM32 存储块1的功能划分
Block 1,用于存储数据,即SRAM空间,其功能划分如图5.2-5所示 ,图示为大容量产品,也仅用了64KB( 如STM32F103ZET6,STM32F103C8T6则只有20KB),用于SRAM访问,同时也有大量保留地址用于扩展。
5.2.4 储存器Block2 内部区域功能划分
图5.2-6 STM32 存储块2的功能划分
Block 2,用于外设访问,STM32内部大部分的外设都是放在这个块里面的,该存储块里面包括了AHB、APB1和APB2三个总线相关的外设,其中AHB和APB2是高速总线(72MHZ),APB1是低速总线(36MHZ)。其功能划分如图5.2-6所示. 同样可以看到,各个总线之间,都有预留地址空间,方便后续扩展。关于STM32各个外设具体挂在哪个总线上面,大家可以参考前面的 STM32F103系统结构图和STM32F103存储器映射图进行查找对应。
5.3.寄存器映射
给存储器分配地址的过程叫存储器映射,寄存器是一类特殊的存储器,它的每个位都有特定的功能,可以实现对外设/功能的控制,给寄存器的地址命名的过程就叫寄存器映射。
举例说明,我们的纸质笔记本就好比通用存储器,用来记录数据是完全没问题的,但是不会有具体的动作,只能做记录使用。而我们家中的电闸开关,就好比寄存器了,如下图家中电闸箱有8个熔断器控制着8处用电设备(相当于一个8位寄存器),这些熔断器开关也可以记录状态,同时还能让对应用电器开/关,是会产生具体动作的。同时我们也可以通过读取这些熔断器的状态了解一些用电器是否存在问题,比如某些熔断器一直处于断开的状态,可能是对应的用电器存在短路等问题。为了方便区分和使用,我们会给每个开关命名,如厨房开关、大厅开关、卧室开关等,给开关命名的过程,就是寄存器映射。
图5.3-1 寄存器作用举例(家中电闸开关)
STM32内部的寄存器有非常多,远远不止8个开关这么简单,但是原理是差不多的,每个寄存器的每一个位,一般都有特定的作用,涉及到寄存器描述,可以参考《STM32F10x参考手册》对应章节的寄存器描述部分,有详细的描述。
在存储器Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起
始地址,然后通过C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
比如,我们找到GPIOB端口的输出数据寄存器ODR 的地址是0x40010C0C(这个地址如何找到的后面我们会讲),ODR 寄存器是32bit,低16bit 有效,对应着16 个外部IO,写0/1 对应的IO 则输出低/高电平。现在我们通过C 语言指针的操作方式,让GPIOB的16 个IO 都输出高电平:
-
-
*(unsigned int*)(0x40010C0C) = 0xFFFF;
|
0x40010C0C 在我们看来是GPIOB 端口ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4001 0C0C,然后再对这个指针进行* 操作。
通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器别名的方式来操作,见代码如下代码: (其中GPIOB_BASE 指的是GPIOB 端口的基地址,稍后会讲)。
-
-
#define GPIOB_ODR (unsigned int *)(GPIOB_BASE+0x0C)
-
|
为了方便操作,我们直接把指针操作“*”也定义到寄存器别名里面,具体见如下代码:
-
-
#define GPIOB_ODR *(unsigned int *)(GPIOB_BASE+0x0C)
-
|
5.3.1 STM32 的外设地址映射
5.3.1.1 寄存器地址计算
具体某个寄存器地址,由三个参数决定:
1、总线基地址(BUS_BASE_ADDR);
2,外设基于总线基地址的偏移量(PERIPH_OFFSET);
3,寄存器相对外设基地址的偏移量(REG_OFFSET)。
可以表示为:
寄存器地址 = BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET
5.3.1.2 总线基地址
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速外设,APB2 和AHB 挂载高速外设。
相应总线的最低地址我们称为该总线的基地址(BUS_BASE_ADDR),总线基地址也是挂载在该总线上的首个外设的地址。其中APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。 如前面图5.2-6所示,提取见下图。
图5.3-2 外设总线基地址
“相对外设基地址偏移”即该总线地址与“片上外设”基地址0x4000 0000 的差值。
5.3.1.3 外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫XX 外设的边界地址。具体有关STM32F10xx 外设的边界地址请参考《STM32F10xx
参考手册》的存储器映射图。
这里面我们以GPIO 这个外设来讲解外设的基地址,GPIO 属于高速的外设,挂载到APB2 总线上,具体见图5.3-3。最后一列就是外设基于总线基地址的偏移量(PERIPH_OFFSET)。
图5.3-3 GPIO外设基地址
5.3.1.4 外设寄存器
在XX 外设的地址范围内,分布着的就是该外设的寄存器。以GPIO 外设为例,GPIO 是通用输入输出端口的简称,简单来说就是STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO 的引脚连接到LED 灯的阴极,LED 灯的阳极接电源,然后通
过STM32 控制该引脚的电平,从而实现控制LED 灯的亮灭。
GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以
GPIOB 端口为例,来说明GPIO 都有哪些寄存器,具体见图5.3-4。图中的偏移量,就是寄存器基于外设基地址的偏移量(REG_OFFSET)。
图5.3-4 GPIOB 端口的寄存器地址列表
5.3.2 寄存器描述解读
这里以“GPIO 端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见图5.3-5.
图5.3-5 GPIO 端口置位/复位寄存器说明
① 寄存器名称
寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A⋯E)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为A-E,也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOE,这些GPIO 端口都有这样的一个寄存器,一些低端的芯片可能没有这么多端口,一些高端的芯片也可能比这些端口要多,但意思都是一样的。
② 偏移地址
偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x10,从参
考手册中我们可以查到GPIOA 外设的基地址为0x4001 0800 ,我们就可以算出GPIOA 的这个GPIOA_BSRR 寄存器的地址为:0x4001 0800+0x10 ;同理,由于GPIOB 的外设基地址为x4001
0C00,可算出GPIOB_BSRR 寄存器的地址为:0x4001 0C00+0x10 。其他GPIO 端口以此类推即可。
③ 寄存器位表
紧接着的是本寄存器的位表,表中列出它的0-31 位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w 表示只写,r 表示只读,rw 表示可读写。本寄存器中的
位权限都是w,所以只能写,如果读本寄存器,是无法保证读取到它真正内容的。而有的寄存器位只读,一般是用于表示STM32 外设的某种工作状态的,由STM32 硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。
④ 位功能说明
位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为BRy 及BSy,其中的y 数值可以是0-15,这里的0-15 表示端口的引脚号,如BR0、BS0 用于控制GPIOx 的第0 个引脚,若x 表示GPIOA,那就是控制GPIOA 的第0 引脚,而BR1、BS1 就是控制GPIOA 第1 个引脚。
其中BRy 引脚的说明是“0:不会对相应的ODRy 位执行任何操作;1:对相应ODRy 位进行复
位”。这里的“复位”是将该位设置为0 的意思,而“置位”表示将该位设置为1;说明中的ODRy是另一个寄存器的寄存器位,我们只需要知道ODRy 位为1 的时候,对应的引脚y 输出高电平,为0 的时候对应的引脚输出低电平即可。所以,如果对BR0 写入“1”的话,那么GPIOx 的第0 个引脚就会输出“低电平”,但是对BR0 写入“0”的话,却不会影响ODR0 位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位BSy 与BRy 是相反的操作。
5.3.3 STM32库函数的寄存器映射实现
STM32F103所有寄存器映射都在stm32f103xe.h里面完成,包括各种基地址定义、结构体定义、外设寄存器映射、寄存器位定义等,整个文件有1W多行,非常庞大。我们没有必要对该文件进行全面分析,因为很多内容都是相似的,我们只需要知道寄存器是如何被映射的,就可以了。以上所有的关于存储器映射的内容,最终都是为大家更好地理解STM32库函数如何用C 语言控制读写外设寄存器做准备。下面是实现过程:
5.3.3.1 封装总线和外设基地址
在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名,具体见如下代码:
-
-
#define PERIPH_BASE ((unsigned int)0x40000000)
-
-
#define APB1PERIPH_BASE PERIPH_BASE
-
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
-
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)
-
-
#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 GPIOB_CRL (GPIOB_BASE+0x00)
-
#define GPIOB_CRH (GPIOB_BASE+0x04)
-
#define GPIOB_IDR (GPIOB_BASE+0x08)
-
#define GPIOB_ODR (GPIOB_BASE+0x0C)
-
#define GPIOB_BSRR (GPIOB_BASE+0x10)
-
#define GPIOB_BRR (GPIOB_BASE+0x14)
-
#define GPIOB_LCKR (GPIOB_BASE+0x18)
|
上述代码首先定义了“片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE 上加入各个总线的地址偏移,得到APB1、APB2 总线的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写,具体见代码:
-
-
*(unsigned int *)GPIOB_BSRR = 0x01<<16;
-
-
*(unsigned int *)GPIOB_BSRR = 0x01<<0;
-
-
-
temp = *(unsigned int *)GPIOB_IDR;
|
该代码使用(unsigned int *) 把GPIOB_BSRR 宏的数值强制转换成了地址,然后再用“*”号做取
指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32 外设的状态。
5.3.3.2 封装寄存器列表
用上面的方法去定义地址,还是稍显繁琐,例如GPIOA-GPIOE 都各有一组功能相同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入C 语言中的结构体语法对寄存器进行封装,如下代码所示:
-
typedef unsigned int uint32_t;
-
typedef unsigned short int uint16_t;
-
-
-
-
-
-
-
-
-
-
|
这段代码用typedef 关键字声明了名为GPIO_TypeDef 的结构体类型,结构体内有7个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4 个字节,16 位的变量占用2 个字节,具体见图5.3-6.GPIO_TypeDef 结构体成员的地址偏移。 5.3-6.GPIO_TypeDef 结构体成员的地址偏移
也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x4001 0C00(这也是第一个成员变量CRL 的地址),那么结构体中第二个成员变量CRH 的地址即为0x4001 0C00 +0x04,加上的这个0x04,正是代表CRL 所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已注明。
这样的地址偏移与STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,具体见代码如下:
这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指向地址GPIOB_BASE(0x4001 0C00),地址确定下来,然后根据C 语言访问结构体的语法,用GPIOx->ODR 及GPIOx->IDR 等方式读写寄存器。
最后,我们更进一步,直接使用宏定义好GPIO_TypeDef 类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可,具体代码:
-
-
#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)
-
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
这里我们仅是以GPIO 这个外设为例,给大家讲解了C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只
是分析了下这个封装的过程,让大家知其然,也知道其所以然。
|