本系列是基于STM32F429野火库进行学习。

3.1 STM32 长啥样?

STM32F429IGT6芯片实物图

1、学会看丝印;
2、懂得如何辨别正方向。

在这里插入图片描述

在这里插入图片描述

3.2 芯片里面有什么?

我们看到的 STM32 芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的 CPU 与主板、内存、显卡、硬盘的关系。
      STM32F429 采用的是 Cortex-M4 内核,内核即 CPU,由 ARM 公司设计。 ARM 公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如 ST、 TI、 Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、 USART(串口)、 I2C、 SPI 等都叫做片上外设。
在这里插入图片描述

芯片和外设之间通过各种总线连接,其中主控总线有 8 条,被控总线有 7 条,具体见图 3-4。主控总线通过一个总线矩阵来连接被控总线, 总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆圈则表示不可以通信。比如 S0: I 总线只有跟 M0、 M2 和 M6 这三根被控总线交叉的时候才有圆圈,就表示 S0 只能跟这三根被控总线通信。从功能上来理解, I 总线是指令总线,用来取指,指令指的是编译好的程序指令。我们知道 STM32 有三种启动方式, 从 FLASH 启动(包含系统存储器),从内部 SRAM 启动,从外部 RAM 启动, 这三种存储器刚好对应的就是 M0、 M2 和M6 这三条总线。
在这里插入图片描述

                                                      图 3-4 STM32F42xxx 和 STM32F43xxx 器件的总线接口

3.3 存储器映射

在图 3-4 中, 连接被控总线的是 FLASH, RAM 和片上外设,这些功能部件共同排列在一个 4GB 的地址空间内。我们在编程的时候,操作的也正是这些功能部件。

3.3.1 存储器映射

存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射。 如果给存储器再分配一个地址就叫存储器重映射

在这里插入图片描述

在这里插入图片描述

下面内容非常重要,务必注意!

3.3.2 寄存器映射:

让GPIOH端口的16个引脚输出高电平,要怎么实现?
通过绝对地址访问内存单元

在这里插入图片描述

1、 0X40021C14 是GPIOH输出数据寄存器ODR的地址,如何找到?
2、 (unsigned int*)的作用是什么?
()—类型强制转换;unsigned int—32位; * 表示指针类型。即32位指针类型。
3、 学会使用C语言的 * 号

通过寄存器别名方式访问内存单元

在这里插入图片描述

为了方便操作,我们干脆把指针操作 * 也定义到寄存器别名里面

在这里插入图片描述

3.3.3 什么是寄存器?
什么是寄存器?

给有特定功能的内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
什么叫存储器映射?
给存储器分配地址的过程叫存储器映射,再分配一个地址叫重映射。

3.4.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.4.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.4.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 对某位进行取反操作

在这里插入图片描述

 关于修改寄存器位的这些操作,在下一章中有应用实例代码,可配合阅读。