独闷闷网

 找回密码
 立即注册
搜索
楼主: jianhong_wu
打印 上一主题 下一主题
收起左侧

[原创] 从业十年,教你单片机入门基础。(连载)

[复制链接]
81#
 楼主| 发表于 2015-11-9 00:27:49 | 只看该作者
本帖最后由 jianhong_wu 于 2015-11-9 00:31 编辑

第四十五节:二维数组。
       上一节讲一维数组时漏讲了一个知识点,一维数组在定义时,如果预先给它填写若干个初始化的数据,也可以省略中括号[N]里面的元素个数N,这样编译器在编译时会根据初始化的个数来自动识别和定义此一维数组实际元素个数。比如:
unsigned char  y[3]={10,11,12};
        跟
unsigned char  y[]={10,11,12};
       的意义是一样的。注意,省略元素个数时必须要有初始化的数据,否则编译器不知道此数组的长度可能导致编译出错。
       继续回到本节的内容,二维数组。一维数组只有一个下标,像由很多点连成的一条直线,而二维数组有两个下标,布下了一行行的点像一张矩形的网,它的两个下标分别代表了行和列,行和列又像我们所学的y轴和x轴坐标,通过y轴和x轴坐标就可以找到所需的点,也就是数组的某个元素。
       上述是对二维数组的感性描述,二维数组是由一维数组发展而来,所以具备了很多一维数组的特点。二维数组的所有”网点”元素的地址都是挨个相临的,先第0行,再第1行,再第2行…再第N行,上一行尾元素跟下一行头元素的地址是相临连续的。
        二维数组未带初始化时的通用定义格式如下:
类型 数组名[行数Y][列数X];
比如:
unsigned char  a[2][3]; //此处的2代表有2行,3代表有3列。
分析:此二维数组定义了6个变量,跟一维数组一样,下标都是从0开始,到(N-1)时结束,此处的N代表行数或者列数。所以a[2][3]数组的元素挨个分别是a[0][0],a[0][1], a[0][2], a[1][0], a[1][1], a[1][2]这6个变量。
         二维数组有两种常用初始化格式,一种是逐行初始化,一种是整体初始化。
         第一种逐行初始化:
unsigned char   a[2][3]=
{
   {0,1,2},
   {3,4,5}
};
        在逐行初始化定义二维数组时,只要有初始化的数据,也可以省略行下标,但是列下标不能省略,比如:
unsigned char   a[][3]=
{
   {0,1,2},
   {3,4,5}
};
此时编译器会根据元素的个数来确定行数是多少。

        第二种整体初始化,跟一维数组一样,内部数据元素不需要额外增加大括号来分行。
unsigned char   a[2][3]=
{
   0,1,2,3,4,5
};
或者
unsigned char   a[2][3]=
{
   0,1,2,
   3,4,5
};
都行。
       C语言是很自由很丰富的语言,比如二维数组还允许不完全初始化的一些情况,我就不再深入讲解,我讲解的都是挑选一些针对以后单片机项目中可能会经常用到的语法。
       二维数组我在很多项目上还是经常用到的,比如用在一些需要把所得的信息进行查表判断的项目,在每一行里放一条关键词字符串信息,利用循环语句进行逐行查找匹配。至于二维数组如何存放字符串的知识点以后再讲。这节的重点是让大家对二维数组有个初步的认识。
       现在编写一个程序来熟悉一下二维数组的书写和使用格式。最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4.         

  5.    unsigned char  a[2][3]=  //定义和初始化一个二维数组
  6.    {
  7.       {0,1,2},
  8.       {3,4,5}
  9.    };

  10.         
  11.   GuiWdData0=a[0][0];   //把a[0][0]这个元素变量放到窗口变量0里面显示
  12.   GuiWdData1=a[0][1];   //把a[0][1]这个元素变量放到窗口变量1里面显示
  13.   GuiWdData2=a[0][2];   //把a[0][2]这个元素变量放到窗口变量2里面显示
  14.   GuiWdData3=a[1][0];   //把a[1][0]这个元素变量放到窗口变量3里面显示
  15.   GuiWdData4=a[1][1];   //把a[1][1]这个元素变量放到窗口变量4里面显示
  16.   GuiWdData5=a[1][2];   //把a[1][2]这个元素变量放到窗口变量5里面显示
  17.         
  18. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  19.    while(1)  
  20.    {
  21.       initial();
  22.       key_service();
  23.       display_service();
  24.    }

  25. }
复制代码


    查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
    变量元素a[0][0]为0。
    变量元素a[0][1]为1。
    变量元素a[0][2]为2。
    变量元素a[1][0]为3。
    变量元素a[1][1]为4。
    变量元素a[1][2]为5。
    下节预告:while循环语句。
(未完待续)

乐于分享,勇于质疑!
82#
 楼主| 发表于 2015-11-14 02:22:00 | 只看该作者
本帖最后由 jianhong_wu 于 2015-11-14 02:25 编辑

第四十六节:while循环语句。

       46.1   程序的跑道
       “程序跑起来了吗?”同行交流经常用的一句话。程序在哪里跑?有跑道吗?有的。循环语句就像一条椭圆的跑道,程序在跑道上不停的跑,永无止境,一秒几百万圈的速度。单片机main主函数内往往有一条while(1)语句,这就是单片机的“循环跑道”,称之为主循环,主循环内还可以继续嵌套多层while循环语句。

       46.2   while循环的常见格式
while(条件)
{
    语句1;   
    语句2;
     ……   
    语句N;
}

       分析:
(1)先判断条件是否为真。如果为假,就不执行花括号循环体内的语句1至语句N。如果为真,才执行花括号循环体内的语句1至语句N,当执行完花括号循环体内最后的“语句N“时,单片机继续返回while(条件)处,继续判断条件是否为真,如果为假就跳过花括号循环体,如果为真就继续从“语句1“执行到“语句N“,然后再返回while(条件)处,依次循环下去,直到条件为假时才罢休,否则一直循环下去。
(2)while(条件)语句中,条件判断真假的规则跟if语句一模一样,有两种类型:一种是纯常量或者变量类型的,只要此数值不等于0就认为是真,所以while(1)也称死循环语句,因为里面的条件永远不为0,如果不遇到break,return,goto这些语句,那么就永远也别想跳出这个循环;另外一种是关系判断,以及关系语句之间的“与或”判断,跟if语句规则一样,不多讲。break,return,goto这些语句后面章节会讲到。

       46.3   while省略花括号,后面第一条语句前没带分号
while(条件)
         语句1;   
        语句2;
        ……   
        语句N;

        分析:
        跟if语句一样,此时循环体默认只包含“语句1”,相当于:  
while(条件)
{
         语句1;   
}
        语句2;
        ……   
         语句N;


         46.4   while省略花括号,后面第一条语句前带分号
        while(条件);
         语句1;   
        语句2;
        ……   
        语句N;

        分析:
        注意,此时循环体默认不包含“语句1”,而是相当于:   
while(条件)
          ;   //一个分号代表一条空语句,语法上要求加上分号
         语句1;
         语句2;
         ……   
         语句N;
         此时如果条件一直为真,单片机就一直在空循环耗着,执行不到语句1的语句。直到条件为假才罢休。


          46.5   编写程序
          编写一个程序来熟悉一下while语句的书写和使用格式。最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:

  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4.         
  5.   unsigned char a=0;  //观察这个数最后的变化
  6.   unsigned char b=0;  //观察这个数最后的变化

  7.   unsigned char i;  //控制循环体的条件判断变量


  8.   i=3;   
  9.   while(i)  //i不断变为0时才跳出此循环体
  10.   {
  11.      a=a+1;
  12.      i=i-1; //循环的条件不断发生变化,不断减小
  13.   }


  14.   i=0;
  15.   while(i<3)  //i大于等于3时才跳出此循环体
  16.   {
  17.      b=b+2;
  18.      i=i+1;   //循环的条件不断发生变化,不断增加
  19.   }

  20.      
  21.   GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  22.   GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示

  23.         
  24. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  25.    while(1)  
  26.    {
  27.       initial();
  28.       key_service();
  29.       display_service();
  30.    }

  31. }
