前言
在stm32中操作GPIO口有一种非常常见便捷的方式,不需要调用32标准库函数,那就是位带(bit-band)操作。
首先,放上一张在全网广泛流传的照片,在几乎所有的位带操作相关的博客中都能找到这张图片。
在《Cortex-M3权威指南》中出现的存储器映射图能更好的帮助我们理解整个存储架构
简介(下定义)
位带操作,就是一种特殊的,能够实现单独操作某一bit位的操作方式。他的实现原理就是在存储空间中划分出位带区,将位带区的每一位膨胀4个字节后存放到位带别名区,访问操作位带别名区即可操作位带区的每一位。
位带区和位带别名区
STM32中,在SRAM和片上外设的最低1MB空间为位带区,相对应的这两个区域有专属的位带别名区。
外设位带区地址为0x40000000 ~ 0x40100000,大小为1MB,膨胀后地址为0x42000000 ~ 0x43FFFFFF,大小为32MB在stm32F103中这个范围包含了所有的片上外设的寄存器。所以可以实现对所有片上外设的位带操作。
SRAM位带区地址为0x20000000~0x20100000,大小为1MB,膨胀后地址为0x22000000 ~ 0x23FFFFFF,大小也为32MB。
位带区和位带别名区的地址转换
/*设位带区某位所在字节的地址为a,该位在该字节中的序号为n*/
AliasAddr = 0x4200000 + (A - 0x40000000)*8*4 + n*4 //片上外设
AliasAddr = 0x4200000 + (A - 0x40000000)*8*4 + n*4 //SRAM
首先我们需要明确的一点是,stm32中的地址最小代表着一个字节,即相邻地址之间间隔8位。所以每一位相当于膨胀到4个字节。
式子中A - 0x40000000算出相对位带区首地址偏移了多少个字节,然后 *8得出共计多少个位。因为每一个位膨胀四个字节,反映到位带别名区就是地址占4,所以 *4得别名区的地址。
还没完,刚刚得出的是位带区对应字节在别名区的换算,还需要加上位序号在别名区的换算才能真正定位到该字节。位序号代表便宜字节基地址的位数,而每一个位膨胀四个字节,所以位序号 *4就是位序号的换算。两者相加,再加上别名区的基地址,就可以得到该位在别名区的确切地址。
位带操作
一般为了方便,会使用宏定义将以上的操作封装好,并将地址转换成指针,通过访问指针实现对位带别名区的操作。
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_BIT_ADDR(addr,bitnum) MEM_ADDR(BITBAND(addr, bitnum))
在第一个宏定义中,addr & 0xF0000000提取出了地址最高位,片上外设是4,SRAM是2。addr & 0x00FFFFFF屏蔽了高两位,原因是两者相减相当于直接屏蔽高两位,屏蔽的位数和改位带区的最高位有关。最后的位操作左移相当于乘32和乘4。
第二个宏定义将参数转换为指针并访问,第三个宏定义将二者结合,最终直接访问以addr和bitnum为参数的地址。
注意,使用位带操作的时候必须使用volatile关键字来定义,因为系统并不知道有两个地址,所以提醒cpu不需要优化而老老实实地每次认真读写。
工程实践
从手册中可以查到我们想要实现位带操作的寄存器的地址偏移量
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080c
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define PAout(n) BIT_BIT_ADDR(GPIOA_ODR_Addr,n)
#define PAin(n) BIT_BIT_ADDR(GPIOA_IDR_Addr,n)
PAout(0)=0;//io口低电平
PAin(0) =1;//io口高电平
优越性
1.使代码更简洁
2.用于实现共享资源在任务间的“互锁”访问。多任务的共享资源必须满足一次只有一个任务访问它——亦即所谓的“原子操作”。以前的读-改-写需要3 条指令,导致这中间留有两个能被中断的空当。于是可能会出现如下图所示的紊乱危象: