第3章 寄存器介绍

第三章 寄存器介绍

1. STM32长啥样

我们开发板中使用的芯片是STM32F103VET6,具体见下图

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

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

2. 芯片里都有什么

STM32F103采用的是Cortex-M3内核,内核即CPU,由ARM公司设计。

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

芯片和外设之间通过各种总线连接,其中驱动单元有4个,被动单元也有4个,为了方便理解,我们都可以把驱动单元理解成是CPU部分,被动单元都理解成外设。 (驱动单元-CPU内部,被动单元-外设)

更多可以参考:STM32架构相关 - faihung - 博客园 (cnblogs.com)

下面我们简单介绍下驱动单元和被动单元的各个部件吧:

2.1 ICode总线

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

2.2 驱动单元

2.2.1 DCode总线

DCode中的D表示Data,即数据,那说明这条总线是用来取数据的。我们学过C应该知道:常量就是固定不变的,用C语言中的const关键字修饰,是放到内部的FLASH当中的;变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。

因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

2.2.2 系统总线

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

2.2.3 DMA总线

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

更多内容可以参考:【明解STM32】一文读懂STM32芯片总线_stm32总线-CSDN博客

2.3 被动单元

2.3.1 内部的闪存存储器

内部的闪存存储器即FLASH:我们编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。(也可以理解为手机、电脑中的存储空间概念-128GB、512GB...)

2.3.2 内部的SRAM

内部的SRAM:即我们通常说的RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。(也可以理解为手机、电脑中的内存概念-8GB、16GB...)

2.3.3 FSMC

FSMC:灵活的静态的存储器控制器, 是STM32F10xx中一个很有特色的外设, 通过FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点我们要注意的是,FSMC只能扩展静态的内存, 即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。 (也就是存储空间可以扩展,但是内存不行的,就好像插一张SD卡)

2.3.4 AHB到APB的桥(AHB->APB1/2)

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

关于原理可以参考:AHB2APB桥接器设计(1)——基本原理-CSDN博客

3. 存储器映射

被控单元的FLASH, SRAM,FSMC和片上外设(AHB至APB的桥),这些功能部件共同排列在一个4GB的地址空间内。 我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们。

3.1 存储器映射

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

更多内容参考:【嵌入式系统】存储器映射与寄存器映射原理 - 皮皮祥 - 博客园 (cnblogs.com)

3.2 存储器区域功能划分

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

在这8个Block里面,有3个块非常重要是我们必须了解和掌握的:Block0(Code)用来设计成内部FLASH,Block1(SRAM)用来设计成内部RAM, Block2(GPIO等...)用来设计成片上的外设,下面我们简单的介绍下这三个Block里面的具体区域的功能划分。

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

Block0主要用于设计片内的FLASH,我们使用的STM32F103VET6的FLASH是512K。Block内部区域的功能划分具体见表格。

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

Block1用于设计片内的SRAM。我们使用的STM32F103VET6的SRAM是64KB, Block内部区域的功能划分具体见表格。

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

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

4. 寄存器映射

通过前面的讲解,我们知道,存储器本身是没有地址的,我们给存储器分配地址的过程叫存储器映射。

那什么叫寄存器映射?寄存器到底是什么?

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

比如,我们找到GPIOB端口的输出数据寄存器ODR的地址是0x40010C0C, ODR寄存器是32bit,低16bit有效,对应着16个外部IO,写0/1对应的的IO则输出低/高电平。现在我们通过C语言指针的操作方式, 让GPIOB的16个IO都输出高电平:

// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF; // 1111 1111 1111 1111

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

刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们通常使用寄存器的方式来操作:

// GPIOB 端口全部输出 高电平
#define GPIOB_ODR  (unsigned int*)(GPIOB_BASE+0x0C)
*GPIOB_ODR = 0xFF;

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

// GPIOB 端口全部输出 高电平
#define GPIOB_ODR  *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF; // 1111 1111