复制代码


           查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
           变量a为3。
            变量b为6。


            下节预告:while循环语句的嵌套与break语句。
(未完待续)

乐于分享,勇于质疑!
83#
 楼主| 发表于 2015-11-22 08:40:42 | 只看该作者
本帖最后由 jianhong_wu 于 2015-11-22 08:45 编辑

第四十七节:循环语句do while和for。
       47.1   do while语句的常见格式
do{
       语句1;   
       语句2;
       ……   
       语句N;
} while(条件);
      分析:
      单片机从上往下执行语句,先从do那里无条件进来,从语句1开始往下执行,一直执行到语句N,然后开始判断while(条件)的条件是否为真,如果为真继续返回到do的入口处,继续从语句1开始往下执行,依次循环。大家留意到了吗,do while和while语句有什么差别?差别是,dowhile是先无条件进来执行一次循环体(花括号里所有的程序代码),执行到循环体最底部才判断while(条件)的条件是否为真来决定是否继续循环,先上车再买票。而while语句是先判断条件是否为真再决定是否需要进入循环体,先买票再上车。


      47.2   for语句的简介
      for语句也是循环语句,任何for语句实现的功能都可以用while语句来编程实现同样的功能,那for语句和while语句有什么差别呢?for语句把变量初始化,变量的条件判断,变量在执行循环体后的步进变化这三个常见要素集成在语句内部,以标准的格式书写出来。在很多场合下,for在书写和表达方面比while语句显得更加简洁和直观。


      47.3   for语句的自加格式。
for(变量的初始化语句; 变量的条件判断;变量在执行一次循环体后自加的步进变化)
{
       语句1;
       语句2;
       ……
       语句N;
}
在把上述变成具体一点如下:
for(i=0; i<3;i++)
{
      语句1;
      语句2;
      ……
      语句N;
}
       分析:
       单片机从上往下,在进入循环体前,先把变量i初始化赋值0,然后判断i是否小于3这个条件,如果此条件为真,就开始从语句1往下执行到语句N,执行完一次循环体后,i因为“i++”语句就自加1,此时i从原来初始化的0变成了1,接着再返回来到for语句的第二句条件判断”i<3”那里,判断i是否继续小于3这个条件,如果此条件为真就继续往下执行,否则就跳过循环体。因此上述for语句的功能如果用while语句来写,相当于以下代码:

i=0;  //进入循环体之前先初始化给予初值
while(i<3)
{
     语句1;
     语句2;
     ……
     语句N;
     i++;   //执行一次循环体之后此变量自加发生变化
}
上述的循环语句只执行了3次循环体。


      47.4   for语句的自减格式
       刚才讲的for(i=0; i<3;i++)格式,它的变量i是不断自加的,还有一种比较常见的格式是i不断自减的。如下:
for(i=3; i>0;i--)
{
     语句1;
     语句2;
     ……
     语句N;
}
      上述自减的for语句功能如果用while语句来写,相当于以下代码:

i=3;  //进入循环体之前先初始化给予初值
while(i>0)
{
     语句1;
     语句2;
     ……
     语句N;
     i--;   //执行一次循环体之后此变量自减发生变化
}
     上述的循环语句也是只执行了3次循环体。

     47.5   for省略花括号,后面第一条语句前没带分号
for(i=0; i<3;i++)
         语句1;  
         语句2;
         ……   
         语句N;

       分析:
       跟if语句一样,此时循环体默认只包含“语句1”,相当于:  
for(i=0; i<3;i++)
{
         语句1;  
}
         语句2;
         ……   
          语句N;

       47.6   for省略花括号,后面第一条语句前带分号
for(i=0; i<3;i++);
         语句1;  
         语句2;
         ……   
         语句N;

    分析:
    注意,此时循环体默认不包含“语句1”,而是相当于:   
for(i=0; i<3;i++)
           ;  //一个分号代表一条空语句,语法上要求加上分号
       语句1;
       语句2;
       ……   
       语句N;
       此时循环体内先循环执行三次空语句,然后才会结束for循环,接着从语句1开始往下执行。

       47.7   编写程序
       编写一个程序来熟悉一下do while和for语句的使用。最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:
  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4.         
  5.   unsigned char a=0;  //观察这个数最后的变化
  6.   unsigned char b=0;  //观察这个数最后的变化
  7.   unsigned char c=0;  //观察这个数最后的变化


  8.   unsigned char i;  //控制循环体的条件判断变量

  9.   i=3;   
  10.   do
  11.   {
  12.      a=a+1;  //每执行一次循环体a就增加1,此行代码被循环执行了3次
  13.          i=i-1;  //i不断变小
  14.   }while(i); //i不断变小,当i变为0时才跳出此循环体



  15.   for(i=0;i<3;i++)
  16.   {
  17.      b=b+2;  //此行代码被循环执行了3次
  18.   }


  19.   for(i=3;i>0;i--)
  20.   {
  21.      c=c+3;  //此行代码被循环执行了3次
  22.   }



  23.      
  24.   GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  25.   GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示
  26.   GuiWdData2=c;   //把c这个变量放到窗口变量1里面显示
  27.         
  28. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  29.    while(1)  
  30.    {
  31.       initial();
  32.       key_service();
  33.       display_service();
  34.    }

  35. }
复制代码


        查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
        变量a为3。
        变量b为6。
        变量c为9。
         下节预告:循环体内的continue和break语句。
(未完待续)

乐于分享,勇于质疑!
84#
 楼主| 发表于 2015-11-30 09:34:25 | 只看该作者
本帖最后由 jianhong_wu 于 2015-11-30 09:39 编辑

第四十八节:循环体内的continue和break语句。

48.1   continue语句
        通常情况下,单片机在循环体里从上往下执行,一直执行到最底部的花括号才会返回到循环体入口处准备第二次循环,但是,如果在循环体中途遇到continue语句,单片机虽然没有执行到最底部的花括号,也会马上返回到循环体入口处准备第二次循环,在continue语句之后至最底部花括号之间的语句没有被执行到。比如:

while(…)或者for(…)   //循环体的条件判断入口处
{    //循环体开始
    语句1;   
    语句2;
    continue;
    语句3;

    ……   
    语句N;
}    //循环体结束

       分析:
       上述语句中,单片机从语句1开始执行到continue语句,然后马上就返回到循环体的条件判断入口处,也就是语句3至语句N是不可能被执行到的,上述语句简化后相当于:

while(…)或者for(…)   //循环体的条件判断入口处
{    //循环体开始
    语句1;   
    语句2;
}    //循环体结束

       既然可以如此简化,还要continue语句有什么用呢?我们一般应用continue的时候不会像上面那样单独使用,而是配合这if条件判断语句来用,这样continue才有它存在价值,比如:

while(…)或者for(…)   //循环体的条件判断入口处
{    //循环体开始
    语句1;   
    语句2;
    if(某条件)
    {
         continue;
    }
    语句3;

    ……   
    语句N;
}    //循环体结束


48.2    break语句
         continue语句是提前结束当前这一次的循环,准备进入下一次的新循环,是某次结束的概念。而break语句是直接跳出当前循环体,结束当前循环体,是整体结束的概念。
while(…)或者for(…)   //循环体的条件判断入口处
{    //循环体开始
    语句1;   
    语句2;
    break;
    语句3;

    ……   
    语句N;
}    //循环体结束
语句(N+1);  //循环体之外语句

      分析:
      上述语句中,单片机从语句1开始执行到break语句,无需判断while或者for的条件,直接马上跳出循环体,执行到循环体之外的语句(N+1)。在实际项目中,break往往也是会配合if条件判断语句一起使用,比如:

while(…)或者for(…)   //循环体的条件判断入口处
{    //循环体开始
    语句1;   
    语句2;
    if(某条件)
    {
         break;
    }
    语句3;

    ……   
    语句N;
}    //循环体结束
语句(N+1);  //循环体之外语句


48.3    break语句能跳多远
         break语句能跳多远?预知答案请先分析以下这个例子:

