我相信很多朋友在学习单片机之前都学习过51单片机,假设在51单片机的P1.1的IO口上挂了一个LED,那么你单独对LED的操作就是P1.1 = 0或P1.1 = 1,这样你就可以单独的对P1端的第一个IO口进行上下拉操作,然而对于STM32,是没有这种操作的,那么为了像51单片机一样能够单独的对某个端的某一个IO单独操作,就引入了位带操作,简而言之,就是为了去单独操作Cortex-M里面PA的第1个IO口,所以才有了位带这样的操作机制。下面以STM32为例进行讲解。
1 什么是位带操作
在讲解位带操作之前,首先要搞清楚什么是位带操作。我们知道,32位的处理器的32位地址总线提供了4G的地址空间,几乎所有的嵌入式产品是足够用的。Cortex-M就利用了额外的空间实现了称为位带(Bit-Banding)操作的硬件属性,该技术使用地址空间的两个不同区域来指向同一物理地址。在主位带区域,每个地址对应一个字节的数据,在“位带别名”区域中,每个地址对应同一个数据的一个位。
如下图所示。在CM3的寄存器映射图中有1MB的 bit band区,这里被称为位带区,与之对应的是32MB的bit band别名区,这里被称为位带别名区。
STM32的位带别名区会把位带区中的每一位膨胀成一个32位的字,所以相应的别名区的内存也会是位带区的32倍。从上图可以看出,位带操作同时支持SRAM和片上外设,支持位带操作的两个内存区域范围如下:
SRAM区:0x20000000 ~ 0x200FFFFF,最低1M的范围;
片上外设区: 0x40000000 ~ 0x400FFFFF,最低1M的范围;
位带操作就是把位带区中一个地址的8个位分别映射到位带别名区的8个地址(LSB有效,即最低位有效),通过操作相应地址的方式实现操作某个位。以SRAM为例,位带区和位带别名区的映射如下图所示:
位带区里每个地址的每1位膨胀为别名区里一个32位的字(32位处理器中,1字=4字节),例如:0x20000000的第0位对应0x22000000,第1位对应0x22000004等。
2 位带操作的计算公式
既然位带操作属于Cortex-M内核的一部分,那么在Cortex-M官方手册也是给出了相应的计算公式的,其通用公式如下:
别名区地址 = 别名区起始地址 + (位字节地址偏移量 * 8 + n) * 4
其中,8表示一个字节有8位,4表示膨胀了4个字节,因此位带区和位带别名区也就是32倍的关系。
两个区的计算公式为:
SRAM区:AliasAddr = 0x22000000 + (A - 0x20000000) * 32 + n * 4
片上外设区:AliasAddr = 0x42000000 + (A - 0x40000000)* 32 + n * 4
其中,AliasAddr是别名区的地址,A是位带区的地址,n是该端口的上的某一位。
接下来就是对这个地址进行操作了,写1,该位输出1,写0,就输出0。
3 位带操作代码实现
这里STM32F1为例,根据STM32的《RM0008 Reference manual》手册,其GPIO的地址映射如下:
GPIOx_ODR 寄存器如下:
每个寄存器32位,占4个地址,在访问或修改某个寄存器时,是从首地址开始的,逻辑运算则是直接可涵盖到32bit,offset 为 0x0C。GPIOA 的首地址为0x40010800,因此GPIOx_ODR 寄存器的地址为0x4001080C。则所有的GPIO映射如下:
//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口操作宏定义#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))
复制代码好了,接下来使用位带操作来写一个GPIO流水灯,同时使用库函数来做比较。
【main.c】
/* Includes ------------------------------------------------------------------*/#include "stm32f1_bsp_led.h" /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/ /*简单延时函数*/ void Delay(uint32_t xms); /* Private functions ---------------------------------------------------------*/ /** * @brief 主函数 * @param None * @retval */ int main(void) { /* LED 初始化 */ LED_GPIO_Config(); while (1) { #if 0 GPIO_SetBits(GPIOB,GPIO_Pin_0); // 亮 Delay(0xfFfff); GPIO_ResetBits(GPIOB,GPIO_Pin_0); // 灭 GPIO_SetBits(GPIOG,GPIO_Pin_6); // 亮 Delay(0xfFfff); GPIO_ResetBits(GPIOG,GPIO_Pin_6); // 灭 GPIO_SetBits(GPIOG,GPIO_Pin_7); // 亮 Delay(0xffFff); GPIO_ResetBits(GPIOG,GPIO_Pin_7); // 灭 #else PBout(0) = 1; // 亮 Delay(0xfFfff); PBout(0) = 0; // 灭 PGout(6) = 1; // 亮 Delay(0xfFfff); PGout(6) = 0; // 灭 PGout(7) = 1; // 亮 Delay(0xffFff); PGout(7) = 0; // 灭 #endif } } /** * @brief 延时函数 * @param xms 延时长度 * @retval None */ void Delay( uint32_t xms) { while(xms--);//(方法二) }
复制代码/* Includes ------------------------------------------------------------------*/#include "stm32f1_bsp_led.h" /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/ /* Private functions ---------------------------------------------------------*/ /** * @brief 初始化LED的GPIO * @param None * @retval None */ void LED_GPIO_Config(void) { /*定义一个GPIO_InitTypeDef类型的结构体*/ GPIO_InitTypeDef GPIO_InitStructure; /*开启LED的外设时钟*/ RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE); /*设置IO口*/ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置引脚模式为通用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置引脚速率为50MHz /*调用库函数,初始化GPIOB0*/ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //选择要控制的GPIOB引脚 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;/*选择要控制的引脚*/ GPIO_Init(GPIOG, &GPIO_InitStructure); /* 开启所有led灯*/ GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOG, GPIO_Pin_6|GPIO_Pin_7); }
复制代码#ifndef __STM32F1_BSP_LED_H__#define __STM32F1_BSP_LED_H__ #ifdef __cplusplus extern "C" { #endif /* Includes ------------------------------------------------------------------*/ #include "stm32f10x.h" /* Exported types ------------------------------------------------------------*/ /* Exported constants --------------------------------------------------------*/ /* Exported macro ------------------------------------------------------------*/ //位带操作,实现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) //输入 #define ON 1 #define OFF 0 /* 带参宏,可以像内联函数一样使用 */ #define LED1(a) if (a) \ GPIO_SetBits(GPIOB,GPIO_Pin_0);\ else \ GPIO_ResetBits(GPIOB,GPIO_Pin_0) #define LED2(a) if (a) \ GPIO_SetBits(GPIOG,GPIO_Pin_6);\ else \ GPIO_ResetBits(GPIOG,GPIO_Pin_6) #define LED3(a) if (a) \ GPIO_SetBits(GPIOG,GPIO_Pin_7);\ else \ GPIO_ResetBits(GPIOG,GPIO_Pin_7) /* 直接操作寄存器的方法控制IO */ #define digitalHi(p,i) {p->BSRR=i;} //设置为高电平 #define digitalLo(p,i) {p->BRR=i;} //输出低电平 #define digitalToggle(p,i) {p->ODR ^=i;} //输出反转状态 /* 定义控制IO的宏 */ #define LED1_TOGGLE digitalToggle(GPIOB,GPIO_Pin_0) #define LED1_ON digitalHi(GPIOB,GPIO_Pin_0) #define LED1_OFF digitalLo(GPIOB,GPIO_Pin_0) #define LED2_TOGGLE digitalToggle(GPIOC,GPIO_Pin_4) #define LED2_ON digitalHi(GPIOG,GPIO_Pin_6) #define LED2_OFF digitalLo(GPIOG,GPIO_Pin_6) #define LED3_TOGGLE digitalToggle(GPIOC,GPIO_Pin_3) #define LED3_ON digitalHi(GPIOG,GPIO_Pin_7) #define LED3_OFF digitalLo(GPIOG,GPIO_Pin_7) /* Exported functions ------------------------------------------------------- */ void LED_GPIO_Config(void); #ifdef cplusplus } #endif #endif /* __STM32F1_BSP_LED_H__ */
复制代码实例:欲设置地址 0x2000_0000 中的比特 2,则使用普通操作和位带操作的设置过程如下图所示:
普通操作和位带操作的汇编对比代码如下:
位带读操作相对简单,普通操作和位带操作的设置过程如下图所示:
普通操作和位带操作的汇编对比代码如下:
可以看出位带操作的步骤更少,相对普通操作更简洁。
而且位带操作属于原子操作,在多任务系统中,位带操作可以解决共享资源中的紊乱危象,关于该部分内容可以参看《Cortex-M3权威指南》。
总的来说,位带的主要优点是数据的一个单独位可以通过一条指令来读或者写,而不需要操作一些利的寄存器。例如,一条从位带别名区域地址进行读操作的LDR指令会将值0或者1加1载入寄存器。类似的,一条STR指令在向位带别名区的地址写入时,只是修改主区域中数据的一位。当然修改需要由硬件来执行读写操作,但是只有一条指令(STR)被取指并执行。
来源:嵌入式实验楼
https://www.toutiao.com/article/7189963408018866700/