STM32学习总结——GPIO位带操作

cathy的头像

本章参考资料:《STM32F10X-中文参考手册》存储器和总线构架章节、GPIO 章节,《CM3 权威指南 CnR2》存储器系统章节。
位带简介
  
位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32没有这样的关键字,而是通过访问位带别名区来实现。
  
在STM32中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,令一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。

“”

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

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

位带区和位带别名区地址转换
  
位带区的一个比特位经过膨胀之后,虽然变大到 4 个字节,但是还是 LSB (最低位)才有效。有人会问这不是浪费空间吗,要知道 STM32 的系统总线是 32 位的,按照 4 个字节访问的时候是最快的,所以膨胀成 4 个字节来访问是最高效的。我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。那这两个地址直接如何转换,我们简单介绍一下。

外设位带别名区地址

对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
  
1、 AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
0X42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节,一个字节有 8 位,所以*8,一个位膨胀后是 4 个字节,所以*4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。

SRAM 位带别名区地址

对于 SRAM 位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则
该比特在别名区的地址为:

1 AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4

公式分析同上。

统一公式

为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名区地址统一成一个宏。
  
1 、// 把“位带地址+位序号”转换成别名地址的宏
  
2、 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr &0x00FFFFFF)<<5)+(bitnum<<2))
  
addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是外设,则取出的是 4,+0X02000000 之后就等于 0X42000000,0X42000000 是外设别名区的起始地址。如果是 SRAM,则取出的是 2,+0X02000000 之后就等于 0X22000000,0X22000000 是 SRAM 别名区的起始地址。
  
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X2010 0000,跟起始地址 0X20000000 相减的时候,总是低 5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。SRAM 同理分析即可。<<5 相当于*8*4,<<2 相当于*4,这两个我们在上面分析过。
  
最后我们就可以通过指针的形式操作这些位带别名区地址,最终实现位带区的比特位操作。   

1 // 把一个地址转换成一个指针
2 #define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
3 // 把位带别名区地址转换成指针
4 #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum)) 

 
这里说明下 volatile 关键字,volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

位带操作的优点

在 STM32 应用程序开发中虽然可以使用库函数操作外设,但如果加上位操作就如虎添翼。想想 51 单片机内位操作的方便,就可以理解为什么要对 STM32 使用位操作。STM32 位操作优点非常多,我们这里就列举几个突出的:

(1)对于控制 GPIO 的输入和输出非常简单

(2)操作串行接口芯片非常方便(DS1302、74HC595 等),如果采用库函数的话,那么这个时序编写就非常不方便。

(3)代码简洁,阅读方便

GPIO 位带操作
  
我们已经知道 STM32F1 支持的位带操作区有两个,其中应用最多的还是外设位带区,在外设位带区中包含了 APB1、APB2 还有 AHB 总线上的所有外设寄存器,使用位带操作应用最多的外设还属 GPIO,通过位带操作控制 STM32 引脚输入与输出。
  
外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。但这个在实际项目中不是很现实,也很少人会这么做,我们在这里仅仅演示下 GPIO 中 ODR 和 IDR 这两个寄存器的位操作。
  
从手册中我们可以知道 ODR 和 IDR 这两个寄存器对应 GPIO 基址的偏移是 12 和 8,我们先实现这两个寄存器的地址映射,其中 GPIOx_BASE 在库函数里面有定义。

GPIO 寄存器映射

 1 //IO口地址映射
 2 #define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
 3 #define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
 4 #define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
 5 #define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
 6 #define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
 7 #define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
 8 #define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    
 9 
10 #define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
11 #define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
12 #define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
13 #define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
14 #define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
15 #define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
16 #define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 

现在我们就可以用位操作的方法来控制 GPIO 的输入和输出了,其中宏参数 n 表示具体是哪一个 IO 口,n(0,1,2...16)。这里面包含了端口 A~G ,并不是每个单片机型号都有这么多端口,使用这部分代码时,要查看你的单片机型号,如果是 64pin 的则最多只能使用到 C 端口。

GPIO 位操作

 1 //IO口操作,只对单一的IO口!
 2 //确保n的值小于16!
 3 #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出 
 4 #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入 
 5 
 6 #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 
 7 #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入 
 8 
 9 #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出 
10 #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入 
11 
12 #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出 
13 #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入 
14 
15 #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出 
16 #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入
17 
18 #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出 
19 #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入
20 
21 #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出 
22 #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入

将上面的代码整理

 1 #ifndef _system_H
 2 #define _system_H
 3 
 4 
 5 #include "stm32f10x.h"
 6 
 7 
 8 //位带操作,实现51类似的GPIO控制功能
 9 //具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
10 //IO口操作宏定义
11 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
12 #define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
13 #define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 
14 //IO口地址映射
15 #define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
16 #define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
17 #define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
18 #define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
19 #define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
20 #define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
21 #define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    
22 
23 #define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
24 #define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
25 #define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
26 #define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
27 #define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
28 #define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
29 #define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
30  
31 //IO口操作,只对单一的IO口!
32 //确保n的值小于16!
33 #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出 
34 #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入 
35 
36 #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 
37 #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入 
38 
39 #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出 
40 #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入 
41 
42 #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出 
43 #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入 
44 
45 #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出 
46 #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入
47 
48 #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出 
49 #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入
50 
51 #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出 
52 #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入
53 
54 
55 #endif

主函数

该工程我们直接从 LED-库函数 操作移植过来,有关 LED GPIO 初始化和软件延时等函数我们直接用,修改的是控制 GPIO 输出的部分改成了位操作。该实验我们让 IO 口输出高低电平来控制 LED 的亮灭,负逻辑点亮。具体使用哪一个 IO 和点亮方式由硬件平台决定。

 1 #include "system.h"
 2 #include "led.h"
 3 
 4 
 5 /*******************************************************************************
 6 * 函 数 名         : delay
 7 * 函数功能           : 延时函数,通过while循环占用CPU,达到延时功能
 8 * 输    入         : i
 9 * 输    出         : 无
10 *******************************************************************************/
11 void delay(u32 i)
12 {
13     while(i--);
14 }
15 
16 /*******************************************************************************
17 * 函 数 名         : main
18 * 函数功能           : 主函数
19 * 输    入         : 无
20 * 输    出         : 无
21 *******************************************************************************/
22 int main()
23 {
24     LED_Init();
25     while(1)
26     {
27         led1=!led1;
28         delay(6000000);
29     }
30 }

本文作者:最迷人的地方不是远方,而是在这里却没有欣赏到此刻的美丽—北极星!
本文链接:https://www.cnblogs.com/zhjblogs/p/12790708.html
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。