while(…)
{  
      语句1;   
      语句2;
        while(…)
      {   
           语句3;   
           break;
           语句4;
     }   
     语句5;
}   
语句6;

       上述例子中,在while循环里面有藏着第二个while循环,这种循环之中还有循环,通常称之为循环嵌套。单片机从上往下执行语句,当遇到break后,它会跳到语句5那里呢,还是会跳到语句6那里?正确答案是语句5那里。说明了break语句跳远范围仅仅刚好能跳出当前的循环体。在上述循环嵌套的例子中,最内层的break只能跳出最内层的循环体,不能跳到最外层的语句6那里,如果需要继续跳出最外层的语句6那里,可以继续在外层的循环体内再增加一个break语句。


48.4    还有哪些语句可以无条件退出循环体
         还有return和goto语句可以跳出循环体。这部分的内容大家只需大概了解一下即可。return语句比break语句还厉害,它不仅仅跳出当前循环体,还是跳出了当前函数,也就是提前结束了当前函数,这部分的内容后面章节会讲到,暂时不用管。而goto语句在C语言中大家都公认不建议用,因为它很容易扰乱大家常用的C语言编程结构,我本人也从来没有用过goto语句,所以这条语句我也不再深入讲解它。


48.5   编写程序
         编写一个程序来熟悉一下continue和break语句的使用。最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:
  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4.         
  5.   unsigned char a=0;  //观察这个数最后的变化
  6.   unsigned char b=0;  //观察这个数最后的变化
  7.   unsigned char c=0;  //观察这个数最后的变化
  8.   unsigned char d=0;  //观察这个数最后的变化

  9.   unsigned char i;  //控制循环体的条件判断变量


  10. //i<6的条件判断是在进入循环体之前判断,而i的自加1是在执行完一次循环体之后才自加的。
  11.   for(i=0;i<6;i++)  
  12.   {
  13.      a=a+1;    //被执行了6次,分别是第0,1,2,3,4,5次
  14.      if(i>=3)
  15.      {
  16.         continue;  //提前结束本次循环,准备进入下一次循环
  17.      }
  18.      b=b+1;  //被执行了3次,分别是第0,1,2次
  19.   }


  20. //i<6的条件判断是在进入循环体之前判断,而i的自加1是在执行完一次循环体之后才自加的。
  21.   for(i=0;i<6;i++)  
  22.   {
  23.      c=c+1;   //被执行了4次,分别是第0,1,2,3次
  24.      if(i>=3)
  25.      {
  26.         break; //马上跳出当前循环体
  27.      }
  28.      d=d+1;   //被执行了3次,分别是第0,1,2次
  29.   }

  30.      
  31.   GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  32.   GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示
  33.   GuiWdData2=c;   //把c这个变量放到窗口变量2里面显示
  34.   GuiWdData3=d;   //把d这个变量放到窗口变量3里面显示
  35.         
  36. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  37.    while(1)  
  38.    {
  39.       initial();
  40.       key_service();
  41.       display_service();
  42.    }

  43. }
复制代码


       查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
       变量a为6。
       变量b为3。
       变量c为4。     
       变量d为3。
       下节预告:for和while的循环嵌套。
(未完待续)

乐于分享,勇于质疑!
85#
 楼主| 发表于 2015-12-5 23:32:59 | 只看该作者
本帖最后由 jianhong_wu 于 2015-12-6 11:15 编辑

第四十九节:for和while循环体的嵌套。

        49.1      循环体嵌套的执行顺序
         for循环体内又包含了for循环体可以称为循环体嵌套。比如:
for(i=0;i<2;i++)  //大循环
{
       语句1;
       for(k=0;k<3;k++) //内嵌的小循环
       {
               语句2;
       }
       语句3;
}
         上述例子中,带i的for称为大循环,带k的for称为小循环,单片机从大循环入口进来,由上往下执行,执行第1次大循环,先执行1次“语句1”,接着进入小循环,小循环要连续循环执行3次“语句2”才跳出小循环,之后执行1次“语句3”,然后再返回到大循环入口判断i条件是否满足,此时条件满足,继续执行第2次大循环,1次“语句1”,3次“语句2”,1次“语句3”,第2次循环结束后又返回到大循环入口判断i条件,此时i已经等于2不再小于2了,因此条件不满足,结束整个循环嵌套。上述执行的语句顺序如下:
         语句1;   //第1次大循环开始
         语句2;   
         语句2;
         语句2;
         语句3;
         语句1;   //第2次大循环开始
         语句2;
         语句2;
         语句2;
         语句3;
         根据此顺序,再看一个具体的程序例子:
a=0;  
b=0;
for(i=0;i<2;i++)  //大循环
{
     a=a+1;  //被执行了2次
     for(k=0;k<3;k++) //内嵌的小循环
     {
           b= b+1;  //被执行了6次
     }
}
         上述例子中,执行完程序后,a的值变成了2,b的值变成了6。重点分析b的变化,“b=b+1”在内嵌循环体里被执行了6次,6次从何而来?就是i乘以k等于6。这个乘法次数是循环嵌套一个很重要的特性。上述程序如果用while语句来实现如下:
a=0;  
b=0;
i=0;  //控制大循环的变量初始化
while(i<2)  //大循环
{
     a=a+1;  //被执行了2次
     k=0;    //控制小循环的变量初始化
     while(k<3) //内嵌的小循环
     {
         b= b+1;  //被执行了6次
         k=k+1;
    }
     i=i+1;
}


        49.2   循环嵌套的常见用途---二维数组的应用
         前面章节讲到了二维数组a[2][3],它有6个变量,在没有学for语句之前,要依次把每个元素单独赋值清零真不容易,要写6次赋值语句如下:
a[0][0]=0;
a[0][1]=0;
a[0][2]=0;
a[1][0]=0;
a[1][1]=0;
a[1][2]=0;
         自从懂了for嵌套语句之后,直接写代码如下,简洁了许多:
  1. for(i=0;i<2;i++)  //大循环
  2. {
  3.      for(k=0;k<3;k++) //内嵌的小循环
  4.      {
  5.           a[i][k]=0;
  6.       }
  7. }
复制代码


        49.3     循环嵌套的常见用途---大延时
        以后接触单片机项目后,会经常会用到delay这个延时函数,大部分都是利用for循环来实现,小延时函数往往不用嵌套,直接如下编写:
for(k=0;k<N;k++);  
      上述的N是控制循环次数,每次循环都要消耗单片机一点时间,如果N越大需要消耗的时间就越多,起到延时的作用。但是N所能取的最大值受它所定义的类型所限制,比如是unsigned char类型时最大范围是255,是unsigned int类型时最大范围是65535,是unsigned long类型时最大范围是4294967295。如果要实现更大的延时怎么办?就可以用for的循环嵌套,利用循环嵌套次数的乘法翻倍特性,很容易编写大的延时函数。比如:
for(i=0;i<M;i++)  //大循环
{
      for(k=0;k<N;k++); //内嵌的小循环
}
      此时循环的次数是N乘以M的乘积。
      现在编写一个循环嵌套的练习程序,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,修改的main程序代码如下:
  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4.         
  5.   unsigned char a=0;  //观察这个数最后的变化
  6.   unsigned char b=0;  //观察这个数最后的变化
  7.   unsigned char c=0;  //观察这个数最后的变化


  8.   unsigned char i;  //控制大循环体的条件判断变量
  9.   unsigned char k;  //控制内嵌小循环体的条件判断变量

  10.   for(i=0;i<2;i++)  //大循环
  11.   {
  12.      a=a+1;    //被执行了2次
  13.      for(k=0;k<3;k++)  //内嵌小循环
  14.      {  
  15.           b=b+1;  //被执行了6次,也就是i乘以k,2乘以3等于6.
  16.      }
  17.      c=c+1;    //被执行了2次
  18.   }

  19.      
  20.   GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  21.   GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示
  22.   GuiWdData2=c;   //把c这个变量放到窗口变量2里面显示

  23.         
  24. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  25.    while(1)  
  26.    {
  27.       initial();
  28.       key_service();
  29.       display_service();
  30.    }

  31. }
复制代码


          查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
         变量a为2。
          变量b为6。
          变量c为2。
          下节预告:switch语句。