我们在对GPIOB_ODR操作的时候其实是对0x4001 0C0C这个内存地址进行操作,但是这串数字比较难记,所以我们通过指针的方式给这个地址命名,下次我们要用就直接使用名字就行了(这种方式我们会在很多地方见到,比如库函数)不过我们在学习51时,应该很少看见这种写法吧,这点是51转stm32很多初学者的烦恼,包括我当初也是这样,刚开始看指针满天飞真是头疼....不过稍微剧透一下,我们还是不用担心这个-因为我们有强大的库函数😄

4.1 STM32的外设地址映射

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

4.1.1 总线基地址

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

4.1.2 外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。

这里面我们以GPIO这个外设来讲解外设的基地址,GPIO属于高速的外设 ,挂载到APB2总线上,具体见表格。(APB2总线基地址为0x4001 0000)

4.1.3 外设寄存器

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

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

这就好像套娃一样,有一个总线基地址(比如这里的APB2),外设基地址(GPIOA~G)又基于总线基地址有偏移,当然外设又有多个寄存器(比如GPIOX_ODR),这些寄存器又基于外设基地址偏移

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

  • ①名称

    寄存器说明中首先列出了该寄存器中的名称,“(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 。

  • ③寄存器位表

    紧接着的是本寄存器的位表,表中列出它的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的时候对应的引脚输出低电平即可。

    所以,如果对BR0写入“1”的话, 那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”, 就需要对“BS0”位写入“1”,寄存器位BSy与BRy是相反的操作。

4.2 C语言对寄存器的封装

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

4.2.1 封装总线和外设基地址

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

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

/* 总线基地址 */
#define APB1PERIPH_BASE       PERIPH_BASE                // APB1基地址
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000) // APB2基地址
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x00020000) // AHB基地址


/* GPIO外设基地址 */
#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800) // GPIOA外设基地址
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00) // GPIOB外设基地址
#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000) // GPIOC外设基地址
#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400) // GPIOD外设基地址
#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800) // GPIOE外设基地址
#define GPIOF_BASE            (APB2PERIPH_BASE + 0x1C00) // GPIOF外设基地址
#define GPIOG_BASE            (APB2PERIPH_BASE + 0x2000) // GPIOG外设基地址


/* 寄存器基地址,以GPIOB为例 */
#define GPIOB_CRL             (GPIOB_BASE+0x00) // 寄存器CRL基地址(偏移GPIOB外设基地址 0x00)
#define GPIOB_CRH             (GPIOB_BASE+0x04) // 寄存器CRH基地址(偏移GPIOB外设基地址 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)

一旦有了具体地址,就可以用指针读写:

// 使用指针控制BSRR寄存器

/* 控制GPIOB 引脚0输出低电平(BSRR寄存器的BR0置1-前面我们学习过要使BRx置1就得赋值0) */
*(unsigned int*)GPIOB_BSRR = (0x01<<(16+0)); // 0000 0000 0001向左移动16位就是0000 0000 0000

/* 控制GPIOB 引脚0输出高电平(BSRR寄存器的BS0置1-前面我们学习过要使BSx置1就得赋值1) */
*(unsigned int*)GPIOB_BSRR = 0x01<<0; // 向左移动0位0x01 还是000 0000 0000 0001

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

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

4.2.2 封装寄存器列表

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

// 使用结构体对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个字节:

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

这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来, 然后就能以结构体的形式访问寄存器,那这样就比较方便了:

// 通过结构体指针访问寄存器

GPIO_TypeDef *GPIOx; // 定义一个GPIO_TypeDef型结构体指针GPIOx
GPIOx = GPIOB_BASE; // 把指针地址设置为宏GPIOB_BASE地址
GPIOx->IDR = 0xFFFF; // 1111 1111 1111 1111
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端口的首地址, 使用时我们直接用该宏访问寄存器即可

// 定义好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语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。

好消息是, 这部分工作都由固件库帮我们完成了,我们需要使用的话只要别名就行了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。


2024.7.19 第一次修订

2024.8.18 第二次修订,后期不在维护

posted @ 2024-08-18 14:36  hazy1k  阅读(33)  评论(0编辑  收藏  举报