单片机学习心得

第一节:跟我学单片机到底是学什么?我的两个比喻和一个规则。

开篇第一节,我问大家一个问题,跟我学单片机到底是学什么?我的回答是像驾驶汽车一样驾驭单片机。我教给大家的是驾驶汽车的技术而不是研发汽车的技术。因此每当别人问我学51单片机,PIC,AVR,stm32哪个更加有前途,应该先学哪个再学哪个时,我的回答是既然你是学驾驶技术,那么你用桑塔纳车来学还是用宝马车来学有差别吗?差别很小的,它们只是不同的厂家而已,只要会一种其它的就触类旁通了。把学单片机当作考驾照这是我所说的第一个比喻。

学单片机最核心的是程序,程序跟单片机芯片是什么关系?我的回答是像歌曲跟MP3播放器的关系。我们写的程序就像预先录制好的歌曲,单片机芯片就是一个MP3播放器。把不同的歌曲下载到同一个MP3里就可以播放出不同的美妙音乐,当前下载的歌曲决定了MP3可以播放的音乐。所以我们当前编写的程序下载进单片机之后,就决定了单片机能干哪些工作,“下载程序”也俗称“烧录程序”。把单片机芯片当作MP3播放器是我第二个比喻。 单片机芯片内部细节的工作原理是什么,为什么它能实现那么神奇的功能?我的回答是不用纠结这个问题,因为这不是我们学习的方向。考驾照的也只能告诉你汽车是由四个轮,发动机,制动系统,离合器,方向盘等部分构成,其它内部细节的原理恐怕也不会教你,不是不想教你,而确实是两个不同的学习方向。学单片机的也只能告诉你它内部是由运算器,寄存器,IO口,复位电路,晶振电路,程序存储器ROM,数据存储器RAM等部分组成,至于运算器的原理和构成也不是我们的学习方向。所以尽管我搞单片机有很多年,但是我并不是完全理解它最本质的原理。尽管我与人打交道有30多年,但是人为什么能通过大脑来灵活控制双手去活动对于我来说仍然是个迷,我只知道人是由脑袋,心脏,四肢等构成。每当有这样疑惑的时候该怎么办?我的回答是用“游戏规则”这个概念去应付它。因为游戏规则是不需要解释的,只要遵守就可以了。在应用的技术领域,把暂时不解的东西当作一种游戏规则来解读和遵守是我常用的思维方式,这个游戏规则的概念就是我所说的一个规则。 下一节预告,我眼中学习单片机的四个阶段。

第二节:我眼中学习单片机的四个阶段。

第一阶段:学会C语言的常用语法,熟悉51单片机开发平台软件keil的操作,了解单片机的大概原理特性,能读懂按键,数码管,跑马灯,串口的简单程序,能熟悉几个常用的外围芯片驱动。网上这方面优秀的教程很多,我正在写的这个连载《从业十年,教你51单片机入门基础》也是属于这类入门教程之一。

第二阶段:我认为这个阶段是四个阶段中最重要的阶段。很多初学者完成了第一阶段的学习,真正去面对一个小项目的时候,还是无从下手。他们不知道按键,显示,通讯,应用程序之间是如何关联起来的,他们一旦遇到多任务项目的时候不知道如何并行处理,他们最缺的是程序的框架思路。网上有很多热心牛人分享的程序框架思想,都值得大家学习和借鉴。我平时做项目是用状态机的思路,就是用switch语句实现多任务的切换,再外加一个定时中断产生不同的时间计时,有兴趣的朋友可以看看我去年在本论坛写的连载贴子《从业将近十年,手把手教你单片机程序框架》。

第三阶段:大家在做项目时,除了写单片机的软件,还不可避免的要跟整个电路的硬件打交道,掌握一些常用的硬件电路知识就显得尤其重要。电阻,电容,电感,二极管,三极管,光藕的应用,电压差和参考地的关系,两系统通信时需不需要共地的原因,隔离与非隔离的本质,常见的变压整流电路,常见的外围驱动电路等等。这方面优秀的电子基础教程很多,大家应该主动找这方面的资料来学习学习,我过一两年后也打算写写这方面的连载贴子《从业十年,单片机常用硬件知识讲解》。

第四阶段:有了前面三个阶段的主动学习和积累,就可以去做项目了,在项目中学习。

根据工作的需要来选择学习哪个厂家的单片机,比如PIC,AVR,stm32等单片机厂家;根据工作的需要来决定是否需要学习汇编语言,有一些台湾厂家的单片机并不能用C语言开发,只能用汇编;根据工作的需要来深入研究相关行业所需的硬件电路知识;根据工作的需要来学习相关的外围芯片驱动程序,这个阶段的学习正如郭天翔老师所讲的“缺什么补什么”。我本人也打算过两三年后写写这方面的贴子,作为大家项目开发时的参考工具书来用,叫《从业十年,单片机常用外围驱动程序集》。

下节预告,单片机一个最重要的特性。

第三节:单片机一个最重要的特性。

“道生一,一生二,二生三,三生万物。”《道德经》认为,世间万物,缤纷多彩,它们都起源自一个东西,这个“一”的东西就是“道”。电子世界也存在“一”这个东西,这个“一”繁衍出手机,电脑,电视机,机器人等丰富多彩的电子世界。这个“一”就是单片机一个最重要的特性:程序下载进单片机的内存后,可以识别管脚上的高低电平信号,管脚也可以输出不同时间长度的高低电平。下面我把这句话的5个重要关健词提取出来,详细解读它的含义。 程序。有3种,C程序,汇编程序,机器程序。能下载进单片机的只有机器程序,C程序和汇编程序都不能直接下载进单片机,所以C程序和汇编程序最终要经过专用编译软件翻译成机器程序后,才能下载进单片机执行。程序就是语言,语言就是用来交流的,交流就必须存在两个对象,这两个对象分别是程序员和单片机。程序员用C语言或者汇编语言,单片机只用机器语言,他们两者交流就必需一个翻译家,这个翻译家就是编译软件,俗称编译器,它专门把C语言或者汇编语言翻译成单片机能识别的机器语言。现在单片机开发的主流是用C语言,我本人出来工作后就从来没有用过汇编语言,所以我的观点是,C语言是必修课,汇编语言是选修课;C语言是白话文简单易懂,汇编语言是文言文繁琐难读。当然汇编也有它的优点和不可替代的场合,汇编的翻译效率高,往往是一句汇编语言对应一句机器语言,而一句C语言有可能对应几句机器语言,所以很多嵌入式系统某段要求简洁高效的源代码都是用汇编来写的,也有少数一些很便宜的单片机不提供C编译器,只能用汇编语言开发。所以要不要学汇编,我的建议是最好根据个人的工作需求来决定。

内存。既然程序可下载进单片机,那么单片机必然有一个存储程序的内存。单片机内存包括ROM和RAM两部分。ROM的优点是掉电后存储的内容不会丢失,缺点是除非在烧录(下载)过程中,否则上电后它存储的内容也不能更改。并且,虽然ROM在烧录(下载)过程中可以更改内容,但是更改的次数有限制,也就是烧录(下载)的次数有限制,一般最大次数是10万次,当然这里所说ROM是指FLASH的单片机,如果是OTP的单片机,那么最大次数是1次。而RAM恰好反过来,RAM的优点是上电后存储的内容可以被程序指令随时更改,而且还没有更改次数限制,缺点是掉电后内容会丢失。正因为ROM和RAM各有特点,所以它们的分工有所不同。程序包括指令和数据两部分。指令是指程序中的判断,跳转,赋值等指令,这些内容是程序烧录进单片机时就固定下来的,不可更改的,所以存储在ROM中。数据也分两种,程序指令可更改的数据和程序指令不可更改的数据。程序指令可更改的数据存储在RAM中,程序指令不可更改的数据存储在ROM中。那么谁在幕后进行这些分类存储?是编译器软件和下载器(烧录器)。编译器除了把C语言翻译成机器语言之外,还帮我们分好了类,分配好了存储的地址和位置,下载器(烧录器)再根据这些信息把程序存储到内存中。

管脚。它是单片机与外部电路进行能量和信息交互的桥梁。有电源,复位,晶振和IO口这4种类型管脚。第一种电源管脚。是给单片机内部电路供电的接口。单片机有两种常用的供电电压,一般不是3.3V就是5V,有的单片机两种电压都兼容。第二种复位管脚。单片机上电后需要外部电路给它一个瞬间高电平或者低电平的复位信号,才能启动工作。这类外

部的复位电路通常是用电容和电阻组成的充电电路来实现,也有一些系统是用专门的复位芯片来实现。第三种晶振管脚。任何单片机想要工作必须要有晶振。单片机执行程序指令是按一个节拍一个节拍来执行的。而晶振产生固定频率的脉冲就是这个节拍的基础源泉。所以把晶振比喻成单片机的心脏是非常恰当的。当然,现在很多单片机都把晶振集成到内部了,不用再外接晶振。第四种IO口管脚。这是跟我们编写程序关联最密切的管脚。前面提到的电源,复位,晶振这3种管脚是为了让单片机能工作,俗称单片机工作的三要素。而单片机工作的具体内容就是通过IO口管脚来体现的。比如,IO口能识别按健的输入,也能驱动继电器的开关,也能跟外围器件进行通信。

电平。单片机IO口管脚检测到的电压低于或等于0.8V时是低电平,程序里读取到的是0数字。检测到的电压高于或等于2.4V时是高电平,程序里读取到的是1数字,当然IO口输入的最大电压不能超过单片机的供电电压。单片机输出的低电平是0V,单片机输出的高电平等于它的供电电压值。

时间。时间是单片机程序必不可少的一个元素。跟外围芯片通信的时序节拍需要时间,驱动发光二极管闪烁需要时间,工控自动化的某些延时需要时间。单片机的时间来源自两方面。第一方面源自指令的周期时间。单片机是根据节拍来执行程序指令的,所以每执行一条指令都要消耗一点时间,只要让程序执行一些无实际意义的指令,并且通过调整所执行指令的条数就可以得到所需要的时间长度。第二方面源自单片机内部自带的定时器。假如设置定时器每20毫秒产生一次中断,现在要获取10秒钟的时间,只需程序统记500次定时中断就可以了,因为1秒等于1000毫秒。

下节预告,平台软件和编译器软件的简介。

第四节:平台软件和编译器软件的简介。

C语言代码写在哪里,谁负责把它翻译成Hex格式机器码?这就涉及到编辑和编译,从而诞生了平台和编译这两种软件。平台软件负责编辑源代码,编译软件负责把源代码翻译成Hex格式的机器码。

不同厂家的单片机,它所用的平台和编译器软件都不一样。即使是同样一个厂家的单片机,它也有可能存在多种不同的第三方平台软件和编译器软件,下面列举的一些例子只是主流的平台和编译软件,并不是说它们是唯一的。

PIC单片机的平台软件是MPLAB,8位单片机是PICC编译器,12位单片机是PIC18编译器,16位单片机是C30编译器。这个例子从侧面也说明了一个平台软件可以嵌入多种不同的编译器软件,平台软件和编译器软件存在一对多的关系。

51单片机的平台软件是keil,编译器是C51。

以上所述,单片机程序开发需要用到两种软件,但是实际项目开发的时候,我们只是跟平台软件打交道就可以了,因为编译器软件是当做一种独立配件嵌入到平台软件里,统一受平台软件控制。我在用PIC的8位单片机时,需要安装一次MPLAB平台软件,也需要独立再安装一次PICC编译器软件,然后运行MPLAB平台软件,在里面操作某个菜单设置选项,把PICC编译器跟MPLAB平台软件关联起来,也就是我所说的把PICC编译器嵌入到MPLAB平台软件里,统一接受平台软件的控制,但我写代码只需要跟MPLAB平台软件打交道就可以了。我早期在做51单片机开发时,也是需要把keil平台软件和C51软件分开安装,然后再把它们关联起来,但是现在从keil2版本开始,在安装keil平台软件时就已经默认把C51安装好了,并且自动把C51嵌入到了keil平台软件。我现在用keil4这个版本的平台软件,只需要安装一次keil平台软件就可以了,不需要像早期那样再单独安装C51编译器。

