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


    【STM32】STM32单片机总目录

    1、简述

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

    2、原理图

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

    2.1 普通按键

    实际中没有KEY3,忽略它。这几个普通按键没有去抖电路,因此,需要在代码中用软件去抖。
    按键去抖电路(硬件去抖)可以参见下面下复位按键电路。
    在这里插入图片描述

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

    2.2 复位按键

    复位按键并联一个电容,实现了硬件去抖动设计,如下图所示:
    在这里插入图片描述

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

    3、数据手册

    GPIOF寄存器映射如下:
    在这里插入图片描述
    外部复位引脚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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    相关变量、函数定义在另一个文件中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);  		//按键扫描函数	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    手册中关于定时器时钟的描述:
    在这里插入图片描述

    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);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;	  	    
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    相关手册:《STM32F3与F4系列Cortex M4内核编程手册》
    在这里插入图片描述
    在这里插入图片描述

    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
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.5 按键定义(位带别名)

    #define KEY0 		PFin(6)   
    #define KEY1 		PFin(7)		
    #define KEY2 		PFin(8)		
    #define KEY3 	  	PFin(9)	
    
    • 1
    • 2
    • 3
    • 4

    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)
    
    • 1

    和BITBAND宏对应,其中

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

    关于位带别名的详细说明参见下面“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)  //输入
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    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;		
    	}    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

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

    5、位带别名

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

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

    在这里插入图片描述

    设置一个位带别名操作,在《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)
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    支持位带操作的两个内存区的范围是:

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

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

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

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

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

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

  • 相关阅读:
    ubuntu下pycharm闪退
    项目开发中关于 uniapp实现 Android和IOS获取App缓存,清除缓存功能
    Android源码分析挖掘-开天辟地init进程
    华为数据中心VS技术理论讲解
    Zebec 生态 AMA 回顾:Nautilus 以及 $ZBC 的未来
    Ceph分布式存储的简单介绍与Ceph集群的部署搭建
    “咕”了 73 天,何同学终于回归:最喜欢 3D 打印机,但不要买
    Linux中断编程
    c++备忘录
    java从入门到进阶
  • 原文地址:https://blog.csdn.net/u010168781/article/details/126100943