【STM32】入门(三):按键使用-GPIO端口输出控制

【STM32】STM32单片机总目录

1、简述

在“【STM32】入门(二):跑马灯-GPIO端口输出控制”中,我们是从代码入手,然后分析的手册及原理。本节将会从原理图入手,查询手册,然后分析代码。体验下实际的开发流程。

2、原理图

原理图如下,实际的按键只有KEY0、KEY1、KEY2以及一个复位按键。

2.1 普通按键

实际中没有KEY3,忽略它。这几个普通按键没有去抖电路,因此,需要在代码中用软件去抖。
按键去抖电路(硬件去抖)可以参见下面下复位按键电路。
【STM32】入门(三):按键使用-GPIO端口输出控制_第1张图片

KEY0、KEY1、KEY2 分别连接在引脚PF6、PF7、PF8上
在这里插入图片描述

2.2 复位按键

复位按键并联一个电容,实现了硬件去抖动设计,如下图所示:
【STM32】入门(三):按键使用-GPIO端口输出控制_第2张图片

复位按键连接在NRST引脚上:
在这里插入图片描述

3、数据手册

GPIOF寄存器映射如下:
【STM32】入门(三):按键使用-GPIO端口输出控制_第3张图片
外部复位引脚NRST为低电平时,产生系统复位。在手册“复位和时钟控制(RCC)”一章可以看到相关说明。

4、代码分析

4.1 源码如下

KEY0:按下时执行动作;
KEY1:按下抬起后执行动作;
KEY2:长按1秒后执行动作。

int main(void)
{ 
	delay_init();     //延时函数初始化
	LED_Init();		  //LED初始化
	KEY_Init();       //按键初始化

	while(1)
	{
		key_scan(0);	
		#a)KEY0:按下时执行动作
		if(keydown_data==KEY0_DATA)
		{
		  	LED0=0;
			LED1=1;
			LED2=1;
		}
		#b)KEY1:按下抬起后执行动作;
		if(keyup_data==KEY1_DATA)
		{
			LED0=1;
			LED1=0;
			LED2=1;
		}

		#c)KEY2:长按1秒后执行动作,由于延时5ms扫描一次按键,所以5ms*200=1S
		if(key_tem==KEY2_DATA && key_time>200) 
		{
			LED0=1;
			LED1=1;
			LED2=0;
		}

    	delay_ms(5);
	}
}

相关变量、函数定义在另一个文件中key.h中

#define KEY0 		PFin(6)   
#define KEY1 		PFin(7)		
#define KEY2 		PFin(8)		
#define KEY3 	  PFin(9)		


//按键值定义
#define KEY0_DATA	  1
#define KEY1_DATA	  2
#define KEY2_DATA	  3
#define KEY3_DATA   4

//变量声明
extern u8   keydown_data;    //按键按下后就返回的值
extern u8   keyup_data;      //按键抬起返回值
extern u16  key_time;
extern u8   key_tem; 

//函数声明
void KEY_Init(void);	      //IO初始化
void key_scan(u8 mode);  		//按键扫描函数	

4.2 delay_init

延时函数初始化,在跑马灯程序中也有,但是没有进一步分析。这里详细分析下,延时相关函数。
延时使用Systick定时器也叫滴答定时器,是内核级别的24位倒计数简单定时器,常用做延迟和系统心跳时钟。

定时器的操作步骤:

  • 设置 SysTick 定时器的时钟源
  • 设置 SysTick 定时器的重装初始值(如果要使用中断的话,就将中断 使能打开)
  • 清零 SysTick 定时器当前计数器的值。
  • 打开 SysTick 定时器

delay_init初始化主要是设置 SysTick 定时器的时钟源

void delay_init()
{
	#a)设置 SysTick 定时器的时钟源:采用AHB总线时钟的八分频
 	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
	#b)计算SYSCLK的8分频时,经过1us的计数次数
	fac_us=SYSCLK/8;	 
	fac_ms=(u16)fac_us*1000; //每个ms需要的systick时钟数   
}

# c)实际配置函数很简单,最终执行:SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
  if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
  {
    SysTick->CTRL |= SysTick_CLKSource_HCLK;
  }
  else
  {
    SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
  }
}

