5. 什么是寄存器

本章参考资料:《STM32F76xxx参考手册》、《STM32F767规格书》。

学习本章时,配合《STM32F76xxx参考手册》“存储器和总线架构”、“嵌入式FLASH接口”及“通用I/O(GPIO)”章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。

5.1. 什么是寄存器

在学习STM32编程时,通常有两种编程方式,一种是寄存器编程,另外一种是函数库编程,其中寄存器编程是基础, 而函数库编程是在寄存器编程的基础上升级而来的一种易于学习和开发的编程方式,是我们学习STM32编程的时候需要重点掌握的一种编程方法。 函数库编程对于项目开发固然简单和快速,但是从学习的角度开发,学习寄存器编程的方法我们也不能丢。其实,我们在学习8位或者16位单片机的时候, 居多都是采用寄存器编程,那么这个寄存器编程里面的寄存器是什么东西?它在芯片的什么地方?在探索STM32寄存器深层次的问题时, 我们从STM32表面的外观开始,一层层的往里面深入学习。最后,在学习完本章内容之后, 看看大家能否用一句话给寄存器下一个定义。

5.2. STM32长啥样

以我们的F767-挑战者开发板为例,F767-挑战者开发板中使用的芯片是176pin的STM32F767IGT6,具体见 图5_1。 这个就是我们接下来要学习的STM32,它将带领我们进入嵌入式的殿堂。

芯片正面是丝印,ARM应该是表示该芯片使用的是ARM的内核,STM32F767IGT6是芯片型号,后面的字应该是跟生产批次相关,最下面的是ST的LOGO。

芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。 开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器芯片), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作,通过做实验的方式来学习STM32芯片的各个资源。

开发板是一种评估板,板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。

image1

图 5‑1 STM32F767IGT6 实物图(图中方框部分的芯片)

image2

图 5‑2 STM32F767IGT6正面引脚图

5.3. 芯片里面有什么

我们看到的STM32芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。

STM32F767、STM32H743、STM32H750 采用的都是Cortex-M7内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。 芯片生产厂商(SOC) 如ST、TI、NXP等,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。具体见 图5_3

image3

图 5‑3 STM32芯片架构简图

芯片主系统架构基于两个子系统,一个是AXI转多层AHB桥,多层AHB总线矩阵。

AXI转多层AHB桥,从AXI4协议转成AHB-Lite协议, 其中包含3个AXI转32-bit AHB桥通过32-bit的AHB总线矩阵连接到外部存储器FMC接口、 外部存储器Quad SPI接口、内部SRAM(SRAM1 and SRAM2)。 还包含一个AXI转64-bit AHB桥通过64-bit总线矩阵连接到内部FLASH。

多层AHB总线矩阵,其中32-bit多层AHB总线矩阵互联11个主设备和8个从设备,64-bit多层AHB总线矩阵则是CPU通过AXI转AHB桥通过这个64-bit多层AHB总线矩阵连接到内部Flash。 DMA主设备通过32-bit AHB总线矩阵通过这个64-bit多层AHB总线矩阵连接到内部Flash。具体见 图5_4。 主控总线通过一个总线矩阵来连接被控总线,总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。 总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆圈则表示不可以通信。

图 5_4  STM32F42xxx 和 STM32F43xxx 器件的总线接口

5.4. 存储器映射

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

5.4.1. 存储器映射

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

图 5_5 STM32F767存储器映射

图 5‑5 存储器映射(摘自参考手册-存储器映射章节)

5.4.2. 存储器区域功能划分

在这4GB的地址空间中,ARM已经粗线条的平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表格 5‑1。每个块的大小都有512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。

表格 5‑1 存储器功能分类

序号

用途

地址范围

Block 0

SRAM

0x0000 0000 ~ 0x1FFF FFFF(512MB)

Block 1

SRAM

0x2000 0000 ~ 0x3FFF FFFF(512MB)

Block 2

片上外设

0x4000 0000 ~ 0x5FFF FFFF(512MB)

Block 3

FMC的bank1 ~ bank2

0x6000 0000 ~ 0x7FFF FFFF(512MB)

Block 4

FMC的bank3 ~ bank4

0x8000 0000 ~ 0x9FFF FFFF(512MB)

Block 5

FMC

0xA000 0000 ~ 0xCFFF FFFF(512MB)

