一、什么是嵌入式?
嵌入式系统是小型计算机的一个分支系统。平常用的PC,就属于功能比较专一的计算机,从核心的处理器来说,可以分成嵌入式微处理器和嵌入式微控制器,我们传统意义上的那种单片机,比如说像51、AVR还有按里面比较低配的一些,比如说像Cortex-M系列的这一类,我们都把它划分为微控制器,微处理器呢,就相对来说处理能力,运算能力要强一些,比如ARM9以上的系列和 Cortex-A以及以上系列。STM32属于一个微控制器,请大家牢牢记住微控制器这四个字。STM32自带了各种常用通信接口,比如USART、I2C、SPI等,可接非常多的传感器,可以控制很多的设备。现实生活中,我们接触到的很多电器产品都有STM32的身影,比如智能手环,微型四轴飞行器,平衡车、移动POST机,智能电饭锅,3D打印机等等。
二、STM32长啥样
以STM32F429IGT6为例。芯片正面是丝印,ARM表示该芯片使用的是ARM的内核,STM32F429IGT6是芯片型号。芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,连接到各种传感器上,然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作,通过做实验的方式来学习STM32芯片的各个资源。
三、芯片里面有什么
我们看到的STM32芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。
STM32F429采用的是Cortex-M4内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。
四、存储器映射
请牢记住这一句话连接被控总线的是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大小的呢?
- 我们首先把这些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=1024*1024个字节)
1KByte=1024Byte, (1KB=1024个字节)
1Byte=8bit(一个字节由8个二进制位组成)
int i;int类型占4个字节就是4*8=32个bit位(最大可表示的数为2^31-1=2147483647)
double i;double类型占8个字节就是8*8=64个bit位(最大可表示的数为2^63-1)
至此我们就清楚了4GB的大小空间,在内存中有多少的个地址。
在这4GB中,分为8块。每一块的大小就是512MB字节。
其中第三块,也就是Block2地址从0x4000 0000 到 0x5FFF FFFF
分配给了我们的片上外设用如GPIO、USART(串口)、I2C、SPI等。在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Boock0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设。
4.1、存储器Block0内部区域功能划分
Block0主要用于设计片内的FLASH,F429系列片内部FLASH最大是2MB,我们使用的STM32F429IGT6的FLASH是1MB。要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大,ST能在追求性价比的同时做到1MB以上,实乃良心之举。Block内部区域的功能划分具体见下图。
4.2、储存器Block1内部区域功能划分
Block1用于设计片内的SRAM。F429内部SRAM的大小为256KB,其中64KB的CCM RAM位于Block0,剩下的192KB位于Block1,分SRAM1112KB,SRAM216KB,SRAM364KB,Block内部区域的功能划分具体见下图。
4.3、储存器Block2内部区域功能划分
Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB
两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2,从小到大依次是APB1、APB2、AHB1、AHB1
。具体见下图。还有一个AHB3包含了Block3/4/5/6,这四个Block用于扩展外部存储器,如SDRAM,NORFLASH和NANDFLASH等。
在这里我希望大家一看到地址就要知道它是在内存的哪一个区域的,要会熟练地把地址和内存大小空间联系起来。如果你第一次不懂没关系,在后面遇到了一定要回过头来再看一遍,直到看懂为止。
五、寄存器映射
上面讲的是存储器映射,就是给存储器划分大小,分配地址,给存储器编号。
下面讲的是寄存器映射,就是给寄存器划分大小,分配地址,给寄存器编号。
在存储器 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 都有哪些寄存器
这里我们以“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引脚,而 BR1、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 上加入各个总线的地址偏移,得到 APB1、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 ,假如这个结构体的首地址为 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