前言:
学过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有两个可以映射的区域,即下面两个映射区
通过《STM32F4xx中文参考手册.pdf》我们可以知道,GPIO确实在其中的一个可映射的地址范围之内(当然这里也可以看出要GPIO的时钟,是依附于AHB1的)
需要注意的是,GPIO位带操作我们用到它的GPIOx_IDR/GPIOx_ODR这两个寄存器是读取和写IO数据的,并不能改变其输入输出模式,所以如果是类似模拟I2C,里面的数据线可能是作输出,可能是设输入,对数据线这个IO幅值、读值操作之前,要先设置其输入输出模式。
下面来看代码:
先是和GPIO初始化有关的:
1.GPIO对应的组的时钟开启
这个开发板两个LED灯的管脚分别是:DS0 接 PF9,DS1 接 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<<pinpos; //一个个位检查
curpin=BITx&pos;//检查引脚是否要设置
if(curpin==pos) //需要设置
{
GPIOx->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<<pinpos) ; //清除原来的设置
GPIOx->OTYPER|=OTYPE<<pinpos; //设置新的输出模式
}
GPIOx->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位,映射到位带别名区的地址
这样当用户对位带别名区的地址操作,内部机制会转化为对相应寄存器的相应的位进行操作
和手册上所说的地址映射范围以及规则是一样的:
以下来看一下模拟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);
}
}
位带操作就是这样啦~完