Block 6

FMC

0xD000 0000 ~ 0xDFFF FFFF(512MB)

Block 7

Cortex-M7内部外设

0xE000 0000 ~ 0xFFFF FFFF(512MB)

在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设,下面我们简单的介绍下这三个Block里面的具体区域的功能划分。

5.4.2.1. 存储器Block0内部区域功能划分

Block0是存放程序代码的区域。 用户也可以在此处放置数据。通过ITCM或AXIM接口执行指令提取和数据访问。 Block内部区域的功能划分具体见表格 存储器Block0内部区域功能划分

表格 5‑2 存储器Block0 内部区域功能划分

用途说明

地址范围

Block0

预留

0x1FFF 0020 ~ 0x1FFF FFFF

16个字节用于锁定对应的OTP数据块。

0x1FFF 0000 ~ 0x1FFF 001F

预留

0x0820 0000 ~ 0x1FFE FFFF

FLASH:我们的程序就放在这里。

0x0800 0000 ~ 0x081F FFFF

预留

0x0030 0000 ~ 0x07FF FFFF

FLASH存储基于ITCM总线接口,不支 持写操作,即只读。

0x0020 0000 ~ 0x003F FFFF

预留

0x0011 0000 ~ 0x001F FFFF

系统存储器:里面存的是ST出厂时烧写好的 ISP自举程序,用户无法改动。串口下载的 时候需要用到这部分程序。

0x0010 0000 ~ 0x0010 EDBF

预留

0x0000 4000 ~ 0x000F FFFF

ITCM RAM,只能被CPU访问,不用经过总线矩 阵,属于高速的RAM。

0x0000 0000 ~ 0x0000 3FFF

5.4.2.2. 储存器Block1内部区域功能划分

Block1用于设计片内的SRAM。用户也可以在这里进行编码。通过DTCM或AXIM接口执行指令提取和数据访问。 F767 内部SRAM的大小为512KB,分SRAM1 368KB,SRAM2 16KB,DTCM 128KB。 H743内部跟H750一样,SRAM的大小为868KB,分SRAM1 128KB,SRAM2 128KB,SRAM3 32KB, SRAM4 64KB以及预留了4KB的SRAM。 Block内部区域的功能划分具体见表格 5-3。

表格 5‑3 存储器Block1 内部区域功能划分

用途说明

地址范围

Block1

预留

0x2008 0000 ~ 0x3FFF FFFF

SRAM2 16KB

0x2007 C000 ~ 0x2007 FFFF

SRAM1 368KB

0x2002 0000 ~ 0x2007 BFFF

DTCM 128KB

0x2000 0000 ~ 0x2001 FFFF

5.4.2.3. 储存器Block2内部区域功能划分

Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2, 具体见表 5-4存储器Block2 内部区域功能划分。还有一个AHB3包含了Block3/4/5,AHB3包含的3个Block用于扩展外部存储器, 如SRAM,NORFLASH和NANDFLASH等。

表格 5‑4 H767存储器Block2 内部区域功能划分

用途说明

地址范围

Block2

APB1 总线外设

0x4000 0000 ~ 0x4000 7FFF

预留

0x4000 7800 ~ 0x4000 FFFF

APB2 总线外设

0x4001 0000 ~ 0x4001 6BFF

预留

0x4001 5800 ~ 0x4001 FFFF

AHB1 总线外设

0x4002 0000 ~ 0x4007 FFFF

预留

0x4008 0000 ~ 0x4FFF FFFF

AHB2 总线外设

0x5000 0000 ~ 0x5006 0BFF

预留

0x5006 0C00 ~ 0x5FFF FFFF

H743跟H750的存储器Block2 内部区域功能划分

用途说明

地址范围

Block2

APB1 总线外设

0x4000 0000 ~ 0x4000 D3FF

预留

0x4000 D400 ~ 0x4000 FFFF

APB2 总线外设

0x4001 0000 ~ 0x4001 77FF

预留

0x4001 7800 ~ 0x4001 FFFF

AHB1 总线外设

0x4002 0000 ~ 0x400B FFFF

预留

0x400C 0000 ~ 0x4801 FFFF

AHB2总线外设

0x4802 0000 ~ 0x4802 2BFF

预留

0x4802 2C00 ~ 0x4FFF FFFF