第五节:用keil软件新建,关闭,打开一个完整工程的操作流程。

Keil平台软件的安装我就不多讲了,网上这方面的资料很多,大家可以百度一下如何安装keil的教程。下面开始讲解用keil软件新建,关闭,打开一个完整工程的操作流程。 第一步:新建一个工程文件夹。先在电脑D盘目录下新建一个文件夹,取名为“stc89c52rc”。

有2个地方需要解释:

(1)文件夹以及后面所取的文件名不要用中文,请全部用英文,数字,或者下划线这些字符。keil软件支不支持中文名无所谓,但是在单片机这个行业,有一些单片机厂家的平台软件,某些版本是不支持中文名的,所以大家养成这个习惯,以后可以避免遇到一些不必要的麻烦。

(2)新建的文件夹请直接放在某盘的根目录下,而不要放到某个已有文件夹的目录下。一方面是因为已有的文件名往往带有中文字,另外一方面是有一些单片机厂家的平台软件不支持嵌入层次太深的文件目录,所以大家养成这个习惯,以后可以避免遇到一些不必要的麻烦。

第二步:启动keil软件。双击桌面”keil uVision4”的图标启动keil软件。

第三步:关闭默认被打开的已有工程。打开keil软件时,如果发现此软件默认打开了一个之前已经存在的工程,请先关闭此工程。如果默认没有打开已有工程,这一步可以忽略跳过。关闭已有工程的操作是这样子的:点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”即可。

第四步:利用工具向导新建一个工程。点击上面”Project”选项,在弹出的下拉菜单中选择“new uVision Project...”,在弹出的对话框中,选择保存的目录是刚才第一步新建的文件夹“stc89c52rc”目录下,输入跟文件夹名称一样的文件名“stc89c52rc”,然后单击“保存”按键,此时会弹出一个选择单片机型号的对话框,双击”Atmel”这个厂家,在展开的下拉选项中选中“AT89C52”这个型号,然后点击“OK”,此时会弹出一个英文询问框“是否要复制STARTUP.A51这个文件到工程里?”我们单击“否”即可。

有3个地方需要解释:

(1)以上新建的保存文件名应该跟我们第一步在D盘新建的文件夹名称一致,因为有一些单片机厂家的平台软件是有这个要求的,所以大家养成这个习惯,以后可以避免遇到一些不必要的麻烦。

(2)上面之所以选择Atmel厂家的AT89C52单片机,是因为朱兆祺51学习板所用的单片机是STC89C52RC这个单片机,而STC89C52RC跟AT89C52是兼容的。

(3)在弹出的询问框“是否要复制STARTUP.A51这个文件到工程里?”中,STARTUP.A51这个文件有什么含义?STARTUP.A51是一个启动程序文件,在单片机进入.c程序执行main函数之前,先去执行这个启动程序,这个启动程序是专门用来初始化RAM和设置堆栈等,如果我们选“否”不添加这个启动程序,编译器也会自动加入一段我们不能更改的默认启动程序。如果选“是”,那么这个文件就会出现在我们工程里,我们可以根据需要进行更改。但是大多数的情况下,我们都不会去更改这个文件的,所以无论你选“是”还是“否”,只要你不更改START.A51这个文件,对我们都是一样的。我本人一般情况下都是选“否”。

第五步:新建一个.c源文件。点击上面”File”选项,在弹出的下拉菜单中选择“New ...”,会看到出来一个名字为”Text1”的文件。再一次点击上面”File”选项,在弹出的下拉菜单中选择“Save”,会弹出一个保存的对话框,还是选择保存在第一步新建的文件夹目录下,文件名取“stc89c52rc.c”,单击“保存”。

有2个地方需要解释:

(1)以上所取的文件名必须带.c这个扩展名,表示此文件是C文件格式。

(2)第五步仅仅相当于在工程文件夹里新建了一个.c格式的C文件,此C文件目前跟工程还没有任何关联。

第六步:把刚才新建的.c源文件添加到工程里,跟工程建立起关联的关系。点击左边”Porject”选项框里面的”Target 1”前面的“+”号(如果没有发现Project,请按以下第2条解释操作),在展开的下拉菜单下看到“Source Group 1”。右键单击“Source Group 1”选项,在下拉菜单中选择“Add Existing Files to Group ?Source Group 1?...”选项,弹出一个文件选择对话框,单击选中刚才新建的.c源文件,然后单击一次“Add”按钮,此时虽然对话框没有关闭,但是已经把.c源文件添加到工程里了,这时只要再点击一次“Close”按钮即可把此对话框关闭。这时发现左边的“Source Group 1”前面多了一个”+”号,单击此”+”号展开,发现下面刚才我们新添加进去的.c源文件“stc89c52rc.c”。

有2个地方需要解释:

(1)以上有一个地方,我本人觉得keil软件的用户体验做得不够好,容易引起误解。在弹出一个文件选择对话框时,先单击选中刚才新建的.c源文件,此时单击一次“Add”按钮,已经相当于把.c文件添加进工程了,但是此时keil软件并没有自动关闭对话框,这样很容易让初学者误以为.c源文件还没有被添加进去。

(2)如果没有以上操作的时候没有发现左边Project窗口,请点击左下角的Project选项来切换。

第七步:双击打开左边被添加进工程的“stc89c52rc.c”.c源文件,就可以在此“stc89c52rc.c”文件下输入我们的C语言代码了,请把以下范例代码复制进去,然后再一次点击”File”选项,在弹出的下拉菜单中选择“Save”保存。此时,新建一个工程的步骤已经完成。

供复制的范例代码:

1. #include "REG52.H"

2.

3. void delay_long(unsigned int uiDelayLong); //延时函数

4.

5. sbit led_dr=P3^5;

6.

7. void main()

8. {

9. while(1)

10. {

11. led_dr=1; //LED亮

12. delay_long(100); //延时50000个空指令的时间

13. led_dr=0; //LED灭

14. delay_long(100); //延时50000个空指令的时间

15. }

16. }

17.

18. void delay_long(unsigned int uiDelayLong) //延时函数

19. {

20. unsigned int i;

21. unsigned int j;

22. for(i=0;i<uiDelayLong;i++)

23. {

24. for(j=0;j<500;j++); //内嵌循环的空指令数量

25. }

26. }

27.

复制代码

有1个地方需要解释:

(1)把代码复制到keil4时,中文注释出现乱码怎么办?解决办法如下:

点击左上角"Edit",在下拉菜单中选最后一项“Configuration”,在弹出的对话框中把Encoding的选项改成“Chinese GB2312(Simplified)”.

重新复制一次代码进去就恢复正常了。

第八步:打开一个现成的工程。前面七步已经讲解完了如何新建一个工程,现在教如何打开一个现成的工程。先单击右上角”X”关闭整个keil软件,然后双击桌面”keil uVision4”的图标重新启动keil软件,如果发现此软件默认打开了一个之前已经存在的工程,请先按照前面第三步关闭此工程。然后,点击上面”Project”选项,在弹出的下拉菜单中选择“Open Project...”,在弹出的文件对话框中,找到第一步新建的工程文件夹,单击选中“stc89c52rc.uvproj”这个文件名,然后点击“打开”,就可以打开一个现有的工程文件了。

下节预告:把.c源代码编译成.hex机器码的操作流程。

第六节:把.c源代码编译成.hex机器码的操作流程。

第一步:打开一个现成的工程。双击桌面”keil uVision4”的图标启动keil软件,如果发现此软件默认打开了一个之前已经存在的工程,请点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”先关闭当前工程。然后,继续点击上面”Project”选项,在弹出的下拉菜单中选择“Open Project...”,在弹出的文件对话框中,在D盘找到上一节已经建立的工程文件夹stc89c52rc,单击选中“stc89c52rc.uvproj”这个文件名,点击“打开”,就可以打开一个现有的工程了。

第二步:设置编译环境让它允许产生.hex格式的机器码文件。鼠标右键点击选中左边”Porject”选项框里面的”Target 1”选项,在右键下拉菜单中选择“Options for Target‘Target 1’...”选项,弹出一个编译环境设置对话框,左键单击上面子菜单切换到“Output”窗口下,把“Create

Hex File”勾选上。点击“OK”退出。

有1个地方需要解释:

(1)这个选项很重要,必须把“Create Hex File”选项勾上,否则后续的操作不能在工程文件夹的目录里生成.Hex的机器码文件。对于一个工程模板,只需要设置一次就可以保存起来的,下次开电脑重新打开此工程模板时不需要再设置,这些被设置的参数都是能掉电保存起来的。

第三步:启动编译。在确保stc89c52rc.c源文件里面有C语言源代码的情况下,点击上面”Project”选项,在弹出的下拉菜单中点击“Rebuild all target files”编译命令,编译器开始编译工作。

第四步:在”Build Output”窗口下观察编译结果。可以在最下方的”Build Output”窗口下观察到编译的过程提示。如果没有发现”Build Output”窗口,请把鼠标的光标移动到最下方的滑动条下边,当它呈现移动光标的形状时,按住左键往上拖动就可以看到“Build Output”窗口了。当“Build Output”窗口提示显示“creating hex file from "stc89c52rc"..."stc89c52rc" - 0 Error(s), 0 Warning(s).”等信息时,表示翻译工程结束了。其中0 Error(s)代表编译成功,没有任何错误。0 Warning(s)代表没有任何警告。只要有一个错误Error产生,就说明编译不通过。如果没有任何错误Error产生,但是有几个警告Warning产生,在这种情况下很多时候都不影响程序的正常运行,只有少数情况下是会影响代码的正常运行的,因此我本人建议哪怕是一个警告,大家也不要放过它,要找到产生这个警告的原因。查找错误的时候,只需要双击错误提示error那行内容,光标就会自动跳到源代码错误的附近,方便大家寻找语法错误。

最终观察到的Build Output窗口如下:

第五步:编译后生成.hex机器码文件的目录位置。以上编译成功后,我们只要打开电脑D盘的stc89c52rc文件夹,就可以找到.hex扩展名的机器码文件,这个文件就是我们要下载到单片机的机器码文件。

下节预告:利用现有工程模板编译不同项目源代码的方法以及代码备份管理技巧。

第七节:重复利用现有工程模板进行程序开发的方法以及代码备份管理技巧。

是不是每做一个新项目都要新建一个工程?在同一个项目中,是不是每修改一次源代码都要新建一个工程?很多情况下都不用。这节介绍如何重复利用现有工程模板进行程序开发的方法以及代码备份管理技巧。

重复利用现有工程模板,有三个必须。第一个必须是一个源文件的,而不是多文件编程(大家暂时不了解啥叫多文件编程也没关系)。第二个必须是同样的厂家同样的单片机型号。第三个必须进行代码备份管理,每完成一个项目的小进度,都要及时把源代码存储到电脑硬盘里,电脑硬盘里每个项目对应一个项目文件夹,每个项目文件夹里包含很多不同版本编号的源代码文件,每个源代码文件名必须有流水编号,方便识别最新版本的程序,每天下班前都要把最新版本的源代码文件上传到网盘备份,在互联网时代,把源代码存到自己的网盘,可以随时异地存取,即使遇到电脑故障损坏也不担心数据永久丢失。

现在举一个例子来介绍它的操作流程。要修改一个LED项目的源代码,电脑D盘上已经有一个“LED项目”的文件夹,文件夹里已经有一个名称为”LED_1”的源代码文件,这个文件是.txt格式的文本文档,文件名称的后缀_1代表流水编号,要求修改此源代码后,再保存在此文件夹目录下的”LED_2”文本文档里,并且上传到网盘进行备份。

第一步:打开一个现有的keil工程。双击桌面”keil uVision4”的图标启动keil软件,如果发现此软件默认打开了一个之前已经存在的工程,请点击上面”Project”选项,在弹出

