位带操作顾名思义就是可以单独对CPU寄存器某个位进行读写操作,采取这种方式,将会大大减少我们写代码的难度,不用像上篇博客那样进行与或运算和移位,降低了出错的可能性。如果之前有使用过8051单片机的话,应该体会过这种操作的好处。
例如,在51单片机中,P1.0上挂了一个灯,我们想要他点亮,可以直接P1.0=0或者P1.0=1这样写,直接对P1端的某个IO口进行操作。但在STM32中并不允许这样操作,而为了在STM32中实现类似51那样的操作,就引入了位带操作这种机制。
在学习位带操作时,我们可以参考下Cortex-M3权威指南这份文档,从83页开始对位带操作的原理有很详细的讲解,在这里,我就不想将文档里的东西再照搬一遍,就说下我自己对这个位带操作机制的理解。
我们还是以上篇的按键控制LED灯为例,上一篇中写到GPIOB->CRL |= 0X00300000;
,这一句我们还可以地址这样来写(*volatile unsigned long)0X4001 0C00 = (3<<20)|(0<<22);//这里暂时不管其他位
,为什么这样写呢?我们先打开STM32的参考手册,在里面看到这样的一个表格
从以上图片我们看到GPIOB的起始地址为0X40010C00,且CRL寄存器的偏移地址为0X00,,这样GPIOB的CRL寄存器的地址便是0X40010C00,再接着我们看到CRL控制GPIOB5的bit为20、21、22、23,给这四个bit进行赋值。
配置好CRL后,我们就要控制GPIOB5的输出高低电平,原理跟CRL一样,先找出ODR寄存器的地址,再赋值。
(*volatile unsigned long)0X4001 0C0C = 1<<5;
然后我们要开启GPIOB的时钟,就要先找到APB2ENR寄存器的地址,在文档中我们找到RCC在位带区的地址为0x4002 1000,且RCC->APB2ENR的偏移地址为0x18,所以(*volatile unsigned long)0x4002 1018 = 1<<3;
,就这样我们完成了点亮LED灯。但在实际操作上,我们基本不会看到这样写代码,更多的是看到如下代码,原理也跟上面所说的差不多。
#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))
BITBAND()是根据ST官方提供的别名区地址计算公式
AliasAddr= 0x42000000+((A‐0x40000000)*8+n)*4 =0x42000000+ (A‐0x40000000)*32 + n*4
来得到某个比特的别名区地址,“*4”表示一个字为4 个字节,“*8”表示一个字节中有8 个比特,addr为GPIO寄存器的别名区地址,bitnum为GPIO上第几个口。 MEM_ADDR()则是将地址转为指针,这样,我们就可以来对IO口进行以下的定义
#define GPIOA_ODR_ADDR (0x4001 0800+12) //0x4001080C
#define GPIOB_ODR_ADDR (0X4001 0C00+12) //0x40010C0C
#define GPIOC_ODR_ADDR (0x4001 1000+12) //0x4001100C
#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) //输入
实际上,在stm32f10x.h里已经有对GPIOA~G的别名区地址的宏定义,可以直接调用,不用自己去查。
现在我们采用位带操作来写下按键控制LED灯:
//LED初始化
RCC->APB2ENR|=1<<3;
GPIOB->CRL&=0XFF0FFFFF;
GPIOB->CRL|=0X00300000;
PBOUT(5) = 1; // GPIOB->ODR |= 1<<5;
//按键初始化
RCC->APB2ENR|=1<<6;
GPIOE->CRL&=0XFFF0FFFF;
GPIOE->CRL|=0X00080000;
PEOUT(4) = 1; // GPIOE->ODR |= 1<<4;
//按键扫描
if(PEIN(4) == 0) //(GPIOE->IDR&0x10)>>4
{
PBOUT(5) = 0; //GPIOB->ODR |= ~(1<<5)
}
else{
PEOUT(5)=1; // GPIOB->ODR |= 1<<5;
}
最后说句,学位带操作时,自己根据公式算下别名区地址,会比较容易搞懂,单纯看,有时会越看越晕。