STM32F1xx官方资料:
《STM32中文参考手册V10》-第8章通用和复用功能IO(GPIO和AFIO)
《Cortex-M3权威指南(中文)》第5章 位带操作
硬件连接
假设跑马灯实验的硬件连接如上图所示,LED0连接PB5,LED1连接PE5。由于在LED的另一端是VCC3.3,所以当PB5或PE5为低电平的时候,LED灯会亮。此时GPIO应采取推挽输出的模式。
GPIO的相关配置寄存器
STM32的每组GPIO口包括7个寄存器。也就是说,每个寄存器可以控制一组GPIO的16个GPIO口。
这7个寄存器分别为:
- GPIOx_CRL:端口配置低寄存器(32位)
- GPIOx_CRH:端口配置高寄存器(32位)
- GPIOx_IDR:端口输入寄存器(32位)
- GPIOx_ODR:端口输出寄存器(32位)
- GPIOx_BSRR:端口位设置/清除寄存器(32位)
- GPIOx_BRR:端口位清除寄存器(16位)
- GPIOx_LCKR:端口配置锁存寄存器(32位)
端口配置低寄存器(GPIOx_CRL)
由于每个GPIO口需要4位来进行配置输入输出模式(2位配置MODE,2位配置CNF),这样的话每组16个GPIO口则需要64位,这也就表明需要两个32位寄存器。于是GPIOx_CRL用于配置GPIO0-GPIO7的输入输出模式。同理GPIOx_CRH则用于配置GPIO8-GPIO15的输入输出模式。
同时对于上面的这个表可以总结出端口位配置的信息:
需要注意的是,当MODE选择00,CNF为选择10时,代表着上拉/下拉输入模式。到底是上拉还是下拉呢?此时需要PxODR(端口输出寄存器)来确定,0为下拉输入,1为上拉输入。
端口输入寄存器(GPIOx_IDR)
IDR寄存器低16位,每个位控制该组GPIO口的一个IO口,对应的是该IO口的输入电平。在输入模式下,可以读取I/O端口的电平值;在输出模式下,也可以读取I/O端口的电平值(在开漏输出时,读取到的I/O端口的电平值,不一定就是输出的电平值)。
端口输出寄存器(GPIOx_ODR)
ODR寄存器的低16位,每个位控制该组GPIO口的一个IO口,对应的是该IO口的输出电平。在输出模式下,可以通过写寄存器的值,来达到某个IO口的电平输出;在输入模式下,还可以通过写值,来确定是上拉还是下拉输入模式。
端口位设置/清除寄存器(GPIOx_BSRR)
在GPIO的开漏输出模式或者推挽输出模式下,都可以直接给ODR寄存器赋值来进行某个IO口的电平输出;同时,也可以通过对BSRR进行赋值来达到对ODR寄存器的控制来进行对IO口的电平输出。其实,BSRR寄存器的底层也是对ODR寄存器的控制。
BSRR寄存器的低16位可以只对ODR赋1,高16位可以只对ODR赋0。这样的好处是,只可以对ODR的某些特定位产生影响,而不对其他的位产生影响。而且可以一次性对ODR的许多位同时进行控制。
端口位清除寄存器(GPIOx_BRR)
BRR寄存器的功能其实和BSRR寄存器的高16位的功能是一样的,通常情况下,使用BSRR寄存器的低16位来赋1,使用BRR寄存器来赋0。
GPIO的寄存器版本
1. 使能IO口时钟。配置寄存器RCC_APB2ENR;
2. 初始化IO口模式。配置寄存器GPIOx_CRH/CRL;
3. 操作IO口,输出高低电平。配置寄存器GPIOX_ODR或者BSRR/BRR。
具体的程序内容:
void LED_Init(void){ RCC->APB2ENR|=1<<3; RCC->APB2ENR|=1<<6; GPIOB->CRL&=0xFF0FFFFF; GPIOB->CRL|=0x00300000; GPIOB->ODR|=1<<5; GPIOE->CRL&=0xFF0FFFFF; GPIOE->CRL|=0x00300000; GPIOE->ODR|=1<<5; }
int main(void) { LED_Init(); delay_init(); while(1){ GPIOB->ODR|=1<<5; GPIOE->ODR|=1<<5; delay_ms(500); GPIOB->ODR&=~(1<<5); GPIOE->ODR&=~(1<<5); delay_ms(500); } }
GPIO的库函数版本
需引用的文件:stm32f10x_gpio.h、stm32f10x_rcc.h、misc.h
需定义的文件:led.h
GPIO库函数介绍
• 1个初始化函数:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
作用:初始化一个或者多个IO口(同一组)的工作方式和速度。该函数主要是操作GPIO_CRL(CRH)寄存器,在上拉或者下拉的时候有设置BSRR或者BRR寄存器 。
注意:外设(包括GPIO)在使用之前,几乎都要先使能对应的时钟。
• 2个读取输入电平函数:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
作用:读取某个(某组)GPIO的输出电平。实际操作的是GPIO_ODR寄存器。
• 4个设置输出电平函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal); void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
作用:设置某个IO口输出为高电平(低电平)。实际操作BSRR寄存器。后两个函数的作用类似。
GPIO库函数版本的跑马灯
1. 使能IO口时钟。调用函数RCC_APB2PeriphColckCmd();
2. 初始化IO口模式。调用函数GPIO_Init();
3. 操作IO口,输出高低电平。调用函数GPIO_SetBits();GPIO_ResetBits()。
具体的程序内容:
void LED_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE); GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_5); GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOE,&GPIO_InitStructure); GPIO_SetBits(GPIOE,GPIO_Pin_5); }
int main(void) { LED_Init(); delay_init(); while(1){ GPIO_SetBits(GPIOB,GPIO_Pin_5); GPIO_SetBits(GPIOE,GPIO_Pin_5); delay_ms(500); GPIO_ResetBits(GPIOB,GPIO_Pin_5); GPIO_ResetBits(GPIOE,GPIO_Pin_5); delay_ms(500); } }
GPIO的位操作版本
位操作的原理
把每位膨胀为一个32位的地址,当访问这些地址的时候就达到了访问该位的目的。比如说BSRR寄存器有32个位,那么可以映射到32个地址上,我们去访问(读-改-写)这32个地址就达到访问32个比特的目的。
也就是说,位操作就是可以读、写单独的一个比特位,由于在STM32中没有像51单片机的sbit来实行位定义,但是它可以通过位带别名区来实现。
哪些区域支持位操作:
SRAM 区的最低 1MB 范围,0x20000000 ‐ 0x200FFFFF(SRAM区中的最低 1MB);
片内外设区的最低 1MB范围,0x40000000 ‐ 0x400FFFFF(片上外设区中最低 1MB)。
位带区:支持位带操作的地址区
位带别名:对别名地址的访问最终作用到位带区的访问上(注意:这中间有一个地址映射过程)
这两个1MB的空间可以像普通RAM一样操作外(修改内容时用读、改、写),它们还有自己的位带别名区,位带别名区把这1MB的空间的每一位膨胀为一个32位的字。确切的说,这个字就是一个地址,当操作这个地址时,就可以达到操作这个位带区某个位的目的。
在位带区中,每个比特位都映射到别名地址区的一个地址,注意,这只是只有LSB有效的字(最低一位有效的字)。当一个别名地址被访问时,会把该地址转换为为位带操作。
映射方式
对片内外设位带区的某个比特位,记它的所在字节的地址为A,位序号为n(0<=n<=7),则该比特位在别名区的地址为:
AliasAddr = 0x42000000 + ((A - 0x40000000) * 8 + n) * 4 = 0x42000000 + (A - 0x40000000) * 32 + n * 4
一开始,我对n(0<=n<=7)很不理解,既然n表示位序号,为什么不是0<=n<=31呢?其实是我忽略了“所在字节”四个字,也就是说在位带区中,不是以一个寄存器一个寄存器为分隔单元,而是以一个字节一个字节来分隔单元的。
对于映射的公式,稍微解释一下:
1. A - 0x40000000 = 当前字节偏离外设基地址的偏移字节数 ;
2. 偏移字节数 * 8 = 偏移了多少位 ;
3. 因为位带区每一位对应位带别名区的一个地址(32位4字节),而地址是以字节计算的,所以位带别名区对应偏移量最后一个的地址 = 偏移了多少位 * 4 ;
4. n * 4 = 偏移量后面的n位对应位带别名区的地址。
同理,对于SRAM位带区的某个比特位,记它所在字节地址为A,位序号为n(0<=n<=7),则该比特位在别名区的地址为:
AliasAddr = 0x22000000 + ((A - 0x20000000) * 8 + n) * 4 = 0x22000000 + (A - 0x20000000) * 32 + n * 4
只是对位带基地址和位带别名区基地址做了改变即可。
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址 + 位序号”转换成别名地址:
//把位带区地址 + 位序号转换成位带别名去的宏 #define BITBAND(addr, bit_num) ((addr & 0xf0000000) + 0x02000000 + ((addr & 0x00ffffff) << 5) + (bit_num << 2))
位带操作的优越性
位带操作可以简化跳转的判断。比如之前需要跳转到某一位时,必须这样做:
- 读取整个寄存器;
- 掩蔽不需要的位;
- 比较并跳转。
现在使用位带操作只需要:
- 从位带别名区读取状态位;
- 比较并跳转。
在SYSTEM文件夹的sys.h文件中,对GPIO输入输出部分功能实现了位带操作。具体的实现过程如下面的程序所示:
#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 GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808 //IO口操作,只对单一的IO口! //确保n的值小于16! #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
通过PAout(n)实现对GPIOA_ODR寄存器的第n位赋值的功能,通过PAin(n)实现对GPIOA_IDR寄存器的第n位赋值的功能。按照这种写法,之前库函数版本的主程序可以改写成:
int main(void) { LED_Init(); delay_init(); while(1){ PBout(5)=1; PEout(5)=1; delay_ms(500); PBout(5)=0; PEout(5)=0; delay_ms(500); } }
本文转自:CSDN - Yngz_Miao,转载此文目的在于传递更多信息,版权归原作者所有。