背景介绍
位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32-M3, M4内核的单片机中没有这样的关键字,而是通过访问位带别名区来实现位操作。
以STM32F407为例,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,另一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字( F407 的系统总线是 32 位的,按照 4 个字节访问的时候是最快的,所以膨胀成 4 个字节来访问是最高效的),当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。下图所示为STM32F407的位带地址区域。
外设位带区的地址为:0x40000000~0x400F0000,大小为 1MB,这 1MB 的大小包含了 APB1/2 和 AHB1 上面所有外设的寄存器,AHB2/3 总线上的寄存器没有包括。AHB2 总线上的外设地址范围为:0x50000000~0x50060BFF,AHB3 总线上的外设地址范为:0xA0000000~0xA0000FFF。外设位带区经过膨胀后的位带别名区地址为:0x42000000~0x43FFFFFF,这部分地址空间为保留地址,没有跟任何的外设地址重合。
SRAM位带区的地址为:0x20000000~0x200F0000,大小为 1MB,经过膨胀后的位带别名区地址为:0x22000000~0x23FFFFFF,大小为 32MB。通过位带别名操作 SRAM 的比特位用得很少。
我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。
资料来源:
- 《STM32F4xx 中文参考手册》存储器和总线构架章节、GPIO 章节
- 《Cortex®-M4 内核编程手册》2.2.5 Bit-banding
GPIO位带操作的实现
对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr = 0x42000000+ (A-0x40000000)*8*4 +n*4
0x42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节,一个字节有 8 位,所以*8,一个位膨胀后是 4 个字节,所以*4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。
对于 SRAM 位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
公式分析同上。
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名区地址统一成一个宏。
#define BITBAND(addr, bitnum) ((addr&0xF0000000)+0x2000000+((addr&0x000FFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是外设,则取出的是 4,+0x0200 0000 之后就等于 0x4200 0000,0x4200 0000 是外设别名区的起始地址。如果是 SRAM,则取出的是 2,+0x0200 0000 之后就等于 0x2200 0000,0x22000000 是 SRAM 别名区的起始地址。
addr & 0x000F FFFF 屏蔽了高三位,相当于减去 0x2000 0000 或者 0x4000 0000,但是为什么是屏蔽高三位?因为外设的最高地址是:0x2010 0000,跟起始地址 0x2000 0000 相减的时候,总是低 5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。SRAM 同理分析即可。<<5 相当于*8*4 (2^5),<<2 相当于*4 (2^2)。
实现GPIO位带操作的源码如下:
/*----Realization of bit-band operation of GPIO----*/
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_VAL(addr) *((unsigned long *)(addr))
#define MEM_ADDR(addr) ((unsigned long *)addr)
#define BIT_VAL(addr, bitnum) MEM_VAL(BITBAND(addr, bitnum))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO address mapping by value
#define GPIOA_ODR_Addr (GPIOA_BASE+20) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+20) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+20) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+20) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+20) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+20) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+20) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+16) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+16) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+16) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+16) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+16) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+16) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+16) //0x40011E08
//Operation of single IO.
//Note that n must less than 16.
#define GPIOA_OUT(n) BIT_VAL(GPIOA_ODR_Addr,n) //Output
#define GPIOA_IN(n) BIT_VAL(GPIOA_IDR_Addr,n) //Input
#define GPIOB_OUT(n) BIT_VAL(GPIOB_ODR_Addr,n)
#define GPIOB_IN(n) BIT_VAL(GPIOB_IDR_Addr,n)
#define GPIOC_OUT(n) BIT_VAL(GPIOC_ODR_Addr,n)
#define GPIOC_IN(n) BIT_VAL(GPIOC_IDR_Addr,n)
#define GPIOD_OUT(n) BIT_VAL(GPIOD_ODR_Addr,n)
#define GPIOD_IN(n) BIT_VAL(GPIOD_IDR_Addr,n)
#define GPIOE_OUT(n) BIT_VAL(GPIOE_ODR_Addr,n)
#define GPIOE_IN(n) BIT_VAL(GPIOE_IDR_Addr,n)
#define GPIOF_OUT(n) BIT_VAL(GPIOF_ODR_Addr,n)
#define GPIOF_IN(n) BIT_VAL(GPIOF_IDR_Addr,n)
#define GPIOG_OUT(n) BIT_VAL(GPIOG_ODR_Addr,n)
#define GPIOG_IN(n) BIT_VAL(GPIOG_IDR_Addr,n)
//get address
#define GPIOA_OUT_ADDR(n) BIT_ADDR(GPIOA_ODR_Addr,n) //Output
#define GPIOA_IN_ADDR(n) BIT_ADDR(GPIOA_IDR_Addr,n) //Input
#define GPIOB_OUT_ADDR(n) BIT_ADDR(GPIOB_ODR_Addr,n)
#define GPIOB_IN_ADDR(n) BIT_ADDR(GPIOB_IDR_Addr,n)
#define GPIOC_OUT_ADDR(n) BIT_ADDR(GPIOC_ODR_Addr,n)
#define GPIOC_IN_ADDR(n) BIT_ADDR(GPIOC_IDR_Addr,n)
#define GPIOD_OUT_ADDR(n) BIT_ADDR(GPIOD_ODR_Addr,n)
#define GPIOD_IN_ADDR(n) BIT_ADDR(GPIOD_IDR_Addr,n)
#define GPIOE_OUT_ADDR(n) BIT_ADDR(GPIOE_ODR_Addr,n)
#define GPIOE_IN_ADDR(n) BIT_ADDR(GPIOE_IDR_Addr,n)
#define GPIOF_OUT_ADDR(n) BIT_ADDR(GPIOF_ODR_Addr,n)
#define GPIOF_IN_ADDR(n) BIT_ADDR(GPIOF_IDR_Addr,n)
#define GPIOG_OUT_ADDR(n) BIT_ADDR(GPIOG_ODR_Addr,n)
#define GPIOG_IN_ADDR(n) BIT_ADDR(GPIOG_IDR_Addr,n)
使用指南
上述的源码中主要定义了四种宏(以GPIOA为例):
- GPIOA_OUT(n),GPIOA的第n个引脚输出寄存器对应的值
- GPIOA_OUT_ADDR(n),GPIOA的第n个引脚输出寄存器对应的位带地址
- GPIOA_IN(n),GPIOA的第n个引脚输入寄存器对应的值
- GPIOA_IN_ADDR(n),GPIOA的第n个引脚输入寄存器对应的位带地址
通过使用GPIOA的第n个引脚输出/输入寄存器对应的值的宏定义,可以方便的设定/获取GPIO端口的值。示例如下:
#define SYS_LED GPIOA_OUT(15)
#define SYS_EXTI_0_STATE GPIOB_IN(0)
SYS_LED = 1; //点亮LED灯,设定PA15端口的值为1
SYS_LED = 0; //关闭LED灯,设定PA15端口的值为0
uint8_t state = GPIOB_IN(0); //获取PB0端口的值
有时候我们需要需要顺序操作多个GPIO端口的值,一种便捷的方法就是构造GPIO位带操作指针数组,然后通过数组的标号枚举操作每个GPIO端口。示例如下:
#define CD4066_1_E1_ADDR GPIOA_OUT_ADDR(4)
#define CD4066_2_E1_ADDR GPIOA_OUT_ADDR(1)
#define CD4066_3_E1_ADDR GPIOA_OUT_ADDR(0)
#define CD4066_1_E2_ADDR GPIOE_OUT_ADDR(8)
#define CD4066_2_E2_ADDR GPIOE_OUT_ADDR(7)
#define CD4066_3_E2_ADDR GPIOB_OUT_ADDR(1)
#define CD4052_A1_ADDR GPIOE_OUT_ADDR(3)
#define CD4052_B1_ADDR GPIOE_OUT_ADDR(4)
#define CD4052_INH1_ADDR GPIOE_OUT_ADDR(5)
#define CD4052_A2_ADDR GPIOC_OUT_ADDR(4)
#define CD4052_B2_ADDR GPIOC_OUT_ADDR(5)
#define CD4052_INH2_ADDR GPIOB_OUT_ADDR(0)
#define WAS3157B_SEL1_ADDR GPIOE_OUT_ADDR(2)
#define WAS3157B_SEL2_ADDR GPIOA_OUT_ADDR(7)
/**
* @brief 位带操作的端口指针 数组
* 设置这个数组的目的是方便在for循环中顺序操作端口的高低电平
*/
unsigned long *PortArray_Addr[] =
{
CD4066_1_E1_ADDR,
CD4066_2_E1_ADDR,
CD4066_3_E1_ADDR,
CD4066_1_E2_ADDR,
CD4066_2_E2_ADDR,
CD4066_3_E2_ADDR,
CD4052_A1_ADDR,
CD4052_B1_ADDR,
CD4052_INH1_ADDR,
CD4052_A2_ADDR,
CD4052_B2_ADDR,
CD4052_INH2_ADDR,
WAS3157B_SEL1_ADDR,
WAS3157B_SEL2_ADDR,
};
/**
* @brief 刷新线圈寄存器,根据线圈寄存器的值执行对应动作
* 将GPIO端口的输出状态设置为线圈寄存器对应变量的值
* @param none
* @retval none
*/
void User_RefreshCoilsRegister()
{
/*省略无关代码*/
for(uint8_t i = 0; i < sizeof(PortArray_Addr)/sizeof(*PortArray_Addr); i++)
{
*PortArray_Addr[i] = CoilsReg_GetBit(4 + i);
}
/*省略无关代码*/
}
在某些项目中,某些功能是数个GPIO端口操作的集合,同时这些功能又在每个通道中重复一次,这个时候如果为每个功能都定义一个函数会比较麻烦。可以采用带参数的宏定义来处理多通道中的GPIO端口操作的集合。示例如下:
//定义位带操作的端口指针数组(每个变量都是多个通道同一个功能控制端口的集合)
unsigned long *CD4066_1E_PortArray_Addr[] = { CD4066_1E_1_ADDR, CD4066_1E_2_ADDR };
unsigned long *CD4066_2E_PortArray_Addr[] = { CD4066_2E_1_ADDR, CD4066_2E_2_ADDR };
unsigned long *CD4066_3E_PortArray_Addr[] = { CD4066_3E_1_ADDR, CD4066_3E_2_ADDR };
unsigned long *CD4052_A_PortArray_Addr[] = { CD4052_A1_ADDR, CD4052_A2_ADDR };
unsigned long *CD4052_B_PortArray_Addr[] = { CD4052_B1_ADDR, CD4052_B2_ADDR };
unsigned long *CD4052_INH_PortArray_Addr[] = { CD4052_INH1_ADDR, CD4052_INH2_ADDR };
unsigned long *WAS3157B_SEL_PortArray_Addr[] = { WAS3157B_SEL1_ADDR, WAS3157B_SEL2_ADDR };
//采用带参数的宏定义来处理多通道中的GPIO端口操作的集合,从而避免麻烦的函数声明与定义
#define SENSOR_FUN_AcceToVelo(n) *CD4052_INH_PortArray_Addr[n] = 0;\
*CD4052_A_PortArray_Addr[n] = 0;\
*CD4052_B_PortArray_Addr[n] = 1;\
*WAS3157B_SEL_PortArray_Addr[n] = 1
#define SENSOR_FUN_AcceToDisp(n) *CD4052_INH_PortArray_Addr[n] = 0;\
*CD4052_A_PortArray_Addr[n] = 0;\
*CD4052_B_PortArray_Addr[n] = 0;\
*WAS3157B_SEL_PortArray_Addr[n] = 0
#define SENSOR_FUN_VeloToVelo(n) *CD4052_INH_PortArray_Addr[n] = 0;\
*CD4052_A_PortArray_Addr[n] = 0;\
*CD4052_B_PortArray_Addr[n] = 0;\
*WAS3157B_SEL_PortArray_Addr[n] = 1
#define SENSOR_FUN_VeloToDisp(n) *CD4052_INH_PortArray_Addr[n] = 0;\
*CD4052_A_PortArray_Addr[n] = 1;\
*CD4052_B_PortArray_Addr[n] = 0;\
*WAS3157B_SEL_PortArray_Addr[n] = 0
#define SENSOR_FUN_DispToDisp(n) *CD4052_INH_PortArray_Addr[n] = 0;\
*CD4052_A_PortArray_Addr[n] = 0;\
*CD4052_B_PortArray_Addr[n] = 1;\
*WAS3157B_SEL_PortArray_Addr[n] = 0
//在业务处理函数中,可以像调用函数那样调用宏定义
void User_SensorFunctionHandler(uint8_t chan, uint8_t fun)
{
switch(fun)
{
case 0:
SENSOR_FUN_AcceToVelo(chan);
break;
case 1:
SENSOR_FUN_AcceToDisp(chan);
break;
case 2:
SENSOR_FUN_VeloToVelo(chan);
break;
/*省略部分代码*/
default:
break:
}
}