写位带操作的起因,是我用沁恒的ch32v30x系列单片机时,移植了STM32上一些外设的代码,但发现STM32对于GPIO部分用到了很多位带操作,而基于RISC-V架构的ch32并没有位带操作,因此特地重新温习一下STM32的位带操作原理。

文章目录

  • 一、位带操作是什么
  • 二、位带与位带别名区
  • 1.外设位带区
  • 2.SRAM 位带区
  • 3.地址映射关系
  • 三、使用实例
  • 1.位带操作宏定义
  • 2.GPIO的位带操作
  • 3.使用效果
  • 总结

  • 一、位带操作是什么

    用教科书的话来说,位带操作(Bit Banding),位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32 没有这样的关键字,而是通过访问位带别名区来实现。
      在 STM32 中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,令一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。它通过将内存的每一位映射到一个独立的地址空间,从而实现对单个位的读写操作。
    具体来说,位带操作是通过将一个32位的内存地址映射到另外一个32位的地址空间来实现的。例如,假设我们要对一个变量的第0位进行操作,那么就可以将该变量的地址通过位带映射到一个新的地址空间,该地址空间的第0位对应原地址变量的第0位,这样就可以直接对该位进行读写操作,而不需要对整个变量进行读写。

    用人话来说,就是STM32如果要具体对比如PA的1号引脚进行比特位级别的操作时,是无法直接实现的,那为了单独地对这个比特位进行操作赋值,就要使用位带操作。把这个bit位映射到一个长度为32bit,即4个字节的另外的一个内存空间里去,也就是说给这个1bit位起了一个别名,但是你可以直接操作这个别名,效果等同映射给本名。将原来PA1的地址扩展成一个32位的字地址,对32位的地址进行操作。
    更通俗的说,我想吃西瓜、苹果、梨这三种水果的某一个,但是有明确的限制,不允许吃有具体名称的水果,那我给西瓜起个别名叫地上长的水果,苹果叫树上长的水果,那么,我可以,并且吃的是地上长的水果,实际上吃的就是西瓜,绕开了这个限制,同时吃到了想要吃的具体水果。

    二、位带与位带别名区

    1.外设位带区

    外设外带区的地址为:0X40000000~0X40100000,大小为 1MB,这 1MB 的大小在 103系列大/中/小容量型号的单片机中包含了片上外设的全部寄存器,这些寄存器的地址为:0X40000000~0X40029FFF 。
    外设位带区经过膨胀后的位带别名区地址为0X42000000~0X43FFFFFF,这个地址仍然在 CM3 片上外设的地址空间中。在 103 系列大/中小容量型号的单片机里面,0X40030000~0X4FFFFFFF 属于保留地址,膨胀后的 32MB位带别名区刚好就落到这个地址范围内,不会跟片上外设的其他寄存器地址重合。
      STM32 的全部寄存器都可以通过访问位带别名区的方式来达到访问原始寄存器比特位的效果,这比 51 单片机强大很多。因为 51 单片机里面并不是所有的寄存器都是可以比特位操作,有些寄存器还是得字节操作,比如 SBUF。
      虽然说全部寄存器都可以实现比特操作,但我们在实际项目中并不会这么做,甚至不会这么做。有时候为了特定的项目需要,比如需要频繁的操作很多 IO 口,这个时候我们可以考虑把 IO 相关的寄存器实现比特操作。

    2.SRAM 位带区

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

    3.地址映射关系

    以外设位带区为例:
    对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
      AliasAddr= =0x42000000+ (A-0x40000000)✖8✖4 +n✖4
    0x42000000是位带别名区域的起始地址,A是输出数据寄存器GPIOA->ODR的地址,A的地址先减去位带区基地址,得到的是相对于位带区基地址的偏移地址,那么膨胀之后还是一个偏移地址,是相对于位带别名区基地址的偏移量,加上位带别名区域基地址,就得到了其对应的别名区地址,这是总的原理,一个字节有 8 位,所以✖8,一个位膨胀后是 4 个字节,所以乘以4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也乘以4。

    三、使用实例

    1.位带操作宏定义

    这里经常会遇到的一个STM32的实例,是对GPIO输入输出的操作,具体代码为:

    //位带操作,实现51类似的GPIO控制功能
    //具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
    //IO口操作宏定义
    #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)) 
    

    BITBAND(addr, bitnum):用于将一个地址和一个位号映射到位带地址空间中。具体来说,它将地址addr和位号bitnum映射到了一个位于0x20000000~0x3FFFFFFF之间的地址空间中,并返回该地址空间中对应的地址。
    说人话,addr & 0xF0000000是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是外设,则取出的是 4。+0X02000000 之后就等于 0X42000000,0X42000000 是外设别名区的起始地址。addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X2010 0000,跟起始地址 0X20000000 相减的时候,总是低 5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果。<<5是将其左侧的数值乘以2的5次方,即左移5位相当于将其左侧的数值乘以32,同理<<2是乘以4,bitnum是具体比特位的序号。

    MEM_ADDR(addr):用于将一个地址转换为一个指向该地址的指针。具体来说,它将地址addr转换为一个指向unsigned long类型的volatile变量的指针。

    BIT_ADDR(addr, bitnum):用于将一个地址和一个位号映射到位带地址空间中,并返回该地址空间中对应的地址的指针。具体来说,它使用了BITBAND和MEM_ADDR两个宏,先将地址addr和位号bitnum映射到位带地址空间中,然后使用MEM_ADDR将该地址转换为指针。

    2.GPIO的位带操作

    首先,GPIO的ODR和IDR是GPIO端口的输出数据寄存器和输入数据寄存器,用于控制GPIO引脚的输入和输出状态。
    其次,ODR 和 IDR 这两个寄存器对应 GPIO 基址的偏移是 12 和 8,这两个寄存器的地址映射如下:

    //IO口地址映射
    #define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
    #define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
    #define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
    #define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
    #define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
    #define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
    #define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    
    
    #define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
    #define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
    #define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
    #define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
    #define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
    #define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
    #define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
    

    最后,实现GPIO的位操作:

    //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)  //输入
    

    GPIOA_ODR_Addr就是GPIOA的ODR寄存器的地址,n是具体引脚序号,这样便完成了针对GPIO具体引脚的位带操作。

    3.使用效果

    #define SDA PCout(4)
    SDA=1;
    

    这里的宏定义#define SDA PCout(4)实际上是将SDA定义为一个函数式宏,将PC4引脚的输出状态控制封装到了SDA函数中。具体来说,PCout(4)是一个函数式宏,用于控制PC4引脚的输出状态。当PC4引脚需要被设置为高电平时,SDA=1会被展开为PCout(4)=1,从而将PC4引脚的输出状态设置为高电平。
    这里的PCout(4)是另外一个宏定义,用于将GPIO端口的ODR寄存器和引脚的编号映射到对应的位带地址空间中,并返回该地址空间中对应的引脚状态的指针。因此,SDA=1实际上是通过修改位带地址空间中对应引脚状态的指针来间接控制引脚的输出状态。

    总结

    STM32 位操作优点比如:
    (1)对于控制 GPIO 的输入和输出非常简单
    (2)操作串行接口芯片非常方便(DS1302、74HC595 等),如果采用库函数的话,那么这个时序编写就非常不方便。
    (3)代码简洁,阅读方便。
    这就是位带操作,但对于其他架构的芯片,没有位带操作时,就只能老老实实用相关函数来实现功能。下一篇文章,我将详细分析串口接收中断与串口空闲中断。

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32位带操作详解

    发表评论