单片机C语言知识点全攻略(三)

控制/MCU

1877人已加入

描述

单片机
 
       电子发烧友网讯:继《单片机学习知识点全攻略》得到广大读者好评,根据有网友提出美中不足的是所用单片机编程语言为汇编,基于此,电子发烧友网再接再厉再次为读者诚挚奉上非常详尽的《单片机C语言知识点全攻略》系列单片机C语言学习教程,本教程共分为四部分,本文为第三部分,主要知识点如下所示。

参阅相关系列文章,

单片机C语言知识点全攻略(一)
单片机C语言知识点全攻略(二)

第三部分知识点:

  第十课 C51表达式语句及仿真器

  第十一课 C51复合语句和条件语句

  第十二课 C51开关分支语句

  第十三课 C51循环语句

  第十四课 C51函数


  第十课、C51表达式语句及仿真器

  前面学习了大部分的基本语法,以下所要学习的各种基本语句的语法能说是组成程序的灵魂。在前面的课程中的例子里,也简单理解过一些语句的使用方法,能看出C语言是一种结构化的程序设计语言。C 语言供给了相当丰富的程序控制语句。学习掌握这些语句的使用方法也是单片机C语言学习中的重点。

  表达式语句是最基本的一种语句。不一样的程序设计语言都会有不一样的表达式语句,如VB就是在表达式后面加入回车就构成了VB 的表达式语句,而在51单片机的C语言中则是加入分号“;”构成表达式语句。举例如下:

  b = b * 10; Count++;

  X = A;Y = B;

  Page = (a+b)/a-1;

  以上的都是合法的表达式语句。在我收到的一些网友的 Email 中,发现很多开始学习的朋友一般在编写调试程序时忽略了分号“;”,造成程序不能被正常的编译。我本人的经验是在遇 到编译错误时先语法是否有误,这在开始学习时一般会因在程序中加入了全角符号、运算符打错 漏掉或没有在后面加“;”。

  在 C 语言中有一个特殊的表达式语句,称为空语句,它仅仅是由一个分号“;”组成。 有时候为了使语法正确,那么就要求有一个语句,但这个语句又没有实际的运行效果那么这 时就要有一个空语句。说起来就像大家在晚自修的时候用书包占位一样,呵呵。

  空语句通常用会以下两种使用方法。

  (1)while,for 构成的循环语句后面加一个分号,形成一个不执行其它操作的空循环体。 我会会常常用它来写等待事件发生的程序。大家要注意的是“;”号作为空语句使用时,要 与语句中有效组成部分的分号相区别,如 for (;a《50000;a++);第一个分号也应该算是 空语句,它会使 a 赋值为 0(但要注意的是如程序前有 a 值,则 a 的初值为 a 的当前值),最后一个分号则使整个语句行成一个空循环。若此时 a=0,那么 for (;a《50000;a++);就相当

  于 for (a=0;a《50000;a++);我本人习惯是写后面的写法,这样能使人更不难读明白。 (2)在程序中为有关语句供给标号,标记程序执行的位置,使相关语句能跳转到要执行

  的位置。这会用在 goto 语句中。

  下面的示例程序是简单说明 while 空语句的使用方法。硬件的功能很简单,就是在 P3.7 上 接一个开关,当开关按下时 P1 上的灯会全亮起来。当然实际应用中按钮的功能实现并没有 这么的简单,一般还要进行防抖动处理等。

  先在我们的实验板上加一个按钮。电路图如图 10-1。

  单片机

  程序如下:

  #include 《AT89x51.h》

  void main(void)

  {

  图 10-1 加了按钮的实验电路图

  unsigned int a;

  do

  {

  P1 = 0xFF; //关闭 P1 上的 LED

  while(P3_7); //空语句,等待 P3_7 按下为低电平,低电平时执行下面的语句 P1 = 0; //点亮 LED

  for(;a《60000;a++); //这也是空语句的使用方法,注意 a 的初值为当前值

  } //这样第一次按下时会有一延时点亮一段时间,以后按多久就亮多久

  while(1); //点亮一段时间后关闭再次判断 P3_7,如此循环

  }

  上面的实验电路已加入了 RS232 串行口电路,只要稍微改变一下,就能变为具有仿真功能的 实验电路。这个改变的关键就是把芯片改用 SST89C58,并在芯片中烧入仿真监控程序。 SST89C58 同样也是一种 51 架构的单片机,它具有 24K+8K 的两个程序存储区,能选择其 一做为程序的启动区。只要把一个叫 SOFTICE.HEX 的监控程序用支持 SST89C58 的编程器烧 录到芯片中(使用编程器或用 CA 版的 SST89C58 烧录 SOFTICE 的具体方法和文件能参考 ),就 能把上 面 的电路升级为

  MON51 仿真实验器。那么怎么用它和 KEIL 实现联机仿真呢?

  单片机

  图 10-2 项目设置菜单

  单片机

  图 10-3 项目设置

       首先要在你要仿真的程序项目设置仿真器所使用的驱动,在 Debug 页中选择对应本仿真器的 KeilMon51 驱 动,如图 10 中 1 所示。图 10-3 的 3 是选择在仿真时能使用的工具窗口,如内存显示,断点等等。按 2 进 行图 10-4 中的仿真器设置。设置好串行口号,波特率,晶体震荡器为 11.0592M 时选 38400。Cache Options 为仿真 缓选取后会加快仿真的运行的速度。设好后编译运行程序就能连接仿真器了,连接成功会出现如图 10-

  5 的画面。如连接不成功就出现图 10-6 的图,这个时候能先复位电路再按“Try Again”,还不成功连接的话则 应检查软件设置和硬件电路。图 10-5 中 1 是指示仿真器的固件版本为 F-MON51V3.4 版。点击 3 中小红 点位置时为设置和取消断点,点击 2 则运行到下一个断点。图 10-7 则是变量和存储器的查看。仿真器在

  软件大概的使用方法和软件仿真相差不多。

  

  图 10-4 仿真器设置

  

  图 10-5 仿真器连接成功

  单片机

  图 10-6 连接不成功提示

  单片机

  图 10-7 变量及内存查看


  第十一课 C51复合语句和条件语句

  曾经在BBS上有朋友问过我{}是什么意思?什么作用?在 C 中是有不少的括号,如{},[],()等,确实会让一些初入门的朋友不解。在 VB 等一些语言中同一个()号会有不一样的 作用,它能用于组合若干条语句形成功能块,能用做数组的下标等,而在 C 中括号的分 工较为明显,{}号是用于将若干条语句组合在一起形成一种功能块,这种由若干条语句组合 而成的语句就叫复合语句。复合语句之间用{}分隔,而它内部的各条语句还是需要以分号“;” 结束。复合语句是允许嵌套的,也是就是在{}中的{}也是复合语句。复合语句在程序运行时,{}中的各行单语句是依次顺序执行的。单片机C语言中能将复合语句视为一条单语句,也就是说 在语法上等同于一条单语句。对于一个函数而言,函数体就是一个复合语句,也许大家会因 此知道复合语句中不单能用可执行语句组成,还能用变量定义语句组成。要注意的是在 复合语句中所定义的变量,称为局部变量,所谓局部变量就是指它的有效范围只在复合语句 中,而函数也算是复合语句,所以函数内定义的变量有效范围也只在函数内部。下面用一段简单的例子简单说明复合语句和局部变量的使用。

  #include 《at89x51.h》

  #include 《stdio.h》

  void main(void)

  {

  unsigned int a,b,c,d; //这个定义会在整个 main 函数中?

  SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

  TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

  TI = 1;

  TR1 = 1; //启动定时器

  a = 5; b = 6; c = 7;

  d = 8; //这会在整个函数有效

  printf(“0: %d,%d,%d,%d ”,a,b,c,d);

  { //复合语句 1

  unsigned int a,e; //只在复合语句 1 中有效

  a = 10,e = 100;

  printf(“1: %d,%d,%d,%d,%d ”,a,b,c,d,e);

  { //复合语句 2

  unsigned int b,f; //只在复合语句 2 中有效

  b = 11,f = 200;

  printf(“2: %d,%d,%d,%d,%d,%d ”,a,b,c,d,e,f);

  }//复合语句 2 结束

  printf(“1: %d,%d,%d,%d,%d ”,a,b,c,d,e);

  }//复合语句 1 结束

  printf(“0: %d,%d,%d,%d ”,a,b,c,d);

  while(1);

  }

  运行结果:

  0:5,6,7,8

  1: 10,6,7,8,100

  2: 10,11,7,8,100,200

  1: 10,6,7,8,100

  0:5,6,7,8 结合以上的说明想想为何结果会是这样。

  读完前面的文章大家都会大概对条件语句这个概念有所认识吧?是的,就如学习语文中 的条件语句一样,C 语言也一样是“如果 XX 就 XX”或是“如果 XX 就 XX 不然 XX”。也就是 当条件符合时就执行语句。条件语句又被称为分支语句,也有人会称为判断语句,其关键字 是由 if 构成,这大众多的高级语言中都是基本相同的。C 语言供给了 3 种形式的条件语句:

  1: if (条件表达式) 语句 当条件表达式的结果为真时,就执行语句,不然就跳过。 如 if (a==b) a++; 当 a 等于 b 时,a 就加 1

  2: if (条件表达式) 语句 1

  else 语句 2

  当条件表达式成立时,就执行语句 1,不然就执行语句 2 如 if (a==b)

  a++;

  else

  a--;

  当 a 等于 b 时,a 加 1,不然 a-1。

  3:if (条件表达式 1) 语句 1

  else if (条件表达式 2) 语句 2

  else if (条件表达式 3) 语句 3

  else if (条件表达式 m) 语句 n else 语句 m

  这是由 if else 语句组成的嵌套,用来实现多方向条件分支,使用应注意 if 和 else 的配对使用,要是少了一个就会语法出错,记住 else 总是与最临近的 if 相配对。一般条件 语句只会用作单一条件或少数量的分支,如果多数量的分支时则更多的会用到下一篇中的开 关语句。如果使用条件语句来编写超过 3 个以上的分支程序的话,会使程序变得不是那么清晰易读。


  第十二课 C51开关分支语句

  学习了条件语句,用多个条件语句能实现多方向条件分支,但是能发现使用过多的 条件语句实现多方向分支会使条件语句嵌套过多,程序冗长,这样读起来也很不好读。这个时候 使用开关语句同样能达到处理多分支选择的目的,又能使程序结构清晰。它的语法为下:

  switch (表达式)

  {

  case 常量表达式 1: 语句 1; break; case 常量表达式 2: 语句 2; break; case 常量表达式 3: 语句 3; break; case 常量表达式 n: 语句 n; break; default: 语句

  }

  运行中 switch 后面的表达式的值将会做为条件,与 case 后面的各个常量表达式的值相 对比,如果相等时则执行 case 后面的语句,再执行 break(间断语句)语句,跳出 switch 语句。如果 case 后没有和条件相等的值时就执行 default 后的语句。当要求没有符合的条 件时不做任何处理,则能不写 default 语句。

  在上面的章节中我们一直在用 printf 这个标准的 C 输出函数做字符的输出,使用它当 然会很方便,但它的功能强大,所占用的存储空间自然也很大,要 1K 左右字节空间,如果 再加上 scanf 输入函数就要达到 2K 左右的字节,这样的话如果要求用 2K 存储空间的芯片时 就无法再使用这两个函数,例如 AT89C2051。在这些小项目中,通常我们只是要求简单的字 符输入输出,这里以笔者发表在本人网站的一个简单的串行口应用实例为例,一来学习使用开 关语句的使用,二来简单了解 51 芯片串行口基本编程。这个实例是用 PC 串行口通过上位机程序 与由 AT89c51 组成的下位机相通信,实现用 PC 软件控制 AT89c51 芯片的 IO 口,这样也就可 以再通过相关电路实现对设备的控制。为了方便实验,在此所使用的硬件还是用回以上课程 中做好的硬件,以串行口和 PC 连接,用 LED 查看实验的结果。原代码请到在笔者的网站 下载,上面有 单片机c语言 下位机源码、PC 上位机源码、电路图等资料。

  代码中有多处使用开关语句的,使用它对不一样的条件做不一样的处理,如在 CSToOut 函数 中根据 CN[1]来选择输出到那个 IO 口,CN[1]=0 则把 CN[2]的值送到 P0,CN[1]=1 则送到 P1, 这样的写法比起用 if (CN[1]==0)这样的判断语句来的清晰明了。当然它们的效果没有太大 的差别(在不考虑编译后的代码执行效率的情况下)。

  在这段代码主要的作用就是通过串行口和上位机软件进行通信,跟据上位机的命令字串, 对指定的 IO 端口进行读写。InitCom 函数,原型为 void InitCom(unsigned char BaudRate), 其作用为初始化串行口。它的输入参数为一个字节,程序就是用这个参数做为开关语句的选择 参数。如调用 InitCom(6),函数就会把波特率设置为 9600。当然这段代码只使用了一种波特 率,能用更高效率的语句去编写,这里就不多讨论了。

  看到这里,你也许会问函数中的 SCON,TCON,TMOD,SCOM 等是代表什么?它们是特殊 功能寄存器。

  SBUF 数据缓冲寄存器 这是一个能直接寻址的串行口专用寄存器。有朋友这样问起 过“为何在串行口收发中,都只是使用到同一个寄存器 SBUF?而不是收发各用一个寄存器。” 实际上 SBUF 包含了两个独立的寄存器,一个是发送寄存,另一个是接收寄存器,但它们都 共同使用同一个寻址地址-99H。CPU 在读 SBUF 时会指到接收寄存器,在写时会指到发送寄

  存器,而且接收寄存器是双缓冲寄存器,这样能避免接收中断没有及时的被响应,数据没

  有被取走,下一帧数据已到来,而造成的数据重叠问题。发送器则不需要用到双缓冲,一般 情况下我们在写发送程序时也不必用到发送中断去外理发送数据。操作 SBUF 寄存器的方法 则很简单,只要把这个 99H 地址用关键字 sfr 定义为一个变量就能对其进行读写操作了,

  如 sfr SBUF = 0x99;当然你也能用其它的名称。通常在标准的 reg51.h 或 at89x51.h 等 头文件中已对其做了定义,只要用#include 引用就能了。

  SCON 串行口控制寄存器 通常在芯片或设备中为了监视或控制接口状态,都会引用 到接口控制寄存器。SCON 就是 51 芯片的串行口控制寄存器。它的寻址地址是 98H,是一个 能位寻址的寄存器,作用就是监视和控制 51 芯片串行口的工作状态。51 芯片的串行口能 工作在几个不一样的工作模式下,其工作模式的设置就是使用 SCON 寄存器。它的各个位的具 体定义如下:

  (MSB) (LSB) SM0 SM1 SM2 REN TB8 RB8 TI RI

  表 8-1 串行口控制寄存器 SCON

  SM0、SM1 为串行口工作模式设置位,这样两位能对应进行四种模式的设置。看表 8

  -2 串行口工作模式设置。

