很多时候画的板子因为IC价格低廉,IO口限制、串口数量等因素,在调试的时候不方便,于是做了个AD按键的板子,为了简单画出来的长这样:
上图用了三根线(VCC、IO、GND),是因为用的板子是一个很简单的stm8s的板子,没有在AD的IO上加上上拉电阻,上拉电阻在这个AD按键板子上,如果集成上去的话就只需要两根线(IO、GND)就可以。下面是原理图:
按键部分非常简单,原理就是采用不同阻值分压,上拉电阻固定22K,下拉根据电压范围我选择了以上十个,把供电电压差不多平均分成11份对应10个按键,当理论上可以分成很多个,这个要根据adc精度来的,比如我测试的stm8s是10bit的adc,最大值就是1024,理论可以支持1023个按键,12bit最大值4096理论可以支持4095个,但是adc采样总会有点误差,所以只要差按键adc的值在误差范围外就可以。接下来就是主要代码部分了,代码详细来解析下:
首先是头文件:
#ifndef __FY_KEY_AD_H
#define __FY_KEY_AD_H
#include "fy_includes.h"
//#define KEY_DOUBLE_THREE //检测双击 三击
//状态
typedef enum
{
KEY_SHORT=0, //单击
KEY_DOUBLE, //双击
KEY_THREE, //三击
KEY_LONG, //长按
KEY_HOLD, //保持
KEY_LONG_UP, //长按抬起
}_typdef_key_sta;
//键值
typedef enum
{
KEY0=0,
KEY1,
KEY2,
KEY3,
KEY4,
KEY5,
KEY6,
KEY7,
KEY8,
KEY9,
KEY_NONE = 0xff
}_typdef_key_val;
typedef struct
{
u16 adc_val;
//filter
u8 used_key;
u8 old_key;
u8 key_counter;
//scan
u8 last_key;
u8 key_press_counter;
u8 key_press_flag;
//value
u8 value;
}_typdef_key;
void Key_Scan(_typdef_key *key);
#endif
/*********************************************END OF FILE**********************************************/
头文件是一些枚举类型的定义,包含按键状态和按键键值两个枚举,还有我这里有个支持双击、三击的宏,我测试的时候效果并不太理想,我在实际应用中是有使用过双击和三击的,不过这个是在应用中去二次判断的,在按键底层驱动去加效果不好还会扰乱正常的程序,所以请忽略这一点,判断双击、三击的代码在C文件里面也有,可以做下参考。
接下来是C文件:
第一部分:
/*按键门槛值*/
#define KEY_BASE_CNT 4
#define KEY_LONG_CNT 75
#define KEY_HOLD_CNT 15
#define KEY_SHORT_CNT 7
#define R_UP 220 //22K
#define ADC_33 (1024) //VCC
//#define ADC_30 (ADC_33*2200/(2200 + R_UP)) //220K
//#define ADC_27 (ADC_33*1000/(1000 + R_UP)) //100K
//#define ADC_23 (ADC_33*510 /(510 + R_UP)) //51K
//#define ADC_20 (ADC_33*330 /(330 + R_UP)) //33K
//#define ADC_17 (ADC_33*240 /(240 + R_UP)) //24K
//#define ADC_13 (ADC_33*150 /(150 + R_UP)) //15K
//#define ADC_10 (ADC_33*91 /(91 + R_UP)) //9.1K
//#define ADC_07 (ADC_33*62 /(62 + R_UP)) //6.2K
//#define ADC_04 (ADC_33*30 /(30 + R_UP)) //3K
//#define ADC_00 (0)
#define ADC_30 (920)
#define ADC_27 (846)
#define ADC_23 (713)
#define ADC_20 (616)
#define ADC_17 (536)
#define ADC_13 (417)
#define ADC_10 (303)
#define ADC_07 (248)
#define ADC_04 (119)
#define ADC_00 (0)
#define AD_NOKEY ((ADC_33 + ADC_30)/2)
#define ADKEY_0 ((ADC_30 + ADC_27)/2)
#define ADKEY_1 ((ADC_27 + ADC_23)/2)
#define ADKEY_2 ((ADC_23 + ADC_20)/2)
#define ADKEY_3 ((ADC_20 + ADC_17)/2)
#define ADKEY_4 ((ADC_17 + ADC_13)/2)
#define ADKEY_5 ((ADC_13 + ADC_10)/2)
#define ADKEY_6 ((ADC_10 + ADC_07)/2)
#define ADKEY_7 ((ADC_07 + ADC_04)/2)
#define ADKEY_8 ((ADC_04 + ADC_00)/2)
const u16 ad_key_table[] =
{
ADKEY_0,ADKEY_1,ADKEY_2,ADKEY_3,ADKEY_4,
ADKEY_5,ADKEY_6,ADKEY_7,ADKEY_8
};
//键值量化
static u8 Get_ADKeyValue(u16 key_value)
{
u8 key_number;
if (key_value > AD_NOKEY) return KEY_NONE;
for (key_number = 0; key_number < sizeof (ad_key_table) / sizeof (ad_key_table[0]); key_number++)
{
if (key_value > ad_key_table[key_number])
break;
}
return key_number;
}
这部分主要包含头文件,不同电压ADC值的一些定义,R_UP为上拉电阻,单位是0.1K,其他的类似,然后就是ADC_33,这个需要注意下,我这里测试的单片机运行是在3.3V,所以我取名为ADC_33,后面的1024是指单片机在上拉时所采集到的ADC的值,也就是电源电压3.3V,因为是10bitADC,所以是1024,如果12位的话那就是4096,这个很好理解。然后再下面的宏定义是不同电阻根据电阻分压原理计算出来的电压值转换后的ADC,先把这些定义好,方便下面用。我这里因为手里没有上述的所有阻值电阻,索性我直接仿真,按住不同按键,得到ADC采样值然后对应放进去,这样省事。下面还有个ADC取中间值的宏定义,这个也好理解,假如KEY0的键值ADC对应的是920,KEY1的是846,那么就取两个的中间值,当大于这个中间值我们就可以认为是KEY0,小于这个就认为是KEY1,这里只是举例2个键,多建一个道理,这样就可以很好的解决精度、偏差问题。
接下来把这些ADC值组成一个常量数组,依次从大到小去拍列好,这个从大到小的顺序是根据键值量化这个函数去定义的,如果修改数组排列顺序,那就需要修改这个函数里面的判断了。键值量化函数就是根据上面提到的方法返回数组的下标,而这个下标是0-9,刚好对应h文件里面的键值枚举。
第二部分:
//滤波
static u8 Key_Filter(_typdef_key *key,u8 key_num)
{
if (key->old_key != key_num)
{
key->key_counter = 0;
key->old_key = key_num;
}
else
{
key->key_counter++;
if (key->key_counter == KEY_BASE_CNT)
{
key->used_key = key_num;
}
}
return key->used_key;
}
这里的滤波次数是KEY_BASE_CNT(4),我们在独立按键的时候就会有一个按键消斗,这里是一样的,就不多解释了。
第三部分:
void Key_Scan(_typdef_key *key)
{
u8 cur_key, key_status, back_last_key;
cur_key = KEY_NONE;
back_last_key = key->last_key;
cur_key = Key_Filter(key,Get_ADKeyValue(key->adc_val));
if (cur_key == key->last_key) //长时间按键
{
if (cur_key == KEY_NONE)
{
key->value = KEY_NONE;
return;
}
key->key_press_counter++;
if (key->key_press_counter == KEY_LONG_CNT) //长按
{
key_status = KEY_LONG;
}
else if (key->key_press_counter == (KEY_LONG_CNT + KEY_HOLD_CNT)) //连按
{
key_status = KEY_HOLD;
key->key_press_counter = KEY_LONG_CNT;
}
else
{
key->value = KEY_NONE;
return;
}
}
else //cur_key = NO_KEY, 抬键
{
key->last_key = cur_key;
if ((key->key_press_counter < KEY_LONG_CNT) && (cur_key != KEY_NONE))
{
}
if ((key->key_press_counter < KEY_LONG_CNT) && (cur_key == KEY_NONE)) //短按抬起
{
key->key_press_counter = 0;
key_status = KEY_SHORT;
}
else if((cur_key == KEY_NONE) && (key->key_press_counter >= KEY_LONG_CNT)) //长按抬起
{
key->key_press_counter = 0;
key_status = KEY_LONG_UP;
}
else
{
key->key_press_counter = 0;
key->value = KEY_NONE;
return;
}
}
#ifdef KEY_DOUBLE_THREE
if(key_status == KEY_SHORT){
key->value = Checks((key_status<<4)|back_last_key);
}
else
key->value = ((key_status<<4)|back_last_key);
#else
key->value = ((key_status<<4)|back_last_key);
#endif
}
这部分就是核心处理部分,首先是把采集到的ADC值经滤波量化后得到当前按键键值,备份上一次的键值,把当前值与上一次的键值进行比较,结果有两个,一个是相等,一个是不等。
相等的话就证明这个键还没松开,那没有松开的话就来判断按下去的时间了,有三个阈值,小与定义长按的时间点,等于长按的时间点,大于长按的时间点。这三个点分别对应短按开始(不输出键值),长按的开始(输出long),和连按开始(输出hold),在达到连按这个点又会重新对计数复制,以保证下一次能满足连按这个点。所以从一个按键按下不松的情况会输出一次long和多次hold。
不等的情况也主要是看时间点,判断到没有按键的时候看有没有满足长按,不满足则输出单击动作short,满足则输出长按抬起动作long_up。
以上就简单的介绍下,关系也很绕,书面起来可能不够彻底,所以如果要使用的话建议还是仿真下。我测试的结果如下:
从上面可以看出,一个按键最少可以输出单击抬起、长按开始、保持、长按抬起四种结果,如果是10个按键,那就是40种结果!还是挺实用的,最少就两根线就可以实现。
想要测试源码也可以QQ加群下载:733945348
By Urien 2019年9月1日 15:19:51