当前位置:网站首页>零基础自学STM32-野火——GPIO复习篇——使用绝对地址操作GPIO

零基础自学STM32-野火——GPIO复习篇——使用绝对地址操作GPIO

2022-07-06 02:12:00 Fecter11

今天主要是复习一下。
结合野火的《零基础开发指南》名字没记住大概是这个
先放一张结构图
在这里插入图片描述

存储器映射(初学重点):
我们的片内外设比如: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 都输出高电平。

操作寄存器的方法一:通过绝对地址访问内存单元

// GPIOB 端口全部输出 高电平
 *(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 是够用的!

那么使用绝对地址访问寄存器的方式缺点就很明显,地址不好记啊,也不好写啊。
操作寄存器的方法二:通过寄存器别名的方式访问寄存器

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

使用宏定义#define 给寄存器的地址重新命名为GPIOB_ODR
然后使用* 号去操作ODR的值
这就是基本的指针操作,不做解释对于(GPIOB_BASE+0x0C)暂时不必纠结。只需要知道他是寄存器ODR的地址。

上面通过寄存器别名访问寄存器更换好的写法是

// GPIOB 端口全部输出 高电平
 #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语言对寄存器进行封装(重点)
总线和外设基地址宏定义

/*片内外设基地址(BLOCK2 的起始地址) */
#define PERIPH_BASE ((unsigned int)0x40000000)//peripheral就是外设的意思
//注意这里使用了强制类型转化,将0x4000 0000转化为一个unsigned int 型的整数



 /* 总线基地址 */
 #define APB1PERIPH_BASE PERIPH_BASE   //TIM2的外设基地址实际上就是我们的BLOCK2 512M内存空间的起始地址
 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)//这是APB2总线外设基地地址,注意相对于片内外设基地址加了偏移量
 #define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)//这是AHB外设总线基地址,注意是相对片内于外设基地址加了偏移量
 //这里的片内外设就是指BLOCK2的起始地址,我们知道BLOCK2上主要挂的就是我们的外设


 /* GPIO 外设基地址 */
 #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


/* 寄存器基地址,以 GPIOB 为例 */    //在外设地址上添加偏移量就是找到了寄存器的地址
 #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口
头文件

//用来存放stm32寄存器映射的代码
//外设基地址 peripheral

# define PERIPHBASE  ((unsigned int)0x40000000)   //片内外设基地址(属于BLOCK2区的起始地址)
//使用unsigned int 强制转化0x4000 0000,将其作为一个32位的整形去存储
	
//总线基地址

# define APB1PERIPH_BASE	PERIPHBASE   //APB1总线基地址与片内外设基地址是一样的
# define APB2PERIPH_BASE  (PERIPHBASE + 0x10000)   //APB2总线基地址,相对于片内外设基地址的偏移
//这里为什么可以直接使用加号而不用强制转化,应该是与之前使用强制转化PERIPHBASE的缘故,暂时不扩展往下继续
# define AHBPERIPH_BASE   (PERIPHBASE + 0x20000)    //AHB总线基地址(为了后面操作RCC时钟),这里实际采用的DMA1的地址作为基地址
//DMA1的地址是0x4002 0000
//RCC的地址是0x4002 1000 显然用DMA1的地址更方便一点

# define RCC_BASE	(AHBPERIPH_BASE + 0x1000)	//RCC复位时钟控制地址
# define GPIOB_BASE	(APB2PERIPH_BASE + 0x0C00)	//GPIOB地址

//以上的内容只是通过总线基地址加偏移量找到了外设的地址
//下面就是具体的寄存器的地址
# define RCC_APB2ENR	*(unsigned int* )(RCC_BASE + 0x18)  //APB2外设时钟使能寄存器
//这个寄存器的第3位控制IO端口B的时钟使能,0是关闭,1是开启


# define GPIOB_CRL	*(unsigned int*)(GPIOB_BASE  + 0x00)	//端口配置低寄存器,控制着0-7位的模式和速度
# define GPIOB_CRH 	*(unsigned int*)(GPIOB_BASE  + 0x04)	//端口配置高寄存器,控制着8-15位的模式和速度
# define GPIOB_ODR  *(unsigned int*)(GPIOB_BASE  + 0x0C)	//数据输出寄存器

接下来就是main.c

//类比51部分
#if 0
# include <reg51.h>
sbit LED = P0^0;  //51是直接使用位定义操作IO口输出高/电平的

void main ()
{
    
	
	P0 = 0xFE;//总线操作 //
	LED = 0;//位操作
	
}

