最近学习stm32位的位带操作,有很多地方不理解,才发现自己对内存的理解不够,搜集资料后,得出以下浅显理解。
基础知识
进制
计算机以二进制代码储存信息,每个二进制数表示一位 (bit),每8个二进制数表示一个字节 (Byte) , 而再往上的KB,就是210倍的字节,总结有以下进制关系。
1 Byte = 8 bit
1 KB = 1024Byte(210=1024)
1 MB = 1024KB
1 GB = 1024MB
1 GB = 230Byte
内存地址
内存地址使用16进制数表示,内存地址只是一个编号表示,一个内存空间,计算机以字节存储数据,所以一个内存地址对应的应该是一个字节(8 bit)的大小,这个之后会详细解释。
这里用32位机的内存做一个图例。32位机的内存地址用8位16进制数表示。
0x00000000----->[8bit];
0x00000001----->[8bit];
0x00000002----->[8bit];
0x00000003----->[8bit];
32位机
32位表示CPU一次可以处理32位数据or一次可以寻址32位,就是4个字节的数据。也就是CPU一次可以读取的数据可以有 232种不同的数,从0000 0000 到1111 1111,这一共232个不同的数。这 232个数就是232个地址,每个地址对应8个实际的二进制位,也就是一个地址管理一个字节的数据。
这也就是CPU寻址能力的来由,32位机的最大寻址能力就是 232Byte = 4GB;
对于32位机,高于4GB的内存将无法一次性寻得,所以32位机理论最大内存是 4GB。
内存的理解
下面的图将说明计算机的内存地址,实际存储样式和人所看到的表现。
现在我们向内存中写入“A” “B”"C"看看实际的存储是怎样的关系。
注:这里的数据是编造的。
第一行的ABC是用户输入的数据,第二行是计算机实际储存的数据
而第三行就是内存地址,可见一个内存地址对应一个字节,而计算机就靠这个地址来找到储存的信息。
对STM32的理解
寄存器地址
首先我们看一下STM32的参考手册
可以看到,这里的内存地址使用8位16进制储存的。一个地址对应一个字节的二进制数。
再以GPIO的寄存器为例
每个寄存器首地址以4个字节递增,每个寄存器占4个字节,对于BSRR寄存器可以画出这样的图
由此我们可以理解:
每个GPIO端口有16个IO口,
而每个 GPIO端口 有7个寄存器对这16个 IO 口进行各种设置,如:输入,输出,模式,速度等等,
每个寄存器占4个字节(Byte),32位(bit)。所以用这个32位就可以对16个 IO 口进行设置。一般这32位有以下集中分配方式:
一·每两位对应一个 IO 口,如 CRL,CRH
二·每一位对应一个IO,但只使用低16位进行设置,高16位保留,如 IDR, ODR, BRR, LCKR
三·每一位对应一个IO,低16位对应一种操作,高十六位对应另一种操作,如 BSRR
位带操作
位带操作是对STM32寄存器的位进行直接操作,有如51单片机中的“sbit”关键字。且只有GPIO 和 SRAW 中的低 1Mb 空间可以被位带操作。
其中寄存器的每一位数据在别名膨胀成4个字节,但只有最后一位有效。如此膨胀的原因是stm32为32位机,一次处理32位数据效率最高。
由以上的分析,位带别名区的转化公式也就很好理解了。
计算公式
如GPIO的计算公式:
AliasAddr = 0x42000000 + (A-0x40000000)*8*4+n*4
首先要明白,这个公式是计算字节的也就是内存地址
A为寄存器地址, 0x40000000 外设基地址是,A-0x40000000 就可以算出寄存器相对外设基地址的偏移了多少个字节(地址),又一个字节有8个位,每个位又膨胀为4个字节,所以先乘8再乘4。
算完寄存器的偏移就要计算位的偏移了,前面算出的其实是每个寄存器0位的偏移量,而 n 表示位号,每位膨胀为4个字节,所以乘4。
最后加上GPIO对应别名区的首地址 0x42000000 ,就可以算出寄存器某位对应别名区的地址了。
同理,SRAM的计算公式只是把对应别名区首地址修改
AliasAddr = 0x22000000 + (A-0x40000000)*8*4+n*4
统一公式
( (addr & 0xF0000000) +0x02000000+ ( (addr & 0x00FFFFFF)<<5 ) + bitnum<<2 )
addr 是要操作的为所在寄存器的地址
bitnum 位号,在寄存器的第几位。
公式解释:
addr & 0xF0000000 留下最高位的数字,再加上0x02000000 就可以得到二者分别公式中的第一项。
addr & 0x00FFFFFF 去除最高的两位,算出寄存器地址和基地址的偏移, <<5 左移5位即为乘32。
bitnum<<2 左移2位即为乘4。
开始编程
使用位带操作,完成按下按键LED点亮和熄灭的操作。电路图如下。
首先,检测按键,要用到GPIO的输入功能,先配置端口输入模式,再读取寄存器 IDR 就可以知道IO端口电平变化。
点亮led灯必须先配置IO输出模式,再对ODR或BRR,BSRR操作即可置位和清零。
第一,制作一个计算器,可以帮对上面位带操作的公式计算
#define SetBit(addr,n) *(unsigned int*)\
((addr & 0xF0000000) +0x02000000+ \
( (addr & 0x00FFFFFF)<<5 ) + (n<<2))
使用一个带参宏就可以完成这个工作。传入 addr 为寄存器的地址, n 为位在寄存器中的位置。
为了检测代码可行性,先尝试对IDR寄存器的读取,以便判断按键是否按下,和对 ODR 的置位,以便点亮LED灯。而对GPIO输入输出的配置沿用之前固件库编程的代码。
一下为检测按键按下的代码
#define GPIOA_IDR GPIOA_BASE + 0x08
#define SetBit(addr,n) *(unsigned int*)((addr & 0xF0000000) +0x02000000+ ( (addr & 0x00FFFFFF)<<5 ) + (n<<2))
int main(void)
{
while(1)
{
if(SetBit(GPIOA_IDR,0) == 1)
{
while(SetBit(GPIOA_IDR,0) == 1);
LED_G_TOGGLE;
}
}
}
GPIOA_BASE
是固件库中GPIOA 的基地址,0x08 是参考手册中的地址偏移,二者相加就是 GPIOA 的 IDR 寄存器的基地址。
if语句检测按下,while语句检测松手。要注意的是,要读取的位是 0 位,这点要注意。最后用一个宏定义 LED_G_TOGGLE
完成led灯的反转。
LED_G_TOGGLE的宏定义如下:
#define GPIOB_ODR GPIOB_BASE + 0x0C
#define SetBit(addr,n) *(unsigned int*)((addr & 0xF0000000) +0x02000000+ ( (addr & 0x00FFFFFF)<<5 ) + (n<<2))
#define LED_G_TOGGLE {SetBit(GPIOB_ODR,0) ^= 1;}
使用异或运算完成对这一位的反转。
完整的代码如下:
#define GPIOA_IDR GPIOA_BASE + 0x08
#define GPIOB_ODR GPIOB_BASE + 0x0C
#define SetBit(addr,n) *(unsigned int*)((addr & 0xF0000000) +0x02000000+ ( (addr & 0x00FFFFFF)<<5 ) + (n<<2))
#define LED_G_TOGGLE {SetBit(GPIOB_ODR,0) ^= 1;}
int main(void)
{
KEY_GPIO_Config();//配置输入端口为浮空,50M,Pin0
LED_GPIO_Config();//配置输出端口为推挽,50M,Pin0
while(1)
{
if(SetBit(GPIOA_IDR,0) == KEY_ON)
{
while(SetBit(GPIOA_IDR,0) ==KEY_ON);
LED_G_TOGGLE;
}
}
}
其中使用固件库配置端口输入输出模式的
KEY_GPIO_Config()
和LED_GPIO_Config()
代码这里不再展示。
遗留问题
上面的程序只是使用位带操完成了对 IDR 寄存器的读取和对 ODR 寄存器的写入。而对GPIO的初始化是使用固件库函数完成的。对应的位带操作要配置 CRL 寄存器,但在配置过程中出现了一系列问题。
第一,删去两句固件库函数写成的初始化函数 KEY_GPIO_Config() 和 LED_GPIO_Config() 后。位带操作不再起效果。通过调试可以看见,使用 SetBit(addr,n) 并未完成对应位的置位。
第二,留下两句初始化函数。在初始化函数之前的位带操作语句没有效果,而初始化函数后面的位带操作有效果。
一句话总结,不用固件库初始化,位带操作就没效果。
百思不得其解。