的下拉菜单中选择“Close Project”先关闭当前工程。然后,继续点击上面”Project”选项,在弹出的下拉菜单中选择“Open Project...”,在弹出的文件对话框中,在D盘目录下找到之前已经建立的工程文件夹stc89c52rc,单击选中“stc89c52rc.uvproj”这个文件名,点击“打开”,就可以打开一个现有的工程了。

第二步:把当前keil工程的全部源代码清空。用Ctrl+A快捷键选中当前工程的全部源代码,按下Backspace退格按键就可以清空当前工程的全部源代码。

第三步:把最新版本的源代码导入到当前的keil工程中。在电脑D盘的“LED项目”文件夹目录下,双击打开“LED_1”的文本文档,用Ctrl+A快捷键选中文本文档的全部源代码,再用Ctrl+C快捷键复制此源代码,切换到keil工程中,把光标移动到工程的源代码编辑区,再用Ctrl+V快捷键粘贴此源代码到keil工程里。以下是复制粘贴到keil工程的源代码:

#include "REG52.H"

void delay_long(unsigned int uiDelayLong); //延时函数

sbit led_dr=P3^5;

void main()

{

while(1)

{

led_dr=1; //LED亮

delay_long(100); //延时50000个空指令的时间

led_dr=0; //LED灭

delay_long(100); //延时50000个空指令的时间

}

}

void delay_long(unsigned int uiDelayLong) //延时函数

{

unsigned int i;

unsigned int j;

for(i=0;i<uiDelayLong;i++)

{

for(j=0;j<500;j++); //内嵌循环的空指令数量

}

}

复制代码

第四步:在keil工程中修改此源代码。把“led_dr=0; //LED灭”这行代码删掉,修改后变成以下代码:

#include "REG52.H"

void delay_long(unsigned int uiDelayLong); //延时函数

sbit led_dr=P3^5;

void main()

{

while(1)

{

led_dr=1; //LED亮

delay_long(100); //延时50000个空指令的时间

delay_long(100); //延时50000个空指令的时间

}

}

void delay_long(unsigned int uiDelayLong) //延时函数

{

unsigned int i;

unsigned int j;

for(i=0;i<uiDelayLong;i++)

{

for(j=0;j<500;j++); //内嵌循环的空指令数量

}

}

复制代码

第五步:启动编译。点击上面”Project”选项,在弹出的下拉菜单中点击“Rebuild all target files”编译命令,编译结束后显示编译操作成功。

第六步:把在keil工程里修改后的源代码备份到电脑硬盘里。

(1)先在D盘的”LED项目”文件夹目录下,点击鼠标右键新建一个文本文档,再右键选中此文本文档图标,重命名为”LED_2”,然后双击打开此文本文档。

(2)切换到keil工程的源代码中,用Ctrl+A快捷键选中keil工程的全部源代码,用Ctrl+C快捷键复制此代码,接着切换回D盘的”LED_2”的文本文档,用Ctrl+V快捷键把修改后的代码粘贴到D盘的”LED_2”的文本文档,并且打开文本文档左上角“文件”的下拉菜单,点击“保存”按钮保存,最后关闭此文本文档。

第七步:把"LED_2"文本文档上传到网盘里备份。我本人比较喜欢用115网盘。关于115网盘的操作,大家可以百度搜索“115网盘”。

下节预告:把.hex机器码下载到单片机的操作流程。

第八节:把.hex机器码下载到单片机的操作流程。

烧录程序也叫下载程序。下载程序的本质是什么?把单片机当做一个存储器,每一条程序指令都对应一个唯一的存储地址,把这些指令一条条存储到指定的存储地址中,这就是下载程序的本质。对于STC89C52RC单片机,在下载程序时需要上位机界面软件和一根USB转串

口线。上位机界面软件负责把指定.hex格式的机器码文件打开,.hex格式的机器码文件里面记录着每条程序指令对应的地址信息,在下载过程中,上位机界面软件根据.hex记录的指令内容和对应的地址信息,经过USB转串口线,跟单片机的内置引导程序进行串口通讯,从而把.hex记录的信息传输到单片机内部的flash存储器中,实现了程序的下载。

在讲操作流程之前,请读者先把以下一个LED灯闪烁的代码编译成.hex格式的文件,这个.hex文件保存在D盘的”stc89c52rc”文件夹里。

#include "REG52.H"

void delay_long(unsigned int uiDelayLong); //延时函数

sbit led_dr=P3^5;

void main()

{

while(1)

{

led_dr=1; //LED亮

delay_long(100); //延时50000个空指令的时间

led_dr=0; //LED亮

delay_long(100); //延时50000个空指令的时间

}

}

void delay_long(unsigned int uiDelayLong) //延时函数

{

unsigned int i;

unsigned int j;

for(i=0;i<uiDelayLong;i++)

{

for(j=0;j<500;j++); //内嵌循环的空指令数量

}

}

复制代码

下面详细讲解把.hex机器码下载到单片机的操作流程。

第一步:安装USB转串口驱动程序的操作流程。所谓上位机界面软件就是安装在电脑端的界面软件,电脑跟单片机进行通讯,需要一根USB转串口线,欲使USB转串口线正常工作,必须预先安装一个USB转串口的驱动程序。具体的操作是这样的:在网盘中下载”51CTO下载-CH340SER(win7 64位可用).zip”这个压缩包文件,解压后分成“CH341SER”和“INSTALL”这两个文件夹,双击打开“CH341SER”这个文件夹,找到“SETUP.EXE”这个安装应用程序,双击启动,在弹出的界面中,单击“安装”按钮即可完成驱动程序的安装。

第二步:记录串口号。我用的电脑是XP系统,现在以XP系统为例。插入USB转串口

线,右击桌面“我的电脑”,选择下拉菜单的“设备管理器”,在弹出的窗口中,点击“端

口”前面的+号,在展开的选项中,会看到“USB-SERTAL CH340(COM6)”这个信息,这

个COM6就是要我们记住的串口号。你们的串口号不一定是COM6,请以你们电脑显示的

串口号为准。

第三步:打开上位机界面软件“STC_ISP”。这个软件可以在宏晶单片机的官网下载获

取此软件。双击打开“STC_ISP.exe”这个上位机界面软件。

第四步:选择单片机型号。在“单片机型号”的下拉菜单中选择“STC89C/LE52RC”这

个型号。如果中途弹出推荐选用其它型号的窗口,可以按确定忽略它,我们只要认准

“STC89C/LE52RC”这个型号就可以了。

第五步:设置串口号。在“串口号”的下拉菜单中,选择跟前面第二步所记录一样的串

口号。

第六步:设置最高波特率。在“最高波特率”的下拉菜单中,选择9600波特率。

第七步:连接硬件USB转串口线和电源线。USB转串口线一端已经连接电脑USB口,

另外一端9针串口端跟坚鸿51学习板的串口端连接。电源线一端用智能手机充电器的USB

端口供电5V,电源线另一端连接坚鸿51学习板的USB供电端口。

第八步:导入.hex格式的机器码文件。点击上位机界面软件的“打开程序文件”的按钮,

在弹出的对话框中,选择D盘下“stc89c52rc”文件夹目录下的“stc89c52rc.hex”,双击把

“stc89c52rc.hex”导入到上位机界面软件。

第九步:启动下载。点击上位机界面软件的“下载/编程”的按钮,发现“正在检测目标

单片机..”的提示信息,此时需要把51学习板重新断电后再上电,很多人也把这个重新上电

的过程称为“冷启动”。

第十步:“冷启动”后观察是否操作成功的信息。执行完前面第九步的“冷启动”后,

如果发现有“...操作成功!”的提示信息,就说明下载成功了。

第十一步:坚鸿51学习板下载程序失败时的解决办法。

(1)可以先松一下卡座,稍微挪动一下单片机,然后再卡紧单片机。卡座必须卡紧单片机, 避免接触不良。

(2)改变供电电源,很多电脑的USB口供电电源干扰非常大,严重影响下载程序,请把

USB电源线插入到手机充电器5V的USB接口,效果显著,明显提高了下载的成功率。

(3)检查确保选择单片机型号是STC89C/LE52RC,如果软件弹出推荐其它型号的单片机

窗口,不用管它,我们就选STC89C/LE52RC。

(4)检查STC-ISP烧写软件是否选择匹配的COM口。

(5)单片机是靠串口烧录程序进去的,单片机的串口是P3.0,P3.1两根线,在烧录程序时,

确保P3.0,P3.1这两根线的黄颜色跳帽必须插上,同时P3.0,P3.1两个IO口不能跳线到外围

器件上。

(6)点击“下载/编程”后,记得再断电并且重新上电一次。看看是否烧录成功。

(7)最低波特率一直设置为2400,然后把最高波特率先改成9600试一下,如果还不行再

把最高波特率改成2400试试。

(8)如果还不行,就退出软件,拔掉USB转串口线,同时断电(必须把整根电源线拔掉!),

重新插入USB串口线,重新插入电源线开电,重新打开软件。

(9)如果还不行,学习板先断电(必须把整根电源线拔掉!),然后重启一次电脑。

(10)总之:如果还不行,就按上述步骤多折腾几次。最后实在不行,就尝试更换到其它

USB口,或者尝试更换到其它电脑上试试。

第九节:程序从哪里开始,要到哪里去?

程序从哪里开始,要到哪里去?为了让初学者了解C语言程序的执行顺序,我把程序分成三个区域:进入主程序前的区域,主程序的初始化区域,主程序的循环区域。

进入主程序前的区域。这是上电后,在单片机执行主程序代码之前就已经完成了的工作。包括头文件的包含,宏定义,内存分配这些工作。这部分的内容可以暂时不用去了解,我会在后面的一些章节中陆续深入讲解。

主程序的初始化区域。这是上电后,单片机进入主程序后马上就要执行的程序代码,这部分区域的代码有一个特点,大家也必须记住的,就是单片机只执行一次。只要单片机不重启,不复位,那么上电后这部分的代码只被执行一次。

主程序的循环区域。单片机在主程序中执行完了初始化区域的代码,紧接着就进入这片循环区域的代码。单片机一直在循环执行这段代码,这就是上电后单片机的最终归宿,一直处在循环的状态。

下面我跟大家分析一个程序源代码的三个区域和执行顺序,大家先看中文解释部分的内容,暂时不用理解每行指令的语法。该源代码实现的功能是:上电后,蜂鸣器鸣叫一声就停止,然后看到一个LED灯一直在闪烁。本程序是基于坚鸿51单片机学习板。

#include "REG52.H" //进入主程序前的区域:头文件包含

sbit beep_dr=P2^7; //进入主程序前的区域:宏定义

sbit led_dr=P3^5; //进入主程序前的区域:宏定义

unsigned long i; //进入主程序前的区域:内存分配

void main() //主程序入口,即将进入初始化区域

{

beep_dr=0; //第一步:初始化区域:蜂鸣器开始鸣叫。

for(i=0;i<6250;i++); //第二步:初始化区域:延时0.5秒左右。也就是蜂鸣器鸣叫的持续时间。

beep_dr=1; //第三步:初始化区域:蜂鸣器停止鸣叫。 while(1) //执行完上面的初始化区域,即将进入循环区域 {

led_dr=1; //第四步:循环区域:LED开始点亮。

for(i=0;i<6250;i++); //第五步:循环区域:延时0.5秒左右。也就是LED点亮的持续时间。

led_dr=0; //LED灭 //第六步:循环区域:LED开始熄灭。

for(i=0;i<6250;i++); //第七步:循环区域:延时0.5秒左右。也就是LED熄灭的持续时间。马上返回上面第四步继续循环往下执行。

}

}

//解释:

//单片机进入主程序后,第一步到第三步是属于初始化区域,只被执行一次。然后进入循环区域,从第四步执行到第七步,

//执行完第七步之后,马上返回上面第四步继续循环往下执行,单片机一直处于第四步到第

七步的循环区域中。

复制代码

经过以上的分析,可以看出这三个区域的大概分布如下:

//...进入主程序前的区域

void main()

