STM32 位带操作


一、简介

如果你学过 51,是否还记得你是如何点亮的 led?很简单,见下:

sbit LED1 = P2^0; // 控制到 P2.0 脚
LED1 = 0; // 输出一个低电平

通过简单的两句话就可以点亮一个 led 了。这就是位带(Bit-banding)操作,支持位带操作后,可以使用普通的加载/存储指令来对单一的比特进行读写。

而我们也知道,在 STM32 中并不能直接操作寄存器的某一个 Bit 位,而只能通过字来读写寄存器,例如:

那能否实现 51 类似的 GPIO 控制功能,能够直接操作位?我们知道,STM32 是 32 位的处理器,其 32 位地址总线提供了 4G 的地址空间。于是 Cortex-M 就利用额外的地址在内核中开辟了一块地址区域(位带别名):可以将 IDR1 这类 Bit(位带区)映射到位带别名区域对应的地址,只需要操作映射后的地址,就可以实现操作这个 IDR1 位了。在主位带区域,每个地址对应一个字节的数据,在“位带别名”区域中,每个地址对应同一个数据的一个位。简单来说就是映射操作。

参考 <<CM3权威指南>> 第五章(P87~P92)

二、映射关系

不过,在 STM32F407 中,有两个地方实现了位带:

  1. SRAM 区的最低 1MB 空间
    SRAM 的位带区的地址为:0X2000 0000~X200F 0000,大小为 1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF,大小为 32MB。操作 SRAM 的比特位这个用得很少。

  2. 外设区最低 1MB 空间。
    外设位带区的地址为:0X40000000~0X400F0000,大小为 1MB,这 1MB 的大小包含了 APB1/2 和 AHB1 上所有外设的寄存器,AHB2/3 总线上的寄存器没有包括。 AHB2 总线上的外设地址范围为:0X50000000~0X50060BFF,AHB3 总线上的外设地址范围为:0XA0000000~0XA0000FFF。 外设位带区经过膨胀后的位带别名区地址为:0X42000000~0X43FFFFFF,这部分地址空间为保留地址,没有跟任何的外设地址重合。

这两个 1MB 的空间除了可以像正常的 RAM 一样操作外, 他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。


也就是说,位带操作就是把位带区中一个地址的 8 个位分别映射到位带别名区的 8 个地址(LSB 有效,即最低位有效),通过操作相应地址的方式实现操作某个位。

为什么 LSB 有效?
因为 STM32 的系统总线是 32 位的, 按照 4 个字节访问的时候是最快的,所以膨胀成 4 个字节来访问是最高效的。

位带区里每个地址的每 1 位膨胀为别名区里一个 32 位的字(32 位处理器中,1字=4字节),例如:0x20000000 的第 0 位对应 0x22000000,第 1 位对应 0x22000004 等。

三、地址转换

1、外设位带别名区地址

对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n ( 0 < = n < = 31 0<=n<=31 0<=n<=31)(n的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:

AliasAddr= =0x42000000 + (A-0x40000000)*8*4 +n*4
  • 0X42000000 是外设位带别名区的起始地址
  • 0x40000000是外设位带区的起始地址,则(A-0x40000000)表示该比特前面有多少个字节
  • 一个字节有 8 位,所以 *8;而一个位膨胀后是 4 个字节,所以 *4
  • n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也 *4

2、SRAM 位带别名区地址

对于 SRAM 位带区的某个比特,记它所在字节的地址为 A,位序号为 n ( 0 < = n < = 31 0<=n<=31 0<=n<=31)(n 的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:

AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4

公式分析同上。

3、统一公式

为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名区地址统一成一个宏。

#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))
  • addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2:
    • 如果是外设,则取出的是 4,+ 0X02000000 之后就等于 0X420000000X42000000 是外设别名区的起始地址
    • 如果是 SRAM,则取出的是 2,+ 0X02000000 之后就等于 0X220000000X22000000 是 SRAM 别名区的起始地址。
  • addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是为什么是屏蔽高三位?
    • 因为外设的最高地址是:0X20100000, 跟起始地址 0X20000000 相减的时候,总是低 5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。
    • SRAM 同理分析即可。<<5 相当于 *8*4<<2 相当于 *4,这两个我们在上面分析过。

最后我们就可以通过指针的形式操作这些位带别名区地址,最终实现位带区的比特位操作。

// 把一个地址转换成一个指针
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr))

// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum))

四、GPIO 位带操作

经过上面的铺垫,就可以轻松实现 STM32 上 GPIO 位带操作了:

#define BITBAND(addr, bitnum) ((addr & 0xF0000000) + 0x2000000 + ((addr & 0x000FFFFF) << 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)  // 输入

使用位带操作,不仅更加方便,而且步骤少,更重要的是位带操作属于原子操作

比如,欲设置地址 0x2000 0000 中的比特 2,则使用位带操作的设置过程如下图所示:

对应的汇编代码如下:

写操作同理:

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值