APB3总线外设

0x5000 0000 ~ 0x5000 3FFF

预留

0x5000 4000 ~ 0x50FF FFFF

AHB3总线外设

0x5100 0000 ~ 0x5200 8FFF

预留

0x5200 9000 ~ 0x57FF FFFF

APB4总线外设

0x5800 0000 ~ 0x5800 6BFF

预留

0x5800 6C00 ~ 0x5801 FFFF

AHB4总线外设

0x5802 0000 ~ 0x5802 67FF

预留

0x5802 6800 ~ 0x5FFF FFFF

5.5. 寄存器映射

我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?

在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

比如,我们找到GPIOH端口的输出数据寄存器ODR的地址是0x4002 1C14或0x5802 1C14(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR寄存器是32bit,低16bit有效,对应着16个外部IO,写0/1对应的的IO则输出低/高电平。现在我们通过C语言指针的操作方式, 让GPIOH的16个IO都输出高电平,具体见代码 代码5_1

代码 5‑1 通过绝对地址访问内存单元

  // GPIOB 端口全部输出 高电平

  *(unsigned int*)(0x4002 1C14) = 0xFFFF;  //H767

  *(unsigned int*)(0x5802 1C14) = 0xFFFF;  //H743繁星/H750/H743_pro/H743挑战者

0x40021C14 或 0x58021C14在我们看来是GPIOH端口数据输出寄存器ODR的地址,但是在编译器看来,这只是一个普通的变量, 是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针, 即(unsigned int *)0x40021C14或 *(unsigned int *)0x58021C14,然后再对这个指针进行 * 操作。

刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器别名的方式来操作,具体见 代码5_2

代码 5‑2 通过寄存器别名方式访问内存单元

 // GPIOH 端口全部输出 高电平
 #define GPIOH_ODR              (unsigned int*)(GPIOH_BASE+0x14)
 *GPIOH_ODR = 0xFF;

为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见 代码5_3

代码 5‑3 通过寄存器别名访问内存单元

 // GPIOH 端口全部输出 高电平
 #define GPIOH_ODR              *(unsigned int*)(GPIOH_BASE+0x14)
 GPIOH_ODR = 0xFF;

5.5.1. STM32的外设地址映射

片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB挂载低速外设,AHB挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。

5.5.1.1. 总线基地址

表格 5‑5 总线基地址

H767 H743挑战者系列

总线名称

总线基地址

相对外设基地址的偏移

APB1

0x4000 0000

0x0

APB2

0x4001 0000

0x0001 0000

AHB1

0x4002 0000

0x0002 0000

AHB2

0x4802 0000

0x0802 0000

APB3

0x5000 0000

0x1000 0000

AHB3

0x5100 0000

0x1100 0000

APB4

0x5800 0000

0x1800 0000

AHB4

0x5802 0000

0x1802 0000

H743繁星 H743_pro H750系列

总线基地址

表 5-5总线基地址中的“相对外设基地址偏移”即该总线地址与“片上外设”基地址0x4000 0000的差值。关于地址的偏移我们后面还会讲到。

5.5.1.2. 外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。 具体有关STM32F7xx外设的边界地址请参考《STM32F76xx数据手册》 的第4章节的存储器映射的表 Table 13.

这里面我们以GPIO这个外设来讲解外设的基地址。

H767 外设GPIO基地址

外设名称

外设基地址

相对AHB1总线的地址偏移

GPIOA

0x4002 0000

0x0

GPIOB

0x4002 0400

0x0000 0400

GPIOC

0x4002 0800

0x0000 0800

GPIOD

0x4002 0C00

0x0000 0C00

GPIOE

0x4002 1000

0x0000 1000

GPIOF

0x4002 1400

0x0000 1400

GPIOG

0x4002 1800

0x0000 1800

GPIOH

0x4002 1C00

0x0000 1C00

H743繁星/H750/H743_pro/H743挑战者 外设GPIO基地址

外设名称

外设基地址

相对AHB1总线的地址偏移

GPIOA

0x5802 0000

0x0

GPIOB

0x5802 0400

0x0000 0400

GPIOC

0x5802 0800

0x0000 0800

GPIOD

0x5802 0C00

0x0000 0C00

GPIOE

0x5802 1000

0x0000 1000

GPIOF

0x5802 1400

0x0000 1400

GPIOG

0x5802 1800

0x0000 1800

GPIOH

0x5802 1C00

0x0000 1C00

GPIOI

0x5802 2000

0x0000 2000

GPIOJ

0x5802 2400

0x0000 2400

GPIOK

0x5802 2800

0x0000 2800

从表 5-6外设GPIO基地址看到,GPIOA的基址相对于AHB1总线的地址偏移为0,我们应该就可以猜到,AHB1总线的第一个外设就是GPIOA。

5.5.1.3. 外设寄存器

在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO外设为例,GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚, 基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED灯的阴极,LED灯的阳极接电源,然后通过STM32控制该引脚的电平,从而实现控制LED灯的亮灭。

GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列, 寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOF端口为例,来说明GPIO都有哪些寄存器, 具体见表 图5_6

H767 GPIOB端口的 寄存器地址列表

寄存器名称

寄存器地址

相对GPIOB基址的偏移

GPIOF_MODER

0x4002 1400

0x00

GPIOF_OTYPER

0x4002 1404

0x04

GPIOF_OSPEEDR

0x4002 1408

0x08

GPIOF_PUPDR

0x4002 140C

0x0C

GPIOF_IDR

0x4002 1410

0x10

GPIOF_ODR

0x4002 1414

0x14

GPIOF_BSRR

0x4002 1418

0x18

GPIOF_LCKR

0x4002 141C

0x1C

GPIOF_AFRL

0x4002 1420

0x20

GPIOF_AFRH

0x4002 1424

0x24

H743繁星/H750/H743_pro/H743挑战者 GPIOB端口的 寄存器地址列表

寄存器名称

寄存器地址

相对GPIOH基址的偏移

GPIOH_MODER

0x5802 1C00

0x00

GPIOH_OTYPER

0x5802 1C04

0x04

GPIOH_OSPEEDR

0x5802 1C08

0x08

GPIOH_PUPDR

0x5802 1C0C

0x0C

GPIOH_IDR

0x5802 1C10

0x10

GPIOH_ODR

0x5802 1C14

0x14

GPIOH_BSRR

0x5802 1C18

0x18

GPIOH_LCKR

0x5802 1C1C

0x1C

GPIOH_AFRL

0x5802 1C20

0x20

GPIOH_AFRH

0x5802 1C24

0x24

有关外设的寄存器说明可参考《STM32F7xxxx参考手册》中具体章节的寄存器描述部分,在编程的时候我们需要反复的查阅外设的寄存器说明。

这里我们以“GPIO端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见 图5_6

image6

图 5‑6 GPIO端口置位/复位寄存器说明

  • ①名称

H767的寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为A-I, 也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOI,这些GPIO端口都有这样的一个寄存器。

H743繁星/H750/H743_pro/H743挑战者系列的寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…K)”这段的意思是该寄存器名 为“GPIOx_BSRR”其中的“x”可以为A-K,也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOK,这些GPIO端口都有这样的一个寄存器。

  • ②偏移地址