(未完待续)


乐于分享,勇于质疑!
86#
 楼主| 发表于 2015-12-14 23:11:17 | 只看该作者
本帖最后由 jianhong_wu 于 2015-12-14 23:16 编辑

第五十节:switch语句。
         switch是非常重要的语句,我几乎所有的单片机项目都是用switch搭建程序主框架。如果说while和for是一对孪生兄弟,那么if-elseif和switch也是一对孪生兄弟,凡是用switch语句能实现的功能都可以用if-elseif实现。switch有条件分支的功能,在条件超过3个以上的条件分支功能里,switch书写会比if-else if更加直观清晰。
         50.1      switch语句的语法
         以下是switch语句常见的格式:

switch(变量)    //根据变量的数值大小从对应的case入口进来
{
     case 0:   //入口0
         语句0;
         break; //switch程序体的出口之一
     case 1:  //入口1
         语句1;
         break; //switch程序体的出口之一
     case 2: //入口2
         语句2;
         break; //switch程序体的出口之一
}    //最下面的花括号也是一个switch程序体的出口之一

        执行顺序分析:
        单片机从switch(变量)进来,根据变量的数值大小,进入对应的case入口里。假如变量等于1,就直接进入到case 1入口,执行“语句1”后,遇到break语句就跳出整个switch程序体。假如变量等于3,单片机从switch(变量)进来,因为没有发现case 3,所以直接跳出switch程序体没有执行任何语句。多补充一句,在case 2选项中,语句2最后的break可以省略,因为case 2是最后一个case,即使没有遇到break也会遇到最下面的花括号而跳出switch程序体。上述程序功能如果用if-elseif语句来实现如下:

if(变量==0)
{
    语句0;
}
else if(变量==1)
{
    语句1;
}
else if(变量==2)
{
    语句2;
}


        50.2      switch语句的break
        大家从50.1的例子中看到了三个关键字分别是:switch,case,break。其实并不是每个case都必须要break配套,break只是起到一个出口的功能。假如没有遇到break,程序会一直往下执行,直到遇到break或者switch最下面的花括号为止。比如:

switch(变量)    //根据变量的数值大小从对应的case入口进来
{
     case 0:   //入口0
         语句0;
         break;
     case 1:  //入口1
         语句1;
     case 2: //入口2
         语句2;
         break;
     case 3: //入口3
         语句3;
         break;
}   //最下面的花括号也是一个switch程序体的出口之一

        执行顺序分析:
        假如此时switch(变量)的变量等于1,就直接进入到case 1入口,执行“语句1”后,没有遇到break语句,继续执行“语句2”,紧接着遇到break语句,才跳出整个switch程序体。本例子中case 1没有break语句,就继续往下执行下面case2里面的语句,直到遇到break或者最下面的花括号为止。

        50.3      switch语句的case有规定顺序吗?必须连贯吗?
switch程序体内部可以写很多case入口,这些case入口是不是必须按从小到大的顺序?是不是规定必须case数字连贯?答:没有规定顺序,也没有规定case数字连贯,case的数值只是代表入口。比如以下两种写法都是合法的:

        50.3.1    case不按从小到大的顺序:

switch(变量)   
{
     case 2:
         语句2;
         break;
     case 0:  
         语句0;
         break;
     case 1:  
         语句1;
         break;
}   

         50.3.2    case的数字不连贯
switch(变量)   
{
    case 0:  
         语句0;
         break;
     case 3:
         语句3;
         break;
     case 9:  
         语句9;
         break;
}   


         50.4      switch语句的default。
         default和break一样,也不是必须的语句,应根据程序需要来选择。default相当于if-else if-else 语句中的else,也就是当switch(变量)的变量没有匹配的case入口时,就会默认进入default入口,就像if-else if-else 语句中当前面所有的条件不满足时,就进入else语句的程序体,比如:

switch(变量)    //根据变量的数值大小从对应的case入口进来
{
     case 0:   //入口0
         语句0;
         break; //switch程序体的出口之一
     case 1:  //入口1
         语句1;
         break; //switch程序体的出口之一
     case 2: //入口2
         语句2;
         break; //switch程序体的出口之一
     default:  //当所有的case不满足,就进入default入口
         语句3;
         break;
}    //最下面的花括号也是一个switch程序体的出口之一

        执行顺序分析:
        假如switch(变量)的变量等于35,因为没有找到case 35,所以就直接从默认的default入口进来执行” 语句3”,然后遇到break语句就跳出switch程序体。上述程序功能如果用if-elseif-else语句来实现如下:

if(变量==0)
{
    语句0;
}
else if(变量==1)
{
    语句1;
}
else if(变量==2)
{
    语句2;
}
else   //相当于switch中的default
{
    语句3;
}


         50.5      switch语句中内嵌switch
         if语句也可以内嵌if语句,while语句也可以内嵌while语句,switch语句当然也可以内嵌switch。比如

switch(a)
{
   case 1:
        switch(b)
        {
             case 1:
                  Break;
             case 2:
                  Break;
        }
        Break;
   case 2:
        Break;
}

       这种switch内嵌switch语句也是合法的,而且在实际项目中也很常用,大家目前先有个大概的了解即可,暂时不深入讲解。

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


  1. void main() //主程序
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/



  4.   unsigned char k;   //此变量为switch的入口判断变量

  5.   unsigned char a; //观察此变量的变化来理解switch的执行顺序

  6.   a=0;
  7.   k=2;

  8.   switch(k)
  9.   {
  10.      case 0:  //入口0
  11.        a++;
  12.        break; //跳出switch
  13.      case 1:  //入口1
  14.        a++;
  15.      case 2:  //入口2,上述k等于2所以从这里进来
  16.        a++;
  17.      case 3:  //入口3
  18.        a++;
  19.      case 4:  //入口4
  20.        a++;
  21.        break;  //跳出switch
  22.      case 5:  //入口5
  23.        a++;
  24.        break;  //跳出switch
  25.      default: //入口default,相当于else。当前面没有遇到对应的case入口时,就默认进入此default入口
  26.        a++;
  27.        break;  //跳出switch
  28.   }            //最后一个switch的花括号也是跳出switch
  29.       
  30.   GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示

  31.   
  32.         
  33. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  34.    while(1)  
  35.    {
  36.       initial();
  37.       key_service();
  38.       display_service();
  39.    }

  40. }
复制代码


        查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0。上坚鸿51学习板观察程序执行的结果如下:
        变量a为3。
        下节预告:空返回空形参的函数。
(未完待续)

乐于分享,勇于质疑!
87#
 楼主| 发表于 2015-12-20 01:20:41 | 只看该作者
本帖最后由 jianhong_wu 于 2015-12-20 01:25 编辑

第五十一节:函数的三要素和执行顺序。

        51.1   增加子函数内容后如何运用第10节模板程序调试
        前面章节讲的内容因为没有涉及到子函数,范例程序的编辑调试只需要在一个main主函数里操作就可以,原来的调试方式如下:
  1. void main() //主函数
  2. {
  3. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  4. unsigned char  a;  //普通局部变量的定义     
  5.   unsigned char  b;  //普通局部变量的定义   
  6.   ……     //一些程序代码
  7.   ……
  8.   ……
  9. GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  10. GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示
  11.   ……      
  12. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  13.   while(1)  
  14.   {
  15.      initial();
  16.      key_service();
  17.      display_service();
  18.   }
  19. }
复制代码

         本节开始增加子函数内容,所以以后的调试方式如下:

  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. void hanshu(void);  //子函数的声明
  3. unsigned char  x;  //全局变量的定义     
  4. unsigned char  y;  //全局变量的定义   
  5. void hanshu(void)  //子函数的定义
  6. {
  7.    unsigned char  c;  //普通局部变量的定义子函数的语句;
  8. ……
  9. }
  10. void main() //主函数
  11. {
  12. unsigned char  a;  //普通局部变量的定义     
  13.   unsigned char  b;  //普通局部变量的定义   
  14. ……     //其它一些程序代码
  15. hanshu() ; //子函数的调用
  16.   ……     //其它一些程序代码
  17. GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示
  18. GuiWdData1=b;   //把b这个变量放到窗口变量1里面显示
  19.   ……      
  20. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  21.   while(1)  
  22.   {
  23.      initial();
  24.      key_service();
  25.      display_service();
  26.   }
  27. }
