5. 什么是寄存器¶
本章参考资料:《STM32F4xx 中文参考手册》、《STM32F4xx英文数据手册》
学习本章时,配合《STM32F4xx 中文参考手册》“存储器和总线架构”、“嵌入式FLASH接口”及“通用I/O(GPIO)”章节一起阅读, 效果会更佳,特别是涉及到寄存器说明的部分。
5.1. 什么是寄存器¶
在学习STM32编程时,通常有两种编程方式,一种是寄存器编程,另外一种是函数库编程,其中寄存器编程是基础, 而函数库编程是在寄存器编程的基础上升级而来的一种易于学习和开发的编程方式,是我们学习STM32编程的时候需要重点掌握的一种编程方法。 函数库编程对于项目开发固然简单和快速,但是从学习的角度开发,学习寄存器编程的方法我们也不能丢。其实,我们在学习8位或者16位单片机的时候, 居多都是采用寄存器编程,那么这个寄存器编程里面的寄存器是什么东西?它在芯片的什么地方?在探索STM32寄存器深层次的问题时, 我们从STM32表面的外观开始,一层层的往里面深入学习。最后,在学习完本章内容之后, 看看大家能否用一句话给寄存器下一个定义。
5.2. STM32长啥样¶
以我们的F407-霸天虎开发板为例,F407霸天虎开发板中使用的芯片是144pin的STM32F407ZGT6,具体见 图5_1。 这个就是我们接下来要学习的STM32,它将带领我们进入嵌入式的殿堂。
芯片正面是丝印,ARM应该是表示该芯片使用的是ARM的内核,STM32F407ZGT6是芯片型号,后面的字应该是跟生产批次相关,最下面的是ST的LOGO。
芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。 开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作,通过做实验的方式来学习STM32芯片的各个资源。
开发板是一种评估板,主要用于学习与评估,其板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。 我们的STM32 F4的开发板上面的STM32型号虽然不完全相同(STM32F407 和 STM32F429),但实际上学习它们所需要的编程参考手册资料是一样的,而数据手册虽然不同但实际上其内容基本上相同,所以初学者不必为此担心。
图 5‑1 STM32F407ZGT6 实物图(图中方框部分的芯片)
图 5‑2 STM32F407ZGT6正面引脚图
5.3. 芯片里面有什么¶
我们看到的STM32芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。
STM32F407、STM32F429采用的都是Cortex-M4内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。 芯片生产厂商(SOC) 如ST、TI、NXP等,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。具体见 图5_3。
图 5‑3 STM32芯片架构简图
芯片内核和外设之间通过各种总线连接,其中主控总线有8条,被控总线有7条,具体见 图5_4。主控总线通过一个总线矩阵来连接被控总线, 总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。总线之间交叉的时候如果有个圆圈则表示可以通信, 没有圆圈则表示不可以通信。比如S0:I总线只有跟M0、M2和M6这三根被控总线交叉的时候才有圆圈,就表示S0只能跟这三根被控总线通信。 从功能上来理解,I总线是指令总线,用来取指,指令指的是编译好的程序指令。我们知道STM32有三种启动方式, 从FLASH启动(包含系统存储器),从内部SRAM启动,从外部RAM启动,这三种存储器刚好对应的就是M0、M2和M6这三条总线。
5.4. 存储器映射¶
在 图5_4 中,连接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。 我们在编程的时候,操作的也正是这些功能部件。
5.4.1. 存储器映射¶
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见 图5_5。如果给存储器再分配一个地址就叫存储器重映射。
图 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-M4内部外设 |
0xE000 0000 ~ 0xFFFF FFFF(512MB) |
在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设,下面我们简单的介绍下这三个Block里面的具体区域的功能划分。
5.4.2.1. 存储器Block0内部区域功能划分¶
Block0主要用于设计片内的FLASH, F407系列片内部FLASH最大是1MB,F429系列片内部FLASH最大是2MB, 我们使用的STM32F407ZGT6(霸天虎)、STM32F407IGT6(骄阳)的FLASH就是1MB,而STM32F429IGT6(挑战者F429核心板)的FLASH也是1MB。 要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大,ST能在追求性价比的同时做到1MB以上,实乃良心之举。 Block0内部区域的功能划分具体见表 5_2 存储器 Block0 内部区域功能划分
表格 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。F407 内部SRAM的大小为128KB,其中SRAM1为112KB,SRAM2为16KB;F429 内部SRAM的大小为192KB,其中SRAM1为112KB,SRAM2为16KB,SRAM3为64KB。 Block1内部区域的功能划分具体见表 5-3a F407 存储器 Block1 内部区域功能划分、表 5-3b F429 存储器 Block1 内部区域功能划分。
表格 5‑3a F407 存储器 Block1 内部区域功能划分
块 |
用途说明 |
地址范围 |
Block1 |
预留 |
0x2002 0000 ~ 0x3FFF FFFF |
SRAM2 16KB |
0x2001 C000 ~ 0x2001 FFFF |
|
SRAM1 112KB |
0x2000 0000 ~ 0x2001 BFFF |
表格 5‑3b F429 存储器 Block1 内部区域功能划分
块 |
用途说明 |
地址范围 |
Block1 |
预留 |
0x2003 0000 ~ 0x3FFF FFFF |
SRAM3 64KB |
0x2002 0000 ~ 0x2002 FFFF |
|
SRAM2 16KB |
0x2001 C000 ~ 0x2001 FFFF |
|
SRAM1 112KB |
0x2000 0000 ~ 0x2001 BFFF |
5.4.2.3. 储存器Block2内部区域功能划分¶
Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2, 具体见表 5-4 F407 存储器 Block2 内部区域功能划分。还有一个AHB3包含了Block3/4/5,AHB3包含的3个Block用于扩展外部存储器, 如SRAM,NORFLASH和NANDFLASH等。
F429 和 F407 存储器APB2的地址范围有所差异,具体的可以查阅两者的数据手册。
表格 5‑4 F407 存储器 Block2 内部区域功能划分
块 |
用途说明 |
地址范围 |
Block2 |
APB1 总线外设 |
0x4000 0000 ~ 0x4000 77FF |
预留 |
0x4000 7800 ~ 0x4000 FFFF |
|
APB2 总线外设 |
0x4001 0000 ~ 0x4001 57FF |
|
预留 |
0x4001 5800 ~ 0x4001 FFFF |
|
AHB1 总线外设 |
0x4002 0000 ~ 0x4007 FFFF |
|
预留 |
0x4008 0000 ~ 0x4FFF FFFF |
|
AHB2 总线外设 |
0x5000 0000 ~ 0x5006 0BFF |
|
预留 |
0x5006 0C00 ~ 0x5FFF FFFF |
5.5. 寄存器映射¶
我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?
在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
比如,我们找到GPIOF端口的输出数据寄存器ODR的地址是0x4002 1414(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR寄存器是32bit,低16bit有效,对应着16个外部IO,写0/1对应的的IO则输出低/高电平。现在我们通过C语言指针的操作方式, 让GPIOF的16个IO都输出高电平,具体见代码 代码5_1。
// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x4002 1414) = 0xFFFF;
0x40021414 在我们看来是GPIOF端口数据输出寄存器ODR的地址,但是在编译器看来,这只是一个普通的变量, 是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针, 即(unsigned int *)0x40021414,然后再对这个指针进行 * 操作。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器别名的方式来操作,具体见 代码5_2 。
// GPIOF 端口全部输出 高电平
#define GPIOF_BASE 0x40021400
#define GPIOF_ODR (unsigned int*)(GPIOF_BASE+0x14)
*GPIOF_ODR = 0xFF;
为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见 代码5_3。
// GPIOF 端口全部输出 高电平
#define GPIOF_BASE 0x40021400
#define GPIOF_ODR *(unsigned int*)(GPIOF_BASE+0x14)
GPIOF_ODR = 0xFF;
5.5.1. STM32的外设地址映射¶
片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB挂载低速外设,AHB挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。
5.5.1.1. 总线基地址¶
表格 5‑5 总线基地址
总线名称 |
总线基地址 |
相对外设基地址的偏移 |
APB1 |
0x4000 0000 |
0x0 |
APB2 |
0x4001 0000 |
0x0001 0000 |
AHB1 |
0x4002 0000 |
0x0001 0000 |
AHB2 |
0x5000 0000 |
0x0001 0000 |
AHB1 |
0x6000 0000 |
已不属于片上外设 |
表 5-5总线基地址中的“相对外设基地址偏移”即该总线地址与“片上外设”基地址0x4000 0000的差值。关于地址的偏移我们后面还会讲到。
5.5.1.2. 外设基地址¶
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。 具体有关STM32F4xx外设的边界地址请参考《STM32F4xx参考手册》的2.3小节的存储器映射的表2:STM32F4xx 寄存器边界地址。
这里面我们以GPIO这个外设来讲解外设的基地址。
表格 5‑6 外设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 |
从表 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-7 GPIOF端口的 寄存器地址列表。
表格 5‑7 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 |
有关外设的寄存器说明可参考《STM32F4xx参考手册》中具体章节的寄存器描述部分,在编程的时候我们需要反复的查阅外设的寄存器说明。
这里我们以“GPIO端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见 图5_6。
图 5‑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是相反的操作。
5.5.2. C语言对寄存器的封装¶
以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用C语言控制读写外设寄存器做准备,此处是本章的重点内容。
5.5.2.1. 封装总线和外设基地址¶
在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来, 总线或者外设都以他们的名字作为宏名,具体见 代码5_4。
/* 外设基地址 */
#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_5。
/* 控制GPIOF 引脚6输出低电平(BSRR寄存器的BR6置1) */
*(unsigned int *)GPIOF_BSRR = (0x01<<(16+6));
/* 控制GPIOF 引脚6输出高电平(BSRR寄存器的BS6置1) */
*(unsigned int *)GPIOF_BSRR = 0x01<<6;
unsigned int temp;
/* 控制GPIOF 端口所有引脚的电平(读IDR寄存器) */
temp = *(unsigned int *)GPIOF_IDR;
该代码使用 (unsigned int )把GPIOB_BSRR宏的数值强制转换成了地址,然后再用“”号做取指针操作,对该地址的赋值, 从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32外设的状态。
5.5.2.2. 封装寄存器列表¶
用上面的方法去定义地址,还是稍显繁琐,例如GPIOA~GPIOH都各有一组功能相同的寄存器, 如GPIOA_MODER/GPIOB_MODER/GPIOC_MODER等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。 为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装,具体见 代码5_6。
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_7。
也就是说,假如我们定义一个GPIO_TypeDef 类型的结构体,且结构体的首地址为0x4002 1400(这也是第一个成员变量MODER的地址), 那么结构体中第二个成员变量OTYPER的地址即为0x4002 1400 +0x04 ,加上的这个0x04 ,正是代表MODER所占用的4个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的BSRR寄存器分成了低16位BSRRL和高16位BSRRH,BSRRL置1引脚输出高电平, BSRRH置1引脚输出低电平,这里分开只是为了方便操作。
这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来, 然后就能以结构体的形式访问寄存器了,具体见 代码5_7 。
GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef型结构体指针GPIOx
GPIOx = GPIOF_BASE; //把指针地址设置为宏GPIOF_BASE地址
GPIOx->BSRRL = 0xFFFF; //通过指针访问并修改GPIOF_BSRRL寄存器
GPIOx->MODER = 0xFFFFFFFF; //修改GPIOF_MODER寄存器
GPIOx->OTYPER =0xFFFFFFFF; //修改GPIOF_OTYPER寄存器
uint32_t temp;
temp = GPIOx->IDR; //读取GPIOF_IDR寄存器的值到变量temp中
这段代码先用GPIO_TypeDef类型定义一个结构体指针GPIOx,并让指针指向地址GPIOF_BASE(0x4002 1400), 使地址确定下来,然后根据C语言访问结构体的语法,用GPIOx->BSRRL、GPIOx->MODER及GPIOx->IDR等方式读写寄存器。
最后,我们更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可, 具体 代码5_8。
/*使用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)
/*使用定义好的宏直接访问*/
/*访问GPIOF端口的寄存器*/
GPIOF->BSRRL = 0xFFFF; //通过指针访问并修改GPIOF_BSRRL寄存器
GPIOF->MODER = 0xFFFFFFF; //修改GPIOF_MODER寄存器
GPIOF->OTYPER =0xFFFFFFF; //修改GPIOF_OTYPER寄存器
uint32_t temp;
temp = GPIOF->IDR; //读取GPIOF_IDR寄存器的值到变量temp中
/*访问GPIOA端口的寄存器*/
GPIOA->BSRRL = 0xFFFF; //通过指针访问并修改GPIOA_BSRRL寄存器
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。
/* 对某位清零 */
//定义一个变量 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,这时候写入的数值一般就是需要设置寄存器的位参数。
/* 对某位赋值 */
//假设 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组的值,其它组不变