嵌入式开发学习之--位带操作
前言
之前的学习我们知道单片机最底层的是地址,然后地址之上的映射是寄存器,寄存器之上的映射是库函数,而我们现在编写代码所依据的大部分是库函数。今天学习的位操作则可以理解为在最底层地址之上除了寄存器以外的另一种映射,实现对单独一个比特位的读和写。
一、位带操作是什么
位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,F429 中没有这样的关键字,而是通过访问位带别名区来实现。
在 F429 中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,另一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。
二、位带别名区
2.1 外设位带区
外设位带区的地址为:0X40000000~0X400F0000,大小为 1MB,这 1MB 的大小包含了 APB1/2 和 AHB1 上所以外设的寄存器,AHB2/3 总线上的寄存器没有包括。AHB2 总线上的外设地址范围为:0X50000000~0X50060BFF,AHB3 总线上的外设地址范围为:0XA0000000~0XA0000FFF。外设位带区经过膨胀后的位带别名区地址为: 0X42000000~0X43FFFFFF,这部分地址空间为保留地址,没有跟任何的外设地址重合。
2.2 SRAM位带区
SRAM 的位带区的地址为:0X2000 0000~X200F 0000,大小为 1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF,大小为 32MB。操作 SRAM 的比特位这个用得很少。
2.3外设位带区转换
对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
解释一下,0X42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址(原地址),所(A-0x40000000)表示该地址前面有多少个字节(地址与地址之间是间隔一个字节),一个字节有 8 位,所以8,一个位膨胀后是 4 个字节,所以4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也*4,然后加上位带区的起始地址,就成功映射在位带别名区的新地址上了。
2.4SRAM位带区转换
对于 SRAM 位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
1 AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
逻辑同上,就是为了将原地址的单独一位映射到新位带地址上去。
2.5统一公式
根据上述的两个转换公式,我们可以统一成如下宏定义,这样只需要输入原地址和位序号,就可以得到位带新地址。
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))
此时我们得到的BITBAND,是根据原addr地址下,序号为bitnum的位而转化成的位带新地址,对这个地址进行操作,我们就可以对addr地址的bitnum位进行读或写。接着我们创建两个宏定义,做一个指针指向该地址。
1 // 把一个地址转换成一个指针 2 #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) 3 4 // 把位带别名区地址转换成指针 5 #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
2.6开始位带操作
2.6.1定义地址
外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。在这里仅仅演示下 GPIO 中 ODR 和 IDR 这两个寄存器的位操作。从手册中可以知道 ODR 和 IDR 这两个寄存器对应 GPIO 基址的偏移是 20 和 16,先实现这两个寄存器的地址映射,其中 GPIOx_BASE 在库函数里面有定义。
1 // GPIO ODR 和 IDR 寄存器地址映射 2 #define GPIOA_ODR_Addr (GPIOA_BASE+20) 3 #define GPIOB_ODR_Addr (GPIOB_BASE+20) 4 #define GPIOC_ODR_Addr (GPIOC_BASE+20) 5 #define GPIOD_ODR_Addr (GPIOD_BASE+20) 6 #define GPIOE_ODR_Addr (GPIOE_BASE+20) 7 #define GPIOF_ODR_Addr (GPIOF_BASE+20) 8 #define GPIOG_ODR_Addr (GPIOG_BASE+20) 9 #define GPIOH_ODR_Addr (GPIOH_BASE+20) 10 #define GPIOI_ODR_Addr (GPIOI_BASE+20) 11 #define GPIOJ_ODR_Addr (GPIOJ_BASE+20) 12 #define GPIOK_ODR_Addr (GPIOK_BASE+20) 13 14 #define GPIOA_IDR_Addr (GPIOA_BASE+16) 15 #define GPIOB_IDR_Addr (GPIOB_BASE+16) 16 #define GPIOC_IDR_Addr (GPIOC_BASE+16) 17 #define GPIOD_IDR_Addr (GPIOD_BASE+16) 18 #define GPIOE_IDR_Addr (GPIOE_BASE+16) 19 #define GPIOF_IDR_Addr (GPIOF_BASE+16) 20 #define GPIOG_IDR_Addr (GPIOG_BASE+16) 21 #define GPIOH_IDR_Addr (GPIOH_BASE+16) 22 #define GPIOI_IDR_Addr (GPIOI_BASE+16) 23 #define GPIOJ_IDR_Addr (GPIOJ_BASE+16) 24 #define GPIOK_IDR_Addr (GPIOK_BASE+16)
现在我们就可以用位操作的方法来控制 GPIO 的输入和输出了,其中宏参数 n 表示具体是哪一个 IO 口,n(0,1,2…16)。这里面包含了端口 A~K ,并不是每个单片机型号都有这么多端口,使用这部分代码时,要查看你的单片机型号,如果是 176pin 的则最多只能使用到 I 端口。
1 // 单独操作 GPIO 的某一个 IO 口,n(0,1,2...16),n 表示具体是哪一个 IO 口 2 #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 3 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 4 5 #define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 6 #define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入 7 8 #define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出 9 #define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入 10 11 #define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出 12 #define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入 13 14 #define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出 15 #define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入 16 17 #define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出 18 #define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入 19 20 #define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出 21 #define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入 22 23 #define PHout(n) BIT_ADDR(GPIOH_ODR_Addr,n) //输出 24 #define PHin(n) BIT_ADDR(GPIOH_IDR_Addr,n) //输入 25 26 #define PIout(n) BIT_ADDR(GPIOI_ODR_Addr,n) //输出 27 #define PIin(n) BIT_ADDR(GPIOI_IDR_Addr,n) //输入 28 29 #define PJout(n) BIT_ADDR(GPIOJ_ODR_Addr,n) //输出 30 #define PJin(n) BIT_ADDR(GPIOJ_IDR_Addr,n) //输入 31 32 #define PKout(n) BIT_ADDR(GPIOK_ODR_Addr,n) //输出 33 #define PKin(n) BIT_ADDR(GPIOK_IDR_Addr,n) //输入
做好这一步配置以后,我们就可以通过宏定义直接拉高或者拉低某引脚
1 int main(void) 2 { 3 /* LED 端口初始化 */ 4 LED_GPIO_Config(); 5 6 while (1) { 7 // PH10 = 0,点亮 LED 8 PHout(10)= 0; 9 SOFT_Delay(0x0FFFFF); 10 11 // PH10 = 1,熄灭 LED 12 PHout(10)= 1; 13 SOFT_Delay(0x0FFFFF); 14 } 15 16 }
总结
位带操作使用起来简单高效,但是更加贴近硬件端,而不是贴近应用端,如资源有限需要高效率可如此操作,否则还是直接调用库函数比较方便。