{

//...初始化区域

while(1)

{

//...循环区域

}

}

第十节:一个用来学习C语言的模板程序。

目前,几乎所有的初学者在学习和上机练习C语言的时候,都是在电脑上安装VC这个调试软件,在源代码里只要调用打印语句printf就可以观察到不同的变量结果,挺方便的。但是现在我要提出另外一种方法,学习单片机的C语言,不一定非要用VC调试软件,也可以直接在坚鸿51学习板上学习和上机练习的。我可以做一个调试模板程序给初学者使用,利用8位数码管和16个LED灯来显示不同的变量结果,利用3个按键来切换显示不同的变量,这样就能达到类似在VC平台下用printf语句来观察变量的效果。甚至我个人认为这样比用VC调试的效果还更加直观。现在重点介绍这个模板程序的使用。

在模板程序里,初学者只需要在主程序的初始化区域填入自己练习的C语言代码,最后把需要观察的变量赋值给窗口变量就可以了,其它部分的代码属于模板的监控调试代码,大家暂时不用读懂它,直接复制过来就可以了。上述所谓的“赋值”,就是“=”这个语句,它表面上像我们平时用的等于号,实际上不是等于号,而是代表“给”的意思,把“=”符号右边的数复制一份给左边的变量,比如“a=36;”就是代表把36这个数值复制一份给变量a,执行这条指令后,a就等于36了。这里的分号“;”代表一条程序指令的结束。窗口变量有几个?有哪些?一共有10个,分别是GuiWdData0,GuiWdData1,GuiWdData2,GuiWdData3,GuiWdData4,GuiWdData5,GuiWdData6,GuiWdData7,GuiWdData8,GuiWdData9。这10个窗口变量是给大家调试专用的,8位数码管可以切换显示10个窗口变量,最左边2位数码管代表窗口变量号,剩下6位数码管显示十进制的窗口变量数值,另外16个LED实时显示此数据的二进制格式。最左边2位数码管从“0-”到“9-”代表从第0个窗口变量到第9个窗口变量,也就是GuiWdData0依次到GuiWdData9。用S1和S5按键可以切换显示不同的窗口变量,按住S9不放可以观察到当前窗口变量的十六进制格式数据,松开S9按键后,又自动返回显示当前窗口变量的十进制数据。

该模板程序是基于坚鸿51学习板,现在跟大家分享这个程序,要让这10个窗口变量分别显示10,11,12,13,14,15,16,17,18,19这10个数,用S1按键可以切换显示从小往大的窗口变量号,用S5按键可以切换显示从大往小的窗口变量号。再强调一次,大家只需要关注主程序main函数的初始化区域就可以了,其它的代码请直接复制过来,不用理解。比如:

void main() //主程序

{

//...初始化区域

while(1)

{

}

}

详细的源代码如下:

#include "REG52.H"

#define const_voice_short 40

#define const_key_time1 20

#define const_key_time2 20

#define const_key_time3 20

void initial(void);

void delay_short(unsigned int uiDelayShort);

void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);

void display_drive(void);

void display_service(void);

void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void T0_time(void);

void key_service(void);

void key_scan(void);

sbit beep_dr=P2^7;

sbit key_sr1=P0^0;

sbit key_sr2=P0^1;

sbit key_sr3=P0^2;

sbit key_gnd_dr=P0^4;

sbit led_dr=P3^5;

sbit dig_hc595_sh_dr=P2^0;

sbit dig_hc595_st_dr=P2^1;

sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;

sbit hc595_st_dr=P2^4;

sbit hc595_ds_dr=P2^5;

unsigned char GucKeySec=0;

unsigned char GucKey3Sr=1;

unsigned int GuiVoiceCnt=0;

unsigned char GucVoiceStart=0;

unsigned char GucDigShow8;

unsigned char GucDigShow7;

unsigned char GucDigShow6;

unsigned char GucDigShow5;

unsigned char GucDigShow4;

unsigned char GucDigShow3;

unsigned char GucDigShow2;

unsigned char GucDigShow1;

unsigned char GucDisplayUpdate=1;

unsigned char GucWd=0;

unsigned int GuiWdData0=0;

unsigned int GuiWdData1=0;

unsigned int GuiWdData2=0;

unsigned int GuiWdData3=0;

unsigned int GuiWdData4=0;

unsigned int GuiWdData5=0;

unsigned int GuiWdData6=0;

unsigned int GuiWdData7=0;

unsigned int GuiWdData8=0;

unsigned int GuiWdData9=0;

code unsigned char dig_table[]=

{

0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00,0x40,

};

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

GuiWdData0=10; //把10这个数值放到窗口变量0里面显示

GuiWdData1=11; //把11这个数值放到窗口变量1里面显示

GuiWdData2=12; //把12这个数值放到窗口变量2里面显示

GuiWdData3=13; //把13这个数值放到窗口变量3里面显示

GuiWdData4=14; //把14这个数值放到窗口变量4里面显示

GuiWdData5=15; //把15这个数值放到窗口变量5里面显示

GuiWdData6=16; //把16这个数值放到窗口变量6里面显示

GuiWdData7=17; //把17这个数值放到窗口变量7里面显示

GuiWdData8=18; //把18这个数值放到窗口变量8里面显示

GuiWdData9=19; //把19这个数值放到窗口变量9里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1) {

initial(); key_service(); display_service(); } }

void display_service(void) {

static unsigned char SucLedStatus16_09=0; static unsigned char SucLedStatus08_01=0; static unsigned int SinWdDataTemp=0;

if(1==GucDisplayUpdate) {

GucDisplayUpdate=0;

switch(GucWd) { case 0:

GucDigShow8=0;

case 1:

GucDigShow8=1;

case 2:

GucDigShow8=2;

case 3:

GucDigShow8=3;

case 4:

GucDigShow8=4;

SinWdDataTemp=GuiWdData0; break; SinWdDataTemp=GuiWdData1; break; SinWdDataTemp=GuiWdData2; break; SinWdDataTemp=GuiWdData3; break; SinWdDataTemp=GuiWdData4; break;

case 5:

GucDigShow8=5;

SinWdDataTemp=GuiWdData5; break;

case 6:

GucDigShow8=6;

SinWdDataTemp=GuiWdData6; break;

case 7:

GucDigShow8=7;

SinWdDataTemp=GuiWdData7; break;

case 8:

GucDigShow8=8;

SinWdDataTemp=GuiWdData8; break;

case 9:

GucDigShow8=9;

SinWdDataTemp=GuiWdData9; break; }

GucDigShow7=17;

GucDigShow6=16;

if(1==GucKey3Sr)

{

if(SinWdDataTemp>=10000)

{

GucDigShow5=SinWdDataTemp/10000;

}

else

{

GucDigShow5=16;

}

if(SinWdDataTemp>=1000)

{

GucDigShow4=SinWdDataTemp%10000/1000;

}

else

{

GucDigShow4=16;

}

if(SinWdDataTemp>=100) {

GucDigShow3=SinWdDataTemp%1000/100; }

else

{

GucDigShow3=16;

}

if(SinWdDataTemp>=10) {

GucDigShow2=SinWdDataTemp%100/10; }

else

{

GucDigShow2=16;

}

GucDigShow1=SinWdDataTemp%10;

}

else

{

GucDigShow5=16;

if(SinWdDataTemp>=0x1000) {

GucDigShow4=SinWdDataTemp/0x1000;

}

else

{

GucDigShow4=16;

}

if(SinWdDataTemp>=0x0100) {

GucDigShow3=SinWdDataTemp%0x1000/0x0100; }

else

{

GucDigShow3=16;

}

if(SinWdDataTemp>=0x0010)

{

GucDigShow2=SinWdDataTemp%0x0100/0x0010;

}

else

{

GucDigShow2=16;

}

GucDigShow1=SinWdDataTemp%0x0010;

}

SucLedStatus16_09=SinWdDataTemp>>8; SucLedStatus08_01=SinWdDataTemp;

hc595_drive(SucLedStatus16_09,SucLedStatus08_01);

}

}

void key_scan(void)

{

static unsigned int SuiKeyTimeCnt1=0;

static unsigned char SucKeyLock1=0;

static unsigned int SuiKeyTimeCnt2=0;

static unsigned char SucKeyLock2=0;

static unsigned int SuiKey3Cnt1=0;

static unsigned int SuiKey3Cnt2=0;

if(1==key_sr1)

{

SucKeyLock1=0;

SuiKeyTimeCnt1=0;

}

else if(0==SucKeyLock1)

{

SuiKeyTimeCnt1++;

if(SuiKeyTimeCnt1>const_key_time1)

{

SuiKeyTimeCnt1=0;

SucKeyLock1=1;

GucKeySec=1;

}

}

if(1==key_sr2)

{

SucKeyLock2=0;

SuiKeyTimeCnt2=0;

}

else if(0==SucKeyLock2)

{

SuiKeyTimeCnt2++;

if(SuiKeyTimeCnt2>const_key_time2) {

SuiKeyTimeCnt2=0;

SucKeyLock2=1;

GucKeySec=2;

}

}

if(1==key_sr3)

{

SuiKey3Cnt1=0;

SuiKey3Cnt2++;

if(SuiKey3Cnt2>const_key_time3) {

SuiKey3Cnt2=0;

GucKey3Sr=1;

}

}

else

{

SuiKey3Cnt2=0;

SuiKey3Cnt1++;

if(SuiKey3Cnt1>const_key_time3) {

SuiKey3Cnt1=0;

GucKey3Sr=0;

}

}

}

void key_service(void)

{

static unsigned char SucKey3SrRecord=1;

if(GucKey3Sr!=SucKey3SrRecord)

{

SucKey3SrRecord=GucKey3Sr;

GucDisplayUpdate=1;

}

switch(GucKeySec)

{

case 1:

GucWd++;

if(GucWd>9)

{ GucWd=9;

}

GucDisplayUpdate=1; GuiVoiceCnt=const_voice_short; GucVoiceStart=1;

GucKeySec=0;

break;

case 2:

GucWd--;

if(GucWd>9)

{ GucWd=0;

}

GucDisplayUpdate=1;

GuiVoiceCnt=const_voice_short; GucVoiceStart=1;

GucKeySec=0;

break;

}

}

void display_drive()

{

static unsigned char SucDigShowTemp=0;

static unsigned char SucDisplayDriveStep=1;

switch(SucDisplayDriveStep)

{

case 1:

SucDigShowTemp=dig_table[GucDigShow1]; dig_hc595_drive(SucDigShowTemp,0xfe); break;

case 2:

SucDigShowTemp=dig_table[GucDigShow2]; dig_hc595_drive(SucDigShowTemp,0xfd); break;

case 3:

SucDigShowTemp=dig_table[GucDigShow3]; dig_hc595_drive(SucDigShowTemp,0xfb); break;

case 4:

SucDigShowTemp=dig_table[GucDigShow4]; dig_hc595_drive(SucDigShowTemp,0xf7); break;

case 5:

SucDigShowTemp=dig_table[GucDigShow5]; dig_hc595_drive(SucDigShowTemp,0xef); break;

case 6:

SucDigShowTemp=dig_table[GucDigShow6]; dig_hc595_drive(SucDigShowTemp,0xdf); break;

case 7:

SucDigShowTemp=dig_table[GucDigShow7]; dig_hc595_drive(SucDigShowTemp,0xbf); break;

case 8:

SucDigShowTemp=dig_table[GucDigShow8]; dig_hc595_drive(SucDigShowTemp,0x7f); break;

}

SucDisplayDriveStep++;

if(SucDisplayDriveStep>8)

{

SucDisplayDriveStep=1;

}

}

void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned ucDigStatusTemp08_01)