手册中关于定时器时钟的描述:
【STM32】入门(三):按键使用-GPIO端口输出控制_第4张图片

4.3 delay_ms

实际延时函数为delay_xms,但是delay_xms最大只能延时798ms;
在超频到248M的时候,delay_xms最大只能延时541ms。
因此在delay_ms中循环调用delay_ms来实现0~65535ms的延时

void delay_ms(u16 nms)
{	 	 
	u8 repeat=nms/540;	//这里用540,是考虑到某些客户可能超频使用,
						          //比如超频到248M的时候,delay_xms最大只能延时541ms左右了
	u16 remain=nms%540;
	while(repeat)
	{
		delay_xms(540);
		repeat--;
	}
	if(remain)delay_xms(remain);
} 

SysTick->LOAD为24位寄存器,所以,最大延时为: nms<=0xffffff81000/SYSCLK,对168M条件下,nms<=798ms

void delay_xms(u16 nms)
{	 		  	  
	u32 midtime;
	#a)时间加载(SysTick->LOAD为24bit),设置 SysTick 定时器的重装初始值
	SysTick->LOAD=(u32)nms*fac_ms;
	#b)清零 SysTick 定时器
	SysTick->VAL =0x00;
	#c)打开 SysTick 定时器 
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; 
	do
	{
		midtime=SysTick->CTRL;
	}
	#d)等待时间到达
	while((midtime&0x01)&&!(midtime&(1<<16)));
	#e)关闭SysTick 定时器   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;       
	SysTick->VAL =0X00;	  	    
} 

相关手册:《STM32F3与F4系列Cortex M4内核编程手册》
【STM32】入门(三):按键使用-GPIO端口输出控制_第5张图片
【STM32】入门(三):按键使用-GPIO端口输出控制_第6张图片

4.4 KEY_Init

按键初始化和跑马灯中的初始化相似,不同的是将引脚模式设置为输入模式,输入引脚不需要设置推挽、开漏

void KEY_Init(void)
{
  	GPIO_InitTypeDef  GPIO_InitStructure;

  	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);    //使能GPIOF时钟
 
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9; //KEY0 KEY1 KEY2 KEY3对应引脚
  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;             //普通输入模式
  	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;       //100M
  	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;             //上拉
  	GPIO_Init(GPIOF, &GPIO_InitStructure);                   //初始化GPIOF6,7,8,9
} 

4.5 按键定义(位带别名)

#define KEY0 		PFin(6)   
#define KEY1 		PFin(7)		
#define KEY2 		PFin(8)		
#define KEY3 	  	PFin(9)	

PFin的宏定义:#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
最终使用的宏定义:#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
在《STM32F3与F4系列Cortex M4内核编程手册》中有关于位带别名计算公式为:

bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4)

和BITBAND宏对应,其中

“+0x2000000”为原地址到位带别名地址的偏移
“(addr &0xFFFFF)<<5”为“byte_offset x 32”(2的五次方=32);
“bitnum<<2”为bit_number × 4(2的二次方=4)

关于位带别名的详细说明参见下面“5、位带别名”

//IO口操作宏定义
#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+20) //0x40020014
#define GPIOB_ODR_Addr    (GPIOB_BASE+20) //0x40020414 
#define GPIOC_ODR_Addr    (GPIOC_BASE+20) //0x40020814 
#define GPIOD_ODR_Addr    (GPIOD_BASE+20) //0x40020C14 
#define GPIOE_ODR_Addr    (GPIOE_BASE+20) //0x40021014 
#define GPIOF_ODR_Addr    (GPIOF_BASE+20) //0x40021414    
#define GPIOG_ODR_Addr    (GPIOG_BASE+20) //0x40021814   
#define GPIOH_ODR_Addr    (GPIOH_BASE+20) //0x40021C14    
#define GPIOI_ODR_Addr    (GPIOI_BASE+20) //0x40022014     

