5. 什么是寄存器

本章参考资料:《STM32F10xx 参考手册》、《STM32F10xx数据手册》、

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

5.1. 什么是寄存器

我们经常说寄存器,那么什么是寄存器?这是我们本章需要讲解的内容,在学习的过程中,大家带着这个疑问好好思考下,到最后看看大家能否用一句话给寄存器下一个定义。

5.2. STM32长啥样

以我们的F103-指南者开发板为例,指南者开发板中使用的芯片是100pin(100个引脚)的STM32F103VET6,具体见图5‑1。 这个就是我们接下来要学习的STM32,它将带领我们进入嵌入式的殿堂。

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

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

开发板是一种评估板,主要用于学习与评估,其板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。 我们的STM32 F1的开发板上面的STM32型号虽然不完全相同,但实际上学习它们所需要的数据手册和参考手册资料是一样的,初学者不必为此担心。

image1

图 5‑1 STM32F103VET6 实物图(红色框中部分)

image2

图 5‑2 STM32F103VET6正面引脚图

5.3. 芯片里面有什么

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

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

image3

图 5‑3 STM32芯片架构简图

芯片(这里指内核,或者叫CPU)和外设之间通过各种总线连接,其中驱动单元有4个,被动单元也有4个,具体见图 5‑4。为了方便理解,我们都可以把驱动单元理解成是CPU部分,被动单元都理解成外设。下面我们简单介绍下驱动单元和被动单元的各个部件。

5.3.1. ICode总线

ICode中的 I 表示 Instruction,即指令。我们写好的程序经过编译之后都是一条条指令,存放在FLASH中,内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,它是专门用来取指的。

5.3.2. 驱动单元

5.3.2.1. DCode总线

DCode中的 D 表示 Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种, 常量就是固定不变的,用C语言中的const关键字修饰,是放到内部的FLASH当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

5.3.2.2. 系统总线

系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。

5.3.2.3. DMA总线

DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的FLASH。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

5.3.3. 被动单元

5.3.3.1. 内部的闪存存储器

内部的闪存存储器即FLASH,我们编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。

5.3.3.2. 内部的SRAM

内部的SRAM,即我们通常说的RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。

5.3.3.3. FSMC

FSMC的英文全称是Flexible static memory controller,叫灵活的静态的存储器控制器,是STM32F10xx中一个很有特色的外设,通过FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点我们要注意的是,FSMC只能扩展静态的内存,即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。

5.3.3.4. AHB到APB的桥

从AHB总线延伸出来的两条APB2和APB1总线,上面挂载着STM32各种各样的特色外设。我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上,这个是我们学习STM32的重点,就是要学会编程这些外设去驱动外部的各种设备。

image4

图 5‑4 STM32F10xx系统框图(不包括互联型)

5.4. 存储器映射

在图 5‑4中,被控单元的FLASH,RAM,FSMC和AHB到APB的桥(即片上外设),这些功能部件共同排列在一个4GB的地址空间内。我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过C语言对它们进行数据的读和写)。

5.4.1. 存储器映射

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

image5

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

5.4.2. 存储器区域功能划分

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

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

序号

用途

地址范围

Block 0

Code

0x0000 0000 ~ 0x1FFF FFFF(512MB)

Block 1

SRAM

0x2000 0000 ~ 0x3FFF FFFF(512MB)

Block 2

片上外设

0x4000 0000 ~ 0x5FFF FFFF(512MB)

Block 3

FSMC的bank1 ~ bank2

0x6000 0000 ~ 0x7FFF FFFF(512MB)

Block 4

FSMC的bank3 ~ bank4

0x8000 0000 ~ 0x9FFF FFFF(512MB)

Block 5

FSMC 寄存器

0xA000 0000 ~ 0xCFFF FFFF(512MB)

Block 6

没有使用

0xD000 0000 ~ 0xDFFF FFFF(512MB)

Block 7

Cortex-M3内部外设

0xE000 0000 ~ 0xFFFF FFFF(512MB)

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

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

