嵌入式_对位带操作的理解和应用
最近返回去研究stm32,看到STM32部分空间支持了位带操作,就是可以使用普通的加载/存储指令来对单一的比特进行读写。所以写篇日志记录以下对于位带操作的理解,本文参考了《Cortex M3权威指南》。
前言
M3内核中,有两个区中实现了位带。其中一个是 SRAM 区的最低 1MB 范围,第二个则是片内外设区的最低 1MB 范围。这两个区中的地址除了可以像普通的 RAM 一样使用外,它们还都有自己的“位带别名区”,位带别名区把每个比特膨胀成一个 32 位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。
一、怎么理解位带操作
根据官方给的解释就是说,在静态RAM区有一个1M大小的存储空间A,把这块A空间上的每个字节的每一位都映射到另外一块32M大小的存储空间B上去,A被称之为位带区,B称之为位带别名区;我们可以通过对B空间进行操作间接性对A空间的每一个位进行精确读写操作。
有人说把这个看作为一种指针变量比较好理解,他的意思是把位带区的某个位X看作仅有1bit大小的存储空间,这个位X对应的位带别名区P理解为这个储存这个位的地址的指针变量。那么0x22000000就是位带区中位X的固定地址,只不过这个地址比较特殊,一般地址都是指向一个字节,这个地址是指向特殊区域的某一个位。
我一开始也是这样理解的,但是后来觉得这种理解方式有问题;
如果P是指针变量,那么它里面存放的是x位的地址,如果对x赋值,也因该是对地址0x22000000取值得到X的地址再进行读写操作:
但现在我们是直接对地址0x22000000进行读写操作,所以看成指针变量说不通,所以指针变量与映射还是有区别的;
二、注意事项
因为它的空间大小只有一个位,所以它的值只能是0或1。根据官方手册上所说,赋值是LSB有效,意思就是假如写了个0x8B(10001011)给这个位,那也只是最低位1有效,就给这个位写了个1 。
根据《Cortex M3权威指南》给出的具体读写过程是:当一个别地址被访问时,会先把该地址变换成位带地址。
读操作:读取位带地址中的一个字,再把需要的位右移到 LSB,并把 LSB 返回
写操作:把需要写的位左移至对应的位序号处,然后执行一个原子的“读-改-写”过程。
当然按照我的个人理解也是没什么问题的。
M3中支持位带操作的两个内存区的范围有两个,其分别是:
0x2000_0000‐0x200F_FFFF( SRAM 区中的最低 1MB)
0x4000_0000‐0x400F_FFFF(片上外设区中的最低 1MB)
对于SRAM位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr= 0x42000000+((A‐0x40000000)*8+n)*4 =0x42000000+ (A‐0x40000000)*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 个比特。
例如:
1 事先在地址 0x20000000 处写入四字节数据 0x3355AACC
2. 读取地址 0x22000008。本次读访问将读0x20000000,并提取比特 2,值为 1
3. 往地址 0x22000008 处写 0。本次操作将被映射成对地址 0x20000000 的“读-改-写操作(原子的),把比特 2 清 0。
4. 现在再读取 0x20000000,将返回 0x3355AAC8( bit[2]已清零)
二、优势与运用
1.对于硬件 I/O 密集型的底层程序最有用处,最简单就是直接操作GPIO 的管脚高低电平。
2.简化对于位的读或写。
3.在多任务务中,用于实现共享资源在任务间的“互锁”访问。多任务的共享资源必须满足一次只有一个任务访问它——亦即所谓的“原子操作”。以前的读-改-写需要 3 条指令,导致这中间留有两个能被中断的空当。是用位操作即是原子操作,不会被打断。
在C语言中并不支持直接位带操作,但是可以通过宏将位带区与位带别名区的地址定义出来进行访问。
STM32F407标准库代码如下(示例):
//把“位带地址+位序号”转换成别名地址的宏
#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,每组只有十六个GPIO!
#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) //输入
在STM32F407使用标准库的位带操作对GPIO 输出0或1的代码如下:
运用代码如下(示例):
#define LEDx PBout(14)
LEDx = 0; //关灯
LEDx = 1; //亮灯
注意:
当你使用位带功能时,要访问的变量必须用 volatile 来定义。因为 C 编译器并不知道同一个比特可以有两个地址。所以就要通过 volatile,使得编译器每次都如实地把新数值写入存储器,而不再会出于优化的考虑 , 在中途使用寄存器来操作数据的复本,直到最后把复本写回(这和 cache 的原理是一样的)
总结
在嵌入式操作中,支持位带操作还是比较方便与实用的
如有错误,欢迎指正,原创不易,转载留名!