作者:sumjess
本章详细介绍单片机程序常用编译软件 Keil 的用法,包括用Kei 建立工程、工程配置、C51单片机程序软件仿真、单步、全速、断点设置、变量查看等。同时还介绍如何使用SST89E516RD 单片机进行计算机与 TX- lC单片机学习板之间的硬件仿真。用一个完整的C51程序来操作发光二极管的点亮与熄灭,然后调用C51库函数来方便地实现流水灯,最后为大家补充蜂鸣器与继电器的操作方法及栠电极开路与漏极开路的概念。从这一章开始我们将手把手地讲解单片机C语言编程。认真学好本章,对千初学者来说将会是一个非常好的开头。
一、Keil 工程建立及常用按钮介绍
1、Keil 工程的建立:
进入 Keil后,屏幕如下图所示:
(1)建立一个新工程单击 [ Project] 菜单中的[new Project…]选项, 如下图所示
( 2) 选择工程要保存的路径, 输入工程文件名。Keil 的一个工程里通常含有很多小文件, 为了方便管理, 通常我们将一个工程放在一个独立文件夹下, 比如保存到part2_1 文件夹, 工程文件的名字为 Sumjess, 如下图所示, 然后单击【保存】 按钮。工程建立后, 此工程名变为 Sumjess.uv2 。
(3) 这时会弹出一个对话框, 要求用户选抒单片机的型号, 可以根据用户使用的单片机来选择。Keil C51 几乎支持所有的 51 内核的单片机, T X-lC 实验板上用的是 STC89C52, 我们在对话框中找不到这个型号的单片机。因为 51 内核单片机具有通用性, 所以我们在这里可以任选一款== 89C52 就行==, Keil 软件的关键是程序代码的编写, 而非用户选择什么硬件, 在这里我们选择 Atmel 的 89C52来说明, 如下图所示。选择 89C52 之后, 右边【Description】栏里是对该型号单片机的基本说明,我们可以单击其他型号单片机浏览一下其功能特点,然后单击[确定]按钮。
注意:可能会出现
(4)完成上一步骤后, 窗口界面如下图所示。
到此为止,我们还没有建立好一个完整的工程,虽然上程名有了,但上程当中还没有任何文件及代码,接下来我们添加文件及代码。
( 5 ) 如下图所示, 单击[ File] 菜单中的[ New]菜单项, 或单击界面上的快捷图标
。
新建文件后窗口界面如下图所示
此时光标在编辑窗口中闪烁,可以输入用户的应用程序,但此时这个新建文件与我们刚才建立的工程还没有直接的联系, 单击图标,窗口界面如下图 所示, 在【文件名( N)】编辑框中, 输入要保存的文件名, 同时必须输入正确的扩展名。注意, 如果用C 语言编写程序, 则扩展名必须为.C; 如果用汇编语言编写程序, 则扩展名必须为.asm。这里的文件名不一定要和工程名相同,用户可以随意填写文件名,然后单击【保存】按钮
( 6 ) 回到编辑界面, 单击 [ Target 1 ] 前面的 "+ " 号, 然后在 [ Source Group 1]选项上单击右键, 弹出如下图所示菜单。然后选择[ Add Files to Group’Source Group 1’】归 菜单项, 对话框如下图所示。
选中[ Sumjess.c] , 单击[ Add】 按钮, 再单击[ Close] 按钮, 然后我们再单击左侧[ Sourse Group I ] 前面的 "+ , 号, 屏幕窗口如下图所示。
这时我们注意到[ Source Group 1】文件夹中多了一个子项 [ Sumjess.】,当 一个工程中有多个代码文件时,都要加在这个文件夹下,这时源代码文件就与工程关联起来了。
通过以上 ( 1 )~~ (6)步我们学习了如何在 Keil 编译环境下建立—个工程, 在 开始编写程序之前,我们有必要先学习编辑界面上一些常用的按钮功能与用法。
2、常用按钮介绍:
按钮一用于显示或隐藏项目窗口, 我们可单击该按钮观察其现象, 项目窗口如下图所示。
按钮----用于显示或隐藏输出信息窗口,当我们进行程序编译时可查看输出信息窗口,查看程序代码是否有错误,是否成功编译,是否生成单片机程序文件等。我们可单击该按钮观察其现象, 输出信息窗口如下图所示。
按钮一用于编译我们正在操作的文件。
按钮—用于编译修改过的文件,并生成应用程序供单片机直接下载。
按钮—用于重新编译当前工程中的所有文件,并生成应用程序供单片机直接下载。因为很多工程有不止一个文件,当有多个文件时,我们可使用此按钮进行编译。
按钮一用于打开[ Oprions for Target] 对话框, 也就是为当前工程设置选项。使用该对话框可以对当前工程进行详细设置,关于该对话框的设置方法将在使用时再做详细讲解。
以上是使用频率最多的几个按钮的功能,大家千万不要被一打开软件时呈现在眼前令人的眼花缭乱的众多按钮所吓着哟。其他一些调试时用到的按钮等我们具体用到时再做介绍。
二、点亮第一个发光二极管
大家是不是已经迫不及待地想编写程序了, 接下来我们就用 C 语言编写一个点亮TX- l C 实验板上第一个发光二极管的程序。由千这是本书的第一个程序,看懂了它,也就意味若你已经踏入了单片机 C 语言编程的第一道门槛, 因此我们在这里要花些时间讲解它, 大家一定要有耐心,认真地弄明臼它。
我们先回到 2.1 节最后的编辑界面 " Sumjess.c " 下, 在当前编辑框中输入如下的 C 语言源程序,注意:在输入源代码时务必将输入法切换成英文半角状态。
【例 2.2.1】编写程序, 点亮第一个发光二极管。
#include <reg52.h> //52系列单片机头文件
sbit led1=P1^0; //声明单片机P1口的第一位
void main() //主函数
{
led1=0; /*点亮第一个发光二极管*/
}
在输入上述程序时, Keil 会自动识别关键字, 并以不同的颜色提示用户加以注意, 这样会使用户少犯错误, 有利于提高编程效率。若新建立的文件没有事先保存的话, Keil 是不会自动识别关键字的, 也不会有不同颜色出现。程序输入完毕后, 如下图所示。
我们暂且不要管这几句程序表示什么意思,先学会编译及错误处理,然后我再详细介绍代码的含义。接下来我们编译此工程,看看程序代码是否有错误。先保存文件,再单击[全部编译】快捷图标。建议大家每次在执行编译之前都先保存一次文件,从一开始就养成良好的习惯对你将来写程序有很大好处, 因为进行编译时, Keil 软件有时会导致计算机死机, 使你不得不重启计算机,若你在编写一个很大的工程文件时没有及时保存,那么重启后你将找不到它的任何踪影,只得重写。虽然这种情况极少发生,但出千安全考虑,建议大家及时保存。编译后的屏幕如下图 所示, 我们重点观察信息输出窗口。
**在上图中, 我们看到信息输出窗口中显示的是编译过程及编译结果。其过程含义如下:
创建目标 "Target’1’’
编译文件 Sumjess.c:’ . ,.
链接..
工 程 “Sumjess”’ 编译结果-0 个错误, 0 个 警告。
以上信息表示此工程成功编译通过。
知识点: reg52.h 头文件的作用
在代码中引用头文件,其实际意义就是将这个头文件中的全部内容放到引用头文件的位 置处,免去我们每次编写同类程序都要将头文件中的语句重复编写。
在代码中加入头文件有两种书写方法,分别为==# include 和#include “reg52.h”==, 包含头文件时都不需要在后面加分号。
两种写法区别如下:
当使用<>包含头文件时,编译器先进入到软件安装文件夹处开始搜索这个头文件,也就 是 Keil\CS1\INC 这个文件夹下, 如果这个文件夹下没有引用的头文件, 编译器将会报错。
当使用双撇号”“包含头文件时,编译器先进入到当前工程所在文件夹处开始搜索该头文件,如果当前工程所在文件夹下没有该头文件,编译器将继续回到软件安装文件夹处搜索这 个头文件, 若找不到该头文件, 编译器将报错。reg52.h 在软件安装文件夹处存在, 所以我们一般写成#include 。
打开该头文件查看其内容, 将鼠标移动到 reg52.h 上, 单击右键, 选择 [ Open document ], 即可打开该头文件,如下图所示。以后若需打开工程中的其他头文件,也可采用这种方式。或者手动定位到头文件所在的文件夹也可。
其全部内容如下:
/*--------------------------------------------------------------------------
REG52.H
Header file for generic 80C52 and 80C32 microcontroller.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/
#ifndef __REG52_H__
#define __REG52_H__
/* BYTE Registers */
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
sfr PSW = 0xD0;
sfr ACC = 0xE0;
sfr B = 0xF0;
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 IE = 0xA8;
sfr IP = 0xB8;
sfr SCON = 0x98;
sfr SBUF = 0x99;
/* 8052 Extensions */
sfr T2CON = 0xC8;
sfr RCAP2L = 0xCA;
sfr RCAP2H = 0xCB;
sfr TL2 = 0xCC;
sfr TH2 = 0xCD;
/* BIT Registers */
/* PSW */
sbit CY = PSW^7;
sbit AC = PSW^6;
sbit F0 = PSW^5;
sbit RS1 = PSW^4;
sbit RS0 = PSW^3;
sbit OV = PSW^2;
sbit P = PSW^0; //8052 only
/* TCON */
sbit TF1 = TCON^7;
sbit TR1 = TCON^6;
sbit TF0 = TCON^5;
sbit TR0 = TCON^4;
sbit IE1 = TCON^3;
sbit IT1 = TCON^2;
sbit IE0 = TCON^1;
sbit IT0 = TCON^0;
/* IE */
sbit EA = IE^7;
sbit ET2 = IE^5; //8052 only
sbit ES = IE^4;
sbit ET1 = IE^3;
sbit EX1 = IE^2;
sbit ET0 = IE^1;
sbit EX0 = IE^0;
/* IP */
sbit PT2 = IP^5;
sbit PS = IP^4;
sbit PT1 = IP^3;
sbit PX1 = IP^2;
sbit PT0 = IP^1;
sbit PX0 = IP^0;
/* P3 */
sbit RD = P3^7;
sbit WR = P3^6;
sbit T1 = P3^5;
sbit T0 = P3^4;
sbit INT1 = P3^3;
sbit INT0 = P3^2;
sbit TXD = P3^1;
sbit RXD = P3^0;
/* SCON */
sbit SM0 = SCON^7;
sbit SM1 = SCON^6;
sbit SM2 = SCON^5;
sbit REN = SCON^4;
sbit TB8 = SCON^3;
sbit RB8 = SCON^2;
sbit TI = SCON^1;
sbit RI = SCON^0;
/* P1 */
sbit T2EX = P1^1; // 8052 only
sbit T2 = P1^0; // 8052 only
/* T2CON */
sbit TF2 = T2CON^7;
sbit EXF2 = T2CON^6;
sbit RCLK = T2CON^5;
sbit TCLK = T2CON^4;
sbit EXEN2 = T2CON^3;
sbit TR2 = T2CON^2;
sbit C_T2 = T2CON^1;
sbit CP_RL2 = T2CON^0;
#endif
从上面代码中可以看到, 该头文件中定义了52 系列单片机内部所有的功能寄存器, 用到了前面讲到的sfr 和sbit 这两个关键字, " sfr P0=0x80;“语旬的意义是,把单片机内部地址0x80 处的这个寄存器重新起名叫 P0, 以后我们在程序中可直接操作 P0就相当于直接对单片机内部的0x80 地址处的寄存器进行操作。说通俗点, 也就是说。通过 sfr 这个关键字, 让 Keil 编译器在单片机与人之间搭建—条可以进行沟通的桥梁, 我们操作的是 P0 口, 而单片机本身并不知道什么是 P0口, 但是它知道它的内部地址 0x80 是什么东西。说到这里我想大家应该已经明白了, 以后凡是编写 51 内核单片机程序时, 我们在源代码的第一行就可直接包含该头文件。
在上面我们还看到,” sbit CY= PSW^7;" 语旬的意思是,== 将 PSW 这个寄存器的最高位,重新命名为 CY == ,以后我们要单独操作 PSW 寄存器的最高位时, 便可直接操作 CY, 其他类同。
讲完了头文件,接下来我们再回到编辑界面,紧接着头文件后面有"//…", 请看知识点。
知识点:C 语言中注释的写法
在C语言中,注释有两种写法:
(1) //…,两个斜扛后面跟着的为注释语句。这种写法只能注释一行,当换行时,又必须在新行上重新写两个斜扛。
(2) /…/,斜扛与星号结合使用,这种写法可以注释任意行,即斜扛星号与星号斜扛之间的所有文字都作为注释。
所有注释都不参与程序编译,编译器在编译过程会自动删去注释,注释的目的是为了我们读程序方便,一般在编写较大的程序时,分段加入注释,这样当我们回过头来再次读程序时,因为有了注释,其代码的意义便一目了然了。若无注释,我们不得不特别费力地将程序重新阅读一遍方可知道代码含义。养成良好的书写代码格式的习惯,经常为自己编写的代码加入注释,以后定能方便许多。
例 2.2.1 程序中接着往下看, "sbit ledl=P1^0; " 语句的含义是, 将单片机 P0 口的最低位定义为 led1。在TX- lC实验板上,8个发光二极管的阴极通过一个 74HC573 锁存器分别连接至单片机的 P0 口, 若要控制某一个发光二极管, 也就是要控制单片机 P0 口的某一位, 必定要声明这一位, 否 则单片机肯定不知道我们要操作的是什么东东。需要注意的是, 这里的 P1 不可随意写, P 是大写, 若写成 p, 编译程序时将报错, 因为编译器并不认识 p1, 而它只认识P1 , 这是因为我们在头文件中定义的是 " sfr P1= 0x90;"。这也是大多初学者编写第一个程序时常犯的错误。
例 2.2.1 程序中再往下就到了主函数main() , 无论一个单片机程序有多大,或多小,所有的单片机在运行程序时,总是从主函数开始运行,关于主函数的写法,我们看下一个知识点。
知识点: main()主函数的写法
main( )函数
格式:void main( )
注意: 后面没有分号。
特点:无返回值,无参数。
无返回值表示该函数执行完后不返回任何值, 上面 main前面的 void 表示“空” ,即 不返回值的意思,后面我们会讲到有返回值的函数,到时大家一对比便会更加明白。
无参数表示该函数不带任何参数, 即 main后面的括号中没有任何参数, 我们只写 "( )"就可以了, 也可以在括号里写上void , 表示“空” 的意思, 如 void main(void)。 .
任何一个单片机 C 程序有且仅有一个 main 函数, 它是整个程序开始执行的入口。大家注意看,在写完 main( )之后,在下面有两个花括号, 这是C 语言中函数写法的基本要求之一,即在一个函数中,所有的代码都写在这个函数的两个大括号内,每条语句结束后都要加上分号,语句与语言之间可以用空格或回车隔开。
例如:
void main()
{
总程序从这里开始执行;
其他语句;
…
}
例 2.2.1 程序中接下来我们看 " led1 =0;" 语句,也就是该程序中最核心的语句。在数字电路中, 电平只有两种状态: 高电平,1 ; 低电平,0。显 然,该 语句的意思是, 让P1 口的最低位清 0。由于没有操作其他口, 所以其余口均保持原来状态不变。那么为什么P1 口的最低位清0, 板上的第一个发光二极管就会亮呢?接下来我们再来讲解电路知识,TX-lC单片机实验板上流水灯与单片机连接方法如下图所示。
图上电路中, 除单片机外,主要元件有三类:P2 ( 1KΩ排阻)、D ( 1~8 ) (发光二极管)、U3 ( 74HC573 锁存器), 下 面分别介绍。
( 1 ) 排阻。通俗地讲,它就是一排电阻, 上图中一共有 8 个发光二极管, 每个管子上串连—个电阻, 然 后在电阻的另一端接电源, 因 为 8 个管子接法相同, 所以我们把 8 个电阻的另一端全部连接在一起, 这样一来, 便共有 9个引脚,其中一个称为公共端。下图左和下图右是直插式和贴片式排阻的实物图。
知识点:由电阻标号认知阻值
一般在排阻上都标有阻值号,其公共端附近也有明显标记。如上图左和上图右中分别为 103 和 150, 103 表示其阻值大小为 10 x10^3Ω, 即10KΩ, 若是102其阻值大小为10x100,即 1 KΩ,150 为15Ω,即 15Ω,其他读法都相同。
我们有时也会看到标号为 1002, 1001 等。1002 表示100x10^2Ω,即10KΩ。
3 位数表示与 4位数表示的阻值读法我们都要会,标号位数不同, 其电阻的精度不同, 一般地, 3 位数表示 5%精度, 4 位数表示 1% 精度。TX- l C 实验板上与发光二极管连接的是102 阻值的 9 引脚直插排阻。
还有的标号如 3R0 ,表示阻值为 3Ω, 4K7 表示阻值为 4.7KΩ , R002 表示阻值为 0.002Ω。
( 2 ) 发光二极管。它具有单向导电性,通过5mA 左右电流即可发光,电流越大,其亮度越强,但若电流过大,会烧毁二极管,一般我们控制在 3~20mA之间。在这里, 给发光二极管串联一个电阻的目的就是为了限制通过发光二极管的电流不要太大, 因此这个电阻又称为“ 限流电阻"。当发光二极管发光时, 测量它两端电压约为 1.7V, 这个电压又叫做发光二极管的 ” 导通压降"。下图左和下图右分别为直插式发光二极管和贴片式发光二极管实物图。发光二极管正极又称阳极,负极又称阴极,电流只能从阳极流向阴极。直插式发光二极管长脚为阳极,短脚为阴极。仔细观察贴片式发光二极管正面的一端有彩色标记, 通常有标记的一端为阴极。大家可观察TX- IC 实验板上贴片发光二极管有一端有绿色标记, 此标记即标识它是管子的阴极。
关于排阻大小的选择: 欧姆定律想必大家都清楚,U=IR,当发光二极管正常导通时,其两端电压约为 1.7 V, 发光管的阴极为低电平,即0v, 阳极串接一电阻,电阳的另一端为 Vcc ,为 5V , 因此加在电阻两端的电压为 5V-l.7V=3.3V , 计算穿过电阻的电流,3.3V/1000Ω =3.3mA 。即穿过发光管的电流也为 3.3mA ,若想让发光管再亮一些,我们可以适当减小该电阻。
(3)74HC573 锁存器。它是一种数字芯片,由千数字芯片种类成千上万,我们不可能将其全部记住,所以只能用一个学一个,然后弄明白它,日积月累,大家必将能灵活地设计出各种电路。关千锁存器我们作为一个知识点来讲解。其直插式和贴片式实物图分别如下左右图所示。
知识点:锁存器
下图左为 74 HC573 的引脚分布图,先对照引脚图分别介绍各个引脚的作用, OE 的专业术语为三态允许控制端(低电平有效),通常叫做输出使能端,或输出允许端都可以; ID~8D 为数据输入端; 1 Q-8 Q 为数据输出端; LE 为锁存允许端, 或叫锁存控制端。
右图为74HC573 的真值表。真值表用来表示数字电路或数字芯片 工作状态的直观特性, 大家务必要看明白。下图右真值表中宇母代码含义如下: H—高电平; L—低电平; X—任意电平; Z—高阻态,也就是既 不是高电平也不是低电平,而它的电平状态由与它相连接的其他电气状态决定; Q。—上次的电平状态。
由真值表可以看出, 当
为高电平时,无论 LE 与 D 端为何种电平状态,其输出都为高阻态很明显,此时该芯片处于不可控状态,而我们将 74HC573 接入电路是必须要控制它的, 因此在设计电路时也就必须将
接低电平,所以TX-l C 实验板上使用的三个锁存器的
端全部接地。
当OE为低电平时,我们再看 LE, 当 LE 为 H 时, D 与 Q 同时为 H 或 L; 而当 LE 为 L时, 无论 D 为何种电平状态, Q 都保持上一次的数据状态。这也就是说, 当 LE 为高电平时,Q 端数据状态紧随 D 端数据状态变化; 而当 L E 为低电平时, Q 端数据将保持住 LE 端变化为低电平之前 Q 端的数据状态。因此我们将锁存器的 LE 端与单片机 的某一引脚相连, 再 将 锁存器的数据输入端与单片机 的某组 1/0 口相连, 便 可通过控制锁存器的锁存端与锁存器的数据输入端的数据状态来改变锁存器的数据输出端的数据状态。
TX-IC 实验板上发光二极管处连接锁存器的目的是, 因 为发光二极管通过锁存器连接到单片机的Pl 口, 而板上 A/D 芯片的数据输出端也连接到单片机的 Pl 口,当 我们在做 A/D 实验时, A/D 芯片的数据输出端的数据就会实时发生变化, 而若不加锁存器, 那 么 发 光 二极管的阴极电平也跟随 AID 的数据输出的变化而变化, 这 样 就 会 看 见 发 光 管 无 规 则 闪 动, 为了在做 AID 实验时, 不 影响发光二极管, 我们在发光二极管与单片机之间加入一个锁存器用以隔离, 当 做 A/D 实验时, 我们可通过单片机将此锁存器的锁存端关闭, 而此时无论单片机 P1口数据怎么变化,发光二极管也不会闪动。当我们做发光二极管的实验时,可将锁存端始终打开, 也就是让锁存器的锁存端处于高电平状态, 而此时发光二极管就会跟随单片机的 P1 口状态而变化。
可能看到这里大家会有疑问了,为什么我们刚才在写程序的时候,并没有写一句控制锁 存器的锁存端置高的语句呢?原因是这样的, 大家一定要牢记, 51 单片机在一上电时, 如果我们没有人为地控制其1/0 口的状态,所有未控制的1/0 口都将默认为高电平,因此我们并不需要写一句让锁存端置高的语句。接下来我们就把前面编写的这段程序生成可以下载到单片机的代码,然后亲自下载到实验板上,看看其效果究竟如何?
回到 Keil 编辑界面,单 击[ Project]菜 单 ,然后在下拉菜单中单击[Options for Target’Target]项,或 直 接单击界面上的工程设置选项快捷图标,弹出如下图所示画面。单击【Output】, 然后选中【Create HEX File】 项 , 使程序编译后产生 HEX 代码, 供 下 载器软件下载到单片机中。这里简单补充一点, 单片机只能下载 HEX 文件或 BIN 文件, HEX 文件是十六进制文件, 英文全称为 hexadecimal, BIN 文件是二进制文件, 英文全称为 binary, 这两种文件可以通过软件相互转换, 其 实 际内容都是一样的。我们也可同时将选项 【Browse Information】 选中, 选中这个选项后,我们在程序中某处调用函数的地方单击右健选择打开函数后,可直接跳转到该函数体中,这个功能在编写比较大的程序中会经常用到。
确定后,我们再将工程编译一次, 信息输出窗口如下图所示。
观察信息窗口可以看到多了一行 " creating hex file from "Sumjess’’… "。再补充一点, 当创建一个工程并编译这个工程时,生成的 HEX 文件名与工程文件名是相同的, 添加的源代码名可以有很多, 但 HEX 文件名只跟随工程文件名。
然后, 我们将此 HEX 文件下载到 TX-IC 单片机实验板上(关千下载过程请大家查看视频教程或实验板配套光盘资料), 实际现象效果图如下图所示。
在上图中, 右侧 8 个发光二极管中, 最上面的这个发光管点亮了, 其余的没有亮, 这说明, 程序按照我们编写的意图工作了。
这种控制1/0 口的方法是一条语句只能控制一个I/O 口, 也就是通常所说的位操作法, 如果我们要同时让 1, 3, 5, 7 这 4 个发光二极管亮, 就要声明 4 个1/0 口, 然后在主程序中再写 4 旬分别点亮 4 个发光管的程序。显然, 这种写法比较麻烦。接下来为大家讲解一种总线操作法。
【例 2.2.2】请大家按以下方法操作: 原来工程中的程序全部删掉,我们在新文件中输入以下语句:
#include <reg52.h> //52系列单片机头文件
void main() //主函数
{
P1=0xaa;
}
这里的" P1 =0xaa;" 就是对单片机 P1口的 8 个 I/0 口同时进行操作, " 0x " 表示后面的数据是以十六进制形式表示的, 十六进制的 aa, 转换成二进制是 10101010, 那么对应的发光二极管便是 1, 3, 5, 7 亮, 2 , 4, 6, 8 灭。我们将 0xaa 转换成十进制后为 170 , 也可直接对Pl 口进行十进制数的赋值, 如 " P1=170;", 其效果是一样的,只是麻烦了许多。因为无论是几进制的数,在单片机内部都是以二进制数形式保存的,只要是同一个数值的数,在单片机内部占据的空间就是固定的, 在这里还是用十六进制比较直观。编译后下载, 实际观察现象效果图如下图所示。
三、while语句
通过上面一节的学习,想必大家已经对点亮实验板上的任意发光二极管轻车熟路了, 但是, 先不要高兴得太早, 上面的程序并不完善, 任何一个程序都要有头有尾才对, 而上面我们写的程序似乎只有头而无尾。我们分析一下看, 当程序运行时, 首先进入主函数,顺序执行里面的所有语句,因为主函数中只有一条语句,当执行完这条语句后,该执行什么了?因为我们没有给单片机明确指示下一步该做什么, 所以单片机在运行时就很有可能会出错。根据经验(并没有详细记录可查),当 Keil 编译器遇到这种情况时,它 会 自动从主函数开始处重新执行语句,所以单片机在运行上面两个程序时,实际上是在不断地重复点亮发光二极管的 操作,而我们的意图是让单片机点亮二极管后就结束,也就是让程序停止在某处,这样一个 有头有尾的程序才完整。
那么如何让程序停止在某处呢?我们用 while 语句就可以实现。
知识点: while( ) 语 句
格式: while (表达式)
{内部语句(内部可为空)}
特点:先判断表达式,后执行内部语句。
原则: 若表达式不是0, 即为真, 那么执行语句。 否则跳出 while 语句 ,执行 后面的语句。
需要注意的三点:
( I ) 在 C 语言中我们一般把 “0” 认 为是“假",“ 非 0" 认 为是 “真”,也就是说,只 要不是 0 就是真, 所以 1, 2, 3 等都是真。
( 2 ) 内部语句可为空, 就是说 while后面的大括号里什么都不写也是可以的, 如"while(l){ };“既然大括号里什么也没有,那么我们就可以直接将大括号也不写,再如"while(1);” 中 “;” 一定不能少, 否则 while( )会把跟在它后面笫一个分号前的语句认 为是 它的内部语句。
例如: while(l)
P1=123;
P2=121;
••• .
上面这个例子中, while()会把 “P1=123;” 当做 它的语句,即使 这 条 语 句并 没有 加 大括号。既然如此, 那么我们以后在写程序时,如 果 while( )内部只有一条语句, 我们就可以省去大括号,而直接将这条语句跟在它的后面。
例如: while(l)
P1=123;
( 3 ) 表达式可以是一个常数、一个运算或一个带返回值的函数。
有了上面的介绍,我 们在程序最后加上 " while(1);"这样一条语句就可以让程序停止。因为该语句表达式值为 1’ 内 部语句为空,执 行 时 先 判 断 表 达 式 值 ,因 为为真, 所以什么也不执行, 然后再判断表达式, 仍 然 为真, 又 不 执 行 , 因 为只有当表达式值为 0 时才可跳出 while() 语句,所以程序将不停地执行这条语句,也就是说单片机点亮发光管后将永远重复执行这条 语句。
初学者可能会这样想,我让单片机把发光二极管点亮后,就让它停止工作,不再执行别的指令,这样不是更好吗?请大家注意,单片机是不能停止工作的,只要它有电,有晶振在起振,它就不会停止工作, 每过一个机器周期,它内部的程序指针就要加 1, 程序指针就指向下一条要执行的指令。想让它停止工作的办法就是把电断掉,不过这样发光二极管也就不会亮了。不过我们可以将单片机设置为休眠状态或掉电模式,这样可以最大限度地降低它的功耗。
#include <reg52.h> //52系列单片机头文件
void main() //主函数
{
P1=0xaa;
}
四、for语句及简单延时语句
知识点: for 语句
格式: for(表达式 l ;表达式 2;表达式 3)
{语句(内部可为空)} 执行过程:
笫1步, 求解一次表达式 l 。
笫2步, 求解表达式 2, 若其值为真( 非 0 即为真 ), 则执行 for 中语句, 然后执行笫 3
步; 否则结束 for 语句, 直接跳出, 不再执行笫 3 步。
笫3步, 求解表达式 3。
笫4步,跳 到笫 2 步重复执行。
需要注意的是, 三个表达式之间必须用分号隔开。
利用 for 语句和 while 语句可以写出简单的延时语句,下 面就用 for 语句来写一个简单的延时语句, 并进一步讲解 for 语句的用法。
unsigned char i; for(i=2;i>0;i–);
看上面这两句, 首先定义一个无符号字符型变量 i, 然后执行 for 语句, 表 达 式 l 是给 i
赋一个初值 2, 表达式 2 是判断i 大于 0 是真还是假, 表达式 3 是i 自减 1’ 我们分析执行过程:
第 1 步, 给i 赋初值 2, 此时 i=2。
第 2 步, 因为 2>0 条件成立, 所以其值为真, 那么执行一次 for 中的语句, 因为 for 内部语句为空, 即什么也不执行。
第 3 步, i 自减 1’ 即 i=2- 1=1。
第 4 步, 跳到第 2 步,因 为 1>0 条件成立, 所以其值为真, 那么执行一次 for 中的语句, 因为 for 内部语句为空, 即什么也不执行。
第 5 步, i 自减 1 , 即 i=1-1=0。
第 6 步, 跳到第 2 步,因 为 0>0 条件不成立, 所以其值为假, 那么结束 for 语句, 直接跳出。
通过以上 6 步, 这个 for 语句就执行完了, 单片机在执行这个for 语句的时候是需要时间的,上 面i 的初值较小, 所以执行的步数就少,当 我们给i 赋的初值越大,它 执 行 所需的时间就越长, 因 此我们就可以利用单片机执行这个for 语句的时间来作为一个简单延时语句。
很多初学者容易犯的错误是,想 用 fo r 语句写一个延时比较长的语旬,那 么 他 可能会这样
写:
unsigned char i; for(i=3000;i>0;i–);
但是结果却发现这样写并不能达到延长时间的效果, 因 为在这里i是一个字符型变量,
它 的 最 大 值 为 255, 当你给它赋一个比最大值都大的数时,编译器自然就出错误了,因此我们尤其要注意,每次给变量赋初值时,都要首先考虑变量类型,然后根据变量类型赋一个合理 的值。
那么怎样才能写出长时间的延时语句呢?我们下面讲解 for 语句的嵌套。
unsigned char i, j; for(i=100;i>0;i–)
for(=200;j>0;j–);
上面这个例子是for 语句的两层嵌套, 大家注意看, 第一个for 后面没有分号, 那么编译器默认第二个 for 语句就是第一个 for 语句的内部语句, 而第二个 for 语句内部语句为空, 程序在执行时, 第 一 个 for 语 句 中 的 i 每 减一次, 第二个 for 语句便执行 200 次, 因 此 上 面这个例子便相当千共执行了 100x200次 for语句。通过这种嵌套我们便可以写出比较长时间的延时语句,我 们 还 可 以 进行 3 层、4 层嵌套来增加时间,或 是 改变变量类型, 将变量初值再增大也可以增加执行时间。
这种 for 语句的延时时间到底有没有精确的算法呢?在 C 语言中这种延时语句不好算出它的精确时间,如果需要非常精确的延时时间,我们在后面会讲到利用单片机内部的定时器来延时,它的精度非常高,可以精确到微秒级。而一般的简单延时语句实际上我们并不需要 太精确,不过我们也是有办法知道它大概延时多长时间的,请看下一节讲解。
五、Keil仿真及延时语句的精确计算
【例 2.5 .1 】利用for 语句的延时特性, 编写一个让实验板上第一个发光二极管以间隔1s 亮灭闪动的程序。我们新建一个文件 Sumjess2.c, 添加到工程中,删去原来的文件,在新文件中输入以下代码:
#include <reg52.h> //52系列单片机头文件
#define uint unsigned int //宏定义
sbit led1 = P1^0; //声明单片机P1口的第一位
uint i,j;
void main() //主函数
{
while(1) //大循环
{
led1=0; //点亮第一个发光二极管
for(i=1000;i>0;i--) //延时
for(j=110;j>0;j--);
led1=1; //关闭第一个发光二极管
for(i=1000;i>0;i--) //延时
for(j=110;j>0;j--);
}
}
观察上面代码, 与之前的代码相比, 关键部分多了#define 语句、while(l ){}、还有两个 for语句。
知识点: #define 宏定义
格式: # define 新名称 原内容
注意后面没有分号, #define 命令用它后面的笫一个字母组合代替该字母组合后面的所有内容,也就是相当于我们给“原内容”重新起一个比较简单的“新名称",方便以后在程序中 直接写简短 的新名称,而不必每次都写烦琐的原内容。
上例中我们使用宏定义的目的就是将unsigned int 用 uint 代替,在上面的程序中可以看到, 当我们需要定义unsigned int 型变量时, 并没有写 “unsigned int i, j;” , 取而代之 的是 “uint i, j ;”’ 在一个程序代码中, 只要宏定义过一次, 那么在整个代码中都可以直接使用它的 “新名称"。注意, 对同一个内容, 宏定义只能定义一次, 若定义两次,将会出现重复定义的错误提示。
while 语句和for 语句在前面都已经讲过, 这里使用 while(1){}语句, 因为 while 里的表达式是 1, 永远为真, 所以程序将永远循环执行这个大括号中的所有语句。单片机在执行指令的时候是按代码从上向下顺序执行的, 我们分析 while 大括号里的语句含义是:“ 点亮灯一延时一会儿一关闭灯一再延时一会儿一点亮灯一延时一会儿…“如此循环下去, 当我们把程序下载到实验板上便可看到小灯亮灭闪动的效果。
我们如何用软件来模拟出这个延时语句究竟是延长多少时间呢?回到 Keil 编辑界面, 打开工程设置对话框, 在 【Target】标签下的 【 Xtal(MHz)】 后面将原来的默认值修改为 TX-IC 单片机实验板上晶振频率值 11.0592MHz, 如下图所示。
Keil 编译器在编译程序时, 计算代码执行时间与该数值有关, 既然我们要模拟真实时间,那么软件模拟运行速度就要与实际硬件一一对应, T X-l C 实验板上使用的外部晶振频率是11.0592MHz, 在实验板上单片机的右下角大家可以看到实物, 如下图所示。
单击[确定]按钮后,再单击窗口上的调试按钮快捷图标
,进入到软件模拟调试模式, 如下图所示。
在软件调试模式下,我们可以设置断点、单步、全速、进入某个函数内部运行程序,同 时还可以查看变量变化过程、模拟硬件 I/O 口电平状态变化、查看代码执行时间等。在开始调试之前我们先熟悉一下调试按钮的功能。调试状态下多了下图所示的几个调试按钮。
对常用的几个按钮介绍如下:
—将程序复位到主函数的最开始处, 准备重新运行程序。
—全速运行,运行程序时中间不停止。
— 停止全速运行, 全速运行程序时激活该按钮, 用来停止正全速运行的程序。
— 进入子函数内部。
—单步执行代码,它不会进入子函数内部,可直接跳过函数。
— 跳 出当前进入的函数,只 有进入子函数内部该按钮才被激活。
—程序直接运行至当前光标所在行。
—显示/隐藏编译窗口, 可以查看每句C 语言编译后所对应的汇编代—
显示/隐藏变量观察窗口,可以查看各个变量值的变化状态。
大家不妨把这些按钮一个个都单击试试看,只有亲自操作过了记忆才会深刻。我们先来看如何在单步执行代码时, 查看硬件I/O口电平变化和变量值的变化。先将硬件I/O口模拟器打开, 在下图左中单击 【Port】项, 弹出下下图所示对话框。
上图显示的是软件模拟出的单片机Pl 口8 位口线的状态,单片机上电后I/O口全为 1 ,即十六进制的0xFF。
我们再单击变量观察窗口的 【Watch #1】习 标签, 窗口变成下图所示, 可以看到上面显示出 " type F2 to edit ( 按F2 进行编辑)”的 字样, 接下来我们分别按两次 F2,输入本程序中用到的两个变量 i 和 j。在右面立即显示出变量的值0x0000, 如下下图所示, 因为 i 和j 在最开始定义的时候并没有给它们赋初值, 编译器默认给它们赋的初值是0, 而当进入 for 语句后, 我们才为 i 和j 分别赋了 1000 和 110 的值。
同时, 在上图左侧的寄存器窗口中可以看到一些寄存器名称及它们的值, 如下图所示。
下图中我们最关心的只有一个, 也就是本小节的核心部分 " sec ", 它后面显示的数据就是程序代码执行所用的时间, 单位是秒, 可以看到上面显示的是 422.09µs, 这是程序启动执行到目前停止位置所花的所有时间。注意:这个时间是累计时间。
我们回到代码编辑框,看到主函数 " ledl =0; "
前面有一个黄色的小箭头,这里需要注意,这个小箭头指向的代码是下一步将要执行的代码,我们单击单步运行快捷图标
w, 这时看到黄色小箭头向下移动了一行, 在 P1 口软件模拟窗口中, P1 的 最 低 位 对 应 的 对 号 没 有 了 , 这 说明 " ledl =0; "
这 条 语 句 执 行 结 束了, 在 实 际 硬 件 中 也 就 点 亮 了 P1口最低位所对应的发光二极管。同时 sec 后面变成为 423.18µ s , 因为我们可以计算出执行这条指令实际花去了 423.l 8-422.09=1.09µs 的时间, 这 个 时 间 恰好就是 51 单片机在 11.0592 晶振频率下,一个机器周期(知识点)所花费的时间。
知识点:单片机的几个周期介绍
( 1 )时钟周期。也称振荡周期, 定 义为时钟频率的倒数
(可以这样来理解, 时钟周期就是单片机外接晶振的倒数,如 12M H z 的 晶振, 它的时钟周期就是 1/12µs ), 它是单片机 中最基本的、最小的时间单位。在一个时钟周期内, CPU仅完成一个最基本的动作。对于某个单片机来讲 ,若采用了1MHz 的时钟频率, 则时钟周期就是 lµs; 若采用 4MHz 的时钟频率,则时钟周期就是 250µs。由于时钟脉冲是 CPU 的基本工作脉冲, 它控制着 CPU 的工作节奏( 使 CPU 的每一步都统一到它的步调上来)。显然,对 同一种单片机,时钟频率越高,单片机的工作速度就越快。但是,由于不同的单片机其内部硬件电路和 电气结构不完全相同, 所以其所需要的时钟频率范围也不一定相同。我们使用的 STC89C系列单片机 的时钟范围约在1MHz~40MHz。
( 2 )状态周期。它是时钟周期的两倍。
( 3 ) 机器周期。单片机 的 基 本 操 作 周期,在 一 个操作周期内, 单片机 完成一项基本操作, 如 取 指 令 、存储器读/写等。它由 12 个时钟周期 ( 6 个状态周期)组成。
( 4 ) 指令周期。它是指CPU 执行一条指令所需要的时间。一般一个指令周期含有 1~4个机器周期。
接着再单击单步运行按钮, 这 时 右 下 角变量查看窗口中的 i 被赋值0x03E8 , 在这个值上单击鼠标右键选择 [ NumberBase-+Decimal] 项, 将 数 值 显 示 方 式 改 成 十 进 制 显 示 , 我们看到i 的值即为 1000 , 实际上就是刚才上一步运行第一个 for 语句时给i 赋的值。继续单步运行可以看到i 的值从 1000 开始往下递减,同时左侧 的 sec在一次次增加,但 j 的 值 始终为0,因为每执行一次外层 for 语句,,内 层 for 语句将执行 110 次,即 j 已经由 110 递减到 0 了,所以我们看上去 j 的值始终都是 0。那如果我们要看这个 for 嵌套语句到底执行了多长时间的话, 是 不 是 就 要单击 1000 次呢?其实不用这么麻烦, 设 置断点可以方便地解决这个问题。
设置断点有很多好处,在软件模拟调试状态下,当程序全速运行时,每遇到断点,程序会自动停止在断点处,即下一步将要执行断点处所在的这条指令。这样,我们只需在延时语句的两端各设置一个断点,然后通过全速运行,便可方便地计算出所求延时代码的执行时间。 设置方式如下:单击复位钮, 然后在第一个 for 所在行前面空白处双击鼠标,前面出现一个红色方框,表示本行设置了一个断点,然后在下面"led1=1;"所在行以同样方式插入另一个断 点, 这两个断点之间的代码就是这个两级for 嵌套语句, 如下图所示。
单击全速运行按钮
, 程序会自动停止在第一个 for 语句所在行, 查看时间显示为423.18 µs, 再单击一次全速运行按钮, 程序停止在第二个for 语句下面一行处, 查看时间显示为 968.31272ms, 我们忽略微秒, 此时间约为 1s , 由千无须精确时间,所以这个精度已经足够, 我们的 for 语句延时时间便计算出来了。
大家可以改变 for 语句中两个变量的初值来重新测试时间,这 里给大家讲讲我曾用过的时间经验: for 语句中两个变量类型都为 unsigned int 型时(注意, 若变量为其他类型则时间不遵循以下规律, 因为变量类型不同,单 片机运行时所需时间就不同), 内层for 语句中变量恒定值为 110 时, 外层 for 中变量为多少, 这个for 嵌套语句就延时约多少毫秒, 大家可自行测试验证, 也可自己测试出更精确的延时语句。
六、不带参数函数的写法及调用
我们先来观察 2.5 节中的例 2.5.1, 可以看到在打开和关闭发光二极管的两条语句之后, 是两个完全相同的for 嵌套语句:
for(i=1000;i>0;i–) //延时
for(j=110;j>0;j–);
在C 语言代码中, 如果有一些语句不止一次用到, 而且语句内容都相同, 我们就可以把这样的一些语旬写成一个不带参数的子函数, 当在主函数中需要用到这些语句时, 直接调用这个子函数就可以了。我们以上面这个for 嵌套语句为例, 其写法如下:
void delay1s()
{
for(i=1000;i>0;i–)
for(j=110;j>0;j–);
{
其中,void 表示这个函数执行完后不返回任何数据,即 它是一个无返回值的函数。delayls 是函数名, 这个名字我们可以随便起, 但是注意不要和 C 语言中的关键字相同。大家写成delay_1s, delay1 miao 等都是可以的, 一般我们写成方便记忆或读懂的名字, 也就是一看到函数名就知道此函数实现的内容是什么。我在这里写成 delay1s 是因为这个函数是一个延时1s 的函数。紧跟函数名后面的是一个括号, 这个括号里没有任何数据或符号(即 C 语言当中的“ 参数")’ 因 此 这个函数是一个无参数的函数。接下来两个大括号中包含着其他要实现的语句。以上讲解的是一个无返回值、不带参数的函数的写法。
需要注意的是, 子函数可以写在主函数的前面或是后面, 但是不可以写在主函数里面。
当写在后面时,必须要在主函数之前声明子函数,声明方法如下:将返回值特性、函数名及 后面的小括号完全复制,若是无参函数,则小括号内为空;若是有参函数,则需要在小括号 里依次写上参数类型,只 写参数类型, 无须写参数, 参 数 类 型 之 间 用 逗号隔开。最后在小括号的后面必须加上分号 “;”。当子函数写在主函数前面时, 不需要声明,因 为写函数体的同时就已经相当千声明了函数本身。通俗地讲, 声明子函数的目的是为了编译器在编译主程序的时候, 当 它 遇 到一个子函数时知道有这样一个子函数存在, 并且知道它的类型和带参情况等信息, 以 方便为这个子函数分配必要的存储空间。
【例 2.6.1】写出一个完整的调用子函数的例子, 让实验板上第一个发光二极管以间隔500ms 亮灭闪动。新建一个文件 Sumjess3.c, 添加到工程中,删去原来的文件,在新文件中输入以下代码:
#include <reg52.h> //52系列单片机头文件
#define uint unsigned int //宏定义
sbit led1 = P1^0; //声明单片机P1口的第一位
void delay1s(); //声明子函数
void main() //主函数
{
while(1) //大循环
{
led1=0; //点亮第一个发光二极管
delay1s(); //调用延时子函数
led1=1; //关闭第一个发光二极管
delay1s(); //调用延时子函数
}
} //子函数体
void delay1s()
{
uint i,j;
for(i=500;i>0;i--) //延时500ms
for(j=110;j>0;j--);
}
在例 2.6.1 中, 我 们 注意到 " uint i , j;" 语句, i 和 j 两个变量的定义放到了子函数里, 而没有写在主函数的最外面。在主函数外面定义的变量叫做全局变量; 像 这 种 定 义 在某个子函数内部的变量被叫做局部变量,这里i 和 j 就是局部变量。注意:局部变量只在当前函数中有效,程序一旦执行完当前子函数,在它内部定义的所有变量都将自动销毁,当下次再调用该函数时, 编 译 器 重 新为其分配内存空间。我们要知道, 在一个 程序中, 每个全局变量都占据着单片机内固定的 RAM, 部变量是用时随时分配,不用时立即销毁。一个单片机的 RAM是有限的, 如 AT89C52 只有 256B 的 RAM, 如果要定义unsigned int 型变量的话, 我们最多只能定义 128 个; STC 单片机内部比较多, 有 512B 的 ,也有 1280B 的。很多时候, 当写一个比较大的程序时,经常会遇到内存不够用的情况,因此我们从一开始写程序时就要坚持能 节省 RAM 空间就要节省, 能用局部变量就不用全局变量的原则。
将程序下载到实验板, 可看见小灯先亮 500ms, 再灭 500ms, 一直闪烁。
七、带参数函数的写法及调用
有了 2.6 节 的知识,本节学起来便容易多了。我们来看 2.6 节中的delay1s( )子函数,i= 500时延时500ms,那么如果我们要延时300ms, 就需要在子函数里把i 再赋值为 300 , 要延时100ms就得改i 为 100 , 这样岂不是很麻烦?有了带参数的子函数就好办多了,写法如下:
void delayms(unsigned int xms)
{
uint i,j;
for(i=xms;i>0;i–)//i= xms 即延时约xms 毫秒
for(j=110;j>0;j–);
}
上面代码中 delayms 后面的括号中多了一句 " unsigned int xms", 这就是这个函数所带的一 个参数, x ms 是一个unsigned int 型变量, 又叫这个函数的形参, 在调用此函数时我们用一个具体真实的数据代替此形参,这个真实数据被称为实参,形参被实参代替之后,在子函数 内部所有和形参名相同的变量将都被实参代替。声明方法在 2.6 节已经讲过,这 里再强调一下, 声明时必须将参数类型带上,如果有多个参数,多个参数类型都要写上,类型后面可以不跟 变量名, 也可 以写上变量名, 具体使用过程请看例 2.7.1。有了这种带参函数, 我们要调用一个延时 300ms 的函数就可以写成 " delayms(300);", 要延时 200ms 可以写成" delayms(200);",这样就方便多了。如下:
【例 2.7.1】写一个完整的程序, 还是让一个小灯闪动, 不过这次我们让它以亮 200ms、灭 800ms 的方式闪动, 完 整 程序代码如下:
#include <reg52.h> //52系列单片机头文件
#define uint unsigned int //宏定义
sbit led1 = P1^0; //声明单片机P1口的第一位
void delay1s(uint); //声明子函数
void main() //主函数
{
while(1) //大循环
{
led1=0; //点亮第一个发光二极管
delay1s(200); //调用延时子函数
led1=1; //关闭第一个发光二极管
delay1s(800); //调用延时子函数
}
} //子函数体
void delay1s(uint xms)
{
uint i,j;
for(i=xms;i>0;i--) //延时xms
for(j=110;j>0;j--);
}
将程序下载到实验板, 可看见小灯先亮 200sm, 再灭 800ms, 一直闪烁。
八、利用C51库函数实现流水灯
实现流水灯的办法有多种,可以用逻辑运算来实现,也可以用C51库自带的函数来实现,本节中我们就调用现成的库函数来)实 现 流 水 灯 ,大 家 打 开Keil 软件安装文件夹, 定 位 到Keil\C51\HL P 文 件 夹 ,打 开此文件夹下的 C51lib文件, 这 是 C51 自带库函数帮助文件。在索引栏我们找到_crol_函数 , 双 击 打 开它的介绍, 内 容 如 下 :
这 个 函 数 包 含 在 intrins.h头文件中, 也 就 是 说 , 如 果 在 程 序 中 要 用 到 这 个 函 数 , 那 么 必须 在 程 序 的 开头处包含 intrins.h 这个头文件。 再来看函数特性 " unsigned char crol (unsigned char c, unsigned char b);" 这 个 函 数 不 像 前 几 节 我 们 讲 过 的 函 数 , 它 前 面 没 有 void, 取而代之的是 unsignedchar; 小 括 号 里 有 两 个 形 参 , unsignedchar c, unsigned char b, 这种函数叫做有返回值、带参数的函数。有返回值的意思是说,程序执行完这个函数后,通过函数内部的某些运算而得出一个新值,该 函 数 最 终 将 这 个 新 值 返 回 给 调 用 它 的 语 句 。 _crol_是 函数 名,不 再多讲。我们再来看看函数实现了什么功能。
上面英文的大意是, Description (描述): _crol_这 个 函 数 的 意 思 是 将 字 符 c 循环左移 b
位,这 是 C51 库自带的内部函数,在 使 用 这 个 函 数 之 前 ,需 要 在 文 件 中 包 含 它 所 在 的 头 文 件 。再 看 后 面 的 Return Value (返回值): _crol_这 个函数返回的是将 c 循环左移之后的值。关于移位操作,我们看下一个知识点。
知识点:移位操作
( 1 ) 左 移 。 C51 中操作符为 "<< "’ 每 执 行 一 次 左 移 指 令 , 被 操 作 的 数 将 最高位移入单片机 PSW 寄 存 器的 CY 位, CY位中原来的数丢弃, 最低位补0, 其他位依次向左移动一位, 如下图所示。
( 2 ) 右移。C51 中操作符为 ">>",每 执 行 一 次 右 移 指 令 , 被 操 作 的 数 将 最 低 位 移 入 单片机 PSW寄存器的 CY 位, CY 位中原来的数丢弃,最 高位 补 0 , 其他位依次向右移动一位,如下图所示。
( 3 ) 循环左移。最高位移入最低位,其 他 位 依 次 向 左 移 一 位 。 C 语 言 中没有专门的指令, 通 过 移 位 指 令 与简单 逻辑运算可以 实现循环左移, 或 直 接 利用 C51 库中自带的函数_crol_实现, 如下图所示。
( 4 ) 循环右移。最低位移入最高位 ,其 他 位 依 次 向 右 移 一 位 。 C 语 言 中没有专门的指令, 通 过 移 位 指 令 与简单逻辑运算可以实现循环右移, 或 直 接 利用 C51 库中自带的函数_cror_实现, 如下图所示。
为了加深印象, 大家可以利用软件模拟的方法在 Keil 中亲自操作一下, 通过单步运行查看变量,左 移 、右 移 时 可 看 到 PS W 寄存器中CY 的变化与被移位数的变化状态,可参照下面两个例程 。
【 例 2 .8 .1】左移程序。
【 例 2 .8 .2 】 右 移 程 序 。
知识点: PSW寄存器。
PSW (Program Status Word ) 全称为程序状态宇标志寄存器,是一个8位寄存器,位于单片机 片内的特殊功能寄存器区,字节地址 DOH, 用来存放运算结果的一些特征,如有无进位、借位等,使 用汇编编程时 PSW 寄存器很有用,但 在 利用 C 语言编程时,编 译 器会自动控制该寄存器,很 少人为操作它,大家只需做简单了解 即可。其每位的具体含义如下图所示。
①CY—进位标志位, 它表示运算是否有进位(或借位)。如果操作结果在最高位有进位( 加 法)或者借位( 减法), 则该位为 1’ 否则为 0。
②AC- 辅助进位标志, 又 称 半 进位 标 志, 它指两个 8 位数运算低四位是否有半进位, 即 低 四 位 相 加 ( 或相减)是否进位( 或借位), 如 有 AC 为 1’ 否则为 0。
③FO— 由 用户使用的一个状态标志位, 可用软件来使它置 1 或清 0 , 也可由软件来测试
它,以控制程序的流向。
④RSI, RSO— 4组工作寄存器区选择控制位,在汇编语言中这两位用来选择4组工作寄存器区中的哪一组为当前工作寄存区。
⑤OV—溢出标志位, 反 映 带符号数的运算结果是否有溢出。有溢出时,此位为1 ,否则为 0。
⑥P—奇偶标志位, 反 映 累加 器 ACC 内容的奇偶性, 如 果 A C C中的运算结果有偶数个1 ( 如 11001lOOB, 其中有 4 个 1 ), 则 P 为 0 ,否则 P为1。
【例 2 .8 .3 】利用 C51 自带的库函数_crol_(), 以间隔 500ms, 在 TX- IC 实验板上实现流水灯程序, 新建文件 part2_6.c, 源代码如下:
我们来解释例 2.8.3中的 " aa=crol(aa, 1);" 语句。因为_crol_是一个带返回值的函数, 本句在执行时,先执行等号右边的表达式,即将 a a 这个变量循环左移一位, 然后将结果再重新赋给 aa 变量,如 aa 初值为0xfe , 二 进制为 11111110, 执行此函数时,将它循环左移一位后为 11111101 , 即0xfd , 然后再将0xfd重新赋给 aa 变量, 等while( 1 )中的最后一条语句执行完后, 将返回到while( 1 )中的第一语句重新执行, 此 时 aa 的 值 变 成 了 0xfd 。
除这种方法实现流水灯外,利用左移、右移指令与逻辑运算指令也可以实现循环移位, 若感兴趣大家可自己编写这方面的程序。
至此,我们已经从最简单的建立工程开始,通过一步步的操作,为大家详细介绍了设计一个完整的流水灯程序的过程, 从中我们学到了 Keil 软件的使用、调试模式下的软件仿真、while 语句、for 语句、各种函数的写法及用法, 本章的知识非常重要, 属于基础入门级讲解, 大家若有不明白之处要多看几遍,多查找相关资料,最重要的是多实践,多操作,以实践与 理论相结合的方式来学习, 真正的将单片机及电子方面的知识全部吸收消化。
标签:语句,函数,Keil,程序,郭天祥,二极管,C语言,单片机,sbit From: https://blog.51cto.com/u_14970037/5947680