{

unsigned char i;

unsigned char ucTempData;

dig_hc595_sh_dr=0;

dig_hc595_st_dr=0;

ucTempData=ucDigStatusTemp16_09;

for(i=0;i<8;i++)

{

if(ucTempData>=0x80)dig_hc595_ds_dr=1;

else dig_hc595_ds_dr=0;

dig_hc595_sh_dr=0;

delay_short(1);

dig_hc595_sh_dr=1;

delay_short(1);

ucTempData=ucTempData<<1;

}

ucTempData=ucDigStatusTemp08_01;

for(i=0;i<8;i++)

{

if(ucTempData>=0x80)dig_hc595_ds_dr=1;

else dig_hc595_ds_dr=0;

dig_hc595_sh_dr=0;

delay_short(1); char

dig_hc595_sh_dr=1;

delay_short(1);

ucTempData=ucTempData<<1;

}

dig_hc595_st_dr=0;

delay_short(1);

dig_hc595_st_dr=1;

delay_short(1);

dig_hc595_sh_dr=0;

dig_hc595_st_dr=0;

dig_hc595_ds_dr=0;

}

void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) {

unsigned char i;

unsigned char ucTempData;

hc595_sh_dr=0;

hc595_st_dr=0;

ucTempData=ucLedStatusTemp16_09;

for(i=0;i<8;i++)

{

if(ucTempData>=0x80)hc595_ds_dr=1;

else hc595_ds_dr=0;

hc595_sh_dr=0;

delay_short(1);

hc595_sh_dr=1;

delay_short(1);

ucTempData=ucTempData<<1;

}

ucTempData=ucLedStatusTemp08_01;

for(i=0;i<8;i++)

{

if(ucTempData>=0x80)hc595_ds_dr=1;

else hc595_ds_dr=0;

hc595_sh_dr=0;

delay_short(1);

hc595_sh_dr=1;

delay_short(1);

ucTempData=ucTempData<<1; }

hc595_st_dr=0;

delay_short(1);

hc595_st_dr=1;

delay_short(1);

hc595_sh_dr=0;

hc595_st_dr=0;

hc595_ds_dr=0;

}

void T0_time(void) interrupt 1

{

TF0=0;

TR0=0;

if(1==GucVoiceStart)

{

if(GuiVoiceCnt!=0) {

GuiVoiceCnt--; beep_dr=0; }

else

{

beep_dr=1;

GucVoiceStart=0; }

}

key_scan();

display_drive();

TH0=0xfe;

TL0=0x0b;

TR0=1;

}

void delay_short(unsigned int uiDelayShort)

{

static unsigned int i;

for(i=0;i<uiDelayShort;i++);

}

void initial(void)

{

static unsigned char SucInitialLock=0;

if(0==SucInitialLock)

{

SucInitialLock=1;

key_gnd_dr=0;

led_dr=0;

beep_dr=1;

TMOD=0x01;

TH0=0xfe;

TL0=0x0b;

EA=1;

ET0=1;

TR0=1;

}

}

第十一节:变量的定义与赋值语句。

写程序到底是写什么?我用七个字概括是:对象之间的行为。假设以下a,b,c,d,e.这些都是对象,那么程序往往是对象之间的以下这些行为:

(1)把某个数值赋值给对象a。

(2)把对象b赋值给对象a。

(3)把对象b与对象c运算的结果赋值给对象a。

(4)如果对象d等于某个数值,则把某个数值赋值给对象a。

(5)如果对象d等于某个数值,则把对象b赋值给对象a。

(6)如果对象d等于某个数值,则把对象b与对象c运算的结果赋值给对象a。

(7)如果对象d等于对象e,则把某个数值赋值给对象a。

(8)如果对象d等于对象e,则把对象b赋值给对象a。

(9)如果对象d等于对象e,则把对象b与对象c运算的结果赋值给对象a。

(10)...等等,不一一列举。

从上述可以看出,程序的两个要素是:对象和行为。如果把对象看作是单片机的RAM数据存储器,那么行为就是单片机的ROM程序存储器。如果把对象看作是变量,那么行为就是指令语句。本节标题“变量的定义与赋值语句”,其中“变量的定义”就是对象,“赋值语句”就是行为。

变量的定义。一个程序最大允许有多少个对象,是由数据存储器RAM的字节数决定的(字节是一种单位,后面章节会讲到)。stc89c52rc这个单片机有几百个字节的RAM,但是并不意味着程序就一定要全部占用这些RAM。程序需要占用多少RAM,完全是根据程序的实际情况来决定,需要多少就申请多少。这里的“对象”就是变量。这里的“申请”就是变量的定义。

定义变量的关键字。常用有3种容量的变量,每种变量的取值范围不一样。第一种是”unsigned char”变量,取值范围从0到255,占用RAM一个字节,比喻成一房一厅。第二种是”unsigned int”变量,取值范围从0到65535,占用RAM两个字节,比喻成两房一厅。第三种是“unsigned long”变量,取值范围从0到4294967295,占用RAM三个字节,比喻成三房一厅。unsigned char,unsigned int和unsigned long都是定义变量的关键字。

定义变量的语法格式。定义变量的语法格式由3部分组成:关键字,变量名,分号。比如:

unsigned char a;

其中unsigned char就是关键字,a就是变量名,分号”;”就是一条语句的结束符号。 变量名的命名规则。变量名的第一个字符不能是数字,必须是字母或者下划线,字母或者下划线后面可以带数字,一个变量名之间的字符不能带空格。变量名不能跟编译器的关键字重名,不能跟函数名重名。比如:

unsigned char 3a; //不合法,第一个字符不能是数字。

unsigned char char; //不合法,char是编译器的关键字。

unsigned char a b; //不合法,ab是一个变量名,a与b的中间不能有空格。 unsigned char a; //合法。

unsigned char abc; //合法。

unsigned char _ab; //合法。

unsigned char _3ab; //合法。

unsigned char a123; //合法。

unsigned char a12ced; //合法。

定义变量与RAM的内在关系。当我们定义一个变量时,相当于向单片机申请了一个RAM空间。C编译器会自动为这个变量名分配一个RAM空间,每个字节的RAM空间都有一个固定的地址。把每个字节的RAM空间比喻成 房间,这个地址就是房号。地址是纯数字编号,不利于我们记忆,C语言编译器为了降低我们的工作难度,不用我们记每个变量的地址,只需要记住这个变量的名称就可以了。操作某个变量名,就相当于操作到对应地址的RAM空间。变量名与对应地址RAM空间的映射关系是C编译器暗中帮我们做好了。比如:

unsigned char a; //a占用一个字节的RAM空间,这个空间的地址由C编译自动分配。 unsigned char b; //b占用一个字节的RAM空间,这个空间的地址由C编译自动分配。 unsigned char c; //c占用一个字节的RAM空间,这个空间的地址由C编译自动分配。 上述a,b,c三个变量名占用一个字节的RAM空间,同时被C编译器分配了3个不同的RAM空间地址。

赋值语句的含义。赋值语句是行为。把右边对象的内容复制一份给左边对象。 赋值语句有一个很重要的特性,就是覆盖性,左边对象原来的内容会被右边对象复制过来的新内容所覆盖。比如,左边对象是变量a,原来a里面存的数据是3,右边对象是立即数6,执行赋值语句后,把6赋值给了对象a,那么a原来的数据3就被覆盖丢失了,变成了6.。 赋值语句的格式。赋值语句的语法格式由4部分组成:左边对象,关键字,右边对象,分号。比如:

a=b;

其中a就是左边对象。

其中“=”就是关键字。写法跟我们平时用的等于号是一样,但是在C语言里不是等于的意思,而是代表赋值的意思。跟等于号是两码事。

其中b就是右边对象。

其中分号“;”代表一条语句的结束符。

赋值语句与ROM的内在关系。赋值语句是行为,凡是程序的行为指令都存储在单片机的ROM区。C编译器会把一条赋值语句翻译成对应的一条或者几条机器码,机器码指令也是以字节为单位的。下载程序的时候,这些机器码就会被下载进单片机的ROM区。比如以下这行赋值语句:

a=b;

经过C编译器编译后会生成以字节为单位的机器码。这些机器码记录着这些信息:变量a的RAM地址,变量b的RAM地址,以及把b变量RAM地址里面的内容赋值到a变量地址里面的RAM空间。

变量定义的初始化。讲了赋值语句之后,再回过头来讲变量定义的初始化。变量定义之后,等于被C编译器分配了一个RAM空间,那么这个空间里面存储的数据是什么?如果没有刻意给它初始化,那么RAM空间里面存储的数据是不太确定的,是默认的。有些场合,需要在给变量分配RAM空间时就给它一个固定的初始值,这就是变量定义的初始化。变量初始化的语法格式由3部分组成:关键字,变量名赋值,分号。比如:

unsigned char a=9;

其中unsigned char就是关键字。

其中a=9就是变量名赋值。a从被C编译器分配RAM空间那一刻起,就默认是存了9这个数据。

分号”;”就是一条语句的结束符号。

接下来练习一个程序实例。直接复制前面章节中第十节的模板程序,只需要在main函数里编写练习代码,编译后,把程序下载进坚鸿51学习板,通过按S1或者S5按键即可在数码管上观察不同的变量数值。其它部分的模板程序代码就不贴出来了,详细的main函数源代码讲解如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了一个字节的RAM空间,里面保存的数

据是默认值0.

unsigned char b; //定义一个变量b,并且分配了一个字节的RAM空间,里面保存的数据是默认值0.

unsigned char c; //定义一个变量c,并且分配了一个字节的RAM空间,里面保存的数据是默认值0.

unsigned char d=9; //定义一个变量d,并且分配了一个字节的RAM空间,里面保存的数据被初始化成9.

b=3; //把3赋值给变量b,b原来的默认数据是0被覆盖了,此时变量b保存的数值是3 c=b; //把右边变量b的内容复制一份赋值给左边的变量c,c原来的默认数据0被覆盖了,此时,c保存的数值跟b的数值一样,都是3.

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

上坚鸿51学习板观察程序执行的结果:

变量a的数值是0,

变量b的数值是3,

变量c的数值是3,

变量d的数值是9,

第十二节:两个变量的数据交换。

为了加深理解赋值语句的一个重要特性“覆盖性”,本节利用赋值语句“=”做一个实验。要求把变量a与b的两个数据进行交换,假设a原来的数据是1,b原来的数据是5,交换数据后,a的数据应该变为5,b的数据应该变为1。

很多初学者刚看到这么简单的题目,会想当然的根据我们日常生活的思路,你把你的东西给

我,我把我的东西给你,就两个步骤,so easy!请直接复制第十节的模板程序,仅修改main函数后,main函数源代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a=1; //定义一个变量a,并且分配了一个字节的RAM空间,里面保存的数据被初始化成1.

unsigned char b=5; //定义一个变量b,并且分配了一个字节的RAM空间,里面保存的数据被初始化成5.

b=a; //第一步:为了交换,先把a的数赋值给b。

a=b; //第二步:为了交换,再把b的数赋值给a。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

上坚鸿51学习板观察程序执行的结果:

变量a的数值是1。

变量b的数值是1。

上述实验结果并没有达到交换数据的目的,为什么?因为赋值语句有一个重要的特性,就是覆盖性。分析如下:

b=a; //第一步

分析点评:执行第一步后,此时虽然b得到了a的数据1,但是b原来自己的数据5已经被覆盖丢失了!

a=b; //第二步

分析点评:由于b的数据在执行第一步后变成了1,执行第二步后,此时相当于把1赋值给a,并没有5!所以a和b的数据都是1,不能达到交换后“a为5,b为1”的目的。 上述交换数据的程序宣告失败!怎么办?既然赋值语句具有覆盖性,那么两变量想交换数据,就必须借助第三方寄存,此时只需要多定义一个第三方变量t。main函数源代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a=1; //定义一个变量a,并且分配了一个字节的RAM空间,里面保存的数据被初始化成1.

unsigned char b=5; //定义一个变量b,并且分配了一个字节的RAM空间,里面保存的数据被初始化成5.

unsigned char t; //定义一个变量t,并且分配了一个字节的RAM空间,里面默认是什么数据不重要。

t=b; //第一步:为了避免b的数据在执行第二步后被覆盖丢失,先把b的数据寄存在第三方变量t那里。

