一、位带操作
位带操作常用于I/O高度密集访问的芯片。
参考权威指南:Bit-band operation support allows a single load/store operation to access (read/write) to a single data bit. In the Cortex-M3 and Cortex-M4 processors, this is supported in two pre-defined memory regions(静态映射) called bit-band regions. One of them is
位带操作支持允许单个加载/存储操作访问(读/写)单个数据位。在Cortex-M3和Cortex-M4处理器中,这在两个预定义的内存区域中得到支持(静态映射) 称为位带区域
。
回想以前写51代码
P0 = 0x10; //将P0端口设置为0x10
P1_0=1; //将P1端口0号引脚设置为高电平
a = P2_2; //获取P2端口2号引脚的电平
根据上述的方法,我们可以发现快速定位修改某个引脚的电平还有获取引脚的状态
GPIO_SetBits、GPIO_ResetBits、GPIO_WriteBit操作IO口的性能没有达到极致,因为这些函数都需要进行现场保护和现场恢复的动作,比较耗时间,没有进行一步到位,使用位带操作则没有上述的烦恼,简单快速!
举例来说,可以利用其实现从通用目的输入/输出CGPIO)端口往串行设备的串行数据传输。 由于串行数据和时钟信号的访问是分开的,因此应用程序代码实现起来也非常简单。
示例1:
GPIO_SetBits(GPIOF,GPIO_Pin_9);
修改为
PFout(9)=1;
示例2:
GPIO_ResetBits(GPIOF,GPIO_Pin_9);
修改为
PFout(9)=0;
因为使用对引脚设置电平或读取电平,库函数效率是不高的,很难应付高性能的场合,如下代码,修改某引脚电平要执行起码3行代码,还不包括隐含的调用函数与函数返回的过程。
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRRL = GPIO_Pin;
}
对于这些8位处理器,可位寻址的数据具有特殊的数据类型,而且需要特殊的 指令来访问位数据。尽管Cortex-M3和Cortex-M4处理器中没有位操作的特殊指令,不过定义了特殊的存储器区域
后,对这些区域的数据访问会被自动转换为位段操作。
了解位段操作之前,首先得了解位带区
和别名区
。
二、位带区和别名区
1.定义
位带区
是存储器映射包括两个位段区域
。这些区域将存储器别名区域中的每个字映射 到存储器位段区域中的相应位。在别名区域写入字时,相当于对位段区域的目标位执行读-修改-写操作。其中一个位于SRAM
区域的第一个1MB,另一个则位于外设区域
的第一个1MB ,这两个区域可以同普通存储器一样访问,而且还可以通过名为位段别名的一块独立的存储器区域进行访问。
别名区
是按照一定的映射关系,将位带区的每一个bit 映射到位带别名区的每一个字 (不是字节,stm32中字的宽度为4字节)。
在实际使用过程中,操作位带别名区的字,就是操作位带区的bit。
There are two regions of memory for bit-band operations:(以下两个区域用于位带操作)
• 0x20000000~0x200FFFFF (SRAM, 1MB)
• 0x40000000~0x400FFFFF (Peripherals, 1MB)
2.映射表
我们可以看到表格从0x20000000bit[0]——》0x0x20000000bit[1],别名区地址从0x22000000bit[0]——》0x22000004bit[0],增加了4个字节,也就是32bit。
下面为一个简单的例子:
(1)将地址0x20000000设置为0x3355AACC。
(2)读地址0x22000008。本次读访问被重映射为到0x20000000的读访问,返回值为1。(0x3355AACC 的bit[2])。
(3)将0x22000008写为0。本次写访问被重映射为到地址0x20000000的读一修改写。数值0x3355AACC 被从存储器中读出来,清除第2位后,结果0x3355AAC8被写入地址0x20000000。
(4)现在读取0x20000000,这样会得到返回值0x3355AAC8( bit[2]被清除)。在访问位段别名地址时,只会用到数据的LSB(bi[0])。另外,对位段别名区域的访问不应该是非对齐的。若非对齐访问在位段别名地址区域内执行,结果是不可预测的。
三、位段操作优势
参考权威指南,位段操作还可简化跳转决断。例如,若跳转应该基于外设中某个状态寄存器的一位来执行,除了:
· 读取整个寄存器
· 屏蔽未使用的位
· 比较和跳转
还可以将操作简化为:
· 通过位段别名读取状态位(得到0或1)
· 比较和跳转
除了可以提高少数几个指令的位操作速度外,Cortex-M3 和Cortex-M4处理器的位段特性还可用于资源(如I/0端口的各引脚)被不止一个进程
共用的情形。位段操作最重要的一个优势或特点在于它的原子性。换句话说, 读—修改一写
的流程不能被其他总线
行为打断。若没有这种特性,在进行读一修改一写
的软件流程时,可能会出现下面的问题:假定输出端口的第0位被主程序使用而第1 位被中断处理使用,所示,基于读一修改一写操作的软件可能会引起数据冲突。利用位段特性,这种竞态现象是可以避免的,这是因为读修改写是在硬件等级执行的,是原子性的,而中断无法在操作时产生。多任务系统
中也有类似的问题。例如,若输出端口的第0位被进程A使用而第1 位被进程B使用,基于软件的读修改写可能会引起数据冲突。与前面的类似,位段特性可以确保每个任务的位访问是独立的,因此不会产生数据冲突。
可通过一个映射公式说明别名区域中的每个字与位段区域中各个位之间的对应关系。
四、映射公式
关于IO引脚对应的访问地址,可以参考以下公式
SRAM
:寄存器的位带别名
= 0x22000000 + (寄存器的地址-0x20000000)32 + 引脚编号4
外设区域
:寄存器的位带别名
= 0x42000000 + (寄存器的地址-0x40000000)32 + 引脚编号4
示例:
下例说明如何将 SRAM 地址 0x20000300 处字节的位 2 映射到别名区域:0x22006008 = 0x22000000 + (0x30032) + (24)
对地址 0x22006008 执行写操作相当于在 SRAM 地址 0x20000300 处字节的位 2 执行读-修 改-写操作。
对地址 0x22006008 执行读操作将返回 SRAM 地址 0x20000300 处字节的位 2 的值(0x01 表示位置位,0x00 表示位复位)。
五、寄存器地址与别名地址转换技巧
1.确定某端口访问起始地址,如端口F访问起始地址为GPIOF_BASE
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint16_t BSRRL; /*!< GPIO port bit set/reset low register, Address offset: 0x18 */
__IO uint16_t BSRRH; /*!< GPIO port bit set/reset high register, Address offset: 0x1A */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
2.根据要访问的寄存器地址计算偏移值,如计算
GPIOF的ODR寄存器地址 = GPIOF_BASE+0x14;
3.根据以下公式进行换算
寄存器的位带别名地址 = 0x42000000 + (寄存器的地址-0x40000000)32 + 引脚编号4
详细示意图参考如下:
4.设置PF9引脚电平代码如下
uint32_t *PF9_BitBand = (uint32_t *)(0x42000000 + (GPIOF_BASE + 0x14 - 0x40000000)*32 + 9*4);
更优解的方法:
uint32_t *PF9_BitBand = (uint32_t *)(0x42000000 + ((uint32_t)&GPIOF->ODR - 0x40000000)*32 + 9*4);
将端口的访问封装为Pxout、Pxin,例如端口F引脚电平设置PFout,端口A引脚电平读取PAin。
六、代码调整
#define PFout(n) *(volatile uint32_t *)(0x42000000 + (GPIOF_BASE + 0x14 - 0x40000000)*32 + n*4)
#define PAin(n) *(volatile uint32_t *)(0x42000000 + (GPIOA_BASE + 0x10 - 0x40000000)*32 + n*4)
七、编译优化
优化:编译器想尽办法去压缩程序存储空间,提高运行速度。
一般编译器,优化有多个等级:-O0、-O1、-O2、-O3。
-O0:缺省优化级别,不压缩程序存储空间,不提高程序运行速度,保证程序的可靠执行。
-O1:轻度优化,轻度压缩程序存储空间,轻度优化程序运行速度。
-O2:推荐优化等级,在程序存储空间和程序运行速度取得平衡点。
-O3:最高级别的优化等级,有可能导致程序不能运行,也会使用以空间换时间的方法,导致程序体积增大。
在编译器中也可以设计代码优化程度
示例1:-O0
示例1:-O2
按键例子1,任何时刻按下按键,灯无法响应:
#define PAin(n) *((uint32_t *)(0x42000000 + (((uint32_t)&GPIOA->IDR) - 0x40000000)*32 + (n)*4))
PFout(9) = PAin(0);
经过编译阶段,会得到恒定的结果。
PFout(9)=1;
按键例子2,任何时刻按下按键,灯能够立即响应点亮或熄灭:
#define PAin(n) *((volatile uint32_t *)(0x42000000 + (((uint32_t)&GPIOA->IDR) - 0x40000000)*32 + (n)*4))
PFout(9) = PAin(0);
编译器不会去优化*((volatile uint32_t *)(0x42000000 + (((uint32_t)&GPIOA->IDR) - 0x40000000)*32 + (n)4))变为恒定的值;
而是每次都是小心翼翼地取执行((volatile uint32_t *)(0x42000000 + (((uint32_t)&GPIOA->IDR) - 0x40000000)*32 + (n)*4)),读取该地址上的值。
PFout(9) = PAin(0);
八、volatile关键字
目的为了防止
编译器优化
需要注意的是,在使用位段特性时,可能需要将被访问的变量定义为volatile 。C编译器不知道同个数据会以两个不同的地址访问,因此需要利用volatile属性,以确保在每次访问变量时,操作的是存储器位置而不是处理器内的本地备份。
1.应用场景
volatile
关键字分析,往往应用在三种场合
(1)多线程编程共享全局变量的时候,该全局变量要加上volatile进行修饰,让编译器不要省略该变量的访问。
(2)裸机编程的时候,某函数与中断服务函数共享全局变量的时候,该全局变量要加上volatile进行修饰,让编译器不要省略该变量的访问。
(3)ARM定义寄存器的时候,寄存器是指向一个地址,要加上volatile进行修饰,让编译器不要优化而省略该变量的访问。
编译器不要优化该变量指的是防止编译器出现优化过度,导致代码运行失效。
加上volatile关键字生成的汇编代码会发生明显的变化,同样调用delay函数,灯的速度发生变化!
2.delay函数在-O2等级,是否添加volatile关键字,反汇编分析。
不添加volatile关键字
添加volatile关键字
跑马灯例程:
/******************************************sys.h********************************/
注意:代码不完整,可以自行去下载正点原子stm32f407的例程看看
#define SYSTEM_SUPPORT_OS 0
#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+20) //0x40020014
#define GPIOB_ODR_Addr (GPIOB_BASE+20) //0x40020414
#define GPIOC_ODR_Addr (GPIOC_BASE+20) //0x40020814
#define GPIOD_ODR_Addr (GPIOD_BASE+20) //0x40020C14
#define GPIOE_ODR_Addr (GPIOE_BASE+20) //0x40021014
#define GPIOF_ODR_Addr (GPIOF_BASE+20) //0x40021414
#define GPIOG_ODR_Addr (GPIOG_BASE+20) //0x40021814
#define GPIOH_ODR_Addr (GPIOH_BASE+20) //0x40021C14
#define GPIOI_ODR_Addr (GPIOI_BASE+20) //0x40022014
#define GPIOA_IDR_Addr (GPIOA_BASE+16) //0x40020010
#define GPIOB_IDR_Addr (GPIOB_BASE+16) //0x40020410
#define GPIOC_IDR_Addr (GPIOC_BASE+16) //0x40020810
#define GPIOD_IDR_Addr (GPIOD_BASE+16) //0x40020C10
#define GPIOE_IDR_Addr (GPIOE_BASE+16) //0x40021010
#define GPIOF_IDR_Addr (GPIOF_BASE+16) //0x40021410
#define GPIOG_IDR_Addr (GPIOG_BASE+16) //0x40021810
#define GPIOH_IDR_Addr (GPIOH_BASE+16) //0x40021C10
#define GPIOI_IDR_Addr (GPIOI_BASE+16) //0x40022010
#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)
void WFI_SET(void);
void INTX_DISABLE(void);
void INTX_ENABLE(void);
void MSR_MSP(u32 addr);
#endif
/**********************************************sys.c********************************/
__asm void WFI_SET(void)
{
WFI;
}
__asm void INTX_DISABLE(void)
{
CPSID I
BX LR
}
__asm void INTX_ENABLE(void)
{
CPSIE I
BX LR
}
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
/**********************************************main.c********************************/
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与LED连接的硬件接口
while(1)
{
PAout(8)=1; //LED0输出低
PDout(2)=0;//LED1输出高
delay_ms(500);
PAout(8)=0;//LED0输出高
PDout(2)=1;//LED1输出低
delay_ms(500);
}
}
总结
位段操作并不局限于字传输,字节传输或半字传输也可以执行。例如,在用字节访问指令C LDRB/STRB)访问位段别名地址区域时,所产生的对位段区域的访问就是字节大小的。类似地,对位段别名的半字传输CLDRH/STRH)则会被重映射到对位段区域的半字大小的传输。在位段别名地址上执行非字传输时,地址值仍然应该是字对齐的。继续学习!!