GPIO—位带操作
本章参考资料:《STM32F4xx 中文参考手册》存储器和总线构架章节、GPIO章节,《Cortex®-M4内核编程手册》2.2.5 Bit-banding。学习本章时,配套这些参考资料学习效果会更佳。
14.1. 位带简介
位操作就是可以单独的对一个比特位读和写,这个在51单片机中非常常见。51单片机中通过关键字sbit来实现位定义,F407中没有这样的关键字,而是通过访问位带别名区来实现。
在F407中,有两个地方实现了位带,一个是SRAM区的最低1MB空间,另一个是外设区最低1MB空间。这两个1MB的空间除了可以像正常的RAM一样操作外, 他们还有自己的位带别名区,位带别名区把这1MB的空间的每一个位膨胀成一个32位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。
F407位带地址
14.1.1. 外设位带区
外设位带区的地址为:0X4000 0000~0X400F 0000,大小为1MB,这1MB的大小包含了APB1/2和AHB1上所以外设的寄存器,AHB2/3总线上的寄存器没有包括。 AHB2总线上的外设地址范围为:0X5000 0000~0X5006 0BFF,AHB3总线上的外设地址范围为:0XA000 0000~0XA0000FFF。 外设位带区经过膨胀后的位带别名区地址为:0X4200 0000~0X43FF FFFF,这部分地址空间为保留地址,没有跟任何的外设地址重合。
14.1.2. SRAM位带区
SRAM的位带区的地址为:0X2000 0000~X200F 0000,大小为1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF,大小为32MB。操作SRAM的比特位这个用得很少。
14.1.3. 位带区和位带别名区地址转换
位带区的一个比特位经过膨胀之后,虽然变大到4个字节,但是还是LSB才有效。有人会问这不是浪费空间吗,要知道F407的系统总线是32位的, 按照4个字节访问的时候是最快的,所以膨胀成4个字节来访问是最高效的。
我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。那这两个地址直接如何转换,我们简单介绍一下。
14.1.3.1. 外设位带别名区地址
对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7,其实n为100甚至10000都不影响,因为下面的公式等号右边完全是以位来计算的,虽然表面上是以字节计算)(n的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
(最后这个为啥为4的原因:因为1个位会膨胀成32位,即4个字节)
0X42000000是外设位带别名区的起始地址,0x40000000是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节, 一个字节有8位,所以8,一个位膨胀后是4个字节,所以4,n表示该比特在A地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。
14.1.3.2. SRAM位带别名区地址
对于SRAM位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7)(n的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
(最后这个为啥为4的原因:因为1个位会膨胀成32位,即4个字节)
公式分析同上。
14.1.3.3. 统一公式
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名区地址统一成一个宏。
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000是为了区别SRAM还是外设,实际效果就是取出4或者2,如果是外设,则取出的是4,+0X02000000之后就等于0X42000000, 0X42000000是外设别名区的起始地址。如果是SRAM,则取出的是2,+0X02000000之后就等于0X22000000,0X22000000是SRAM别名区的起始地址。
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去0X20000000或者0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X20100000, 跟起始地址0X20000000相减的时候,总是低5位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。 SRAM同理分析即可。<<5相当于84,<<2相当于*4,这两个我们在上面分析过。
最后我们就可以通过指针的形式操作这些位带别名区地址,最终实现位带区的比特位操作。
// 把一个地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
14.2. GPIO位带操作
外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。 但这个在实际项目中不是很现实,也很少人会这么做,我们在这里仅仅演示下GPIO中ODR和IDR这两个寄存器的位操作。
从手册中我们可以知道ODR和IDR这两个寄存器对应GPIO基址的偏移是20和16,我们先实现这两个寄存器的地址映射,其中GPIOx_BASE在库函数里面有定义。
14.2.1. GPIO 寄存器映射
代码清单:位带操作-1 GPIO ODR 和 IDR 寄存器映射
// 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 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)
现在我们就可以用位操作的方法来控制GPIO的输入和输出了,其中宏参数n表示具体是哪一个IO口,n(0,1,2…15)。这里面包含了端口A~K , 并不是每个单片机型号都有这么多端口,使用这部分代码时,要查看你的单片机型号,如果是144pin的则最多只能使用到H端口。
14.2.2. GPIO位操作
代码清单:位带操作-2 GPIO位操作
注意这段话:单独操作 GPIO的某一个IO口,n(0,1,2…15) ,需思考为什么可以是大于8,按道理1个字节只有8位噢,上面有解释原因
// 单独操作 GPIO的某一个IO口,n(0,1,2...15),
// 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) //输入
14.2.3. 主函数
该工程我们直接从“GPIO输出—使用固件库点亮LED”操作移植过来,有关LED GPIO 初始化和软件延时等函数我们直接用, 修改的是控制GPIO输出的部分改成了位操作。该实验我们让相应的IO口输出高低电平来控制LED的亮灭,负逻辑点亮。 具体使用哪一个IO和点亮方式由硬件平台决定。
代码清单:位带操作-3 main 函数
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* LED 端口初始化 */
LED_GPIO_Config();
while (1) {
// PF6 = 0,点亮LED
PFout(6)= 0;
SOFT_Delay(0x0FFFFF);
// PF6 = 1,熄灭LED
PFout(6)= 1;
SOFT_Delay(0x0FFFFF);
}
}
14.2.4. 下载验证
把编译好的程序下载到开发板并复位,可看到红灯在闪烁
注意:位带实操细节
0X40000000地址位3置1操作:
//AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
*(uint32_t*)(0x40000000) |= (1<<3);
//等价于 (0x40000000 + 12 = 0x40000000 + 0x0c)
*(uint32_t*)(0x4200000c) = 0x01;//只要最低位是1即可
//也等价于
*(uint32_t*)(0x4200000c) = 0x03;
//也等价于
*(uint32_t*)(0x4200000c) = 0x05;
0X40000000地址位3置0操作:
//AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
*(uint32_t*)(0x40000000) &= ~(1<<3);
//等价于 (0x40000000 + 12 = 0x40000000 + 0x0c)
*(uint32_t*)(0x4200000c) = 0x00;//只要最低位是0即可
//也等价于
*(uint32_t*)(0x4200000c) = 0x02;
//也等价于
*(uint32_t*)(0x4200000c) = 0x04;
0X40000002地址位3置1操作:
//AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
*(uint32_t*)(0x40000002) |= (1<<3);
//等价于 (0x40000000 + 64 + 12 = 0x40000000 + 76 = 0x40000000 + 0x4c)
*(uint32_t*)(0x4200004c) = 0x01;//只要最低位是1即可
//也等价于
*(uint32_t*)(0x4200004c) = 0x03;
//也等价于
*(uint32_t*)(0x4200004c) = 0x05;