5.从点亮led看位带操作

前言:

        学过51的应该都清楚,51的IO口输出是非常方便的,可以直接对某个IO幅值0或者1,例如P1.1=1,或者P1.1=0;而stm32呢,操作GPIO的步骤就相对多一些~

初始化方面:

1.GPIO对应的组的时钟开启

2.每个GPIO组都各有GPIOx_MODER(设置IO模式,输入/输出/复用等),GPIOx_OTYPER(设置IO状态,推挽/开漏),GPIOx_OSPEEDR(设置IO速度),GPIOx_PUPDR(设置电气属性,上下拉),GPIOx_IDR/GPIOx_ODR(读/写IO)这么多寄存器,每个GPIO组一般都有16个IO口,例如GPIOA,就有PA_0,PA_1 ...PA_15,所以以上的寄存器大多是每两个位控制一个IO口,不过也有像读写IO寄存器,它们是每一个位对应一个IO口。我们使用某一个IO口,需要初始化对应的寄存器对应的位,可以说比较麻烦。

3.读取GPIOx_IDR可以读取GPIO电平, 向GPIOx_ODR写可以让GPIO输出想要的电平,上面的配置也就罢了,配置一次麻烦点就麻烦点,但是输出0和1就没有像51一样位带操作(直接对某一个位进行操作)那么方便了。

stm32怎么实现位带操作呢?

例如:

		LED0=0;				//DS0亮
		LED1=1;				//DS1灭

ARM Cortex-M4这个其实已经安排好了,有一些地址的每一个位都可以映射到另一个地址上去(注意,是每一个位都有了一个地址可以直接操作),从效果上来讲,我们对新地址幅值0和1,就相当于对原来的位,幅值0和1.

按照这个思路,如果我们把GPIOx_IDR/GPIOx_ODR映射过去,实现位带操作,那不就可以直接对里面的每个位(即这个GPIO组的每一个IO口)进行操作了么,尤其是对于GPIOx_ODR来说,可以直接让它输出高电平或者低电平。

位带操作实现方法:

首先我们要明白,原来的位和新的地址的关系,原来的1bit映射到新地址后占据4B的地址,例如

0x40020014.0这个位对应---->>0x42400280~0x42400283 这四个地址

0x40020014.1这个位对应---->>0x42400284~0x42400287 这四个地址

.......

知道了这个我们只需要知道,哪片地址空间是原地址,哪片地址空间是位带别名区,查看《STM32F3与F4系列Cortex M4内核编程手册.pdf》31页我们可以得知,M4有两个可以映射的区域,即下面两个映射区

5.从点亮led看位带操作_第1张图片

通过《STM32F4xx中文参考手册.pdf》我们可以知道,GPIO确实在其中的一个可映射的地址范围之内(当然这里也可以看出要GPIO的时钟,是依附于AHB1的)

5.从点亮led看位带操作_第2张图片

        需要注意的是,GPIO位带操作我们用到它的GPIOx_IDR/GPIOx_ODR这两个寄存器是读取和写IO数据的,并不能改变其输入输出模式,所以如果是类似模拟I2C,里面的数据线可能是作输出,可能是设输入,对数据线这个IO幅值、读值操作之前,要先设置其输入输出模式。

下面来看代码:

先是和GPIO初始化有关的:

1.GPIO对应的组的时钟开启

这个开发板两个LED灯的管脚分别是:DS0 PF9DS1 PF10,所以先使能GPIOF的时钟

RCC->AHB1ENR|=1<<5;//使能PORTF时钟 

2.配置GPIO输入/输出,上拉/下拉,速度等

正点原子给了一个函数:直接用就行