H767的偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x18,从参考手册中我们可以查到GPIOA外设的基地址为0x4002 0000 , 我们就可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x4002 0000+0x18 ; 同理,由于GPIOB的外设基地址为0x4002 0400, 可算出GPIOB_BSRR寄存器的地址为:0x4002 0400+0x18 。其他GPIO端口以此类推即可。

H743繁星/H750/H743_pro/H743挑战者系列的偏移地址是指本寄存器相对于这个外设的基地址的偏移。 本寄存器的偏移地址是0x18,从参考手册中我们可以查到GPIOA外设的基地址为0x5802 0000 , 我们就可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x5802 0000+0x18 ; 同理,由于GPIOB的外设基地址为0x5802 0400,可算出GPIOB_BSRR寄存器的地址为: 0x5802 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是相反的操作。

5.5.2. C语言对寄存器的封装

以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用C语言控制读写外设寄存器做准备,此处是本章的重点内容。

5.5.2.1. 封装总线和外设基地址

在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来, 总线或者外设都以他们的名字作为宏名,具体见 代码5_4

代码 5‑4 H767的总线和外设基址宏定义

/* 外设基地址 */
#define PERIPH_BASE           ((unsigned int)0x40000000)

/* 总线基地址 */
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)

/* GPIO外设基地址 */
#define GPIOA_BASE            (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE            (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE            (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE            (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE            (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE            (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE            (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE            (AHB1PERIPH_BASE + 0x1C00)

