在STM32单片机的学习中,有一个最大的特点就是你所编写的STM32的程序操作,基本上都是总线的操作,比如GPIOB->ODR = 0x0001(或者直接等于1),这样的操作必须要考虑整个寄存器的数据,但其实如果学过51单片机的人都知道,我们除了对于总线操作外,我们还是经常会对单独的位进行操作,比如
#include <reg52.h>
sbit led1 = P0^0;
led1 = 1 ;
我想学过51的人对于上面的代码再熟悉不过了,这就是我们对单独的一个位进行的操作,那难道在STM32里面,就没办法这样操作了吗,答案是否定的,这就要引出位带操作这个概念了。
首先什么是位带操作,按照个人的理解,就是把每个bit,膨胀(映射)到32位的存储器上去,这样开发者就可以对每一位的数据编写。
那为什么要引入位带操作呢,首先一点就是操作比较的简单,我们只要考虑一位的数据就可以了,不用考虑总线上所有的数据变化。
还有一点,也是很重要的一点(但可能有点难理解,要是我说的不清楚还请见谅)。就是使用位带操作的时候,在硬件层面是不允许中断打断的,这就是我们所说的“原子”特性,我们都知道在我们写的程序中会有很多很多的中断,这样也就会存在一个问题,那就是如果我们的变量还没有发生变化,就被中断打断,去执行了中断的程序,那么变量的值就会和我们想要的值有了出入,然后我们等代码再回到原程序运行的时候,这时候我们又会因为上次的赋值没有成功而导致重新的赋值,这样我们的变量的值就彻底和我们预料的不一样了。
(https://blog.csdn.net/Emmy_kanly/article/details/80865318/这张图片我是取自于这个博主,如果涉及版权,我会及时的删除这篇博客。)
从这幅图我们可以看到(左边是没有使用位带操作,右边使用了bit-band),我们可以很清楚的看到,在使用了位带操作的程序里面,在汇编的层面,是把赋值块作为了一个整体,中断只能从赋值前和赋值后进行,在赋值的过程中是没法进行的,这样就确保了整个程序的可靠性和稳定性。
那我们要怎么进行位带操作呢,首先我们要理解位带操作的原理。
首先位带操作不是所有地方都可以用,一般只能用在SARM区和外设寄存器区。SARM的位带区是从0X22000000开始的,而外设的位带区是从0X42000000开始的,由于前面所提到的位带区就是在我们的寄存器膨胀为32位,这样就可以一一的进行操作,根据这个原理,我们就可以写出公式,完成位带区与位带别名区的一个转化。
PERIPHERAL_Addr = 0x4200_0000 + (A - 0x4000_0000)32 + n4
SARM_Addr = 0x2200_0000 + (A - 0x4000_0000)32 + n4
其中A 为寄存器的地址,n代码位号。
*这里稍微的说明一下为什么位带别名区可以膨胀为位带区的32位,这是因为在位带区的一位,相当于是位带别名区的4字节。
再根据上面的两算式,进行一下统一,就可以得到一个统一的位带区与位带别名区的一个转化。
(addr & 0xF000_0000) + 0x2000_0000 + ((addr & 0x00FF_FFFF) << 5)+(bitnum << 2)
这个公式比较难理解的就是((addr & 0x00FF_FFFF) << 5)这块,首先我们看addr & 0x00FF_FFFF,这就是因为在前面(addr & 0xF000_0000) + 0x2000_0000 中,已经完成了前两位的操作,所以我们只看后面的6位,不看前面的2位,所以是&0x00FF_FFFF,之所以要再左移5位,是因为2^5 = 32 , 所以是对应了前面的*32。后面的左移两位也就是同理了。
下面我就以GPIO->ODR寄存器为例,来代码实现一下位带操作。
//bit_band.h
#ifndef BIT_BAND_H
#define BIT_BAND_H
#include "stm32f10x.h"
#define GPIOB_ODR_Addr GPIOB_BASE + 0X0C
#define LED_OUT(bitnum) (GPIOB_ODR_Addr & 0xF000_0000) + 0x2000_0000 + ((GPIOB_ODR_Addr & 0x00FF_FFFF) << 5)+(bitnum << 2)
void LED_OUTPUT();
#endif /*BIT_BAND_H*/
//bit_band.c
void LED_OUTPUT(void)
{
LED_OUT(0) = 0;
}
//main函数
int main(void)
{
LED_OUTPUT();
}
这样我们就可以用位带操作来点亮一个led灯,使我们的程序更加的可靠。