复制代码

        对比原来,现在的“C语言学习区域的开始”分割线往上移动增大了范围,其它部分内容不用变,还是直接在第10节的模板程序里更改和添加这部分的内容就可以了。


        51.2   函数的三要素。
        有的人习惯把函数称为程序,比如主程序,子程序,这时的主程序对应主函数,子程序对应子函数,是一回事,只是每个人的表达习惯不一样而已。函数的三要素是声明,定义,调用。新构造一个函数时,尽量遵守这个三个要素来做就可以减少一些差错。什么叫函数的声明,定义,调用?先让大家有一个感性的认识,请先看下面这个例程:

  1.   /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. void hanshu(void);   //子函数的声明
  3. void hanshu(void)    //子函数的定义
  4. {
  5. ……     //子函数的代码语句
  6. }
  7. void main() //主函数
  8. {
  9. hanshu() ;      //子函数的调用
  10. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  11.   while(1)  
  12.   {
  13.       ……
  14.   }
  15. }
复制代码



         51.3   子函数在主函数里的执行顺序
        子函数被其它函数调用时,子函数的名字就相当于一个跳转地址,而子函数的定义就是要跳转的实际地址,单片机在主函数里遇到子函数名称,就直接跳转到子函数定义那里执行子函数定义部分的代码,执行完子函数后再返回到主函数,继续执行子函数名称后面部分的代码,请看下面这个代码的执行顺序,就可以一目了然:

  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. void hanshu(void);   //子函数的声明
  3. void hanshu(void)    //子函数的定义
  4. {
  5. 语句1;
  6. 语句2;
  7. }
  8. void main() //主函数
  9. {
  10. 语句3;
  11. hanshu() ;      //子函数的调用
  12. 语句4;
  13. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  14.   while(1)  
  15.   {
  16.       ……
  17.   }
  18. }
复制代码

        执行顺序分析:
        单片机从主函数main那里进来往下执行,先执行“语句3”,接着遇到hanshu名称的跳转地址,然后马上跳转到hanshu的定义部分,执行“语句1”,“语句2”,执行完子函数hanshu的定义部分,就马上返回到主函数,继续执行hanshu名称后面的“语句4”。整个执行语句的先后顺序如下:
        语句3;
        语句1;
        语句2;
        语句4;

        下节预告:普通局部变量和全局变量在函数里的作用域和优先级。
(未完待续)

乐于分享,勇于质疑!
88#
 楼主| 发表于 2015-12-27 02:04:03 | 只看该作者
本帖最后由 jianhong_wu 于 2015-12-27 02:07 编辑

第五十二节:从局部变量和全局变量中感悟“栈”为何物。


        52.1  什么是局部变量,什么是全局变量。
        局部变量就是在函数内部定义的变量,全局变量就是在函数外面定义的变量。下面举的例子能很清晰的说明局部变量和全局变量的特点:

  1. unsigned char a;     //在函数外面定义的,所以是全局变量。
  2.      void main()
  3. {
  4.           unsigned char b; //在函数内部定义的,所以是局部变量。
  5. }
复制代码


        52.2  局部变量和全局变量的内存模型。
        我在第三节里讲到,单片机内存包括ROM和RAM两部分,ROM存储的是单片机程序中的指令和一些不可更改的常量数据,而RAM存放的是可以被更改的数据,这些可以被程序更改的数据就叫做变量。变量包括局部变量和全局变量,但是局部变量和全局变量在RAM中的数据结构有本质不同。欲知不同在哪,首先要从RAM说起。对于大部分的单片机,RAM都至少分成两部分,一部分是给全局变量,一部分是给局部变量。给全局变量那部分的RAM叫全局数据区,给局部变量那部分的RAM叫做“栈”,因为我后面会用宾馆来比喻“栈”,所以为了方便记忆,可以把“栈”想象成 “客栈”来记忆。 全局数据区就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住,而且是永久的,所以说每个全局变量都有唯一对应的RAM地址,不可能重复的。而“栈”就像宾馆客栈,每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址每天可能住进不同的人,不是唯一的。全局区的全局变量拥有永久产权,“栈”区的局部变量只能临时居住在宾馆客栈,地址不是唯一的,有期限的。全局变量像私人区,局部变量像公共区。


         52.3  借用宾馆客栈比喻局部变量的期限。
  1. void hanshu(void);   //子函数的声明
  2. void hanshu(void)    //子函数的定义
  3. {
  4.      unsigned char a;   //局部变量
  5.      a=1;
  6. }
  7. void main() //主函数
  8. {
  9. hanshu() ;      //子函数的调用
  10. }
复制代码

          请看上面的程序例子,单片机从主函数main往下执行,首先遇到hanshu子函数的调用,所以就跳到hanshu函数的定义那里开始执行,此时的局部变量a开始被分配在RAM的栈区的某个地址,相当于你入住宾馆被分配到某个房间。单片机执行完子函数hanshu后,局部变量a在RAM的栈区所分配的地址被收回,局部变量a消失,被收回的RAM地址可能会被系统重新分配给其它被调用的函数的局部变量,此时相当于你离开宾馆,原来在宾馆居住那个房间被收回,你离开宾馆,从此你跟那个宾馆的房间没有啥关系,它可能会被宾馆老板重新分配给其他的客人入住。结论:局部变量的作用域就是它所在函数的内部范围,而全局变量的作用域是永久性不受范围限制的。


          52.4  局部变量和全局变量的优先级。
          上面最后一段话中得到一个结论:局部变量的作用域就是它所在函数的内部范围,而全局变量的作用域是永久性并且不受范围限制的。那么问题来,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?这个问题就涉及到优先级。注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。

           52.4.1   请看第一个例子:
  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. unsigned char a=5;    //全局变量的定义
  3. void main() //主程序
  4. {
  5.     unsigned char a=2;  //局部变量的定义
  6.     GuiWdData0=a;   //把a这个数值放到窗口变量0里面显示,此时的a是局部变量2,而不是5

  7. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  8.    while(1)
  9.    {
  10.       initial();
  11.       key_service();
  12.       display_service();
  13.    }

  14. }
复制代码

           此时输出显示2。


           52.4.2   请看第二个例子:
  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. void hanshu(void); //函数声明
  3. unsigned char a=5;    //全局变量的定义
  4. void hanshu(void)   //函数定义
  5. {
  6.     unsigned char a=3;  //局部变量的定义
  7. }
  8. void main() //主程序
  9. {
  10.     unsigned char a=2;   //局部变量的定义
  11.     hanshu();  //子函数被调用
  12.     GuiWdData0=a;   //把a这个数值放到窗口变量0里面显示,此时的a是局部变量2
  13. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  14.    while(1)
  15.    {
  16.       initial();
  17.       key_service();
  18.       display_service();
  19.    }

  20. }
复制代码

           此时输出显示2。
           分析:虽然子函数hanshu内部也有一个a=3,但是当hanshu被调用完结束后,这个a就消失不起作用了,所以此时显示的是main函数内部的局部变量。


           52.4.3   请看第三个例子:
  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/

  2. void hanshu(void); //函数声明

  3. unsigned char a=5;    //全局变量的定义

  4. void hanshu(void)   //函数定义
  5. {
  6.     unsigned char a=3;  //局部变量的定义
  7. }


  8. void main() //主程序
  9. {

  10.     hanshu();  //子函数被调用

  11.     GuiWdData0=a;   //把a这个数值放到窗口变量0里面显示,此时的a是全部变量5

  12.       
  13. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  14.    while(1)
  15.    {
  16.       initial();
  17.       key_service();
  18.       display_service();
  19.    }

  20. }
