再造STM32---第三部分:什么是寄存器?
2019-06-28 来源:eefocus
本系列是基于STM32F429野火库进行学习。
3.1什么是寄存器?
我们经常说寄存器,那么什么是寄存器?这是我们本章需要讲解的内容,在学习的过程中,大家带着这个疑问好好思考下,到最后看看大家能否用一句话给寄存器下一个定义。
3.2 STM32 长啥样?
我们开发板中使用的芯片是 176pin 的 STM32F429IGT6,具体见图 3-1。这个就是我们接下来要学习的 STM32,它讲带领我们进入嵌入式的殿堂。芯片正面是丝印, ARM 应该是表示该芯片使用的是 ARM 的内核, STM32F429IGT6是芯片型号,后面的字应该是跟生产批次相关,最下面的是 ST 的 LOGO。芯片四周是引脚,左下角的小圆点表示 1 脚,然后从 1 脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,连接到各种传感器上,然后在 STM32 上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作,通过做实验的方式来学习 STM32 芯片的各个资源。开发板是一种评估板,板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。
图 3-1 STM32F429IGT6 实物图
图 3-2 STM32F429IGT6 正面引脚图
3.3 芯片里面有什么?
我们看到的 STM32 芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的 CPU 与主板、内存、显卡、硬盘的关系。
STM32F429 采用的是 Cortex-M4 内核,内核即 CPU,由 ARM 公司设计。 ARM 公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如 ST、 TI、 Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、 USART(串口)、 I2C、 SPI 等都叫做片上外设。具体见图 3-3。
图 5-3 STM32 芯片架构简图
芯片和外设之间通过各种总线连接,其中主控总线有 8 条,被控总线有 7 条,具体见图 3-4。主控总线通过一个总线矩阵来连接被控总线, 总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆圈则表示不可以通信。比如 S0: I 总线只有跟 M0、 M2 和 M6 这三根被控总线交叉的时候才有圆圈,就表示 S0 只能跟这三根被控总线通信。从功能上来理解, I 总线是指令总线,用来取指,指令指的是编译好的程序指令。我们知道 STM32 有三种启动方式, 从 FLASH 启动(包含系统存储器),从内部 SRAM 启动,从外部 RAM 启动, 这三种存储器刚好对应的就是 M0、 M2 和M6 这三条总线。
图 3-4 STM32F42xxx 和 STM32F43xxx 器件的总线接口
3.4 存储器映射
在图 3-4 中, 连接被控总线的是 FLASH, RAM 和片上外设,这些功能部件共同排列在一个 4GB 的地址空间内。我们在编程的时候,操作的也正是这些功能部件。
3.4.1 存储器映射
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见图 3-5。 如果给存储器再分配一个地址就叫存储器重映射。
图 3-5 存储器映射
1. 存储器区域功能划分
在这 4GB 的地址空间中, ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,具体分类见表格 3-1。每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。
表格 3-1 存储器功能分类
在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。 Boock0 用来设计成内部 FLASH, Block1 用来设计成内部 RAM, Block2 用来设计成片上的外设,下面我们简单的介绍下这三个 Block 里面的具体区域的功能划分。
存储器 Block0 内部区域功能划分:
Block0 主要用于设计片内的 FLASH, F429 系列片内部 FLASH 最大是 2MB,我们使用的 STM32F429IGT6 的 FLASH 是 1MB。要在芯片内部集成更大的 FLASH 或者 SRAM都意味着芯片成本的增加,往往片内集成的 FLASH 都不会太大, ST 能在追求性价比的同时做到 1MB 以上,实乃良心之举。 Block 内部区域的功能划分具体见表格 3-2。
表格 3-2 存储器 Block0 内部区域功能划分
储存器 Block1 内部区域功能划分:
Block1 用于设计片内的 SRAM。 F429 内部 SRAM 的大小为 256KB,其中 64KB 的CCM RAM 位于 Block0,剩下的 192KB 位于 Block1,分 SRAM1 112KB, SRAM2 16KB,SRAM3 64KB, Block 内部区域的功能划分具体见表格 3-3。
表格 3-3 存储器 Block1 内部区域功能划分
储存器 Block2 内部区域功能划分:
Block2 用于设计片内的外设,根据外设的总线速度不同, Block 被分成了 APB 和 AHB两部分,其中 APB 又被分为 APB1 和 APB2, AHB 分为 AHB1 和 AHB2,具体见表格 3-4。还有一个 AHB3 包含了 Block3/4/5/6,这四个 Block 用于扩展外部存储器,如 SDRAM,NORFLASH 和 NANDFLASH 等。
表格 3-4 存储器 Block2 内部区域功能划分
下面内容非常重要,务必注意!
3.5 寄存器映射:
我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
比如,我们找到 GPIOH 端口的输出数据寄存器 ODR 的地址是 0x40021C14(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR 寄存器是 32bit,低16bit有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOH的 16 个 IO 都输出高电平,具体见代码 3-1。
代码 3-1 通过绝对地址访问内存单元
0x4002 1C14 在我们看来是 GPIOH 端口 ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int * )0x4002 1C14,然后再对这个指针进行 * 操作。unsigned int是32位的。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见代码 3-2。
代码 3-2 通过寄存器别名方式访问内存单元
为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见代码 3-3。
代码 3-3 通过寄存器别名访问内存单元
3.5.1 STM32 的外设地址映射:
片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设, APB挂载低速外设, AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
1. 总线基地址
表格 3-5 总线基地址:
表格 5-5 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000的差值。关于地址的偏移我们后面还会讲到。
2. 外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫 XX 外设的边界地址。具体有关 STM32F4xx 外设的边界地址请参考《STM32F4xx 参考手册》的 2.3 小节的存储器映射的表 2: STM32F4xx 寄存器边界地址。或者参考《STM32F4xx 参考手册》的存储器映射章节,这两个手册都有详细的讲解。
这里面我们以 GPIO 这个外设来讲解外设的基地址,具体见表格 3-6。
表格 3-6 外设 GPIO 基地址
从表格 3-6 看到, GPIOA 的基址相对于 AHB1 总线的地址偏移为 0,我们应该就可以猜到, AHB1 总线的第一个外设就是 GPIOA。
3. 外设寄存器
在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例, GPIO是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极, LED 灯的阳极接电源,然后通过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOH 端口为例,来说明 GPIO 都有哪些寄存器,具体见表格 3-7。
表格 3-7 GPIOH 端口的 寄存器地址列表
有关外设的寄存器说明可参考《STM32F4xx 参考手册》中具体章节的寄存器描述部分,在编程的时候我们需要反复的查阅外设的寄存器说明。这里我们以“GPIO 端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见图 3-6。
图 3-6 GPIO 端口置位/复位寄存器说明
①名称
寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为 A-I,也就是说这个寄存器说明适用于 GPIOA、 GPIOB 至 GPIOI,这些 GPIO 端口都有这样的一个寄存器。
②偏移地址
偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18,从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4002 0000 ,我们就可以算出GPIOA 的这个 GPIOA_BSRR 寄存器的地址为: 0x4002 0000+0x18 ;同理,由于 GPIOB 的外设基地址为 0x4002 0400,可算出 GPIOB_BSRR 寄存器的地址为: 0x4002 0400+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 是相反的操作。
3.5.2 C 语言对寄存器的封装:
以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用 C 语言控制读写外设寄存器做准备,此处是本章的重点内容。
1. 封装总线和外设基地址
在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名,具体见代码 3-4。
代码 3-4 总线和外设基址宏定义
以下为解释:
第1~3行的依据:
第4~9行的依据:
第10~19行依据:
第20~30行依据:
代码 3-4 首先定义了 “片上外设”基地址 PERIPH_BASE,接着在 PERIPH_BASE 上加入各个总线的地址偏移,得到 APB1、 APB2 等总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到 GPIOA、 GPIOH 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针操作读写了,具体见代码 3-5。
代码 3-5 使用指针控制 BSRR 寄存器
该代码使用 (unsigned int * ) 把 GPIOH_BSRR 宏的数值强制转换成了地址,然后再用“ * ”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态。
以下为GPIOH_BSRR和GPIOH_IDR寄存器的说明:
2. 封装寄存器列表
用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOH 都各有一组功能相同的寄存器,如 GPIOA_MODER/GPIOB_MODER/GPIOC_MODER 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装,具体见代码 3-6。
代码 3-6 使用结构体对 GPIO 寄存器组的封装
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 8 个成员变量,变量名正好对应寄存器的名字。 C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节, 16 位的变量占用 2 个字节,具体见图 3-7。
图 3-7 GPIO_TypeDef 结构体成员的地址偏移
也就是说,我们定义的这个 GPIO_TypeDef , 假如这个结构体的首地址为 0x40021C00(这也是第一个成员变量 MODER 的地址) , 那么结构体中第二个成员变量OTYPER 的地址即为 0x4002 1C00 +0x04 , 加上的这个 0x04 ,正是表 MODER 所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的 BSRR 寄存器分成了低 16 位 BSRRL 和高 16 位 BSRRH, BSRRL 置 1 引脚输出高电平, BSRRH 置 1 引脚输出低电平,这里分开只是为了方便操作。
这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器了,具体见代码 3-7。
代码 3-7 通过结构体指针访问寄存器
这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指向地址GPIOH_BASE(0x4002 1C00),使用地址确定下来,然后根据 C 语言访问结构体的语法,用GPIOx->BSRRL、 GPIOx->MODER 及 GPIOx->IDR 等方式读写寄存器。
最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可,具体代码 3-8。
代码 3-8 定义好 GPIO 端口首地址址针
这里我们仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。
3.5.3 修改寄存器的位操作方法:(!!!重要!!!)
使用 C 语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到 C 语言的位操作方法了。
1. 把变量的某位清零
此处我们以变量 a 代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a 的某一位清零,且其它位不变,方法见代码清单 3-1。
代码清单 3-1 对某位清零
2. 把变量的某几个连续位清零
由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零,且其它位不变,方法见代码清单 3-2。
代码清单 3-2 对某几个连续位清零
3. 对变量的某几位进行赋值。
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变,方法见代码清单 3-3,这时候写入的数值一般就是需要设置寄存器的位参数。
代码清单 3-3 对某几位进行赋值
4. 对变量的某位取反
某些情况下,我们需要对寄存器的某个位进行取反操作,即 1 变 0 , 0 变 1,这可以直接用如下操作,其它位不变,见代码清单 3-4。
代码清单 3-4 对某位进行取反操作
关于修改寄存器位的这些操作,在下一章中有应用实例代码,可配合阅读。