/* 寄存器基地址,以GPIOF为例 */
#define GPIOF_MODER             (GPIOF_BASE+0x00)
#define GPIOF_OTYPER            (GPIOF_BASE+0x04)
#define GPIOF_OSPEEDR           (GPIOF_BASE+0x08)
#define GPIOF_PUPDR             (GPIOF_BASE+0x0C)
#define GPIOF_IDR               (GPIOF_BASE+0x10)
#define GPIOF_ODR               (GPIOF_BASE+0x14)
#define GPIOF_BSRR              (GPIOF_BASE+0x18)
#define GPIOF_LCKR              (GPIOF_BASE+0x1C)
#define GPIOF_AFRL              (GPIOF_BASE+0x20)
#define GPIOF_AFRH              (GPIOF_BASE+0x24)

代码 5-4首先定义了 “片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE上加入各个总线的地址偏移, 得到APB1、APB2、AHB1和AHB2总线的基地址APB1PERIPH_BASE、APB2PERIPH_BASE,AHB1PERIPH_BASE和AHB2PERIPH_BASE。 然后在AHB1总线基地址上加上GPIO外设的地址偏移,得到GPIOA~GPIOH的外设基地址,最后在外设基地址上加入各寄存器的地址偏移, 得到特定寄存器的地址。一旦有了具体地址,就可以用指针进行读写操作,具体 代码5_6

代码 H743繁星/H750/H743_pro/H743挑战者系列 总线和外设基址宏定义

/*片上外设基地址  */
#define PERIPH_BASE           ((unsigned int)0x40000000)
/*总线基地址 */
#define D3_AHB1PERIPH_BASE    (PERIPH_BASE + 0x18020000)
/*GPIO外设基地址*/
#define GPIOH_BASE            (D3_AHB1PERIPH_BASE + 0x1C00)

/* GPIOH寄存器地址,强制转换成指针 */
#define GPIOH_MODER       *(unsigned int*)(GPIOH_BASE+0x00)
#define GPIOH_OTYPER      *(unsigned int*)(GPIOH_BASE+0x04)
#define GPIOH_OSPEEDR     *(unsigned int*)(GPIOH_BASE+0x08)
#define GPIOH_PUPDR       *(unsigned int*)(GPIOH_BASE+0x0C)
#define GPIOH_IDR         *(unsigned int*)(GPIOH_BASE+0x10)
#define GPIOH_ODR         *(unsigned int*)(GPIOH_BASE+0x14)
#define GPIOH_BSRRL       *(unsigned int*)(GPIOH_BASE+0x18)
#define GPIOH_BSRRH       *(unsigned int*)(GPIOH_BASE+0x1A)
#define GPIOH_LCKR        *(unsigned int*)(GPIOH_BASE+0x1C)
#define GPIOH_AFRL        *(unsigned int*)(GPIOH_BASE+0x20)
#define GPIOH_AFRH        *(unsigned int*)(GPIOH_BASE+0x24)

代码5_5 首先定义了 “片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE上加入各个总线的地址偏移, 得到总线地址D3_AHB1PERIPH_BASE,在其之上加入外设地址的偏移,得到GPIOA、GPIOH的外设地址, 最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。 一旦有了具体地址,就可以用指针操作读写了,具体见代码 代码5_6

代码 5‑6 使用指针控制BSRR寄存器

 /* 控制GPIOH 引脚10输出低电平(BSRR寄存器的BR10置0) */
 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));

 /* 控制GPIOH 引脚10输出高电平(BSRR寄存器的BS10置1) */
 *(unsigned int *)GPIOH_BSRR = 0x01<<10;

 unsigned int temp;
 /* 控制GPIOH 端口所有引脚的电平(读IDR寄存器) */
 temp = *(unsigned int *)GPIOH_IDR;

该代码使用 (unsigned int *)把GPIOH_BSRR宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值, 从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32外设的状态。

5.5.2.2. 封装寄存器列表

用上面的方法去定义地址,还是稍显繁琐,例如GPIOA~GPIOH都各有一组功能相同的寄存器, 如GPIOA_MODER/GPIOB_MODER/GPIOC_MODER等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。 为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装,具体见 代码5_7

