今天主要是复习一下。 结合野火的《零基础开发指南》名字没记住大概是这个 先放一张结构图
存储器映射(初学重点): 我们的片内外设比如:Flash,Sram,Fsmc,以及挂在AHB 总线上的外设,我们都需要知道他的地址来操作这些器件。而这些外设的地址都被分配在一个4g的内存空间里(4g的存储器,下文中的)。 为什么是4g的? 2的32次方就是4g-byte。 存储器映射 存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见图存储器映射。如果给存储器再分配一个地址就叫存储器重映射。 注意看4g的存储空间分位8个部分,每部分都给分配了各自的起始地址和末尾的地址信息。每块占用512MByte 显然8*512=4096MByte 4096MByte/1024=4g 拿其中几个举个例子: 第一部分就包含了Flash 第二部分就包含了Sram我的板子买的霸道,也就是F103zet6 64k的SRAM 第三部分就是FSMC 我是初学,咱时不管这部分(用到啥学啥,你没那么好的记性和时间) 其余的暂时不列出来。
重点来了!!!!!!! 我们操作的主要部分时在BLOCK2 我们首先得明白,分配的这些地址首先他是连续的。这一点很重要。 那么BLOCK2包含了哪些?? 眼神好的自己看 《STMF103X英文数据手册》 简言之,这部分详细的描述了我们的外设及其分配到的地址信息。(有大用) 以端口举例,详细的描述了各端口的起始地址和终止地址,同样也是连续的。可以数数看。 对于这8个块,主要看三块就可以,BLOCK0对应的FLASH,BLOCK1对应的SRAM,BLOCK2对应的片上外设。 我们先直接看BLOCK2部分 BLOCK2上有两个总线,AHB和APB,这两个总线主要区别在于其速度不同。 APB又被分为APB1和APB2 而APB2和AHB被称为高速总线,挂高速外设,APB1是低速总线,挂低低速外设。 寄存器的映射 在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作(实际就是每32个位也就是4个字节为一个寄存器,而每一个寄存器负责一个具体的功能。巧了我们的单片机正好就是32位的,他正好能一次处理32位的数据)。 我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个 内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。(了解一下,后面在代码中一下就能明白)。 比如: 我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x40010C0C(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB的 16 个 IO 都输出高电平。
操作寄存器的方法一:通过绝对地址访问内存单元
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
简单解释一下 0x4001 0C0C是GPIOB的ODR寄存器的地址,先别管他怎么来的。只需要知道我们操作这个地址就可以控制寄存器对应的GPIO输出。 只不过对于单片机来说这个地址是一个变量,是一个立即数,而不是地址。(这里我没太明白。上网查了一下,我的理解就是说这个只是个数据而无实际意义,我们将他给Int变量那么这个就是int型数据,把他给指针变量那他就是地址,如果不赋值给一个具体的变量类型那他就啥也不是)。 我们需要强制转化为地址才行,所以使用了(unsigned int),也就是说这个地址是一个32位无符号整型的指针(指针就是地址)。* 对于这个地址空间其存储了一个数据0xFFFF。(一定要懂) 说明一下0xFFFF,我们知道对于一个16进制数 来说,一个F对应的4位。那么4个F对应的正好就是16位,而我们的ODR寄存器也正好能够操作的就只有16位。(后面针对ODR寄存器讲解的时候能用到) 而unsigned int 是32位的,对于0x4001 0C0C 是够用的!
那么使用绝对地址访问寄存器的方式缺点就很明显,地址不好记啊,也不好写啊。 操作寄存器的方法二:通过寄存器别名的方式访问寄存器
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;
使用宏定义#define 给寄存器的地址重新命名为GPIOB_ODR 然后使用* 号去操作ODR的值 这就是基本的指针操作,不做解释对于(GPIOB_BASE+0x0C)暂时不必纠结。只需要知道他是寄存器ODR的地址。
上面通过寄存器别名访问寄存器更换好的写法是
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xff;
下面介绍STM32的外设地址映射 片上外设区分为三条总线AHB总线和APB1,APB2(APB1和APB2共同构成APB总线),根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。 相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。(这句就很有用,记住要考) 其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
表格总线基地址 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000 的差值。(这个差值就是相对总线基地址的偏移量) 这里还不够直观我重新描述一下(对比以下三张图) 在STM32F103x的中文参考手册里能够找到
在STM32F03Xz
我们知道TM2是属于APB1总线的,并且与APB1总线的基地址的值是一样的。那么GPIOB是属于APB2总线的,而APB2总线的基地址是 对比APB1和APB2的地址,就能得到地址的差值(就是偏移量),0x0001 0000
外设基地址 我们知道总线地址的范围,还需要知道具体要操作的外设的地址。将目标进一步的细化。 这里以GPIO为例子。 我们可以对照一下这两张图。第一个是野火给出的,第二个来自数据手册 很显然我们的GPIO的外设地址范围是0x4001 0800到0x4001 23FF这个范围内 有ABCDEFG这几个端口。 查看参考手册知道了这几个GPIO端口都是挂接在APB2这个总线上的。 而APB2是高速总线,总线基地址是 我们的APB2的总线基地址是0x4001 0000 这么一来就能知道端口相对与APB2总线基地址的偏移量 我们知道了外设的基地址还没结束,我们知道最终我们是需要操作寄存器的,所以我们接下来就要去找对应外设的寄存器的地址信息。 以GPIOB为例子 我们在知道了外设基地址的时候比如GPIOB为0x4001 0C00 每个GPIO都 有很多个寄存器,而每一个寄存器都有特定的功能。 每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。 然后我们就找到的最终的目标,寄存器地址。 野火指南里给出了分析寄存器功能的方法,这里以端口位设置清零寄存器为例子。 ① 名称 寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…E)”这段的 意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为 A-E,也就是说这个寄 存器说明适用于 GPIOA、GPIOB 至 GPIOE,这些 GPIO 端口都有这样的一个寄存 器。(注意区分,这里说GPIOA到GPIOE都有这样的一个寄存器,别忘了他们各自的GPIO的基地址是不同的,虽然偏移量是相同的) ② 偏移地址 偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x10,从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4001 0800 ,我们就 可以算出 GPIOA 的这个 GPIOA_BSRR 寄存器的地址为:0x4001 0800+0x10;同 理,由于 GPIOB 的外设基地址为 0x4001 0C00,可算出 GPIOB_BSRR 寄存器的 地址为:0x4001 0C00+0x10。其他 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 位执行任何操作;(给BRy赋值为0,不会影响到ODR寄存器的值) 1:对相应ODRx 位进行复位”。 这里的“复位”是将该位设置为 0 的意思,而“置位”表示将该位设置为 1; 说明中的 ODRx 是另一个寄存器的寄存器位,我们只需要知道 ODRx 位为 1 的时候,对应的引脚 x 输出高电平,为 0 的时候对应的引脚输出低 电平即可 (感兴趣的读者可以查询该寄存器 GPIOx_ODR 的说明了解)。所以,如 果对 BR0 写入“1”的话,那么 GPIOx 的第 0 个引脚就会输出“低电平”,但是 对 BR0 写入“0”的话,却不会影响 ODR0 位,所以引脚电平不会改变。要想该 引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位 BSy 与 BRy 是相 反的操作。(这部分先了解,之后通过编程具体操作时就能明白)
使用C语言对寄存器进行封装(重点) 总线和外设基地址宏定义
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000
#define GPIOB_CRL (GPIOB_BASE+0x00)
#define GPIOB_CRH (GPIOB_BASE+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)
这里总共提到了4部分 第一部分是8个分区其中的片内外设地址: 一个是最外层的BLOCK2的片内外设基地值,我们所有的外设都在这512m字节的空间内部。(如果忘记8个分区时什么,请往前面找)
第二部分是总线基地址: 而在BLOCK2的内部总共分为APB1,APB2,AHB三个总线。所以我们就在片内外设基地址的基础之上加上偏移量,就能得到APB1,或者APB2,或者AHB的总线的地址。
第三部分是外设地址: 然后我们在这三个总线的基地址上加上偏移量就能够找到具体的外设。
第四个部分是寄存器地址: 然后在具体的外设的地址上加上偏移量就能找到具体的寄存器。
下面我将以代码的形式展现如何通过操作寄存器的地址来实现控制GPIO口 头文件
# define PERIPHBASE ((unsigned int)0x40000000)
# define APB1PERIPH_BASE PERIPHBASE
# define APB2PERIPH_BASE (PERIPHBASE + 0x10000)
# define AHBPERIPH_BASE (PERIPHBASE + 0x20000)
# define RCC_BASE (AHBPERIPH_BASE + 0x1000)
# define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
# define RCC_APB2ENR *(unsigned int* )(RCC_BASE + 0x18)
# define GPIOB_CRL *(unsigned int*)(GPIOB_BASE + 0x00)
# define GPIOB_CRH *(unsigned int*)(GPIOB_BASE + 0x04)
# define GPIOB_ODR *(unsigned int*)(GPIOB_BASE + 0x0C)
接下来就是main.c
#if 0
# include <reg51.h>
sbit LED = P0^0;
void main ()
{
P0 = 0xFE;
LED = 0;
}
#endif
#include "stm32f10x.h"
void SystemInit(void);
int main (void)
{
# if 0
*(unsigned int *)0x40021018 |=((1)<<3);
*(unsigned int *)0x40010C00 |=((1)<<(4*0));
*(unsigned int *)0x40010C0C &=~((1)<<0);
#else
RCC_APB2ENR |= ((1)<<3);
GPIOB_CRL |= ((1)<<(4*0));
GPIOB_ODR |= (1<<0);
#endif
}
void SystemInit(void)
{
}
对寄存器配置部分代码的说明:
*(unsigned int *)0x40021018 |=((1)<<3);
*(unsigned int *)0x40010C00 |=((1)<<(4*0));
*(unsigned int *)0x40010C0C &=~((1)<<0);
下面详细介绍一下,这段代码中使用到的三个寄存器: 第一个是:RCC下面的APB2外设时钟使能寄存器(RCC_APB2ENR) 我们知道在GPIOB是挂接到APB2这条总线上的,而与51不同的是,32在操作GPIO是需要使能他的IO时钟的。 IO端口B的时钟使能是1打开,0关闭。 所以才有了下面的使能操作
*(unsigned int *)0x40021018 |=((1)<<3);
第二个是端口配置低寄存器(GPIOxCRL) CRL寄存器决定了IO端口是工作在输入还是输出模式 另外他还决定了速度 我们此处使用的是通用推挽输出,和10Mhz的速度 手册中规定了CNF0和MODE0控制着端口位0。 通用推挽输出要求CNFNy为00,MODEy为01
*( unsigned int * )0x40010C00 &= ~( (0x0f) << (4*0) );
*( unsigned int * )0x40010C00 |= ( (1) << (4*0) );
*( unsigned int * )0x40010C00 &= ~( (0x0f) << (4*0) );
第三个是端口输出数据寄存器(GPIOx_ODR) 端口输出数据寄存器显然只有16位而已 我们需要操作ODR的具体某一个位只需要直接赋值为1或者0即可。
//将最低位清零的操作
*(unsigned int *)0x40010C0C &=~((1)<<0);
//将最低位置一的操作
*(unsigned int *)0x40010C0C |=((1)<<0);
//另外我需要说明一下上面的代码
//指针是unsigned int的,意味着0x40010C0C是一个真正意义上的16进制的32位的地址。
//这样一来我们后面的1可以看作是一个32位十六进制的0x0000 0001其实还是1而已,通过<<0。也就是左移动0位然后或等于的操作来实现按位操作。
//
我们操作的就是PB0(端口B的第0位,PortB_0) 好了,通过基本的寄存器的绝对地址指针,实现点亮LED的操作就是这样
|