MCU软件最佳实践——独立按键


1. 引子

在进行mcu驱动和应用开发时,经常会遇到独立按键驱动的开发,独立按键似乎是每一个嵌入式工程师的入门必修课。笔者翻阅了许多书籍(包括上大学时候用的书籍)同时查阅了网上许多网友的博客,无一例外,清一色采用检测、延时、再检测,千篇一律,似乎是按键过于简单,因此没有人愿意推陈出新。
本文介绍一种独立按键驱动,消除抖动不采用软件延时(让CPU死等,mcu无休止的运行_nop_()函数,在稍微复杂应用场景,cpu时间捉襟见肘的场合,一个独立按键便让CPU如此耗力,是非常不明智的)。本驱动短小精悍,可以是适用于裸机应用,也可方便移植到rtos。

2. 思路

分析独立按键按下的过程,分为稳定状态非稳定状态
根据经验值,抖动消除时间为10ms左右;
累计检测到超过10ms按下状态,视为按键按下;
累计检测到超过10ms弹起状态,视为按键弹起;

2.1 按键驱动key.c

/**
 * @file: key.c
 * @brief: 按键驱动实现
 */
#include 
#define STAT_RELEASED 	(1)
#define STAT_PRESSED  	(0)
unsigned char (*keyfn)(void);// 应用提供本函数实现,返回1表示读到高电平,返回0表示读到低电平
unsigned char flg_down;
unsigned char flg_up;
unsigned char stat; // 默认是弹起状态
unsigned char timer;
 
void key_init(unsigned char (*fn)(void))
{
	stat = STAT_RELEASED;
	keyfn = fn;
}
 
void key_scan(void)
{
	if(keyfn && !keyfn()) // 有按键按下
	{
		if(timer < 1)  // 非稳态
		{
			timer++;
			return;
		}
		if(stat == STAT_RELEASED)
		{
			stat = STAT_PRESSED;
			// post down event
			flg_down = 1;
		}
	}
	else
	{
		if(timer > 0) // 非稳态
		{
			timer--;
			return;
		}
		if(stat == STAT_PRESSED)
		{
			stat = STAT_RELEASED;
			// post up event
			flg_up = 1;
		}
	}
}

2.2 按键对外接口key.h

/**
 * @file:key.h
 * @brief:按键驱动对外接口
 */
 /**
  * @function: key_init
  * @param fn: 函数指针,有返回值,读到高电平返回1,读到低电平返回0
  * @brief: 按键模块初始化函数,传递io电平读取函数到按键模块
  */
extern void key_init(unsigned char (*fn)(void));
extern unsigned char flg_down;
extern unsigned char flg_up;
extern void key_scan(void); // 10ms周期调用

3 实验

假设单片机P2.1口为按键;
按下和弹起时,串口打印输出

为了方便实验,添加串口驱动、定时器驱动程序。

3.1 串口驱动

/**
 * @file: uart.c
 * @brief: 串口驱动,波特率9600bps,10bit模式
 *
 */
#include 
 
void uart_init(void)
{
	SCON = 0X50; // 10bit 可变波特率模式
	
	//T1: SM1SM0=10,8bit auto reload,波特率9600bps
	TMOD = (TMOD & 0X0F) | (1 << 5);
	TH1 = TL1 = 0XFD;
	TR1 = 1;
	
	ES = 1;
	EA = 1;
	TI = 1; // start transmit if using putchar provided by c51 lib
}
 
void uart_isr(void) interrupt 4
{
	if(RI)
	{
		RI = 0;
	}
	if(TI)
	{
	}
}
/**
 * @file: uart.h
 */
// 串口模块
extern void uart_init(void);

3.2 定时器驱动

/**
 * @file: timer0.c
 * @brief: 产生10ms事件
 */
#include 
int systick;
unsigned char flg_10ms;
unsigned char flg_50ms;
unsigned char flg_sec;
void timer_init(unsigned char ms)
{
	TMOD = (TMOD & 0XF0);  // 模式0:13bit 定时器模式,最大计数值8192
	TH0 = (8192 - ms * 1000) / 32; // TH0的8位保存13bit初值的高8bit
	TL0 = (8192 - ms * 1000) % 32; // TL0的低5位用来存储13bit初值得低5bit
	
	TR0 = 1;
	
	ET0 = 1;
	EA = 1;
}
 
 
void timer_isr(void) interrupt 1
{
	TR0 = 0;
	timer_init(1);
	
	systick++;
	if(systick % 10 == 0)
	{
		flg_10ms = 1;
		if(systick % 50 == 0)
		{
			flg_50ms = 1;
			if(systick % 1000 == 0)
			{
				flg_sec = 1;
			}
		}
	}
}
/**
 * @file:timer0.h
 */
// 定时器模块
extern unsigned char flg_10ms;
extern unsigned char flg_50ms;
extern unsigned char flg_sec;
extern void timer_init(unsigned char ms);

3.3 编写应用

  1. 初始化串口、定时器、按键模块
  2. 10ms为周期扫描按键
  3. 监测按键事件并处理
#include 
#include 
#include 
#include 
#include 

sbit button = P2^1;
 
unsigned char kfn(void)
{
	return button? 1: 0;
}
 
void main(void)
{
	uart_init();
	timer_init(1);
	key_init(kfn);
	while(1)
	{
		if(flg_10ms)
		{
			flg_10ms = 0;
			key_scan(); // 10ms扫描1次
		}
		if(flg_down)
		{
			flg_down = 0;
			printf("key pressed\r\n");
		}
		if(flg_up)
		{
			flg_up = 0;
			printf("key released\r\n");
		}
	}
}