单片机
返回首页

第5章 什么是寄存器—零死角玩转STM32-F429系列

2019-09-19 来源:eefocus

本章参考资料:《STM32F4xx 中文参考手册》、《STM32F429xx数据手册》、


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


5.1 什么是寄存器

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


5.2 STM32长啥样

我们开发板中使用的芯片是176pin的STM32F429IGT6,具体见图 51。这个就是我们接下来要学习的STM32,它讲带领我们进入嵌入式的殿堂。


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


芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,连接到各种传感器上,然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作,通过做实验的方式来学习STM32芯片的各个资源。开发板是一种评估板,板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。

图 51 STM32F429IGT6 实物图

图 52 STM32F429IGT6正面引脚图


5.3 芯片里面有什么

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


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

图 53 STM32芯片架构简图


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


图 54 STM32F42xxx 和 STM32F43xxx 器件的总线接口


5.4 存储器映射

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


5.4.1 存储器映射

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

图 55 存储器映射


1.    存储器区域功能划分

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


表格 51 存储器功能分类

image.png

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


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

Block0主要用于设计片内的FLASH, F429系列片内部FLASH最大是2MB,我们使用的STM32F429IGT6的FLASH是1MB。要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大,ST能在追求性价比的同时做到1MB以上,实乃良心之举。Block内部区域的功能划分具体见表格 52。


表格 52 存储器Block0 内部区域功能划分

image.png

image.png

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

Block1用于设计片内的SRAM。F429 内部SRAM的大小为256KB,其中64KB的CCM RAM 位于 Block0,剩下的192KB位于Block1,分SRAM1 112KB,SRAM2 16KB,SRAM3 64KB,Block内部区域的功能划分具体见表格 53。


表格 53 存储器Block1 内部区域功能划分

image.png

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

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


表格 54 存储器Block2 内部区域功能划分

image.png

5.5 寄存器映射

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


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


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


代码 51 通过绝对地址访问内存单元


1 // GPIOH 端口全部输出高电平


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


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


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


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


1 // GPIOH 端口全部输出高电平


2 #define GPIOH_ODR (unsigned int*)(GPIOH_BASE+0x14)


3 * GPIOH_ODR = 0xFF;


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


代码 53 通过寄存器别名访问内存单元


1 // GPIOH 端口全部输出高电平


2 #define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14)


3 GPIOH_ODR = 0xFF;


5.5.1 STM32的外设地址映射

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


1.    总线基地址

表格 55 总线基地址

image.png

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


2.    外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为'XX外设基地址',也叫XX外设的边界地址。具体有关STM32F4xx外设的边界地址请参考《STM32F4xx参考手册》的2.3小节的存储器映射的表2:STM32F4xx 寄存器边界地址。或者参考《STM32F4xx参考手册》的存储器映射章节,这两个手册都有详细的讲解。


这里面我们以GPIO这个外设来讲解外设的基地址,具体见表格 56。


表格 56 外设GPIO基地址

image.png

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


3.    外设寄存器

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


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


表格 57 GPIOH端口的寄存器地址列表

image.png

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


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


图 56 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语言控制读写外设寄存器做准备,此处是本章的重点内容。


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

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


代码 54 总线和外设基址宏定义


1 /* 外设基地址 */


2 #define PERIPH_BASE ((unsigned int)0x40000000)


3


4 /* 总线基地址 */


5 #define APB1PERIPH_BASE PERIPH_BASE


6 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)


7 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)


8 #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)


9


10 /* GPIO外设基地址 */


11 #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)


12 #define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)


13 #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)


14 #define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)


15 #define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)


16 #define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)


17 #define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)


18 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)


19


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


21 #define GPIOH_MODER (GPIOH_BASE+0x00)


22 #define GPIOH_OTYPER (GPIOH_BASE+0x04)


23 #define GPIOH_OSPEEDR (GPIOH_BASE+0x08)


24 #define GPIOH_PUPDR (GPIOH_BASE+0x0C)


25 #define GPIOH_IDR (GPIOH_BASE+0x10)


26 #define GPIOH_ODR (GPIOH_BASE+0x14)


27 #define GPIOH_BSRR (GPIOH_BASE+0x18)


28 #define GPIOH_LCKR (GPIOH_BASE+0x1C)


29 #define GPIOH_AFRL (GPIOH_BASE+0x20)


30 #define GPIOH_AFRH (GPIOH_BASE+0x24)


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


代码 55 使用指针控制BSRR寄存器


1 /* 控制GPIOH 引脚10输出低电平(BSRR寄存器的BR10置1) */