//GPIO通用设置 
//GPIOx:GPIOA~GPIOI.
//BITx:0X0000~0XFFFF,位设置,每个位代表一个IO,第0位代表Px0,第1位代表Px1,依次类推.比如0X0101,代表同时设置Px0和Px8.
//MODE:0~3;模式选择,0,输入(系统复位默认状态);1,普通输出;2,复用功能;3,模拟输入.
//OTYPE:0/1;输出类型选择,0,推挽输出;1,开漏输出.
//OSPEED:0~3;输出速度设置,0,2Mhz;1,25Mhz;2,50Mhz;3,100Mh. 
//PUPD:0~3:上下拉设置,0,不带上下拉;1,上拉;2,下拉;3,保留.
//注意:在输入模式(普通输入/模拟输入)下,OTYPE和OSPEED参数无效!!
void GPIO_Set(GPIO_TypeDef* GPIOx,u32 BITx,u32 MODE,u32 OTYPE,u32 OSPEED,u32 PUPD)
{  
	u32 pinpos=0,pos=0,curpin=0;
	for(pinpos=0;pinpos<16;pinpos++)
	{
		pos=1<MODER&=~(3<<(pinpos*2));	//先清除原来的设置
			GPIOx->MODER|=MODE<<(pinpos*2);	//设置新的模式 
			if((MODE==0X01)||(MODE==0X02))	//如果是输出模式/复用功能模式
			{  
				GPIOx->OSPEEDR&=~(3<<(pinpos*2));	//清除原来的设置
				GPIOx->OSPEEDR|=(OSPEED<<(pinpos*2));//设置新的速度值  
				GPIOx->OTYPER&=~(1<OTYPER|=OTYPE<PUPDR&=~(3<<(pinpos*2));	//先清除原来的设置
			GPIOx->PUPDR|=PUPD<<(pinpos*2);	//设置新的上下拉
		}
	}
} 

调用如下:这种没什么技术含量的直接阔批

	GPIO_Set(GPIOF,PIN9|PIN10,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU); //PF9,PF10设置

3.用位带操作来读写IO


//LED端口定义
#define LED0 PFout(9)	// DS0
#define LED1 PFout(10)	// DS1	 	

LED0=1;//LED0关闭
LED1=1;//LED1关闭

代码就是以上这样

下面来看位带操作在代码上是怎么搞的

从上面我们可知,对LED宏操作就是对PFout(9)和PFout(10)操作,再追溯看PFout(n)是什么东东:

//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出 
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入 

#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 
#define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入 

#define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出 
#define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入 

#define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出 
#define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入 

#define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出 
#define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入

#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出 
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入

#define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出 
#define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入

#define PHout(n)   BIT_ADDR(GPIOH_ODR_Addr,n)  //输出 
#define PHin(n)    BIT_ADDR(GPIOH_IDR_Addr,n)  //输入

#define PIout(n)   BIT_ADDR(GPIOI_ODR_Addr,n)  //输出 
#define PIin(n)    BIT_ADDR(GPIOI_IDR_Addr,n)  //输入

里面由套了一个宏,继续追查!备注一下,

先不看BIT_ADDR,我们先看一下GPIOx_ODR_Addr和GPIOx_IDR_Addr是什么:它也是宏定义,只不过是各个GPIO组的ODR和IDR寄存器(输出0/1以及读取IO电平的寄存器)的地址罢了

//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+20) //0x40020014
#define GPIOB_ODR_Addr    (GPIOB_BASE+20) //0x40020414 
#define GPIOC_ODR_Addr    (GPIOC_BASE+20) //0x40020814 
#define GPIOD_ODR_Addr    (GPIOD_BASE+20) //0x40020C14 
#define GPIOE_ODR_Addr    (GPIOE_BASE+20) //0x40021014 
#define GPIOF_ODR_Addr    (GPIOF_BASE+20) //0x40021414    
#define GPIOG_ODR_Addr    (GPIOG_BASE+20) //0x40021814   
#define GPIOH_ODR_Addr    (GPIOH_BASE+20) //0x40021C14    
#define GPIOI_ODR_Addr    (GPIOI_BASE+20) //0x40022014     


#define GPIOA_IDR_Addr    (GPIOA_BASE+16) //0x40020010 
#define GPIOB_IDR_Addr    (GPIOB_BASE+16) //0x40020410 
#define GPIOC_IDR_Addr    (GPIOC_BASE+16) //0x40020810 
#define GPIOD_IDR_Addr    (GPIOD_BASE+16) //0x40020C10 
#define GPIOE_IDR_Addr    (GPIOE_BASE+16) //0x40021010 
#define GPIOF_IDR_Addr    (GPIOF_BASE+16) //0x40021410 
#define GPIOG_IDR_Addr    (GPIOG_BASE+16) //0x40021810 
#define GPIOH_IDR_Addr    (GPIOH_BASE+16) //0x40021C10 
#define GPIOI_IDR_Addr    (GPIOI_BASE+16) //0x40022010 

继续追查:参数1是数据寄存器地址,参数2则是第n个IO口

#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出 
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入

BIT_ADDR这个宏其实就是位带映射了,你输入一个地址以及第n个IO口,它就能转换成位带别名区的地址(也就是上面说的,原地址的某个位,映射到位带别名区的地址),然后我们对这个位带别名区的地址操作,比如写0或者写1,就相当于对原地址的对应位写0或写1

//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 

带个值进去理解一下:

原来的一个bit膨胀为32个bit,即4个地址
((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
找一个带入进去,例如对 PA_1 输出数据寄存器ODR操作,即 GPIOA_ODR_Addr = 0x40020014
((0x40020014 & 0xF0000000)+0x2000000+((0x40020014 &0xFFFFF)<<5)+(1<<2)) 
(0x40020014 & 0xF0000000) = 0x40000000  ((0x40020014 &0xFFFFF)<<5) = 0x00020014*32
0x00020014*32即0x00020014*8*4    
0x00020014*8是算出这些地址 共有多少个bit 再乘以4是算出该寄存器基地址在位带别名区的地址是多少
后面bitnum<<2即n*4,算出寄存器的第n位在位带别名区的地址是多少 
所以这个语句的结果就是:0x40000000 + 0x02000000 + 0x00020014*32 + 1*4 = 0x42400284
这个地址就是GPIOA_ODR这个寄存器的第1位,映射到位带别名区的地址
这样当用户对位带别名区的地址操作,内部机制会转化为对相应寄存器的相应的位进行操作

和手册上所说的地址映射范围以及规则是一样的:

5.从点亮led看位带操作_第3张图片

以下来看一下模拟I2C的IO宏定义加深对位带操作的理解:

//IO方向设置
#define SDA_IN()  {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2;}	//PB9输入模式
#define SDA_OUT() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2;} //PB9输出模式


//IO操作函数	 
#define IIC_SCL    PBout(8) //SCL
#define IIC_SDA    PBout(9) //SDA	 
#define READ_SDA   PBin(9)  //输入SDA 

例如在I2C发送数据时,要设置SDA的方向之后,再操作数据线

//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答			  
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {              
        IIC_SDA=(txd&0x80)>>7;
        txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 	    

位带操作就是这样啦~完

你可能感兴趣的:(单片机,单片机,嵌入式硬件)