Block0主要用于设计片内的FLASH,我们使用的STM32F103RCT6(MINI)的FLASH为256KB,而STM32F103VET6(指南者和拂晓)和STM32F103ZET6(霸道)的FLASH都是512KB,这三者都属于大容量。 要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大,ST能在追求性价比的同时做到512KB,实乃良心之举。Block内部区域的功能划分具体见表格 5‑2。

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

用途说明

地址范围

Block0

预留

0x1FFE C008 ~ 0x1FFF FFFF

选项字节:用于配置读写保护、 BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不小心被锁住之后,我们可以从RAM里面启动来修改这部分相应的寄存器位。

0x1FFF F800 - 0x1FFF F80F

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

0x1FFF F000- 0x1FFF F7FF

预留

0x0808 0000 ~ 0x1FFF EFFF

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

0x0800 0000 ~ 0x0807 FFFF (512KB)

预留

0x0008 0000 ~ 0x07FF FFFF

取决于BOOT引脚,为FLASH、系统存储器、SRAM的别名。

0x0000 0000 ~ 0x0007 FFFF

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

Block1用于设计片内的SRAM。我们使用的STM32F103RCT6(MINI)的SRAM为48KB,STM32F103VET6(指南者和拂晓)和STM32F103VET6(指南者)的SRAM都是64KB, Block内部区域的功能划分具体见表格 5‑3。

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

用途说明

地址范围

Block1

预留

0x2001 0000 ~ 0x3FFF FFFF

SRAM 64KB

0x2000 0000 ~0x2000 FFFF

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

Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,具体见表格 5‑4。

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

用途说明

地址范围

Block2

APB1 总线外设

0x4000 0000 ~ 0x4000 77FF

APB2 总线外设

0x4001 0000 ~ 0x4001 3FFF

AHB 总线外设

0x4001 8000 ~ 0x5003 FFFF

5.5. 寄存器映射

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

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

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

代码 5‑1 通过绝对地址访问内存单元
  // GPIOB 端口全部输出 高电平

  *(unsigned int*)(0x4001 0C0C) = 0xFFFF;

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

刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器别名的方式来操作,具体见 代码5_2 (其中GPIOB_BASE指的是GPIOB端口的基地址,稍后会讲到)。

代码 5‑2 通过寄存器别名方式访问内存单元
  // GPIOB 端口全部输出 高电平

 #define GPIOB_ODR (unsigned int *)(GPIOB_BASE+0x0C)

  * GPIOB_ODR = 0xFF;

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

代码 5‑3 通过寄存器别名访问内存单元
  //GPIOB 端口全部输出 高电平

  #define GPIOB_ODR *(unsigned int *)(GPIOB_BASE+0x0C)

  GPIOB_ODR = 0xFF;

5.5.1. STM32的外设地址映射

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

5.5.1.1. 总线基地址

表格 5‑5 总线基地址

总线名称

总线基地址

相对外设基地址的偏移

APB1

0x4000 0000

0x0

APB2

0x4001 0000

0x0001 0000

AHB

0x4001 8000

0x0001 8000

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

5.5.1.2. 外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。具体有关STM32F10xx外设的边界地址请参考《STM32F10xx参考手册》的2.3小节的存储器映射的表1:STM32F10xx 寄存器边界地址。

这里面我们以GPIO这个外设来讲解外设的基地址,GPIO属于高速的外设 ,挂载到APB2总线上,具体见表格 5‑6。

表格 5‑6 外设GPIO基地址

外设名称

外设基地址

相对APB2总线的地址偏移

GPIOA

0x4001 0800

0x0000 0800

GPIOB

0x4001 0C00

0x0000 0C00

GPIOC

0x4001 1000

0x0000 1000

GPIOD

0x4001 1400

0x0000 1400

GPIOE

0x4001 1800

0x0000 1800

GPIOF

0x4001 1C00

0x0000 1C00

GPIOG

0x4001 2000

0x0000 2000

