云计算百科
云计算领域专业知识百科平台

【STM32】Proteus仿真STM32教程(寄存器)4--寄存器版流水灯程序优化(STM32位带区&位带操作)

💡写在前面

大家好! 最近很多刚入门的小伙伴私信我:上节课写的寄存器版流水灯,每次改IO电平都要写GPIOB->ODR |= (1<<n)或者GPIOA->ODR &= ~(1<<n),移位写多了容易搞混位数,还担心写错掩码把其他位改崩了,有没有像51单片机那样sbit LED = P0^0直接操作某一位的方法?

当然有!今天我们就学习STM32专门为简化位操作设计的位带机制,再也不用记复杂的位与、位或逻辑,新手也能写出可读性拉满的寄存器代码~

准备好了吗?坐稳扶好,我们要发车了!🏎️

STM32位带区&位带操作

  • 一、先搞懂:什么是位带操作?为什么我们需要它?
    • 1.1 传统寄存器位操作的痛点
    • 1.2 位带操作的通俗解释
  • 二、零基础也能算:位带地址的换算公式
    • 2.1 公式推导说明
    • 2.2 举个实际例子算一遍
    • 2.3 公式优化(工程实用版)
  • 三、一劳永逸:把位带操作封装成通用宏
  • 四、实操:用位带操作改写流水灯程序
    • 4.1 完整代码示例
    • 4.2 代码讲解
  • 五、位带操作的优点(为什么推荐初学者学?)
  • 六、 免费获取资料:

一、先搞懂:什么是位带操作?为什么我们需要它?

1.1 传统寄存器位操作的痛点

我们先回忆一下上节课操作PB7引脚输出电平的代码:

// 熄灭PB7
GPIOB->ODR &= ~(1 << 7);
// 点亮PB7
GPIOB->ODR |= (1 << 7);

这种写法有3个很不友好的点:

  • ❌ 容易写错移位位数:比如把7写成5,操作的引脚直接错了
  • ❌ 可读性差:新手看代码要反应半天才能知道是操作第7位
  • ❌ 风险高:如果掩码写错(比如漏写~),会把ODR寄存器的其他位覆盖,导致同组其他IO电平异常

那有没有办法直接给某一个IO引脚的电平赋值,完全不影响其他位?就像这样:

PB7 = 1; // 直接点亮PC13
PB7 = 0; // 直接熄灭PC13

STM32的位带操作,就是帮我们实现这个功能的底层机制。

1.2 位带操作的通俗解释