#define GPIOA_IDR_Addr    (GPIOA_BASE+16) //0x40020010 
#define GPIOB_IDR_Addr    (GPIOB_BASE+16) //0x40020410 
#define GPIOC_IDR_Addr    (GPIOC_BASE+16) //0x40020810 
#define GPIOD_IDR_Addr    (GPIOD_BASE+16) //0x40020C10 
#define GPIOE_IDR_Addr    (GPIOE_BASE+16) //0x40021010 
#define GPIOF_IDR_Addr    (GPIOF_BASE+16) //0x40021410 
#define GPIOG_IDR_Addr    (GPIOG_BASE+16) //0x40021810 
#define GPIOH_IDR_Addr    (GPIOH_BASE+16) //0x40021C10 
#define GPIOI_IDR_Addr    (GPIOI_BASE+16) //0x40022010 
 
//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)  //输入

#define PHout(n)   BIT_ADDR(GPIOH_ODR_Addr,n)  //输出 
#define PHin(n)    BIT_ADDR(GPIOH_IDR_Addr,n)  //输入

#define PIout(n)   BIT_ADDR(GPIOI_ODR_Addr,n)  //输出 
#define PIin(n)    BIT_ADDR(GPIOI_IDR_Addr,n)  //输入

4.6 key_scan

当 mode 为 0 的时候, key_scan函数将不支持连续按, 扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。

当 mode 为 1 的时候, KEY_Scan 函数将支持连续按,如果某个按键一直按下,则会一直返回这个按键的键值,这样可以方便的实现长按检测。 该函数有返回值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0。

void key_scan(u8 mode)
{	 
	#a)为了保证键抬起值只有一次有效,再次进入扫描时,将键抬起清零  
	keyup_data=0;
	#b)有键正按下
	if(KEY0==0||KEY1==0||KEY2==0||KEY3==0){   
		if(KEY0==0)      key_tem=1;
		else if(KEY1==0) key_tem=2;
		else if(KEY2==0) key_tem=3;
		#c)有键按下后第一次扫描不处理,与else配合第二次扫描有效,这样实现了去抖动
		if (key_tem == key_bak){
			#d)有键按下后执行一次扫描函数,该变量加1
			key_time++;
			#e)按键值赋予keydown_data
			keydown_data=key_tem;
			#f)在单按模式下key_time>1时按键值无效;如果mode为1就为连按
			if( (mode==0)&&(key_time>1) )
				keydown_data=0;
		#g)去抖动
       	}else{
		       key_time=0;
		       key_bak=key_tem;
	    }
	#h)键抬起
	}else{
		#i)按键抬起后返回一次按键值
	    if(key_time>2){
	    	#j)键抬起后按键值赋予keydown_data 
			keyup_data=key_tem;   						
	   	}
		
		#k)清零,不然下次执行扫描程序时按键的值跟上次按的值一样,抖动功能将会失效
		key_bak=0;
	    key_time=0;
		keydown_data=0;		
	}    
}

执行完后,通过变量 keydown_data、keyup_data、key_tem及key_time 来获取按下、抬起、长按的键

5、位带别名

位带区:支持位带操作的地址区;
位带别名:对别名地址的访问最终作用到位带区的访问上(这中途有一个地址映射过程);

位带别名操作就是:操作位带别名的一个字相当于操作位带区中一位,映射关系如下图:

【STM32】入门(三):按键使用-GPIO端口输出控制_第7张图片

设置一个位带别名操作,在《STM32F3与F4系列Cortex M4内核编程手册》中有如下说明:
计算公式为:bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4)
和代码中 BITBAND 宏对应,其中

“+0x2000000”为原地址到位带别名地址的偏移
“(addr &0xFFFFF)<<5”为“byte_offset x 32”(2的五次方=32);
“bitnum<<2”为bit_number × 4(2的二次方=4)

【STM32】入门(三):按键使用-GPIO端口输出控制_第8张图片
支持位带操作的两个内存区的范围是:

0x2000_0000‐0x200F_FFFF(SRAM 区中的最低 1MB)
0x4000_0000‐0x400F_FFFF(片上外设区中的最低 1MB)

对 SRAM 位带区的某个比特,记它所在字节地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:

AliasAddr=0x22000000+((A-0x20000000)*8+n)*4=0x22000000+(A-0x20000000)*32+n*4

对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:

AliasAddr=0x42000000+((A-0x40000000)*8+n)*4=0x42000000+(A-0x40000000)*32+n*4

上式中,“*4”表示一个字为 4 个字节,“*8”表示一个字节中有 8 个比特。

你可能感兴趣的:(stm32,stm32,单片机)