大家好,我是老耿,高职青椒一枚,一直从事单片机、嵌入式、物联网等课程的教学。 对于高职的学生层次,同行应该都懂的,老师在课堂上教学几乎是没什么成就感的。正因如此,才有了借助 CSDN 平台寻求认同感和成就感 的想法。在这里,我准备陆续把自己花了很多心思的教学设计分享出来,主要面向广大师生朋友,单片机老鸟就略过吧。欢迎点赞+关注,各位的支持是本人持续输出的动力,多谢多谢!
前面,我们介绍了STM32的IO口作为输出的使用,这一章,我们将向大家介绍如何使用IO口作为输入。在本章中,我们将利用开发板上的按键来控制LED的亮灭。通过本章的学习,我们将明白按键的电路原理,了解按键消抖是怎么回事,巩固GPIO的初始化配置,学习GPIO端口输入函数等知识。
【学习目标】
- 了解按键防抖、锁存的方法
- 巩固GPIO初始化的过程,独立完成代码编写
- 理解按键单击、双击、长按的程序算法
**按键是初学嵌入式的第一类输入器件,****入门不难,但是一旦按法多样化(单击/双击/长按),或是结合其他被控器件,就需要用上中断、定时器、状态机等知识,难度也就上来了。**本章还是基于GPIO输入电平的传统方法来按键,计划分两个部分,本文是第一部分。
一、认识按键开关
1.1 按键开关的形态和结构
按键开关主要是指轻触式按键开关,广泛用于各种电器和电子消费品中,有各种各样不同的形态,如图1所示,用圈标注的是我们开发板使用的类型。不管何种形态,它都是一种电子开关,使用时以满足操作力的条件向开关操作方向施压,开关可以闭合接通,当撤销压力时开关即断开,其内部结构是靠金属弹片受力变化来实现通断的。
图1 不同形态的按键开关
如图2所示,我们的开发板上一共有4个按键开关,用KEYx来表示。这种开关虽然有四个引脚,但我们来看图3,内部①脚和②脚是常闭的,③脚和④脚也是常闭的,这样其实相当于只有两个引脚了。
图2 开发板上的四个按键
图3 四脚按键开关的内部结构示意
1.2 按键的抖动和消抖
按键机械触点断开或闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生带波纹信号,如图4所示,所以需要对波纹信号进行消抖处理,否则可能会引起误判。
图4 按键接通或断开瞬间的抖动纹波
显然上图中的纹波是我们不希望的,因此就需要想办法消抖。消抖的办法有两种:硬件消抖和软件消抖,硬件消抖是在按键两端并联一个0.1uF的电容,利用电容充放电的延时,消除波纹。硬件消抖很少采用,更普遍的做法是通过软件消抖,通过延时10~20ms的方式避开抖动,也可以采用连续多次检测电平的方式避开抖动。
二、按键控制LED编程实践
2.1 任务描述
本实验的任务是用一个按键实现对一个LED的控制,每按一次,LED的状态就改变一次。在控制方式上,使用了无锁存和有锁存两种方法,分别实现了按下有效和松开有效。
2.2 硬件电路
图5是开发板上四个按键与STM32连接的电路原理图,连接方式都是一样的,这里就以SW1(KEY1)为例进行介绍。当SW1按下时,KEY1端(PC13)与GND接通,为低电平;当SW1松开时,KEY1端电平被上拉电阻R48拉高。这样,我们通过检测PC13的输入电平就知道KEY1是按下还是松开了。同理,KEY2、KEY3、KEY4的状态需要分别检测PC11、PC12、PD2的输入电平。
图5 四个按键的原理图
2.3 工程文件清单
本实验的工程文件清单如图6所示,在HARDWARE目录中添加了一对按键的驱动文件 key.c 和 key.h。由于用到了LED,因此之前的 led.c 和 led.h 需要保留。
图6 按键工程文件清单
2.4 工程代码剖析
1. key.h文件源码
头文件里依然是与IO有关的宏和函数声明,如代码清单1所示。
cpp
//-------------------------------------------------------
// 代码清单1:key.h
//-------------------------------------------------------
#ifndef _KEY_H_
#define _KEY_H_
#include "stm32f10x.h"
//-------------------------------------------------------
// 必要的宏定义
//-------------------------------------------------------
#define KEY1_PIN GPIO_Pin_13
#define KEY2_PIN GPIO_Pin_11
#define KEY3_PIN GPIO_Pin_12
#define KEY4_PIN GPIO_Pin_2
//-------------------------------------------------------
// 库函数操作宏定义
//-------------------------------------------------------
#define READ_KEY1 GPIO_ReadInputDataBit(GPIOC, KEY1_PIN)
#define READ_KEY2 GPIO_ReadInputDataBit(GPIOC, KEY2_PIN)
#define READ_KEY3 GPIO_ReadInputDataBit(GPIOC, KEY3_PIN)
#define READ_KEY4 GPIO_ReadInputDataBit(GPIOD, KEY4_PIN)
//--------------------------------------------------------
// 函数声明
//--------------------------------------------------------
void Key_Init(void); //按键初始化函数
#endif
这里,我们用 GPIO_ReadInputDataBit() 这个库函数来读取一个IO口的电平,函数名虽然长了点,但确实见名就能知意。它有两个参数:GPIOx和GPIO_Pin_x,返回值就是读到的电平(1或0),确实也很直观。
2. key.c文件源码
该文件里就一个函数 Key_Init(),用来初始化按键的IO口,如代码清单2所示。
cpp
/*
************************************************************************
* 代码清单2:key.c
* 描 述:按键的初始化、驱动
* 平 台:麒麟座V3.2
* 作 者:老耿
* 日 期:yyyy-mm-dd
* 固 件 库:ST3.5.0
* 版 本:V1.0
* 修改记录:无
************************************************************************
*/
//必要的头文件
#include "key.h"
#include "delay.h"
/**
************************************************************************
* 函 数 名:Key_Init
* 功 能:按键端口初始化
* 入口参数:无
* 出口参数:无
* 说 明:将按键端口设置成输入模式
************************************************************************
**/
void Key_Init(void)
{
//定义一个GPIO初始化对象(结构体)
GPIO_InitTypeDef gpio_initstruct;
//打开必要的外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
//填充初始化结构体,上拉输入模式,并执行生效
gpio_initstruct.GPIO_Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN;
gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;
//输入模式不用配置Speed参数
//gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &gpio_initstruct);
//KEY4与KEY1/2/3不是一组端口,单独再初始化
gpio_initstruct.GPIO_Pin = KEY4_PIN;
GPIO_Init(GPIOD, &gpio_initstruct);
}
虽然本实验只使用到了KEY1,但在代码中,我们将KEY2、KEY3、KEY4的IO口也一并进行了初始化。需要注意的是,GPIO要配置为输入模式。由于按键松开时对应的引脚为高电平,所以我们将其配置为上拉输入模式。
那为什么没有配置为下拉输入模式呢?那是因为下拉模式,引脚默认为低电平,这相当于按键被按下的状态。这样的话,程序将无法检测按键是否被按下,因为无论按键是否被按下,引脚电平都为低电平了。当然,配置成浮空输入模式当然也是可以的。
还有一点大家是否发现,上面代码中,没有配置引脚的速度,那是因为引脚处于输入模式时,并不需要配置引脚速度。引脚速度仅是指单片机向引脚刷新电平的频率,所以只有在引脚处于输出模式时才有效。
3. main.c文件源码
主程序里编写了无锁存和有锁存两种按键控灯的方法,如代码清单3所示,大家测试的时候请保留一种方式的代码,注释掉另一种,来体会这两种方式的差异。
cpp
/**
******************************************************
* 代码清单3:main.c
* 项 目:按键控制LED
* 任务描述:按一次KEY1,变一次LED
* 实验平台:OneNET STM32开发板V3.2
* 作 者:老耿
* 日 期:yyyy/mm/dd
******************************************************
**/
//-----------------------------------------------------
// 必要的头文件
//-----------------------------------------------------
#include "delay.h"
#include "led.h"
#include "key.h"
//-----------------------------------------------------
// 主函数
//-----------------------------------------------------
int main()
{
delay_init(); //延时初始化
Led_Init(); //LED初始化
Key_Init(); //按键初始化
while(1) //以下两种方式保留一种,注释掉另一种
{
/*-------------- 无锁存方式 -------------*/
// if(!READ_KEY1) //按住红灯亮
// RED_ON;
// else
// RED_OFF; //松开红灯灭
/*-------------- 有锁存方式 -------------*/
if(!READ_KEY1) //按下KEY1
{
delay_ms(10); //延时消抖
if(!READ_KEY1) //确认按下
{
while(!READ_KEY1); //等待松开
RED_TOG; //红灯变化
}
}
}
}
2.5 验证与测试
我们对两种方式分别下载和测试后,大家应该可以发现,无锁存方式下需要按住不放,LED才亮着,一旦松开,LED就灭了。而有锁存的方式下,需要完成按下和松开这一组动作(即单击)才能改变LED的状态,实现这个效果的就是代码清单3中的第36行,当按键被按下时,while的条件将永远成立,这样将导致程序一直停留在此处,只有按键松开时,后续代码才可以得到执行,也就达到了通过按键锁存程序状态的目的。
此外,在有锁存方式中,当按键被按下后,并不是马上进行后续动作,而是延时了10ms,再次判断按键是否被按下,这样就避免了按键因电平抖动造成被误判的可能。大家也可以尝试去掉消抖这条语句,看每次按键按下的操作是否都能有效。
(第一部分完,共两部分)