用户与单片机之间的信息交互依赖两类设备:输入设备和输出设备。
LED小灯、数码管、点阵都是输出设备。
按键属于是输入设备。
单片机最小系统
电源
上图为STC89C52单片机的数据手册
在手册中,工作电压:3.4 ~ 5.5V(5V单片机),说明这个单片机正常的工作电压是一个范围值,只要电源VCC在3.4 ~ 5.5V都可以正常工作,电压超过5.5V是绝对不允许的,会烧坏单片机,电压低于3.4V,单片机不会损坏,但是也不能正常工作。而在这个范围内,最典型、最常用的电压值就是5V,这就是后面括号里“5V单片机”名称的由来。
晶振
晶振通常分为无源晶振和有源晶振两种类型,无源晶振一般称为crystal晶体,而有源晶振则叫做oscillator振荡器。
有源晶振是一个完整的谐振振荡器,它是利用石英晶体的压电效应起振,所以有源晶振需要供电,当把有源晶振电路做好后,不需要外接其他器件,只要给它供电,它就可以主动产生振荡频率,并且可以提供高精度的频率基准,信号质量也比无源晶振信号稳定。
无源晶振自身无法振荡起来,它需要芯片内部的振荡电路一起工作才能振荡,它允许不同的电压,但是信号质量和精度较有源晶振差一些。相对价格来说,无源晶振要比有源晶振价格便宜很多。
无源晶振两侧通常会有一个电容,一般其容值为10 ~ 40pF,如果手册中有具体的电容值的要求则要根据要求来选择电容,如果手册上没有要求,则用20pF是比较好的选择,这是一个长久的经验值,具有极其普遍的适用性。
上图为有源晶振实物图,有源晶振通常有4个引脚,VCC、GND、晶振输出引脚和一个用不到的悬空引脚(有些晶振也把该引脚作为使能引脚)。
对于有源晶振,只要接到单片机晶振的输入引脚,输出引脚不需要连接,接法图如下
上图为无源晶振实物图,无源晶振有2或者3个引脚,如果是3个引脚,则中间的引脚接晶振外壳,使用时要接到GND,两侧的引脚就是晶体的两个引出脚了,这两个引脚作用是等同的,就像电阻的两个引脚一样,没有正负之分。
对于无源晶振,用单片机上的两个晶振引脚接上即可,接法如下
复位电路
上图为KST-51开发板上的复位电路
当这个电路处于稳态的时候,电容起到隔离直流的作用,隔离了+5V,而左侧的复位按键是弹起状态,复位按键下面部分电路就不产生电压差,所以按键和电容C11以下部分的电位都是和GND相等的,也就是0V。这个单片机是高电平复位,低电平正常工作,所以正常工作的电压是0V。
从没有电到上电的瞬间,电容C11上方电压是5V,下方电压是0V,根据以往所学知识,电容C11需要进行充电,正离子从上往下充电,负离子从GND往上充电,这时电容对电路来说相当于一根导线,全部电压都加在R31上,RST端口位置的电压就是5V,随着电容充电越来越多,电流会越来越小,而RST端口上的电压值等于电流成衣R31的阻值,因此也会越来越小,一直到电容充满电后,这时RST和GND的电位就是相等了,也就是0V。
从这个过程来看,加上这个电路后,单片机系统上电后,RST引脚会先保持一小段时间的高电平而后变成低电平,这个过程就是上电复位过程。这个“一小段时间”到底是多少才合适呢?每种单片机是不一样的,51单片机手册里写的是持续时间不少于两个机器周期。每种单片机的复位电压值不完全一样,按照通常值0.7VCC作为复位电压值,复位时间的结论:t=1.2RC,其中R=4700欧姆,C=0.0000001F,那么计算出来的 t 就是0.000564s,即564us,远远大于两个机器周期(2us),在电路设计的时候一般留够余量就行。
按键复位(即手动复位)有两个过程,按下按键之前,RST的电压是0V,当按下按键后,同时电容也会在瞬间进行放电,RST的电压值变为4700VCC/(4700+18),处于高电平复位状态。松开按键后和上电复位类似,先是电容充电,后电流逐渐减少直到RST电压变为0V。按下按键的时间通常会有几百毫秒,这个时间足够复位了。按下按键的瞬间电容两端的5V电压(注意不是电源的5V和GND之间)会直接接通,此刻会有一个瞬间的大电流冲击,在局部范围内产生电磁干扰,为了抑制这个大电流所引起的干扰,这里再电容放电回路里面串入了一个18欧姆的电阻来限流。
函数调用
在一个程序的编写过程中,随着代码量的增加,如果把所有的语句都写到main函数中,一方面程序会显得比较乱,另一方面,当同一个功能需要再不同的地方执行时,就得再重复写一遍相同的语句。
如果把一些零碎的功能单独写成一个函数,则在需要它们时只需进行一些简单的函数调用,这样既有助于程序结构的清晰条理,又可以避免大量的代码重复。
在实际工程项目中,一个程序通常都是由很多个子程序模块组成,一个模块实现一个特定的功能,在C语言中,这个模块用函数表示。
一个C程序一般由一个主函数和若干个其他函数构成。
主函数可以调用其他函数,其他函数也可以相互调用,但其他函数不能调用主函数。
在51单片机程序中,还有中断服务函数,在相应的中断到来自动调用,不需要由其他函数调用。
函数调用的一般形式是:
函数名(实参列表)
函数名就是需要调用的函数名称,实参列表就是根据实际需求调用函数要传递给被调用函数的参数列表,不需要传递参数时,只保留括号就可与了,传递多个参数时参数之间要用逗号隔开。
数码管采用中断且调用函数显示
在函数调用的时候,不需要加函数类型。
调用函数与被调用函数的位置关系如下:
C语言规定:函数在被调用之前,必须先被定义或声明。意思就是说:在一个文件中,一个函数应该先定义,然后才能被调用,也就是调用函数应位于被调用函数的下方。但是作为一种通常的编程规范,一般推荐main函数写在最前面(因为起到提纲的作用),其后再定义各个功能函数,而中断函数则写在文件的最后。在文件的开头(所有函数定义之前)开辟一块区域,叫作函数声明区,用来把被调用的函数声明一下,如此,该函数就可以被随意调用了。
函数声明的时候必须加函数类型,函数的形式参数,最后加上一个分号表示结束。函数声明与函数定义行的唯一区别就是最后的分号,其他的都必须保持一致。
函数的形式参数和实际参数
- unsigned char add(unsigned char x, unsigned char y);//函数声明
- void main()
- {
- //定义参数
- unsigned char a = 1;
- unsigned char b = 2;
- unsigned char c = 0;
- //调用函数,此时a,b为实参,执行完毕后,值返回给c
- c = add(a, b);
-
- while (1);
- }
- unsigned char add(unsigned char x, unsigned char y)
- {
- unsigned char z = 0;
- z = x + y;//此时x,y是形参
- return z;//返回值z的类型就是函数的类型
- }
复制代码 关于形参和实参有以下几点需要注意:
- 函数定义中指定的形参,在未发生函数调用时,不占内存,只有函数调用时,函数的形参才会被分配内存单元。在调用结束后,形参所占的内存单元也被释放,形参是局部变量。
- 实参可以是常量,也可以是简单或者复杂的表达式,但是要求它们必须有确定的值,在调用发生时将实参的值传递给形参。
- 形参必须指定数据类型,和定义变量一样,因为它本来就是局部变量。
- 形参和实参的数据类型应该相同或者赋值兼容。和变量赋值一样,当形参和实参出现不同类型时,按照不同类型数值的赋值规则进行转换。
- 主调函数在调用函数之前,应对被调函数做原型声明。
- 实参向形参的数据传递是单向的,不能由形参再传回实参。也就是说,实参值传递给形参后,调用结束,形参单元被释放,而实参单元仍保留并且维持原值。
按键
独立按键
常用的按键电路有两种新式:
独立式按键原理图如下:
4条输入线接到单片机的I/O口上。
当按下按键K1时,+5V依次通过电阻R1和按键K1最终进入GND形成一条通路,这条线路的全部电压加到R1上,引脚KeyIn1就是一个低电平。
当松开按键后,线路断开,不会有电流通过,KeyIn1和+5V应该是等电位,是一个高电平。
因此,可以通过引脚KeyIn1这个I/O口的高低电平来判断是否有按键按下。
在单片机I/O口内部,也有一个上拉电阻。按键是接到P2口上,P2口上电默认是准双向I/O口。
现在绝大多数单片机的I/O口都是使用MOS管而非三极管,但用在这里的MOS管,其原理和三级管是一样的,因此用三极管替代MOS管来理解该电路图。
三极管
在上图中,框内的电路都是指单片机内部部分,框外的就是外接的上拉电阻和按键。当要读取外部信号的时候,单片机必须先给该引脚写“1”,也就是高电平,这样子才能正确读取到外部按键信号。
当内部输出的是高电平,经过一个反向器变成低电平,NPN三极管不会导通,单片机I/O口从内部来看,由于上拉电阻的存在,所以是一个高电平。当外部没有按键按下将电平拉低,VCC也是+5V,它们之间虽然有两个电阻,但是没有压降差,就不会有电流,线上所有位置都是高电平,这时就可以正常读取按键的状态了。
当内部输出是低电平时,经过一个反向器变成高电平,NPN三家管导通,单片机的内部I/O口是一个低电平,这时候,虽然有一个上拉电阻的存在,但是两个电阻并是并联的关系,不管按键是否按下,单片机的I/O口上输入单片机的内部的状态都是低电平,因此无法正常读取按键状态。
只要一边是低电位,电流就会顺流而下,由于只有上拉电阻,下边没有分压电阻,直接接到GND上,所以不管另一边是高电位还是低电位,电平都是低电平。
这种具有上拉的准双向I/O口,如果正常读取外部信号的状态,必须首先保证自己内部输出是1,如果内部输出是0,则无论外部信号是1还是0,这个引脚读进来的都是0。
矩阵按键
在某一个系统设计中,当需要使用很多按键时,做成独立按键会大量占用I/O口,因此引入了矩阵按键的设计。
上图一共有4组按键,暂且只看其中一组,如下:
当KeyOut1为高电平时,K1、K2、K3、K4则为独立按键,此时,KeyOut2、KeyOut3、KeyOut4需要为高电平,才能保证不影响该条线路。
独立按键的扫描
- #include <reg52.h>
- sbit ADDR0 = P1 ^ 0;
- sbit ADDR1 = P1 ^ 1;
- sbit ADDR2 = P1 ^ 2;
- sbit ADDR3 = P1 ^ 3;
- sbit ENLED = P1 ^ 4;
- sbit LED_2 = P0 ^ 0;
- sbit LED_3 = P0 ^ 1;
- sbit LED_4 = P0 ^ 2;
- sbit LED_5 = P0 ^ 3;
- sbit KEY1 = P2 ^ 4;
- sbit KEY2 = P2 ^ 5;
- sbit KEY3 = P2 ^ 6;
- sbit KEY4 = P2 ^ 7;
- void main(void)
- {
- ENLED = 0;
- ADDR3 = 1;//u3使能
- ADDR2 = 1;
- ADDR1 = 1;
- ADDR0 = 0;//Q16导通
-
- P2 = 0xF7;
- while (1)
- {
- LED_2 = KEY1;
- LED_3 = KEY2;
- LED_4 = KEY3;
- LED_5 = KEY4;
- }
- }
复制代码 绝大多数情况下,按键是不会一直按住的,所以通常检测按键的动作并不是检测一个固定的电平值,而是检测电平值的变化,即按键在按下和弹起这两种状态之间的变化,只要发生了这种变化就说明现在按键产生了动作。
程序上,可以把每次扫描到的按键状态都保存起来,当一次按键状态扫描进来的时候,与前一次的状态做比较,如果发现这两次状态不一致,则说明按键产生了动作。若上一次的状态是未按下而现在是按下,则此时按键的动作就是“按下”;若上一次的状态是按下而现在是未按下,则此时按键的动作就是“弹起”。显然,每次按键动作都会包含一次“按下”和一次“弹起”,可以任选其一来执行程序,或者两个都用,以执行不同的程序也是可以的。- #include <reg52.h>
- sbit ADDR0 = P1 ^ 0;
- sbit ADDR1 = P1 ^ 1;
- sbit ADDR2 = P1 ^ 2;
- sbit ADDR3 = P1 ^ 3;
- sbit ENLED = P1 ^ 4;
- sbit KEY1 = P2 ^ 4;
- unsigned char code LedChar[] =
- {
- 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
- 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
- };//真值表
- void main(void)
- {
- bit buckup = 1;
- char cnt = 0;
- ENLED = 0;
- ADDR3 = 1;//u3使能
- ADDR2 = 0;
- ADDR1 = 0;
- ADDR0 = 0;//Q17导通
-
- P2 = 0xF7;
- while (1)
- {
- if (buckup != KEY1)//值不同,状态改变
- {
- if (0 == buckup)//弹起状态
- {
- cnt++;
- if (cnt >= 10)//一个数码管,最大数字到9
- cnt = 0;
- P0 = LedChar[cnt];//数码管对应起真值表
- }
- }
- buckup = KEY1;//备份值
- }
- }
复制代码 51单片机有一种特殊的变量类型就是bit型。
unsigned char型是定义了一个无符号的8位的数据,它占用一个字节Byet的内存,而bit型是一位数据,只占用了1位bit内存,用法和标准C中其他的基本数据类型是一致的。
bit型的优点是节省内存空间,8个bit型才相当于一个char型变量所用空间。虽然它只有0和1两个值,但是可以表示很多信息,例如小灯的亮和灭,按键的按下和弹起等。
按键消抖
通常按键所用的开关都是机械弹性开关,当机械触点断开和闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动,如下:
按键稳定闭合时间长短由操作人员决定的,通常都会在100ms以上,刻意快速按能达到40到50ms,很难再低了。
抖动时间是由按键的机械特性决定的,一般都会在10ms以内,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。
当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或者断开稳定后再进行处理。按键消抖分为硬件消抖和软件消抖。
硬件消抖就是在按键上并联一个电容,利用电容的充放电特性对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。但是在实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中的应用并不多。硬件消抖原理图如下:
在绝大多数情况下是用软件即程序来实现消抖的。最简单的消抖原理,就是当检测到按键变化后,先等待10ms左右的延迟,让抖动消失后再进行一次按键状态的检测,如果与刚才检测到的状态相同,则可以确认按键已经稳定动作。- #include <reg52.h>
- sbit ADDR0 = P1 ^ 0;
- sbit ADDR1 = P1 ^ 1;
- sbit ADDR2 = P1 ^ 2;
- sbit ADDR3 = P1 ^ 3;
- sbit ENLED = P1 ^ 4;
- sbit KEY1 = P2 ^ 4;
- unsigned char code LedChar[] =
- {
- 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
- 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
- };//真值表
- void delay();
- void main(void)
- {
- bit buckup = 1;
- bit keyBuf = 1;
- char cnt = 0;
- ENLED = 0;
- ADDR3 = 1;//u3使能
- ADDR2 = 0;
- ADDR1 = 0;
- ADDR0 = 0;//Q17导通
-
- P2 = 0xF7;
- while (1)
- {
- if (buckup != KEY1)//值不同,状态改变
- {
- keyBuf = KEY1;
- delay();
- if (keyBuf == KEY1)
- {
- if (0 == buckup)//弹起状态
- {
- cnt++;
- if (cnt >= 10)//一个数码管,最大数字到9
- cnt = 0;
- P0 = LedChar[cnt];//数码管对应起真值表
- }
- buckup = keyBuf;
- }
- }
- }
- }
- void delay()
- {
- int i = 0;
- for(i = 0; i < 1000; ++i) ;
- }
复制代码 这个程序采用了简单的延时算法实现了消抖。现在这个是单一功能,肯定是冇问题的,但是在做实际项目的时候,程序量很大,各种状态也很多,主程序需要不停的扫描各种状态值是否发生变化,及时的进行任务调度,如果程序中间加了这种延迟操作,则很可能某一事件发生了,但是延时还在进行,等延时完再去检测就已经晚了,检测不到那个事件了。为了避免这种情况发生,要尽量缩短主循环单次时间,如果需要进行长时间的延时操作,必须用其他办法来处理。
其实除了简单的延时,还有更加优异的办法来处理按键抖动的问题。例如:启动一个定时中断,每2ms进行一次中断,扫描一次按键状态并存储起来,则连续扫描8次后,看看这8次的按键状态是否一致。8次按键的时间大概是16ms,这16ms内如果按键状态一直保持一致,那就可以确定现在按键处于稳定状态,而非抖动状态。
[code]#include sbit ADDR0 = P1 ^ 0;sbit ADDR1 = P1 ^ 1;sbit ADDR2 = P1 ^ 2;sbit ADDR3 = P1 ^ 3;sbit ENLED = P1 ^ 4;sbit KEY1 = P2 ^ 4;unsigned char code LedChar[] ={ 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};//真值表bit keyStatu = 1;void main(void){ bit buckup = 1; char cnt = 0; ENLED = 0; ADDR3 = 1;//u3使能 ADDR2 = 0; ADDR1 = 0; ADDR0 = 0;//Q17导通 EA = 1;//总中断使能 TMOD = 0x01;//设置定时器0工作 TH0 = 0xF8; TL0 = 0xCD;//设置定时器0初值 ET0 = 1;//定时器中断打开 TR0 = 1;//定时器开始定时 P2 = 0xF7; while (1) { if (buckup != keyStatu)//值不同,状态改变 { if (0 == buckup)//弹起状态 { cnt++; if (cnt >= 10)//一个数码管,最大数字到9 cnt = 0; P0 = LedChar[cnt];//数码管对应起真值表 } buckup = keyStatu; } }}void interruptTimer0 (void) interrupt 1{ static unsigned char keyBuf = 0xff; TH0 = 0xF8; TL0 = 0xCD;//设置定时器0初值 keyBuf = (keyBuf 99)//高位0不显示 { ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; // 三极管导通 P0 = LedBuff[2]; } i++; break; case 3: if (llLedShowValue > 999)//高位0不显示 { ADDR2 = 0; ADDR1 = 1; ADDR0 = 1; // 三极管导通 P0 = LedBuff[3]; } i++; break; case 4: if (llLedShowValue > 9999)//高位0不显示 { ADDR2 = 1; ADDR1 = 0; ADDR0 = 0; // 三极管导通 P0 = LedBuff[4]; } i++; break; case 5: if (llLedShowValue > 99999)//高位0不显示 { ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; // 三极管导通 P0 = LedBuff[5]; } i = 0; break; default: break; }}void keyFlash(){ unsigned char i; static unsigned char keyOut = 0; static unsigned char keyBuf[4][4] = { { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF } }; //保留当前按键状态 keyBuf[keyOut][0] = (keyBuf[keyOut][0] 9)//高位0不显示 { ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; // 三极管导通 P0 = LedBuff[1]; } i++; break; case 2: if (llLedShowValue > 99)//高位0不显示 { ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; // 三极管导通 P0 = LedBuff[2]; } i++; break; case 3: if (llLedShowValue > 999)//高位0不显示 { ADDR2 = 0; ADDR1 = 1; ADDR0 = 1; // 三极管导通 P0 = LedBuff[3]; } i++; break; case 4: if (llLedShowValue > 9999)//高位0不显示 { ADDR2 = 1; ADDR1 = 0; ADDR0 = 0; // 三极管导通 P0 = LedBuff[4]; } i++; break; case 5: if (llLedShowValue > 99999)//高位0不显示 { ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; // 三极管导通 P0 = LedBuff[5]; } i = 0; break; default: break; }}void keyFlash(){ unsigned char i; static unsigned char keyOut = 0; static unsigned char keyBuf[4][4] = { { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF } }; //保留当前按键状态 keyBuf[keyOut][0] = (keyBuf[keyOut][0] |