51单片机8个IO口扫描检测64个按键+数码管显示+程序+Proteus仿真+算法实现和心法要点讲解 - 扫描原理想通了很简单,就是通过一个IO拉低,其检测这一组其他7个IO口状态,如果检测到有被拉低了,说明就有按键按下了,因为检测到拉低的IO口,是被被用来检测的IO口拉低的。相当于我用一个接GND的探针去碰一个高电平的IO口,肯定是会拉低的。电路原理的话,利用二极管单向导通特性,在检查过程中,如果IO口被拉低,只能构成一个回路,依次不断轮询进行。
- 知道了按键扫描原理然后去实现的过程并不是很艰难,我觉得最困难的是调试过程遇到的各种问题。掌握其原理并不那么复杂,在写完之后,代码并不是很顺利按照自己的思路去运行的,也许就是一个项目的学习过程。一个DIY创意可能很简单,真正让其按照自己的想法运行还是有很多细节点要打通的。
- 小bug折腾的时间比整个写代码花费的精力和时间多得多,想想一个稳定好用的产品都需要几个版本的迭代。
总结要领 最难的地方是,控制56-64最后一排的数码管显示,因为,这个你是对P0总线端口自身的扫描,最容易出问题的地方,也是卡在这个地方最长时间。一定要了解其单片机运行和按键扫描原理。在没有延时或打断的情况下,按键动作的时间一定是快不过单片机运行的速度。所以在处理最后一排按键时,需要特别注意,显示时要比其他行扫描处理的时间留长一点,不然就很容易跳数,按下的按键,和显示的数值不是你想要的结果,下面我会将经验一一写下来。
仿真原理图如下(proteus仿真工程文件可到本帖附件中下载)
- 在处理最后一排按键,我有想过两个办法来处理逻辑判断问题:
- 利用复合逻辑来写,很直观,但是代码阅读和可执行性看起来相对很臃肿一样,写的时候很爽,单片机处理逻辑,运行的时间会多一些。是我最先想到的第一种办法,写法如下:
- if(P0==0x7f||P0==0xbf||P0==0xdf||P0==0xef||P0==0xf7||P0==0xfb||P0==0xfd||P0==0xfe)
复制代码
- 第二种办法,通过二分查找的方式:(为什么可以采用二分查找算法来快速筛查对象,是有讲究的):二分查找的条件就是注意事项,定义的数组必须是有序序列才行。最优方法看不懂不要紧,直接搜算法拿来用就行!写法如下:
- int Search(uchar arr[], int len, int flag)
- {
- int right = len - 1;
- int left = 0;
- while (left <= right)
- {
- int mid = (right + left) / 2;
- if (arr[mid] > flag)
- {
- right = mid - 1;
- }
- else if (arr[mid] < flag)
- {
- left = mid + 1;
- }
- else
- {
- return arr[mid];
- }
- }
- return 0;
- }
复制代码
- 第三种遍历方法,就不需要参照二分查找算法那样考虑什么注意事项了,随便写一个简单的遍历程序即可,执行效率虽然慢一点,起码实现起来简单,代码可敲性强,比起二分查找算法写起来。
- uchar libian(uchar a[], int value, int n)
- {
- int i;
- for (i = 0; i < n; i++)
- {
- if (value == a[i])
- {
- return a[i];
- }
- }
- return 0;
- }
复制代码
0-64个按键,需要考虑消抖的只有最后一排56-64的8个按键的响应。为什么这么说呢?这是因为按键从设计原理和实现来看的。0-56的按键不管你怎么长按还是短按,单片机给你的响应数值都是一样的不会变,但是在处理56-64这8个按键时,是做了特殊处理,连接的是GND,如果敲代码没注意的话,就很容易造成跳数字,单片机扫描是通过按照规定先给指定的IO口拉点,再去检测其他7个IO的电平状态,所以在处理第56-64按键时,如果你操作的按键按下时,单片机扫描按键的速度已经从你按下那一到弹起前已经超过了你的速度,那么会造成,单片机读取到错误的响应数据,处理方式如下:- /****************自身端口读取*********************/
- P0=0xff;//扫描第8行
- // delay(5);
- tmp=P0;
- if (Search(arr, 8, tmp))
- {
- //将检测到的P0状态值赋值给临时变量
- switch(Search(arr, 8, tmp))
- { //临时变量对逐个IO口进行查询
- case 0xfe:
- keynum=57;
- break;//第1行第1个按键按下
- case 0xfd:
- keynum=58;
- break;//第1行第2个按键按下
- case 0xfb:
- keynum=59;
- break;//第1行第3个按键按下
- case 0xf7:
- keynum=60;
- break;//第1行第4个按键按下
- case 0xef:
- keynum=61;
- break;//第1行第5个按键按下
- case 0xdf:
- keynum=62;
- break;//第1行第6个按键按下
- case 0xbf:
- keynum=63;
- break;//第1行第7个按键按下
- case 0x7f:
- keynum=64;
- break;//第1行第8个按键按下
- }
- display();//这里必须单独处理P0的IO检测和显示,否则容易跳数
- delay(80);//阻塞按键扫描,防止数码管跳变,其他行扫描不需要此处的延时。
- }
复制代码 完整实现代码:- #include <reg52.h>
- #include<intrins.h>
- #define uchar unsigned char
- #define uint unsigned int
- //共阴极数码管0~9
- uchar code table[]= {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};//0-9共阴数码管
- uchar code arr[] = {0x7f, 0xbf, 0xdf, 0xef, 0xf7, 0xfb, 0xfd, 0xfe};
- uchar duanZhi[]= {0,0};//保存每段数码管显示位数的数值
- sbit P36=P3^6;//数码管时能端
- sbit P37=P3^7;
- sbit ST=P3^0;//定义74HC595移位寄存器
- sbit SH=P3^2;
- sbit DS=P3^1;
- sbit P33=P3^3;
- sbit P34=P3^4;
- sbit P35=P3^5;
- uchar shi,ge;//数码管个位和十位显示
- uchar tmp;//暂存P0的值
- static uchar keynum=0;//按键值
- unsigned char Trg;
- unsigned char Cont;
- static char count=1;
- //毫秒级延时
- void delay(uint z)
- {
- uint x,y;
- for(x=z; x>>0; x--)
- for(y=110; y>>0; y--);
- }
- void SendTo595(uchar byteData);
- int Search(uchar arr[], int len, int flag)
- {
- int right = len - 1;
- int left = 0;
- while (left <= right)
- {
- int mid = (right + left) / 2;
- if (arr[mid] > flag)
- {
- right = mid - 1;
- }
- else if (arr[mid] < flag)
- {
- left = mid + 1;
- }
- else
- {
- return arr[mid];
- }
- }
- return 0;
- }
- /*----------------------------------------------------------------------------------
- 显示
- void display2()
- {
- ge = keynum%10;
- shi = keynum/10;
- duanZhi[0]=table[ge];
- duanZhi[1]=table[shi];
- P34=0x00;
- SendTo595(duanZhi[0]); //
- delay(5);
- P34=0x01;//消隐
- P33=0x00;
- SendTo595(duanZhi[1]);//
- delay(5);
- P33=0x01;//消隐
- }
- ----------------------------------------------------------------------------------*/
- void display()
- {
- ge = keynum%10;
- shi = keynum/10;
- duanZhi[0]=table[ge];
- duanZhi[1]=table[shi];
- //显示个位
- P37=0;
- SendTo595(duanZhi[0]); //
- delay(2);
- P37=1;//消隐
- //显示十位
- P36=0;
- SendTo595(duanZhi[1]);//
- delay(2);
- P36=1;//消隐
- }
- /***********************************************************
- *函数名 :SendTo595
- *功能 :串行发送8个比特(一个字节)的数据给595,再并行输出
- *参数 :byteData
- ************************************************************/
- void SendTo595(uchar byteData)
- {
- uchar i=0;
- ST = 0; //ST //先拉低,为后面的上升沿做准备
- for(i; i<8; i++)
- {
- SH = 0;//先拉低,
- if(byteData&0x80)DS=1;
- else DS=0;
- // DS = (byteData&0x80)?1:0;
- byteData = byteData <<1; //该字节右移一位
- SH = 1;//上升沿,让串行输入时钟变为高电平,并延时2个时钟周期
- _nop_();
- _nop_();
- SH = 0; //上升沿,让串行输入时钟变为高电平,并延时2个时钟周期
- }
- /*位移寄存器数据准备完毕,转移到存储寄存器*/
- ST =1;
- _nop_();
- _nop_();
- ST = 0;
- }
- void key_scan()
- {
- // P0=0xff;
- // delay(6);
- /********************第1行扫描**************************/
- P0=0x7F;//扫描第1行0111 1111
- delay(5);
- if (!Search(arr, 8, tmp))//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0x7e:
- keynum=50;
- break;//第1行第1个按键按下
- case 0x7d:
- keynum=51;
- break;//第1行第2个按键按下
- case 0x7b:
- keynum=52;
- break;//第1行第3个按键按下
- case 0x77:
- keynum=53;
- break;//第1行第4个按键按下
- case 0x6f:
- keynum=54;
- break;//第1行第5个按键按下
- case 0x5f:
- keynum=55;
- break;//第1行第6个按键按下
- case 0x3f:
- keynum=56;
- break;//第1行第7个按键按下
- }
- }
- /********************第2行扫描**************************/
- P0=0xbf;//扫描第2行
- delay(5);
- if(P0!=0xbf)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xbe:
- keynum=43;
- break;//第2行第1个按键按下
- case 0xbd:
- keynum=44;
- break;//第2行第2个按键按下
- case 0xbb:
- keynum=45;
- break;//第2行第3个按键按下
- case 0xb7:
- keynum=46;
- break;//第2行第4个按键按下
- case 0xaf:
- keynum=47;
- break;//第2行第5个按键按下
- case 0x9f:
- keynum=48;
- break;//第2行第6个按键按下
- case 0x3f:
- keynum=49;
- break;//第2行第7个按键按下
- }
- }
- /********************第3行扫描**************************/
- P0=0xdf;//扫描第3行
- delay(5);
- if(P0!=0xdf)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xde:
- keynum=36;
- break;//第3行第1个按键按下
- case 0xdd:
- keynum=37;
- break;//第3行第2个按键按下
- case 0xdb:
- keynum=38;
- break;//第3行第3个按键按下
- case 0xd7:
- keynum=39;
- break;//第3行第4个按键按下
- case 0xcf:
- keynum=40;
- break;//第3行第5个按键按下
- case 0x9f:
- keynum=41;
- break;//第3行第6个按键按下
- case 0x5f:
- keynum=42;
- break;//第3行第7个按键按下
- }
- }
- /********************第4行扫描**************************/
- P0=0xef;//扫描第4行
- delay(5);
- if(P0!=0xef)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xee:
- keynum=29;
- break;//第4行第1个按键按下
- case 0xed:
- keynum=30;
- break;//第4行第2个按键按下
- case 0xeb:
- keynum=31;
- break;//第4行第3个按键按下
- case 0xe7:
- keynum=32;
- break;//第3行第4个按键按下
- case 0xcf:
- keynum=33;
- break;//第4行第5个按键按下
- case 0xaf:
- keynum=34;
- break;//第4行第6个按键按下
- case 0x6f:
- keynum=35;
- break;//第4行第7个按键按下
- }
- }
- /********************第5行扫描**************************/
- P0=0xf7;//扫描第5行
- delay(5);
- if(P0!=0xf7)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xf6:
- keynum=22;
- break;//第5行第1个按键按下
- case 0xf5:
- keynum=23;
- break;//第5行第2个按键按下
- case 0xf3:
- keynum=24;
- break;//第5行第3个按键按下
- case 0xe7:
- keynum=25;
- break;//第5行第4个按键按下
- case 0xd7:
- keynum=26;
- break;//第5行第5个按键按下
- case 0xb7:
- keynum=27;
- break;//第5行第6个按键按下
- case 0x77:
- keynum=28;
- break;//第5行第7个按键按下
- }
- }
- /********************第6行扫描**************************/
- P0=0xfb;//扫描第6行
- delay(5);
- if(P0!=0xfb)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xfa:
- keynum=15;
- break;//第6行第1个按键按下
- case 0xf9:
- keynum=16;
- break;//第6行第2个按键按下
- case 0xf3:
- keynum=17;
- break;//第6行第3个按键按下
- case 0xeb:
- keynum=18;
- break;//第6行第4个按键按下
- case 0xdb:
- keynum=19;
- break;//第6行第5个按键按下
- case 0xbb:
- keynum=20;
- break;//第6行第6个按键按下
- case 0x7b:
- keynum=21;
- break;//第6行第7个按键按下
- }
- }
- /********************第7行扫描**************************/
- P0=0xfd;//扫描第7行
- delay(5);
- if(P0!=0xfd)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xfc:
- keynum=8;
- break;//第7行第1个按键按下
- case 0xf9:
- keynum=9;
- break;//第7行第2个按键按下
- case 0xf5:
- keynum=10;
- break;//第7行第3个按键按下
- case 0xed:
- keynum=11;
- break;//第7行第4个按键按下
- case 0xdd:
- keynum=12;
- break;//第7行第5个按键按下
- case 0xbd:
- keynum=13;
- break;//第7行第6个按键按下
- case 0x7d:
- keynum=14;
- break;//第7行第7个按键按下
- }
- }
- /********************第8行扫描**************************/
- P0=0xfe;//扫描第8行
- delay(5);
- if(P0!=0xfe)//有按键按下
- {
- tmp=P0;//将检测到的P0状态值赋值给临时变量
- switch(tmp)
- { //临时变量对逐个IO口进行查询
- case 0xfc:
- keynum=1;
- break;//第8行第1个按键按下
- case 0xfa:
- keynum=2;
- break;//第8行第2个按键按下
- case 0xf6:
- keynum=3;
- break;//第8行第3个按键按下
- case 0xee:
- keynum=4;
- break;//第8行第4个按键按下
- case 0xde:
- keynum=5;
- break;//第8行第5个按键按下
- case 0xbe:
- keynum=6;
- break;//第8行第6个按键按下
- case 0x7e:
- keynum=7;
- break;//第8行第7个按键按下
- }
- }
- /****************自身端口读取*********************/
- P0=0xff;//扫描第8行
- // delay(5);
- tmp=P0;
- if (Search(arr, 8, tmp))
- {
- //将检测到的P0状态值赋值给临时变量
- switch(Search(arr, 8, tmp))
- { //临时变量对逐个IO口进行查询
- case 0xfe:
- keynum=57;
- break;//第1行第1个按键按下
- case 0xfd:
- keynum=58;
- break;//第1行第2个按键按下
- case 0xfb:
- keynum=59;
- break;//第1行第3个按键按下
- case 0xf7:
- keynum=60;
- break;//第1行第4个按键按下
- case 0xef:
- keynum=61;
- break;//第1行第5个按键按下
- case 0xdf:
- keynum=62;
- break;//第1行第6个按键按下
- case 0xbf:
- keynum=63;
- break;//第1行第7个按键按下
- case 0x7f:
- keynum=64;
- break;//第1行第8个按键按下
- }
- display();//这里必须单独处理P0的IO检测和显示,否则容易跳数
- delay(80);//阻塞按键扫描,防止数码管跳变,其他行扫描不需要此处的延时。
- }
- display();//这里的显示是保存上一次的显示值
- }
- void main()
- {
- keynum=0;
- P0=0xff;
- while(1)
- {
- key_scan();
- }
- }
复制代码 仿真器件选选择也是很有讲究的。附上截图
- 注意:二极管选型,不能选择发光二极管,有些其他的二极管也不好使,我使用的是10A01
- 注意走线,线路连接中最好不要出现重复线或者点,有多余的线或者连接点最好删除,不然出问题了排查问题带来难度,有可能也会影响仿真效果,电阻选择不能过大。仿真时,会比较占电脑内存。按键操作和响应速度可能会存在卡顿。
我会附上源码,提供两套不同实现方案的代码。
8个IO口检测64个按键.zip
(118.94 KB, 下载次数: 42)
- 创作不易,写出来调试代码,折腾了我好几天,对于有帮助的朋友,希望能用上,避免重复的道路上采坑。
|