b=a; //第二步:把a的数赋值给b,b原来的数据虽然被覆盖丢失,但是b在t变量那里有备份,再也不用担心了。

a=t; //第三步:由于此时b已经获得了a的数据,如果想交换,此时只能把b在t变量里的备份赋值给a,而不能用b。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

上坚鸿51学习板观察程序执行的结果:

变量a的数值是5。

变量b的数值是1。

交换成功!

第十三节:二进制与字节单位,以及各种定义变量的取值范围。

为什么是二进制?人类日常生活明明是十进制的,为何数字电子领域偏要选择二进制?这是由数字硬件电路决定的。人有十个手指头,人可以发出十种不同声音来命名0,1,2,3...9这些数字,人可以肉眼识别十种不同状态的信息,但是数字电路要直接处理十进

制却很难,相对来说,二进制就轻松多了。一颗LED灯的亮与灭,一根IO口的输出是高电平和低电平,读取某一个点的电压是高于2V还是低于0.8V,只需要用三极管等元器件就可把处理电路搭建起来,二进制广泛应用在数字电路的存储,通讯和运算等领域,想学好单片机就必须掌握它。

二进制如何表示成千上万的数值?现在用LED灯的亮和灭来跟大家讲解。

(1)1个LED灯:

灭 第0种状态

亮 第1种状态

合计:共2种状态。

(2)2个LED灯挨着:

灭灭 第0种状态

灭亮 第1种状态

亮灭 第2种状态

亮亮 第3种状态

合计:共4种状态。

(3)3个LED灯挨着:

灭灭灭 第0种状态

灭灭亮 第1种状态

灭亮灭 第2种状态

灭亮亮 第3种状态

亮灭灭 第4种状态

亮灭亮 第5种状态

亮亮灭 第6种状态

亮亮亮 第7种状态

合计:共8种状态。

(4)8个LED灯挨着:

灭灭灭灭灭灭灭灭 第0种状态

灭灭灭灭灭灭灭亮 第1种状态

......

亮亮亮亮亮亮亮灭 第254种状态

亮亮亮亮亮亮亮亮 第255种状态

合计:共256种状态。

(5)16个LED灯挨着:

灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭 第0种状态

灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭亮 第1种状态

......

亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮灭 第65534种状态

亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮 第65535种状态

合计:共65536种状态。

(6)32个LED灯挨着:

灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭

第0种状态

灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭亮

第1种状态

......

亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮灭

第4294967294种状态

亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮

第4294967295种状态

合计:共4294967296种状态。

什么是位?以上一个LED灯就代表一位,8个LED灯就代表8位。一个变量的位数越大就意味着这个变量的取值范围越大。一个单片机的位数越多大,就说明这个单片机一次处理的数据范围就越大,意味着运算和处理速度就越快。我们日常所说的8位单片机,32位单片机,就是这个位的概念。为什么32位的单片机比8位单片机的处理和运算能力强,就是这个原因。位的英文名是用bit来表示。

什么是字节?字节是计算机很重要的一个基本单位,一个字节有8位。8个LED灯挨着能代表多少种状态,就意味着一个字节的数据范围有多大。从上面举的例子中,我们知道8个LED灯挨着,能表示从0到255种状态,所以一个字节的取值范围就是从0到255。 各种定义变量的取值范围。前面第十一节讲了常用变量的定义有3种,unsigned char,unsigned int ,unsigned long。但是没有讲到它们的取值范围,现在讲到二进制和字节了,可以回过头来跟大家讲讲这3种变量的取值范围,而且很重要。

unsigned char的变量占用1个字节RAM,共8位,根据前面LED灯的例子,取值范围是从0到255。

Unsigned int的变量占用2个字节RAM,共16位,根据前面LED灯的例子,取值范围是从0到65535。

Unsigned long的变量占用4个字节RAM,共32位,根据前面LED灯的例子,取值范围是从0到4294967295。

现在我们编写一个程序来验证unsigned char和unsigned int的取值范围。定义两个unsigned char变量a和b,a赋值255,b赋值256,255和256恰好处于unsigned char的取值边界。另外再定义两个unsigned int变量c和d,c赋值65535,d赋值65536,65535和65536恰好处于unsigned int的取值边界。最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。

unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。

unsigned int c; //定义一个变量c,并且分配了2个字节的RAM空间。

unsigned int d; //定义一个变量d,并且分配了2个字节的RAM空间。

a=255;//把255赋值给变量a,a此时会是什么数?会超范围溢出吗?

b=256;//把256赋值给变量b,b此时会是什么数?会超范围溢出吗?

c=65535;//把65535赋值给变量c,c此时会是什么数?会超范围溢出吗? d=65536;//把65536赋值给变量d,d此时会是什么数?会超范围溢出吗?

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

上坚鸿51学习板观察程序执行的结果如下:

unsigned char变量a的数值是255。

unsigned char变量b的数值是0。

unsigned int 变量c的数值是65535。

unsigned int 变量d的数值是0。

通过以上现象分析,我们知道unsigned char变量最大能取值到255,如果非要赋值256就会超出范围溢出后变成了0。而unsigned int变量最大能取值到65535,如果非要赋值65536就会超出范围溢出后变成了0。

多说一句,至于unsigned long的取值范围,大家暂时不用尝试,因为我现在给大家用的模板程序能观察的最大变量是16位的unsigned int类型,暂时不支持32位的unsigned long类型。

下节预告:二进制与十六进制。

第十四节:二进制与十六进制。

C51编译器并不支持二进制的书写格式,即使添加某个头文件后能支持二进制的书写格式,二进制的书写还是有个弊端,就是数字太多太长了,写起来非常费劲不方便,怎么办?解决办法就是用十六进制。十六进制是二进制的缩写,之所以称它为二进制的缩写,是因为它们的转换关系非常简单直观,不需要借助计算器即可相互转换。

何谓十六进制?欲搞清楚这个问题,还得先从十进制说起。所谓十进制,就是用一位字符可以表示从0到9这十个数字。所谓二进制,就是用一位字符可以表示从0到1这二个数字。所谓十六进制,当然也就是用一位字符可以表示从0到15这十六个数字。但是马上就会面临一个问题,十六进制的10到15这6个数其实是有两位字符组成的,并不是一位呀?于是C语言用一个字符A,B,C,D,E,F分别替代10,11,12,13,14,15这6个数,10前面的0到9还是跟十进制的字符一致。A,B,C,D,E,F也可以用小写a,b,c,d,e,f来替代,不区分大小写。

前面提到了十六进制是二进制的缩写,它们的转换关系非常简单直观,每1位十六进制的字符,对应4位二进制的字符。关系如下:

十进制 二进制 十六进制

0 0000 0

1 0001 1

2 0010 2

3 0011 3

4 0100 4

5 0101 5

6 0110 6

7 0111 7

8 1000 8

9 1001 9

10 1010 A

11 1011 B

12 1100 C

13 1101 D

14 1110 E

15 1111 F

二进制转换成十六进制的时候,如果不是4位的倍数,则最左边高位默认补上0凑合成4位的倍数。比如二进制101001,可以在左边补上2个0变成00101001,然后把每4位字符转成1个十六进制的字符。左边高4位0010对应十六进制的2,右边低4位1001对应十六进制的9,所以合起来最终的十六进制是29。

十六进制的标准书写格式。刚才提到的十六进制29,在C语言里不能直接写29,否则就跟十进制的写法混淆了。为了把十六进制和十进制的书写格式进行区分,C语言规定凡是十六进制必须加一个数字0和一个字母x作为前缀,也就是十六进制必须以0x作为前缀,刚才的十六进制29就应该写成0x29。凡是不加前缀的就默认为十进制。

现在我们编写一个程序来观察十六进制和二进制的关系,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。

unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。

unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。

unsigned char d; //定义一个变量d,并且分配了1个字节的RAM空间。

a=0x06; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。

b=0x0A; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。

c=0x0e; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。

d=0x2C; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察十六进制和二进制?S1和S5按键是切换窗口按键。按住S9按键不松手,就可以观察当前窗口数据的十六进制格式了。松开S9按键就是当前窗口的十进制数据格式。而坚鸿51学习板右上角的16个LED灯就代表了当前窗口的二进制,亮的代表1,灭的代表0。

上朱兆祺51学习板观察程序执行的结果如下:

十六进制 二进制 十进制 unsigned char变量a 6 0000 0110 6

unsigned char变量b A 0000 1010 10

unsigned char变量c E 0000 1110 14

unsigned char变量d 2C 0010 1100 44

多说一句,在程序里,可以用十六进制,也可以用十进制,比如:

d=0x2C与d=44的含义是一样的。十六进制的0x2C和十进制的44最终都会被C51编译器翻译成二进制00101100。

下节预告:十进制与十六进制。

第十五节:十进制与十六进制。

十六进制是二进制的缩写形式,而C语言程序里只用了十进制和十六进制这两种书写格式。它们各有什么应用特点?十六进制方便人理解机器,通常应用在配置寄存器,底层通讯驱动,底层IO口驱动,以及数据的移位,转换和合并等场合。而十进制则方便人理解值的大小,在应用层经常用。总之,进制只是数据的表现形式而已。

十进制与十六进制如何相互转换?其实很多教科书上有介绍它们之间如何通过手工计

算进行转换的方法。但是实际应用中,我从来没有用过这种手工计算方法,我用的方法是最简单直接的,就是借助电脑自带的计算器进行转换即可。现在把这种方法介绍给大家。 第一步:点击电脑左下角“开始”菜单,在下拉菜单中把鼠标移动到“所有程序”,在下拉菜单中把鼠标移动到“附件”,在下拉菜单中点击“计算器”,此时会弹出“计算器” 的窗口。

第二步:点击计算器窗口上面的“查看”菜单,在下拉菜单中点击“科学型”,此时“计算器” 的窗口会变长。按键上方出现“十六进制”,“十进制”,“八进制”,“二进制”等单选项。

第三步:在按键上方“十六进制”,“十进制”,“八进制”,“二进制”等单选项中,单击所要切换到的进制,然后按数字按键输入数据。输完数据后,再单击切换到所要转换的进制中,即可完成各进制的数据切换。注意,在切换到“十六进制”的时候,在右边“四字”,“双字”,“单字”,“字节”中选中“四字”。

第四步:把十进制转换到十六进制的方法如下:单击切换到“十进制”,然后按数字按键输入数据。输完数据后,再单击切换到“十六进制”,即可完成进制的转换。比如输入十进制的“230”,切换到十六进制就变成了“E6”。

第五步:把十六进制转换到十进制的方法如下:单击切换到“十六进制”,然后按数字按键输入数据。输完数据后,再单击切换到“十进制”,即可完成进制的转换。比如输入十六进制的“AC”,切换到十进制就变成了“172”。

现在我们编写一个程序来观察十进制和十六进制的关系,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。

unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。

a=230; //把十进制的230赋值给变量a,在朱兆祺51学习板上观察一下它的十六进制是不是E6。

b=0xAC; //把十六进制的AC赋值给变量b,在朱兆祺51学习板上观察一下它的十进制是不是172。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察十进制和十六进制?S1和S5按键是切换窗口按键。按住S9按键不松手,就可以观察当前窗口数据的十六进制格式了。松开S9按键就是当前窗口的十进制数据格式。而坚鸿51学习板右上角的16个LED灯就代表了当前窗口的二进制,亮的代表1,灭的代表0。

上坚鸿51学习板观察程序执行的结果如下:

十六进制 十进制

unsigned char变量a E6 230

unsigned char变量b AC 172

下节预告:判断语句“if”和等于关系符“==”。

第十六节:加法运算的5种常用格式。

根据上一节的预告,本来这节应该讲判断语句的,但是考虑到后续章节的连贯性,决定先讲运算语法。

