(一)按键行列扫描与蜂鸣器
(1)技术体会:在行列式扫描结构的薄膜按键里,干扰很大,按键扫描程序非常讲究,尤其是去抖动的处理。(2)功能需求:每按一个按键,蜂鸣器就响一次。
(3)硬件原理:
(a)用4个IO来做2X2按键行列扫描,其中作为输入的2个IO口必须接上拉电阻20K左右。
(b)用1个IO经过8050三极管来驱动有源蜂鸣器,有源蜂鸣器通电就一直响,断电就停止。而无源蜂鸣器是要靠断断续续的开关信号来驱动才能响,就是要频率来驱动。
(4)源码适合的单片机:PIC18F4620,晶振为22.1184MHz
(5)源代码讲解如下:
#include
//补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr
#define beep_dr LATA1 //蜂鸣器输出
#define key_dr1 LATB3 //2X2按键行输出
#define key_dr2 LATB4 //2X2按键行输出
#define key_sr1 RB6 //2X2按键行输入
#define key_sr2 RB7 //2X2按键行输入
//补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量
//前缀都用cnt_表示。
#define cnt_delay_cnt1 25 //按键去抖动延时阀值
#define cnt_delay_cnt2 5 //按键行输出信号稳定的小延时阀值
#define cnt_voice_time 60 //蜂鸣器响的声音长短的延时阀值
void delay1(unsigned int de) ;//小延时程序,时间不宜太长,因为内部没有喂看门狗
//补充说明:吴坚鸿程序风格是这样的,凡是按键扫描函数都放在定时中
//断里,凡是按键服务程序都是放在main函数循环里。有人说不应该把子程序//放在中断里,别听他们,信鸿哥无坎坷。
void key_scan(); //按键扫描函数,放在定时中断里
void key_service(); //按键服务函数,放在main函数循环里
//补充说明:吴坚鸿程序风格是这样的,凡是switch()语句括号里面的变量名
//后缀都用_step表示。
unsigned char key_step=1; //按键扫描步骤变量,在switch()语句的括号里
//补充说明:吴坚鸿程序风格是这样的,凡是按键或者感应输入的自锁变量名
//后缀都用_lock表示。
unsigned char key_lock1=0; //按键自锁标志
//补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量
//后缀都用_cnt表示。
unsigned int delay_cnt1=0; //延时计数器的变量
unsigned int delay_cnt2=0; //延时计数器的变量
unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时
//补充说明:吴坚鸿程序风格是这样的,凡是做类型的变量的分类
//后缀都用_sec表示。
Unsigned char key_sec=0; //哪个按键被触发
//主程序
main()
{
ADCON0=0x00;
ADCON1=0x0f; //全部为数字信号
ADCON2=0xa1; //右对齐
RBPU=0; //上拉电阻
SSPEN=0; //决定RA5不作为串口
TRISB3=0; //配置按键行扫描IO为输出
TRISB4=0; //配置按键行扫描IO为输出
TRISB6=1; //配置按键列扫描IO为输入
TRISB7=1; //配置按键列扫描IO为输入
T1CON=0x24; //定时器中断配置
TMR1H=0xF5;
TMR1L=0x5F;
TMR1IF=0;
TMR1IE=1;
TMR1ON=1;
TMR1IE=1;
//补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异,
//大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可
beep_dr=0; //关蜂鸣器,上电初始化IO
while(1)
{
CLRWDT(); //喂看门狗,大家不用过度关注此行
key_service(); //按键服务
}
}
void key_scan() //按键扫描函数
{
//补充说明:如果中断一次就把所有的按键都扫描完,中断占用的时间片就会太多,势//必会影响main函数里其他子程序的运行,为了避免一口气把所//的按键都扫描完,此
//处用switch语句把4个按键分成2等分,一次中断只扫描2个按键
switch(key_step) //按键扫描步骤,
{
case 1: //扫描1号键,2号键
key_dr1=0; //按键行扫描输出第一行低电平
key_dr2=1;
delay_cnt2=0; //延时计数器清零
key_step++; //切换到下一个运行步骤
break;
case 2:
delay_cnt2++;
if(delay_cnt2>cnt_delay2) //小延时,但不是去抖动延时,替代一直受网友争议的delay1(40)
{
delay_cnt2=0;
key_step++; //切换到下一个运行步骤
}
break;
case 3:
If (key_sr1==1&&key_sr2==1)
{ //如果没有按键按下,则2个IO输入都是高电平
key_step++; //如果没有按键按下,下一个中断扫描下2个按键
key_lock1=0; //按键自锁标志清零
delay_cnt1=0; //按键去抖动延时计数器清零,此行非常巧妙
}
Else if (key_sr1==0&&key_sr2==1&&key_lock1==0)
{ // key_lock1按键自锁,避免按键一直触发,下降沿有效
++delay_cnt1; //延时计数器
//补充说明:有按键触发之后,不要马上响应,要延时一段时间去抖动,此处本人设计非常
//巧妙,很多人仅仅知道按键延时的时候要保证还能去处理别的程序,这样是还不够的,
//在延时去抖动的时候,还必须要监控延时这段时间里,按键IO输入口是否会由于受到某//种干扰突然由低变成高,如果一旦变成高,那么延时计数器delay_cnt1必须重新清零
//我当年就是因为这样处理,把卖给富士康100台受干扰死机的设备修好了,老板马上
//给我加薪1000元。
if(delay_cnt1>cnt_delay_cnt1) //延时计数器超过一定的数值
{
delay_cnt1=0;
key_lock1=1; //自锁按键置位,避免一直触发,只有松开按键,
//此标志位才会被清零
key_sec=1; //触发1号键
}
}
else if(key_sr1==1&&key_sr2==0&&key_lock1==0)
{
++delay_cnt1;
if(delay_cnt1>cnt_delay_cnt1)
{
delay_cnt1=0;
key_lock1=1; //自锁按键置位,避免一直触发
key_sec=2; //触发2号键
}
}
break;
case 4: //扫描//扫描3号键,4号键
key_dr1=1;
key_dr2=0; //按键行扫描输出第二行低电平
delay_cnt2=0; //延时计数器清零
key_step++; //切换到下一个运行步骤
break;
case 5:
delay_cnt2++;
if(delay_cnt2>cnt_delay2) //小延时,但不是去抖动延时,替代一直受网友争议的delay1(40)
{
delay_cnt2=0;
key_step++; //切换到下一个运行步骤
}
break;
case 6:
if(key_sr1==1&&key_sr2==1)
{
key_step++;
key_lock1=0;
delay_cnt1=0;
}
Else if(key_sr1==0&&key_sr2==1&&key_lock1==0)
{
++delay_cnt1;
if(delay_cnt1>cnt_delay_cnt1)
{
delay_cnt1=0;
key_lock1=1;
key_sec=3; //触发3号键
}
}
else if(key_sr1==1&&key_sr2==0&&key_lock1==0)
{
++delay_cnt1;
if(delay_cnt1>cnt_delay_cnt1)
{
delay_cnt1=0;
key_lock1=1; //自锁按键置位,避免一直触发
key_sec=4; //触发4号键
}
}
break;
}
if(key_step>6) //第1组按键与第2组按键反复轮流扫描
{
key_step=1;
}
}
void key_service() //按键服务函数
{
switch(key_sec) //按键服务状态切换
{
case 1:// 1号键
// 补充说明:voice_time_cnt只要不为0蜂鸣器就会响,中断里判断voice_time_cnt不为0
//时,会不断自减,一直到它为0时,自动把蜂鸣器关闭
voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停
key_sec=0; //相应完按键处理程序之后,把按键选择变量清零,
//避免一直触发
break;
case 2:// 2号键
voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停
key_sec=0; //相应完按键处理程序之后,把按键选择变量清零,
//避免一直触发
break;
case 3://3号键
voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停
key_sec=0; //相应完按键处理程序之后,把按键选择变量清零,
//避免一直触发
break;
case 4://4号键
voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停
key_sec=0; //相应完按键处理程序之后,把按键选择变量清零,
//避免一直触发
break;
}
}
//中断
void interrupt timer1rbint(void)
{
if(TMR1IE==1&&TMR1IF==1) //定时中断
{
TMR1IF=0; //定时中断标志位关闭
TMR1ON=0; //定时中断开关关闭
key_scan(); //按键扫描函数
if(voice_time_cnt) //控制蜂鸣器声音的长短
{
beep_dr=1; //蜂鸣器响
--voice_time_cnt; //蜂鸣器响的声音长短的计数延时
}
else
{
beep_dr=0; //蜂鸣器停止
}
TMR1H=0xF5; //重新设置定时时间间隔
TMR1L=0x5F;
TMR1ON=1; //定时中断开关打开
}
}
void delay1(unsigned int de)
{
unsigned int t;
for(t=0;t } (6)小结: 以上是我常用的编程结构。后续我做的所有项目基本上是这样一种编程结构。这一节技术上要特别重视按键扫描。有按键触发之后,不要马上响应,要延时一段时间去抖动,此处本人设计非常巧妙,很多人仅仅知道按键延时的时候要保证还能去处理别的程序,这样是还不够的,在延时去抖动的时候,还必须要监控延时这段时间里,按键IO 输入口是否会由于受到某种干扰突然由低变成高,如果一旦变成高,那么延时计数器delay_cnt1必须重新清零,我当年就是因为这样处理,把卖给富士康100台受干扰死机的设备修好了,老板马上给我加薪1000元。(未完待续下一节) 第二节:独立按键扫描与蜂鸣器 (1)学习目标:利用上一节的程序框架,把按键行列扫描方式改成独立按键扫描方式。通过这节的练习,加深熟悉吴坚鸿的程序框架,以及独立按键扫描的编程方式。 (2)功能需求:每按一个按键,蜂鸣器就响一次。 (3)硬件原理: (a)用4个IO来做独立按键扫描,4个IO口都必须接上拉电阻20K左右。 (b)用1个IO经过8050三极管来驱动有源蜂鸣器,有源蜂鸣器通电就一直响,断电就停止。 (4)源码适合的单片机:PIC18F4620,晶振为22.1184MHz (5)源代码讲解如下: #include //补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr #define beep_dr LATA1 //蜂鸣器输出 #define key_sr1 RB6 //独立按键输入 #define key_sr2 RB7 //独立按键输入 #define key_sr3 RB3 //独立按键输入 #define key_sr4 RB4 //独立按键输入 //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量 //前缀都用cnt_表示。凡是延时常量,都应该根据上机实际情况来调整出最佳的数值 #define cnt_delay_cnt1 25 //按键去抖动延时阀值 #define cnt_voice_time 60 //蜂鸣器响的声音长短的延时阀值 //补充说明:吴坚鸿程序风格是这样的,凡是按键扫描函数都放在定时中 //断里,凡是按键服务程序都是放在main函数循环里。有人说不应该把子程序//放在中断里,别听他们,信鸿哥无坎坷。 void key_scan(); //按键扫描函数,放在定时中断里 void key_service(); //按键服务函数,放在main函数循环里 //补充说明:吴坚鸿程序风格是这样的,凡是switch()语句括号里面的变量名 //后缀都用_step表示。 unsigned char key_step=1; //按键扫描步骤变量,在switch()语句的括号里 //补充说明:吴坚鸿程序风格是这样的,凡是按键或者感应输入的自锁变量名 //后缀都用_lock表示。 unsigned char key_lock1=0; //按键自锁标志 unsigned char key_lock2=0; //按键自锁标志 unsigned char key_lock3=0; //按键自锁标志 unsigned char key_lock4=0; //按键自锁标志 //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量 //后缀都用_cnt表示。 unsigned int delay_cnt1=0; //延时计数器的变量 unsigned int delay_cnt2=0; //延时计数器的变量 unsigned int delay_cnt3=0; //延时计数器的变量 unsigned int delay_cnt4=0; //延时计数器的变量 unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时 //补充说明:吴坚鸿程序风格是这样的,凡是做类型的变量的分类 //后缀都用_sec表示。 Unsigned char key_sec=0; //哪个按键被触发 //主程序 main() { ADCON0=0x00; ADCON1=0x0f; //全部为数字信号 ADCON2=0xa1; //右对齐 RBPU=0; //上拉电阻 SSPEN=0; //决定RA5不作为串口 TRISB3=1; //配置独立按键IO为输入 TRISB4=1; //配置独立按键IO为输入 TRISB6=1; //配置独立按键IO为输入 TRISB7=1; //配置独立按键IO为输入 T1CON=0x24; //定时器中断配置 TMR1H=0xF5; TMR1L=0x5F; TMR1IF=0; TMR1IE=1; TMR1ON=1; TMR1IE=1; //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异, //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可 beep_dr=0; //关蜂鸣器,上电初始化IO while(1) { CLRWDT(); //喂看门狗,大家不用过度关注此行 key_service(); //按键服务 } } void key_scan() //按键扫描函数 { //补充说明:因为独立按键扫描没有了行列按键扫描的delay1(40)延时,所以速度很//快,可以一次中断扫描完所有的按键,不用switch(key_step)语句来分段 if(key_sr1==1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位{ key_lock1=0; //按键自锁标志清零 delay_cnt1=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock1==0) //有按键按下,且是第一次被按下 { ++delay_cnt1; //延时计数器 if(delay_cnt1>cnt_delay_cnt1) { delay_cnt1=0; key_lock1=1; //自锁按键置位,避免一直触发 key_sec=1; //触发1号键 } } if(key_sr2==1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位{ key_lock2=0; //按键自锁标志清零 delay_cnt2=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock2==0) //有按键按下,且是第一次被按下 { ++delay_cnt2; //延时计数器 if(delay_cnt2>cnt_delay_cnt1) { delay_cnt2=0; key_lock2=1; //自锁按键置位,避免一直触发 key_sec=2; //触发2号键 } } if(key_sr3==1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位{ key_lock3=0; //按键自锁标志清零 delay_cnt3=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock3==0) //有按键按下,且是第一次被按下 { ++delay_cnt3; //延时计数器 if(delay_cnt3>cnt_delay_cnt1) { delay_cnt3=0; key_lock3=1; //自锁按键置位,避免一直触发 key_sec=3; //触发3号键 } } if(key_sr4==1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位{ key_lock4=0; //按键自锁标志清零 delay_cnt4=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock4==0) //有按键按下,且是第一次被按下 { ++delay_cnt4; //延时计数器 if(delay_cnt4>cnt_delay_cnt1) { delay_cnt4=0; key_lock4=1; //自锁按键置位,避免一直触发 key_sec=4; //触发4号键 } } } void key_service() //按键服务函数 { switch(key_sec) //按键服务状态切换 { case 1:// 1号键 // 补充说明:voice_time_cnt只要不为0蜂鸣器就会响,中断里判断voice_time_cnt不为0 //时,会不断自减,一直到它为0时,自动把蜂鸣器关闭 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 2:// 2号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 3://3号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 4://4号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; } } //中断 void interrupt timer1rbint(void) { if(TMR1IE==1&&TMR1IF==1) //定时中断 { TMR1IF=0; //定时中断标志位关闭 TMR1ON=0; //定时中断开关关闭 key_scan(); //按键扫描函数 if(voice_time_cnt) //控制蜂鸣器声音的长短 { beep_dr=1; //蜂鸣器响 --voice_time_cnt; //蜂鸣器响的声音长短的计数延时 } else { beep_dr=0; //蜂鸣器停止 } TMR1H=0xF5; //重新设置定时时间间隔 TMR1L=0x5F; TMR1ON=1; //定时中断开关打开 } } (6)小结: 利用上一节的程序框架,把行列扫描改成独立按键扫描的速度非常快,体现了吴坚鸿程序框架的优越性。通过此节的学习,让大家加深熟悉吴坚鸿程序框架,同时也掌握了独立按键的编程方式。(未完待续下一节) 第三节:AD按键扫描与蜂鸣器 (1)开场白: AD按键可以实现一个IO口驱动几个按键的目的,以鸿哥的经验,一个IO口控制的按键数量最好不要超过3个。从实际应用来考虑,独立按键的方式最好,其次是行列按键,最次是AD按键。 (2)功能需求:每按一个按键,蜂鸣器就响一次。 (3)硬件原理: (a)用4个电阻竖着串联起来,最上端接5V,最下端接地。从上往下,最上端接5V的算第一个节点,最下端接地算最后一个节点,共5个节点。用一个带AD的IO口连接到第2个节点上,此节点上连接第一个按键,按键的另一端接地。以此方式,第二个按键连接到第3个节点,第三个按键连接到第4个节点。这4个电阻的目的主要是用来分压,靠不同的电压来识别不同的按键。 (b)用1个IO经过8050三极管来驱动有源蜂鸣器,有源蜂鸣器通电就一直响,断电就停止。 (4)源码适合的单片机:PIC16f73,晶振为4MHz (5)源代码讲解如下: #include //补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr #define beep_dr RB0 //蜂鸣器输出 //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量 //前缀都用cnt_表示。凡是延时常量,都应该根据上机实际情况来调整出最佳的数值 #define cnt_delay_cnt1 40 //按键去抖动延时阀值 #define cnt_voice_time 75 //蜂鸣器响的声音长短的延时阀值 #define cnt_key_nc 185 //没有按键按下时电压对应的AD数值 #define cnt_key1_up 185 //1号按键按下时电压对应的AD数值上限 #define cnt_key2_down 117 //2号按键按下时电压对应的AD数值下限 #define cnt_key2_up 137 //2号按键按下时电压对应的AD数值上限 #define cnt_key3_down 160 //3号按键按下时电压对应的AD数值下限 #define cnt_key3_up 175 //3号按键按下时电压对应的AD数值上限 //补充说明:吴坚鸿程序风格是这样的,凡是按键扫描函数都放在定时中 //断里,凡是按键服务程序都是放在main函数循环里。有人说不应该把子程序//放在中断里,别听他们,信鸿哥无坎坷。 void key_scan(); //按键扫描函数,放在定时中断里 void key_service(); //按键服务函数,放在main函数循环里void ad_samping(); //AD采样, 放在main函数循环里 void delay100(); //小延时 //补充说明:吴坚鸿程序风格是这样的,凡是switch()语句括号里面的变量名 //后缀都用_step表示。 unsigned char ad_step=0; //AD扫描步骤变量, //补充说明:吴坚鸿程序风格是这样的,凡是按键或者感应输入的自锁变量名 //后缀都用_lock表示。 unsigned char key_lock1=0; //按键自锁标志 //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量 //后缀都用_cnt表示。 unsigned int delay_cnt1=0; //延时计数器的变量 unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时 //补充说明:吴坚鸿程序风格是这样的,凡是做类型的变量的分类 //后缀都用_sec表示。 Unsigned char key_sec=0; //哪个按键被触发 //补充说明:吴坚鸿程序风格是这样的,凡是只有两种状态(0或者1)的变量, //后缀都用_flag表示。 Unsigned char AD_Flag=0; //用来指示单片机内部硬件AD处理完成的标志 //跟AD有关的变量 Unsigned char key_value=0; //跟电压成比例关系的AD数值 //主程序 main() { ADCON0=0x40; //设置AD模式 ADCON1=0x04; // AD输入通道 TRISA0=1; TRISA1=1; TRISA3=1; TRISB0=0; //配置蜂鸣器输出 T1CON=0x24;//定时中断 TMR1H=0xFE; TMR1L=0xEF; INTCON=0xC0; TMR1IF=0; TMR1IE=1; PIE1=0X00; PIE2=0X00; ADIF=0; //A/D转换中断允许 ADIE=1; //A/D转换中断允许 PEIE=1; //外围中断允许GIE=1; TMR1ON=1; //开定时中断 //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异, //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可beep_dr=0; //关蜂鸣器,上电初始化IO while(1) { CLRWDT(); //喂看门狗,大家不用过度关注此行 ad_samping(); //AD采样 key_service(); //按键服务 } } void key_scan() //按键扫描函数 { if(key_value>cnt_key_nc) //空闲,没有按下 { key_lock1=0; //按键自锁标志清零 delay_cnt1=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock1==0) //有按键按下,且是第一次被按下 { if(key_value { ++delay_cnt1; //延时计数器 If(delay_cnt1>cnt_delay_cnt1) { delay_cnt1=0; key_lock1=1; //自锁按键置位,避免一直触发 key_sec=1; //触发1号键 } } else if(key_value> cnt_key2_down&&key_value< cnt_key2_up) //K2按下{ ++delay_cnt1; //延时计数器 If(delay_cnt1>cnt_delay_cnt1) { delay_cnt1=0; key_lock1=1; //自锁按键置位,避免一直触发 key_sec=2; //触发2号键 } } else if(key_value> cnt_key3_down&&key_value< cnt_key3_up) //K3按下{ ++delay_cnt1; //延时计数器 If(delay_cnt1>cnt_delay_cnt1) { delay_cnt1=0; key_lock1=1; //自锁按键置位,避免一直触发 key_sec=3; //触发3号键 } } } void key_service() //按键服务函数 { switch(key_sec) //按键服务状态切换 { case 1:// 1号键 // 补充说明:voice_time_cnt只要不为0蜂鸣器就会响,中断里判断voice_time_cnt不为0 //时,会不断自减,一直到它为0时,自动把蜂鸣器关闭 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 2:// 2号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 3://3号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; } } void ad_samping() //AD采样函数,放在main循环里 { switch(ad_step) { case 0: ADCON0=0x59; //切换直流输入通道AD采样AN3 delay100(); //此处为无厘头一个。如果有两个以上的AD通道进行切换,必须把这//个延时加上,否则 不好使。这个完全是我的实战经验,这样的事情我经常会遇到,这种 //事情完全靠经验,我当年第一次遇到的时候也被折腾了好久才发现。当然这个项目只有 //1个AD通道,所以也许可以不用。 ADIF=0; CLRWDT(); ADGO=1; //启动AD ad_step=1; //下一次循环进入下一个步骤,用switch来切换流程,一直是我//的最爱,尤其是通道多的时候,它的优越性更加能发挥得淋漓尽致。 break; case 1: if(AD_Flag==1) // AD采样完成 { AD_Flag=0; key_value=ADRES; //采集按键电压的AD数值, ADIF=0; CLRWDT(); ADGO=1; //启动AD ad_step=0; //下次循环切换回最前面那个步骤,这种控制方式我最喜欢 } break; } } //小延时 void delay100() { unsigned char k; for(k=0;k<100;k++); } //中断 void interrupt timer1rbint(void) { if(ADIF==1&&ADIE==1) //AD转换完成中断 { TMR1IE=0; //禁止定时中断,避免两个中断相互扯淡 ADIF=0; //清除中断标志 AD_Flag=1; //置AD转换完成标志 TMR1IE=1; //允许定时中断 } if(TMR1IE==1&&TMR1IF==1) //定时中断 { ADIE=0; TMR1IF=0; TMR1ON=0; key_scan(); //按键扫描函数 if(voice_time_cnt) //控制蜂鸣器声音的长短 { beep_dr=1; //蜂鸣器响 --voice_time_cnt; //蜂鸣器响的声音长短的计数延时 } else { beep_dr=0; //蜂鸣器停止 } TMR1H=0xFF; TMR1L=0xC8; TMR1ON=1; ADIE=1; } } (6)小结: 有两路AD通道进行切换时,必须加一个小延时delay100(),否则会出现无厘头现象。“无厘头现象“是鸿哥发明的一个新词,专门用来表示那些莫名其妙的,用理论不好解释的现象。(未完待续下一节) 第四节:“鸿哥三宝”之74HC165(按键扫描篇) (1)开场白: 这节将要跟大家介绍一下鸿哥的“三宝”,它们分别是74HC165,74HC595,ULN2003A.之所以它们在我心中的地位那么高,是因为很多工控小项目经常用到它。74HC165使我们从此不再为单片机的输入口不足而烦恼,我们用3根IO口就可以检测100多路的输入信号。74HC595使我们从此不再为单片机的输出口不足而烦恼,我们用4根IO口就可以驱动100多个继电器或者LED。而ULN2003A则大大简化了我们的三极管驱动电路,一个芯片就集成了7个三极管,它有500mA的驱动能力,内部自带续流二极管,用来驱动继电器的时候,二极管也省了。74HC165对静电很敏感,很脆弱,在通电的情况,绝对不要用手摸到他的引脚,我曾经用非绝缘的镊子来短接其输入口,烧坏了很多个,因此对于74HC165我们要懂得怜香惜玉,小心呵护。总之,“鸿哥三宝”实乃电子工程师居家旅行之必备良药。 (2)功能需求:每按一个按键,蜂鸣器就响一次。 (3)硬件原理: (a)把两个74HC165联级起来,就可以达到用3根IO口来检测16个按键的目的。此电路的本质是并入串出的原理。具体的电路读者只要下载芯片资料一看就明。还是那句话,按键那里记得接20K左右的上拉电阻。 (b)用1个IO经过8050三极管来驱动有源蜂鸣器,有源蜂鸣器通电就一直响,断电就停止。 (4)源码适合的单片机:PIC18f4520,晶振为22.1184MHz (5)源代码讲解如下: #include //补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr #define beep_dr LATA2 //蜂鸣器输出 # define hc165_cp_dr LATA0 //74hc165的3根驱动IO之一 # define hc165_pl_dr LATA1 //74hc165的3根驱动IO之一 # define hc165_q7_sr RE0 //74hc165的3根驱动IO之一 //补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量 //前缀都用cnt_表示。 #define cnt_delay_cnt1 40 //按键去抖动延时阀值 #define cnt_voice_time 150 //蜂鸣器响的声音长短的延时阀值 //补充说明:吴坚鸿程序风格是这样的,凡是按键扫描函数都放在定时中 //断里,凡是按键服务程序都是放在main函数循环里。有人说不应该把子程序//放在中断里,别听他们,信鸿哥无坎坷。 void key_scan(); //按键扫描函数,放在定时中断里 void key_service(); //按键服务函数,放在main函数循环里 //补充说明:吴坚鸿程序风格是这样的,凡是switch()语句括号里面的变量名 //后缀都用_step表示。 unsigned char key_step=1; //按键扫描步骤变量,在switch()语句的括号里 //补充说明:吴坚鸿程序风格是这样的,凡是按键或者感应输入的自锁变量名 //后缀都用_lock表示。 unsigned char key_lock1=0; //按键自锁标志 unsigned char key_lock2=0; //按键自锁标志 unsigned char key_lock3=0; //按键自锁标志 unsigned char key_lock4=0; //按键自锁标志 //补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量 //后缀都用_cnt表示。 unsigned int delay_cnt1=0; //延时计数器的变量 unsigned int delay_cnt2=0; //延时计数器的变量 unsigned int delay_cnt3=0; //延时计数器的变量 unsigned int delay_cnt4=0; //延时计数器的变量 unsigned int voice_time_cnt; //蜂鸣器响的声音长短的计数延时 //补充说明:吴坚鸿程序风格是这样的,凡是做类型的变量的分类 //后缀都用_sec表示。 Unsigned char key_sec=0; //哪个按键被触发 Unsigned int key_status=0; //一个字节8位,此处2个字节,共16位,每一位代表一////个按键的状态 //主程序 main() { ADCON0=0x00; ADCON1=0x0f; //全部为数字信号 ADCON2=0xa1; //右对齐 RBPU=0; //上拉电阻 SSPEN=0; //决定RA5不作为串口 TRISA2=0; //蜂鸣器输出 TRISA0=0; //74hc165的3根驱动IO之一 TRISA1=0; //74hc165的3根驱动IO之一 TRISE0=1; //74hc165的3根驱动IO之一 T1CON=0x24; //定时器中断配置 TMR1H=0xFE; TMR1L=0xEF; TMR1IF=0; TMR1IE=1; TMR1ON=1; TMR1IE=1; //补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异, //大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可 beep_dr=0; //关蜂鸣器,上电初始化IO while(1) { CLRWDT(); //喂看门狗,大家不用过度关注此行 key_service(); //按键服务 } } void key_scan() //按键扫描函数 { unsigned char j; //中间循环变量 Key_status =0x0000; //每个按键的电平状态,共16个 hc165_pl_dr=0; asm("nop"); asm("nop"); hc165_pl_dr=1; asm("nop"); asm("nop"); for(j=0;j<16;j++) { hc165_cp_dr=0; asm("nop"); asm("nop"); key_status=key_status<<1; if(hc165_q7_sr==1)key_status=key_status+1; hc165_cp_dr=1; asm("nop"); asm("nop"); } //以上一小段代码是通过驱动2个74HC165来获取16个按键的电平状态//key_status //以下代码通过解析每一位电平状态来确定哪个按键被触发 if((key_status &0x0001)==0x0001) { key_lock1=0; //按键自锁标志清零 delay_cnt1=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock1==0) { ++ delay_cnt1; if(delay_cnt1> cnt_delay_cnt1) //延时计数去抖动 { delay_cnt1=0; key_lock1=1; key_sec=1; //触发1号键 } } if((key_status &0x0002)==0x0002) { key_lock2=0; //按键自锁标志清零 delay_cnt2=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock2==0) { ++ delay_cnt2; if(delay_cnt2> cnt_delay_cnt1) //延时计数去抖动 { delay_cnt2=0; key_lock2=1; key_sec=2; //触发2号键 } } if((key_status &0x0004)==0x0004) { key_lock3=0; //按键自锁标志清零 delay_cnt3=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock3==0) { ++ delay_cnt3; if(delay_cnt3> cnt_delay_cnt1) //延时计数去抖动 { delay_cnt3=0; key_lock3=1; key_sec=3; //触发3号键 } } if((key_status &0x0008)==0x0008) { key_lock4=0; //按键自锁标志清零 delay_cnt4=0; //按键去抖动延时计数器清零,此行非常巧妙 } else if(key_lock4==0) { ++ delay_cnt4; if(delay_cnt4> cnt_delay_cnt1) //延时计数去抖动 { delay_cnt4=0; key_lock4=1; key_sec=4; //触发4号键 } } //如果要接16个按键,读者可以继续往下添加类似的代码,本例只触发4个按键作为演示 } void key_service() //按键服务函数 { switch(key_sec) //按键服务状态切换 { case 1:// 1号键 // 补充说明:voice_time_cnt只要不为0蜂鸣器就会响,中断里判断voice_time_cnt不为0 //时,会不断自减,一直到它为0时,自动把蜂鸣器关闭 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停 key_sec=0; //相应完按键处理程序之后,把按键选择变量清零, //避免一直触发 break; case 2:// 2号键 voice_time_cnt= cnt_voice_time; //蜂鸣器响“滴”一声就停