第3章 LED灯和按键控制案例 微控制器原理及应用是一门实践性很强的计算机硬件开发课程,教学中需要加强学生的实践动手能力,并在教学之外辅以大量的创新应用实践,才能让学生灵活掌握技术原理的应用。大部分高校在课程实践教学中都是使用微控制器实验箱进行实践教学,存在实验箱易误操作、易损坏、设备成本高的问题,而且实验课时不足,导致实验效果不佳。针对应用型本科院校微控制器原理及应用课程实验教学存在的问题,本书设计了丰富的微控制器原理及应用实验仿真案例,将传统物理实验箱的实验内容,如硬件设计、软件编程、系统调试和效果展现全部迁移到仿真软件系统中,能够完成课程大纲要求的课内实验和课外创新设计。 基于Proteus硬件仿真工具和Keil程序设计软件构成的仿真设计环境,本章设计了与LED灯和按键控制相关的基本案例。主要内容包括流水灯、单键识别、汽车灯光模拟控制、I/O接口应用、汇编指令、键盘接口、74LS244的应用、74LS138译码器的应用、8255A的应用和RXT51应用,每个案例都提供了详尽的软硬件设计,包括仿真电路和完整的参考程序,有助于学生自主学习和掌握微控制器内部基本硬件模块的工作原理和应用编程。 3.1流水灯 3.1.1案例概述 通过8051系列微控制器的P1口接8个LED发光二极管,要求编写程序实现: (1) 从下往上每次点亮一个LED,当点亮所有LED时,全灭。再从上往下每次点亮一个LED,当点亮所有LED时,全灭; (2) 全灭、全亮2次; (3) 隔一个交替灭、亮2次; 重复上述过程。 3.1.2要求 (1) 学习微控制器I/O接口结构特点及相关寄存器的使用方法; (2) 掌握一个简单具体的微控制器项目的开发流程; (3) 熟悉Proteus ISIS软件及使用方法。 3.1.3知识点 8051微控制器内部并行I/O接口结构和寄存器的用法、C51语言应用程序设计。 3.1.4电路原理图 案例控制电路如图31所示。8个LED灯通过灌电流连接方式连接在微控制器的P1口的8个引脚上。限流电阻阻值不宜太大,阻值设定为200Ω,否则LED灯不亮。 图31流水灯电路图 3.1.5案例应用程序 #include //8051特殊功能寄存器定义 #define LED_PORT1 P1//用P1口驱动灯 void time(unsigned int ucMs);//延时单位: ms void main(void) { unsigned char ucTimes; #define DELAY_TIME 400 while(1) { LED_PORT1=0xff; //灭8个灯 time(200); //从左往右依次点亮LED for(ucTimes=0;ucTimes<8;ucTimes++){//循环点亮P1口灯 LED_PORT1=LED_PORT1-(0x80>>ucTimes); //低电平驱动灯亮 time(DELAY_TIME); } LED_PORT1=0xff; //灭P1口灯 time(200); //然后从右往左依次点亮LED for(ucTimes=0;ucTimes<8;ucTimes++){//循环点亮P1口灯 LED_PORT1=LED_PORT1-(0x01<>uctime),0x80即是右边第一个灯灭的时候的驱动数据1000 0000B,用11111111B减去它即为0111 1111B,从而实现了第一个灯亮的控制。其他每个灯依次点亮的控制原理类似,通过8次循环执行LED_PORT1=LED_PORT1(0x80>>ucTimes),实现点亮所有流水灯的效果。同理,如要实现从上往下一个一个亮,P1口的动态驱动数据应设计为LED_PORT1=LED_PORT1(0x01< Sbit BY1=P3^4;//定义按键的输入端S2键 unsigned char count;//按键计数,每按一下,count加1 unsigned char temp; unsigned char a,b; void delay10ms(void) //延时程序 { unsigned char i,j; for(i=20;i>0;i--) for(j=248;j>0;j--); } key() //按键判断程序 { if(BY1==0)//判断是否按下键盘 { delay10ms();//延时,软件去干扰 if(BY1==0)//确认按键按下 { count++;//按键计数加1 if(count==8)//计8次重新计数 { count=0;//将count清零 } } while(BY1==0); //等待本次按键松开,确保每按一次count只加1 } } move()//移动函数 { a=temp<>(8-count); P1=a|b; } main() { count=0; temp=0xfe; P1=0xff; P1=temp; while(1)//无限循环,判断按键是否按下 { key();//调用按键识别函数 move();//调用移动函数 } } 3.2.6案例分析 在该案例中,按键判断函数key()用于判断按键是否按下,每一次按下后就改变变量count的值,即加1运算。而move()函数根据变量count的最新值改变变量temp的值,再由temp的新值控制LED灯的亮灭变化。 向左移动函数move()的前3轮循环结果如下: 第一轮, count=0 temp=0xfe;// temp =11111110B a=temp<<0;// a=11111110B b=temp>>8;// b=00000000B P1=a|b;// P1=11111110B 第二轮, count=1; temp=0xfe;// temp =11111110B a=temp<<1;// a=11111100B b=temp>>7;// b=00000001B P1=a|b;// P1=11111101B 第三轮, count=2 temp=0xfe;// temp =11111110B a=temp<<2;// a=11111000B b=temp>>6;// b=00000011B P1=a|b;// P1=11111011B 以此类推,实现每按一次按键,与P1 口相连的8个LED二极管依次点亮下一个。 3.3汽车灯光模拟控制 3.3.1案例概述 (1) 通过8051系列微控制器实现汽车左转向灯、右转向灯的模拟控制; (2) 在左转向、右转向控制的基础上增加汽车故障灯,编程实现故障灯控制功能,要求故障灯控制不影响转向灯的 控制; (3) 在上述基础上增加汽车倒车灯,编程实现倒车灯控制功能,要求倒车灯打开后常亮,但是不影响故障灯和 转向灯的控制功能的控制。 3.3.2要求 (1) 学习使用微控制器各I/O接口的输入/输出功能及应用; (2) 熟悉汽车灯光模拟控制应用程序的开发; (3) 熟悉软件延时程序的编写方式。 3.3.3知识点 微控制器I/O接口的输入/输出原理、C51语言应用程序设计。 3.3.4电路原理图 案例转向模拟控制电路如图33所示。 图33汽车左转向、右转向灯模拟控制 案例中,汽车左右转向由三向开关控制,案例中使用P3的P3.0、P3.1两个引脚分别作为左、右控制开关。汽车左右转向灯通过灌电流连接方式连接在微控制器的I/O接口P2的P2.0、P2.1两个引脚上,限流电阻阻值设定为200Ω。 在汽车左转向灯、右转向灯模拟控制基础上,增加两个故障灯和两个倒车灯。使用微控制器的P3.2引脚连接倒车灯控制开关,使用P3.3 引脚连接故障灯控制开关,如图34所示。 图34故障灯、倒车灯控制 3.3.5案例应用程序 按照上述电路,相应的参考程序如下: #include //片内寄存器定义 #include //输入/输出函数库 #include //内部函数库 sbit L=P3^0; //左转向灯开关 sbit R=P3^1; //右转向灯开关 sbit B=P3^2; //倒车灯开关 sbit E=P3^3; //故障灯开关 sbit leftLed=P2^0; //左转向灯 sbit rightLed=P2^1; //右转向灯 sbit backLed=P0^0; //倒车灯 sbit errLed=P1^0; //故障灯 #define ON_leftLedleftLed=0 #define OFF_leftLedleftLed=1 #define ON_rightLedrightLed=0 #define OFF_rightLedrightLed=1 #define ON_backLedbackLed=0 #define OFF_backLedbackLed=1 #define ON_errLederrLed=0 #define OFF_errLederrLed=1 void time(unsigned int ucMs); //延时单位: ms void main (void) { while(1){//3种灯可以同时工作 if (!L){//打开左转向灯,闪烁 ON_leftLed;time(100); OFF_leftLed;time(100); } if (!R){//打开右转向灯,闪烁 ON_rightLed;time(100); OFF_rightLed;time(100); } if (!B){ON_backLed;time(100);}//打开倒车灯,常亮 elseOFF_backLed;//必须要关闭灯,否则倒车灯不能灭掉 if (!E){//打开故障灯,闪烁 ON_errLed;time(400); OFF_errLed;time(100); } } } void delay_5us(void) //延时5μs,对于22.1184MHz晶振而言,需要4个_nop_(); //对于11.0592MHz晶振而言,需要2个_nop_(); { _nop_(); _nop_(); _nop_(); _nop_(); } void delay_50us(void)//延时50μs { unsigned char i; for(i=0;i<4;i++) { delay_5us(); } } void delay_100us(void) { delay_50us(); delay_50us(); } void time(unsigned int ucMs)//延时单位: ms { unsigned char j; while(ucMs>0){ for(j=0;j<10;j++) delay_100us(); ucMs--; } } 另外,一个更简便的方法如图35所示。 图35故障灯、倒车灯控制 在图35中,两个故障灯分别由P2.2、P2.3引脚驱动控制其亮灭,两个倒车灯分别由P3.0、P3.1引脚驱动控制其亮灭。两个左转向灯分别由P1.0、P1.3引脚驱动控制其亮灭,两个右转向灯分别由P1.1、P1.5引脚驱动控制其亮灭。使用微控制器的P2.4引脚连接倒车灯控制开关,使用P3.2 引脚连接故障灯控制开关。汽车左右转向灯控制由三向开关控制。参考程序如下: #include Sbit broken=P2^4;//故障灯控制开关,常亮 sbit back=P3^2;//倒车灯控制开关,闪烁 void delay(unsigned char com) { unsigned char i,j,k; for(i=5;i>0;i--) for(j=132;j>0;j--) for(k=com;k>0;k--); } main(void){ unsigned char dat; while(1){ broken=1; P1=0xff;//第二次循环开始控制左转向灯和右转向灯灭掉,形成闪烁 P3=0xff;//第二次循环开始控制倒车灯灭掉,形成闪烁 //P2=0xff; //故障灯常亮,不要闪烁,所以不能灭掉,除非开关断开灭掉 delay(100);//灭一段时间 dat=P1&0x03;//按位逻辑与,判断P1.0、P1.1 if(dat==1)P1=0xcf; //若P1.1=0,即P1=0000 0001,P1.4=P1.5=0,右转向灯亮即P1=11001111 if(dat==2)P1=0xf3; //若P1.0=0,即P1=0000 0010,P1.2=P1.3=0,左转向灯亮即P1=11110011 if(broken==0){P2=0xf3;} else P2=0xff; //若P2.4为0,则故障灯常亮,即设置P2为11110011 //此处必须控制,若开关断开后,灯要灭掉 if(back==0)P3=0xfc; //倒车灯亮,闪烁11111100 delay(100); //控制灯亮一段时间 } } 3.3.6案例分析 按照第二种案例程序实现方法,程序实现中,3种汽车灯的控制开关都是直接接地,如果合上则开关连接的微控制器I/O接口引脚状态变为0,表明开启相应的灯光控制。初始时汽车灯全灭,通过将汽车灯引脚置为高电平灭掉灯,并延时一段时间。 broken=1; P1=0xff; P3=0xff; delay(100); 然后判断左转向灯和右转向灯的控制开关是否合上,截取P1的最低2位的二进制数据(P1&0x03)值即可分析转向方向。P1的最低2位为1,即P1.1为0,右转向灯开关合上,立即将P1.1、P1.5引脚置为0,即可点亮右转向灯。同理,P1的最低2位为2,即P1.0为0,左转向灯开关合上,立即将P1.2、P1.3引脚置为0,即可点亮左转向灯。 倒车灯控制开关的状态由if(back==0)来判断,如果合上,则设置P3=0xfc,即将P3.0、P3.1引脚置为0,即可点亮倒车灯。 故障灯控制开关的状态由if(broken==0)来判断,如果合上,则设置P2=0xf3,即将P2.2、P2.3引脚置为0,即可点亮故障灯。如果故障灯控制开关断开,则设置P2=0xff,必须控制故障灯灭掉。 3种汽车灯的控制逻辑代码执行一遍之后,延时一段时间,从而实现转向灯和倒车灯的一次亮灭变化。由于控制逻辑代码是无限循环执行,故而可以实现转向灯和倒车灯的周期性亮灭变化,即闪烁效果。 另外,3种汽车灯的控制互不干扰,而且故障灯开启后状态不应闪烁,而应处于常亮的状态,所以在故障灯控制中,通过“if(broken==0){P2=0xf3;} else P2=0xff;”实现故障灯开关断开后立即熄灯,从而不出现闪烁的效果。 3.4I/O接口应用 3.4.1案例概述 通过8051系列微控制器编程实现如下的32个LED灯的显示效果: (1) 从L1~L32按序轮流点亮一个LED,然后熄灭。每个LED灯亮时间约150ms。 (2) 在全部灯灭的情况下,按序依次从L32~L1点亮每个LED,每个灯亮间隔150ms。即点亮L32,150ms后再点亮L31,依次按顺序点亮其他灯,最后点亮L1,直到灯全部被点亮。 (3) 在全部灯亮的情况下,从L1~L32依次熄灭LED,熄灭L1,延时150ms,然后熄灭L2,延时150ms,依次按顺序熄灭其他灯,最后熄灭L32,最终全部灯被熄灭。 重复上述过程。 3.4.2要求 (1) 学习微控制器I/O接口结构特点及相关寄存器的使用方法; (2) 掌握一个简单具体的微控制器项目的开发流程; (3) 熟悉Proteus ISIS软件及使用方法。 3.4.3知识点 8051系列微控制器的并行I/O接口、I/O接口的结构和特殊功能寄存器的用法。 3.4.4电路原理图 案例控制电路如图36所示,在微控制器的4个I/O接口P1、P0、P3和P2的32个引脚上按顺序分别接一个LED灯,灯采用灌电流连接方式连接。限流电阻阻值不宜太大,阻值设定为200Ω,否则LED灯不亮。 图36I/O接口应用电路图 3.4.5案例应用程序 1. 循环移位控制方法 通过循环移位控制LED灯的亮灭,代码如下: #include void Delay() { unsigned char i,j; for(i=0;i<255;i++) for(j=0;j<255;j++); } void main() { unsigned char i; P1=0xff; P0=0xff; P3=0xff; P2=0xff; while(1) { //1 for(i=0; i<8; i++) { P1=P1-(0x01 << i); Delay(); } P1=0xff; for(i=0; i<8; i++) { P0=P0-(0x01 << i); Delay(); } P0=0xff; for(i=0; i<8; i++) { P3=P3-(0x01 << i); Delay(); } P3=0xff; for(i=0; i<8; i++) { P2=P2-(0x01 << i); Delay(); } P2=0xff; //2 for(i=0; i<8; i++) { P2=0x7f >> i; //按位右移,i=1,P2=00111111。 i=2,P2=00011111 Delay(); } for(i=0; i<8; i++) { P3=0x7f >> i; Delay(); } for(i=0; i<8; i++) { P0=0x7f >> i; Delay(); } for(i=0; i<8; i++) { P1=0x7f >> i; Delay(); } //3 for(i=0; i<8; i++) { P1=P1|(0x01 << i); //按位或,如 00000000|00000001、00000001|00000010= //00000011… Delay(); } for(i=0; i<8; i++) { P0=P0|(0x01 << i); Delay(); } for(i=0; i<8; i++) { P3=P3|(0x01 << i); Delay(); } for(i=0; i<8; i++) { P2=P2|(0x01 << i); Delay(); } } } 2. 定义数组数据控制方法 通过定义数组数据控制LED灯亮灭,代码如下: #include unsigned char code tab1[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f,0xff};//左移单个 //点亮 unsigned char code tab2[]={0x7f,0x3f,0x1f,0x0f,0x07,0x03,0x01,0x00};//右移逐个点亮 unsigned char code tab3[]={0x01,0x03,0x07,0x0f,0x1f,0x3f,0x7f,0xff};//左移逐个熄灭 void Delay() { unsigned char i,j; for(i=0;i<255;i++) for(j=0;j<255;j++); } void main() { unsigned char i; while(1) { //左移单个点亮 for(i=0;i<9;i++) { P1=tab1[i]; //单个点亮L1~L8,0xfe=11111110、0xfd=11111101 Delay(); } for(i=0;i<9;i++) { P0=tab1[i]; //单个点亮L9~L16 ,0xfe=11111110、0xfd=11111101 Delay(); } for(i=0;i<9;i++) { P3=tab1[i]; //单个点亮L17~L24 Delay(); } for(i=0;i<9;i++) { P2=tab1[i]; //单个点亮L25~L32 Delay(); } //右移逐个点亮,最后全亮 for(i=0;i<8;i++) { P2=tab2[i];//逐个点亮L32~L25,0x7f=01111111 //最后一个是0x00,P2=0x00,保持不变。8次循环后退出 Delay(); } for(i=0;i<8;i++) { P3=tab2[i];//逐个点亮L24~L17 Delay(); } for(i=0;i<8;i++) { P0=tab2[i];//逐个点亮L16~L9 Delay(); } for(i=0;i<8;i++) { P1=tab2[i];//逐个点亮L8~L1 Delay(); } //左移逐个熄灭 for(i=0;i<8;i++) { P1=tab3[i];//逐个熄灭L1~L8,0x01=00000001、0x03=00000011 Delay(); } for(i=0;i<8;i++) { P0=tab3[i];//逐个熄灭L9~L16 Delay(); } for(i=0;i<8;i++) { P3=tab3[i];//逐个熄灭L17~L24 Delay(); } for(i=0;i<8;i++) { P2=tab3[i];//逐个熄灭L25~L32 Delay(); } } } 3.4.6案例分析 循环移位方法的程序分析如下: (1) 实现从L1~L32按序轮流点亮一个LED,然后熄灭。 初始时,32个LED灯全灭,即将I/O接口P1、P0、P3 和P2的32个引脚全部设置为高电平。要实现32个LED灯逐个轮流点亮,将每个I/O接口的8位二进制状态值按顺序逐个置为0即可。以P1为例,实现语句如下: for(i=0; i<8; i++) { P1=P1-(0x01 << i); Delay(); } 其中,语句P1=P1-(0x01i)在8次循环执行过程中,P1的状态值分别为11111110、11111100、…、00000000。经过8次循环后再将P1的值置为0xff,为下次轮流点亮做准备。 (2) 在灯全部灭的情况下,实现按照顺序L32~L1依次点亮每个LED,直到点亮全部灯结束。 初始时,32个LED灯全灭,要实现按顺序依次从L32~L1点亮每个LED,通过将每个I/O接口的8位二进制状态值按顺序依次置为0即可依次点亮每个LED灯。以P1为例,实现语句如下: for(i=0; i<8; i++) { P2=P2 & (0x7f >> i); Delay(); } 其中,语句P2=P2&(0x7fi) 在8次循环执行过程中,P2的状态值分别为01111111、01111110、…、00000000。 (3) 在全部灯亮的情况下,要实现从L1~L32依次熄灭LED,最后全部灯被熄灭。 由于前边的程序运行后,P1、P0、P3和P2的值都为全0,32个灯处于全亮状态。 通过将每个I/O接口的8位二进制状态值按顺序依次置为1即可依次熄灭每个LED灯。以P1为例,实现语句如下: for(i=0;i<8;i++) { P1=P1|(0x01 << i); Delay(); } 其中,语句P1=P1|(0x01i) 在8次循环执行过程中,P1的状态值分别为00000001、00000011、…、11111111。 3.5汇编指令 3.5.1案例概述 通过8051系列微控制器控制实现功能: 每按下一次按钮,计数值加 1,通过微控制器的P2.0~P2.3引脚连接的LED灯显示出其对应的二进制计数值,引脚低电平控制LED灯亮。 3.5.2要求 (1) 熟悉ISIS模块的汇编程序编辑、编译与调试过程; (2) 完成实验的汇编语言程序的设计与编译; (3) 练习ISIS汇编程序调试方法,并最终实现实验的预期功能。 3.5.3知识点 8051微控制器的汇编指令语法、汇编应用程序设计、调试方法。 3.5.4电路原理图 案例原理图如图37所示,图中输入电路由外接在P1.3引脚的1个按钮(but)组成;输出电路由外接在P2口的8只低电平驱动的发光二极管组成。此外,还包括时钟电路、复位电路和片选电路。 图37汇编指令实验原理图 3.5.5案例应用程序 ORG 0100 Sbitbutton=P1^5; START: MOV R1,#00H //R1为0,从0开始计数 MOV A, R1 CPL A //取反指令 MOV P2,A //送入P1口,由发光二极管显示 kkk: JB button,kkk //判断but是否按下 LCALL DELAY50 //若but按下则延时50ms JB button,kkk //消抖动,再判断SP1是否真的按下 INC R1 //若确实按下则进行按键处理,即将计数内容加1后送入P1口 MOV A,R1 ; CPL A //发光二极管显示 MOV P2,A ; JNB button,$ //等待but释放 SJMP kkk //继续对but扫描 DELAY50: MOV R3,#50 //延时12.5ms L1: MOV R4,#125 DJNZ R4,$ DJNZ R3,L1 RET END 3.5.6案例分析 关于按钮的消除抖动问题,程序通过间隔12.5ms时间连续两次判断按钮是否按下来实现。 kkk: JB button,kkk //判断but是否按下 LCALLDELAY50 //若but按下则延时12.5ms JB button,kkk //消抖动,再判断SP1是否真的按下 另外,关于按键恶意按住不放的问题,程序实现方法如下: JB button,kkk//若button=1则转移到标号kkk,否则顺序执行下一条指令 JNB button,$//若button=0则转移到标号kkk,否则顺序执行下一条指令 此时,指令判断按钮是否按下,如果是按下则跳转到$(本条指令的地址),即重复执行本条指令“JNB button,$”。如果判断按钮松开,则继续执行下一条指令。该指令作用相当于是等待按钮but 释放才继续执行下一条指令。 程序编译调试方法: 通过ISIS模块的汇编程序编辑、编译与调试工具,熟悉嵌入式微控制器的汇编语言的基本开发方法和仿真调试过程。首先单击ISIS 主菜单“源代码”的“添加/移除源代码”命令,结果如图38所示。 图38添加/移除源代码 如果已有程序文件,则单击“更改”按钮,找到程序文件所在的位置并选择,之后单击“确定”按钮。如果没有程序文件,则单击“新建”按钮,建立一个文件类型为.ASM的文件,单击“打开”按钮,再单击“确定”按钮。接着单击“源代码”按钮,可以在框中看到程序文件,双击可打开编辑(注意: 这里的程序文件只能是一个,否则编译的时候不知道是哪个)。 建立源文件之后可以单击ISIS 主菜单“源代码”的“全部编译”命令进行程序编译,在弹出的对话框中可以看到自己的代码是否有误。接着可以单击ISIS 主菜单“调试”的“开始/重新启动调试”命令进行程序调试,同时可以打开“调试”菜单的最后5项,以便通过Watch Window、Registers、SFR Memory、Internal(IDATA)Memory、Source Code窗口观察分析程序执行的结果,如图39所示。 图39“调试”菜单的最后5项 在Source Code代码调试窗口中选择“单步越过命令行”按钮,即单步执行程序,可一步一步观察程序的运行结果,并进行逻辑功能分析,如图310所示。 图310代码调试窗口 3.6键盘接口应用 3.6.1案例概述 通过8051系列微控制器编程编写4×4矩阵键盘按键扫描程序。按下任意键时,数码管会显示该键的序号。 3.6.2要求 掌握微控制器控制行列式矩阵键盘的方法、接口电路设计和按键扫描应用程序设计。 3.6.3知识点 行列式矩阵键盘原理、按键识别方法和应用程序开发。 3.6.4电路原理图 1. 程序查询法 电路如图311所示。 图311程序查询键盘接口电路图 案例电路图中,微控制器的P1口的高4位控制行列键盘的行信号,P1口的低4位控制行列键盘的列信号。每一水平线(行线)与垂直线(列线)的交叉处接一个按键,由按键控制是否连通。利用这种行列矩阵结构,只需4个行线和4个列线即可组成4×4个按键的键盘。 另外,P0口连接一个七段共阴极数码管,以便实时显示当前按下的按键序号。蜂鸣器直接用P3.0引脚的高电平驱动,以便提示当前有新的按键按下。 2. 程序中断扫描法 电路如图312所示,微控制器的P1口的高4位控制行列键盘的行信号,P1口的低4位控制行列键盘的列信号。每一水平线(行线)与垂直线(列线)的交叉处接一个按键,由按键控制是否连通。另外,矩阵键盘的4个行信号引脚输入到一个与门,与门的输出连接到外部中断1的输入引脚P3.3。 图312中断扫描键盘接口电路图 3.6.5案例应用程序 为了减少键盘与微控制器接口时所占用I/O接口线的数目,在键数较多时,通常都将键盘排列成行列矩阵式。微控制器对按键的识别方法有以下两种方式: 程序控制扫描方式(即利用程序连续地对键盘进行扫描)和中断扫描方式(即按键按下引起中断,然后微控制器对键盘进行扫描。该方法能够有效提高CPU的效率)。 1. 程序控制扫描方式 该方法通过CPU执行程序连续地对键盘进行扫描,以便查询按键是否按下并识别键号。 当按下任意一个按键时,键盘扫描程序首先判断按键发生在哪一列,然后根据所发生的行附加不同的值,从而得到按键的序号。实现代码如下所示: #include #define uchar unsigned char #define uint unsigned int uchar code led[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e,0x00}; sbit BEEP=P3^0; uchar oldKeyNo=16,newKeyNo=16; void DelayMS(uint ms) { uchar t; while(ms--) { for(t=0;t<120;t++); } } void Key() { uchar T; P1=0x0f; DelayMS(1); T=P1 ^ 0x0f; switch(T)//求列号 { case 1: newKeyNo=0; break; case 2: newKeyNo=1; break; case 4: newKeyNo=2; break; case 8: newKeyNo=3; break; default: newKeyNo=16; } P1=0xf0; DelayMS(1); T=P1 >> 4 ^ 0x0f; switch(T) { case 1: newKeyNo+=0; break; case 2: newKeyNo+=4; break; case 4: newKeyNo+=8; break; case 8: newKeyNo+=12; } } void Beep()//蜂鸣器 { uchar i; for(i=0;i<100;i++) { DelayMS(1); BEEP=~BEEP; } BEEP=0; } void main() { P0=0x00; while(1) { P1=0xf0; if(P1 !=0xf0) Key(); if(oldKeyNo !=newKeyNo) { P0 =~led[newKeyNo]; Beep(); oldKeyNo=newKeyNo; } DelayMS(100); } } 2. 中断扫描方法 #include #define uchar unsigned char #define uint unsigned int char led_mod[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x58,0x5e,0x79,0x71}; //数码管段码 uchar oldKeyNo=16,newKeyNo=16; void DelayMS(uint x) { uchar i; while(x--) for(i=0;i<120;i++); } //外部中断1的中断函数 void Key() interrupt 2{ uchar Tmp; P1=0x0f; //P1高4位的行置0,低4位的4列置1 DelayMS(1); Tmp=P1^0x0f; //是异或运算符 //求列号 switch(Tmp) { case 1:newKeyNo=0;break; case 2:newKeyNo=1;break; case 4:newKeyNo=2;break; case 8:newKeyNo=3;break; default:newKeyNo=16; } P1=0xf0;//低4位的4列置0,高4位的4行置1 DelayMS(1); Tmp=P1>>4^0x0f; //求行号 switch(Tmp)//对0~3行分别附加起始值0,4,8,12 { case 1:newKeyNo+=0;break; case 2:newKeyNo+=4;break; case 4:newKeyNo+=8;break; case 8:newKeyNo+=12; } //数码管显示键号 if(oldKeyNo!=newKeyNo) { P0=led_mod[newKeyNo]; oldKeyNo=newKeyNo; } DelayMS(100); } void main(void) { P0=0x00; IT1=1; EX1=1; EA=1; P1=0xf0; while(1); } 3.6.6案例分析 1. 程序控制扫描方式 按下任意键时,数码管都会显示其键序号,扫描程序首先判断按键发生在哪一列,然后根据所发生的行附加不同的值,从而得到按键的序号。假设按键5按下,获取键号5的方法如下: 第一步,求列号。 按照该方法,先将键盘4个行引脚状态全设置为0,4个列引脚状态全设置为1,通过设置P1=0x0f实现。延时一段时间后,再次读取P1的低4位的状态值。然后根据低4位的值得到按键的列号。关键代码如下: P1=0x0f; DelayMS(1); T=P1 ^ 0x0f; 例如,按下一个键后P1的状态值0x0f变成0000xxxxB,低4位xxxx中一个为0,3个仍为1,通过异或把3个1变为0,唯一的0变为1。比如序号5的键被按下后,P1.1=0,即P1=00001101。故T=P1^0x0f=2,再按照二进制序号,获得当前按键5的列号KeyNo=1。 如果无按键按下,则当前按键的序号变量KeyNo=16,即无效键号。 case 1:KeyNo=0;break; case 2:KeyNo=1;break; case 4:KeyNo=2;break; case 8:KeyNo=3;break; default:KeyNo=16; //无键按下 第二步,求行号。 先将键盘4个行引脚状态全设置为1,4个列引脚状态全设置为0,即通过设置P1=0x0f0实现。延时一段时间后,再次读取P1的高4位的状态值,然后根据低4位的值得到按键的列号。关键代码如下: P1=0x0f0; DelayMS(1); T=P1 >> 4 ^ 0x0f; 例如,按下一个键后P1的状态值0x0f0变成xxxx0000,高4位xxxx中有一个为0,3个仍为1; 为了便于计算,再将高4位转移到低4位,并由异或操作得到改变的值。比如按键5被按下后,P1.5=0,即P1=11010000,故T=P1>>4^0x0f=2,即当前按键5的行号是2。 第三步,求键值。 通过前面获得按键的行号之后,可以根据不同的行号对第一步得到的列号加一个附加码,从而计算出当前按键的键号。按照矩阵键盘的排列序号,第0 ~ 3行分别添加的附加码值分别为0,4,8,12。关键代码如下: switch(T) { case 1: newKeyNo+=0; break; case 2: newKeyNo+=4; break; case 4: newKeyNo+=8; break; case 8: newKeyNo+=12; } 例如,按键5被按下后,P1.5=0,即P1=11010000,再将高4位转移到低4位变量T=P1>>4^0x0f=2,即行号为2,最终按键5的键值为KeyNo+4即5。 2. 中断扫描方式 本方式在程序控制扫描方式的基础上做了改进,即将中断技术引入按键扫描过程中,即只有当有按键被按下后,才引起外部中断,触发微控制器对键盘进行扫描并获取键号,否则微控制器执行其他程序,相当于键盘不存在。中断扫描键盘可以有效提升微控制器中CPU的利用率。具体如下: 主程序初始化外部中断INT1之后,再将键盘的4个行信号引脚全部设置为1,键盘的4个列信号引脚全部设置为0,即P1=0xf0。 如果有键按下,则该按键所在的行信号变为0,4个行信号输入与门,与门输出低电平送至外部中断1的输入引脚P3.3,触发外部中断1。此时暂停主程序,转而执行外部中断1的服务程序。通过将程序控制扫描获取键值的代码放在中断服务程序中,每次触发一次外部中断1,就可以获取一个最新的按键的键值并送数码管显示。 没有按键按下的时候,4个行信号引脚与4个列信号引脚全部断开。4个行信号状态保持1111,与门输出高电平送至外部中断1的输入引脚P3.3,不会触发外部中断1。这样可大大减少无按键按下时按键扫描程序耗费的CPU的时间。 3.774LS244的应用 3.7.1案例概述 8051系列微控制器通过扩展的三态缓冲器74LS244读取8个开关断开的状态,并统计断开的个数并送数码管显示。 3.7.2要求 掌握利用缓冲器来扩展微控制器的输出引脚的方法。 3.7.3知识点 三态缓冲器74LS244的硬件特点、接口设计和应用编程。 3.7.4电路原理图 案例电路原理图如图313所示。 图31374LS244应用电路图 74LS244是一种三态输出的8总线缓冲驱动器,无锁存功能,当OE为低电平时,输入到引脚Ai的信号传送到输出引脚Yi; 当OE为高电平时,Yi处于禁止高阻状态。Proteus中的74LS244是4位输出。 本案例利用74LS244作为输入口,读取8个开关状态,并将此状态通过74LS244输入到微控制器,由微控制器处理后送数码管显示开关的断开个数。 3.7.5案例应用程序 #include #include #define a74ls244 XBYTE[0xBFFF]//定义74LS244的外部RAM地址 unsigned char code LED[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88}; //共阳 //极数码管的段码表 //计算变量中为1的个数 unsigned char jisuan(unsigned char k) { unsigned char sss=0; while(k)//P0获取的开关状态的实时值,不为0就计算 { if(k%2==1) sss++; k/=2; } return sss; } int main(void) { unsigned char i,ccc,t; P1=~LED[0];//初始时显示0 while(1) { for(i=0;i<250;i++)//延时 { } t=P0;//读取74LS244的端口的状态 ccc =jisuan(t);//计算1的个数,即断开关的个数 P1=~LED[ccc];//得到有几位拨码开关断开,并查表得到段码输出显示 } } 3.7.6案例分析 将三态缓冲器74LS244作为微控制器的外部RAM设备,设置它的外部RAM地址,对该地址单元读入数据,以便获取8个开关的闭合断开状态的数据。 案例中,微控制器的读信号RD和P2.6配合,即二者输入到或非门74HC02,或非门输出作为74LS244的OE信号,从而选中74LS244工作,再通过此74HC273输出数据的段码实现显示。两片74LS244的使能端OE都接或非门74HC02的输出端,即两片74LS244的端口地址相同,可被微控制器同时选中,74LS244每个输入端Ai分别连接一个开关。拨动开关,观察数码管的变化。 案例中将74LS244的外部RAM地址0x7fff=01111111 B,即P2.6=1。P2.6与读信号RD(P3.7)送到或非门74HC02的输入端,或非门的输出为0,送到74LS244的OE,使之工作。74LS244 读取 8个开关的断开状态,通过P0口送入微控制器内部分析计算,从而将 8个开关中处于断开状态的个数显示在数码管上。 3.874LS138译码器的应用 3.8.1案例概述 通过8051系列微控制器I/O接口控制74LS138译码器的输出信号变化,并控制P1连接的8个LED灯的流水亮灭。 3.8.2要求 掌握利用74LS138译码器来扩展微控制器的输出引脚的方法。 3.8.3知识点 74LS138译码器的硬件特点、接口设计和应用编程。 3.8.4电路原理图 案例控制电路如图314所示。 图31474LS138译码器应用电路图 8个LED灯通过灌电流连接方式连接在74LS138译码器的8个输出引脚Y0~Y7上,限流电阻阻值设定为200Ω。原理图中P2口的P2.0、P2.1、P2.2引脚作为74LS138译码器的输入信号ABC,微控制器通过P2.0、P2.1和P2.2输出的8种不同编码控制74LS138译码器的输出变化。38译码器74LS138的真值表如表31所示。 表3174LS138的真值表 E1E2E3 CBA Y7~Y0 有效输出 001 000 1 1 1 1 1 1 1 0 Y0 001 001 1 1 1 1 1 1 0 1 Y1 001 010 1 1 1 1 1 0 1 1 Y2 001 011 1 1 1 1 0 1 1 1 Y3 001 100 1 1 1 0 1 1 1 1 Y4 001 101 1 1 0 1 1 1 1 1 Y5 001 110 1 0 1 1 1 1 1 1 Y6 001 111 0 1 1 1 1 1 1 1 Y7 74LS138译码器的输入地址信号CBA有8种编码,当一个选通端(E1)为高电平,另两个选通端(E2)和(E3)为低电平时,可根据输入端地址(CBA)的二进制编码控制输出端Y0~Y7对应的一个引脚以低电平译出。比如,当CBA=110时,Y6输出端输出低电平信号。 3.8.5案例应用程序 #include bitFlg=0;//T1标志位 unsigned char ccc=0; void Timer1(void) interrupt 3 using 2//定时器1中断处理函数 { ET1=0;//关闭T1中断 TH1=0x4C; TL1=0x00;//初始化定时器初值 ET1=1;//打开T1中断 ccc++; if(ccc ==20)//定时1s { Flg=1;//定时器中断标志位置位 ccc=0; } } int main(void) { TMOD=0x10;//初始化T1工作方式1 TH1=0x4C; TL1=0x00;//初始化定时器值,50ms ET1=1; TR1=1;//启动T1 EA=1;//开中断 P2=0; while(1) { while(Flg==0);//1s延时完成的标志位 Flg=0; P2=(P2+1)%8;//始终在0~7选择 } } 3.8.6案例分析 案例代码中,主要有两个并行的函数: main()和timer1()。主程序中,每隔1s就改变I/O接口P2的状态值,继而改变输入到74LS138译码器的输入信号CBA的值,最终改变74LS138译码器译码输出引脚的状态,因为只有一个输出引脚为低电平,故只有一盏灯亮。具体实现代码如下: while(1) { while(Flg==0);//1s延时完成的标志位 Flg=0; P2=(P2+1)%8;//始终在0~7选择 } 在定时器函数中,定时器硬件一次定时时间是50ms,只有完成20次50ms的定时后,定时1s完成的标志变量Flg的值才设置为1。主程序中就是通过变量Flg的值来判断1s定时是否完成。具体实现代码如下: if(ccc ==20) //定时1s { Flg=1;//定时器中断标志位置位 ccc=0; } 3.98255A的应用 3.9.1案例概述 8051系列微控制器通过8255A 的输出口A、B、C控制连接的3个LED数码管显示120s。 3.9.2要求 掌握利用8255A来扩展微控制器的并行输出引脚的方法。 3.9.3知识点 8255A的硬件原理、特点、接口设计和应用编程。 3.9.4电路原理图 案例电路原理图如图315所示。 图3158255A应用电路图 案例原理图中,采用8255A的3个输出端口A、B、C分别连接一个数码管,以便分别驱动数码管显示不同的内容。微控制器的P0口连接双向缓冲锁存器74LS373的8个输入引脚,微控制器就可通过74LS373来驱动8255A(即微控制器通过74LS373向8255A发送地址信号,P0的输出数据信号直接输出到8255A的数据输入端D0~D7)。微控制器可以分别选中8255A的某一个输出口工作,再通过此输出口输出段码驱动一个数码管显示。微控制器的读、写控制信号RD、WR分别连接8255A的RD、WR。锁存器74LS373的输出Q0、Q1分别连接8255A的地址输入端A0、A1引脚。微控制器通过P2.5引脚控制8255A的片选信号引脚CS。 74LS373是带有三态门的8D锁存器,当使能信号线OE为低电平时,三态门处于导通状态,允许D0~D7输出到Q0~Q7,当OE端为高电平时,输出三态门断开,输出线处于浮空状态。LE称为数据输入线,当74LS373用作地址锁存器时,首先应使三态门的使能信号OE为低电平,这时,当LE端输入端为高电平时,锁存器输出(Q0~Q7)状态和输入端(D0~D7)状态相同; 当LE端从高电平返回到低电平(下降沿)时,输入端(D0~D7)的数据锁入Q0~Q7的8位锁存器中。 3.9.5案例应用程序 #include #include //外部内存空间宏定义 #define dA XBYTE[0xd000]//外部寄存器定义A端口 #define dB XBYTE[0xd001]//外部寄存器定义B端口 #define dC XBYTE[0xd002]//外部寄存器定义C端口 #define control XBYTE[0xd003]//外部寄存器定义8255A的控制端口 unsigned char ccc=0;//计数器 bit flg=0;//T0标志位 unsigned char code LED[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; //共阴极 //数码管的段码表 void t1(void) interrupt 3 using 1//定时器1中断处理函数 { ET1=0;//关闭中断 TH1=0x3C;//设置定时器的初值 TL1=0xB0; ET1=1;//打开T1中断 ccc++; if(ccc ==20)//定时1s的控制 { flg=1;//定时器中断标志位置位 ccc=0; } } int main(void) { unsigned char ttt=0; TMOD=0x10; //初始化T1工作在方式1 TH1=0x3C; TL1=0xB0;//50ms定时初值 ET1=1;//开启定时器1中断 TR1=1;//启动定时器 EA=1;//开中断 control=0x80;//初始化8255A的工作方式 while(1) { //循环输出0~9 while(flg ==0); //判断1s定时时间是否完成 flg=0; ttt++; if(ttt > 120)//秒计数器最大值为120 { ttt=0;//到120s则恢复到0 } dA=LED[ttt/100];//送百位显示 dB=LED[ttt/10%10]; //送十位显示 dC=LED[ttt%10];//送个位显示 } } 3.9.6案例分析 本案例通过8051系列微控制器I/O接口P0控制8255A 的输出端口A、B、C信号变化,进一步控制A、B、C连接的3个LED数码管显示状态。 8255A可编程外围接口芯片是Intel公司生产的通用并行接口芯片,它具有A、B、C 3个并行接口,用+5V 单电源供电,能在以下3种方式下工作: 方式0,基本输入/输出方式。 方式1,选通输入/输出方式。 方式2,双向选通工作方式。 本实验中,8255A 端口A、B、C都工作在方式0 并作为输出口,输出段码以便控制数码管的显示。 电路图中,将8255A作为微控制器的外部RAM设备,所以可以设置8255A的外部RAM地址。8255A内部4个寄存器可定义4个RAM地址。本例中的8255A的外部RAM地址采用绝对宏XBYTE实现。因为8255A 的CS信号接P2.5,所以A、B、C端口的地址分别为1101 0000 0000 0000、1101 0000 0000 0001、1101 0000 0000 0010,控制口地址为1101 0000 0000 0011。另外,8255A 的CS接P2.5,写、读控制信号WR和RD分别接微控制器的P3.6和P3.7,A1、A0分别接地址锁存器74LS373的输出引脚Q1和Q0。 当地址为0xd000,即P2.5=0,信号输入到8255A 的CS,低电平有效,8255A的输出口A口可以工作,微控制器再将秒计数变量ttt的百位值送到A口,并驱动数码管显示。 当地址为0xd001,即P2.5=0,信号输入到8255A 的CS,低电平有效,8255A 的输出口B口可以工作,微控制器再将秒计数变量ttt的十位值送到B口,并驱动数码管显示。 当地址为0xd002,即P2.5=0,信号输入到8255A 的CS,低电平有效,8255A 的输出口C口可以工作,微控制器再将秒计数变量ttt的个位值送到C口,并驱动数码管显示。 在定时器函数中,定时器硬件一次定时时间是50ms,只有完成20次50ms的定时溢出中断后,定时标志变量flg的值才设置为1。具体实现代码如下: if(ccc ==20) //定时1s { flg=1;//定时器中断标志位置位 ccc=0; } 在主程序中,每隔1s时间就改变3个数码管的秒计数显示。1s定时是否完成是通过循环检测定时标志变量flg的值来判断。1s时间完成之后,就分别通过对8255A的3个外部RAM地址赋值,输出秒计数变量ttt的百、十、个位的值,驱动3个数码管显示。具体实现代码如下: while(flg ==0); //判断1s定时时间是否完成 flg=0; ttt++; dA=LED[ttt/100];//送百位显示 dB=LED[ttt/10%10]; //送十位显示 dC=LED[ttt%10];//送个位显示 3.10RTX51的应用 3.10.1案例概述 假设在8051微控制器的P2口接有8个LED,使用RTX51 Tiny,编写程序使8个LED以不同的频率闪烁。 3.10.2要求 (1) 学习使用多任务实时操作系统RTX51 Tiny软件设计方法; (2) 熟悉OS_WAIT()函数的使用。 3.10.3知识点 (1) RTX51 Tiny的多任务并发的应用编程; (2) OS_WAIT()函数的挂起、唤醒功能。 3.10.4电路原理图 案例控制电路如图316所示,8个LED灯连接在微控制器的P2口的8个引脚上,灯通过灌电流连接方式连接。限流电阻阻值设定为200Ω。 图316RTX51 Tiny的应用 3.10.5案例应用程序 #include #include //包含RTX51 Tiny的头文件 #define uintunsignedint #define uchar unsigned char sbitP20=P2^0; sbitP21=P2^1; sbitP22=P2^2; sbitP23=P2^3; sbitP24=P2^4; sbitP25=P2^5; sbitP26=P2^6; sbitP27=P2^7; void init(void)_task_ 0 { os_create_task(1); os_create_task(2); os_create_task(3); os_create_task(4); os_create_task(5); os_create_task(6); os_create_task(7); os_create_task(8); os_delete_task(0); } void Pt0(void)_task_ 1 { while(1) { P20=!P20; os_wait(K_TMO,25,0); //如果将25修改为100,则闪烁明显变慢 } } void Pt1(void)_task_ 2 { while(1) { P21=!P21; os_wait(K_TMO,35,0); } } void Pt2(void)_task_ 3 { while(1) { P22=!P22; os_wait(K_TMO,50,0); } } void t3(void)_task_ 4 { while(1) { P23=!P23; os_wait(K_TMO,95,0); } } void t4(void)_task_ 5 { while(1) { P24=!P24; os_wait(K_TMO,95,0); } } void t5(void)_task_ 6 { while(1) { P25=!P25; os_wait(K_TMO,50,0); } } void t6(void)_task_ 7 { while(1) { P26=!P26; os_wait(K_TMO,35,0); } } void t7(void)_task_ 8 { while(1) { P27=!P27; os_wait(K_TMO,25,0); } } 3.10.6案例分析 在8051系列微控制器中基于RTX51 Tiny进行多任务并发程序设计之前,需要完成3个基本准备工作,即Keil中配置实用RTX51 Tiny、工程代码中包含头文件RTX51TNY.h以及工程中添加操作系统的配置CONF_TNY.A51。 首先,在工程目标名称上右击,选择Options for Target 'Target 1'命令, 如图317所示。 图317工程设置 图318配置选项卡 Target 在如图318所示的对话框中,在Target选项卡Operating下拉列表框中选择 RTX51 Tiny,即可使用嵌入式实时操作系统RTX51精简版。 为了配置的有效和正确,必须将RTX51的配置文件复制到工程目录下,并加入到工程中。通过配置文件的设置来定制RTX51的配置。CONF_TNY.A51主要的配置参数有INT_CLOCK和TIMESHARING。 INT_CLOCK指定定时器产生中断前的指令周期数,默认值为10000。该参数用于计算定时器所设初值,即65536-INT_CLOCK。 TIMESHARING是循环设置参数,默认的值是5。该参数指定每个任务在循环任务切换前运行的滴答数,默认的值5表示多任务并发执行中每个任务切换的时间片时间长度是5个滴答。TIMESHARING为0 表示禁止循环任务切换,多任务并发执行采用挂起函数和唤醒函数切换。RTX51中一个滴答为10000个机器周期,一个机器周期等于12个时钟周期。本案例禁止循环任务分时切换,即TIMESHARING和INT_CLOCK设置如下: INT_CLOCKEQU 10000; TIMESHARING EQU0; 本案例需要建立9个任务: 初始化任务和8个LED闪烁任务,在初始化任务中建立8个LED闪烁任务,之后删除自身。使用OS_WAIT()函数等待超时进行任务切换,修改CONF_TNY.A51中的TIMESHARING禁止循环人为切换。程序中使用的挂起任务函数OS_WAIT()及其参数说明: char os_wait( unsigned char event_set,//要等待的事件 unsigned char ticks,//要等待的滴答数 unsigned intdummy);//无用参数 该函数挂起当前任务,并等待一个或几个事件,如时间间隔、超时或从其他任务和中断发来的信号。参数event_set指定要等待的事件,事件类型如表32所示,可以是表中几种事件的组合。ticks表示要等待的时间长度。本案例用到的等待事件是时间间隔K_IVL。 表323种等待事件 事件 描述 K_IVL 等待滴答值为单位的时间间隔 K_SIG 等待一个信号 K_TMO 等待一个以滴答值为单位的超时 事件可以用竖线符(|)进行逻辑或。例如,K_TMO|K_SIG指定任务等待一个超时或者一个信号。 ticks参数指定要等待的时间间隔事件(K_IVL)或超时事件(K_TMO)的定时器滴答数。参数dummy是为了提供与RTX51 Full的兼容性而设置的,在RTX51 Tiny中并不使用。 函数返回值: 当有一个指定的事件发生时,任务进入就绪态。任务恢复执行时,由返回的常数指出使任务重新启动的事件,返回值类型如表33所示。 表33返回值类型 返回值 描述 RDY_EVENT 任务的就绪标志位是被os_set_ready或isr_set_ready置位的 SIG_EVENT 收到一个信号 TMO_EVENT 超时完成,或时间间隔到 NOT_OK event_set参数的值无效 案例中关键程序原理分析: (1) 案例中的每个任务实际花很短时间,即执行完P2x=!P2x后,该任务就挂起。因此8个任务瞬间都挂起了,等待间隔时间到了之后就被唤醒。 (2) 8个任务瞬间都挂起了,但还是有先后次序的,有很短的时间差。 (3) 8个任务挂起后等待间隔时间到了之后被唤醒,每个任务按照OS_WAIT()函数中的定时时间长短依次被唤醒。 (4) 如果两个任务等待的定时时间相同,如两个定时都是等待25个滴答的任务,它们不会在同一时刻被唤醒,因为各个任务的挂起时间不同,有很小的差别。 (5) 定时25、50个滴答的任务也不会同时唤醒,因为挂起的时刻不一样。