5.5.1.3. 外设寄存器

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

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

表格 5‑7 GPIOB端口的 寄存器地址列表

寄存器名称

寄存器地址

相对GPIOB基址的偏移

GPIOB_CRL

0x4001 0C00

0x00

GPIOB_CRH

0x4001 0C04

0x04

GPIOB_IDR

0x4001 0C08

0x08

GPIOB_ODR

0x4001 0C0C

0x0C

GPIOH_BSRR

0x4001 0C10

0x10

GPIOH_BRR

0x4001 0C14

0x14

GPIOH_LCKR

0x4001 0C18

0x18

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

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

image6

图 5‑6 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的外设基地址为0x4001 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的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器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 总线和外设基址宏定义
/* 外设基地址 */

#define PERIPH_BASE ((unsigned int)0x40000000)

/* 总线基地址 */

#define APB1PERIPH_BASE PERIPH_BASE

#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)

#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)

/* GPIO外设基地址 */

#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)

/* 寄存器基地址,以GPIOB为例 */

#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)

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

代码 5‑5 使用指针控制BSRR寄存器
 /* 控制GPIOB 引脚0输出低电平(BSRR寄存器的BR0置1) */
 *(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));

 /* 控制GPIOB 引脚0输出高电平(BSRR寄存器的BS0置1) */
 *(unsigned int *)GPIOB_BSRR = 0x01<<0;

 unsigned int temp;
 /* 读取GPIOB 端口所有引脚的电平(读IDR寄存器) */
 temp = *(unsigned int *)GPIOB_IDR;

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

5.5.2.2. 封装寄存器列表

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

代码 5‑6 使用结构体对GPIO寄存器组的封装
 typedef unsigned           int uint32_t; /*无符号32位变量*/
 typedef unsigned short     int uint16_t; /*无符号16位变量*/

 /* GPIO寄存器列表 */
 typedef struct {
     uint32_t CRL;     /*GPIO端口配置低寄存器    地址偏移: 0x00 */
     uint32_t CRH;     /*GPIO端口配置高寄存器    地址偏移: 0x04 */
     uint32_t IDR;     /*GPIO数据输入寄存器      地址偏移: 0x08 */
     uint32_t ODR;     /*GPIO数据输出寄存器      地址偏移: 0x0C */
     uint32_t BSRR;    /*GPIO位设置/清除寄存器   地址偏移: 0x10 */
     uint32_t BRR;     /*GPIO端口位清除寄存器     地址偏移: 0x14 */
     uint16_t LCKR;    /*GPIO端口配置锁定寄存器   地址偏移: 0x18 */
 } GPIO_TypeDef;

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

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

也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x4001 0C00(这也是第一个成员变量CRL的地址), 那么结构体中第二个成员变量CRH的地址即为0x4001 0C00 +0x04 ,加上的这个0x04 ,正是代表CRL所占用的4个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。

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

代码 5‑7 通过结构体指针访问寄存器
  GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef型结构体指针GPIOx
  GPIOx = GPIOB_BASE; //把指针地址设置为宏GPIOB_BASE地址
  GPIOx->IDR = 0xFFFF;
  GPIOx->ODR = 0xFFFF;
  uint32_t temp;
  temp = GPIOx->IDR; //读取GPIOB_IDR寄存器的值到变量temp中

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

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

代码 5‑8 定义好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)

 /*使用定义好的宏直接访问*/
 /*访问GPIOB端口的寄存器*/
 GPIOB->BSRR = 0xFFFF;       //通过指针访问并修改GPIOB_BSRR寄存器
 GPIOB->CRL = 0xFFFF;        //修改GPIOB_CRL寄存器
 GPIOB->ODR =0xFFFF;         //修改GPIOB_ODR寄存器

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

 /*访问GPIOA端口的寄存器*/
 GPIOA->BSRR = 0xFFFF;
 GPIOA->CRL = 0xFFFF;
 GPIOA->ODR =0xFFFF;

 //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

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