代码 5‑7 使用结构体对GPIO寄存器组的封装

typedef unsigned           int uint32_t; /*无符号32位变量*/
typedef unsigned short     int uint16_t; /*无符号16位变量*/

/* GPIO寄存器列表 */
typedef struct {
 uint32_t MODER;    /*GPIO模式寄存器             地址偏移: 0x00      */
 uint32_t OTYPER;   /*GPIO输出类型寄存器          地址偏移: 0x04      */
 uint32_t OSPEEDR;  /*GPIO输出速度寄存器          地址偏移: 0x08      */
 uint32_t PUPDR;    /*GPIO上拉/下拉寄存器         地址偏移: 0x0C      */
 uint32_t IDR;      /*GPIO输入数据寄存器          地址偏移: 0x10      */
 uint32_t ODR;      /*GPIO输出数据寄存器          地址偏移: 0x14      */
 uint16_t BSRRL;    /*GPIO置位/复位寄存器低16位部分 地址偏移: 0x18     */
 uint16_t BSRRH;    /*GPIO置位/复位寄存器高16位部分 地址偏移: 0x1A     */
 uint32_t LCKR;     /*GPIO配置锁定寄存器          地址偏移: 0x1C      */
 uint32_t AFR[2];   /*GPIO复用功能配置寄存器       地址偏移: 0x20-0x24  */
} GPIO_TypeDef;

这段代码用typedef 关键字声明了名为GPIO_TypeDef的结构体类型,结构体内有8个成员变量, 变量名正好对应寄存器的名字。C语言的语法规定,结构体内变量的存储空间是连续的, 其中32位的变量占用4个字节,16位的变量占用2个字节,具体见 图5_8

图 5‑8 GPIO_TypeDef结构体成员的地址偏移

也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的H767的首地址为0x4002 1C00, H743繁星/H750/H743_pro/H743挑战者系列这个结构体的首地址为0x5802 1C00(这也是第一个成员变量MODER的地址), 那么结构体中第二个成员变量OTYPER的地址即为H767的首地址0x4002 1C00 或 H743繁星/H750/H743_pro/H743挑战者系列的地址0x5802 1C00+0x04 , 加上的这个0x04 ,正是代表MODER所占用的4个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出。

这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来, 然后就能以结构体的形式访问寄存器了,具体见 代码5_8

代码 5‑8 通过结构体指针访问寄存器

 GPIO_TypeDef * GPIOx;        //定义一个GPIO_TypeDef型结构体指针GPIOx
 GPIOx = GPIOH_BASE;          //把指针地址设置为宏GPIOH_BASE地址
 GPIOx->BSRR  = 0x0000FFFF;   //通过指针访问并修改GPIOH_BSRR寄存器
 GPIOx->MODER = 0xFFFFFFFF;   //修改GPIOH_MODER寄存器
 GPIOx->OTYPER =0xFFFFFFFF;   //修改GPIOH_OTYPER寄存器

 uint32_t temp;
 temp = GPIOx->IDR;          //读取GPIOH_IDR寄存器的值到变量temp中

这段代码先用GPIO_TypeDef类型定义一个结构体指针GPIOx,并让指针指向地址GPIOH_BASE(首地址), 使用地址确定下来,然后根据C语言访问结构体的语法,用GPIOx->BSRR、GPIOx->MODER及GPIOx->IDR等方式读写寄存器。

最后,我们更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可,具体 代码5_9

代码 5‑9 定义好GPIO端口首地址址针

 /*使用GPIO_TypeDef把地址强制转换成指针*/
 #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)

 /*使用定义好的宏直接访问*/
 /*访问GPIOH端口的寄存器*/
 GPIOH->BSRR = 0xFFFF;       //通过指针访问并修改GPIOH_BSRR寄存器
 GPIOH->MODER = 0xFFFFFFF;    //修改GPIOH_MODER寄存器
 GPIOH->OTYPER =0xFFFFFFF;    //修改GPIOH_OTYPER寄存器

 uint32_t temp;
 temp = GPIOH->IDR;          //读取GPIOH_IDR寄存器的值到变量temp中

 /*访问GPIOA端口的寄存器*/
 GPIOA->BSRR = 0xFFFF;       //通过指针访问并修改GPIOA_BSRR寄存器
 GPIOA->MODER = 0xFFFFFFF;    //修改GPIOA_MODER寄存器
 GPIOA->OTYPER =0xFFFFFFF;    //修改GPIOA_OTYPER寄存器

 //uint32_t temp;     //该变量前面已定义
 temp = GPIOA->IDR;          //读取GPIOA_IDR寄存器的值到变量temp中