在讲运算语法之前,先讲一个我在前面忘了讲的知识点,那就是注释语句。何谓注释语句?在我前面一些章节的main函数中,经观察,发现一个规律,凡是中文解说的文字,要么前面有符号”//”,要么就是被包含在“/*”和”*/”之间。符号“//”和“/* */”都是注释语句。注释语句是用来添加文字备忘,方便程序员阅读记忆的。在注释语句里的文字是不会被编译器翻译成机器码的,也就是说即使注释里面的文字再多,也不会增加单片机的程序容量,它是被编译器过滤忽略的,仅仅方便程序员做备注文字而已。

符号“//”和“/* */”都是注释语句,但应用方面有点小差异。符号“//”是用来注释一行文字。而“/* */”往往是用来注释一段文字,当然“/* */”也可以注释一行文字。但是符号“//”仅仅能注释一行文字,却不能注释一段文字。

讲完注释语句,继续回到本节正题。单片机本身具备了简单的加减乘除运算能力,我们只需要通过C语言调用相关的运算语法,即可指示单片机按我们的要求进行简单的运算。至于内部具体的运算细节我们可以不管,除非是涉及到大数据的运算才需要我们额外编写算法。请先看以下的加法语法格式:

“保存变量”=“加数1”+“加数2”+...+“加数N”;

含义是:右边的“加数”与“加数”相加,并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。而右边的“加数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,何谓变量和常量?变量就是可以在程序中被更改的,是分配的一个RAM空间。而常量往往就是数字,或者是被分配在ROM空间的一个具体数值。下面根据右边“被加数”与“加数”的不同组合,列出了加法运算的5种常用格式。

第1种:“加数1”是常量,“加数2”是常量。比如:

unsigned char a;

a=3+15;

数字“3”和“15”都是常量。执行上述语句后,保存变量a变成了18。

第2种:“加数1”是变量,“加数2”是常量。比如:

unsigned char b;

unsigned char x=10;

b=x+15;

x是变量,“15”是常量。由于原来x变量里面的数值是10,执行上述语句后,保存变量b变成了25。而变量x则保持不变,x还是10。

第3种:“加数1”是变量,“加数2”是变量。比如:

unsigned char c;

unsigned char x=10;

unsigned char y=6;

c=x+y;

x是变量,y也是变量。由于原来x变量里面的数值是10,y变量里面的数值是6,执行上述语句后,保存变量c变成了16。而变量x和y则保持不变,x还是10,y还是6。

第4种:“加数1”是保存变量本身,“加数2”是常量。比如:

unsigned char d=2;

d=d+18;

d=d+7;

d是保存变量,“18”是常量。这类语句有一个特点,具备了自加功能,可以更改自己本身自己的数值。比如原来保存变量d的数值是2,执行“d=d+18;”语句后,d变成了20,接着再执行完“d=d+7;”语句后,d最后变成了27。

第5种:“加数1”是保存变量本身,“加数2”是变量。比如:

unsigned char e=2;

unsigned char x=10;

unsigned char y=6;

e=e+x;

e=e+y;

e是保存变量,x与y都是变量。这类语句有一个特点,具备了自加功能,可以更改自己本身自己的数值。比如原来保存变量e的数值是2,执行“e=e+x;”语句后,e变成了12,接着再执行完“e=e+y;”语句后,e最后变成了18。

现在编写一个程序来练习上述5种格式的加法语句,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。 unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。 unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。

unsigned char d=2; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为2.

unsigned char e=2; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为2.

unsigned char x=10; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为10.

unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.

//第1种:“加数1”是常量,“加数2”是常量。

a=3+15;

//第2种:“加数1”是变量,“加数2”是常量。

b=x+15;

//第3种:“加数1”是变量,“加数2”是变量。

c=x+y;

//第4种:“加数1”是保存变量本身,“加数2”是常量。

d=d+18;

d=d+7;

//第5种:“加数1”是保存变量本身,“加数2”是变量。

e=e+x;

e=e+y;

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

GuiWdData4=e; //把变量e这个数值放到窗口变量4里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/

while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察a,b,c,d,e这5个变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。上坚鸿51学习板观察程序执行的结果如下: 变量a为18。

变量b为25。

变量c为16。

变量d为27。

变量e为18。

下节预告:加法的连写和自加运算的简写。

第十七节:连加以及自加运算的简写。

上一节我列举的加法例子中,右边的加数个数都是两个。实际上,C语言规则没有限制加数的个数,它的通用格式如下:

“保存变量”=“加数1”+“加数2”+...+“加数N”;

当右边的加数个数超过两个的时候,这种情况就是我所说的“连加”,每个加数的属性没有限定,可以是常量,也可以是变量。比如:

a=1+69+102; //加数全部是常量。

b=q+x+y+k+r; //加数全部是变量。

c=3+x+y+5+k; //加数有的是常量,有的是变量。

连加的运行顺序是,赋值符号“=”右边的加数挨个相加,把每一次的运算结果放在一个临时的隐蔽变量里,这个隐蔽的变量我们看不到,是单片机系统内部参与运算时的专用寄存器,等右边所有的加数连加的计算结果出来后,再把这个隐蔽变量所保存的计算结果赋值给左边的“保存变量”。

讲完了连加的格式,接着讲自加的简写。何谓自加?当右边的加数只要其中有一个是“保存变量”本身时,这种情况就是我所说的“自加”。比如:

“保存变量”=“保存变量”+“加数1”;

“保存变量”=“保存变量”+“加数1”+“加数2”+...+“加数N”;

当这类自加计算式中,右边的加数有且仅有一个是“保存变量”本身时,那么上述自加计算式可以简写成如下格式:

“保存变量”+=“加数1”;

“保存变量”+=“加数1”+“加数2”+...+“加数N”;

这种格式就是我所说的自加简写。现在举几个例子如下:

d+=6; //相当于d=d+6;

e+=x; //相当于e=e+x;

f+=18+y+k; //相当于f=f+18+y+k;

这些例子都是很常规的自加简写,再跟大家讲一种很常用的特殊简写。当右边只有2个加数,当一个加数是“保存变量”,另一个是常数1时,格式如下:

“保存变量”=“保存变量”+1;

这时候,可以把上述格式简写成如下两种格式:

“保存变量”++;

++“保存变量”;

这两种格式也是俗称的“自加1”操作。比如:

g++; //相当于g=g+1或者g+=1;

++h; //相当于h=h+1或者h+=1;

也就是说自加1符号“++”可以在变量的左边,也可以在变量的右边,它们在这里本质是一样的,没有差别。当然,如果是在循环条件语句中,这时自加1符号“++”在左边还是在右边是有一点点微弱的差别,这方面的内容以后再讲。

现在编写一个程序来练习刚才讲到的内容,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。 unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。 unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。

unsigned char d=5; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char e=5; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char f=5; //定义一个变量f,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char g=5; //定义一个变量g,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char h=5; //定义一个变量h,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char q=1; //定义一个变量q,并且分配了1个字节的RAM空间。初始化默认为1.

unsigned char x=3; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为3.

unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.

unsigned char k=2; //定义一个变量k,并且分配了1个字节的RAM空间。初始化默

认为2.

unsigned char r=8; //定义一个变量r,并且分配了1个字节的RAM空间。初始化默认为8.

//第1个知识点:连加。

a=1+69+102; //加数全部是常量。a的结果为:172。

b=q+x+y+k+r; //加数全部是变量。b的结果为:20。

c=3+x+y+5+k; //加数有的是常量,有的是变量。c的结果为:19。

//第2个知识点:自加的常规格式。

d+=6; //相当于d=d+6; d的结果为:11。

e+=x; //相当于e=e+x; e的结果为:8。

f+=18+y+k; //相当于f=f+18+y+k; f的结果为:31。

//第3个知识点:自加的特殊格式。

g++; //相当于g=g+1或者g+=1; g的结果为:6。

++h; //相当于h=h+1或者h+=1; h的结果为:6。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

GuiWdData4=e; //把变量e这个数值放到窗口变量4里面显示

GuiWdData5=f; //把变量f这个数值放到窗口变量5里面显示

GuiWdData6=g; //把变量g这个数值放到窗口变量6里面显示

GuiWdData7=h; //把变量h这个数值放到窗口变量7里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察a,b,c,d,e,f,g,h这8个变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。上坚鸿51学习板观察程序执行的结果如下:

变量a为172。

变量b为20。

变量c为19。

变量d为11。

变量e为8。

变量f为31。

变量g为6。

变量h为6。

下节预告:加法的溢出和优先级

第十八节:加法运算的溢出。

我前面介绍的三种数据类型unsigned char ,unsigned int ,unsigned long,都是有最大范围限制的,它们最大范围分别是255,65535,4294967295,如果加法运算的结果超过了参与运算的变量本身,会出现什么结果,有什么规律,这就是本节要讲解的溢出问题。

(1)何谓溢出?比如以下例子:

unsigned char a;

a=0x8536;

分析:

因为a是unsigned char变量,位数是8位,也就是1个字节,而0x8536是16位,2个字节,这种情况下,把0x8536赋值给单字节变量a,变量a只能接收到最低位的一个字节0x36,而高位字节的0x85就被丢失了,这个就是本节所说的“溢出”了。

(2)再看一个例子如下:

unsigned char b=0xff;

b=b+1;

分析:

b默认值是0xff,再加1后,变成了0x0100保存在一个隐藏的中间变量,然后再把这个中间变量赋值给单字节变量b,b只能接收到低位字节0x00,所以运算后b的数值由于溢出变成了0x00。

(3)再看一个例子如下:

unsigned char c=0xff;

c=c+2;

分析:

c默认值是0xff,再加2后,变成了0x0101保存在一个隐藏中间变量,然后再把这个中间变量赋值给单字节变量c,c只能接收到低位字节0x01,所以运算后c的数值由于溢出变成了0x01。

(4)再看一个例子如下:

Unsigned int d=0xfffe;

d=d+5;

分析:

d默认值是0xfffe,再加5后,变成了0x10003保存在一个隐藏中间变量,由于这个隐藏的中间变量是unsigned int类型,只能保存2个字节的数据,所以在中间变量这个环节就溢出了,实际上隐藏的中间变量只保存了0x0003,然后再把这个中间变量赋值给双字节变量d,d理所当然也是0x0003。

(5)再看一个例子如下:

unsigned long e=0xfffffffe;

e=e+5;

分析:

e默认值是0xfffffffe,再加5后,变成了0x100000003保存在一个隐藏中间变量,由于这个隐藏的中间变量是unsigned long类型,只能保存4个字节的数据,所以在中间变量这个环节就溢出了,实际上隐藏的中间变量只保存了0x00000003,然后再把这个中间变量赋值给4字节变量e,e理所当然也是0x00000003。

现在编写一个程序来练习上述前面4个例子,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a;

unsigned char b=0xff;

unsigned char c=0xff;

unsigned int d=0xfffe;

a=0x8536;

b=b+1;

c=c+2;

d=d+5;

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察a,b,c,d这4个变量的十六进制?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量,只要按住S9按键不放,此时显示的就是该变量的十六进制。上坚鸿51学习板观察程序执行的结果如下:

变量a为0x36。

变量b为0x00。

变量c为0x01。

变量d为0x0003。

这一节提到了一个“隐藏中间变量”的概念,这个神秘的“隐藏中间变量”到底是unsigned int类型还是unsigned long类型?有什么规律?如果运算中存在多种不同变量类型该怎么办,实际应用中有解决的办法吗?预知详情,请看一节内容。

下节预告:加法运算中,神秘中间变量的类型以及解决“掺杂多种变量类型”的办法。

第十九节:加法运算中,神秘中间变量的类型以及解决“掺杂多种变量类型”的办法。 在开始本节内容之前,先告诉大家前面第十一节内容有一处笔误,unsigned long的数据长度应该是4个字节,而不是3个字节。

上一节提到了一个“隐藏中间变量”的概念,两个加数相加,其结果先保存在一个“隐藏中间变量”里,然后再把这个“隐藏中间变量”赋值给左边的“保存变量”。这里的“隐藏中间变量”到底是unsigned int类型还是unsigned long类型?为了研究它的规律,在keil自带的C51编译环境下,我专门编写了好几个测试程序来观察实际运行的结果。 “保存变量”=“加数1”+“加数2”;

