在51单片机里,如果要对某一位进行操作,可以直接使用关键字sbit,但是在stm32里面没有这样的关键字,而是通过访问位带别名区来实现。
在stm32里,有两个地方实现了位带,一个是SRAM区的最低1MB空间,地址范围为0x20000000~0x20100000(在block1的起始位置处),另一个是外设区最低1MB空间,地址范围为0x40000000~0x40100000(在block2的起始位置处)。这些位带区除了像正常的RAM一样操作外,它们还有自己的位带别名区,位带别名区把这1MB空间的每一位膨胀成32位,访问位带别名区的这些字,就可以达到访问位带区某个位的目的。
将位带区的每个位膨胀成32位(即4个字节)之后,那么每个位都与位带别名区的一个地址相映射。至于为什么访问位带别名区就可以操作位,这是由内核里的硬件设计决定的,位带操作会增加硬件成本,比如在更便宜的stm32的F0系列里就没有位带操作。
位带操作的好处有:
①对于控制GPIO的输入输出非常简单;
②操作串行接口芯片非常方便,如果采用库函数的话,时序的编写将非常不方便;
③代码简洁,阅读方便。
另外,位带区的一个位经过膨胀之后,虽然变大到4个字节,但是还是最低位有效,有人会问,这不是浪费空间吗?要知道stm32的系统总线是32位的,按照4字节访问的时候是最快的,所以膨胀成4个字节来访问是最高效的。并且32MB的位带别名区刚好是位于原来保留的地址范围,不会与其他寄存器地址重合。
接下来对这两个位带区进行介绍。
位带区介绍
1、外设位带区
外设位带区的地址为0x40000000~0x40100000,大小为1MB,这1MB的大小包含了片上外设的全部寄存器,膨胀后的位带别名区地址为0x42000000~0x43FFFFFF。虽然说全部寄存器都可以实现位操作,但在实际中不会这么做,在需要频繁地操作IO口时,可以考虑把IO相关的寄存器实现位操作。
2、SRAM位带区
SRAM位带区的地址为0x20000000~0x20100000,大小为1MB,经过膨胀之后的位带别名区地址为0x22000000~0x23FFFFFF,操作SRAM的位用的比较少。
二、地址转换
地址转换主要是把握“位带区的一个位膨胀为32位”。我们得把位带区的每个位映射一个地址。所以也就是从位带区的“位”→别名区的字节(地址)。
对于片上外设位带区的某个位,记它所在字节的地址为A,位序号为n(0~7),则该位在别名区的地址为:
0x42000000+(A-0x40000000)*8*4+n*4;
同理,对于SRAM位带区的某个位,类似为
0x22000000+(A-0x20000000)*8*4+n*4;
这也很好理解,就是将位带区的位*4即得到别名区的地址(因为32位就是4个字节)。而位带区的位包含两部分,一部分是地址(字节)*8,另一部分就是n,也就是
(字节数*8+位数n)*4=别名区地址;
其中字节数就是某寄存器的地址减去起始地址。
为了将上述两个公式统一,可以采用下面的操作
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000取出最高位是4还是2,用来区分是外设还是SRAM
addr &0xFFFFF把高三位屏蔽掉了,保留低五位,相当于是减去了起始地址。因为不管是外设区还是SRAM区,减去起始地址之后,都在只剩下低五位了。左移5位即*32,左移两位即*4。这样就将两个公式统一起来了。这一部分可以用具体的例子来验证一下。
三、GPIO位带操作
接下来具体说一下如何用位带操作来操作GPIO中的ODR寄存器。我们知道在每个GPIO端口都有一个ODR寄存器,它的低16位控制了对应管脚的输出。比如GPIOC的ODR寄存器的第0位控制了PC0的输出。那么该如何操作具体的某一位呢?
首先,定义好寄存器的地址。
#define GPIOC_ODR_Addr (GPIOC_BASE+12)//偏移量为12
然后①要把位带区的地址映射到别名区,②并且将地址转化为指针,为书写方便把上述两个步骤统一起来,实现代码如下
#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))
所以,如果要使PC0输出,可以这样写
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n)
有了上面的宏定义,可以非常方便的控制每个引脚的输出,当然,如果要操作其他寄存器,只要用类似的方法进行封装就好了。
例:用位带操作实现流水灯闪烁
int main()
{
int k;
LED_Init();
while(1)
{
for (k=0;k<8;k++)
{
if(k==0)
{
PCout(k)=0;
PCout(k+7)=1;
delay(6000000);
}
else
{
PCout(k)=0;
PCout(k-1)=1;
delay(6000000);
}
}
}
}
需要注意的是点亮每个灯时要把前一个灯熄灭。
总结:位带操作就是把位带区的每个位膨胀为32位,因为stm32不能对位进行读写,只能对字或者半字操作,并且执行32位效率会更高。并不是所有的单片机都能进行位带操作,这是由内核设计决定的。
要操作哪个寄存器的某个位,只要定义好它的地址,再经过地址转化,就可以用指针的方式访问了。