文章目录
1 位带
- “位带”这两个字表示的是一种内存访问机制,允许通过特定的地址映射来直接操作内存中某个字节的单个位,从而实现更高效的位级控制。
2 位操作与总线操作的区别
- 位操作
- 是指对单个比特位进行读和写的过程。操作允许开发者直接控制某个特定的位,而不需要操作整个字或字节;
- 总线操作
- 是指 对整个字或字节进行的读写操作。这种方式更适合处理需要同时操作多个位的数据。
3 STM32 中的位操作
在 STM32 中,位操作的实现主要依赖于位带别名区。STM32F4 系列(如 F429)提供了两个主要的位带区域:
- SRAM 区的位带别名区:
- 位于 SRAM 的最低 1MB 空间;
- 允许对 SRAM 中的每个位进行独立的读写操作。
- 外设区的位带别名区:
- 位于外设区域的最低 1MB 空间;
- 允许直接对外设的控制寄存器中的某个位进行操作。

4 位带别名区的工作原理
位带操作的核心思想是通过将特定的比特位(位带区)映射到一个32位的内存空间(位带别名区),使得可以通过操作这个32位的地址来间接地操作原始的比特位。
4.1 为什么要映射
每个比特位通过位带机制被映射到一个32位(4字节)的地址空间。尽管每个比特位的实际存储需求是1位,但为了高效地访问,使用了4字节的别名地址。这里空间变大了,为什么还会高效呢?
在STM32F429 中系统总线是32 位的,按照4个字节访问是最快的,所以膨胀成 4 个字节来访问是最高效的。
4.2 位带区和位带别名区地址转换
4.2.1 转换公式
AliasAddr= =bit_band_alias_base+ (A–bit_band_base)*8*4 +bit_number*4
在这个公式中,每个步骤运行后的单位会很混乱,让人理解不透这个公式,在下方进行了详细的解释:
- AliasAddr:代表别名区域中将映射到目标位的字的地址
- 单位:字节(Byte)
- bit_band_alias_base: 代表别名区域的起始地址
- 单位:字节(Byte)
- bit_band_base: 代表位带区域的起始地址
- 单位:字节(Byte)
- A–bit_band_base:代表目标位所在位段区域中的字节偏移
- 单位:字节(Byte)
- 8:因为一个字节有8位,所以要乘以8来计算比特位的偏移
- 由于一个字节包含8位(bit),所以在这个步骤中,我们将字节偏移量乘以8,结果仍然是字节(Byte)。这个操作的目的是将比特位的偏移量转换为字节级别的偏移量。
- 4:膨胀后的每个位占用4个字节(32位)
- 在此步骤中,我们将上一步的字节偏移量再乘以4。这里的4代表每个比特位在位带别名区中占用的字节数(即一个32位单元占用4个字节)。所以,最终的结果也是字节(Byte),表示该比特位在位带别名区中的具体地址偏移量。
- bit_number: 代表目标位的位位置 (0-7)
- 单位:无单位
- 这里的 n 是一个整数,表示我们要操作的比特位的索引
- 4:一个位经过膨胀后是4个字节
- 单位:字节
- 在位带操作中,每个比特位被映射到一个32位(4字节)的内存单元。因此,乘以4是为了将比特位的索引 n 转换为相应的字节偏移量。
4.2.2 参考表格
位带区地址 (A) | 比特位索引 (n) | 计算的别名地址 (AliasAddr) |
0x40000000 | 0 | 0x42000000 |
0x40000000 | 1 | 0x42000004 |
0x40000000 | 2 | 0x42000008 |
0x40000000 | 3 | 0x4200000C |
0x40000000 | 4 | 0x42000010 |
0x40000000 | 5 | 0x42000014 |
0x40000000 | 6 | 0x42000018 |
0x40000000 | 7 | 0x4200001C |
0x40000001 | 0 | 0x42000020 |
0x40000001 | 1 | 0x42000024 |
0x40000001 | 2 | 0x42000028 |
0x40000001 | 3 | 0x4200002C |
0x40000001 | 4 | 0x42000030 |
0x40000001 | 5 | 0x42000034 |
0x40000001 | 6 | 0x42000038 |
0x40000001 | 7 | 0x4200003C |
4.2.3 合并公式
在这里根据外设和SRAM 基地址的不同,转换公式也不同,如下所示:
- 外设转换公式:
- AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +bit_number*4
- SRAM 转换公式:
- AliasAddr= =0x22000000+ (A-0x20000000)*8*4+bit_number*4
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址 + 位序号”转换成别名区地址统一成一个宏。
- 合并后的公式
- ((bit_band_alias_base& 0xF0000000)+0x02000000+((bit_band_alias_base& 0x000FFFFF)<<5)+(bit_number<<2))
- bit_band_alias_base & 0xF0000000:取出地址的高 4 位,以判断是 SRAM 还是外设。
- 0x02000000:固定偏移量,根据地址类型选择不同的别名起始地址。
- bit_band_alias_base& 0x000FFFFF:屏蔽了高 3 位,主要是为了减去起始地址 0x20000000或 0x40000000
- 由于外设的最高地址是 0x20100000,与起始地址相减时,只有低 5 位是有效的,因此屏蔽高 3 位是为了简化计算。同样,SRAM 的最高地址也需要类似的处理。
- << 5:相当于乘以 32,用于计算位带地址的偏移。
- << 2:相当于乘以 4,用于计算位索引的偏移。
5 GPIO 位带操作
外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。
5.1 GPIO 寄存器映射
// 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) //输入
5.2 完整代码
以下代码来源于野火整合:
https://gitee.com/Embedfire-stm32f429-tiaozhanzhe/ebf_stm32f429_tiaozhanzhe_std_code
完整项目从 固件库模板 迁移:
http://ckun.fun/index.php/2024/12/03/standard_library/
修改main.c:
#include "stm32f4xx.h" // 把“位带地址+位序号”转换成别名地址的宏 #define BITBAND(addr, bitnum)((addr & 0xF0000000) + 0x02000000 + ((addr & 0x000FFFFF) << 5) + (bitnum << 2)) // 把一个地址转换成一个指针 #define MEM_ADDR(addr) * ((volatile unsigned long * )(addr)) // 把位带别名区地址转换成指针 #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum)) // 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) //输入 void LED_GPIO_Config(uint16_t GPIO_Pin); void SOFT_Delay(__IO uint32_t nCount); int main(void) { /* LED 端口初始化 */ LED_GPIO_Config(GPIO_Pin_10); while (1) { // 点亮LED PHout(10) = 0; SOFT_Delay(0x0FFFFF); // 熄灭LED PHout(10) = 1; SOFT_Delay(0x0FFFFF); } } // PH10 <-> 红灯 void LED_GPIO_Config(uint16_t GPIO_Pin) { // 定义一个GPIO_InitTypeDef类型的结构体 GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIO的时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH, ENABLE); // 选择要控制的IO口 GPIO_InitStructure.GPIO_Pin = GPIO_Pin; // 设置IO口输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 设置IO输出为推挽模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // IO 内部上拉 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // IO 输出速率为50M GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 初始化IO口 GPIO_Init(GPIOH, & GPIO_InitStructure); // IO 默认输出高电平 GPIO_SetBits(GPIOH, GPIO_Pin); } // 简陋的软件延时函数 void SOFT_Delay(__IO uint32_t nCount) { for (; nCount != 0; nCount--); }