我测试的程序如下:

(1)“保存变量”为 unsigned int类型,“加数1”为unsigned char类型,“加数2”为unsigned char 类型。

unsigned int a;

unsigned char x=0x12;

unsigned char y=0xfe;

a=x+y;

复制代码

运行结果:a等于0x0110。

分析过程:两个char类型的数相加,当运算结果大于char本身时,并没有发生溢出现象,

int型的“保存变量”a最终得到了完整的结果。

初步结论:这种情况,“隐藏中间变量”应该为unsigned int 类型。

(2)“保存变量”为 unsigned long类型,“加数1”为unsigned int类型,“加数2”为unsigned char 类型。

unsigned long a;

unsigned int x=0xfffe;

unsigned char y=0x12;

a=x+y;

复制代码

运行结果:a等于十六进制的0x0010。

分析过程:一个int类型的数与一个char类型的数相加,当运算结果大于其中最大加数int类型本身时,本来以为运算结果应该是long类型的0x00010010,结果是int类型的0x0010,发生了溢出现象。

初步结论:这种情况,“隐藏中间变量”应该为unsigned int 类型。

(3)“保存变量”为 unsigned long类型,“加数1”与“加数2”都为常量。 unsigned long a;

a=50000+50000;

复制代码

运行结果:a等于100000。

分析过程:int的最大数据范围是65535,而两个常量相加,其结果超过了65535还能完整保存下来。

初步结论:这种情况,“隐藏中间变量”等于左边的“保存变量”类型。

(4)“保存变量”为 unsigned long类型,“加数1”为unsigned int类型,“加数2”为常量。

unsigned long a;

unsigned long b;

unsigned int x=50000;

a=x+30000;

b=x+50000;

复制代码

运行结果:a等于14464,b等于100000。

分析过程:本来以为a应该等于80000的,结果是14464发生了溢出。而b是100000没有发生溢出。

初步结论:这是一种很怪异的现象,为什么同样的类型,因为常量的不同,一个发生了溢出,另外一个没有发生溢出?这时的“隐藏中间变量”到底是int类型还是long类型我无法下结论。

经过上述简单的测试,我发现规律是模糊的,模糊的规律就不能成为规律。如果真要按这种思路研究下去,那真是没完没了,因为还有很多情况要研究,当超过3个以上加数相加,同时存在long,int,char,常量这4种类型时又是什么规律?在不同的C编译器里又会是什么现象?即使把所有情况的规律摸清楚了又能怎么样,因为那么繁杂很容易忘记导致出错。有什么解决的办法吗?现在跟大家分享一种很简单的解决办法。

当遇到有争议的问题时,还有一种解决思路是:与其参与争议越陷越深,还不如想办法及时抽身绕开争议。在上述运算中,只要经过简单的变换,让它们遵循“所有参与运算的变量,左边的变量类型必须跟右边的保存变量类型一致”这个原则,那么就不会存在这些争议了。

(5)比如上述第(4)个例子,其转换方法如下:

unsigned long a;

unsigned long b;

unsigned int x=50000;

Unsigned long t; //多增加一个long类型的变量,用来变换类型

t=0; //把变量的高位和低位全部清零。

t=x; //把x的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。 a=t+30000;

b=t+50000;

复制代码

运行结果:a等于80000,b等于100000。都没有发生溢出。

(6)比如上述第(2)个例子,其转换方法如下:

unsigned long a;

unsigned int x=0xfffe;

unsigned char y=0x12;

unsigned long t; //多增加一个long类型的变量,用来变换类型。

unsigned long r; //多增加一个long类型的变量,用来变换类型。

t=0;//把变量的高位和低位全部清零。

t=x; //把x的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。 r=0; //把变量的高位和低位全部清零。

r=y //把y的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。 a=t+r;

复制代码

运行结果:a等于十六进制的0x00010010,没有发生溢出现象。

下节预告:减法运算的常见格式。

第二十节:减法运算的5种常见格式。

请先看以下的减法语法格式:

“保存变量”=“减数1”-“减数2”-...-“减数N”;

含义是:右边的“减数”与“减数”相减,并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。右边的“减数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,何谓变量和常量?变量是可以在程序中被更改的,是被分配的一个RAM空间。常量往往是数字,或者是被分配在ROM空间的一个具体数值。下面根据右边“被减数”与“减数”的不同组合,列出了减法运算的5种常见格式。

第1种:“减数1”是常量,“减数2”是常量。比如:

unsigned char a;

a=15-3;

数字“15”和“3”都是常量。执行上述语句后,保存变量a变成了12。

第2种:“减数1”是变量,“减数2”是常量。比如:

unsigned char b;

unsigned char x=15;

b=x-10;

x是变量,“10”是常量。由于原来x变量里面的数值是15,执行上述语句后,保存变量b变成了5。而变量x则保持不变,x还是15。

第3种:“减数1”是变量,“减数2”是变量。比如:

unsigned char c;

unsigned char x=15;

unsigned char y=6;

c=x-y;

x是变量,y也是变量。由于原来x变量里面的数值是15,y变量里面的数值是6,执行上述语句后,保存变量c变成了9。而变量x和y则保持不变,x还是15,y还是6。 第4种:“减数1”是保存变量本身,“减数2”是常量。比如:

unsigned char d=18;

d=d-2;

d=d-7;

d是保存变量,“2”和“7”都是常量。这类语句有一个特点,具备了自减功能,可以更改自己本身自己的数值。比如原来保存变量d的数值是18,执行“d=d-2;”语句后,d变成了16,接着再执行完“d=d-7;”语句后,d最后变成了9。

第5种:“减数1”是保存变量本身,“减数2”是变量。比如:

unsigned char e=28;

unsigned char x=15;

unsigned char y=6;

e=e-x;

e=e-y;

e是保存变量,x与y都是变量。这类语句有一个特点,具备了自减功能,可以更改自己本身自己的数值。比如原来保存变量e的数值是28,执行“e=e-x;”语句后,e变成了13,接着再执行完“e=e-y;”语句后,e最后变成了7。

现在编写一个程序来练习上述5种格式的减法语句,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。 unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。 unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。

unsigned char d=18; //定义一个变量d,并且分配了1个字节的RAM空间。初始化

默认为18.

unsigned char e=28; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为28.

unsigned char x=15; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为15.

unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.

//第1种:“减数1”是常量,“减数2”是常量。

a=15-3;

//第2种:“减数1”是变量,“减数2”是常量。

b=x-10;

//第3种:“减数1”是变量,“减数2”是变量。

c=x-y;

//第4种:“减数1”是保存变量本身,“减数2”是常量。

d=d-2;

d=d-7;

//第5种:“减数1”是保存变量本身,“减数2”是变量。

e=e-x;

e=e-y;

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

GuiWdData4=e; //把变量e这个数值放到窗口变量4里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

如何在坚鸿51学习板上观察a,b,c,d,e这5个变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。上坚鸿51学习板观察程序执行的结果如下: 变量a为12。

变量b为5。

变量c为9。

变量d为9。

变量e为7。

下节预告:减法的连写和自减运算的简写。

第二十一节:减法的连写和自减运算的简写。

连减。上一节我列举的减法例子中,右边的减数只有一个。实际上,C语言规则没有限制减数的个数,它的通用格式如下:

“保存变量”=“被减数”-“减数1”-“减数2”-...-“减数N”;

被减数与减数的属性。当右边的减数个数总共超过1个的时候,就是我所说的“连减”。被减数和减数的属性没有限定,可以是常量,也可以是变量。比如:

a=68-3-15; //被减数和减数全部是常量。

b=q-x-y-k; //被减数和减数全部是变量。

c=63-x-5-k; //被减数和减数,有的是常量,有的是变量。

连减的运行顺序。赋值符号“=”右边的被减数挨个与减数相减,每一次的运算结果都放在一个临时的隐蔽变量里,这个隐蔽的变量我们看不到,是单片机系统内部参与运算时的专用寄存器,当与所有减数相减的计算结果出来后,再把隐蔽变量所保存的计算结果赋值给左边的“保存变量”。

自减。当被减数是“保存变量”本身时,这种情况就是我所说的“自减”。比如: “保存变量”=“保存变量”-“减数1”;

“保存变量”=“保存变量”-“减数1”-“减数2”-...-“减数N”;

自减的简写。当被减数是“保存变量”本身,并且只有一个减数时,那么上述自减计算式可以简写成如下格式:

“保存变量”-=“减数1”;

“保存变量”-=“减数1”-“减数2”-...-“减数N”;

这种格式就是我所说的自减简写。现在举几个例子如下:

d-=6; //相当于d=d-6;

e-=x; //相当于e=e-x;

f-=18-y-k; //相当于f=f-(18-y-k);

自减的特殊简写。在自减运算中,只有一个减数,并且这个减数是常数1时,格式如下:

“保存变量”=“保存变量”-1;

这时候,可以把上述格式简写成如下两种格式:

“保存变量”--;

--“保存变量”;

这两种格式也是俗称的“自减1”操作。比如:

g--; //相当于g=g-1或者g-=1;

--h; //相当于h=h-1或者h-=1;

自减1符号“--”可以在变量的左边,也可以在变量的右边,它们在这里本质是一样的,没有差别。当然,如果是在循环条件语句中,这时自减1符号“--”在左边还是在右边是有一点点微弱的差别,这方面的内容以后再讲。

上机练习。现在编写一个程序来练习刚才讲到的内容,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

void main() //主程序

{

/*---C语言学习区域的开始---------------------------------------------------------------------------*/

unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。 unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。 unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。

unsigned char d=65; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为65.

unsigned char e=38; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为38.

unsigned char f=29; //定义一个变量f,并且分配了1个字节的RAM空间。初始化默认为29.

unsigned char g=5; //定义一个变量g,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char h=5; //定义一个变量h,并且分配了1个字节的RAM空间。初始化默认为5.

unsigned char q=50; //定义一个变量q,并且分配了1个字节的RAM空间。初始化默认为50.

unsigned char x=3; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为3.

unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.

unsigned char k=2; //定义一个变量k,并且分配了1个字节的RAM空间。初始化默认为2.

//第1个知识点:连减。

a=68-3-15;//被减数和减数全部是常量。a的结果为:50。

b=q-x-y-k;//被减数和减数全部是变量。b的结果为:39。

c=63-x-5-k;//被减数和减数,有的是常量,有的是变量。c的结果为:53。

//第2个知识点:自减的常规格式。

d-=6;//相当于d=d-6; d的结果为:59。

e-=x;//相当于e=e-x; e的结果为:35。

f-=18-y-k;//相当于f=f-(18-y-k); f的结果为:19。

//第3个知识点:自减的特殊格式。

g--;//相当于g=g-1或者g-=1; g的结果为:4。

--h;//相当于h=h-1或者h-=1; d的结果为:4。

GuiWdData0=a; //把变量a这个数值放到窗口变量0里面显示

GuiWdData1=b; //把变量b这个数值放到窗口变量1里面显示

GuiWdData2=c; //把变量c这个数值放到窗口变量2里面显示

GuiWdData3=d; //把变量d这个数值放到窗口变量3里面显示

GuiWdData4=e; //把变量e这个数值放到窗口变量4里面显示

GuiWdData5=f; //把变量f这个数值放到窗口变量5里面显示

GuiWdData6=g; //把变量g这个数值放到窗口变量6里面显示

GuiWdData7=h; //把变量h这个数值放到窗口变量7里面显示

/*---C语言学习区域的结束---------------------------------------------------------------------------*/ while(1)

{

initial();

key_service();

display_service();

}

}

复制代码

查看运算结果的方法。如何在坚鸿51学习板上观察a,b,c,d,e,f,g,h这8个变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。上坚鸿51学习板观察程序执行的结果如下:

变量a为50。

变量b为39。

变量c为53。

变量d为59。

变量e为35。

变量f为19。

变量g为4。

变量h为4。

下节预告:减法运算的溢出。

相关推荐