单片机 

 表 8-2 串行口工作模式设置

  在这里只说明最常用的模式 1,其它的模式也就一一略过,有兴趣的朋友能找相关的 硬件资料查看。表中的 fosc 代表振荡器的频率,也就是晶体震荡器的频率。UART 为(Universal Asynchronous Receiver)的英文缩写。

  SM2 在模式 2、模式 3 中为多处理机通信使能位。在模式 0 中要求该位为 0。

  REM 为允许接收位,REM 置 1 时串行口允许接收,置 0 时禁止接收。REM 是由软件置位或 清零。如果在一个电路中接收和发送引脚 P3.0,P3.1 都和上位机相连,在软件上有串行口中断 处理程序,当要求在处理某个子程序时不允许串行口被上位机来的控制字符产生中断,那么可 以在这个子程序的开始处加入 REM=0 来禁止接收,在子程序结束处加入 REM=1 再次打开串行口 接收。大家也能用上面的实际源码加入 REM=0 来进行实验。

  TB8 发送数据位 8,在模式 2 和 3 是要发送的第 9 位。该位能用软件根据需要置位或 清除,通常这位在通信协议中做奇偶位,在多处理机通信中这一位则用于表示是地址帧还是 数据帧。

  RB8 接收数据位 8,在模式 2 和 3 是已接收数据的第 9 位。该位可能是奇偶位,地址/ 数据标识位。在模式 0 中,RB8 为保留位没有被使用。在模式 1 中,当 SM2=0,RB8 是已接 收数据的停止位。

  TI 发送中断标识位。在模式 0,发送完第 8 位数据时,由硬件置位。其它模式中则是在 发送停止位之初,由硬件置位。TI 置位后,申请中断,CPU 响应中断后,发送下一帧数据。 在任何模式下,TI 都必须由软件来清除,也就是说在数据写入到 SBUF 后,硬件发送数据,

  中断响应(如中断打开),这个时候 TI=1,表明发送已完成,TI 不会由硬件清除,所以这个时候必须

  用软件对其清零。

  RI 接收中断标识位。在模式 0,接收第 8 位结束时,由硬件置位。其它模式中则是在接 收停止位的半中间,由硬件置位。RI=1,申请中断,要求 CPU 取走数据。但在模式 1 中,SM2=1 时,当未收到有效的停止位,则不会对 RI 置位。同样 RI 也必须要靠软件清除。

  常用的串行口模式 1 是传输 10 个位的,1 位起始位为 0,8 位数据位,低位在先,1 位停止 位为 1。它的波特率是可变的,其速率是取决于定时器 1 或定时器 2 的定时值(溢出速率)。 AT89c51 和 AT89C2051 等 51 系列芯片只有两个定时器,定时器 0 和定时器 1,而定时器 2

  是 89C52 系列芯片才有的。

  波特率 在使用串行口做通信时,一个很重要的参数就是波特率,只有上下位机的波特率 一样时才能进行正常通信。波特率是指串行端口每秒内能传输的波特位数。有一些开始学习 的朋友认为波特率是指每秒传输的字节数,如标准 9600 会被误认为每秒种能传送 9600 个字节,而实际上它是指每秒能传送 9600 个二进位,而一个字节要 8 个二进位,如用串 口模式 1 来传输那么加上起始位和停止位,每个数据字节就要占用 10 个二进位,9600 波特 率用模式 1 传输时,每秒传输的字节数是 9600÷10=960 字节。51 芯片的串行口工作模式 0 的波特率是固定的,为 fosc/12,以一个 12M 的晶体震荡器来计算,那么它的波特率能达到 1M。 模式 2 的波特率是固定在 fosc/64 或 fosc/32,具体用那一种就取决于 PCON 寄存器中的 SMOD 位,如 SMOD 为 0,波特率为 focs/64,SMOD 为 1,波特率为 focs/32。模式 1 和模式 3 的波 特率是可变的,取决于定时器 1 或 2(52 芯片)的溢出速率。那么我们怎么去计算这两个模 式的波特率设置时相关的寄存器的值呢?能用以下的公式去计算。

  波特率=(2SMOD÷32)×定时器 1 溢出速率

  上式中如设置了 PCON 寄存器中的 SMOD 位为 1 时就能把波特率提升 2 倍。通常会使用 定时器 1 工作在定时器工作模式 2 下,这个时候定时值中的 TL1 做为计数,TH1 做为自动重装值 , 这个定时模式下,定时器溢出后,TH1 的值会自动装载到 TL1,再次开始计数,这样能不 用软件去干预,使得定时更准确。在这个定时模式 2 下定时器 1 溢出速率的计算公式如下:

  溢出速率=(计数速率)/(256-TH1) 上式中的“计数速率”与所使用的晶体振荡器频率有关,在 51 芯片中定时器启动后会

  在每一个机器周期使定时寄存器 TH 的值增加一,一个机器周期等于十二个振荡周期,所以

  能得知 51 芯片的计数速率为晶体振荡器频率的 1/12,一个 12M 的晶体震荡器用在 51 芯片上, 那么 51 的计数速率就为 1M。通常用 11.0592M 晶体是为了得到标准的无误差的波特率,那 么为何呢?计算一下就知道了。如我们要得到 9600 的波特率,晶体震荡器为 11.0592M 和 12M,定 时器 1 为模式 2,SMOD 设为 1,分别看看那所要求的 TH1 为何值。代入公式:

  11.0592M

  9600=(2÷32)×((11.0592M/12)/(256-TH1))

  TH1=250 //看看是不是和上面实例中的使用的数值一样?

  12M

  9600=(2÷32)×((12M/12)/(256-TH1)) TH1≈249.49

  上面的计算能看出使用 12M 晶体的时候计算出来的 TH1 不为整数,而 TH1 的值只能取

  整数,这样它就会有一定的误差存在不能产生精确的 9600 波特率。当然一定的误差是能 在使用中被接受的,就算使用 11.0592M 的晶体振荡器也会因晶体本身所存在的误差使波特

  率产生误差,但晶体本身的误差对波特率的影响是十分之小的,能忽略不计。


  第十三课 C51循环语句

  循环语句是几乎每个程序都会用到的,它的作用就是用来实现需要反复进行多次的操 作。如一个 12M 的 51 芯片应用电路中要求实现 1 毫秒的延时,那么就要执行 1000 次空语句 才能达到延时的目的(当然能使用定时器来做,这里就不讨论),如果是写 1000 条空语 句那是多么麻烦的事情,再者就是要占用很多的存储空间。我们能知道这 1000 条空语句, 无非就是一条空语句重复执行 1000 次,因此我们就能用循环语句去写,这样不但使程序

  结构清晰明了,而且使其编译的效率大大的提高。在 C 语言中构成循环控制的语句有 while,do-while,for 和 goto 语句。同样都是起到循环作用,但具体的作用和使用方法又大不一 样。我们具体来看看。

  goto 语句

  这个语句在很多高级语言中都会有,记得小时候用 BASIC 时就很喜欢用这个语句。它是 一个无条件的转向语句,只要执行到这个语句,程序指针就会跳转到 goto 后的标号所在的 程序段。它的语法如下:

  goto 语句标号; 其中的语句标号为一个带冒号的标识符。示例如下

  void main(void)

  {

  unsigned char a;

  start: a++;

  if (a==10) goto end;

  goto start;

  end:;

  }

  上面一段程序只是说明一下 goto 的使用方法,实际编写很少使用这样的手法。这段程序的意思

  是在程序开始处用标识符“start:”标识,表示程序这是程序的开始,“end:”标识程序的 结束,标识符的定义应遵循前面所讲的标识符定义原则,不能用 C 的关键字也不能和其它变 量和函数名相同,不然就会出错了。程序执行 a++,a 的值加 1,当 a 等于 10 时程序会跳到 end 标识处结束程序,不然跳回到 start 标识处继续 a++,直到 a 等于 10。上面的示例说明 goto 不但能无条件的转向,而且能和 if 语句构成一个循环结构,这些在 C 程序员的程 序中都不太常见,常见的 goto 语句使用方法是用它来跳出多重循环,不过它只能从内层循环 跳到外层循环,不能从外层循环跳到内层循环。在下面说到 for 循环语句时再略为提一提。 为何大多数 C 程序员都不喜欢用 goto 语句?那是因为过多的使用它时会程序结构不清晰,

  过多的跳转就使程序又回到了汇编的编程风格,使程序失去了 C 的模块化的优点。

  while 语句

  while 语句的意思很不难理解,在英语中它的意思是“当…的时候…”,在这里我们可 以理解为“当条件为真的时候就执行后面的语句”,它的语法如下:

  while (条件表达式) 语句;

  使用 while 语句时要注意当条件表达式为真时,它才执行后面的语句,执行完后再次回

  到 while 执行条件判断,为真时重复执行语句,为假时退出循环体。当条件一开始就为假时, 那么 while 后面的循环体(语句或复合语句)将一次都不执行就退出循环。在调试程序时要

  注意 while 的判断条件不能为假而造成的死循环,调试时适当的在 while 处加入断点,也许 会使你的调试工作更加顺利。当然有时会使用到死循环来等待中断或 IO 信号等,如在第一 篇时我们就用了 while(1)来不停的输出“Hello World!”。下面的例子是显示从 1 到 10 的累 加和,读者能修改一下 while 中的条件看看结果会如果,从而体会一下 while 的使用方法。

  #include 《AT89X51.H》

  #include 《stdio.h》

  void main(void)

  {

  unsigned int I = 1;

  unsigned int SUM = 0; //设初值

  SCON = 0x50; //串行口方式 1,允许接收

  TMOD = 0x20; //定时器 1 定时方式 2

  TCON = 0x40; //设定时器 1 开始计数

  TH1 = 0xE8; //11.0592MHz 1200 波特率

  TL1 = 0xE8; TI = 1;

  TR1 = 1; //启动定时器

  while(I《=10)

  {

  SUM = I + SUM; //累加

  printf (“%d SUM=%d ”,I,SUM); //显示

  I++;

  }

  while(1); //这句是为了不让程序完后,程序指针继续向下造成程序“跑飞”

  }

  //最后运行结果是 SUM=55;

  do while 语句

  do while 语句能说是 while 语句的补充,while 是先判断条件是否成立再执行循环体,

  而 do while 则是先执行循环体,再根据条件判断是否要退出循环。这样就决定了循环体无 论在任何条件下都会至少被执行一次。它的语法如下:

  do 语句 while (条件表达式)

  用 do while 怎么写上面那个例程呢?先想一想,再参考下面的程序。

  #include 《AT89X51.H》

  #include 《stdio.h》

  void main(void)

  {

  unsigned int I = 1;

  unsigned int SUM = 0; //设初值

  SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

  TCON = 0x40; //设定时器 1 开始计数

  TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

  TI = 1;

  TR1 = 1; //启动定时器

  do

  {

  SUM = I + SUM; //累加

  printf (“%d SUM=%d ”,I,SUM); //显示 I++;

  }

  while(I《=10);

  while(1);

  }

  在上面的程序看来 do while 语句和 while 语句似乎没有什么两样,但在实际的应用中要注

  意任何 do while 的循环体一定会被执行一次。如把上面两个程序中 I 的初值设为 11,那么 前一个程序不会得到显示结果,而后一个程序则会得到 SUM=11。


  for 语句

  在明确循环次数的情况下,for 语句比以上说的循环语句都要方便简单。它的语法如下: for ([初值设定表达式];[循环条件表达式];[条件更新表达式]) 语句 中括号中的表达式是可选的,这样 for 语句的变化就会很多样了。for 语句的执行:先

  代入初值,再判断条件是否为真,条件满足时执行循环体并更新条件,再判断条件是否为 真……直到条件为假时,退出循环。下面的例子所要实现的是和上二个例子一样的,对照着 看不难理解几个循环语句的差异。

  #include 《AT89X51.H》

  #include 《stdio.h》

  void main(void)

  {

  unsigned int I;

  unsigned int SUM = 0; //设初值

  SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

  TCON = 0x40; //设定时器 1 开始计数

  TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

  TI = 1;

  TR1 = 1; //启动定时器

  for (I=1; I《=10; I++) //这里能设初始值,所以变量定义时能不设

  {

  SUM = I + SUM; //累加

  printf (“%d SUM=%d ”,I,SUM); //显示

  }

  while(1);

  }

  如果我们把程序中的 for 改成 for(; I《=10; I++)这样条件的初值会变成当前 I 变量的

  值。如果改成 for(;;)会怎么样呢?试试看。

  continue 语句

  continue 语句是用于中断的语句,通常使用在循环中,它的作用是结束本次循环,跳 过循环体中没有执行的语句,跳转到下一次循环周期。语法为:

  continue;

  continue 同时也是一个无条件跳转语句,但功能和前面说到的 break 语句有所不一样, continue 执行后不是跳出循环,而是跳到循环的开始并执行下一次的循环。在上面的例子 中的循环体加入 if (I==5) continue;看看什么结果?

  return 语句

  return 语句是返回语句,不属于循环语句,是要学习的最后一个语句所以一并写下了。 返回语句是用于结束函数的执行,返回到调用函数时的位置。语法有二种:

  return (表达式);

  return; 语法中因带有表达式,返回时先计算表达式,再返回表达式的值。不带表达式则返回的

  值不确定。

  下面是一个同样是计算 1-10 的累加,所不一样是的用了函数的方式。

  #include 《AT89X51.H》

  #include 《stdio.h》

  int Count(void); //声明函数

  void main(void)

  {

  unsigned int temp;

  SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

  TCON = 0x40; //设定时器 1 开始计数

  TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

  TI = 1;

  TR1 = 1; //启动定时器

  temp = Count();

  printf (“1-10 SUM=%d ”,temp); //显示

  while(1);

  }

  int Count(void)

  {

  unsigned int I, SUM;

  for (I=1; I《=10; I++)

  {

  SUM = I + SUM; //累加

  }

  return (SUM);

  }


 

  第十四课 C51函数

  上一篇的最后一个例子中有用到函数,其实一直出现在例子中的 main()也算是一个函数,只不过它比较特殊,编译时以它做为程序的开始段。有了函数 C 语言就有了模块化的优 点,一般功能较多的程序,会在编写程序时把每项单独的功能分成数个子程序模块,每个子 程序就能用函数来实现。函数还能被反复的调用,因此一些常用的函数能做成函数库 以供在编写程序时直接调用,从而更好的实现模块化的设计,大大提高编程工作的效率。 一.函数定义

  通常 C 语言的编译器会自带标准的函数库,这些都是一些常用的函数,Keil uv 中也不 例外。标准函数已由编译器软件商编写定义,使用者直接调用就能了,而无需定义。但是 标准的函数不足以满足使用者的特殊要求,因此 C 语言允许使用者根据需要编写特定功能的 函数,要调用它必须要先对其进行定义。定义的模式如下:

  函数类型 函数名称(形式参数表)

  {

  函数体

  }

  函数类型是说明所定义函数返回值的类型。返回值其实就是一个变量,只要按变量

  类型来定义函数类型就行了。如函数不需要返回值函数类型能写作“void”表示该函数没 有返回值。注意的是函数体返回值的类型一定要和函数类型一致,不然会造成错误。函数名 称的定义在遵循 C 语言变量命名规则的同时,不能在同一程序中定义同名的函数这将会造成 编译错误(同一程序中是允许有同名变量的,因为变量有全局和局部变量之分)。形式参数 是指调用函数时要传入到函数体内参与运算的变量,它能有一个、几个或没有,当不需要 形式参数也就是无参函数,括号内能为空或写入“void”表示,但括号不能少。函数体中 能包含有局部变量的定义和程序语句,如函数要返回运算值则要使用 return 语句进行返 回。在函数的{}号中也能什么也不写,这就成了空函数,在一个程序项目中能写一些 空函数,在以后的修改和升级中能方便的在这些空函数中进行功能扩充。

  二.函数的调用

  函数定义好以后,要被其它函数调用了才能被执行。C 语言的函数是能相互调用的, 但在调用函数前,必须对函数的类型进行说明,就算是标准库函数也不例外。标准库函数的 说明会被按功能分别写在不一样的头文件中,使用时只要在文件最前面用#include 预处理语 句引入相应的头文件。如前面一直有使用的 printf 函数说明就是放在文件名为 stdio.h 的 头文件中。调用就是指一个函数体中引用另一个已定义的函数来实现所需要的功能,这个时候函 数体称为主调用函数,函数体中所引用的函数称为被调用函数。一个函数体中能调用数个 其它的函数,这些被调用的函数同样也能调用其它函数,也能嵌套调用。笔者本人认为 主函数只是相对于被调用函数而言。在 c51 语言中有一个函数是不能被其它函数所调用的, 它就是 main 主函数。调用函数的一般形式如下:

  函数名 (实际参数表) “函数名”就是指被调用的函数。实际参数表能为零或多个参数,多个参数时要用逗

  号隔开,每个参数的类型、位置应与函数定义时所的形式参数一一对应,它的作用就是把参 数传到被调用函数中的形式参数,如果类型不对应就会产生一些错误。调用的函数是无参函 数时不写参数,但不能省后面的括号。

  在以前的一些例子我们也能看不一样的调用方式:

  1.函数语句

  如 printf (“Hello World! ”); 这是在 我们的第一个程序中出现的,它以 “Hello

  World! ”为参数调用 printf 这个库函数。在这里函数调用被看作了一条语句。

  2.函数参数 “函数参数”这种方式是指被调用函数的返回值当作另一个被调用函数的实际参

  数,如 temp=StrToInt(CharB(16));CharB 的返回值作为 StrToInt 函数的实际参数传递。

  3.函数表达式

  而在上一篇的例子中有 temp = Count();这样一句,这个时候函数的调用作为一个运算 对象出现在表达式中,能称为函数表达式。例子中 Count()返回一个 int 类型的返回 值直接赋值给 temp。注意的是这种调用方式要求被调用的函数能返回一个同类型的值, 不然会出现不可预料的错误。

  前面说到调用函数前要对被调用的函数进行说明。标准库函数只要用#include 引入已 写好说明的头文件,在程序就能直接调用函数了。如调用的是自定义的函数则要用如下形 式编写函数类型说明

  类型标识符 函数的名称(形式参数表); 这样的说明方式是用在被调函数定义和主调函数是在同一文件中。你也能把这些写到

  文件名.h 的文件中用#include “文件名.h”引入。如果被调函数的定义和主调函数不是在同 一文件中的,则要用如下的方式进行说明,说明被调函数的定义在同一项目的不一样文件之上, 其实库函数的头文件也是如此说明库函数的,如果说明的函数也能称为外部函数。

  extern 类型标识符 函数的名称(形式参数表); 函数的定义和说明是完全不一样的,在编译的角度上看函数的定义是把函数编译存放在

  ROM 的某一段地址上,而函数说明是告诉编译器要在程序中使用那些函数并确定函数的地 址。如果在同一文件中被调函数的定义在主调函数之前,这个时候能不用说明函数类型。也就 是说在 main 函数之前定义的函数,在程序中就能不用写函数类型说明了。能在一个函 数体调用另一个函数(嵌套调用),但不允许在一个函数定义中定义另一个函数。还要注意 的是函数定义和说明中的“类型、形参表、名称”等都要相一致。

  三.中断函数 中断服务函数是编写单片机应用程序不可缺少的。中断服务函数只有在中断源请求响应

  中断时才会被执行,这在处理突发事件和实时控制是十分有效的。例如:电路中一个按钮, 要求按钮后 LED 点亮,这个按钮何时会被按下是不可预知的,为了要捕获这个按钮的事件, 通常会有三种方法,一是用循环语句不断的对按钮进行查询,二是用定时中断在间隔时间内 扫描按钮,三是用外部中断服务函数对按钮进行捕获。在这个应用中只有单一的按钮功能, 那么第一种方式就能胜任了,程序也很简单,但是它会不停的在对按钮进行查询浪费了

  CPU 的时间。实际应用中一般都会还有其它的功能要求同时实现,这个时候能根据需要选用第 二或第三种方式,第三种方式占用的 CPU 时间最少,只有在有按钮事件发生时,中断服务函 数才会被执行,其余的时间则是执行其它的任务。

  如果你学习过汇编语言的话,刚开始写汇编的中断应用程序时,你一定会为出入堆栈的 问题而困扰过。单片机c语言 语言扩展了函数的定义使它能直接编写中断服务函数,你能不必考 虑出入堆栈的问题,从而提高了工作的效率。扩展的关键字是 interrupt,它是函数定义时 的一个选项,只要在一个函数定义后面加上这个选项,那么这个函数就变成了中断服务函数。

  在后面还能加上一个选项 using,这个选项是指定选用 51 芯片内部 4 组工作寄存器中的

  那个组。开始学习者能不必去做工作寄存器设定,而由编译器自动选择,避免产生不必要的错 误。定义中断服务函数时能用如下的形式。

  函数类型 函数名 (形式参数) interrupt n [using n]

  interrupt 关键字是不可缺少的,由它告诉编译器该函数是中断服务函数,并由后面的

  n 指明所使用的中断号。n 的取值范围为 0-31,但具体的中断号要取决于芯片的型号,像 AT89c51 实际上就使用 0-4 号中断。每个中断号都对应一个中断向量,具体地址为 8n+3, 中断源响应后处理器会跳转到中断向量所处的地址执行程序,编译器会在这地址上产生一个 无条件跳转语句,转到中断服务函数所在的地址执行程序。下表是 51 芯片的中断向量和中 断号。

  单片机