这里我们仅是以GPIO这个外设为例,给大家讲解了C语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。

5.5.3. 修改寄存器的位操作方法

使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某一位或者某几位的值,且其它的寄存器位不变,这个时候我们就需要用到C语言的位操作方法了。

5.5.3.1. 把变量的某位或某几个位进行清零(按位与 &)

此处我们以变量 a 代表一个8位的寄存器(STM32的寄存器是32位的),并假设寄存器中本来已有数值,此时我们需要把变量 a 的某一位清零,且其它位不变; 由于寄存器中有时也会存在连续几个寄存器位用于控制某个功能的情况,所以我们也假设需要把寄存器的某几个连续位清零,且其它位不变。 方法见 代码清单5_1

代码清单 5‑1 对某位清零、以及对某几位清零
/* 对某位清零 */

 //定义一个变量 a = 1001 1111 b (二进制数)
 unsigned char a = 0x9f;


 //对 bit2 清零
 a &= ~(1<<2);

 //上面一行代码右边括号中的1左移两位,(1<<2)得二进制数:0000 0100 b(这个便是位2的掩码)
 //然后按位取反,~(1<<2)得 1111 1011 b
 //假如a中原来的值为二进制数: a = 1001 1111 b
 //所得的数与a作“位与&”运算: a = (1001 1111 b) & (1111 1011 b)
 //经过运算后,a的值 a=1001 1011 b
 //这样,a的 bit2 位就被清零,而其它位不变。



 /* 下面是对某几个连续位清零 */

 //同样我们首先定义一个变量 a = 1001 1111 b (二进制数)
 unsigned char a = 0x9f;

 //若把a中的二进制位分成2个一组
 //即 bit0、bit1为第0组,bit2、bit3为第1组,
 //   bit4、bit5为第2组,bit6、bit7为第3组
 //现在,我们要对第1组的bit2、bit3清零

 a &= ~(3<<2*1);

 //括号中的3左移两位,(3<<2*1)得二进制数:0000 1100 b(这个是位3、位2的掩码)
 //然后按位取反,~(3<<2*1)得 1111 0011 b
 //假如a中原来的值为二进制数: a = 1001 1111 b
 //所得的数与a作”位与&”运算: a = (1001 1111 b) & (1111 0011 b)
 //经过运算后,a的值 a=1001 0011 b
 //最后 a的第1组的bit2、bit3就被清零了,而其它位不变。

 //上述 (~(3<<2*1)) 中的 1 即为组编号; 如清零第3组bit6、bit7此处应为3,即要左移6位
 //括号中的 2 为每组的位数,每组有2个二进制位; 若分成4个一组,此处即为 4
 //括号中的 3 是组内所有位都为1时的值; 若分成4个一组,此处即为二进制数“1111 b”

 //例如对第2组bit4、bit5清零,3就要左移4位
 a &= ~(3<<2*2);

5.5.3.2. 对变量的某一位或某几位进行赋值(按位或 |)

寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变,方法见 代码清单5_3,这时候写入的数值一般就是需要设置寄存器的位参数。

代码清单 5‑3 对某几位进行赋值
/* 对某位赋值 */

//假设 a = 1000 0011 b
a |= (1<<4);
//此时对变量 a 的 bit4 置1
//置1后,即 a = 1001 0011 b


/* 对某几位进行赋值 */

 //假设 a = 1000 0011 b
 a |= (1<<2*2);
 //此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”(也就是“01 b”左移4位)
 //即 a = 1001 0011 b,成功设置了第2组的值,其它组不变

5.5.3.3. 对变量的某位取反(按位异或 ^)

某些情况下,我们需要对寄存器的某个位进行取反操作,即 1变0 ,0变1,这可以直接用如下的操作,其它位不变,见 代码清单5_4

代码清单 5‑4 对某位进行取反操作
 //a = 1001 0011 b
 //把bit6取反,其它位不变

 a ^=(1<<6);
 //a = 1101 0011 b

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