目录
1. 位带简介
2. 别名区地址的计算
2.1 合并计算
3. 位带操作访问ODR和IDR寄存器
4. GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2 / GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2
位带操作在写单片机程序时,或者在宏定义某个变量时,是非常有用且方便的。
1. 位带简介
位带操作其实就是库函数操作下最底层的操作逻辑。
//在STM32中如果要使PA0输出低电平,可以是 GPIOA->ODR|=0<<0; //在51单片机中,位带操作是通过sbit来定义的。 //P0=0xFF; //总线操作 sbit LED1=P^0 //位操作 LED1=0;
在51单片机中,通过 sbit 定义的LED1,在函数中调用 LED1 就等同于调用了 P0 引脚,给 LED1 低电平,LED 灯就会亮。
拿一个寄存器来说明位带操作的必要性:
一个32位的寄存器,整个寄存器是有自己特定的地址的,当访问寄存器的地址时,会操作整个寄存器。但有些时候,我们只需要访问寄存器的某一位,将该位设为低电平0/高电平1就可以实现特定功能,寄存器的每一位都是有特定地址的,访问寄存器中特定位的操作,就称为位带操作。
支持了位带操作以后,可以使用普通的加载/存储指令来对单一的比特进行读写。在SRAM静态存储器的最低1MB范围内可以实现位带,也可以在片内最低1MB范围内实现位带操作。这两个区的地址除了可以像普通的ARM一样使用外,还都有自己的 “位带别名区” ,位带别名区把每个比特膨胀成一个32位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。
比方说:PB引脚的ODR寄存器的地址是0x40010C0C,此时我们操作PB0,其最底层的操作逻辑就是操作ODR寄存器的第0位,那么这个位新的地址就是0x42000000+(0x40010C0C-0x40000000)32+0*4;通过宏定义给这个地址命名为PBout(0)。
#define PBout(0) 0x42000000+(0x40010C0C-0x40000000)84+0*4
2. 别名区地址的计算
支持位带操作的两个内存区的范围是:
0x2000_0000-0x200F_FFFF(SRAM区中的最低1MB)
0x4000_0000-0x400F_FFFF(片上外设区中的最低1MB)
对于SRAM位带区的某个比特位,记它所在字节地址为A,位序号为n(0<=n<=7),则该比特位在别名区的地址为:
AliasAddr=0x22000000+((A-0x20000000)*8+n)*4=0x22000000+(A-0x20000000)*32+n*4
对于片上外设的某个比特位,记它所在字节的地址为A,位序号为n(0<=n<=7),则该比特位在别名区的地址为:
AliasAddr=0x42000000+((A-0x40000000)*8+n)*4=0x42000000+(A-0x40000000)*32+n*4
其中,4表示一个字为4个字节,8表示一个字节中有8个比特位。
2.1 合并计算
外设与SRAM位带区与位带别名区的地址统一用一个公式表示:
((addr&0xF000000)+0x02000000+((addr&0x00FFFFFF)<<5)+(bitnum<<2))
其中:addr:要操作的位所在的寄存器地址
bitnum:位号,也就是在寄存器中的第几位
公式中:
addr&0xF0000000的意思是拿出要操作位所在的寄存器的最高位,如果是外设位带区,由上图,则拿出的最高位就是4,0x40000000+0x02000000=0x42000000,也就是外设位带别名区地址;如果是SRAM位带区,由上图,则拿出来的最高位就是2,0x20000000+0x02000000=0x22000000,也就是SRAM位带别名区地址;整个过程本质就是区分是外设位带操作还是SRAM位带操作。
addr&0x00FFFFFF的意思是屏蔽掉高两位,由上图可知,对于没有合并之前的公式来说,A-0x40000000就是最高位地址减去起始地址。对于外设位带区来说,最高地址就是0x40100000,SRAM位带区的最高地址就是0x20100000,不管是0x40100000-0x40000000还是0x20100000-0x20000000,其最终相减的地址都是低六位有效,最高两位是相同的,在做减法的过程中没有实际作用,所以&0x00FFFFFF就是把高2位屏蔽掉。
(addr&0x00FFFFFF)<<5左移5位的意思表示乘以,也就是乘以32,正好是对应没有合并之前的公式,左移5位表示放大了32倍。因为在C语言的位操作中,左移表示向左移位,右侧补0。
bitnum<<2左移2位表示乘以,也就是乘以4,正好也是对应了没有合并公式之前的*4,表示 bitnum 放大了4倍。
3. 位带操作访问ODR和IDR寄存器
在学习GPIO口时,我们已经学习过了GPIO的ODR寄存器和IDR寄存器,分别是GPIO输出数据寄存器和GPIO输入数据寄存器。
//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))
//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
//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) //输入
4. GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2 / GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2
在上一节IIC的代码程序中,其IIC.h头文件中使用位操作进行了串行数据总线SDA输入输出的设置。这里我们具体来讲解一下代码是如何定义的。
#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输出模式
在GPIO口的学习中,我们知道访问某一IO口时,需要初始化GPIO结构体,结构体包括MODE、OTYPE、PUPD、PIN,其中MODE是通过GPIOx_MODER(GPIO端口模式寄存器)来控制的。
该寄存器为32位寄存器,每两位控制一个引脚,则分别控制GPIOx的Px0-15引脚(x可取A、B、C、D、E、F、G)。
操作该寄存器的位带操作的主要思想是:首先将想要操作的那一确定位清0,即使用与&操作来清零;然后将想要的功能通过软件来写入,即用或|操作来赋值。功能主要也就是
- 00:输入
- 01:通用输出
- 10:复用功能
- 11:模拟功能
明白了这些,我们现在再来看代码:
#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输出模式
//GPIOB->MODER表示通过指针调用GPIOB模式寄存器
//该寄存器是每两个位控制一个引脚,9*2,也就是操作PB9引脚,这里也就默认我们想要操作的引脚就是PB9引脚
//3<<(9*2)的意思是:在32位寄存器下,
//3=0000 0000 0000 0000 0000 0000 0000 0011,左移9*2,那么最低2位11就会被移动到17 18位的地址上
// 0000 0000 0000 0011 0000 0000 0000 0000,然后取反就是
// 1111 1111 1111 1100 1111 1111 1111 1111,然后和GPIOB->MODER进行与&操作后就是
// 0000 0000 0000 0000 0000 0000 0000 0000,表示将17 18位地址清零,
//到这里完成了位带操作的第一步清0,然后第二部进行赋值
//0<<9*2表示 0000 0000 0000 0000 0000 0000 0000 0000,GPIOB->MODER|=0<<9*2就是把17 18位地址置为00,对应寄存器也就是PB9输入模式
//同理PB9输出模式也是这样,首先将17 18位清零,然后将这两位写入01
//1<<9*2表示 0000 0000 0000 0000 0000 0000 0000 0001
0000 0000 0000 0001 0000 0000 0000 0000 此时根据寄存器,也就表示PB9输出模式
//寄存器定义
//00:输入
//01:通用输出
//10:复用功能
//11:模拟功能