08_stm32位带操作

一:位带操作介绍

1.   位带操作

  在学习 51 单片机的时候就使用过位操作, 通过关键字 sbit 对单片机 IO 口进行位定义。 但是 STM32 没有这样的关键字, 而是通过访问位带别名区来实现。即将每个比特位膨胀成一个 32 位字, 当访问这些字的时候就达到了访问比特的目的。 比方说 BSRR 寄存器有 32 个位, 那么可以映射到 32 个地址上, 当我们去访问这 32 个地址就达到访问 32 个比特的目的。
  STM32F1 中有两个区域支持位带操作, 一个是 SRAM 区的最低 1MB 范围, 一个是片内外设区的最低 1MB 范围(APB1、 APB2、 AHB 外设) 。 如图

  

   SRAM 的最低 1MB 区域, 地址范围是 0X2000 0000-0X200FFFFF。 片内外设最低 1MB 区域, 地址范围是 0X4000 0000-0X400F FFFF, 在这个地址范围内包括了 APB1、 APB2、 AHB 总线上所有的外设寄存器。
  在 SRAM 区中还有 32MB 空间, 其地址范围是 0X2200 0000-0X23FF FFFF, 它是 SRAM 的 1MB 位带区膨胀后的位带别名区, 前面已经说过位带操作, 要实现位操作即将每一位膨胀成一个 32 位的字, 因此 SRAM 的 1MB 位带区就膨胀为 32MB的位带别名区, 通过访问位带别名区就可以实现访问位带中每一位的目的。片内外设区的 32MB 的空间也是一样的原理。 片内外设区的 32MB 地址范围是0X4200 0000-0X43FF FFFF。
  通常我们使用位带操作都是在外设区, 在外设区中应用比较多的也就是GPIO 外设, SRAM 区内很少使用位操作。

2.   位带区与位带别名区的转换

  前面已经说过, 位带操作就是将位带区中的每一位膨胀成位带别名区中的一个 32 位的字, 通过访问位带别名区中的字就实现了访问位带区中位的目的。 因此我们就可以使用指针来访问位带别名区的地址, 从而实现访问位带区内位的目的。 那么位带别名区与位带区地址是如何转换的, 我们下面就来介绍下。
(1) 外设位带别名区地址
  对于片上外设位带区的某个比特, 记它所在字节的地址为 A,位序号为 n, n值的范围是 0-7, 则该比特在别名区的地址为:
    AliasAddr=0x42000000+ (A-0x40000000)*8*4 +n*4
  0x42000000 是外设位带别名区的起始地址, 0x40000000 是外设位带区的起始地址, (A-0x40000000) 表示该比特前面有多少个字节, 一个字节有 8 位, 所以*8, 一个位膨胀后是 4 个字节, 所以*4, n 表示该比特在 A 地址的序号, 因为一个位经过膨胀后是四个字节, 所以也*4。
(2) SRAM 位带别名区地址
  对于 SRAM 位带区的某个比特, 记它所在字节的地址为 A,位序号为 n, n 值的范围是 0-7, 则该比特在别名区的地址为:
    AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
  0x22000000 是 SRAM 位带别名区的起始地址, 0x20000000 是 SRAM 位带区的起始地址, (A-0x20000000) 表示该比特前面有多少个字节, 一个字节有 8 位,所以*8, 一个位膨胀后是 4 个字节, 所以*4, n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节, 所以也*4。

  上面我们已经把外设位带别名区地址和 SRAM 位带别名区地址使用公式表示出来, 为了操作方便, 我们将这两个公式进行合并, 通过一个宏来定义, 并把位带地址和位序号作为这个宏定义的参数。 公式如下:
    #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr&0xFFFFF)<<5)+(bitnum<<2))
  addr & 0xF0000000 是为了区分我们操作的是 SRAM 还是外设, 实际上就是获取最高位的值是 4 还是 2。 如果操作的是外设, 那么 addr & 0xF0000000 结果就是 0x40000000, 后面+0x2000000 就等于 0X42000000, 0X42000000 是外设别名区的起始地址。 如果操作的是 SRAM, 那么 addr & 0xF0000000 结果就是0x20000000, 后面+0x2000000 就等于 0X22000000, 0X22000000 是 SRAM 别名区的起始地址。

 addr & 0x000FFFFF 屏 蔽 了 高 三 位 , 相 当 于 减 去 0X20000000 或 者0X40000000, 屏蔽高三位是因为 SRAM 和外设的位带区最高地址是 0X200F FFFF和 0X400F FFFF, SRAM 或者外设位带区上任意地址减去其对应的起始地址, 总是低 5 位 有 效 , 所 以 这 里 屏 蔽 高 3 位 就 相 当 于 减 去 了 0X20000000 或 者0X40000000。 <<5 相当于*8*4, <<2 相当于*4, 其作用在前面已经分析过。最后就可以通过指针形式来操作这些位带别名区地址, 实现位带区对应位的操作。 代码如下:

  最后就可以通过指针形式来操作这些位带别名区地址, 实现位带区对应位的操作。 代码如下:
  //把 addr 地址强制转换为 unsigned long 类型的指针
    #define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
  //把位带别名区内地址转换为指针 , 获取地址内的数据
    #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
  这里说明下 volatile 关键字, volatile 提醒编译器它后面所定义的变量随时都有可能改变, 因此编译后的程序每次需要存储或读取这个变量的时候, 都会直接从变量地址中读取数据。 如果没有 volatile 关键字, 则编译器可能优化读取和存储, 可能暂时使用寄存器中的值, 如果这个变量由别的程序更新了的话,将出现不一致的现象。 更详细的内容大家可以百度查找。

