GPIO位带的操作:
首先先简单回顾一下,51单片机怎样点亮一盏LED灯呢?
假设51单片机的P3^1口外挂一个带上拉电阻的LED灯,那么我们直接通过一个关键字sbit进行位定义,如:
#include<reg52.h>
sbit P31 =P3^1; //位定义
int main(void)
{
P31 =0; //点亮led
P31 =1; //关闭led
}
就可实现led的点亮,但是我们的stm32并没有这样的关键字哦。也就是它不能单独的对一个bit进行读写,那么我们怎样做呐?
可以通过对位带别名区进行访问进而实现对某一个位进行读写。
什么是位带?
将一个单元的某一位,通过简单的地址变换算法,映射到一个地址的空间(简称位带别名区),然后对地址进行操作,进而达到对位操作的效果。
为辅助大家理解,特此制作一张图:
PS: 要想对左边某位进行读写,只需对右边映射后的地址进行操作即可(将RAM和外设中的每一个bit映射到一个独立的地址,对这个地址的32位读写操作实现对一个bit的操作,就像51单片机中的位寻址区域一样)。
在stm32中能实现位带功能的也仅仅有两个地方(简称位带区):
-
sram区最低1MB空间-0x20000000--0x200fffff
-
外设区最低1MB空间 -0x40000000--0x400fffff
图:来源Cortex-M3权威指南Cn P84
这里两个位带区的每一位都可以分别通过映射算法映射到某一地址空间(这里的映射关系可以通过一个数学公式表达,即:映射算法=公式):
仅有限范围可以实现位绑定:
sram区: 0x2000 0000 ~0x200f ffff---1M—2^20
公式:AliasAddr =0x22000000 + ((A-0x20000000)*8+n)*4
=0x22000000 + (A-0x20000000)*32 + n*4
片上外设:0x4000 0000 ~0x400f ffff---1M
公式:AliasAddr =0x42000000 + ((A-0x40000000)*8+n)*4
=0x42000000 + (A-0x40000000)*32 + n*4
其中:
A --位带区某个bit所在的地址
0x20000000 --sram区的位带区的起始地址
0x40000000 --外设区的位带区的起始地址
AliasAddr --映射后的位带别名区
0x22000000 --sram区的位带别名区的起始地址
0x42000000 --外设位带别名区的起始地址
n --代表在A地址的第几位(范围:0--7,一个地址单元只有8位)
公式解读:
①这里的A-0x20000000或A-0x40000000代表的意思是,该地址之前有多少个地址单位,又因为STM32的一个地址单元对应8个bit位(一个字节),所以然后乘以8转化为bit;
②因为位带区的1bit映射到位带别名区后就是4字节(32bit),同时位带别名区的一个地址单元也是1Byte,也就是需要4个Byte, 所以后面需要将第一步转换的bit乘以4
③位带别名区起始地址在①②两步偏移后,然后将我们所需要操作的位在此基础上进行偏移
举例:利用位带对GPIOA的PA3进行位设置或位清除 GPIOA->ODR |= (1<<3);或者 GPIOA->ODR &= ~ (1<<3);
①计算(A - 0x40000000) *8 将偏移地址转换为bit
GPIOA_BASE =APB2PERIPH_BASE + 0x0800 = PERIPH_BASE + 0x10000 + 0x0800 = 0x40000000 + 0x10000 + 0x0800 =0x40010800
A= GPIOA_BASE (基地址) + ODR偏移地址 = 0x40010800 + 0x0C = 0x4001080C
(A - 0x40000000) *8 = (0x4001080C - 0x40000000)*8
②然后将第一步bit膨胀到位带别名区,乘以4字节(位带区的1bit 变成了位带别名区的4Byte),并在位带别名区进行偏移
0x42000000 + 0x4001080C - 0x40000000)*8 *4
③那么由①②两步已经将要操作的 ODR寄存器的那个位所在的地址0x4001080C 映射到位带别名区了,这时候我们只需要把位带区该地址(0x4001080C)要操作的位ODR2(bit3)膨胀到位带别名区所在的地址:
(volitile unsigned long *)(0x42000000 +(A-0x40000000)*32 + 3*4)
④然后我们对其定义,就可以对该地址操作,不就是对ODR的bit3位进行操作了吗?从而实现了位操作
u32 *PA_out_3= (volitile unsigned long *)(0x42000000 +(A-0x40000000)*32 + 3*4);
*PA_out_3 = 0;//复位该位
*PA_out_3 =1;//置位该位
The range of value for n, we should pay special attention to this situation:
According to the header file stm32f10x.h, then we know that the selection of ODR offset address directly affects the value of n.
When we only operate on the lower six bits of the port output data register, its offset address is 0x0C, and then n ranges from 0 to 7.
When we only operate on the highest six bits of the port output data register, its offset address can be 0x0C or 0x0D:
If its offset address is 0x0C, and then n ranges from 0 to 15.
If its offset address if 0x0D, and then n ranges from 0 to 7.
图:来源Cortex-M3权威指南Cn P87
位带区的每一个位 1 bit 都与别名区的一个字 32 bit 相对应(由 4 个字节组成)。 位带区的一个bit位需4个字节0x2000 0000的第0位对应别名区地址0x2200 0000~0x2200 0003,第2位对0x22000004~0x2200 0007。这样対别名区进行的字操作, 都会映射到位带区的相应的位上, 从而实现对位带区的一 个位进行快速操作。
我们可以对输出数据寄存器进行操作,将ODR位置1,则该位对应的管脚就会输出高电平。
接下来就可以实现点灯操作:
<1>确定所要操作的寄存器的第几位,确定偏移地址和管脚基地址,最终明确 A 的值和所位于的位带区(sram/外设)
<2>确定 n 的范围
<3>由映射关系公式进行计算得到所操作的地址
<4>对要操作的地址进行访问即可
实验一:逐个位定义实现LED的点亮
实验代码:
(1)初始化
void LED_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE); //开时钟--APB2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // A8 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure);// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //GPIOD2 GPIO_Init(GPIOD, &GPIO_InitStructure); }
(2)main函数
#define GPIOA_ODR_Addr (GPIOA_BASE + 0x0C) //A的地址表示: GPIOA基地址 + 偏移地址 #define GPIOD_ODR_Addr (GPIOD_BASE+ 0x0C) //0x4001140C u32 *PA_out_8 =(u32*)(0x42000000 +(GPIOA_ODR_Addr-0x40000000)*32 + 8*4);//PA8 u32 *PC_out_2 =(u32*)(0x42000000 +(GPIOD_ODR_Addr-0x40000000)*32 + 2*4);//PD2 int main(void) { LED_init();//初始化LED *PA_out_8=1;// shut down led PA8 *PA_out_8=0;//open led
//是因为最终经映射所得到的地址,对其操作时,仅关心最低位(也就是最低位有效)
*PA_out_8=3;//shut down led 只关心最后一位 b11
*PA_out_8=2;//open led b10
*PC_out_2=1; //shut down led PD2
*PC_out_2=0; // open led
}
通过以上方法,虽然可以实现像51一样进行对某一位进行操作,但是当操作的位不仅在sram的位带区,还可能在外设的位带区,这时候此方法显得捉襟见肘了。那么我们有没有好的办法呢?
简洁的位带宏定义
通过简单的宏定义即可实现两个位带区通用的公式,也就不需要进行多个指针变量进行定义了。
实验二:
使用宏定义进行位操作点灯
实验代码:
(1)初始化
void LED_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE); //开时钟--APB2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // A8 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure);// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //GPIOD2 GPIO_Init(GPIOD, &GPIO_InitStructure); }
(2)main函数
#define BIT_ADDR(Addr,Bitnum) *((volatile unsigned long *)((Addr & 0xF0000000)+0x02000000+((Addr&0xfffff)<<5)+(Bitnum<<2))) //#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) //ÊäÈë int main(void) { LED_init(); PAout(8)=0; //点灯 PDout(2)=0; }