(stm32学习总结)—对寄存器的理解
芯片里面有什么
我们看到的 STM32 芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的 CPU 与主板、内存、显卡、硬盘的关系。STM32F103 采用的是 Cortex-M3 内核,内核即 CPU,由 ARM 公司设计。ARM 公司并不生产芯片,而是出售其芯片技术权。芯片生产厂商(SOC)如 ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、USART(串口)、I2C、SPI 等都叫做片上外设。
1. ICode 总线
ICode 中的 I 表示 Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在 FLASH 中,内核要读取这些指令来执行程序就必须通过 ICode 总线,它几乎每时每刻都需要被使用,它是专门用来取指的。
DCode 总线
DCode 中的 D 表示 Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种,常量就是固定不变的,用 C 语言中的 const 关键字修饰,是放到内部的 FLASH 当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
S系统总线
系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
DMA 总线
DMA 总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的 FLASH。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
2. 被动单元
内部的闪存存储器
内部的闪存存储器即 FLASH,我们编写好的程序就放在这个地方。内核通过 ICode 总线来取里面的指令。
内部的 SRAM
内部的 SRAM,即我们通常说的 RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过 DCode 总线来访问它。
FSMC
FSMC 的英文全称是 Flexible static memory controller,叫灵活的静态的存储器控制器,是 STM32F10xx 中一个很有特色的外设,通过 FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH 和 NORFLASH。但有一点我们要注意的是,FSMC 只能扩展静态的内存,即名称里面的 S:static,不能是动态的内存,比如 SDRAM 就不能扩展。
AHB 到 APB 的桥
从 AHB 总线延伸出来的两条 APB2 和 APB1 总线,上面挂载着 STM32 各种各样的特色外设。我们经常说的 GPIO、串口、I2C、SPI 这些外设就挂载在这两条总线上,这个是我们学习 STM32 的重点,就是要学会编程这些外设去驱动外部的各种设备。
3.总线矩阵
总线矩阵协调内核系统总线和DMA主控总线之间的访问仲裁,仲裁利用轮换算法。在互联型产品中,总线矩阵包含5个驱动部件(CPU的DCode、系统总线、以太网DMA、DMA1总线和DMA2总线)和3个从部件(闪存存储器接口(FLITF)、SRAM和AHB2APB桥)。在其它产品中总线矩阵包含4个驱动部件(CPU的DCode、系统总线、DMA1总线和DMA2总线)和4个被动部件(闪存存储器接口(FLITF)、SRAM、FSMC和AHB2APB桥)。AHB外设通过总线矩阵与系统总线相连,允许DMA访问。
4.AHB/APB桥(APB)
两个AHB/APB桥在AHB和2个APB总线间提供同步连接。APB1操作速度限于36MHz,APB2操作于全速(最高72MHz)。
有关连接到每个桥的不同外设的地址映射请参考表1。在每一次复位以后,所有除SRAM和FLITF以外的外设都被关闭,在使用一个外设之前,必须设置寄存器RCC_AHBENR来打开该外设的时钟。
注意: 当对APB寄存器进行8位或者16位访问时,该访问会被自动转换成32位的访问:桥会自动将8位
或者32位的数据扩展以配合32位的向量。
存储器组织
程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。
存储器区域功能划分
在这 4GB 的地址空间中,ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,具体分类见表格 6-1。每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。
在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。
Block0 用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设,下面我们简单的介绍下这三个 Block 里面的具体区域的功能划分。
存储器 Block0 内部区域功能划分
Block0 主要用于设计片内的 FLASH, STM32F103ZET6是 512KB,属于大容量。要在芯片内部集成更大的 FLASH 或者 SRAM 都意味着芯片成本的增加,往往片内集成的 FLASH 都不会太大,ST 能在追求性价比的同时做到 512KB,实乃良心之举。Block 内部区域的功能划分具体见
表格 6-2。
储存器 Block1 内部区域功能划分
Block1 用 于 设 计 片 内 的 SRAM 。 STM32F103ZET6 的 SRAM 都是 64KB,Block 内部区域的功能划分具体见表格6-3
表格 6-3 存储器 Block1 内部区域功能划分
储存器 Block2 内部区域功能划分
Block2 用于设计片内的外设,根据外设的总线速度不同,Block 被分成了 APB 和 AHB两部分,其中 APB 又被分为 APB1 和 APB2,具体见表格 6-4。
以上解释了stm32f103的整体结构,也讲解了存储器地址如何分配,那我们怎么操作这些存储器内的内容单元呢?毕竟微控制器就是通过cpu读取存储器中的指令把最终运算得到的数据输出的过程,而我们编程的过程就是告诉cpu怎么样读取存储器的指令执行的、存储器中放了那些内容让CPU去读取、寄存器中内容是什么要实现什么功能等等,编程的过程就是对这些存储器/寄存器赋值的过程,然后交给CPU去运算,所以我们必须给这些存储器所对应的地址起个名方便我们对这些存储器内容的操作。起名字的过程叫做寄存器映射。寄存器也是存储器的一种。
寄存器映射
我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?
在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
下面以——让GPIOB端口的16个引脚输出高电平,要怎么实现?——这个问题来看看寄存器映射的过程。
通过绝对地址访问内存单元
1、0X40010C0C 是GPIOB输出数据寄存器ODR的地址,如何找到?
2、(unsigned int*)的作用是什么?
将0x40010c0c这个立即数 强制类型位无符号整形地址 (说白了就是告诉编译器0x40010c0c这个对应的不是立即数而是一个无符号的整形地址)
3、学会使用C语言的 * 号
*这的符号用法很多比方上面(unsigned int*)+立即数就是上面解释的意思
那么 *地址 是什么意思呢就像上面 *(unsigned int*) (0x40010c0c) 这样的形式就代表了这种无符号整形地址对应的内存空间
*(unsigned int*) (0x40010c0c) = 0xFFFF; 就是对这个内存空间赋值0xFFFF
那么stm32中如何去对这些寄存器操作呢?
通过寄存器别名方式访问内存单元
上面就是stm32对寄存器内存单元的操作形式,对相应寄存器对应的地址进行宏定义 (已将将立即数强制转换成无符号整形地址)之后就是 *(宏定义名)就代表是这个寄存器的内存单元了,但是这么操作还不是很完美毕竟在宏定义寄存器的名字前面还加了一个*,不想在51单片机中直接P1就能代表P1这个I/o端口一样 。
所以为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面
当然stm32这么多的寄存器我们都要这样一个一个去命名吗?
②偏移地址
当然不会,这样学习stm32开发成本高了些,不说别的,只是单纯对寄存器命名就有很大的工作量,这样谁还会去选择这样的芯片去开发,所以就有了库函数这样的东西,里面把所有的寄存器都已经按照上面介绍的方式已经封装好了,我们拿出来使用就ok了,库函数不只是这点功能,不要着急以后的章节里会慢慢道来。
什么是寄存器?
给有特定功能的内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元
取别名的过程就叫寄存器映射
就像上面对 *(unsigned int*) (0x40010c0c) 命名GPIO_ODR ;
这个宏定义可以随便命名,你可以命名成zhangsan/lisi,但是,这样的命名方式肯定不行的,不好记忆也不好类比。
什么叫存储器映射?
给存储器分配地址的过程叫存储器映射,再分配一个地址叫重映射。
既然说道对寄存器命名就要知道各个寄存器对应的地址是什么呢?
总线基地址(对应总线的起始地址)
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫 XX 外设的边界地址。
这里面我们以 GPIO 这个外设来讲解外设的基地址,GPIO 属于高速的外设 ,挂载到APB2 总线上。
GPIO基地址(外设是什么)(挂接在总线上的设备)
外设寄存器
在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例,GPIO是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。
GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOB 端口为例,来说明 GPIO 都有哪些寄存器
同一个外设也有不同的功能,那不同的功能是如何来实现的呢?
想要外设工作、想要外设以什么样的功能工作,就需要对外设内的寄存器赋特定的值,外设内的寄存器所赋的值不同,最终实现的功能也有所不同。
GPIOB端口的寄存器列表
上面是对GPIOB端口下的寄存器的命名,每个寄存器都是32位的,每一个寄存器都有不同的功能,,而且对同一个寄存器赋予不同的值时,最终的效果也不同,下面看看GPIOX_ODR(X可以代表A/B/C...)这个寄存器下的每一位都有什么作用吧
GPIOx端口数据输出寄存器ODR描述
如何理解上边寄存器的说明呢?
①名称
寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…E)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为 A-E,也就是说这个寄存器说明适用于 GPIOA、GPIOB 至 GPIOE,这些 GPIO 端口都有这样的一个寄存器。
偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18,从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4001 0800 ,我们就可以算出GPIOA 的这个 GPIOA_BSRR 寄存器的地址为:0x4001 0800+0x18 ;同理,由于 GPIOB 的外设基地址为 0x4001 0C00,可算出 GPIOB_BSRR 寄存器的地址为:0x4001 0C00+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 是相反的操作。
C语言对寄存器的封装过程
总线和外设基址宏定义
让PB0输出低/高电平,要怎么实现?
为什么使用结构体封装寄存器列表?
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节,具体见图 6-7。
也就是说,我们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x40010C00(这也是第一个成员变量 CRL 的地址), 那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04 ,加上的这个 0x04 ,正是代表 CRL 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,具体见代码 6-7。
使用结构体指针访问寄存器
这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指向地址GPIOB_BASE(0x4001 0C00),使用地址确定下来,然后根据 C 语言访问结构体的语法,用GPIOx->ODR 及 GPIOx->IDR 等方式读写寄存器。最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可,具体代码 6-8。
定义GPIOX端口基地址指针
这里我们仅是以GPIO这个外设为例,给大家讲解了C语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。
我们向往远方,却忽略了此刻的美丽