复制代码

          此时输出显示5。
          分析:子函数hanshu 里面的a的作用域只是它本身,因此子函数hanshu里面的a没办法影响到其它函数的a。而main函数内部没有了局部变量,那么此时起作用的只能是全局变量,因此显示5。假如此时连全局变量也没有了怎么办?这种情况是语法错误不能编译通过,因为任何变量的调用都必须要被先定义,定义有两种,要么是在函数内被定义成局部变量,要么是在函数外被定义成全局变量。


          52.5   局部变量和全局变量的总结归纳.
         (1)每定义一个新的全局变量,就意味着多开销一个新的RAM内存。而每定义一个局部变量,只要在函数内部所定义的局部变量总数不超过单片机的“栈”区,此时的局部变量不开销新的RAM内存,因为局部变量是临时借用“栈”区的,使用后就还给“栈”,“栈”是公共区,可以重复利用,可以服务若干个不同的函数内部局部变量。
         (2)单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化,全局变量是一直保存之前最后一次更改的值。


下节预告:空返回空形参的函数。
(未完待续)

乐于分享,勇于质疑!
89#
 楼主| 发表于 2016-1-4 05:05:31 | 只看该作者
本帖最后由 jianhong_wu 于 2016-1-10 01:36 编辑

第五十三节:函数的作用和四种常见书写类型。
第五十三节_pdf文件.pdf (109.18 KB, 下载次数: 2464)

【53.1   函数的作用和分类。】

        函数的作用。通常把一些可能反复用到的算法或者过程封装成一个函数,函数就是一个模块,给它输入特定的参数,就可以输出想要的结果,比如一个加法函数,只要输入加数和被加数,然后就会输出相加结果之和,里面具体的算法过程只要写一次就可以重复调用,极大的节省单片机程序容量,也节省程序开发人员的工作量。
        函数的分类。从输入输出的角度来看,有四种常见的书写类型。分别是无输出无输入,无输出有输入,有输出无输入,有输出有输入。输出是看函数名的前缀,前缀如果是void表示无输出,否则就是有输出。输入是看函数名括号里的内容,如果是void或者是空着就表示无输入,否则就是有输入。输出和输入是比较通俗的说法,专业一点的说法是,有输出表示函数有返回,无输出表示函数无返回。有输入表示有形参,无输入表示无形参。下面举一个加法函数的例子,分别用四种不同的函数类型来实现。大家对比一下它们在书写方面有哪些不同,又有哪些规律。

【53.2   无输出无输入的函数。】

  1. unsigned char a;  //此变量用来接收最后相加结果的和。
  2. unsigned char g=2;
  3. unsigned char h=3;
  4. void hanshu(void)  //无输出无输入函数的定义。
  5. {
  6.    a=g+h;
  7. }
  8. main()
  9. {
  10.     hanshu();  //函数调用时的样子。
  11. }
复制代码
分析:
         void hanshu(void),此函数名的前缀是void,括号内也是void,属于无输出无输入函数。这类函数表面看是无输出无输入,其实在实际应用中也可以通过全局变量来输入输出,比如上面的例子就是靠a,g,h这三个全局变量来传递信息,只不过表达方式上像隐藏起来一样没有那么直观。

53.3   无输出有输入的函数。】

  1. unsigned char b;  //此变量用来接收最后相加结果的和。
  2. void hanshu(unsigned char i,unsigned char k)   //无输出有输入函数的定义
  3. {
  4.    b=i+k;
  5. }
  6. main()
  7. {
  8.     hanshu(2,3);  //函数调用时的样子。
  9. }
复制代码
分析:
         void hanshu(unsigned char i,unsigned char k),此函数名的前缀是void,括号内是(unsigned char i,unsigned char k),属于无输出有输入的函数。括号的两个变量ik是函数内的局部变量,也是跟对外的桥梁接口,它们有一个专业的名称叫形参。外部要调用此函数时,只要给括号填入对应的变量或者数值,这些变量和数值就会被复制一份传递给作为函数形参的局部变量,从而外部调用者跟函数内部就发生了数据信息的传递。这种书写方式的特点是把输入接口封装了出来。

53.4   有输出无输入的函数。】

  1. unsigned char c;   //此变量用来接收最后相加结果的和。
  2. unsigned char m=2;
  3. unsigned char n=3;
  4. unsigned char hanshu(void)     //有输出无输入函数的定义。
  5. {
  6. unsigned char p;
  7. p=m+n;
  8. return p;
  9. }
  10. main()
  11. {
  12.     c= hanshu();  //函数调用时的样子。
  13. }
复制代码
分析:
         unsigned char hanshu(void),此函数名的前缀是unsigned char类型,括号内是void,属于有输出无输入的函数。函数前缀的unsigned char表示此函数最后退出时会返回一个unsigned char类型的数据给外部调用者。而且这类函数内部必须有一个return语句配套,表示立即退出当前函数并且返回某个变量或者常量的数值给外部调用者。这种书写方式的特点是把输出接口封装了出来。

53.5   有输出有输入的函数。】

  1. unsigned char d;    //此变量用来接收最后相加结果的和。
  2. unsigned char hanshu(unsigned char r,unsigned char s)     //有输出无输入函数的定义
  3. {
  4. unsigned char t;
  5. t=r+s;
  6. return t;
  7. }
  8. main()
  9. {
  10.     d= hanshu(2,3);  //函数调用时的样子。
  11. }
复制代码
分析:
        unsigned char hanshu(unsigned char r,unsigned char s),此函数名的前缀是unsigned char类型,括号内是(unsigned char r,unsigned char s),属于有输出有输入的函数。输入输出的特点跟前面介绍的函数一样,不多讲。这种书写方式的特点是把输出和输入接口都封装了出来。

53.6   调用函数时特别要注意。】

        注意:在函数调用时,凡是“void”关键词都要省略,否则编译不通过。

53.7   练习程序。】

       现在把上述的4加法函数写成一个程序,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,添加和修改main程序代码如下:
  1. /*---C语言学习区域的开始---------------------------------------------------------------------------*/
  2. //第1种:无输出无输入函数的声明。
  3. void hanshu_1(void);  
  4. //第2种:无输出有输入函数的声明。
  5. void hanshu_2(unsigned char i,unsigned char k);   
  6. //第3种:有输出无输入函数的声明。
  7. unsigned char hanshu_3(void);     
  8. //第4种:有输出无输入函数的声明。
  9. unsigned char hanshu_4(unsigned char r,unsigned char s);  
  10. //第1种:无输出无输入
  11. unsigned char a;  //此变量用来接收最后相加结果的和。
  12. unsigned char g=2;
  13. unsigned char h=3;
  14. //第2种:无输出有输入
  15. unsigned char b;  //此变量用来接收最后相加结果的和。
  16. //第3种:有输出无输入
  17. unsigned char c;   //此变量用来接收最后相加结果的和。
  18. unsigned char m=2;
  19. unsigned char n=3;
  20. //第4种:有输出有输入
  21. unsigned char d;    //此变量用来接收最后相加结果的和。
  22. //第1种:无输出无输入函数的定义。
  23. void hanshu_1(void)  
  24. {
  25.    a=g+h;
  26. }
  27. //第2种:无输出有输入函数的定义
  28. void hanshu_2(unsigned char i,unsigned char k)   
  29. {
  30.    b=i+k;
  31. }
  32. //第3种:有输出无输入函数的定义。
  33. unsigned char hanshu_3(void)   
  34. {
  35. unsigned char p;
  36. p=m+n;
  37. return p;
  38. }
  39. //第4种:有输出无输入函数的定义
  40. unsigned char hanshu_4(unsigned char r,unsigned char s)  
  41. {
  42. unsigned char t;
  43. t=r+s;
  44. return t;
  45. }
  46. void main() //主程序
  47. {
  48.     hanshu_1();       //第1种:无输出无输入函数。
  49.     hanshu_2(2,3);      //第2种:无输出有输入函数。
  50.     c=hanshu_3();      //第3种:有输出无输入函数。
  51.     d=hanshu_4(2,3);     //第4种:有输出无输入函数。
  52.     GuiWdData0=a;   //把a这个数值放到窗口变量0里面显示。
  53.     GuiWdData1=b;   //把b这个数值放到窗口变量1里面显示。
  54.     GuiWdData2=c;   //把c这个数值放到窗口变量2里面显示。
  55.     GuiWdData3=d;   //把d这个数值放到窗口变量3里面显示。
  56.         
  57. /*---C语言学习区域的结束---------------------------------------------------------------------------*/
  58.    while(1)  
  59.    {
  60.       initial();
  61.       key_service();
  62.       display_service();
  63.    }
  64. }
  65.       
