1. 位带操作
-
在51单片机中,可以单独对一个bit位进行读和写。例如:P1^1 = 0 ;
-
STM32里,内存最小的寻址单位是字节,1个字节8个bit,对这8个bit进行操作,就需要用到位带。
2. 位带区
- 支持位带操作的区域就是位带区。
- 如图中①处,是STM32中两个位带区。
(1)SRAM区,1M字节的地址空间,0x2000 0000 ~ 0x2010 0000。
(2)片内外设区,1M字节的地址空间,0x4000 0000 ~ 0x4010 0000。
3. 位带别名区
- 定义:
将1M位带区的地址每一个bit映射到32M字节里面,就是位带别名区,操作位带别名区就是在操作位带。 - 将位带区膨胀成位带别名区的操作:
如(位带别名区)图所示:将0x4000 0000地址的bit3映射到0x4200 0010地址。 - 位带别名区大小计算:
位带区大小为1M=1024K=10248bit,每个bit对应4个字节32位(也就是32个bit),那么位带别名区大小为10248*32=32M。 - 位带区的地址范围:
如(位带区)图所示,图中②处就是位带别名区地址范围。
SRAM的位带别名区:0x2200 0000 ~ 0x23FF FFFF
片内外设区的位带别名区:0x4200 0000 ~ 0x43FF FFFF - 位带区bit位膨胀4个字节的原因:
STM32总线是32位的,按照4个字节访问的时候是最快的。 - STM32和51区别:
STM32的全部寄存器都可以通过访问位带别名区的方式,达到访问原始寄存器比特位的效果。51单片机不是,有部分仍需字节操作。
4. 外设别名区的地址
-
假设已经知道了需要操作的字节所在地址为A,需要操作字节的位号为n,那么对应的位带别名区的地址是多少呢?
答:位带别名区地址 = 0x4200 0000 + (A - 0x4000 0000)84 +n*4其中0x4200 0000是外设位带别名区的起始地址,0x4000 0000是外设位带区的起始地址。(A - 0x4000 0000)表示该比特前面有多少个字节,一个字节有8位,所以8。一个位膨胀后是4个字节,所以4。n表示字节的位号,在A地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。
5. SRAM位带别名区的地址
- 位带别名区地址 = 0x2200 0000 + (A - 0x2000 0000) * 8 * 4 + n*4
6. 统一公式
-
为了方便操作,可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名 区地址,统一成一个宏。
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
-
(addr & 0xF000 0000)是为了区别SRAM还是外设,实际效果就是取出4或者2,如果是外设,则取出的是4,+0x0200 0000之后就等于0x4200 0000(0x4200 0000是外设别名区的起始地址)。如果是SRAM,则取出的是2,+0x0200 0000之后就等于0x2200 0000(0x2200 0000是SRAM别名区的起始地址)。
-
(addr & 0x00FF FFFF)屏蔽了高三位,相当于减去0x2000 0000或者0x4000 0000,但是为什么是屏蔽高三位? 因为外设的最高地址是:0x2010 0000,跟起始地址0x2000 0000相减的时候,总是低5位才有效,所以屏蔽高三位来达到减去起始地址的效果。SRAM同理。 <<5相当于84,<<2相当于*4。
7. 实际使用
// 定义BITBAND,通过公式获得位带别名区地址
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
// 把一个地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//输入需要操作的字节和位号,公式会查找到它的位带别名区,地址转换成指针类型,通过指针操作该地址的内容,达到对字节的位操作效果。
BIT_ADDR(0x40000000, 3) = 1;
也可以不使用复杂的宏定义,直接通过寄存器对位带别名区进行操作:
注:位带别名区的有效位,因为位带区的bit只能是0或者1。所以投射到位带别名区的每一个32位的地址,只有最低位有效,只要最低位是1,对应的位带区的位就是1。
- 例1:对0x4000 0000地址位3置1操作
*(uint32_t*)(0x40000000) |= (1<<3); //直接对该字节进行处理
等价于
*(uint32_t*)(0x4200000C) = 0x01;
*(uint32_t*)(0x4200000C) = 0x03;
*(uint32_t*)(0x4200000C) = 0x05; //只要最低位是1即可
- 例2:对0x4000 0000地址位3置0操作
*(uint32_t*)(0x40000000) &= ~(1<<3);
等价于
*(uint32_t*)(0x4200000C) = 0x00;
*(uint32_t*)(0x4200000C) = 0x02;
*(uint32_t*)(0x4200000C) = 0x04; //只要最低位是0即可
8. 常用操作:GPIO的位带操作
- 外设的位带区,覆盖了全部的片上外设的寄存器,可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。这样处理意义不大,通常只需要对经常需要操作位的寄存器进行宏定义。较为常用的:GPIO中ODR和IDR这两个寄存器的位操作。
- STM32F103C8T6中GPIO的ODR和IDR这两个寄存器对应GPIO基址的偏移是12和8,下面是实现这两个寄存器的地址映射,GPIOx_BASE表示对应IO类的地址。
// GPIO ODR 和 IDR 寄存器地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
// 单独操作 GPIO的某一个IO口,n(0,1,2...16),n表示具体是哪一个IO口
#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) //输入
- 可以用位操作的方法来控制GPIO的输入和输出,其中宏参数n表示具体是哪一个IO口,n(0,1,2…16)。 这里面包含了端口A~G ,并不是每个单片机型号都有这么多端口,使用这部分代码时,要查看单片机型号,如果是64pin的则最多只能使用到C端口。
9. 位带操作的特点:
- 访问速度快,硬件完成,不可被异常打断。
如果在裸机开发中,位带操作相比于直接的读-改-写操作访问速度快。在操作系统的开发中,多任务并发运行的时候就有可能在任务切换的过程中发生不可预料的问题,而位带操作由于是属于硬件完成的不可被异常打断的操作(原子操作),所以相对于读-写-改的操作模式的话会更安全些。