第5章 数码显示与键盘接口 第2~4章分别讲述了STC15单片机的片内资源与内部结构,51单片机的C51语言编程基础,单片机仿真与调试技术,分别对应单片机的硬件、软件和常用开发工具基础知识,以此为基础,可以将单片机内部集成的外围设备资源介绍与单片机应用实例相结合,探讨单片机应用系统开发技术。 单片机应用系统是由单片机和相关的外围设备共同组成的。不同的应用需求需要配置不同的外围设备,而人机对话设备通常是单片机应用系统中不可缺少的组成部分,数码显示与矩阵键盘是最常用的人机对话设备,本章专题研究其接口和编程技术。 视频 5.1数码管及其显示接口 LED七段数码显示器俗称数码管,具有价廉、可视性好、结构简单、品种丰富、接口灵活等特点,广泛应用于单片机系统中,是最常用的显示设备(输出设备)。 5.1.1数码管及其分类 七段数码显示器(或数码管)是由8只发光二极管按一定空间位置排列构成的,有多种尺寸规格,封装有1位(单码)、2位、4位、6位和8位等形式,不同的尺寸和封装的数码显示器引脚排列也不同。例如,0.5英寸1位数码管的实物和引脚封装如图5.1(a)和(b)所示,其中第3和8脚为公共端,称为数码管的位选线,其余8脚各对应内部8只LED的一个极,称为数码管的段选线a,b,…,g,dp。根据内部8个LED的连接方式,数码管可分为共阴极和共阳极两种结构,分别如图5.1(c)和(d)所示。 图5.1数码管结构图 共阳极数码管是将8只笔段LED的阳极连接在一起,如图5.1(c)所示。通常将公共阳极接高电平(一般接电源),各笔段(阴极)引脚分别串联电阻后接段驱动电路输出端。当某笔段驱动电路的输出端为低电平时,则该笔段导通并被点亮,如图5.2(a)所示。 共阴极数码管是将8只笔段LED的阴极连接在一起,如图5.1(d)所示。通常将公共阴极接低电平(一般接地),各笔段(阳极)引脚分别串联电阻后接笔段驱动电路输出端。当某笔段驱动电路的输出端为高电平时,该笔段导通并被点亮。 5.1.2数码管驱动电路 在STC15单片机应用系统中,准双向模式I/O口可直接驱动共阳极数码管; 推挽模式I/O口可直接驱动共阴极或共阳极数码管。无论是采用共阴极还是共阳极连接,都需要根据各笔段的额定导通电流和外接电源来确定各笔段所需的限流电阻。由于二极管伏安特性的离散性导致二极管并联时电流分配是不均匀的,因此二极管一般不宜并联使用,数码管应避免在共阳极(或共阴极)串接公共的限流电阻,如图5.2所示,其中图(b)接法是错误的,8只笔段LED的总电流是固定的,显示不同字符时,点亮的LED数目不同,数码管的亮度不同。 图5.2单只共阳极数码管驱动电路 要使数码管显示某个字符,必须从8个笔段驱动接口输出相应的字形编码。假设在单片机应用系统中,以单片机Pn口的Pn.0~Pn.7分别驱动数码管的a~dp笔段。当端口输出高电平时,共阴极数码管对应的笔段将被点亮; 反之,当端口输出低电平时,共阳极数码管对应的笔段将被点亮。据此可得数码管显示各种字符的字形编码如表5.1所示,字形编码也称作段码。 表5.1数码管显示字符的段码表(不带小数点) 显示字符共阴极字形码共阳极字形码显示字符共阴极字形码共阳极字形码 03FHC0H96FH90H 106HF9HA77H88H 25BHA4Hb7CH83H 34FHB0HC39HC6H 466H99Hd5EHA1H 56DH92HE79H86H 67DH82HF71H8EH 707HF8H全亮7FH80H 87FH80H全灭00HFFH 5.1.3数码管显示方式 1. 数码管静态显示 静态显示是指数码管显示某一字符时,相应的发光二极管恒定导通或恒定截止。这种显示方式的各位数码管相互独立,公共端(即位选线)恒定接地(共阴极)或接正电源(共阳极)。每个数码管的8个笔段线分别串接电阻后与一个8位I/O口相连,I/O口只要有段码输出,相应的字符即显示出来并保持不变,直到I/O口输出新的段码。采用静态显示方式,较小的电流即可获得较高的亮度,且显示驱动程序占用CPU时间少,编程简单,电路故障和软件错误易排查,但其占用的I/O口多,硬件电路复杂,成本高,只适用于显示位数较少的场合。 【例5.1】数码管静态显示。标准单片机AT89C51的I/O口是准双向口,灌电流驱动能力比较大,可直接驱动共阳极数码管。如图5.3所示,AT89C51单片机主时钟12.0MHz,用其P1和P2口直接驱动2位共阳极数码管,小数点不显示,dp笔段悬空,数码管采用静态驱动。为该硬件系统设计一软件,要求实现以下功能: 2位数码管每隔1秒向左滚屏1位,依次显示9,8,7,…,0,9,8,… 图5.3用AT89C51的P1和P2口直接驱动2只共阳极数码管(静态显示) #include #define uchar unsigned char #define uint unsigned int uchar code segTab[]={//在CODE区定义七段码译码表 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8, 0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e }; uchar data disBuf[2]={0,0};//在DATA区定义显示缓冲区 void disp_s();//声明静态显示函数 void delay(uchar); void disp_s()//静态显示函数定义 { P1=segTab[disBuf[0]&0x0f]; P2=segTab[disBuf[1]&0x0f]; } void delay(uchar td)//定义延时函数,延时100ms*td,主时钟12.0MHz { uint i; while(td--) for(i=0;i<8329;i++); } void main() { uchar j; while(1) { for(j=0;j<10;j++) {disBuf[0]=9-j;//显示缓冲区赋值 disBuf[1]=(9-j)>0?(8-j):9; disp_s();//调用静态显示函数 delay(10);//延时1秒 } } } 2. 数码管动态显示 动态显示是指多位LED数码管逐位被轮流点亮,这种逐位点亮的方式称为位扫描,2位共阳数码管动态显示电路如图5.4所示。在数码管动态显示电路中,通常将各位数码管对应的段选线并联在一起,分别串接电阻后由1个8位的I/O口控制,各个数码管的位选线(即com端)由其他的I/O口线分别控制。当数码管以动态方式显示时,各位数码管分时轮流选通,即在某一时刻只选通一位数码管,并送出该位字符对应的段码,在下一时刻选通下一位数码管,再送出该位字符对应的段码。依此规律循环即可使各位数码管显示不同的字符,虽然这些字符是在不同的时刻分别显示,但由于人眼存在视觉暂留效应,只要所有数码管的循环扫描频率达到每秒30次以上,人眼就感觉不到闪烁,达到各位数码管连续稳定地显示不同的字符的效果。例如,4位数码管动态显示方式其段选和位选输出数据流如表5.2所示,如果每位扫描时间为2.5ms,则循环扫描周期为10ms,扫描频率达到每秒100次。 表5.24位数码管动态显示方式段选和位选数据流 时间流……→时间进程→…… 扫描前个扫描周期当前扫描周期下个扫描周期 时间片……2.5ms2.5ms2.5ms2.5ms2.5ms2.5ms2.5ms2.5ms…… 段码数据 (8位)……第3位 段码第4位 段码第1位 段码第2位 段码第3位 段码第4位 段码第1位 段码第2位 段码…… 第1位选线……禁止禁止允许禁止禁止禁止允许禁止…… 第2位选线……禁止禁止禁止允许禁止禁止禁止允许…… 第3位选线……允许禁止禁止禁止允许禁止禁止禁止…… 第4位选线……禁止允许禁止禁止禁止允许禁止禁止…… 与静态显示方式相比,动态显示方式有如下特点: (1) 动态显示方式节省I/O口,限流等硬件电路也比较简单,如4位数码管,静态显示需32个I/O口,而动态显示仅需要12个I/O口。 (2) 各位数码管是轮流导通,如果不提高每位数码管导通时的瞬时电流,那么各位数码管的平时电流将下降,数码显示的亮度也下降,因此动态显示时,数码管导通的瞬时电流要提高(限流电阻降低)。 (3) CPU要依次循环扫描各个数码管,导致显示驱动程序复杂,占用较多的CPU时间。 (4) 每位数码管导通的时间必须是均等的,否则将导致数码管显示亮度不均匀,另外,单片机程序要避免在某一局部模块内循环。 【例5.2】将例5.1的数码管改为动态显示。如图5.4所示为2位共阳极数码管动态显示电路,AT89C51的P1口为段选驱动,P2.6和P2.7口为位选控制,程序修改如下: 图5.4两只共阳极数码管动态显示电路(P1段码驱动,P2.6和P2.7位扫描控制) #include #define uchar unsigned char #define uint unsigned int uchar code segTab[]={//在CODE区定义七段码译码表 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8, 0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e }; uchar code disScan[2]={~(1<<6),~(1<<7)};//在CODE区定义位选码 uchar data disBuf[2];//在DATA区定义显示缓冲区 uchar data disNum;//在DATA区定义扫描位控制变量 void disp_d();//声明动态显示函数 void delay2ms5(); void disp_d()//动态显示函数定义 {P2|=3<<6;//关闭显示器 P1=segTab[disBuf[disNum]&0x0f];//输出扫描位的段码 P2&=disScan[disNum];//输出扫描位的扫描码 disNum=(disNum+1)%2;//准备扫描下一位 } void delay2ms5() {uint i; for(i=0;i<427;i++);//2.5ms延时函数 } void main() {uchar j=0; uint tx=0; while(1) {delay2ms5();//延时2.5ms disp_d(); tx++; if(tx==400)//1s到? {tx=0; disBuf[0]=9-j;//显示缓冲区赋值 disBuf[1]=(9-j)>0?(8-j):9; j=(j+1)%10; } } } 如图5.4所示,在数码管动态显示接口电路中,不管数码管有几位,输出段码仅需要占用单片机的1个8位I/O口,当数码管位数增加时,所需的位选控制端口则相应增加。 5.1.4用74HC595扩展数码显示接口 单片机应用系统常用带锁存功能的8位移位寄存器74HC595扩展LED显示接口,IAP15W4K58S4实验板上的8位共阴极数码显示模块即采用此扩展方法,其电路如图4.36所示。为了不过多占用IAP15W4K56S4单片机的I/O资源,使用2片具有三态输出且带锁存功能的8位串入、并出或串出 图5.574HC595的内部结构与功能框图 的移位寄存器74HC595扩展了一个串行输入转16位并行输出的接口,由于74HC595的输出端Qn的拉电流(或灌电流)最大可达35mA,整个芯片的最大功率可达500mW,可直接用16位的扩展并行输出口驱动数码管显示模块。这样仅用STC15单片机的3条I/O口线(最少I/O资源),实现8位数码管显示模块的扫描控制,其中P4.0作为串行数据输出线DS、P4.3作为移位脉冲输出线SH_CP、P5.4作为移位寄存器内部数据锁存脉冲线ST_CP。图5.5给出了74HC595的内部结构与功能框图,其真值表如表5.3所示。 表5.374HC595的真值表 输入内部输出功能 SH_CPST_CPOEMRDSQ0′Q1′…Q7′Qn ××LL×LL…L不变MR低电平,移位寄存器清零 ×↑LL×LL…LLST_CP上升沿将移位寄存器零存储 ××HL×LL…L高阻OE高电平,输出悬浮(高阻态) ↑×LH×DSQ0′…Q6′不变SH_CP上升沿串行数据内部移位 ×↑LH×不变不变…不变Qn′ST_CP上升沿将移位寄存器值存储 ↑↑LH×DSQ0′…Q6′Qn′ST_CP上升沿将移位寄存器先前值存储,SH_CP上升沿串行数据内部移位 使用2片74HC595扩展16位的串入并出接口,其工作原理是数码显示模块扫描数据的串行传输,详细说明如下: (1) 所谓串行数据传输,是指仅用1条信号线DS传输信息,如要传输1字节的二进制数据,那么数据只能按位依序(先高位后低位,或先低位后高位)分时用该信号线传输,每个时间片只传输1位数据。例如,将1字节的数据0x4e以先高位后低位的顺序从P4.0端口(即DS端)串行输出,则P4.0将依次输出0,1,0,0,1,1,1,0,每位数据占用多少时间与传输速率有关。 (2) 74HC595是8位串入/并出或串出的移位寄存器,带锁存和三态输出功能,从真值表5.3的倒数第3行可见,SH_CP引脚的脉冲上升沿触发内部8级移位寄存器移位,原先内部Q0′~Q6′的数据移到了Q1′~Q7′,DS端的数据移入Q0′,即DS→Q0′→Q1′→…→Q6′→Q7′,Q7′是74HC595的串行数据的输出端,用于级联下1个74HC595芯片; 从真值表5.3的倒数第2行可见,当输出允许OE=0时,ST_CP引脚的脉冲上升沿触发内部移位数据Q0′~Q7′锁存到8位存储寄存器,并从8位三态输出口Q0~Q7输出。图4.36中,单片机的串行数据从P4.0输出,传输给U6(即左边的74HC595电路)的串行数据输入端DS,U6的串行数据输出端Q7′再传输给U5(即右边的74HC595电路)的串行数据输入端DS,U6和U5两芯片的移位脉冲引脚SH_CP并接,数据锁存脉冲引脚ST_CP并接,两片8位移位寄存器74HC595级联,构成16位串入/并出的扩展I/O口。在16位的扩展并行输出口中,U5的输出口Q7~Q0对应高8位,作为数码管显示模块的8条位选线,U6的输出口Q7~Q0对应低8位,作为数码管显示模块的段码输出端口。 (3) 如有2字节(16位)数据,其二进制数据位分别为B2.7~B2.0和B1.7~B1.0,要将该2字节数据从扩展的16位串入/并出接口输出,单片机输出的接口信号P40_HC595_DS、P43_HC595_SH、P54_HC595_ST如图5.6所示。B2.7是最早串行输出的数据位,经过16次移位(16个SH_CP脉冲作用)传输,最终从U5的Q7端口输出。 图5.6从16位串入/并出扩展接口输出2字节数据的接口信号 要控制实验板共阴极数码管显示模块以动态方式显示,核心的程序是如何将数码管扫描的段码和位选码从16位扩展并口输出。如图5.6所示,可先编写从STC单片机串行输出1字节数据的函数send_595(uchar x)如下: sbit P_595_DS=P4^0;//定义74HC595的串行数据接口 sbit P_595_SH=P4^3;//定义74HC595的移位脉冲接口 sbit P_595_ST=P5^4;//定义74HC595的输出寄存器锁存信号接口 void send_595(uchar);//声明移位输出1字节数据函数 void send_595(uchar x)//从STC单片机移位输出1字节数据 {uchar i; for(i=0;i<8;i++)//循环移位,共8位 {x<<=1;//左移1位,最高位移出到CY P_595_DS=CY;//CY从串行数据口输出 P_595_SH=1;//输出移位脉冲 P_595_SH=0; } } 先执行send_595(disScan[disNum]),再执行send_595(segTab[disBuf[disNum]]),即可将当前扫描位的位选码和段码数据分别发送到移位寄存器U5和U6的内部(未锁存),然后再从P_595_ST(即P5^4)引脚输出1个锁存脉冲ST_CP,即将当前扫描位的位选码和段码数据从16位串入/并出接口输出,完成当前位的扫描。实验板8位共阴极数码管动态扫描函数如下: uchar code segTab[]={//在CODE区定义七段码译码表 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71 }; uchar code disScan[8]={ //在CODE区定义扫描码 ~(1<<0),~(1<<1),~(1<<2),~(1<<3),~(1<<4),~(1<<5),~(1<<6),~(1<<7) }; uchar data disBuf[8];//在DATA区定义显示缓冲区 uchar data disNum;//在DATA区定义位扫描控制变量 void disp_d();//声明动态显示函数 void disp_d(void)//显示驱动函数 {send_595(disScan[disNum]);//将当前扫描位扫描码发送 send_595(segTab[disBuf[disNum]]);//将当前扫描位段码发送 P_595_ST=1;//16位数据移位后锁入输出寄存器中 P_595_ST=0; disNum=(disNum+1)%8;//调整扫描位的值,指向下一位 } 视频 5.2键盘接口电路及其消抖动 按键或其他开关元件也是单片机应用系统最常用的信息或控制量输入元件,与通用计算机系统不同,单片机系统的按键没有统一固定的规格,也没有专用接口电路来处理按键等开关控制量,需要根据应用系统的需要配置按键等开关元件。 5.2.1按键开关及其接口电路 1. 按键开关的结构与分类 单片机系统通常使用廉价的、简单的、小型化的按键等开关元件作为开关量输入装置,常用的有: 按钮、按钮式薄膜开关、触摸开关、拨码开关、数字拨码开关、拨动开关(或微动开关)、自锁按钮开关、限位开关、干簧管等。 按键或开关按工作原理可以分为两类: 一类是触点式开关按键,如机械式开关和导电橡胶式开关等; 另一类是无触点开关按键,如开关管、晶闸管和固态继电器等。按结构特点可分为按钮型开关和闸刀型开关(或拨动型)。按钮开关按其开关状态可以分为两类: 一类是常开型,即按下闭合,释放则断开; 另一类是常闭型,即按下断开,释放则闭合。 2. 按键开关与单片机的简单接口 按键开关总是通过一定的接口电路与CPU相连,以两种不同的逻辑电平表示“闭合”和“断开”两种状态,或用逻辑电平跃变的上升沿和下降沿表示“按下”和“释放”状态。CPU可以通过查询或中断的方式确定是否有按键被按下以及是哪个按键被按下,进而根据系统既定的功能执行相应的程序代码。图5.7是开关及其与单片机的简单接口电路,不论是按钮开关还是闸刀开关,与单片机的接口均可采用下拉或上拉方式。 图5.7开关及其与单片机的简单接口电路 图5.7(c)和(d)中10kΩ的电阻为上拉或下拉电阻(量级为10kΩ级),其作用是保证开关断开时,单片机的I/O端口有确定的电平,上拉式为高电平,下拉式为低电平; 300Ω电阻可以省略(量级为百欧级),其作用是I/O端口保护。采用上拉式开关接口时,开关导通时,I/O端口逻辑电平为0,断开为1,开关状态用负逻辑表示; 采用下拉式开关接口时,开关导通时,I/O端口逻辑电平为1,断开为0,开关状态用负正逻辑表示。 3. 键盘的结构和工作方式 键盘通常由一组规则排列的按键组成,根据按键的接线方式的不同可分为独立式键盘和矩阵式键盘,其结构如图5.8所示。 独立式键盘是直接用I/O端口构成简单的按键接口电路,如图5.8(a)所示,其特点是每个按键(或开关)单独占用一根I/O口线,每个按键的工作不会影响其他I/O口线的状态。独立式键盘电路配置灵活,软件结构简单,但是占用I/O端口较多,一般只在按键数量较少的场合下使用。 图5.8键盘结构 矩阵式键盘由行列控制线组成,按键(或开关)跨接在行列控制线之间,如图5.8(b)所示,行列控制线简称行线、列线。矩阵式键盘可以在使用比较少的I/O端口的情况下得到比较多的按键数量,但是软件结构相对比较复杂,一般在对按键数量要求比较多的场合下使用。 键盘电路中上拉电阻的作用是确保在没有按键按下的情况下将CPU端口上拉为高电平。实际应用中可根据单片机I/O口内部电路结构进行取舍,若单片机I/O口内部本身已经有上拉电阻,则键盘中的上拉电阻可省略。 键盘的工作方式应根据实际应用系统中CPU的工作状况而定,其选取的原则是既要保证CPU能及时响应按键操作,又不要过多地占用CPU的工作时间。通常,键盘的工作方式有3种,即编程扫描、定时扫描和中断扫描。 编程扫描方式是利用CPU完成其他工作的空余时间调用键盘扫描子程序来响应键盘输入的要求。在执行键功能程序时,CPU不再响应键输入要求,直到CPU重新扫描键盘为止。 定时扫描方式是指每隔一段时间对键盘扫描一次,根据键盘的输入要求执行相应的程序功能。在对扫描时间精度要求不高的情况下,可利用主软件延时程序的方法实现定时扫描。在对扫描时间精度要求较高的情况下,可利用定时器定时中断实现,有关定时器中断内容详见第6章。 中断扫描方式是指将键盘电路采用一定的方式与CPU的外中断信号输入引脚进行关联。当有键盘操作的时候产生相应的外中断请求,CPU在中断响应中进行相应的键盘处理,有关外中断内容详见第6章。 5.2.2按键抖动与键信号消抖动处理 机械式按钮按下或释放时,由于机械弹性作用的影响,总是伴随有一定时间的触点机械抖动,之后触点才稳定下来。在抖动期间,触点的连 图5.9机械抖动导致下拉式按键 接口信号电平的抖动 接状态、导电特性不稳定,接口信号的电平也不稳定,其抖动过程如图5.9所示,图中t1和t3为抖动时间,其时长与按钮开关的机械特性有关,一般小于20ms; t2为按键闭合的稳定期,其时间由使用者按键的动作确定,一般为几百毫秒以上,t0和t4为按键释放期。 与处理速度为微秒级的单片机相比而言,这种机械抖动是不可忽略的。如果在触点抖动期间进行按键的通断状态检测,那么可能会导致判断出错,即按键一次操作(按下或释放)被错误地认为是多次操作,从而使单片机产生错误的动作,这是不允许出现的。因此,为了避免按键触点机械抖动所导致的检测误判,必须采取相应的去抖动措施。消除按键抖动可以采用硬件方法,如在按键电路中增加RS触发器电路或RC积分电路进行消抖; 也可采用软件方法,在按键扫描程序中增加相应的代码进行消抖。前者需要增加电路成本,且设备体积也随之增大; 后者仅占用少量的CPU时间,单片机应用系统多采用软件方法消抖。 软件实现键信号去抖动处理的基本思想是: 延时法,即当CPU检测到有按键按下时,执行一个20ms左右(时长可按键类型适当调整)的延时程序后再进行按键检测,如果检测到按键仍处于被按下状态,则确认按键被按下; 反之,则认为是机械抖动引起的状态变化。对按键释放识别也是采用相同的办法处理。需要注意的是,如果单片机软件系统采用按键定时扫描方式,且扫描周期比软件去抖动的延时时间短,则需要对去抖动的延时程序做特殊的处理,否则可能会引起键盘误读错误。 【例5.3】使用实验板的以下硬件资源: (1) 单片机IAP15W4K58S4; (2) 两个连接到P3.2和P3.3端口的独立式下拉按键SW17和SW18,如图4.29所示; (3) 数码显示模块的最左边2位,如图4.32所示。 试用这些硬件资源构成一个键控计数器,要求实现以下功能: (1) 启动时系统显示50; (2) SW17键作为“加1”键,按该键一次,显示数值加1,最大计数值99; (3) SW18作为“减1”键,按该键一次,显示数值减1,最小计数值01。 #include #define uchar unsigned char #define uint unsigned int uchar code segTab[]={//在CODE区定义七段码译码表 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71 }; uchar code disScan[2]={~(1<<0),~(1<<1)}; //在CODE区定义扫描码仅最左边2位 uchar data disBuf[2];//在DATA区定义显示缓冲区 uchar data disNum;//在DATA区定义位扫描控制变量 sbit SW17=P3^2; sbit SW18=P3^3; sbit P_595_DS=P4^0;//定义74HC595的串行数据接口 sbit P_595_SH=P4^3;//定义74HC595的移位脉冲接口 sbit P_595_ST=P5^4;//定义74HC595的输出寄存器锁存信号接口 void send_595(uchar);//声明移位输出1字节数据函数 void disp_d();//声明动态显示函数 void delay2ms5(); void delay20ms(); void send_595(uchar x)//从STC单片机移位输出1字节数据 {uchar i; for(i=0;i<8;i++)//循环移位,共8位 {x<<=1;//左移1位,最高位移出到CY P_595_DS=CY;//CY从串行数据口输出 P_595_SH=1;P_595_SH=0;//输出移位脉冲 } } void disp_d()//动态显示函数定义 {send_595(disScan[disNum]);//将当前扫描位扫描码发送 send_595(segTab[disBuf[disNum]]);//将当前扫描位段码发送 P_595_ST=1;P_595_ST=0;//16位数据移位后锁入输出寄存器中 disNum=(disNum+1)%2;//准备扫描下一位,仅2位 } void delay2ms5() {uint i; for(i=0;i<2930;i++);//2.5ms延时函数 } void delay20ms() {uint i; for(i=0;i<24000;i++);//20ms延时函数 } void main() {uchar cnt=50;//定义计数变量cnt P3M1&=~(3<<2);P3M0&=~(3<<2); P4M1&=~9;P4M0&=~9;P5M1&=~16;P5M0=~16; while(1) {disBuf[0]=cnt/10;disBuf[1]=cnt%10; delay2ms5();//延时2.5ms disp_d(); SW17=1;//P3.2读之前写1 if(SW17==0)//有SW17键? {delay20ms();//延时消抖 while(!SW17);//等SW17键释放 delay20ms();//延时消抖 if(cnt<99)cnt++;//计数未达上限时,执行+1 } SW18=1;//P3.3读之前写1 if(SW18==0)//有SW18键? {delay20ms();//延时消抖 while(!SW18);//等SW18键释放 delay20ms();//延时消抖 if(cnt>1)cnt--;//计数未达下限时,执行-1 } } } 其中加黑的3个程序行是仅使用数码显示最左边2位时动态显示程序的相应修改。将代码下载到实验板中试运行,可验证系统功能正确性。 程序中使用编程扫描方式读取键状态,结合延时消抖、等待键释放等措施实现键每按下一次,增减一个计数值。但这个程序存在致命问题,即延时消抖、等待键释放与数码管动态显示的循环扫描驱动相冲突,导致键按下时,数码管显示停留在某一位上。 视频 5.3数码动态显示与键信号消抖动处理的协同 例5.3中有数码动态显示与键信号消抖动处理两个任务,前者需要定时循环,后者需要延时并等待键释放,二者的冲突源自两个任务处理程序是独立编写的,没有从多任务系统的角度考虑整个系统编程问题,本节尝试解决这一问题。与操作系统所述的多任务处理有所不同,本节所述的多任务都不是并发的,是可以采用轮询方式分时处理的。一个可在实际系统中运用得好的键信号消抖动处理程序应具备以下几个特点: (1) 能正确识别按键信息; (2) 具有软件消抖动功能; (3) 与其他程序模块(特别是数码管动态显示程序)能协同工作,不互相影响; (4) 可以给出多种按键信息,如键状态变化前后沿提取。 5.3.1多任务系统程序结构 含数码动态显示和键信号消抖动处理的多任务单片机系统流程如图5.10所示,程序每隔2.5ms启动一次主循环,完成定时读键盘保存键状态、数码显示动态扫描、完成按键状态消抖动等处理,及其他任务(如按键发出“嘀”声响、按键解释与响应等)。主程序在2.5ms延时后完成所有各项子任务,若所有任务都比较简单,则处理时间可忽略不计,可大致认为程序的循环时间就是2.5ms。主程序的循环之所以取2.5ms,是考虑到若将本例的方法移植到8位数码显示模块的系统,这样的循环扫描速度仍不会出现显示器频闪。在第6章,2.5ms延时改用定时器实现,CPU就有更多的时间处理更复杂的任务。 5.3.2键信号处理 1. 消抖动处理 所有的机械按键在按下的最初20ms内,触点未达到稳定接触,连接按键的I/O端口的电平不稳定,CPU读到的键状态不稳定,即所谓的键抖动,如图5.11所示。按键消抖动处理方法有多种,软件延时消抖动是最常用的方法,其算法原理是CPU一旦检测到键状态非零,表明有键按下,延时一段时间(不小于20ms)后,等待键状态稳定后,再读取有效的键状态。 图5.10数码动态显示与键信号 处理流程 图5.11键抖动及处理 定义全局变量edgk用于保存从I/O接口读取的键状态,此处规定键状态统一用正逻辑表示,即按下或闭合为1,释放或断开为0。edgk应是“可位寻址的”,每位对应一个键,以便主程序可对各键状态进行查询,另定义全局变量ktmr作为消抖计时器。若采用如图5.10所示的主程序循环,则系统每2.5ms调用一次“扫描键盘保存键状态”及“键状态消抖等处理”函数,其中“键状态消抖等处理”算法流程图 图5.12键状态消抖等处理 子程序流程图 如图5.12所示。用已读取的键状态判断,无键时消抖计时器清零; 当有键按下时,消抖计时器加1,由于主循环为2.5ms,因此消抖计时器的每个计数值相当于2.5ms,消抖计时器计数达到8即已延时20ms。消抖计时未达20ms,则丢弃不稳定的键状态; 若20ms计时已到,则启用新读取的稳定的键状态。 2. 键状态变化沿提取 在微机系统中,按键通常有两种用法。其一是不论按键按下时间长短,按一次只起一次作用,这种按键是键状态变化的前沿或后沿(对应按下或释放)起作用,不妨称这种按键为触发键; 其二是按键只在按下时才有作用,一旦按键释放其作用消失,这种按键是键状态(持续的导通或断开)起作用,不妨称这种键为开关键。如前所述,用变量edgk保存键状态变化沿,有变化沿为1,无变化沿为0,即edgk保存触发键的键状态; 另定义变量key用于保存键状态,它保存的是开关键的键状态。若前后两次循环的键状态不同(异或为1),且本次循环的键状态为1,则表明出现了键状态变化前沿,或发生了键按下; 若前后两次循环的键状态不同(异或为1),且前次循环的键状态为1,则表明出现了键状态变化后沿,或发生了键释放; 若前后两次循环的键状态相同(异或为0),则表明没有键动作发生。综上,键状态变化沿提取算法可表示为: 键状态变化前沿=[(前次循环键状态)^(本次循环键状态)]&(本次循环键状态) 键状态变化后沿=[(前次循环键状态)^(本次循环键状态)]&(前次循环键状态) 【例5.4】按本节所述方法,重新设计例5.3软件。 新程序代码清单如下,与例5.3比较,其中加黑的程序行为新增代码。 #include #define uchar unsigned char #define uint unsigned int uchar code segTab[]={//在CODE区定义七段码译码表 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71 }; uchar code disScan[2]={~(1<<0),~(1<<1)};//在CODE区定义扫描码 uchar data disBuf[2];//在DATA区定义显示缓冲区 uchar data disNum;//在DATA区定义位扫描控制变量 uchar bdata key;//声明变量,键状态 uchar bdata edgk;//声明变量,键变化前沿 uchar data ktmr;//定义变量,消抖计时器 uchar data kcode;//定义变量,键号 sbit EK1=edgk^0;//SW17的键状态(触发型) sbit EK2=edgk^1;//SW18的键状态(触发型) sbit P_595_DS=P4^0;//定义74HC595的串行数据接口 sbit P_595_SH=P4^3;//定义74HC595的移位脉冲接口 sbit P_595_ST=P5^4;//定义74HC595的输出寄存器锁存信号接口 void send_595(uchar);//声明移位输出1字节数据函数 void disp_d();//声明动态显示函数 void delay2ms5();//声明2.5ms延时函数 void readkey();//读键盘保存键状态函数 void keytrim();//键状态消抖动处理函数 void send_595(uchar x)//从STC单片机移位输出1字节数据 {uchar i; for(i=0;i<8;i++)//循环移位,共8位 {x<<=1;//左移1位,最高位移出到CY P_595_DS=CY;//CY从串行数据口输出 P_595_SH=1;P_595_SH=0;//输出移位脉冲 } } void disp_d()//动态显示函数定义 {send_595(disScan[disNum]);//将当前扫描位扫描码发送 send_595(segTab[disBuf[disNum]]);//将当前扫描位段码发送 P_595_ST=1;P_595_ST=0;//16位数据移位后锁入输出寄存器中 disNum=(disNum+1)%2;//准备扫描下一位 } void delay2ms5() {uint i; for(i=0;i<2930;i++);//2.5ms延时函数 } void readkey()//扫描键盘存键状态 {P3|=3<<2;//P3.3、P3.2准双向,读之前先写1 edgk=(~P3>>2)&0x03;//读键状态,求反转正逻辑 } void keytrim()//键状态消抖动,键前沿提取,求键号 {uchar temp;//本行以下为: 消抖动 if(edgk==0)ktmr=0;//无键,消抖计时器清零 else {if(ktmr<255)ktmr++;//有键,消抖计时器+1(防溢出) if(ktmr<8)edgk=0;//延时未到弃不稳定键 } temp=edgk;//本行以下为: 键前沿提取。键状态暂存 edgk=(key^edgk)&edgk;//此时key还保存着上次循环键状态 key=temp;//暂存的本次循环健状态移至key if(edgk!=0)//本行以下为: 求键编号,无键为0x10 {temp=edgk; for(kcode=0;temp&1==0;kcode++)temp>>=1; } else kcode=0x10; } void main() {uchar cnt=50;//定义计数变量cnt P3M1&=~(3<<2);P3M0&=~(3<<2); P4M1&=~9;P4M0&=~9;P5M1&=~16;P5M0=~16; while(1) {disBuf[0]=cnt/10;disBuf[1]=cnt%10; delay2ms5();//调用延时2.5ms disp_d();//调用数码动态显示扫描 readkey();//调用读键状态保存健状态 keytrim();//调用键信号消抖动等处理 if(EK1)//有SW17键? if(cnt<99)cnt++;//计数未达上限时,执行+1 if(EK2)//有SW18键? if(cnt>1)cnt--;//计数未达下限时,执行-1 } } 将代码下载到实验板中试运行,可验证系统功能的正确性。 在例5.3的新软件中,键状态消抖动和键状态变化沿提取(或触发型键状态生成)是keytrim()函数中的重要算法。为加深对键状态变化前沿提取算法的理解,下面以SW17键按动一次为例,每次调用keytrim()函数前后,键状态信号处理过程中的各种重要中间结果如表5.4所示。从表5.4可清晰地看出键状态的消抖动和键状态变化沿提取的算法实现。 表5.4SW17键按动一次,键状态消抖动的变化沿提取过程一些重要的中间结果 键动作消抖前键状态 edgk消抖计时器 ktmr消抖后键状态 edgk触发型键状态 edgk(沿提取后)开关型键状态 key(沿提取后) 无键0000 000000000 00000000 00000000 0000 SW17按下0000 000110000 00000000 00000000 0000 SW17抖动0000 000000000 00000000 00000000 0000 SW17按稳0000 000110000 00000000 00000000 0000 ……………… SW17按稳0000 000170000 00000000 00000000 0000 SW17按稳0000 000180000 00010000 00010000 0001 SW17按稳0000 000190000 00010000 00000000 0001 ……………… SW17按稳0000 0001<=2550000 00010000 00000000 0001 SW17释放0000 000000000 00000000 00000000 0000 SW17抖动0000 000110000 00000000 00000000 0000 SW17抖动0000 000120000 00000000 00000000 0000 SW17抖动0000 000000000 00000000 00000000 0000 SW17抖动0000 000110000 00000000 00000000 0000 SW17释稳0000 000000000 00000000 00000000 0000 5.4矩阵键盘及其应用 视频 当单片机应用系统的输入开关型控制量数量超过8个,采用矩阵键盘接法可以节省单片机的I/O口资源,本节讲解矩阵键盘的读键(或键的识别)方法,即键盘扫描方法。 5.4.1矩阵键盘的扫描方法 在矩阵键盘中,通常行线必须有上拉(若行线所用I/O端口没有内部弱上拉,则必须外部上拉),以保证无键按下时,行线处在高电平状态; 列线可以设置为准双向或开漏模式。为说明键盘扫描识别按键的原理,以图5.13(a)所示1行×2列最小矩阵键盘为例。矩阵键盘扫描是分时逐列识别各行与该列跨接的键的状态,扫描某一列时,仅当前列线输出低电平,其他列线均输出高电平。如图5.13(a),当扫描X列时,X列线输出低电平,Y列线输出高电平,此时键盘接口的等效电路如图5.13(b)所示,不论按键AY是否按下,行线A为的信号电平只与按键AX的状态有关,键AX按下,行线A逻辑电平0; 键AX释放,行线A逻辑电平1。若直接用行线逻辑电平表示键状态,则按键状态用负逻辑表示,即逻辑0表示键导通,逻辑1表示键断开; 也可用行线逻辑电平的“非”表示键状态,则按键状态用正逻辑表示。 图5.13矩阵键盘扫描方法 上述矩阵键盘识别方法就是所谓的行列扫描法,是单片机系统读矩阵键盘最常用的方法,以图5.8(b)所示的4×4矩阵键盘为例,其扫描过程如下: (1) 图5.8(b)所示矩阵键盘,P0口应设置成准双向口,由于有内部弱上拉,图中的电阻R57、R58、R59、R60可省略,不妨以P0.7~P0.4为列线,P0.3~P0.0为行线。 (2) 准备扫描P0.7列,给P0口送该列扫描字~(1<<7)(即0x7f),将P0.7置0,其余列置1,行线P0.3~P0.0是准双向口,作为输入口,读之前也要先写1。 (3) 从行线P0.3~P0.0读取与P0.7列线跨接的四键(KF、KE、KD、KC)的状态,若某行线逻辑电平为0,则表示该行线与P0.7列线跨接的键被按下,如KD键被按下,则行线P0.1的逻辑电平为0。 (4) 准备扫描下一列,修改列扫描字并从P0口输出,P0.6、P0.5、P0.4列扫描字分别为~(1<<6)、~(1<<5)、~(1<<4)。 (5) 重复步骤(3)和(4),直到矩阵键盘中所有的列被扫描完成后,退出键盘扫描。 键盘扫描程序的编写有3种方式: 编程扫描方式、定时扫描方式、中断扫描方式,其中定时扫描方式最为常用。 5.4.2矩阵键盘应用举例 【例5.5】按键显示系统硬件如图5.14所示,用AT89C51的P0口外接4×4的矩阵键盘,其中P0.7~P0.4为列线,P0.0~P0.3为行线; 用反相器74HC04驱动4位共阳数码显示模块的阳极,显示位扫描由P2.0~P2.3控制,P1口作为动态数码显示器的笔段驱动; P2.4经反相器驱动蜂鸣器,用于产生按键提示音。试为该按键显示系统设计软件,要求实现以下功能: 图5.14按键显示系统硬件(蜂鸣器选用: DC Operated BuzzerOutput Via Sound Card) (1) 启动时显示0123; (2) 4×4键盘对应十六进制数码0~9、A~F,当按动按键时,与该键对应的数码从数码显示器的右边滚入; (3) 按键处理程序应能与动态显示程序模块协同工作、有键消抖功能、有按键提示音、按键仅在前沿起作用(即每次按键仅在按下时发生作用)。 图5.15模块化软件 结构 为便于将键盘扫描和键状态消抖动、数码动态显示等程序移植到其他系统中,本例运用模块化编程思想,将软件分为3个模块。其一key4r4l.c,含与键信号处理相关的函数; 其二disp4ca.c,含数码动态显示; 其三main.c,为系统主程序。将3个模块添加到本例的软件工程项目中,结构如图5.15所示。 (1) key4r4l.c是定义了与键信号处理相关的函数,含4行×4列矩阵键盘扫描保存键状态、键状态消抖动、键状态变化前沿提取、求按键编号、发按键提示音“嘀”等,算法原理如前所述,代码如下: #include #define uchar unsigned char #define uint unsigned int extern uint bdata key;//声明外部变量,键状态 extern uint bdata edgk;//声明外部变量,键变化前沿 uchar data kcode;//定义变量,键编号 uchar data ktmr;//定义变量,消抖计时器 uchar data beeftmr;//定义变量,蜂鸣计时器 sbit BEEF=P2^4;//定义变量,蜂鸣器控制I/O void readkey()//扫描键盘存键状态 {uchar i,j; for(i=7;i>3;i--) {P0=~(1<>1;//kcode初值,temp=待查16个键位 for(kcode=0;temp!=0;kcode++)temp>>=1;//逐位查键,未查出kcode+1 } else kcode=0x10;//无键,kcode=0x10 } void keysound()//按键发出“嘀”的声响 {if(edgk!=0)beeftmr=40;//有变化沿,蜂鸣100ms初值 if(beeftmr==0)BEEF=0;//蜂鸣时间已到,蜂鸣关 else {beeftmr--;BEEF=1;}//蜂鸣时间未到,走时、蜂鸣开 } 与例5.4不同,本例键盘共有16键,保存键状态的全局变量edgk和key的数据类型定义为16位的unsigned int型,K0键的键状态处理在最低位,KF键的键状态处理在最高位,由于键状态edgk和key、按键编号kcode是主程序的重要输入开关型控制变量,因此这3个变量在主程序main.c模块中定义,本模块使用extern声明引用。 (2) disp4ca.c是4位共阳极数码显示器扫描驱动子程序。 #include #define uchar unsigned char uchar code segTab[]={//在CODE区定义七段码译码表 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8, 0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e }; uchar code disScan[4]={~(1<<0),~(1<<1),~(1<<2),~(1<<3)}; //4种位选码数据 uchar data disBuf[4];//定义变量,显示缓冲器 uchar data disNum;//定义变量,当前扫描位 void disp_d(void)//显示驱动函数 {P2|=0x0f;//关闭所有位 P1=segTab[disBuf[disNum]];//将当前扫描位七段码送P1口 P2&=disScan[disNum];//将当前扫描位选线信号送P2口 disNum=(disNum+1)%4;//调整扫描位的值,指向下一位 } 同理,由于显示缓冲器disBuf[4]存放的是显示器所要显示的内容,当前显示扫描位disNum也是主程序中很重要的控制变量,主程序中要频繁使用该变量,因此这2个变量在主程序main.c模块中定义,本模块使用extern声明引用。 (3) main.c是主程序,完成2.5ms定时器初始化,显示内容选择和简单的按键解释与响应,代码如下: #include #define uchar unsigned char #define uint unsigned int extern void disp_d();//声明函数,显示扫描函数 extern void readkey();//声明函数,扫描键盘存键状态 extern void keytrim();//声明函数,键状态消抖等处理 extern void keysound();//声明函数,有键发出“嘀”的声响 extern uchar data disBuf[];//声明外部变量,显示缓冲器 extern uchar data disNum;//声明外部变量,当前扫描位 extern uchar data kcode;//声明外部变量,键编号 uint bdata key;//定义变量,键状态 uint bdata edgk;//定义变量,键状态变化前沿 sbit K0=key^8;//定义变量,开关型键状态位 sbit K8=key^0; //定义变量,开关型键状态位 sbit EK0=edgk^8;//定义变量,触发型键状态位 sbit EK8=edgk^0;//定义变量,触发型键状态位 sbit BEEF=P2^4;//定义变量,蜂鸣器控制I/O void delay2ms5() {uint i; for(i=0;i<427;i++);//2.5ms延时函数 } void main(void) {BEEF=0;//关闭蜂鸣器 disBuf[0]=0x0; disBuf[1]=0x1;//默认显示"0123" disBuf[2]=0x2; disBuf[3]=0x3; while(1) {delay2ms5(); readkey();//调用扫描键盘存键状态函数 disp_d();//调用显示扫描函数 keytrim();//调用键状态消抖等处理函数 keysound();//调用有键发出“嘀”声响函数 if(kcode<16) {disBuf[0]=disBuf[1]; disBuf[1]=disBuf[2];//键号右边滚入 disBuf[2]=disBuf[3]; disBuf[3]=kcode; } } } 用μVision平台完成上述软件项目设计,经编译和调试,在Proteus中验证系统软件功能的正确性。 本章小结 键盘和数码管显示模块是单片机应用系统最重要的输入/输出通道,由于成本问题,绝大多数的系统都不采用专门的键盘和数码显示接口电路,用单片机的I/O端口直接驱动数码管动态显示,或从I/O端口直接读入键盘信息,然后再作软件消抖动处理。为此,本章主要介绍数码管结构、驱动电路、显示方式和常用的扩展数码显示接口、按键开关及其接口电路、键信号消抖动处理、矩阵键盘扫描方法等基本知识; 着重讨论数码动态显示与键信号消抖动处理等的软件协同问题,从多任务系统软件结构出发,引入触发型键状态和开关型键状态的概念,用键状态变化沿提取算法生成前沿触发型键状态或后沿触发型键状态。 习题 5.1数码管显示一个字符时,用一字节的数据表示其各笔段的驱动电平,从高位到低位各笔段排序如下: dp,g,f,e,d,c,b,a,则称该字节数据为段码。试分析数码管显示以下字符的段码。 (1) 共阳极数码管,显示字符“4.”和“H”; (2) 共阴极数码管,显示字符“7”和“L.”。 5.2如图5.4所示,2只共阳极数码管动态显示电路,如果稳定显示“87”字符,试填写以下数据表,列出CPU分时从P1口输出的段码流,和从P2.7~P2.6输出的位选码流。 时间流…→时间进程→… 扫描…前个扫描周期当前扫描周期下个扫描周期… 时间片…2.5ms2.5ms2.5ms2.5ms2.5ms2.5ms… 段码数据(P1)…… 位选码P2[7:6]…… 5.3在例5.5中,变量edgk、key、ktmr、kcode、beeftmr为什么必须定义为全局变量? 5.4在例5.5中,键状态消抖动和键状态变化沿(或触发型键状态)提取是keytrim()函数(键状态消抖等处理)的重要算法,采用后沿提取算法,为加深对该算法的理解,请以K9键按动一次为例填写下表,分析键状态信息处理过程的几个重要的中间结果。 键动作原始键状态 Edgk高字节消抖计时器 ktmr消抖后键状态 edgk高字节触发型键状态 edgk高字节开关型键状态 key高字节 无键0000 000000000 00000000 00000000 0000 K9按下0000 00101 K9抖动0000 00000 K9按稳0000 00101 ……………… K9按稳7 K9按稳8 K9按稳9 ……………… K9按稳<=255 K9释放0000 0000 K9抖动0000 0010(假设) K9抖动0000 0000(假设) K9抖动0000 0010(假设) K9抖动0000 0010(假设) K9释放0000 0000 5.5试以图5.8(b)所示的4行×4列矩阵键盘为例说明矩阵键盘的识别方法——扫描法,以及读键盘的步骤。 5.6如题图5.6所示,使用以下硬件资源: (1) 单片机AT89C51,主频率为12.0MHz; (2) 两个连接到P3.2和P3.3端口的独立式下拉按键SW1和SW2,R11和R12属性设置为DIGITAL; (3) 2位共阳极数码显示器,动态显示驱动; (4) 用P2.5驱动蜂鸣器,蜂鸣器的元件模型选用经声卡输出的直流运行式(ACTIVE库,DC Operated BuzzerOutput Via Sound Card),属性设置: 运行电压+5/负载阻抗330/频率500Hz。 试用这些硬件资源设计一个键控计数器,要求实现以下功能: (1) 启动时系统显示“50”; (2) SW1键作为“加1”键,按该键一次,显示数值加1,最大计数值99; (3) SW2作为“减1”键,按该键一次,显示数值减1,最小计数值01; (4) 键信号需有去抖动处理、键状态变化前沿提取处理,按键每按动一次,蜂鸣器发出一声时长为100ms的提示音“嘀”。 题图5.6用AT89C51构成键控计数器