#endif




//32部分
#include "stm32f10x.h"

void SystemInit(void);//不用管整个

int main (void)
{
    
	
	
	# if 0
	
	//使用指针控制寄存器
	*(unsigned int *)0x40021018 |=((1)<<3);        //控制RCC寄存器,打开gpiob端口的时钟,清零IO关闭,置1端口打开
	//为什么使用的是左移三位?
	//因为GPIOB的端口时钟使能位,正好就在第三位(从第0位开始数),所以左移三位
	
	*(unsigned int *)0x40010C00 |=((1)<<(4*0));    //控制CRL寄存器, Set_Output ODR_REG
	//*(unsigned int *)0x40010C0C &=~(1<<0); //控制ODR寄存器, Data_Output CRL_REG
	
	*(unsigned int *)0x40010C0C &=~((1)<<0);	     //控制ODR寄存器, Data_Output CRL_REG
	

	
	#else
	
	RCC_APB2ENR |= ((1)<<3);          //GPIOB时钟使能

	GPIOB_CRL |= ((1)<<(4*0));         //端口配置低寄存器
	//GPIOB_ODR &= ~(1<<0); //数据输出寄存器
	GPIOB_ODR |= (1<<0); 
	
	
	
	#endif
}
//置位 |=
//清零 &=~

void SystemInit(void)//不用管这个
{
    
	//函数体为空
}



对寄存器配置部分代码的说明:

//置一操作:
//配置RCC寄存器
*(unsigned int *)0x40021018 |=((1)<<3);
//说明一下使用|=的目的就是按位或,我们将想要配置为1的位使用|操作配置上去。|操作就是有1|1就是1,1|0也是1。

//置一操作:
//配置CRL寄存器
*(unsigned int *)0x40010C00 |=((1)<<(4*0));

//清零操作:
//配置ODR寄存器
*(unsigned int *)0x40010C0C &=~((1)<<0);
//说明一下是用&=的目的就是按位与,我们先将需要配置的位按位&1,然后取反就是0了。按位与是1&0是0,1&1是1。

下面详细介绍一下,这段代码中使用到的三个寄存器:
第一个是:RCC下面的APB2外设时钟使能寄存器(RCC_APB2ENR)
在这里插入图片描述
我们知道在GPIOB是挂接到APB2这条总线上的,而与51不同的是,32在操作GPIO是需要使能他的IO时钟的。
在这里插入图片描述
IO端口B的时钟使能是1打开,0关闭。
所以才有了下面的使能操作

//置一操作:
//配置RCC寄存器,使能IOB的时钟
*(unsigned int *)0x40021018 |=((1)<<3);
//说明一下使用|=的目的就是按位或,我们将想要配置为1的位使用|操作配置上去。|操作就是有1|1就是1,1|0也是1。

第二个是端口配置低寄存器(GPIOxCRL)
CRL寄存器决定了IO端口是工作在输入还是输出模式
另外他还决定了速度
在这里插入图片描述
我们此处使用的是通用推挽输出,和10Mhz的速度
手册中规定了CNF0和MODE0控制着端口位0。
通用推挽输出要求CNFNy为00,MODEy为01

//清零操作:
//配置CRL寄存器
*( unsigned int * )0x40010C00 &=  ~( (0x0f) << (4*0) );//这个下面我会解释为什么要加这一句
*( unsigned int * )0x40010C00 |=  ( (1) << (4*0) );
//说明一下是用&=的目的就是按位与,我们先将需要配置的位按位&1,然后取反就是0了。按位与是1&0是0,1&1是1。
//CRL寄存器的复位值是0x4444 4444 转化为2进制就是
//0100 0100 0100 0100 0100 0100 0100 0100
//而低四位正好是0100显然这个状态是在浮空输入模式,这就需要我们留意寄存器在复位后的初始状态,而且要做好处理。如果我们直接使用|=操作去置位最低位位1,也得不到我们想要的结果。所以就需要下面这句
*( unsigned int * )0x40010C00 &=  ~( (0x0f) << (4*0) );
//将我们的寄存器重复位后的输入状态下的低四位0100全部清零
//然后使用|=的操作去置位最低位,从而达到通用输出的模式,复位后的初始状态对于操作的影响这点很重要,需要留心。

第三个是端口输出数据寄存器(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的操作就是这样

原网站

版权声明
本文为[Fecter11]所创,转载请带上原文链接,感谢
https://blog.csdn.net/Fecter11/article/details/125547901