复制代码
        查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0上坚鸿51学习板观察程序执行的结果如下:
       变量a为5。
       变量b为5。
       变量c为5。
       变量d为5。

       下节预告:return语句在函数中的作用
(未完待续)



乐于分享,勇于质疑!
90#
 楼主| 发表于 2016-1-10 01:19:32 | 只看该作者
本帖最后由 jianhong_wu 于 2016-1-10 01:35 编辑

第五十四节:return语句在函数中的作用以及容易被忽略的四个功能
第五十四节_pdf文件.pdf (117.79 KB, 下载次数: 2383)

【54.1   return深入讲解

       “return”在英语单词中有“返回”的意思,在上一节提到,凡是“有输出函数”,函数内部必须有一个“return+变量或者常量”与之配套,表示返回的结果给外部调用者接收,这个初学者都很容易理解,容易被忽略的是return另外四个功能:
       第一个是return语句隐含了立即退出的功能。退出哪?退出当前函数。只要执行到return语句,就马上退出当前函数。即使return语句身陷多层while或者for的循环中,它也毫不犹豫立即退出当前函数。
       第二个是return语句可以出现在函数内的任何位置。可以出现在第一行代码,也可以出现在中间的某行代码,也可以出现在最后一行的代码,它的位置不受限制。很多初学者有个错觉,以为return只能出现在最后一行,这是错的。
       第三个是return语句不仅仅可以用在“有输出函数”,也可以用在“无输出函数”,也就是可以用在前缀是void的函数里。回顾上一节,在“有输出函数”函数里,return后面紧跟一个变量或者常量,表示返回的数,但是在“无输出函数”里,因为是“无输出”,此时return后面不用跟任何变量或者常量,表示返回的是空的。此时return主要起到立即退出当前函数的作用。
       第四个是return语句可以在一个函数里出现N次,次数不受限制,不一定必须只能一次。不管一个函数内有多少个return语句,只要任何一个return语句被单片机执行到,立即退出当前函数。

【54.2   中途立即退出的功能。】

       下面的书写格式是合法的:
  1. void hanshu(void)  //无输出/函数的定义。
  2. {
  3. 语句1;
  4. return; //立即退出当前函数。
  5. 语句2;
  6. return; //立即退出当前函数。
  7. 语句3;
  8. return; //立即退出当前函数。
  9. }
复制代码
分析:
        当hanshu此函数被调用时,单片机从“语句1”往下执行,当遇到第一个return语句后,马上退出当前函数。在此函数里,后面的“语句2”等代码永远不会被执行到。

【54.3   身陷多层while或者for的循环时的惊人表现。】

       下面的书写格式是合法的:
  1. void hanshu(void)  //无输出/函数的定义。
  2. {
  3. 语句1;
  4. while(1)  //第一个循环
  5. {
  6.     while(1)  //第二个循环中的循环
  7.     {
  8.           return; //立即退出当前函数。
  9.     }
  10.     语句2;
  11.     return; //立即退出当前函数。
  12. }
  13. 语句3;
  14.     return; //立即退出当前函数。
  15. }
复制代码
分析:
        当hanshu此函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个return语句,马上退出当前函数。在此函数里,后面的“语句2”等代码也是永远不会被执行到。虽然表面看起来有那么多可怕的循环约束着,但是一旦碰上return语句都是浮云,立刻退出当前函数。

【54.4   “有输出函数”时是什么样子的?】

        把上面例子中“无输出函数”改成“有输出函数”后:
  1. unsigned char hanshu(void)  //有输出/函数的定义。
  2. {
  3. unsigned char a=9;
  4. 语句1;
  5. while(1)  //第一个循环
  6. {
  7.     while(1)  //第二个循环中的循环
  8.     {
  9.           return a; //返回a变量的值,并且立即退出当前函数。
  10.     }
  11.     语句2;
  12.     return a; //返回a变量的值,并且立即退出当前函数。
  13. }
  14. 语句3;
  15. return a; //返回a变量的值,并且立即退出当前函数。
  16. }
复制代码
分析:
       因为此函数是“有输出/函数”,所以return语句后面必须配套一个变量或者常量,而执行顺序跟前面54.3的例子是一样的。当hanshu函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个“return a”语句,马上退出当前函数。在此函数里,后面的“语句2”等代码也是永远不会被执行到的。再一次说明了,return语句不仅有返回某数的功能,还有立即退出的重要功能。

【54.5   项目应用时,中间的return语句往往是跟if语句搭配使用的。】

       前面的例子只是为了解释return语句的执行顺序和功能,实际项目中,如果中间有多个return语句,中间的return语句不可能像前面的例子那样单独使用,它往往是跟if语句一起搭配使用的,否则单独用return就没有什么意义。比如:
  1. void hanshu(void)  //无输出/函数的定义。
  2. {
  3. 语句1;
  4. if(某条件满足)
  5. {
  6.    return; //立即退出当前函数。
  7. }
  8. 语句2;
  9. if(某条件满足)
  10. {
  11.    return; //立即退出当前函数。
  12. }
  13. 语句3;
  14. }
复制代码
分析:
        单片机从“语句1”开始往下执行,至于在哪个“return”语句处退出当前函数,就要看哪个if的条件满不满足了,如果所有的if的条件都不满足,此函数会一直执行完最后的“语句3”才退出当前函数。

54.6   函数和变量的命名规则。】

       函数的名字和变量的名字一样,第一个字符不能是数字,必须是字母或者下划线“_”,后面紧跟的第2个字符开始可以是数字。在C语言中名字是区分大小写的。可以用下划线“_”,但是不可以用横杠“-”。名字不能跟C编译系统已经征用的关键字重名,比如不能用“unsigned ,char,static”等系统关键词,跟古代时不能跟皇帝重名一样,要避尊者讳。

54.7   练习程序。】

        写一个简单的除法函数,在除法运算中,除数不能为0,如果发现除数为0,就立即退出当前函数,并且返回运算结果默认为0最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,添加和修改main程序代码如下:
  1. /*---C语言学习区域的开始---------------------------------------------*/
  2. //函数的声明。
  3. unsigned int chu_fa(unsigned int bei_chu_shu,unsigned int chu_shu);
  4. unsigned int a;//此变量用来接收除法的运算结果。
  5. unsigned int b;//此变量用来接收除法的运算结果。
  6. //函数的定义。
  7. unsigned int chu_fa(unsigned int bei_chu_shu,unsigned int chu_shu)
  8. {
  9.     unsigned int shang;  //返回的除法运算结果:商。
  10.     if(0==chu_shu)   //如果除数等于0,就立即退出当前函数,并返回0
  11.     {
  12.         return 0; // 退出当前函数并且返回0.此时后面的代码不会被执行。
  13.     }
  14.    
  15.     shang=bei_chu_shu/chu_shu;  //除法运算的算法
  16.     return shang;  //返回最后的运算结果:商。并且退出当前函数。
  17. }
  18. void main() //主程序
  19. {
  20.    
  21.     a=chu_fa(128,0);  //函数调用。128除以0,把商返回给a变量。
  22.     b=chu_fa(128,2);  //函数调用。128除以2,把商返回给b变量。
  23.     GuiWdData0=a;   //把a这个数值放到窗口变量0里面显示。
  24.     GuiWdData1=b;   //把b这个数值放到窗口变量1里面显示。
  25.         
  26. /*---C语言学习区域的结束---------------------------------------------*/
  27.    while(1)  
  28.    {
  29.       initial();
  30.       key_service();
  31.       display_service();
  32.    }
  33. }
复制代码
        查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0上坚鸿51学习板观察程序执行的结果如下:
       变量a为0。
       变量b为64。

       下节预告:static静态局部变量在函数中的重要作用。
(未完待续)


乐于分享,勇于质疑!
91#
 楼主| 发表于 2016-1-17 01:58:36 | 只看该作者