表 9-1 AT89c51 芯片中断号和中断向量

  使用中断服务函数时应注意:中断函数不能直接调用中断函数;不能通过形参传速参数; 在中断函数中调用其它函数,两者所使用的寄存器组应相同。限于篇幅其它与函数相关的知 识这里不能一一加以说明,如变量的传递、存储,局部变量、全部变量等,有兴趣的朋友可 以访问笔者的网站 阅读更多相关文章。

  下面是简单的例子。首先要在前面做好的实验电路中加多一个按钮,接在 P3.2(12 引脚外 部中断 INT0)和地线之间。把编译好后的程序烧录到芯片后,当接在 P3.2 引脚的按钮接下 时,中断服务函数 Int0Demo 就会被执行,把 P3 当前的状态反映到 P1,如按钮接下后 P3.7

  (之前有在这脚装过一按钮)为低,这个时候 P1.7 上的 LED 就会熄灭。放开 P3.2 上的按钮后,

  P1LED 状态保持先前按下 P3.2 时 P3 的状态。

  #include 《at89x51.h》

  unsigned char P3State(void); //函数的说明,中断函数不用说明

  void main(void)

  {

  IT0 = 0; //设外部中断 0 为低电平触发

  EX0 = 1; //允许响应外部中断 0

  EA = 1; //总中断开关

  while(1);

  }

  //外部中断 0 演示,使用 2 号寄存器组

  void Int0Demo(void) interrupt 0 using 2

  {

  unsigned int Temp; //定义局部变量

  P1 = ~P3State(); //调用函数取得 p2 的状态反相后并赋给 P1

  for (Temp=0; Temp《50; Temp++); //延时 这里只是演示局部变量的使用

  }

  //用于返回 P3 的状态,演示函数的使用

  unsigned char P3State(void)

  {

  unsigned char Temp;

  Temp = P3; //读取 P3 的引脚状态并保存在变量 Temp 中

  //这样只有一句语句实在没必要做成函数,这里只是学习函数的基本使用方法

  return Temp;

  }




 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
评论(0)
发评论
yang959719598 2012-07-18
1 回复 举报
有没有电子文档啊?? 收起回复

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分