【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)

STM32F1xx官方资料:

《STM32中文参考手册V10》-第8章通用和复用功能IO(GPIO和AFIO)
《Cortex-M3权威指南(中文)》第5章 位带操作

 

硬件连接

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第1张图片

假设跑马灯实验的硬件连接如上图所示,LED0连接PB5,LED1连接PE5。由于在LED的另一端是VCC3.3,所以当PB5或PE5为低电平的时候,LED灯会亮。此时GPIO应采取推挽输出的模式

 

GPIO的相关配置寄存器

STM32的每组GPIO口包括7个寄存器。也就是说,每个寄存器可以控制一组GPIO的16个GPIO口。这7个寄存器分别为:

  1. GPIOx_CRL:端口配置低寄存器(32位)
  2. GPIOx_CRH:端口配置高寄存器(32位)
  3. GPIOx_IDR:端口输入寄存器(32位)
  4. GPIOx_ODR:端口输出寄存器(32位)
  5. GPIOx_BSRR:端口位设置/清除寄存器(32位)
  6. GPIOx_BRR:端口位清除寄存器(16位)
  7. GPIOx_LCKR:端口配置锁存寄存器(32位)

端口配置低寄存器(GPIOx_CRL)

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第2张图片

由于每个GPIO口需要4位来进行配置输入输出模式(2位配置MODE,2位配置CNF),这样的话每组16个GPIO口则需要64位,这也就表明需要两个32位寄存器。于是GPIOx_CRL用于配置GPIO0-GPIO7的输入输出模式。同理GPIOx_CRH则用于配置GPIO8-GPIO15的输入输出模式。

同时对于上面的这个表可以总结出端口位配置的信息:

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第3张图片

需要注意的是,当MODE选择00,CNF为选择10时,代表着上拉/下拉输入模式。到底是上拉还是下拉呢?此时需要PxODR(端口输出寄存器)来确定,0为下拉输入,1为上拉输入。

端口输入寄存器(GPIOx_IDR)

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第4张图片

IDR寄存器低16位,每个位控制该组GPIO口的一个IO口,对应的是该IO口的输入电平。在输入模式下,可以读取I/O端口的电平值;在输出模式下,也可以读取I/O端口的电平值(在开漏输出时,读取到的I/O端口的电平值,不一定就是输出的电平值)

端口输出寄存器(GPIOx_ODR)

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第5张图片

ODR寄存器的低16位,每个位控制该组GPIO口的一个IO口,对应的是该IO口的输出电平。在输出模式下,可以通过写寄存器的值,来达到某个IO口的电平输出;在输入模式下,还可以通过写值,来确定是上拉还是下拉输入模式。

端口位设置/清除寄存器(GPIOx_BSRR)

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第6张图片

在GPIO的开漏输出模式或者推挽输出模式下,都可以直接给ODR寄存器赋值来进行某个IO口的电平输出;同时,也可以通过对BSRR进行赋值来达到对ODR寄存器的控制来进行对IO口的电平输出。其实,BSRR寄存器的底层也是对ODR寄存器的控制

BSRR寄存器的低16位可以只对ODR赋1,高16位可以只对ODR赋0。这样的好处是,只可以对ODR的某些特定位产生影响,而不对其他的位产生影响。而且可以一次性对ODR的许多位同时进行控制。

端口位清除寄存器(GPIOx_BRR)

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第7张图片

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的输入电平。实际操作的是GPIOx_IDR寄存器。

  • 2个读取输出电平函数:
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(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来实行位定义,但是它可以通过位带别名区来实现。 

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第8张图片

哪些区域支持位操作:

  • 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

上式中,4表示一个字4个字节,8表示一个字节8个比特。

一开始,我对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))

位带操作的优越性

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第9张图片

位带操作可以简化跳转的判断。比如之前需要跳转到某一位时,必须这样做:

  1. 读取整个寄存器;
  2. 掩蔽不需要的位;
  3. 比较并跳转。

现在使用位带操作只需要:

  1. 从位带别名区读取状态位;
  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);
		
	}
 }

【STM32】GPIO的相关配置寄存器、库函数、位操作(实例:STM32控制跑马灯)_第10张图片

 

你可能感兴趣的:(《嵌入式》STM32开发笔记)