你可以把STM32的寄存器当成一个有32个格子的储物柜,每个格子对应1个bit,传统操作是要改某一个格子的东西,得把整个柜子搬出来,找到对应的格子改了再放回去;而位带操作就是给每个格子单独装了一个门,你直接开对应格子的门就能改内容,完全不用碰其他格子。

  • 官方一点的定义:
    • STM32的Cortex-M3内核专门划分了两块「位带区」,位带区的每一个bit,都会映射到「位带别名区」的一个独立的32位地址。我们操作这个位带别名地址的最低位,就等价于直接操作原地址对应的那个bit,硬件保证不会影响其他位。

    STM32有两个位带区,我们新手只需要重点关注外设位带区就行:

    位带区类型位带区地址范围对应的位带别名区地址范围用途
    外设位带区 0x40000000 ~ 0x400FFFFF 0x42000000 ~ 0x43FFFFFF 操作GPIO、串口、SPI等外设寄存器的位
    SRAM位带区 0x20000000 ~ 0x200FFFFF 0x22000000 ~ 0x23FFFFFF 操作SRAM变量的某一位
  • 关键概念:位带别名区
    • 位带区的每1个比特,都会被“膨胀”成1个32位的字(占4字节),这些膨胀出来的地址集合就是「位带别名区」。
    • 比如外设位带区的某一位,对应到别名区就是一个4字节的地址,你给这个地址(4字节)写1/0,就等于给 原位(1个比特)置1/0,完全不用关心整个寄存器的其他位。

    二、零基础也能算:位带地址的换算公式

    我们不用死记硬背公式,只要理解逻辑就能自己推出来:

    我们要操作的寄存器地址为ADDR,要操作的是第n位,那么对应的别名地址计算公式为:

    • 别名地址 = 位带别名区基地址 + (ADDR – 位带区基地址) * 32 + n * 4

    2.1 公式推导说明

    给大家拆解一下每个部分的含义,保证看一遍就懂:

  • ADDR – 位带区基地址:计算你要操作的寄存器,在整个位带区的偏移字节数
  • 乘32:因为1个字节有8个bit,每个bit对应别名区4个字节,所以8 * 4 = 32
  • n * 4:你要操作的是第n位,每个位对应别名区4个字节,所以乘4找对应位置
  • 2.2 举个实际例子算一遍

    比如我们要操作GPIOB的ODR寄存器的第7位:

  • 先查手册:GPIOB基地址是0x40010C00,ODR寄存器的偏移是0x0C,所以ODR的地址ADDR = 0x40010C00 + 0x0C = 0x40010C0C

  • 外设位带区基地址是0x40000000,别名区基地址是0x42000000

  • 代入公式:

    别名地址 = 0x42000000 + (0x40010C0C – 0x40000000) * 32 + 7*4 = 0x4221819C

  • 我们只要往0x4221819C这个地址写1,PB7就会输出高电平;写0就输出低电平,完全不影响GPIO的其他引脚。

  • 2.3 公式优化(工程实用版)

    我们要操作的寄存器地址为ADDR,要操作的是第n位,那么对应的别名地址计算公式为:

    • 别名地址 =((ADDR & 0xF0000000)+0x02000000+((ADDR & 0x000FFFFF)<<5)+(n<<2)

    为什么实际工程采用优化版?

    • 因为优化后的版本能一个公式通吃外设/SRAM,采用逻辑运算符,运算速度快,边界兼容性好

    公式优化说明:

  • 🔹 第一段:(ADDR & 0xF0000000) + 0x02000000 = 对应位带区的别名区基地址
    • ADDR & 0xF0000000是取地址的最高4位,区分当前是SRAM还是外设地址:如果是外设地址,结果就是0x40000000;如果是SRAM地址,结果就是0x20000000,刚好是位带区的基地址
    • 两个位带区的别名区和原区差值固定是0x02000000,加完直接得到对应的别名区基地址,不用手动判断是外设还是SRAM
    • 外设:0x40000000 + 0x02000000 = 0x42000000(外设别名区起始地址) SRAM:0x20000000 + 0x02000000 = 0x22000000(SRAM别名区起始地址)
  • 🔹 第二段:(ADDR & 0x000FFFFF) <<5 = (ADDR – 位带区基地址)*32
    • 两个位带区都只有1MB大小,区内偏移只需要低20位就能表示,ADDR & 0x000FFFFF直接抹掉高位前缀,得到的结果完全等价于ADDR – 位带区基地址
    • 左移5位等价于乘32,和通用公式的运算完全一致,位运算比减法速度更快,也避免了减法可能出现的边界错误
  • 🔹 第三段:n <<2 = n *4
    • 每个位对应别名区4个字节的地址,左移2位等价于乘4,完全等价

    三、一劳永逸:把位带操作封装成通用宏

    不可能每次操作位都自己算地址对吧?我们可以把换算逻辑写成宏,以后直接调用就行,新手直接抄下面的代码,不用改就能用。

    // 核心:位带别名地址换算宏
    // addr: 要操作的寄存器地址 bitnum:要操作的位号(0~31)
    #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+\\
    0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))

    // 把别名地址转成可操作的指针,实现读写
    #define BITADDR(addr, bitnum) *(volatile uint32t *)(BITBAND(addr, bitnum))

  • BITBAND宏: 这个宏的作用是:输入寄存器的地址和要操作的位序号,输出对应的位带别名区地址。每个部分功能上文已经拆解。

  • BIT_ADDR宏:怎么操作别名地址? *(volatile uint32_t )(BITBAND(addr, bitnum)) 做了两件事:

    • 把BITBAND算出的别名地址,强制转换成uint32_t*类型的指针(因为STM32是32位处理器,操作32位地址更高效)
    • 用*对指针做读写操作,本质就是给这个别名地址赋值/取值,等效于操作原寄存器的对应位
      • volatile关键字作用是告诉编译器不要优化这个地址的操作,每次读写都要直接访问内存,

    四、实操:用位带操作改写流水灯程序

    4.1 完整代码示例

    typedef unsigned int uint32_t;
    typedef unsigned char uint8_t;
    // 外设位带区宏定义
    #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+\\
    0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))

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

    // 时钟寄存器地址定义
    #define AHB_RCC_BASE (uint32_t)0x40021000

    #define RCC_CR (AHB_RCC_BASE+0x00)
    #define RCC_CFGR (AHB_RCC_BASE+0x04)
    #define RCC_CIR (AHB_RCC_BASE+0x08)
    #define RCC_APB2RSTR (AHB_RCC_BASE+0x0C)
    #define RCC_APB1RSTR (AHB_RCC_BASE+0x10)
    #define RCC_AHBENR (AHB_RCC_BASE+0x14)
    #define RCC_APB2ENR (AHB_RCC_BASE+0x18)
    #define RCC_APB1ENR (AHB_RCC_BASE+0x1C)
    #define RCC_BDCR (AHB_RCC_BASE+0x20)
    #define RCC_CSR (AHB_RCC_BASE+0x24)

    // GPIOB寄存器地址定义
    #define GPIOB_BASE (uint32_t)0x40010c00

    #define GPIOB_CRL (GPIOB_BASE+0x00)
    #define GPIOB_CRH (GPIOB_BASE+0x04)
    #define GPIOB_IDR (GPIOB_BASE+0x08)
    #define GPIOB_ODR (GPIOB_BASE+0x0C)
    #define GPIOB_BSRR (GPIOB_BASE+0x10)
    #define GPIOB_BRR (GPIOB_BASE+0x14)
    #define GPIOB_LCKR (GPIOB_BASE+0x18)

    // 定义PB的位带操作宏
    #define PB_OUT(n) BIT_ADDR(GPIOB_ODR,n)

    // 延时函数(空循环,新手不用深究,改数值可调整流水速度)
    void Delay(uint32_t time)
    {
    uint32_t i,j;
    for(i=0; i<time; i++)
    for(j=0; j<1000; j++);
    }
    int main(void)
    {
    uint8_t i;
    //开启GPIOB口时钟
    BIT_ADDR(RCC_APB2ENR,3)=1;
    //GPIOB_0~7 配置推挽输出 50mhz
    for(i=0;i<8;i++)
    {
    BIT_ADDR(GPIOB_CRL,(0+4*i))=1;
    BIT_ADDR(GPIOB_CRL,(1+4*i))=1;
    BIT_ADDR(GPIOB_CRL,(2+4*i))=0;
    BIT_ADDR(GPIOB_CRL,(3+4*i))=0;
    }
    //GPIOB_0~7 led off
    for(i=0;i<8;i++)
    {
    PB_OUT(i) = 0;
    }

    while(1)
    {

    // 依次点亮PB0-PB7
    for(i=0;i<8;i++)
    {
    PB_OUT(i) = 1;
    Delay(200);
    PB_OUT(i) = 0;
    }
    }
    }

    void SystemInit(void)
    {

    }

    4.2 代码讲解

    代码里有几个典型场景用到了位带操作,我们逐一解释:

  • PB口的位带操作宏
  • // 定义PB的位带操作宏
    #define PB_OUT(n) BIT_ADDR(GPIOB_ODR,n)

    • PB_OUT(n)完美复刻51单片机sbit的操作逻辑,新手直接用PB_OUT(0)=1就能控制PB0输出高电平,不用记任何移位逻辑
  • 时钟开启用位带操作
  • BIT_ADDR(RCC_APB2ENR,3)=1;

    • 查手册可知RCC_APB2ENR寄存器的位3对应GPIOB的时钟使能位,直接给该位写1即可,不需要写RCC_APB2ENR |= (1<<3),完全不会误改其他外设的时钟开关
  • 配置GPIO口模式(操作GPIOB_CRL寄存器)
  • for(i=0;i<8;i++)
    {
    BIT_ADDR(GPIOB_CRL,(0+4*i))=1;
    BIT_ADDR(GPIOB_CRL,(1+4*i))=1;
    BIT_ADDR(GPIOB_CRL,(2+4*i))=0;
    BIT_ADDR(GPIOB_CRL,(3+4*i))=0;
    }

    • GPIOB_CRL是GPIOB端口低8位的配置寄存器,每4位控制1个IO口(比如第0-3位控制PB0,第4-7位控制PB1,以此类推) 。要配置PB0-PB7为50MHz推挽输出,对应的位值是0011 。
    • 用BIT_ADDR直接操作寄存器的第0+4*i等位,直接赋值1或0,不用先读整个寄存器、修改、再写回,代码更简洁、不容易出错
  • 流水灯控制
  • // 依次点亮PB0-PB7
    for(i=0;i<8;i++)
    {
    PB_OUT(i) = 1;
    Delay(200);
    PB_OUT(i) = 0;
    }

    • 流水灯逻辑完全没有复杂的位运算,哪怕是第一次学STM32的新手也能一眼看懂:逐个点亮、延时、逐个熄灭。和51的LED=1写法几乎一样,非常直观 。

    五、位带操作的优点(为什么推荐初学者学?)

  • 代码极简直观:不用写复杂的位运算(比如GPIOB_ODR |= (1<<0)、GPIOB_ODR &= ~(1<<7)),直接像操作变量一样操作寄存器的某一位
  • 避免“读-改-写”风险:普通位操作需要先读取寄存器值、修改对应位、再写回,多线程/中断场景下可能出错;位带操作直接修改目标位,一步到位
  • 51入门友好:有51基础的话,能快速上手STM32的IO位操作,降低学习门槛
  • 六、 免费获取资料:

    关注下面的 芦苇电子 微信公众号,

    在公众号内 私信回复

    032

    收到后自动发送该仿真资料


    如果你能坚持看到这里,说明你已经具备了成为嵌入式大神的潜质!💪 别光看,赶紧打开你的 Keil,新建一个工程,把这段代码跑起来试试吧!

    如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!——你的认可,是我持续输出嵌入式硬核干货的最大动力!我们下期再见! 🌟

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【STM32】Proteus仿真STM32教程(寄存器)4--寄存器版流水灯程序优化(STM32位带区&位带操作)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!