什么是位带操作和位带区
位带操作
STM32内核的最小寻址单位是字节,一个字节里面有8位,所谓位带操作就是在STM32中实现位操作的方法。,我们先来看什么是位带区。
下面一张图片是Cortex-M内核寻址空间映射图。
图中的SRAM区里的0X20000000-0X200FFFF地址段和片内外设区里的0X40000000-0X400FFFFF地址段就是位带区,也就是设计支持位带操作的内存区域。在野火相关资料文档中,有这样一段解释:
那么,STM32是如何将内核寻址单位缩小到位的呢?位带区这两个1MB的空间除了可以像正常的RAM一样操作外, 他们还有自己的位带别名区,位带别名区把这1MB的空间的每一个位膨胀成一个32位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。
由此可以看出,想要对STM32实现位操作,只有随位带别名区的一个字空间(注意,不是字节空间)进行操作,而之所以不用字节则是因为这样会降低访问速度。
如何进行位带操作
我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。
外设位带别名区地址
对于片上外设位带区的某一位空间,设其所在的字节地址为A,位序号为n(0<=n<=31),则该位对应的位带别名区地址为
0X42000000是外设位带别名区的起始地址,0x40000000是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节, 一个字节有8位,所以*8,一个位膨胀后是4个字节,所以*4,n表示该比特在A地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。
SRAM位带别名区地址
同样可以知道,
宏定义位带别名区地址的转换
#define BITBAND(addr,bitnum)((addr & 0xF0000000)+0x02000000+((addr &0x00FFFFF<<5)+(bitnum<<2))
好好理解一下这个公式!特别是其中按位与运算之巧妙!以下是野火开源资料对上述公示的描述。
addr & 0xF0000000是为了区别SRAM还是外设,实际效果就是取出4或者2,如果是外设,则取出的是4,+0X02000000之后就等于0X42000000, 0X42000000是外设别名区的起始地址。如果是SRAM,则取出的是2,+0X02000000之后就等于0X22000000,0X22000000是SRAM别名区的起始地址。
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去0X20000000或者0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X20100000, 跟起始地址0X20000000相减的时候,总是低5位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。 SRAM同理分析即可。<<5相当于*8*4,<<2相当于*4,这两个我们在上面分析过。
下面介绍常用的宏定义
1 | #define BITBAND(addr,bitnum)((addr & 0xF0000000)+0x02000000+((addr &0x00FFFFF<<5)+(bitnum<<2)) |
上述宏定义中,BITAND用来将位带区某一位空间所在字节的首位地址和位偏移量映射为其在位带别名区的地址;MEM_ADDR用于将位带区某一位空间所在字节的首位地址转化为指针;
BIT_ADDR用于将位带区某一位空间所在字节的首位地址和位偏移量映射为对应的指针。(值得注意的是volatile关键字,需要提醒编译器每次存储或读取addr地址时重新从变量地址读取数据,避免优化);由于地址是32位的,所以用无符号长整形表达地址。
GPIO的位带操作
我们来举一个例子:
如果我们要控制一个LED闪烁,利用库函数,我们需要经历一下算法:
- 初始化GPIO引脚(低电平有效为例)
- 控制GPIO为低电平,延时一段时间
- 控制GPIO为高电平,延时一段时间
- 回到2
然而利用位带操作,我们可以对单个管脚进行操作。当我们要操作一个位,直接用GPIO封装的宏直接访问这一位的数据。这样可以直接利用对应寄存器进行输出!上述算法变为:
- 通过相应GPIO的寄存器地址的位带别名区地址,访问寄存器变量。可以利用宏定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59// GPIO ODR 和 IDR 寄存器地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+20)
#define GPIOB_ODR_Addr (GPIOB_BASE+20)
#define GPIOC_ODR_Addr (GPIOC_BASE+20)
#define GPIOD_ODR_Addr (GPIOD_BASE+20)
#define GPIOE_ODR_Addr (GPIOE_BASE+20)
#define GPIOF_ODR_Addr (GPIOF_BASE+20)
#define GPIOG_ODR_Addr (GPIOG_BASE+20)
#define GPIOH_ODR_Addr (GPIOH_BASE+20)
#define GPIOI_ODR_Addr (GPIOI_BASE+20)
#define GPIOJ_ODR_Addr (GPIOJ_BASE+20)
#define GPIOK_ODR_Addr (GPIOK_BASE+20)
#define GPIOA_IDR_Addr (GPIOA_BASE+16)
#define GPIOB_IDR_Addr (GPIOB_BASE+16)
#define GPIOC_IDR_Addr (GPIOC_BASE+16)
#define GPIOD_IDR_Addr (GPIOD_BASE+16)
#define GPIOE_IDR_Addr (GPIOE_BASE+16)
#define GPIOF_IDR_Addr (GPIOF_BASE+16)
#define GPIOG_IDR_Addr (GPIOG_BASE+16)
#define GPIOH_IDR_Addr (GPIOH_BASE+16)
#define GPIOI_IDR_Addr (GPIOI_BASE+16)
#define GPIOJ_IDR_Addr (GPIOJ_BASE+16)
#define GPIOK_IDR_Addr (GPIOK_BASE+16)
// 单独操作 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) //输入
#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) //输入
#define PJout(n) BIT_ADDR(GPIOJ_ODR_Addr,n) //输出
#define PJin(n) BIT_ADDR(GPIOJ_IDR_Addr,n) //输入
#define PKout(n) BIT_ADDR(GPIOK_ODR_Addr,n) //输出
#define PKin(n) BIT_ADDR(GPIOK_IDR_Addr,n) //输入
进行操作。(库函数中已经写好啦,现场查即可)
- LED=0,延时;LED=1,延时;
- 回到2;
注意:
- STM32F7以上的MCU不支持位带操作。
- out管脚作为左值使用,in管脚作为右值使用。
下面来写一下上述GPIO位带操作的实现举例。
1 |
|