本帖最后由 jianhong_wu 于 2016-1-17 11:40 编辑

第五十五节:static静态局部变量在函数中的重要作用。
第五十五节_pdf文件.pdf (144.04 KB, 下载次数: 2405)

【55.1   变量前加入static关键词后发生“化学反应”】


          前面介绍了两类变量,一类全局变量,一类局部变量。定义时,在任何一类变量前面加入static关键词,变量原有的特性都会发生某些变化,此时static像化学里的催化剂,具有神奇的功能。加static关键词的书写格式如下:
  1.      static unsigned char a;  //全局变量前加的static关键词
  2.      void hanshu(void)
  3.      {
  4.          static unsigned char i;   //局部变量前加的static关键词
  5.      }
复制代码


【55.2   在全局变量前加static】


          static读作“静态”,全局变量前加static,称为静态全局变量。静态全局变量和普通全局变量的功能大体相同,仅在有效范围(作用域)方面有差异。假设整个工程有多个文件组成,普通全局变量的有效范围能覆盖全部文件,在任何一个文件里都畅通无阻。而静态全局变量只能在当前定义的那个文件里起作用,活动范围完全被限定在一个文件,彷佛被加了紧箍咒,由不得你任性。这部分的内容有个大致印象就可以,暂时不用深入研究,等以后学到多文件编程时再关注,因为我当前的程序例子只有一个源文件,还没涉及多文件编程。

【55.3   在局部变量前加static】

         这是本节重点。我常把局部变量比喻宾馆的客房,客人入住时被分配在哪间客房是随机临时安排的,第二天退房时宾馆会把客房收回继续分配给下一位其他的客人,是临时公共区。而加入static后的局部变量,发生了哪些变化?加入static后的局部变量,称为静态局部变量。静态局部变量就像宾馆的VIP客户,VIP客户财大气粗,把宾馆分配的客房永远包了下来,永远不许再给其它客人入住。总结了静态局部变量的两个重要特性:
        第一个,静态局部变量不会在函数调用时被初始化,它只在单片机刚上电时被编译器初始化了一次,因为它的内存模型不是被分配在“栈”,而是在全局变量同一类数据区,拥有自己唯一的地址。但是跟全局变量又有差别,全局变量的有效范围(作用域)是整个工程,而静态局部变量毕竟是“局部”,仅局限于当前函数。而普通局部变量,众所周知,每次被函数调用时,都会被重新初始化。
        第二个,每次函数调用时,静态局部变量比普通局部变量少开销一条潜在的“初始化语句”,原因是普通局部变量每次被函数调用时都要重新初始化,而静态局部变量不用进行这个操作。也就是说,静态局部变量比普通局部变量的效率高一点,虽然这个“点”的时间开销微不足道,但是不留意这“点”,写程序时容易出现瑕疵。


【55.4   静态局部变量的应用场合】


        静态局部变量适用在那些频繁调用的函数,比如main函数主循环while(1)里直接调用的所有函数,还有以后讲到的定时器中断函数,等等。因为静态局部变量每次被调用都不会被重新初始化,用在这类函数时就省去了每次初始化语句的时间。还有就是那些规定不能被函数初始化的场合,比如在很多用switch搭建的程序函数里,这类switch程序俗称为状态机思路。


【55.5   能用全局变量替代静态局部变量吗】


         能用全局变量替代静态局部变量吗?能。哪怕在整个程序里全部用全局变量都可以。全局变量是一把牛刀,什么场合都用牛刀虽然也能解决问题,但是显得鲁莽没有条理。尽量把全局变量,普通局部变量,静态局部变量各自优势充分发挥出来才是编程之道。能用局部变量的尽量用局部变量,这样可以减少全局变量的使用。当局部变量帮分担一部分工作时,最后全局变量只起到一个作用,那就是在各函数之间传递信息。局部变量与全局变量的分工定位明确了,程序代码阅读起来就没有那么凌乱,思路也清晰很多。


【55.6   程序分析】

         编写一个程序来学习刚才讲到的内容,最后把程序编译后下载到坚鸿51学习板观察结果。请直接复制第十节模板程序,添加和修改main程序代码如下:
  1. /*---C语言学习区域的开始-----------------------------------------*/
  2. unsigned char hanshu(void);         //函数声明
  3. unsigned char hanshu_static(void);  //函数声明
  4. unsigned char hanshu(void)  //函数定义
  5. {
  6.    unsigned char i=0;   //普通局部变量,每次函数调用都被初始化为0.
  7.    i++;  //i自加1
  8.    return i;
  9. }
  10. unsigned char hanshu_static(void)  //函数定义
  11. {
  12.    static unsigned char i=0;   //静态局部变量,只在上电是此初始化语句才起作用。
  13.    i++;  //i自加1
  14.    return i;
  15. }
  16. void main() //主程序
  17. {
  18.   unsigned char a; //用来接收函数返回的结果。
  19.   unsigned char b;
  20.   unsigned char c;
  21.   unsigned char d; //用来接收函数返回的结果。
  22.   unsigned char e;
  23.   unsigned char f;
  24.   //下面函数内的i是普通局部变量。
  25.   a=hanshu();  //函数内的i每次重新初始化为0,再自加1,所以a等于1。
  26.   b=hanshu();  //函数内的i每次重新初始化为0,再自加1,所以b等于1。
  27.   c=hanshu();  //函数内的i每次重新初始化为0,再自加1,所以c等于1。
  28.   //下面函数内的i是静态局部变量,第一次上电后默认为0,就不会再被初始化,
  29.   d=hanshu_static(); //d由0自加1后等于1。
  30.   e=hanshu_static(); //e由1自加1后等于2。
  31.   f=hanshu_static(); //f由2自加1后等于3。
  32.      GuiWdData0=a;   //把a这个变量放到窗口变量0里面显示,下同。
  33.   GuiWdData1=b;  
  34.   GuiWdData2=c;  
  35.   GuiWdData3=d;
  36.   GuiWdData4=e;  
  37.   GuiWdData5=f;  
  38.         
  39. /*---C语言学习区域的结束----------------------------------------*/
  40.    while(1)  
  41.    {
  42.       initial();
  43.       key_service();
  44.       display_service();
  45.    }
  46. }
  47.       
复制代码


         查看运算结果的方法。如何在坚鸿51学习板上观察变量?按下S1或者S5按键即可切换显示不同的窗口,从而显示不同的变量。按下S9按键不松手就可以切换到十六进制的显示界面,松开手后会自动切换到十进制的界面。16个LED灯显示的就是当前变量的二进制数,亮代表1,灭代表0上坚鸿51学习板观察程序执行的结果如下:
         变量a为1。
         变量b为1。
         变量c为1。

         变量d为1。
         变量e为2。
         变量f 为3。

【55.7   为什么中止此连载帖的更新了】

    我以前一直想写两本书,一本讲单片机入门基础,一本讲单片机程序框架。现在发现,单片机基础和程序框架并没有明显的分水岭,基础中有框架,框架中有基础,应该合二为一,读起来才会连贯舒畅。所以我决定中止当前已写到55节的《从业十年,教你单片机入门基础》连载帖,另外新开一个连载帖叫《从单片机基础到程序框架(连载)》,希望大家关注我的新连载帖。
    再提一下我2014年写的《从业将近十年,手把手教你单片机程序框架》,一方面受到很多网友的好评,另一方面也有一些热心网友提出了宝贵的意见,我今天看来,确实还有一些可待改进的地方。本来计划在2017年重写《……单片机程序框架》那个老帖,现在看来不用那么折腾了,只要把《……单片机程序框架》的内容也整合到新开的帖子里就可以了,这样对我也比较省事。我的时间计划是,先花4年时间写一
个初稿,然后再花2年时间重写一次,最后再花1年时间整理成书,整个过程大概7年时间左右,今年是2016年,估计到2023年左右就可以新书出版了。
    感谢各位朋友的支持。






乐于分享,勇于质疑!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|独闷闷网 ( 粤ICP备12007667号-2 )

GMT+8, 2024-11-29 16:34 , Processed in 0.335560 second(s), 15 queries .

快速回复 返回顶部 返回列表