2 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));


3


4 /* 控制GPIOH 引脚10输出高电平(BSRR寄存器的BS10置1) */


5 *(unsigned int *)GPIOH_BSRR = 0x01<<10;


6


7 unsigned int temp;


8 /* 控制GPIOH 端口所有引脚的电平(读IDR寄存器) */


9 temp = *(unsigned int *)GPIOH_IDR;


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


2.    封装寄存器列表

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


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


1 typedef unsigned int uint32_t; /*无符号32位变量*/


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


3


4 /* GPIO寄存器列表 */


5 typedef struct {


6 uint32_t MODER; /*GPIO模式寄存器地址偏移: 0x00 */


7 uint32_t OTYPER; /*GPIO输出类型寄存器地址偏移: 0x04 */


8 uint32_t OSPEEDR; /*GPIO输出速度寄存器地址偏移: 0x08 */


9 uint32_t PUPDR; /*GPIO上拉/下拉寄存器地址偏移: 0x0C */


10 uint32_t IDR; /*GPIO输入数据寄存器地址偏移: 0x10 */


11 uint32_t ODR; /*GPIO输出数据寄存器地址偏移: 0x14 */


12 uint16_t BSRRL; /*GPIO置位/复位寄存器低16位部分地址偏移: 0x18 */


13 uint16_t BSRRH; /*GPIO置位/复位寄存器高16位部分地址偏移: 0x1A */


14 uint32_t LCKR; /*GPIO配置锁定寄存器地址偏移: 0x1C */


15 uint32_t AFR[2]; /*GPIO复用功能配置寄存器地址偏移: 0x20-0x24 */


16 } GPIO_TypeDef;


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

图 57 GPIO_TypeDef结构体成员的地址偏移


也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x4002 1C00(这也是第一个成员变量MODER的地址),那么结构体中第二个成员变量OTYPER的地址即为0x4002 1C00 +0x04 ,加上的这个0x04 ,正是代表MODER所占用的4个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的BSRR寄存器分成了低16位BSRRL和高16位BSRRH,BSRRL置1引脚输出高电平,BSRRH置1引脚输出低电平,这里分开只是为了方便操作。


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


代码 57 通过结构体指针访问寄存器


1 GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef型结构体指针GPIOx


2 GPIOx = GPIOH_BASE; //把指针地址设置为宏GPIOH_BASE地址


3 GPIOx->BSRRL = 0xFFFF; //通过指针访问并修改GPIOH_BSRRL寄存器


4 GPIOx->MODER = 0xFFFFFFFF; //修改GPIOH_MODER寄存器


5 GPIOx->OTYPER =0xFFFFFFFF; //修改GPIOH_OTYPER寄存器


6


7 uint32_t temp;


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


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


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


代码 58 定义好GPIO端口首地址址针


1 /*使用GPIO_TypeDef把地址强制转换成指针*/


2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)


3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)


4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)


5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)


6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)


7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)


8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)


9 #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)


10


11


12


13 /*使用定义好的宏直接访问*/


14 /*访问GPIOH端口的寄存器*/


15 GPIOH->BSRRL = 0xFFFF; //通过指针访问并修改GPIOH_BSRRL寄存器


16 GPIOH->MODER = 0xFFFFFFF; //修改GPIOH_MODER寄存器


17 GPIOH->OTYPER =0xFFFFFFF; //修改GPIOH_OTYPER寄存器


18


19 uint32_t temp;


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


21


22 /*访问GPIOA端口的寄存器*/


23 GPIOA->BSRRL = 0xFFFF; //通过指针访问并修改GPIOA_BSRRL寄存器


24 GPIOA->MODER = 0xFFFFFFF; //修改GPIOA_MODER寄存器


25 GPIOA->OTYPER =0xFFFFFFF; //修改GPIOA_OTYPER寄存器


26


27 uint32_t temp;


28 temp = GPIOA->IDR; //读取GPIOA_IDR寄存器的值到变量temp中


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


进入单片机查看更多内容>>
相关视频
  • RISC-V嵌入式系统开发

  • SOC系统级芯片设计实验

  • 云龙51单片机实训视频教程(王云,字幕版)

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

精选电路图
  • 家用电源无载自动断电装置的设计与制作

  • PIC单片机控制的遥控防盗报警器电路

  • 短波AM发射器电路设计图

  • 使用ESP8266从NTP服务器获取时间并在OLED显示器上显示

  • 如何构建一个触摸传感器电路

  • 基于TDA2003的简单低功耗汽车立体声放大器电路

    相关电子头条文章