注:以下均属个人片面之理解,有误之处请留言,我很愿意将这篇文章完善的更好
位待操作及地址
STM32地址
基地址、总线地址、寄存器地址是什么
基地址以及偏移地址
这是stm32的系统结构图,只需要看一下红线框出来的地方即可
从下图可以看到,总线分为 4 大块,每块都有一个起始地址,这个起始地址就是基地址,然后到下一块起始地址的时候就会和前一块地址出现偏差,这个差值就是偏移量,即相对基地址的偏移量。
寄存器地址
拿GPIOC口控制寄存器举例子,由上面的图可知GPIOC的基地址是0x4001 1000
我们IO口的7个控制寄存器的地址如下图所示
他们之间有什么样的联系(重点)
每个寄存器是32位相对的偏移为0x04,这是因为stm32是字节编址
即对于内存的最小操作也是字节操作
即对于内存的最小操作也是字节操作
即对于内存的最小操作也是字节操作
STM32位带操作
What 位带操作是什么
如果你用过51单片机你就会知道在51单片机中我们可以直接操作单个IO口来控制外设
比如我们可以通过改变固定的IO口的电平来控制单个led的亮灭
这里我先附一张led的原理图以便后面理解
这个是51单片机led模块的原理图,可以看到模块对应的IO口是P2口,那么我们可以通过整个P2口进行操作来进行控制led的亮灭(通过控制P2口对应的寄存器)
P2=0x01; //0000 0001
我们这么写对应的就是P2^1口对应的led点亮
但是我们还有另外一种改变P2^1口电平的方法,那就是位待操作
sbit led=P2^1; //将对应的IO口定义为一个变量
led=1; //对这个变量进行操作就是对P2^1口进行操作
看到这里我相信大多数同学应该懂了什么是位带操作,其实简而言之就是单独的对一个IO口进行操作
Why 为什么要使用位带操作
如果想知道IO口具体的寄存器控制请看链接: link.
我们在编程时会遇到一些IO口相互影响的问题
例如链接: link.
虽然有些时候可以通过或与操作来解决,但是也难免会有一些解决不了的时候,所以我们这个时候就可以通过位待操作来操作IO口对应的寄存器的每个位。
How 位带操作是如何实现的
看完上面的地址联系应该可以知道为什么stm32不能直接通过地址来进行位待操作
那我们要怎么实现位带操作呢
首先并不是所有的内核都支持位带操作的,在M3的内核中,有两个区支持位带。
第一个是 SRAM 区的最低 1MB 范围
SRAM区地址为 0x2000 0000 ~ 0x200F FFFF 映射地址
(位带区地址)在SRAM地址的基地址下加0x0200 0000
第二个则是片内外设区的最低 1MB范围
0x4000 0000 ~ 0x400F FFFF
(位带区地址)在片内外设区地址的基地址下加0x0200 0000
位待操作可以理解成将地址中的每个字节(内存区)都映射(膨胀)成一个32位的字(映射区)
那么我们通过改变映射区的32位的字即可改变内存区中这个字节里面的位
这么说大家可能不太理解,没关系直接上图,这里拿SRAM区举个例子
拿0x2000 0000这个字节来举例子
从0x2000 0000到0x2000 0001是一个字节8个比特
我们映射(膨胀)到位待区是
从0x2200 0000到0x2200 0100这里是32个字节 32*8个比特
显而易见内存大小扩大了32倍
即内存地址的每个位都对应映射区的4个字节
从0x2000 0000开始 后面的两个字节转化为二进制
位数 | 位待区地址 | 后两位转换成二进制 |
---|---|---|
第一个位 | 0x2200 0000 | 0000 0000 |
第二个位 | 0x2200 0004 | 0000 0100 |
第三个位 | 0x2200 0008 | 0000 1000 |
第四个位 | 0x2200 000C | 0000 1100 |
第五个位 | 0x2200 0010 | 0001 0000 |
第六个位 | 0x2200 0014 | 0001 0100 |
第七个位 | 0x2200 0018 | 0001 1000 |
第八个位 | 0x2200 001C | 0001 1100 |
可以看出每次变化为4个字节 4*8 == 32个字节
所以我们只需操作位待区的32位内存地址即可改变原内存中的位
代码
代码解读
这里直接拿GPIOB.5 LED0 进行举例
#define PERIPH_BASE 0x40000000UL
//宏定义基地址为 0x4000 0000
#define APB2PERIPH_BASE
(PERIPH_BASE + 0x00010000UL)
//宏定义APB2PERIPH_BASE (APB2的基地址)为总线基地址加偏移地址
#define GPIOB_BASE (APB2PERIPH_BASE + 0x00000C00UL)
//宏定义GPIOB的基地址为APB2总线的基地址加相对APB2总线的偏移地址
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
//宏定义GPIOB->ODR寄存器的基地址为GPIOB的基地址加ODR寄存器相对GPIOB基地址的偏移地址
至此为止是用来找到GPIOB->ODR寄存器的基地址
如果有不明白的可以看上面地址关系
基地址映射
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
//将地址映射到位待区
此处addr为寄存器地址
(addr & 0xF0000000) //保留基地址
0x4001 0C0C& 0xF000 0000 = 0xF000 0000
+0x200 0000 = 0xF200 0000 (基地址的映射完成)
偏移地址映射
+((addr &0xFFFFF)<<5)
0x4001 0C0C& 0xF FFFF= 0x0001 0C0C //将偏移地址放大32倍变成了位待区中的偏移地址
再加上基地址的映射地址就是
0xF200 0000 + 0x0001 0C0C = 0xF201 0C0C(寄存器首地址对应的位待区地址)
这时候我们可以展开后面两位
0C ---- 0000 1100
寄存器的位数 | 16进制 | 二进制 |
---|---|---|
0 | 0C | 0000 1100 |
1 | 10 | 0001 0000 |
2 | 14 | 0001 0100 |
3 | 18 | 0001 1000 |
4 | 1C | 0001 1100 |
5 | 20 | 0010 0000 |
(1)第一种理解(二进制)
通过二进制我们可以发现
寄存器的位数 | 位待区中对应的地址(二进制) |
---|---|
0 | 寄存器首地址对应的位待区地址 |
1 | 寄存器首地址对应的位待区地址+0100 |
2 | 寄存器首地址对应的位待区地址+1000 |
3 | 寄存器首地址对应的位待区地址+1100 |
4 | 寄存器首地址对应的位待区地址+0001 0000 |
5 | 寄存器首地址对应的位待区地址+0001 0100 |
可能到这里你还没有发现什么
可以把上面加的地址右移两位试一下
0000 0001 -->1
0000 0010 -->2
0000 0011 -->3
0000 0100 -->4
0000 0101 -->5
所以我们只需要将需要操作的IO口对应的数字左移两位然后再加上寄存器地址对应的位待区地址即可
即**(bitnum<<2)**
(2)第二种理解(十六进制)
(bitnum<<2) 将IO对应的数字左移两位即放大2^2=4倍
所以 0xF201 0C0C(寄存器地址对应的位待区地址)+ bitnum<<2
寄存器的位数 | 移位后 | 对应的位待区地址 |
---|---|---|
0 | 0 | 0xF201 0C0C |
1 | 0x04 | 0xF201 0C0C + 0x04 = 0xF201 0C10 |
2 | 0x08 | 0xF201 0C0C +0x08 =0xF201 0C14 |
3 | 0x0C | 0xF201 0C0C +0x0C =0xF201 0C18 |
4 | 0x10 | 0xF201 0C0C + 0x10 =0xF201 0C1C |
5 | 0x14 | 0xF201 0C0C + 0x14 =0xF201 0C20 |
例程
#ifndef __BIT_H
#define __BIT_H
#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))
#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
#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) //输
#endif