STM32第一章-寄存器你懂吗

1、什么是嵌入式

嵌入式系统是小型计算机的一个分支系统。日常用的PC,就属于功能比较专注的计算机,从核心的处理器来讲,能够分红嵌入式微处理器和嵌入式微控制器,咱们传统意义上的那种单片机,好比说像5一、AVR还有按里面比较低配的一些,好比说像Cortex-M系列的这一类,咱们都把它划分为微控制器,微处理器呢,就相对来讲处理能力,运算能力要强一些,好比ARM9以上的系列和 Cortex-A以及以上系列。STM32属于一个微控制器,请你们紧紧记住微控制器这四个字。STM32自带了各类经常使用通讯接口,好比USART、I2C、SPI等,可接很是多的传感器,能够控制不少的设备。现实生活中,咱们接触到的不少电器产品都有STM32的身影,好比智能手环,微型四轴飞行器,平衡车、移动POST机,智能电饭锅,3D打印机等等。编程

2、STM32长啥样

以STM32F429IGT6为例。芯片正面是丝印,ARM表示该芯片使用的是ARM的内核,STM32F429IGT6是芯片型号。芯片四周是引脚,左下角的小圆点表示1脚,而后从1脚起按照逆时针的顺序排列(全部芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,链接到各类传感器上,而后在STM32上编程(实际就是经过程序控制这些引脚输出高电平或者低电平)来控制各类传感器工做,经过作实验的方式来学习STM32芯片的各个资源。 在这里插入图片描述小程序

3、芯片里面有什么

咱们看到的STM32芯片已是已经封装好的成品,主要由内核片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。 STM32F429采用的是Cortex-M4内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术受权。芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核以外设计部件并生产整个芯片,这些内核以外的部件被称为核外外设或片上外设。如GPIO、USART(串口)、I2C、SPI等都叫作片上外设。markdown

4、存储器映射

请牢记住这一句话链接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。咱们在编程的时候,操做的也正是这些功能部件。存储器自己不具备地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射。若是给存储器再分配一个地址就叫存储器重映射寄存器映射 这个图很是很是重要,初学者可能看不懂这个图。接写来我就详细的将讲解这一张图,让你真正的明白什么是内存,什么是寄存器,什么是寄存器映射。学习

首先看这个图最左边的一竖排方格。咱们前面说到链接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。这里的4GB的地址空间就分布在这个图最左边的一竖排方格中。从0x0000 0000到0xFFFF FFFF。一看到这个东西你们可能又不知道这是个啥,为啥子地址要这样写。0x表明16进制,其实一看到F就知道这是十六进制了(0 1 2 3 4 5 6 7 8 9 A B C D E F16个数)。那0x0000 0000到0xFFFF FFFF是怎么算成是4GB大小的呢?ui

  • 咱们首先把这些16进制化成2进制来看看,就是 从0000 0000 0000 0000 0000 0000 0000 0000 到 1111 1111 1111 1111 1111 1111 1111 1111 要清楚16进制的1位数在2进制中就要表示为4位。例如16进制的F在二进制中2就表示为1111。 从0000 0000 0000 0000 0000 0000 0000 0000 到 1111 1111 1111 1111 1111 1111 1111 1111 一共有2^(32)=4294967296个字节。 4294967296byte/1024=4194304KB, 4194304KB/1024=4096MB, 4096MB/1024=4GB.

额外的小知识 1TByte=1024GByte, 1GByte=1024MByte, 1MByte=1024KByte,(1MB=10241024个字节) 1KByte=1024Byte, (1KB=1024个字节) 1Byte=8bit(一个字节由8个二进制位组成) int i;int类型占4个字节就是48=32个bit位(最大可表示的数为2^31-1=2147483647) double i;double类型占8个字节就是8*8=64个bit位(最大可表示的数为2^63-1)spa

至此咱们就清楚了4GB的大小空间,在内存中有多少的个地址。 内存地址描述图 在这4GB中,分为8块。每一块的大小就是512MB字节。 存储器功能分类其中第三块,也就是Block2地址从0x4000 0000 到 0x5FFF FFFF分配给了咱们的片上外设用如GPIO、USART(串口)、I2C、SPI等。在这8个Block里面,有3个块很是重要,也是咱们最关心的三个块。Boock0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设。设计

4.一、存储器Block0内部区域功能划分

Block0主要用于设计片内的FLASH,F429系列片内部FLASH最大是2MB,咱们使用的STM32F429IGT6的FLASH是1MB。要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增长,每每片内集成的FLASH都不会太大,ST能在追求性价比的同时作到1MB以上,实乃良心之举。Block内部区域的功能划分具体见下图。 存储器Block0内部区域功能划分3d

4.二、储存器Block1内部区域功能划分

Block1用于设计片内的SRAM。F429内部SRAM的大小为256KB,其中64KB的CCM RAM位于Block0,剩下的192KB位于Block1,分SRAM1112KB,SRAM216KB,SRAM364KB,Block内部区域的功能划分具体见下图。 储存器Block1内部区域功能划分指针

4.三、储存器Block2内部区域功能划分

Block2用于设计片内的外设,根据外设的总线速度不一样,Block被分红了APB和AHB两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2,从小到大依次是APB一、APB二、AHB一、AHB1。具体见下图。还有一个AHB3包含了Block3/4/5/6,这四个Block用于扩展外部存储器,如SDRAM,NORFLASH和NANDFLASH等。 储存器Block2内部区域功能划分 在这里我但愿你们一看到地址就要知道它是在内存的哪个区域的,要会熟练地把地址和内存大小空间联系起来。若是你第一次不懂不要紧,在后面遇到了必定要回过头来再看一遍,直到看懂为止。code

5、寄存器映射

上面讲的是存储器映射,就是给存储器划分大小,分配地址,给存储器编号。 下面讲的是寄存器映射,就是给寄存器划分大小,分配地址,给寄存器编号。

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

好比,咱们找到 GPIOH 端口的输出数据寄存器 ODR的地址是 0x40021C14(至于这个地址如何找到能够先跳过,后面咱们会有详细的讲解),ODR 寄存器是 32bit,就是说ODR 寄存器在内存中占据4个空位即四个地址(我以为这样说比较形象,比较好理解)低16bit(后面两个格子能够操做)有效,对应着 16个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。如今咱们经过 C语言指针的操做方式,让 GPIOH 的16个IO都输出高电平。

// GPIOH 端口所有输出 高电平
*(unsigned int*)(0x4002 1C14) = 0xFFFF;
复制代码

有人会说咋不写0x0000 FFFF 这样看不是更直观吗?其实0x0000 FFFF和0xFFFF表示的意思同样,就好比1和01都表示1,直接写1不是更简单吗?对吧。可是你有这个想法很好,说明你在思考。

0x40021C14在咱们看来是 GPIOH端口 ODR的地址,可是在编译器看来,这只是一个普通的变量,是一个当即数,要想让编译器也认为是指针,咱们得进行强制类型转换,把它转换成指针,即(unsigned int*)0x40021C14,而后再对这个指针进行* 操做。刚刚咱们说了,经过绝对地址访问内存单元很差记忆且容易出错,咱们能够经过寄存器的方式来操做.

// GPIOH 端口所有输出 高电平
# define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14)
GPIOH_ODR = 0xFF;
复制代码

5.1.STM32的外设地址映射

上面讲的是存储器映射,就是给存储器划分大小,分配地址,给存储器编号。 寄存器映射,就是给寄存器划分大小,分配地址,给寄存器编号。下面讲STM32的外设地址映射,就是给外设地址划分大小,从新分配地址,给外设地址编号。

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

5.1.1总线基地址

总线基地址
总线基地址

5.1.2外设基地址

总线上挂载着各类外设,这些外设也有本身的地址范围,特定外设的首个地址称为、“XX外设基地址”,也叫 XX外设的边界地址。 外设基地址GPIOA的基址相对于 AHB1总线的地址偏移为 0,咱们应该就能够猜到,AHB1总线的第一个外设就是 GPIOA。

5.1.3外设寄存器

在 XX外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例,GPIO是通用输入输出端口的简称,简单来讲就是 STM32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚链接到 LED 灯的阴极,LED 灯的阳极接电源,而后经过 STM32控制该引脚的电平,从而实现控制 LED 灯的亮灭。GPIO有不少个寄存器,每个都有特定的功能。每一个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里咱们以 GPIOH 端口为例,来讲明 GPIO 都有哪些寄存器 GPIOH 端口 这里咱们以“GPIO 端口置位/复位寄存器”为例,教你们如何理解寄存器的说明, GPIO 端口置位/复位寄存器说明)

  • 名称 寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”能够为 A-I,也就是说这个寄存器说明适用于 GPIOA、GPIOB 至 GPIOI,这些 GPIO 端口都有这样的一个寄存器。
  • 偏移地址 偏移地址是指本寄存器相对于这个外设的基地址的偏移本寄存器的偏移地址是 0x18,从参考手册中咱们能够查到 GPIOA外设的基地址为 0x4002 0000 ,就是AHB1这一块内存的首地址,GPIO是挂载在咱们AHB1的总线上面的。咱们就能够算出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引脚,而 BR一、BS1 就是控制 GPIOA第 1个引脚。其中 BRy引脚的说明是“0:不会对相应的 ODRx 位执行任何操做;1:对相应 ODRx位进行复位”。这里的**“复位”是将该位设置为 0的意思**,而**“置位”表示将该位设置为1**;说明中的 ODRx 是GPIO的另外一个寄存器的寄存器位,咱们只须要知道 ODRx位为 1 的时候,对应的引脚 x输出高电平,为 0 的时候对应的引脚输出低电平。因此,若是对 BR0 写入“1”的话,那么 GPIOx的第0 个引脚就会输出“低电平”,可是对 BR0写入“0”的话,却不会影响 ODR0位,因此引脚电平不会改变。要想该引脚输出“高电平”,就须要对“BS0”位写入“1”,寄存器位BSy与 BRy是相反的操做。

