10-GPIO位带操作

1 位带

  • “位带”这两个字表示的是一种内存访问机制,允许通过特定的地址映射来直接操作内存中某个字节的单个位,从而实现更高效的位级控制。

2 位操作与总线操作的区别

  • 位操作
    • 是指对单个比特位进行读和写的过程。操作允许开发者直接控制某个特定的位,而不需要操作整个字或字节;
  • 总线操作
    • 是指 对整个字或字节进行的读写操作。这种方式更适合处理需要同时操作多个位的数据。

3 STM32 中的位操作

在 STM32 中,位操作的实现主要依赖于位带别名区。STM32F4 系列(如 F429)提供了两个主要的位带区域:

  1. SRAM 区的位带别名区
    • 位于 SRAM 的最低 1MB 空间;
    • 允许对 SRAM 中的每个位进行独立的读写操作。
  2. 外设区的位带别名区
    • 位于外设区域的最低 1MB 空间;
    • 允许直接对外设的控制寄存器中的某个位进行操作。

4 位带别名区的工作原理

位带操作的核心思想是通过将特定的比特位(位带区)映射到一个32位的内存空间(位带别名区),使得可以通过操作这个32位的地址来间接地操作原始的比特位。

4.1 为什么要映射

每个比特位通过位带机制被映射到一个32位(4字节)的地址空间。尽管每个比特位的实际存储需求是1位,但为了高效地访问,使用了4字节的别名地址。这里空间变大了,为什么还会高效呢?

在STM32F429 中系统总线是32 位的,按照4个字节访问是最快的,所以膨胀成 4 个字节来访问是最高效的。

4.2 位带区和位带别名区地址转换

4.2.1 转换公式

AliasAddr= =bit_band_alias_base+ (Abit_band_base)*8*4 +bit_number*4

在这个公式中,每个步骤运行后的单位会很混乱,让人理解不透这个公式,在下方进行了详细的解释:

  • AliasAddr:代表别名区域中将映射到目标位的字的地址
    • 单位:字节(Byte)
  • bit_band_alias_base 代表别名区域的起始地址
    • 单位:字节(Byte)
  • bit_band_base 代表位带区域的起始地址
    • 单位:字节(Byte)
  • Abit_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)
0x4000000000x42000000
0x4000000010x42000004
0x4000000020x42000008
0x4000000030x4200000C
0x4000000040x42000010
0x4000000050x42000014
0x4000000060x42000018
0x4000000070x4200001C
0x4000000100x42000020
0x4000000110x42000024
0x4000000120x42000028
0x4000000130x4200002C
0x4000000140x42000030
0x4000000150x42000034
0x4000000160x42000038
0x4000000170x4200003C

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--);
}

发表评论