3. 位带操作优点

  在 STM32 应用程序开发中虽然可以使用库函数操作外设, 但如果加上位操作就如虎添翼。 想想 51 单片机内位操作的方便, 就可以理解为什么要对 STM32 使用位操作。 STM32 位操作优点非常多, 我们这里就列举几个突出的:
  (1) 对于控制 GPIO 的输入和输出非常简单
  (2) 操作串行接口芯片非常方便(DS1302、 74HC595 等) , 如果采用库函数的话, 那么这个时序编写就非常不方便。
  (3) 代码简洁, 阅读方便

二:GPIO位带操作

  我们已经知道 STM32F1 支持的位带操作区有两个, 其中应用最多的还是外设位带区, 在外设位带区中包含了 APB1、 APB2 还有 AHB 总线上的所有外设寄存器,使用位带操作应用最多的外设还属 GPIO, 通过位带操作控制 STM32 引脚输入与输出, 因此我们就以 GPIO 中 IDR 和 ODR 这两个寄存器的位操作进行讲解。根据《STM32F10x 中文参考手册》 对应的 GPIO 寄存器章节中可以知道, IDR和 ODR 寄存器相对于 GPIO 基地址的偏移量是 8 和 12。 所以可以通过宏定义实现这两个寄存器的地址映射, 具体代码如下:

  //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
  从上述代码中可以看到有 GPIOx_BASE, 这个也是一个宏, 里面封装的是相应 GPIO 端口的基地址, 在库函数中有定义。获取寄存器的地址以后, 就可以采用位操作的方法来操作 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) //输入
  上述代码中我们已经将 STM32F1 芯片的所有端口都进行了位定义封装, 假如要使用 PC0 管脚进行输出, 那么就可以调用 PCout(n)宏, n 值即为 0。 假如使用的是 PC0 管脚作为输入, 那么就可以调用 PCin(n)宏, n 值即为 0。 其他端口调用方法类似。

三:软件设计

  前面我们已经将 GPIO 的 ODR 和 IDR 寄存器进行了位操作, 下面我们把它应用到我们程序中去, 通过位操作实现 D1 指示灯闪烁, D1 指示灯的硬件电路前面已经介绍过, 这里就不重复。
  我们复制上一次的工程文件夹, 重新命名为“ LED 闪烁(使用位带操作) ”,在其目录下新建一个 Public 文件夹, 用于存放 STM32F1 的公共应用程序文件,比如本章所要实现的位操作功能, 后面等到我们讲解 Systick 定时器和串口的时候也会把它们的驱动程序文件放入在此文件夹内, 所以如果要对 STM32F1 系列芯片进行程序开发, 可以复制我们这个文件夹到你的工程中, 实现位操作、 Systick精确延时和串口功能。 打开工程程序, 新建 system.c 和 system.h 文件, 将其存放在 Public 文件夹内, 并在 KEIL5 内添加其头文件路径system.c 内未写任何代码, 只是将其头文件调用进来, 方便工程中其他源文件调用, system.h 内把 GPIO 的 IDR 和 ODR 寄存器位操作进行了封装, 具体代码如下:
  #ifndef _system_H
  #define _system_H
  #include "stm32f10x.h"
//位带操作,实现 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))
//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
//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) //输入
  #endif
  从 system.h 内的代码可以看到, 内部还调用了 stm32f10x.h 文件, 因此在main.c 文件中可以直接调用 system.h, 既可包含 stm32f10x.h 内容也能包含位操作功能。要使用位操作来控制 LED, 需要在 led.h 文件中调用 system.h, 由于我们开发板上有 8 个 LED, 为了清楚操作的是哪个 LED, 我们根据其 IO 管脚进行宏定义,
代码如下:
    #define led1 PCout(0) //D1 指示灯连接的是 PC0 管脚
    #define led2 PCout(1) //D2 指示灯连接的是 PC1 管脚
    #define led3 PCout(2) //D3 指示灯连接的是 PC2 管脚
  其他的我们就不逐个定义, 如果要控制 PC0 管脚输出一个高电平, 直接就可以使用 led1=1 来表示, 这就和 51 单片机管脚操作方法一样。最后看下主函数, 主函数实现的功能比较简单, 首先对 LED 端口时钟及管脚模式配置进行初始化, 然后通过位操作不断取反 PC0 管脚电平, 实现 LED 闪烁效果, 里面还用到了一个 delay 延时, 这个延时函数只是一个大约的延时, 要实现精确延时, 在后面章节中我们会讲解到。  

int main()
{
  LED_Init();
  while(1)
  {
    led1=!led1; //D1 状态取反
    delay(6000000);
  }
}

猜你喜欢

转载自www.cnblogs.com/eokey/p/11989233.html
今日推荐