这个功能说明建议多读几遍,反复的去读,直达完全理解为止。

5.2.C语言对寄存器的封装

5.2.1封装总线和外设基地址

在编程上为了方便理解和记忆,咱们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字做为宏名

/* 外设基地址 */
#define PERIPH_BASE ((uint32_t)0x40000000) // 0x40000000是APB1的首地址,请看最前面的的那张图
#define APB1PERIPH_BASE PERIPH_BASE//使用宏定义 用APB1PERIPH_BASE代替PERIPH_BASE
//下面依次内推便可获得GPIOA_BASE~GPIOK_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)
#define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000)
#define GPIOJ_BASE (AHB1PERIPH_BASE + 0x2400)
#define GPIOK_BASE (AHB1PERIPH_BASE + 0x2800)
/* 寄存器基地址,以 GPIOA 为例 */
#define GPIOA_MODER (GPIOA_BASE+0x00)
#define GPIOA_OTYPER (GPIOA_BASE+0x04)
#define GPIOA_OSPEEDR (GPIOA_BASE+0x08)
#define GPIOA_PUPDR (GPIOA_BASE+0x0C)
#define GPIOA_IDR (GPIOA_BASE+0x10)
#define GPIOA_ODR (GPIOA_BASE+0x14)
#define GPIOA_BSRR (GPIOA_BASE+0x18)
#define GPIOA_LCKR (GPIOA_BASE+0x1C)
#define GPIOA_AFRL (GPIOA_BASE+0x20)
#define GPIOA_AFRH (GPIOA_BASE+0x24)
复制代码

