项目一 单片机控制LED 【项目导入】 本项目将从应用的角度,通过具体的案例一步步地教会大家如何使用单片机,对单片机的四个并行口的学习和掌握是学好本门课程最基础也是最关键的要求,重点是掌握单片机并行口的功能。本项目为单片机控制LED,试图使读者掌握单片机的核心知识——并行口的使用。 【项目分析】 本项目通过分析单片机的引脚及其功能、并行口的结构特点、循环语句的结构和使用以及按键的相关知识等,逐步完成点亮一盏LED小灯、控制小灯循环亮灭、经典的流水灯控制三个任务来完成单片机控制LED的学习,进而对单片机的并行口的知识点有更深的认识。 【能力目标】 (1) 熟悉单片机软件集成开发环境与调试技巧。 (2) 画出单片机控制二极管的电路原理图。 (3) 在最小系统的基础上,搭建控制LED的电路原理图。 (4) 建立软件开发环境,编写控制程序,并编译生成目标文件。 (5) 下载到开发板,调试通过。 【知识目标】 (1) 掌握单片机的引脚及功能。 (2) 掌握单片机并行口的电路结构及特点。 (3) 掌握单片机并行口的控制方式。 (4) 掌握while循环语句和for循环语句的结构和使用。 (5) 掌握按键的去抖和使用。 任务一 点亮一盏LED小灯 【知识储备】 一、51系列单片机的引脚及功能 51系列单片机有3种封装形式:①40引脚双列直插封装(DIP);②44引脚PLCC封装;③48引脚DIP封装。下面以40引脚双列直插封装为例,简单介绍51单片机的引脚分布及功能。图1-1所示为51单片机的引脚分布图。 图1-1 51单片机引脚分布图 1.电源及时钟引脚 VCC(40脚):主电源正端,接+5V。 VSS(20脚):主电源负端,接地。 XTAL1(19脚):片内高增益反向放大器的输入端,接外部石英晶体和电容的一端。若使用外部输入时钟,该引脚必须接地。 XTAL2(18脚):片内高增益反向放大器的输出端,接外部石英晶体和电容的另一端。若使用外部输入时钟,该引脚作为外部输入时钟的输入端。 2.控制信号引脚 RST/VPD(9脚):RST是复位信号输入端,高电平有效,此端保持两个机器周期(24个时钟周期)以上的高电平时,就可以完成复位操作。RST引脚的第二功能 VPD,即备用电源的输入端。当主电源VCC发生故障降低到低电平规定值时,将+5V电源自动接入RST端为RAM提供备用电源,以保证存储在RAM中的信息不丢失,从而使复值后能继续正常运行。 ALE/ (30脚):地址锁存控制信号。在总线方式扩展时,ALE用于控制把P0口输出的低8位地址送入锁存器锁存起来,以实现低位地址和数据的分时传送,目前基本不用。 除此之外,ALE是以六分之一晶振频率的固定频率输出的正脉冲,可作为外部时钟或外部定时脉冲使用。 (29脚):总线扩展方式下,程序存储器的读允许信号输出端,目前基本不用。 /VPP(31脚):片内程序存储器选通控制端,低电平有效。当 端保持低电平时,将只访问片外程序存储器。当 端保持高电平时,执行访问片内程序存储器,但在PC(程序存储器)值超过0FFFH(对51子系列)或1FFFH(对52子系列)时,将自动转向执行片外程序存储器内的程序。 3.输入/输出引脚P0口、P1口、P2口、P3口 P0口(P0.0~P0.7,39~32脚):P0有两种工作方式。一是作为普通I/O口使用时,它是一个8位漏极开路型准双向I/O端口。每一位可驱动8个LSTTL负载。若驱动普通负载,它只有1.6 mA的灌电流驱动能力,拉负载能力仅为几十微安。高电平输出时,要接上拉电阻以增大驱动能力。当P0口作为普通输入接口时,应先向P0口锁存器写1。 P1口(P1.0~P1.7,1~8脚):P1口是唯一的单功能接口,仅能作为通用I/O接口用。它是自带上拉电阻的8位准双向I/O端口,每一位可驱动4个LSTTL负载,当P1口作为输入接口时,应先向P1口锁存器写1。 P2口(P2.0~P2.7,21~28脚):P2口是自带上拉电阻的8位准双向I/O接口,每一位可驱动4个LSTTL负载。当P2口作为输入接口时,应先向P2口锁存器写1。 P3口(P3.0~P3.7,10~17脚):P3口也是自带上拉电阻的8位准双向I/O接口,每一位可驱动4个LSTTL负载。当P3口作为输入接口时,应先向P3口锁存器写1。P3口除了作为一般准双向I/O接口使用外,每个引脚还有第二功能,如表1-1所示。 表1-1 P3口每个管脚的第二功能 P3口线 第二功能 P3.0 RXD(串行接收) P3.1 TXD(串行发送) P3.2 INT0(外部中断0输入,低电平或下降沿有效) P3.3 INT1(外部中断1输入,低电平或下降沿有效) P3.4 T0(定时器0外部输入) P3.5 T1(定时器1外部输入) P3.6 WR(外部数据RAM写使能信号,低电平有效) P3.7 RD(外部数据RAM读使能信号,低电平有效) 二、时钟电路与时序 时钟电路用于产生单片机工作所需要的时钟信号,而时序所研究的是指令执行中各信号之间的相互关系。单片机工作时,是在统一的时钟脉冲控制下一拍一拍地进行的,这个脉冲是由单片机控制器中的时序电路发出的。为了保证各部件间的同步工作,单片机内部电路应在唯一的时钟信号控制下严格地按时序进行工作。 1.时钟电路 在51单片机内部有一个高增益反相放大器,其输入端为芯片引脚XTAL1,输出端为引脚XTAL2,在芯片的外部通过这两个引脚跨接晶体振荡器和微调电容,形成反馈电路,就构成了一个稳定的自激振荡器,如图1-2所示。电路中的电容一般取30 pF左右,而晶体的振荡频率范围通常是1.2~12MHz。 2.CPU时序 振荡器产生的时钟周期经脉冲分配器,可产生多相时序,如图1-3所示。51单片机的时序单位共4个,从小到大依次是:节拍、状态、机器周期和指令周期。 图1-2 时钟电路 图1-3 时序发生器 时序单位之间的关系如图1-4所示,CPU执行一条指令的时间称为指令周期,一般由若干个机器周期组成。指令不同,所需要的机器周期数也不同,有单周期指令、双周期指令和三周期指令之分。而一个机器周期由6个状态周期组成,一个状态又包括两个节拍。 图1-4 各时序单位之间的关系示意图 三、复位电路 复位是单片机的初始化操作,只要给单片机的RST引脚加上2个机器周期以上的高电平信号,就可以使单片机复位。复位的主要功能是把PC初始化为0000H,使单片机从0000H单元开始执行程序。除了进入系统的正常初始化外,当由于程序运行出错或操作错误使系统处于死锁状态时,也需要按复位键重新启动,因而复位是一个很重要的操作方式。单片机本身一般是不能自动进行复位的(在热启动时本身带有看门狗复位电路的单片机除外),必须配合相应的外部电路才能实现。单片机的复位都是靠外部电路实现的,分为上电自动复位和手动按键复位。复位电路的设计与原理将在以后的学习中通过最小系统电路设计给予说明。 除PC之外,复位操作还对其他一些寄存器有影响,它们的复位状态如表1-2所示。复位后除(SP)=07H,P0、P1、P2、P3为0FFH外,其他寄存器都为0。 表1-2 复位时各寄存器的状态 寄 存 器 复位状态 寄 存 器 复位状态 PC 0000H TMOD 00H ACC 00H TCON 00H B 00H TH0 00H PSW 00H TL0 00H SP 07H TH1 00H DPTR 0000H TL1 00H P0~P3 0FFH SCON 00H IP 00H SBUF 不定 IE 00H PCON 0XXXXXXB 结合图1-16所示的下面单片机最小系统电路图,介绍单片机的两种复位过程。 1) 上电复位 单片机上电时,首先要做的事情就是执行初始化操作,而初始化的条件就是要在RST引脚上提供一个超过两个机器周期的高电平。上电初始,回路中有电容C17、电阻R20,此时电容还没被充电,两端没有电压,根据分压原理,VCC提供的5V电压全部被分配在电阻R20上,此时RST引脚为高电平,而后随着电容的逐步充电,电压逐渐向电容C17上转移,R20上的电压就会逐渐变小,这段R20上电压逐渐变小的时间正好可以使RST引脚的电压维持在高电平两个机器周期以上,满足使单片机复位的条件。当电容充满电时,则5V电压几乎全被备份到电容C17上,电阻R20上的电压接近零,RST引脚为低电平,单片机开始正常工作。 2) 手动复位 单片机在工作过程中,如果某种原因导致“程序跑飞”或“死机”,在没有看门狗电路的情况下,手动复位是最简单的办法。当复位按键RST被按下时,VCC的电压全部作用在R20上,此时复位引脚RST端变为高电平,电容C17迅速放电,按键RST松开,该支路断开,由于C17放空,所以VCC的电压又全部承载在R20身上,复位引脚RST端电压重新变为高电平,C17又开始充电,电压又逐渐向C17上转移,此时单片机重复上电复位过程。 四、工程建立、编译的基本步骤 工程的建立是基于编译软件的,本书采用的编译环境是Keil,将在后续章节做详细介绍。这里只是简单演示工程建立、编译链接的基本过程。 (1) 启动Keil C软件,进入如图1-5所示界面。 图1-5 初始界面 (2) 单击菜单栏中的Project→New Project选项,在弹出的对话框中输入工程名称1s_xunhuan,并选择合适的路径(通常为每个工程建一个同名或同义的文件夹,这样便于管理),单击“保存”按钮,这样就创建了一个新的工程文件,文件名为1s_xunhuan.uv2,如图1-6所示。 图1-6 新建工程 (3) 单击“保存”按钮后,弹出如图1-7所示对话框,选择单片机的厂家和型号。 图1-7 器件选择对话框 (4) 选择完器件后,单击“确定”按钮,弹出如图1-8所示对话框。单击“是”按钮,建立工程完毕。 图1-8 询问对话框 (5) 单击菜单栏中的File→New选项,或单击工具栏中的New 图标,新建一个空白的文本文件,如图1-9所示。 图1-9 新建文本文件 (6) 单击File→Save选项,或单击工具栏中的Save 图标,保存文件。汇编保存成A51或ASM格式,C语言保存成.c格式。这里我们采用C语言编写,所以保存成.c格式。文件名称一般与工程名称相同,如图1-10 所示。 图1-10 文件保存类型 (7) 单击“保存”按钮,文本对话框变为如图1-11所示的样子。 图1-11 命名后的文本 (8) 右击“工程管理窗口”中的Source Group 1,从弹出的快捷菜单中选择Add Files to Group “Source Group 1”,弹出如图1-12所示对话框。选择1s_xunhuan.c文件,单击Add按钮,然后单击Close按钮,如图1-13所示,可以看到1s_xunhuan.c文件已经被添加到Source Group 1。接下来就可以在文本编辑框中编写程序了。 图1-12 添加文件到组对话框 (9) 在文本框中编写完程序,编译即可生成目标文件,如图1-14所示。 图1-13 添加文件完成后的界面 图1-14 编译结果 上述操作简单演示了单片机软件开发的大体流程,希望通过本案例可以使读者对单片机软件开发的基本过程以及编译环境的基本应用有一个基本认识,以后进行较复杂单片机软件系统设计时,基本是按照这个流程进行的。 五、P0口的位电路结构及特点 P0口的位电路结构,如图1-15所示。当P0作为普通I/O来用时,P0口为一个准双向口。所谓准双向口就是在读数据之前,先要向相应的锁存器做写1操作的I/O口。从图1-15中可以看出,在读入端口数据时,由于输出驱动FET并接在引脚上,如果T2导通,就会将输入的高电平拉成低电平,产生误读。所以在端口进行输入操作前,应先向端口锁存器写1,使T2截止,引脚处于悬浮状态,变为高阻抗输入。 图1-15 P0口的位电路结构 但在实际应用中好像并不复杂,我们只需要知道,当想改变端口的状态时,只需要把相应的数字状态值赋给P0口,和数字电路中一样,0代表低电平,1代表高电平。 六、控制端口的名称依据 为什么控制端口的标识符写作P0?这个问题就像1+1为什么等于2一样,在头文件中设置并规定了端口的名称,其实就是把端口的实际地址对应一个比较容易记忆的名称。 关于这些名称在头文件中的定义,在程序的开头有#include这条语句,它的含义我们已经很清楚了。但对于reg51.h文件的内部我们却还不了解,下面来看看该文件的内容后。 /*---------------------------------------------------------------------- AT89X51.H Header file for the low voltage Flash Atmel AT89C51 and AT89LV51. Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc. All rights reserved. --------------------------------------------------------------------------*/ #ifndef __AT89X51_H__ #define __AT89X51_H__ /*------------------------------------------------ Byte Registers ------------------------------------------------*/ sfr P0 = 0x80; sfr SP = 0x81; sfr DPL = 0x82; sfr DPH = 0x83; sfr PCON = 0x87; sfr TCON = 0x88; sfr TMOD = 0x89; sfr TL0 = 0x8A; sfr TL1 = 0x8B; sfr TH0 = 0x8C; sfr TH1 = 0x8D; sfr P1 = 0x90; sfr SCON = 0x98; sfr SBUF = 0x99; sfr P2 = 0xA0; sfr IE = 0xA8; sfr P3 = 0xB0; sfr IP = 0xB8; sfr PSW = 0xD0; sfr ACC = 0xE0; sfr B = 0xF0; /*------------------------------------------------ P0 Bit Registers ------------------------------------------------*/ sbit P0_0 = 0x80; sbit P0_1 = 0x81; sbit P0_2 = 0x82; sbit P0_3 = 0x83; sbit P0_4 = 0x84; sbit P0_5 = 0x85; sbit P0_6 = 0x86; sbit P0_7 = 0x87; /*------------------------------------------------ PCON Bit Values ------------------------------------------------*/ #define IDL_ 0x01 #define STOP_ 0x02 #define PD_ 0x02 /* Alternate definition */ #define GF0_ 0x04 #define GF1_ 0x08 #define SMOD_ 0x80 /*------------------------------------------------ TCON Bit Registers ------------------------------------------------*/ sbit IT0 = 0x88; sbit IE0 = 0x89; sbit IT1 = 0x8A; sbit IE1 = 0x8B; sbit TR0 = 0x8C; sbit TF0 = 0x8D; sbit TR1 = 0x8E; sbit TF1 = 0x8F; /*------------------------------------------------ TMOD Bit Values ------------------------------------------------*/ #define T0_M0_ 0x01 #define T0_M1_ 0x02 #define T0_CT_ 0x04 #define T0_GATE_ 0x08 #define T1_M0_ 0x10 #define T1_M1_ 0x20 #define T1_CT_ 0x40 #define T1_GATE_ 0x80 #define T1_MASK_ 0xF0 #define T0_MASK_ 0x0F /*------------------------------------------------ P1 Bit Registers ------------------------------------------------*/ sbit P1_0 = 0x90; sbit P1_1 = 0x91; sbit P1_2 = 0x92; sbit P1_3 = 0x93; sbit P1_4 = 0x94; sbit P1_5 = 0x95; sbit P1_6 = 0x96; sbit P1_7 = 0x97; /*------------------------------------------------ SCON Bit Registers ------------------------------------------------*/ sbit RI = 0x98; sbit TI = 0x99; sbit RB8 = 0x9A; sbit TB8 = 0x9B; sbit REN = 0x9C; sbit SM2 = 0x9D; sbit SM1 = 0x9E; sbit SM0 = 0x9F; /*------------------------------------------------ P2 Bit Registers ------------------------------------------------*/ sbit P2_0 = 0xA0; sbit P2_1 = 0xA1; sbit P2_2 = 0xA2; sbit P2_3 = 0xA3; sbit P2_4 = 0xA4; sbit P2_5 = 0xA5; sbit P2_6 = 0xA6; sbit P2_7 = 0xA7; /*------------------------------------------------ IE Bit Registers ------------------------------------------------*/ sbit EX0 = 0xA8; /* 1=Enable External interrupt 0 */ sbit ET0 = 0xA9; /* 1=Enable Timer 0 interrupt */ sbit EX1 = 0xAA; /* 1=Enable External interrupt 1 */ sbit ET1 = 0xAB; /* 1=Enable Timer 1 interrupt */ sbit ES = 0xAC; /* 1=Enable Serial port interrupt */ sbit ET2 = 0xAD; /* 1=Enable Timer 2 interrupt */ sbit EA = 0xAF; /* 0=Disable all interrupts */ /*------------------------------------------------ P3 Bit Registers (Mnemonics & Ports) ------------------------------------------------*/ sbit P3_0 = 0xB0; sbit P3_1 = 0xB1; sbit P3_2 = 0xB2; sbit P3_3 = 0xB3; sbit P3_4 = 0xB4; sbit P3_5 = 0xB5; sbit P3_6 = 0xB6; sbit P3_7 = 0xB7; sbit RXD = 0xB0; /* Serial data input */ sbit TXD = 0xB1; /* Serial data output */ sbit INT0 = 0xB2; /* External interrupt 0 */ sbit INT1 = 0xB3; /* External interrupt 1 */ sbit T0 = 0xB4; /* Timer 0 external input */ sbit T1 = 0xB5; /* Timer 1 external input */ sbit WR = 0xB6; /* External data memory write strobe */ sbit RD = 0xB7; /* External data memory read strobe */ /*------------------------------------------------ IP Bit Registers ------------------------------------------------*/ sbit PX0 = 0xB8; sbit PT0 = 0xB9; sbit PX1 = 0xBA; sbit PT1 = 0xBB; sbit PS = 0xBC; sbit PT2 = 0xBD; /*------------------------------------------------ PSW Bit Registers ------------------------------------------------*/ sbit P = 0xD0; sbit FL = 0xD1; sbit OV = 0xD2; sbit RS0 = 0xD3; sbit RS1 = 0xD4; sbit F0 = 0xD5; sbit AC = 0xD6; sbit CY = 0xD7; /*------------------------------------------------ Interrupt Vectors: Interrupt Address = (Number * 8) + 3 ------------------------------------------------*/ #define IE0_VECTOR 0 /* 0x03 External Interrupt 0 */ #define TF0_VECTOR 1 /* 0x0B Timer 0 */ #define IE1_VECTOR 2 /* 0x13 External Interrupt 1 */ #define TF1_VECTOR 3 /* 0x1B Timer 1 */ #define SIO_VECTOR 4 /* 0x23 Serial port */ #endif 七、端口的输出控制方式 1.端口字节操作 51单片机端口的电平状态只有两种:高电平1,低电平0。如图1-17所示,8个LED阳极接电源VCC,如果想让8个LED点亮,则LED的阴极应该为低电平,相反,如果想让它们熄灭,则应给它们高电平。因此当想初始化LED全灭时,我们需要让P0端口的状态为高电平,方法就是执行“P0=0xff;”(0xff是十六进制表示法,相当于二进制的0b00000000)这样一条赋值语句。同理,要点亮小灯L1时,需要的P0口的电平状态是,P00为低电平,其余7个端口为高电平,即执行语句“P0=0xfe;”。这样我们就用单片机点亮了你学习历程中的第一盏小灯。 2.端口的位操作 点亮小灯只有这么一种方式吗?当然不是。刚才操作中发现一个问题,在执行“P0=0xfe;”时,其实只是想改变端口P00的状态,但这里实际上对每个端口都进行了赋值操作,只不过是给其他7个端口赋了跟原来相同的值,这种操作方式叫作字节操作。其实可以只对P00这一个端口进行操作,这种只对P0口中的一个端口进行操作的方式叫作位操作。如下例所示: #include #define LED P0 //宏定义,LED以后就等同于P0 sbit L1=P0^0;//位定义,L1就相当于P00端口 void main() { LED=0xff;//先初始化小灯为熄灭状态 L1=0;//点亮L1 while(1); } 上例跟以往有什么不同?这里在改变P00口的状态时,我们使用的是位操作方式,但值得注意的是,如果想使用类似L1这样的名称,必须在程序的前面用位定义的方式(通过sbit定义)进行声明,否则的话,就必须严格采用头文件里对端口P00规定的名称,即P0_0,对应的语句“L1=0;”应写成“P0_0=0;”。 八、关键的while(1) 为什么程序的最后都有一句“while(1);”呢?从C语言的角度来看,while(1)其实就是一个死循环。其实这一句是为了防止程序跑飞故意加上的。假设没有这一句,当操作程序执行到最后一句时,没有后续的语句要执行,但单片机的PC指针仍然会执行加1操作,就有可能使PC指针指向ROM中的一个空白地址,这就是所谓的程序跑飞。加上while(1)之后,就能有效地避免这种情况。当单片机没有任何事情做时,那就让它在这里等待吧,至少PC指针不会乱跑。 有的同学不免会问,如果想再让它干点事情(执行某段程序)时,应该怎么办,还有没有办法?其实单片机的工作机制早就考虑到了这一点,那就是中断机制,这个我们会在后续的章节中介绍。 【任务实践】 1) 工作任务描述 设计出能够驱动8个LED发光二极管工作的基本电路,并点亮P0端口控制的小灯L1。 2) 工作任务分析 项目一中单片机的最小工作系统已经搭建成功,可以在最小系统的基础上,用单片机的P0端口的8根引脚分别连接1个LED,LED的阴极接单片机的端口,阳极通过一个1k?的限流电阻连接电源VCC(VCC为+5V),然后让P0口输出对应电平状态即可。 3) 工作步骤 (1) 搭建最小系统电路,设计LED驱动电路。 (2) 了解单片机端口的输出控制方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 最小系统电路如图1-16所示,LED驱动电路如图1-17所示。 图1-16 单片机最小系统电路 图1-17 P0口控制发光二极管电路 程序示例: #include #define LED P0 //宏定义,LED以后就等同于P0 void main() { LED=0xff;//先初始化小灯为熄灭状态 LED=0xfe;//点亮L1 while(1); } 请读者用之前所讲的知识将程序进行编译,仿真以及下载。 任务二 控制小灯的亮灭 【知识储备】 一、软件延时之delay() 如果把亮的状态和灭的状态放到一个循环体里会怎么样?答案是,它们会交替地重复执行,如果在这两个状态之间加上一定的时间间隔,那么小灯就会按照预期的结果以一定的时间间隔亮灭,这个时间间隔就是“delay();”。我们看到delay()函数的内部其实就是两个for循环的嵌套,通过重复执行某些无意义的语句,消耗单片机的工作时间,从而达到通过软件的方式实现延时的目的。软件延时实现起来非常容易,但有两点不足:①C语言实现的软件延时无法达到一个精确的延时时间;②软件延时的方式确实大大地消耗了CPU的工作时间,降低了工作效率。在后续章节中,会介绍一种更为精确、有效的设置延时的方式,那就是通过定时器来实现,这里先不多做介绍。 二、Keil软件的调试方法及技巧 前面已经学习了如何建立工程、配置工程、编译链接,并获得目标代码,但这只表示源代码没有语法错误,至于源程序中存在的其他错误,必须通过调试才能发现并解决。事实上,除极简单的程序外,绝大多数程序都要通过反复调试才能得到正确的结果。因此,调试是软件开发中的重要环节,熟练掌握程序的调试技巧可以大大提高工作效率。下面将详细介绍调试的方法。 1.Keil软件的调试方法 当工程成功地进行编译链接后,使用菜单命令Debug→Start/Stop Debug Session或直接单击工具栏上的 按钮或使用快捷方式,即可以进入调试环境,如图1-18所示。 进入调试状态后,工程管理窗口自动跳转到寄存器窗口,Debug菜单中原来不可使用的命令现在已经可以使用了,调试工具栏如图1-19所示。 调试程序时,一些程序行必须满足一定的条件才能够被执行。如有键盘输入的程序,要求键盘输入某个指定值时才执行对应程序的情况,有中断产生将执行中断程序,串口接收到数据等。这些条件往往是异步发生的或难以预先设定,这种情况使用单步执行的方法是很难调试的,此时就要用到程序调试中的另一种非常重要的方法——断点调试。 断点调试的方法有很多种,常用的是在某一行程序处设置断点,设置好断点后可以全速运行程序,一旦执行到该行程序即停止执行。可以在此时观察有关变量或寄存器的值,以确定问题所在。在程序行设置/移除断点的方法是将光标定位于需要设置断点的程序行,使用菜单命令Debug→Insert/Remove BreakPoint 设置或移除断点(也可以双击该行实现同样的功能);Debug→Enable/Disable BreakPoint是开启或暂停光标所在行的断点功能;Debug→Disable All BreakPoint是暂停所有断点;Debug→Kill All BreakPoint是清除所有的断点设置。 图1-18 调试仿真环境 图1-19 调试工具栏 2.常用调试窗口介绍 1) 功能寄存器查看窗口 如图1-20所示是功能寄存器查看窗口,寄存器页包括当前的工作通用寄存器组和部分专用寄存器、系统寄存器组,有一些是实际存在的寄存器,如 A、B、SP、DPTR、PSW 等,有一些是实际中并不存在或虽然存在却不能对其操作的,如sec、PC、Status等。每当程序执行到某个寄存器操作时,该寄存器会以高亮(蓝底白字)显示,单击,然后按下 F2 键,即可修改该值。 图1-20 功能寄存器查看窗口 2) 查看窗口 查看窗口是很重要的窗口,寄存器窗口中仅可以观察到工作寄存器和有限的寄存器,如A、B、DPTR等,如果需要查看存储器地址的值或者在观察程序中定义变量的值,就要借助于查看窗口了。查看窗口有5个标签页,分别是调用栈(Call Stack)、局部变量(Locals)、查看1(Watch 1)、存储器1(Memory 1)以及符号(Symbols)。 (1) Call Stack:显示程序执行过程中对子程序的调用情况,如图1-21所示。 图1-21 Call Stack窗口 (2) Locals:显示程序调试过程中当前局部变量的值,如图1-22所示。 (3) Watch 1:显示程序中已经设置的任何变量(如变量、结构体、数组等)在调试过程中的当前值,如图1-23所示。 图1-22 Locals窗口 图1-23 Watch 1窗口 (4) Memory 1:显示系统中各种内存中的值,该窗口将单独介绍。 (5) Symbols:显示调试器中可用的符号信息,如图1-24所示。 注意:在Locals、Watch1窗口中右击鼠标可以改变局部变量或观察点的值,使其按十六进制(HEX)或十进制(Decimal)方式显示,还可以通过选中后按F2键来改变其值。 图1-24 Symbols窗口 3) 存储器窗口 如图1-25所示,存储器窗口可以显示系统中各种内存的值。DATA是可直接寻址的片内数据存储区,XDATA是外部数据存储区,IDATA是间接寻址的片内数据存储区,CODE是程序存储区。通过在 Address编辑框内输入“字母:单元地址”即可显示相应内存值。其中字母可以是 C、D、I、X,其代表的含义如下。 (1) C:代码存储空间。 (2) D:直接寻址的片内存储空间。 (3) I:间接寻址的片内存储空间。 (4) X:扩展的外部 RAM 空间。 图1-25 存储器窗口 数字代表想要查看的地址。例如,输入D:0x00即可观察到地址 0 开始的片内 RAM 单元值。输入C:0x00即可显示从0开始的ROM单元中的值,可以查看程序的二进制代码。该窗口的显示值可以以各种形式显示,如十进制、十六进制、字符型等,改变显示方式的方法是右击数字,在弹出的快捷菜单中选择相应方式,如图1-26所示。 图1-26 显示方式选择 Decimal选项是一个开关,如果选择该选项,则窗口中的值将以十进制的形式显示,否则按默认的十六进制方式显示。 Unsigned 和 Signed分别代表无符号形式和有符号形式。例如,Unsigned的4个选项:Char、Int、Short、Long,分别代表以字符方式显示、整型数方式显示、短整型数方式显示、长整型数方式显示,默认以Unsigned Char型显示。 选择以上任一选项,内容将以整数形式显示。如选择Ascii选项则以字符型式显示;选择Float选项将相邻4字节组成浮点数形式显示;选择Double 选项则将相邻8字节组成双精度形式显示。 当需要更改某一内存单元的数值时,只需双击需要改变的数值的单元,直接从键盘输入应改的数值即可。 4) 反汇编窗口 使用View菜单中的Disassembly Windows命令可以打开反汇编窗口,如图1-27所示。反汇编窗口用于显示目标程序的汇编语言指令、反汇编代码及其地址。当采用单步或断点方式运行程序时,反汇编窗口的显示内容会随指令的执行而滚动。 图1-27 反汇编窗口 反汇编窗口可以使用右键功能,将鼠标指针移至反汇编窗口并右击,可以弹出如图1-28所示的快捷菜单。 其中Mixed Mode选项采用高级语言与汇编语言混合方式显示;Assembly Mode选项采用汇编语言方式显示;Address Range选项用于显示用户程序的地址范围;Show Disassembly at Address...可以设定跳转的某个地址显示汇编代码;Set Program Counter用来设置PC指针的值;Run to Cursor line表示执行到光标所在行;Inline Assembly...选项用于程序调试中的“在线汇编”;Load Hex or Object file...用于重新装入Hex 或Object文件进行调试。 5) 命令窗口 可以通过在命令窗口中输入命令来调用?Vision的调试器的调试功能,如查看和修改变量或寄存器的内容,如图1-29所示。具体命令信息可以查看帮助文件中的μVision4 User’s Guide→Debugging→Command Window。 图1-29 命令窗口 6) 串口窗口 μVision4提供了几个串行窗口,包括调试浏览器、串行输入和输出,可以不需要外部硬件来模拟CPU的UART。单击View → Serial Windows命令,出现图1-30所示的菜单。另外串行输出,还可以使用命令窗口中的assign命令分配给PC的COM端口。 图1-30 串行窗口下拉菜单 注意:μVision4中printf函数的输出信息需通过Debug(printf) Viewer窗口显示,当然使用前必须先配置好串口。 3.通过Peripherals菜单观察仿真结果 为了能够比较直观地了解单片机中定时器、中断、输入/输出端口、串行口等各模块及相关寄存器的状态,Keil提供了一些外围接口对话框,通过Peripherals菜单选择,如图1-31所示。目前51型号繁多,不同型号的单片机具有不同的外围集成功能,μVision4通过内部集成器件库实现对各种单片机外围集成功能的模拟仿真,它的选项内容会根据选用的单片机型号而有所变化。针对51系列单片机有Interrupt(中断)、I/O-Ports(输入/输出端口)、Serial(串行口)、Timer (定时器/计数器)四个功能模块。 (1) 单击Peripherals菜单栏中的Interrupt选项,将弹出如图1-32所示的中断系统观察窗口,用于显示51单片机中断系统状态。 选中不同的中断源,Selected Interrupt栏中将出现与之相对应的中断允许和中断标志位的复选框,通过对这些状态位的置位和复位操作,很容易实现对单片机中断系统的仿真。对于具有多个中断源的单片机如8052等,除了如上所述几个基本中断源之外,还可以对其他中断源如监视定时器(Watchdog Timer)等进行模拟仿真。 图1-32 中断系统观察窗口 (2) 单击Peripherals菜单中的I/O-Ports选项,用于仿真80C51单片机的并行I/O接口Port0、Port1、Port2、Port3.选中Port1后将弹出如图1-33所示窗口。其中,P1栏显示51单片机P1口锁存器状态,Pins栏显示P1口各个引脚的状态,仿真时各位的状态可根据需要进行修改。 (3) 单击Peripherals菜单中的Serial选项,用于仿真80C51单片机的串行口,弹出如图1-34所示窗口。 ① Mode下拉列表框用于选择串行口的工作方式,可以选择8位移动寄存器、8位/9位可变波特率UART、9位固定波特率UART等不同工作方式。选定工作方式后,相应特殊工作寄存器SCON和SBUF的控制字也显示在窗口中。通过对特殊控制位SM2、REN、TB8、RB8、TI和RI复选框的置位和复位操作,很容易实现对51单片机内部串行口的仿真。 ② Baudrate栏用于显示串行口的工作波特率,SMOD位置位是将波特率加倍。 (4) 单击Peripherals菜单中的Timer→Timer0出现图1-35所示定时/计数器0的外围接口界面。 图1-33 P1口观察窗口 图1-34 串行口观察窗口 图1-35 定时/计数器观察窗口 ① Mode下拉列表框用于选择工作方式,可选择定时器或计数器方式,选定工作方式后相应特殊工作寄存器TCON和TMOD的控制字也显示在窗口中,TH0和TL0用于显示计数值,T0 Pin和TF0复选框用于显示T0引脚和定时/计数器溢出状态。 ② Control栏用于显示和控制定时/计数器的工作状态(Run或Stop),TR0、GATE和INT0复选框是启动控制位,通过对这些状态位的置位和复位操作,很容易实现对80C51单片机内部定时/计数器仿真。 其他窗口将在以下的实例中介绍。 调试时,通常我们仅在单步执行时才观察变量或寄存器的值,当程序全速运行时,变量的值是不更新的,只有在程序运行停止后,才会将这些值最新的变化反映出来。但是在一些特殊场合下,需要在全速运行时观察变量或寄存器值的变化,这时可以单击View→Periodic Window Update(周期更新窗口)命令。选中该选项,将会使程序模拟执行的速度变慢。 下面通过实例简单介绍调试的基本过程。还是以本项目的控制LED的例子来介绍,进入调试环境,打开P1口观察窗口,如图1-36所示。 图1-36 调试环境观察窗口 单击菜单栏中的Peripherals—I/O Ports—Port 1命令,打开P1口的观察窗口,在“P1=P1<<1;”代码行双击,即可创建断点标志。然后按键盘上的F5键,或用鼠标单击全速运行快捷键,观察程序的执行情况。可以看到程序在执行到断点处时马上停止,并显示当前各个寄存器、端口,以及程序中的变量的状态。由图1-36可以看出,端口P1的第0位为高电平,其余位为低电平。再按F5键,可以看到P0口各位依次轮流显示高电平。待程序执行完一个周期后,观察程序仿真结果,如果达到预期的目标,就可以将程序下载到目标板上,观察实际运行结果。如有问题可以再调试程序,直到实际运行情况达到预期目标。 【任务实践】 1) 工作任务描述 设计出能够驱动8个LED发光二极管工作的基本电路,并控制小灯L1以一定的时间间隔亮灭。 2) 工作任务分析 电路图在图1-17的基础上不变,然后让P0口P00每隔一段时间输出电平状态翻转一次即可。 3) 工作步骤 (1) 设计LED驱动电路。 (2) 了解单片机端口的输出控制方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 程序示例: #include #define LED P0 //宏定义,LED以后就等同于P0 sbit L1=P0^0;//位定义,L1就相当于P00端口 void delay(); void main() { LED=0xff;//先初始化小灯为熄灭状态 while(1) { L1=0;//点亮L1 delay(); L1=1;//熄灭 delay(); } } void delay( )//延时子程序 { uint a,b; for(a=0;a<=350;a++) for(b=0;b<=1000;b++); } 任务三 经典的流水灯 【知识储备】 方便的intrins.h头文件 对于_crol_( )函数的用法和功能可以直接用Keli的帮助文档中的案例来说明,如下所示: #include void test_crol (void) { char a; char b; a = 0xA5; b = _crol_(a,3); /* b now is 0x2D */ } 这里实际上是通过调用“b=_crol_(a,3);”把无符号字符型变量a循环往左移动了3位,然后把移位后的值赋给了无符号字符型变量b,这个函数之所以能够在这里被调用,原因就在于#include ,_crol_( )函数的声明就在该头文件里。intrins.h头文件的内容如下: /*---------------------------------------------------------------------- INTRINS.H Intrinsic functions for C51. Copyright (c) 1988-2004 Keil Elektronik GmbH and Keil Software, Inc. All rights reserved. ----------------------------------------------------------------------*/ #ifndef __INTRINS_H__ #define __INTRINS_H__ extern void _nop_ (void);//空操作 extern bit _testbit_ (bit);//判位指令 //无符号字符型变量循环右移 extern unsigned char _cror_ (unsigned char, unsigned char); //无符号整型变量循环右移 extern unsigned int _iror_ (unsigned int, unsigned char); //无符号长整型变量循环右移 extern unsigned long _lror_ (unsigned long, unsigned char); extern unsigned char _crol_ (unsigned char, unsigned char); extern unsigned int _irol_ (unsigned int, unsigned char); extern unsigned long _lrol_ (unsigned long, unsigned char); extern unsigned char _chkfloat_(float); extern void _push_ (unsigned char _sfr); extern void _pop_ (unsigned char _sfr); #endif 该头文件中函数对应的功能,其实与51单片机的汇编指令中对应相同功能的指令,比如RLC循环左移、RRC循环右移、NOP空操作、JBC判位指令、PUSH进栈、POP出栈等,因此这些函数场被称为本征函数。 【任务实践】 1) 工作任务描述 设计出能够驱动8个LED发光二极管工作的基本电路,并控制8个LED小灯,从L1开始以一定的时间间隔循环亮灭。 2) 工作任务分析 电路图在图1-17的基础上不变,然后让P0口循环输出点亮状态。 3) 工作步骤 (1) 设计LED驱动电路。 (2) 了解单片机端口的输出控制方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 程序示例: #include #include #define uint unsigned int #define uchar unsigned char #define led P0 void delay() { uint a,b; for(a=0;a<=350;a++) for(b=0;b<=1000;b++); } void main() { uchar temp; led=0xff; temp=0xfe; while(1) { led=temp; temp=_crol_(temp,1); delay(); } } 任务四 独立按键控制LED的亮灭 【知识储备】 一、端口的数据输入 端口的数据输入问题实际上就是CPU如何确认单片机I/O口引脚的电平状态,就是最简单的独立按键问题。如按键KEY1被按下时,则单片机引脚P15接地,此时引脚的电平应为低电平。CPU如果想知道有没有键被按下,唯一的办法就是把该引脚的状态读进来再判断是1还是0,这就是语句if(KEY1==0)的由来。当然还可以一次读入P0口的整个状态,例如“a=P0;”把P0的状态读进来存放在变量a中,能这么做的原因是,51单片机的端口既可以位操作也可以字节操作。 二、按键的去抖动 目前,无论是按键或键盘大部分都是利用机械触点的合、断作用。由于弹性作用的影响,机械触点在闭合及断开瞬间均有抖动过程,从而使电压信号也出现抖动,如图1-37所示。抖动时间的长短与开关机械特性有关,一般为5~10 ms。按键的稳定闭合时间由操作人员的按键动作所决定,一般为十分之几秒至几秒时间。为了保证CPU对键的一次闭合仅作一次键输入处理,必须去除抖动影响。 通常去抖动影响的方法有硬件去抖和软件去抖两种。在硬件方面,通常采取在键输出端加RS触发器或双稳态电路构成去抖动电路,如图1-38所示。图1-38中用两个与非门构成一个RS触发器。当按键未按下时,输出为1;当键按下时,输出为0。此时即使按键因抖动而产生瞬时断开(抖动跳开B),只要按键不返回原始状态A,双稳态电路的状态不改变,输出保持为0,就不会产生抖动的波形。也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出波形为正规的矩形波。 图1-37 键闭合及断开时的电压波动 图1-38 双稳态消抖电路 如果按键较多,则常用软件方法去抖动,即检测出键闭合后执行一个延时程序产生5~10 ms的延时,等前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。当检测到按键释放后,也要给5~10 ms的延时,待后沿抖动消失后才能转入该键的处理程序,从而去除了抖动影响。本例就是采用软件去抖。 【任务实践】 1) 工作任务描述 设计出能够使用按键控制驱动LED发光二极管亮灭的基本电路,当按下独立按键KEY1时L1亮,按下KEY2键点亮L2,按下KEY3键点亮L3,按下KEY4键点亮L4。 2) 工作任务分析 LED的驱动电路如图1-17所示不变,在此基础上在P15、P16、P17、P33端口各连接一个按键,按键另一端接地,然后单片机通过识别哪一个按键被按下,来控制对应的LED小灯点亮。该任务不仅要用到I/O端口的输出控制,同时也包含端口的输入状态的读取。 3) 工作步骤 (1) 设计按键控制LED驱动电路。 (2) 了解单片机端口的输入/输出控制方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 独立按键电路如图1-39所示。 程序示例: #include #include #define uchar unsigned char #define uint unsigned int #define LED P0 //独立按键定义 sbit KEY1=P1^5; sbit KEY2=P1^6; sbit KEY3=P1^7; sbit KEY4=P3^3; void delay(uint times);//延时函数声明 void main() { while(1) { if(KEY1==0)//判断key1有没有被按下 { delay(10);//延时10ms,去抖动 if(KEY1==0)//再次判断,防止误操作 LED=0xfe;//点亮L1 } if(KEY2==0) { delay(10); if(KEY2==0) LED=0xfd; } if(KEY3==0) { delay(10); if(KEY3==0) LED=0xfb; } if(KEY4==0) { delay(10); if(KEY4==0) LED=0xf7; } } } //带参数延时子程序 void delay(uint times) { uint a,b; for(a=0;a<=times;a++) for(b=0;b<=1000;b++); } 项目二 单片机控制数码管显示系统设计 【项目导入】 在单片机应用系统中,显示器是最常用的输出设备。常用的显示器有:数码管(LED)、液晶显示器(LCD)和荧光屏显示器。其中以数码管显示最便宜,而且它的配置灵活,与单片机接口简单,广泛用于单片机系统中。本项目将逐步引导学生学会如何设计与驱动数码管显示模块。 【项目分析】 本项目通过掌握单片机的静态显示、动态显示等知识,让数码管显示不同的数字以及字母,结合项目1所学的知识,建立软件开发环境,编写控制程序,并编译生成目标文件,下载到开发板,调试完成单片机控制数码管显示系统的设计。 【能力目标】 (1) 在最小系统的基础上,搭建控制数码管显示电路原理图。 (2) 编写控制数码管静态显示、动态显示的程序。 (3) 调试控制数码管静态显示、动态显示的程序。 【知识目标】 (1) 熟悉数码管基本结构。 (2) 掌握数码管静态显示和动态显示的具体方法。 (3) 掌握利用74HC595进行端口扩展的方式。 任务一 让数码显示0 【知识储备】 一、数码管结构及显示原理 LED显示器是单片机应用系统中常用的显示器件,它由若干个发光二极管组成。当发光二极管导通时,相应的一个点或一个笔画发亮,控制不同组合二极管导通,就能显示出各种字符,如表2-1所示。常用的LED显示器是七段位数码管,这种显示器有共阳极和共阴极两种。如图2-1所示,共阴极数码管公共端接地,共阳极数码管公共端接电源。每段发光二极管需要5~10 mA的驱动电流才能正常发光,一般需加限流电阻控制电流的大小。 本例程序中,我们将字型码数据0x3f通过write_HC595( )函数送到数码管的段选端,数码管在保证位选端选通的情况下,就会显示数字0。同理,当我们把其他字型数据送入时,数码管也会显示对应的字型。 (a) 外形 (b) 共阴极 (c) 共阳极 图2-1 七段数码管结构图 表2-1 七段LED字型码 显示字符 共阳极字码 共阴极字码 显示字符 共阳极字码 共阴极字码 0 C0H 3FH B 83H 7CH 1 F9H 06H C C6H 39H 2 A4H 5BH D A1H 5EH 3 B0H 4FH E 86H 79H 4 99H 66H F 8EH 71H 5 92H 6DH P 8CH 73H 6 82H 7DH U C1H 3EH 7 F8H 07H L C7H 38H 8 80H 7FH H 89H 76H 9 90H 6FH “灭” 00H FFH A 88H 77H 二、移位寄存器74HC595 74HC595拥有一个8位移位寄存器和一个存储器以及三态输出功能。移位寄存器和存储器拥有独立的时钟。数据在移位脉冲的上升沿作用下移位到寄存器中,在锁存脉冲的上升沿作用下输入存储寄存器中去。如果两个时钟连在一起,则移位寄存器总是比存储寄存器早一个脉冲。移位寄存器有一个串行数据移位输入(DS)和一个串行数据输出(Q7')以及一个异步复位端(低电平有效)。存储寄存器有一个8位并行三态的总线输出,当使能OE时(为低电平),存储寄存器的数据输出到总线,引脚图如图2-2所示。 这里我们仅结合该电路中74HC595的应用方式给大家做简单介绍,详细内容请参照595的数据手册。该电路中主要利用595的串入并出功能以及相应驱动能力。595的工作过程就是将DS端的数据在移位脉冲的作用下依次向前移位,当8个移位脉冲之后,8位数据全部移到了595的内部,再通过一个锁存脉冲将数据锁存在输出端口,即数码管的段选端。如图2-2所示,单片机与595相连的只有3个引脚,即数据输入端DS、移位脉冲输入端STCP和锁存脉冲输入端SHCP,移位数据送到DS端,因此单片机所要做的事情也就很清楚了。 (1) 将段选数据的每一位分拣出来,依次送给端口P2_5。 (2) 在P2_7端口上模拟产生8个移位脉冲。 (3) 在P2_6端口上模拟产生1个锁存脉冲。 上述三件事情由send()函数来实现。 三、段选和位选 数码管实际上是由8个LED小灯按照一定的顺序摆放组成,因此通常称为7段数码管,再加上dp正好8段。所谓段选实际上就是选通这8段的数据端,段选数据就是让数码管能够显示什么内容的数据。而位选是控制数码管的公共端,即决定数码管能不能显示的控制端。简单地说就是段选决定显示什么,位选决定能不能显示。 图2-3所示电路中,当开关拨到下面时,573的CE端接地,573处于选通状态,另外573的锁存端LE始终接VCC,因此573此时的工作状态处于跟随状态,即输出状态始终与输入状态保持一致。数码管位选通过三极管8550接573输出,573的输入接单片机P0口。如此看来,位选端的控制问题又转换成对单片机端口P0的控制了。假设P0口的位状态为0x00(低电平),则573输出端状态也为0x00,8个三极管的基极也为低电平,三极管导通接地,数码管位选端状态为低电平,因为所用数码管为四位共阴极,此时数码管可以正常显示。相反,如果P0口状态为0xff,则8位数码管都不能显示。因此,我们可以通过控制各数码管的位选端来控制让哪一位数码管显示数据。 图2-3 数码管驱动电路 【任务实践】 1) 工作任务描述 设计出能够驱动8位数码管显示的基本电路,并编写程序实现8位数码管全部显示数字0。 2) 工作任务分析 8位数码管可以选用两块四位一体的共阴极数码管,按此计算8位数码管段选共用,需要8根I/O口线。位选独立,需8根I/O口线。位选和段选共需16根口线,而51单片机总共只有32根口线,为尽可能地节省端口资源,应考虑端口扩展和复用的方式。 3) 工作步骤 (1) 选择合适的外围驱动芯片,设计合理的数码管显示驱动电路。 (2) 了解单片机端口的输入输出控制方式,掌握相关外围芯片的硬件连接方式和软件驱动方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 如图2-3所示电路中为了节省单片机的管脚,设计中我们采用两片74HC573作为驱动电路。一片驱动8位数码管,另一片驱动点阵及交通灯电路。数据线接单片机的P0 口,两个驱动芯片的转换通过一个波段开关控制。当开关拨到上边时,点阵驱动电路起作用,相反,当开关拨到下边时,数码管驱动电路起作用。图2-3中有两个四位一体的共阴极数码管,其中数码的段选(数据段)连接到芯片74HC595的输出端,74HC595的串行数据输入端DS连接在单片机的P2_5引脚,移位脉冲输入端SHCP接单片机引脚P2_7,锁存脉冲输入端STCP接单片机引脚P2_6,8位数码管各自的位选端各通过一个三极管8550连接的锁存器74HC573的输出端,而74HC573的输入端与单片机的P0口相连。 程序示例: #include #define uchar unsigned char #define uint unsigned int #define weixuan P0 sbit sck=P2^7;//移位时钟 sbit tck=P2^6;//锁存时钟 sbit data1=P2^5;//串行数据输入 void write_HC595(uchar wrdat); void main() { weixuan=0x00;//让位选全部为低,即打开所有数码管显示 write_HC595(0x3f); while(1); } /********************************************************** //名称:wr595()向595发送一个字节的数据 //功能:向595发送一个字节的数据(先发高位) **********************************************************/ void write_HC595(uchar wrdat) { uchar i; SCK_HC595=0; RCK_HC595=0; for(i=8;i>0;i--) //循环8次,写一个字节 { DA_HC595=wrdat&0x80; //发送BIT0 位 wrdat<<=1; //要发送的数据左移,准备发送下一位 SCK_HC595=0; _nop_(); _nop_(); SCK_HC595=1; //移位时钟上升沿 _nop_(); _nop_(); SCK_HC595=0; } RCK_HC595=0; //上升沿将数据送到输出锁存器 _nop_(); _nop_(); RCK_HC595=1; _nop_(); _nop_(); RCK_HC595=0; } 任务二 0—F依次循环显示 【知识储备】 数码管的静态显示 静态显示就是当要显示某个数字时,将要显示的段选数据始终保持在数码管的段选端。例如,有一个共阴极的数码管,只要给它的abcdef脚提供高电平,g脚和dp端提供低电平即可显示数字0。这种显示方法电路简单,程序也十分简洁。但是这种显示方法占用的I/O端口较多,当显示的位数在一位以上,一般不采用这种显示方法。 如图2-4所示四位静态显示电路,由于显示器中各位相互独立,而且各位的显示字符完全取决于对应口的输出数据,如果数据不改变,那么显示器的显示亮度将不会受影响,所以静态显示器的亮度都较高,但是从图2-4中可以看出,它需要4组8位的数据总线,共32根I/O口线。这对于单片机来说几乎占用了所有的I/O端口,所以显示位数过多时,静态显示这种方法就不再适用了。 图2-4 四位静态显示的电路 任务二实际上只是在任务一的基础上,每隔一定的时间按顺序改变发送的段选数据,位选端不发生任何变化。完成任务一和任务二要求,表示我们已经掌握了静态显示的应用,接下来可以进一步提高题目的难度,见任务三。 【任务实践】 1) 工作任务描述 设计出能够驱动8位数码管显示的基本电路,并编写程序实现8位数码管0—F依次循环显示。 2) 工作任务分析 任务二的硬件电路没有变化,只需要软件上做些变动,与任务一数码管的段选数据(显示0的字形码)始终保持在段选端上不同,任务二数码管的段选数据每隔一段时间就要从0到F依次变化一次。 3) 工作步骤 (1) 选择合适的外围驱动芯片,设计合理的数码管显示驱动电路。 (2) 了解单片机端口的输入/输出控制方式,掌握相关外围芯片的硬件连接方式和软件驱动方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 程序示例: #include #include #define uchar unsigned char #define uint unsigned int #define weixuan P0 sbit sck=P2^7;//移位时钟 sbit tck=P2^6;//锁存时钟 sbit data1=P2^5;//串行数据输入 //############################################# //共阴极数码管显示代码: uchar code seg[16]={0x3f,0x06,0x5b,0x4f, //0,1,2,3, 0x66,0x6d,0x7d,0x07, //4,5,6,7, 0x7f,0x6f,0x77,0x7c, //8,9,A,b, 0x39,0x5e,0x79,0x71}; //C,d,E,F //############################################# void write_HC595(uchar wrdat); void delay(uint time); //延时函数 void main() { uchar num,i; weixuan=0x00; while(1) { for(i=0;i<=7;i++) { num=led[i]; write_HC595(num); delay(350); weixuan=_crol_(weixuan,1); } } } void write_HC595(uchar wrdat) { uchar i; SCK_HC595=0; RCK_HC595=0; for(i=8;i>0;i--) //循环8次,写一个字节 { DA_HC595=wrdat&0x80; //发送BIT0 位 wrdat<<=1; //要发送的数据左移,准备发送下一位 SCK_HC595=0; _nop_(); _nop_(); SCK_HC595=1; //移位时钟上升沿 _nop_(); _nop_(); SCK_HC595=0; } RCK_HC595=0; //上升沿将数据送到输出锁存器 _nop_(); _nop_(); RCK_HC595=1; _nop_(); _nop_(); RCK_HC595=0; } void delay(uint time)//延时函数 { uint a,b; for(a=0;a<=time;a++) for(b=0;b<=1000;b++); } 任务三 单个数码管依次轮流显示0—7 【知识储备】 一、动态显示原理 任务三中,我们对代码做了一些改变,实现了任务要求,简单地说,就是在段选数据改变的同时依次改变位选数据,并且每次只选通一位数码管。即当发送数据0的段码时(段码为0x3f),位选数据状态为0xfe;发送数据1的段码时(段码为0x06),位选数据循环左移一位变成0xfd;以此类推,再加上一定的时间间隔,就实现了任务要求的显示状态。8位数码管每次只有一位显示,并轮流显示数字0—7。设想一下,如果将数字切换的间隔时间逐步调短,也就是将void delay(uint time)的实参值逐渐调小,最后我们看到的将是8位数码管上同时显示数字0—7,这就是动态显示。 所谓动态显示就是将要显示的数按显示数的顺序在各个数码管上一位一位地显示,它利用人眼的驻留效应使人感觉不到是一位一位显示的,而是一起显示的。 【任务实践】 1) 工作任务描述 设计出能够驱动8位数码管显示的基本电路,编写程序,让开发板上的8位数码管首先第0位显示0,其他位不显示,然后第1位显示1,每次只有1位数码管显示,按此顺序显示到7,时间间隔为1秒。 2) 工作任务分析 任务三的硬件电路没有变化,只需要软件上做些变动,相对于任务二的变化是,任务二中只是数码管的段选数据每隔一定时间间隔发生变化,而任务三是段选数据变化的同时,位选数据也跟着变化。 3) 工作步骤 (1) 选择合适的外围驱动芯片,设计合理的数码管显示驱动电路。 (2) 了解单片机端口的输入/输出控制方式,掌握相关外围芯片的硬件连接方式和软件驱动方式。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 程序示例: #include #include #define uchar unsigned char #define uint unsigned int #define weixuan P0 sbit sck=P2^7;//移位时钟 sbit tck=P2^6;//锁存时钟 sbit data1=P2^5;//串行数据输入 //############################################# //共阴极数码管显示代码: uchar code seg[16]={0x3f,0x06,0x5b,0x4f, //0,1,2,3, 0x66,0x6d,0x7d,0x07, //4,5,6,7, 0x7f,0x6f,0x77,0x7c, //8,9,A,b, 0x39,0x5e,0x79,0x71}; //C,d,E,F void write_HC595(uchar wrdat); void delay(uint time); void main() { uchar num,i; weixuan=0xfe; while(1) { for(i=0;i<=7;i++) { num=led[i]; write_HC595(num); delay(350); weixuan=_crol_(weixuan,1); } } } /********************************************************** //名称:wr595()向595发送一个字节的数据 //功能:向595发送一个字节的数据(先发高位) **********************************************************/ void write_HC595(uchar wrdat) { uchar i; SCK_HC595=0; RCK_HC595=0; for(i=8;i>0;i--) //循环8次,写一个字节 { DA_HC595=wrdat&0x80; //发送BIT0 位 wrdat<<=1; //要发送的数据左移,准备发送下一位 SCK_HC595=0; _nop_(); _nop_(); SCK_HC595=1; //移位时钟上升沿 _nop_(); _nop_(); SCK_HC595=0; } RCK_HC595=0; //上升沿将数据送到输出锁存器 _nop_(); _nop_(); RCK_HC595=1; _nop_(); _nop_(); RCK_HC595=0; } void delay(uint time) { uint a,b; for(a=0;a<=time;a++) for(b=0;b<=1000;b++); } 任务四 00—99计数显示 【知识储备】 一、简单的位值提取 经过对前面3个任务的学习与实践,我们掌握了数码管的基本结构、显示原理和驱动方式,并编程实现了具体功能。任务四也仅仅是对前面所学的组合应用,根据任务要求,我们应该可以很清楚地得出设计思路,就是设定一个计数变量num,初始化为零,然后每隔1秒num加1,当加到100时,num清零开始下一轮计数。这期间利用数码管的动态显示方式将计数值的个位和十位值显示出来。对于一个初学者来讲,整理出这样的思路应该不成问题,但具体细节,比如个位和十位怎么得到?一时难以解决。 其实问题很简单,只需要num对10求模、取余就可以了。程序中“gewei=num%10;”和“shiwei=num/10;”这两句代码就实现了该功能。同理,如果获取一个三位数的百位、十位和个位,只需要对100和10执行相同操作就可以了。 【任务实践】 1) 工作任务描述 硬件电路参照图1-17所示,利用前两位数码管显示,实现一个简单的从00到99循环计数的秒表。 2) 工作任务分析 00—99计数显示,实际上是利用两位数码管的动态显示,实现00—99之间的任意两位数的显示。由于我们还没有实现精确计时的方式,这里只能通过软件延时的方式实现粗略计时,每隔一定时间间隔,计数变量加1,然后通过一定程序算法,将计数值的个位和十位分离出来,分别送到段选端,当计数变量的值加到99时,计数清0。 3) 工作步骤 (1) 选择合适的外围驱动芯片,设计合理的数码管显示驱动电路。 (2) 了解单片机端口的输入/输出控制方式,掌握相关外围芯片的硬件连接方式和软件驱动方式,掌握C程序设计的简单算法。 (3) 打开集成开发环境,建立一个新的工程。 (4) 编写控制程序,编译生成目标文件。 (5) 下载调试。 4) 工作任务设计方案及实施 程序示例: #include #define uchar unsigned char #define uint unsigned int #define weixuan P0 sbit sck=P2^7;//移位时钟 sbit tck=P2^6;//锁存时钟 sbit data1=P2^5;//串行数据输入 //############################################# //共阴极数码管显示代码: uchar code seg[16]={0x3f,0x06,0x5b,0x4f, //0,1,2,3, 0x66,0x6d,0x7d,0x07, //4,5,6,7, 0x7f,0x6f,0x77,0x7c, //8,9,A,b, 0x39,0x5e,0x79,0x71}; //C,d,E,F void write_HC595(uchar wrdat); void delay(uint time); void main() { uchar num,gewei,shiwei,i; num=0; while(1) { gewei=num%10; shiwei=num/10; while(1) { weixuan=0xfd; write_HC595(seg[gewei]); delay(1); weixuan=0xfe; write_HC595(seg[shiwei]); delay(1); i++; if(i==70) {i=0; break;} } num++; if(num==100) num=0; } } void write_HC595(uchar wrdat) { uchar i; SCK_HC595=0; RCK_HC595=0; for(i=8;i>0;i--) //循环8次,写一个字节 { DA_HC595=wrdat&0x80; //发送BIT0 位 wrdat<<=1; //要发送的数据左移,准备发送下一位 SCK_HC595=0; _nop_(); _nop_(); SCK_HC595=1; //移位时钟上升沿 _nop_(); _nop_(); SCK_HC595=0; } RCK_HC595=0; //上升沿将数据送到输出锁存器 _nop_(); _nop_(); RCK_HC595=1; _nop_(); _nop_(); RCK_HC595=0; } void delay(uint time) { uint a,b; for(a=0;a<=time;a++) for(b=0;b<=500;b++); }