<>什么是寄存器?
我们现在在开发STM32时,已经很少用到寄存器编程,更多的使用ST公司所提供的标准库和最新的HAL库进行编程实现,但是不管是标准库还是HAL库都是在原来的寄存器层面上进行了封装,知道寄存器是什么,还是很重要的,了解寄存器的原来,对我们使用标准库和HAL库也是有很大的帮助。我下面会以STM32F103VET6为例,解释寄存器到底是什么?
在STM32编程,实际上就是通过程序控制这些引脚输出高低电平来控制各种传感器工作
下图是STM32F103VET6的引脚分布图
我们常见的STM32芯片是已经封装好的成品,主要是由内核和片上外设组成,就像电脑中的CPU和主板、内存、显卡、硬盘的关系。
STM32F103采用的是Cortex-M3内核,内核就是CPU,它由ARM公司设计,ST公司生产,ST负责设计内核之外的整个芯片,这些内核之外的部件,被称为片上外设。例如GPIO、USART、I2C、SPI等都叫做片上外设
上图是STM32芯片架构简图
内核和各种外设之间是通过各种总线连接,听到总线,应该不少人听过电脑中的南桥北桥总线,其实我们STM32中也是一样的,STM32中的驱动单元是4个,被动单元也是4个,我们把驱动单元当成CPU的一部分,同样可以将被动单元当成外设,下面我将介绍驱动单元和被动单元的各个部件。
<>ICode总线
I代表Instruction,即命令,我们程序编译后都是一条条指令,存于FLASH中,内核通过ICode总线来读取这些指令,进而根据这些指令执行程序,它是专门用来读取指令的。
<>驱动单元
<>DCode总线
D是数据Data,即数据,说明这条总线是用来读取数据的,我们知道数据分为常量和变量,常量是固定不变的,在C语言中常量是用const关键字修饰的,它存放在FLASH中,变量是可变的,不管是局部变量还是全局变量,都存放在SRAM中,数据可以被DCode总线和DMA总线访问,为了避免冲突,在读取的时候需要一个总线矩阵来进行总裁,决定哪个总线在读取数据。
<>系统总线
系统总线主要是用于访问外设的寄存器,我们经常说的寄存器编程,即读写寄存器,都是通过这根系统总线来完成的。
<>DMA总线
DMA总线主要用于传输数据,数据可以是某个外设的数据寄存器,也可以是SRAM,也可以是内部的FLASH,因为数据可以被 Dcode 总线和 DMA
总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
<>被动单元
<>内部的闪存存储器
即FALSH,用于存放编写好的程序,内核通过ICode总线来取里面的指令
<>内部的SRAM
就是我们常说的RAM,程序的变量,以及堆栈的开销都是基于SRAM的。内核通过DCode总线来访问它
<>FSMC
全称,灵活的静态的存储器控制器,也是STM32的一个外设,通过该外设我们可以扩展内存,如外部的SRAM等,但是FSMC只能扩展静态的内存,像动态内存SDRAM就不能扩展
<>AHB到APB的桥
从AHB总线延伸出来的两条APB2和APB1总线,上面挂在这STM32的各种各样的外设,就是我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上,我们学习STM32,就是学会利用编程这些外设去驱动外部的各种设备。
STM32F10XX系统框图
在上图中,被控单元的FLASH,RAM,FSMC和AHB到APB2的桥,这些功能部件共同排列在一个4GB的地址空间中,在编程时,我们可以通过它们的地址找到它们,然后操作。
<>存储器映射
存储器本身不具有地址信息,它的地址是由芯片厂商或者用户分配的,给存储器分配地址的过程就称为存储器映射。如果给存储器再分配一个地址就叫做存储器重映射。
在这4GB的地址空间中,ARM被粗细线条分成了8个块,每块512M,每块都规定了用途,芯片厂商在每个块的范围内设计外设时并不一定用得完,都只是用了其中的一部分。
<>寄存器映射
铺垫了那么多,是为了更好的理解寄存器是什么,简而言之寄存器就是内存,我们都知道,寄存器本身没有地址,给寄存器分配地址的过程叫寄存器映射,那么什么是寄存器映射呢?
在存储器中的某片区域,设计的是片上外设,它们以4个字节为一个单元,共32bit,每一个单元对应不同的功能,我们通过控制这些单元就可以驱动外设工作。我们可以先找到每个单元的起始地址,然后通过C语言的指针的操作方式来访问这些单元,但是每次都要通过地址的方式访问,难记忆也容易出错,于是,我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们常说的寄存器,给已经分配好地址的有特定功能的内存单元取别名的过程就叫做寄存器映射。
比如,我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x4001 0C0CODR ,寄存器是 32bit,低 16bit有效,对应着 16
个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB 的 16 个 IO 都输出高电平
// GPIOB 端口全部输出 高电平 *(unsigned int*)(0x4001 0C0C) = 0xFFFF;
0x4001 0C0C 在我们看来是 GPIOB 端口 ODR
的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int
*)0x4001 0C0C,然后再对这个指针进行 * 操作。
刚才我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作
// GPIOB 端口全部输出 高电平 #define GPIOB_ODR //(unsigned int*)(GPIOB_BASE+0x0C) *
GPIOB_ODR= 0xFF;
为了操作方便,我们也直接把指针操作的“*”也定义到寄存器中
// GPIOB 端口全部输出 高电平 #define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR= 0xFF;
<>STM的外设地址映射
片上外设分为三条线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2和AHB挂在高速外设。对应总线的最低地址称为总线的基地址,总线基地址也是挂载在这个总线上的首个外设的地址,其中APB1总线地址最低,片上外设从这里开始,也叫做外设基地址
<>外设寄存器
在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例, GPIO是通用输入输出端口的简称,简单来说就是 STM32
可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极, LED 灯的阳极接电源,然后通过
STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为
32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描
述。这里我们以 GPIOB 端口为例,来说明 GPIO 都有哪些寄存器
这里以端口置位/复位寄存器为例
(GPIOx_BSSR)(x=A…E)这段的意思是该寄存器为GPIOx_BSSR其中的“x”可以是A-E,也就是这个寄存器适用于GPIOA、GPIOB至GPIOE,所有GPIO端口都有这样的一个寄存器
偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18,从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4001
0800 ,我们就可以算出GPIOA 的这个 GPIOA_BSRR 寄存器的地址为: 0x4001 0800+0x18 ;同理,由于 GPIOB
的外设基地址为 0x4001 0C00,可算出 GPIOB_BSRR 寄存器的地址为: 0x4001 0C00+0x18 。其他 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:不会对相应的 ODRx 位执行任何操作; 1:对相应 ODRx位进行复位”。这里的“复位”是将该位设置为 0
的意思,而“置位”表示将该位设置为
1;说明中的 ODRx 是另一个寄存器的寄存器位,我们只需要知道 ODRx 位为 1 的时候,对应的引脚 x 输出高电平,为 0
的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器 GPIOx_ODR 的说明了解)。所以,如果对 BR0 写入“ 1”的话,那么 GPIOx
的第0 个引脚就会输出“低电平”,但是对 BR0 写入“ 0”的话,却不会影响 ODR0 位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对“
BS0”位写入“ 1”,寄存器位BSy 与 BRy 是相反的操作
<>封装总线和外设基地址
在编程上为了方便记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以它们的名字作为宏名
首先定义了 “片上外设”基地址 PERIPH_BASE,接着在 PERIPH_BASE 上
加入各个 总线 的地址 偏移, 得到 APB1、 APB2 总线 的地址 APB1PERIPH_BASE
APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到 GPIOA-G
的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写。
该代码使用 (unsigned int ) 把 GPIOB_BSRR 宏的数值强制转换成了地址,然后再用
“”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态
用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器,如
GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C
语言中的结构体语法对寄存器进行封装,
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量,变量名正好对应寄存器的名字。 C
语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节, 16 位的变量占用 2 个字节
也就是说,我们定义的这个 GPIO_TypeDef , 假如这个结构体的首地址为 0x4001
0C00( 这也是第一个成员变量 CRL 的地址) , 那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04 , 加上的这个
0x04 ,正是代表 CRL 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移
最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可
这里只是以GPIO这个外设为例,讲解了C语言对寄存器的封装,以此类推,其他外设也可以用这种方法来封装,不过现在这部分工作都由固件库帮我们完
成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然
本文主要参考了野火的资料