代码首先定义了 “片上外设”基地址 PERIPH_BASE(0x40000000),接着在 PERIPH_BASE 上加入各个总线的地址偏移,获得 APB一、APB2 等总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,获得 GPIOA到GPIOK的外设地址,最后在外设地址上加入各寄存器的地址偏移,获得特定寄存器的地址。一旦有了具体地址,就能够用指针操做读写了,具体见代码

/* 控制 GPIOA 引脚 10 输出低电平(BSRR 寄存器的 BR10 置 1) */
*(unsigned int *)GPIOA_BSRR = (0x01<<(16+10));
/* 控制 GPIOA 引脚 10 输出高电平(BSRR 寄存器的 BS10 置 1) */
*(unsigned int *)GPIOA_BSRR = 0x01<<10;
unsigned int temp;
/* 控制 GPIOH 端口全部引脚的电平(读 IDR 寄存器) */
temp = *(unsigned int *)GPIOA_IDR;
复制代码

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

5.2.2封装寄存器列表

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

typedef unsigned int uint32_t;       /*无符号 32 位变量 占4个字节*/
typedef unsigned short int uint16_t; /*无符号 16 位变量 占2个字节*/
/* 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 个字节,具体见。 GPIO_TypeDef 结构体成员的地址偏移 也就是说,咱们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x40021C00(这也是第一个成员变量 MODER的地址), 那么结构体中第二个成员变量OTYPER的地址即为 0x4002 1C00 +0x04 ,加上的这个 0x04 ,正是表明 MODER所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的 BSRR寄存器分红了低 16位 BSRRL和高 16位 BSRRH,BSRRL置 1 引脚输出高电平,BSRRH 置 1引脚输出低电平,这里分开只是为了方便操做。 这样的地址偏移与 STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址肯定下来,而后就能以结构体的形式访问寄存器了,具体见代码

GPIO_TypeDef * GPIOx;      //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
GPIOx = GPIOH_BASE; //把指针地址设置为宏 GPIOH_BASE 地址
GPIOx->BSRRL = 0xFFFF; //经过指针访问并修改 GPIOH_BSRRL 寄存器
GPIOx->MODER = 0xFFFFFFFF; //修改 GPIOH_MODER 寄存器
GPIOx->OTYPER =0xFFFFFFFF; //修改 GPIOH_OTYPER 寄存器
uint32_t temp;
temp = GPIOx->IDR; //读取 GPIOH_IDR 寄存器的值到变量 temp 中
复制代码

这段代码先用 GPIO_TypeDef类型定义一个结构体指针 GPIOx,并让指针指向地址GPIOH_BASE(0x4002 1C00),使用地址肯定下来,而后根据 C语言访问结构体的语法,用GPIOx->BSRRL、GPIOx->MODER及 GPIOx->IDR 等方式读写寄存器。最后,咱们更进一步,直接使用宏定义好 GPIO_TypeDef类型的指针,并且指针指向各个 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)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)
复制代码

这里咱们仅是以 GPIO 这个外设为例,给你们讲解了 C 语言对寄存器的封装。以此类推,其余外设也一样能够用这种方法来封装。好消息是,这部分工做都由固件库帮咱们完成了,这里咱们只是分析了下这个封装的过程,让你们知其然,也只其因此然。 下面写一个小程序,其余的都已经省略:

GPIO_InitTypeDef  GPIO_InitStructure;//GPIO_InitTypeDef是一个结构体,GPIO_InitStructure是定义的一个结构体变量,你能够起名字为阿猫阿狗均可以,只不过咱们习惯用GPIO_InitStructure,起到见明知意的效果,过有人都看得懂。
/* 第1步:打开GPIOA时钟,必须的一步 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
/* 第2步:配置全部的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; /* PA13 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;/* 设为输入口 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;/* 设为推挽模式 */
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;/* 无需上下拉电阻 */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;/* IO口最大速度 */
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 第1步:打开GPIOA的第13个引脚置位操做 */
GPIO_WritePin(GPIOA,GPIO_PIN_13,GPIO_PIN_SET); //引脚PA13拉高,置位1
GPIO_WritePin(GPIOA,GPIO_PIN_13,GPIO_PIN_RESET); //引脚PA13拉低,置位0
复制代码
相关文章
相关标签/搜索