打印
[51单片机]

从单片机基础到程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
141
jianhong_wu|  楼主 | 2016-6-27 11:11 | 只看该作者 |只看大图 回帖奖励 |倒序浏览
第二十四节:借用unsigned long类型的中间变量可以减少溢出现象。
第二十四节_pdf文件.pdf (62.79 KB)
【24.1   为什么要借用unsigned long类型的中间变量?】

       为什么要借用unsigned long类型的中间变量进行算术运算?其实就是为了减少溢出的问题。溢出是因为数据超过了它的最大范围,unsigned char ,unsigned int ,unsigned long三种数据类型中,unsigned long的取值是最大的。当参与运算变量中存在非unsigned long类型的时候,在运算前,先让每个非unsigned long类型的变量借用一个unsigned long类型的中间变量,然后才开始运算,可以大大减少运算中的溢出问题。 unsigned long的取值是从0到4294967295,万一数据超过了4294967295怎么办?可用BCD码的数组方式进行运算,这种数组运算的方法我以后会跟大家介绍,初学者现在暂时不用深入了解它。

【24.2   如何借用unsigned long类型的中间变量?】

        借用中间变量的方法是引入中间变量,有多少个非unsigned long类型变量就引入多少个unsigned long中间变量,再借这个“壳”进行运算,最后再把中间变量的计算结果返回给实际变量。请看下面例子。

        转换之前:
unsigned int  a;
unsigned char x=195;
unsigned long y=101;
a=x-y;      //进行算术减法运算

       分析:
      上述公式用到3个变量,其中a和x都不是unsigned long变量,因此需要为它们分别引入两个unsigned long类型的中间变量t和s,于是乎,继续往下看......

       转换之后:
unsigned int  a;
unsigned char x=195;
unsigned long y=101;

unsigned long t; //引入的中间变量t,用来给a借用。
unsigned long s; //引入的中间变量s,用来给x借用。

//第一步:使用之前先清零
t=0;             //t在用之前,先把t的32位全部清零。
s=0;             //s在用之前,先把s的32位全部清零。

s=x;             //s接收x原数据,等效于x借用unsigned long中间变量s这个壳。
t=s-y;           //此处unsigned long类型的t就默认代表了unsigned int类型的变量a。

//第二步:因为其它的变量都是临时的,所以运算结束后再返回计算结果给原来的变量。
a=t;             //运算结束后再把计算结果返回给原来的变量a。

      分析:
      第一步:unsigned long类型的中间变量在转换之前为什么要先赋值0进行清零,比如上述代码的“s=0;”?因为它是32位的数据类型,它也是一个随机数,如果不清零,后续的其它类型的变量可能是16位或者8位的类型变量,这些宽度不一的变量在给32位的变量赋值的时候,只能覆盖到32位变量的低16位或者低8位,无法等效于实际借用者变量的数值,所以有可能会出错。
      第二步:因为其它的变量都是临时的,所以运算结束后应该再返回计算结果给原来的实际变量。在这里要多说一句,实际项目中,最后接收运算结果的变量应该根据项目所需去选择它的类型,建议尽量选择unsigned long类型吧,否则,如果中间变量的计算结果大于接收变量本身的类型范围,也会发生溢出。比如,上述最后一行代码a=t,如果此时t的数值大于65535,a也会发生溢出的现象。 但是如果a本身是unsigned long 类型,就不会发生这种现象。
       加法,乘法,除法在借用中间变量的时候,跟本节减法例子中的思路也大同小异。

【24.3   建议在算术运算中确保所有的变量都是unsigned long类型。】

       不管是以前讲的加法,现在讲的减法,还是未来讲的乘法和除法,我都会建议“在加减乘除四则运算中,凡是非unsigned long类型的变量,都应该借用unsigned long类型的中间变量进行运算,最后再返回计算结果给实际的变量。”unsigned long变量是三种数据类型中取值范围最大的数,借用此类型的中间变量,可以减少在简单运算中可能出现的溢出问题。

使用特权

评论回复
142
kiki@cjy| | 2016-6-30 10:34 | 只看该作者
给楼主10个赞.这么独具匠心的好资料不出书太可惜了

使用特权

评论回复
143
jianhong_wu|  楼主 | 2016-7-4 09:14 | 只看该作者
第二十五节:乘法运算中的5种常用组合。
第二十五节_pdf文件.pdf (72.44 KB)
【25.1   乘法语法格式。】

      乘法语法格式:
      “保存变量”=“乘数1”*“乘数2”*..*“乘数N”;
      含义:为什么C语言的乘法符号并不是我们熟悉的“X”而是“*”?我猜测是因为“X”跟键盘的大写字母“X”重复有冲突了,而“*”轮廓跟“X”很相似,并且也可以在键盘上通过“Shift+8”的组合键直接键入“*”,所以用“*”作为乘法符号。上述乘法格式中,右边的“乘数”与“乘数”相乘(这里暂时把平时所说的被乘数也归类为乘数),并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。右边的“乘数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,什么是变量和常量?变量是可以在程序中被更改的,被分配的一个RAM空间。常量往往是数字,或者被分配在ROM空间的一个具体数值。下面根据右边“乘数”与“乘数”的不同组合,列出了乘法运算的5种常用组合。

       第1种:“乘数1”是常量,“乘数2”是常量。比如:
       unsigned char a;
       a=15*3;

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

       第2种:“乘数1”是变量,“乘数2”是常量。比如:
       unsigned char b;
       unsigned char x=15;
       b=x*10;

       分析:x是变量,“10”是常量。由于原来x变量里面的数值是15,执行上述语句后,保存变量b变成了150。而变量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变成了90。而变量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变成了36,接着再执行完“d=d*7;”语句后,d最后变成了252。

      第5种:“乘数1”是保存变量本身,“乘数2”是变量。比如:
      unsigned char e=2;
      unsigned char x=15;
      unsigned char y=6;
      e=e*x;
      e=e*y;

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

【25.2   例程练习和分析。】

      现在我们编写一个程序来验证上面讲到的5个乘法例子:
      程序代码如下:

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

void main() //主函数
{
    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=2;   //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为2.

    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;

     View(a);   //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);   //把第2个数b发送到电脑端的串口助手软件上观察。
     View(c);   //把第3个数c发送到电脑端的串口助手软件上观察。
     View(d);   //把第4个数d发送到电脑端的串口助手软件上观察。
     View(e);   //把第5个数e发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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


      在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:45
十六进制:2D
二进制:101101

第2个数
十进制:150
十六进制:96
二进制:10010110

第3个数
十进制:90
十六进制:5A
二进制:1011010

第4个数
十进制:252
十六进制:FC
二进制:11111100

第5个数
十进制:180
十六进制:B4
二进制:10110100

分析:        
         通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【25.3   如何在单片机上练习本章节C语言程序?】

          直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
144
乐逍遥6| | 2016-7-7 14:21 | 只看该作者
追帖中

使用特权

评论回复
145
jianhong_wu|  楼主 | 2016-7-12 10:14 | 只看该作者
第二十六节:连乘、自乘、自乘简写,溢出。
第二十六节_pdf文件.pdf (79.58 KB)
【26.1   连乘。】

      上一节的乘法例子中,右边的乘数只有两个。实际上,C语言规则没有限制乘数的个数,它的通用格式如下:
       “保存变量”=“乘数1”*“乘数2”...*“乘数N”;

      当右边的乘数个数超过两个的时候(这里暂时把平时所说的被乘数也归类为乘数),这种情况就是“连乘”。每个乘数的属性没有限定,可以是常量,也可以是变量。比如:
      unsigned char x=3;   //定义一个变量x,初始化默认为3.
      unsigned char y=6;   //定义一个变量y,初始化默认为6.      
      unsigned char k=2;   //定义一个变量k,初始化默认为2.
      a=2*5*3;  //乘数全部是常量。a的结果为30。
      b=k*x*y;  //乘全部是变量。b的结果为36。
      c=x*5*y;  //乘数,有的是常量,有的是变量。c的结果为90。

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

【26.2   自乘与自乘简写。】

      什么是自乘?当赋值符号“=”右边的乘数只要其中有一个是“保存变量”本身时,这种情况就是“自乘”,常见格式如下:
      “保存变量”=“保存变量”*“乘数1”;
      “保存变量”=“保存变量”*(“乘数1”*“乘数2”...*“乘数N”);

      上述自乘计算式可以简写成如下格式:
       “保存变量”*=“乘数1”;
       “保存变量”*=“乘数1”*“乘数2”...*“乘数N”;

       这种格式就是“自乘简写”。现在举几个例子如下:
       unsigned char d=5;       //定义一个变量d,初始化默认为5.
       unsigned char e=5;       //定义一个变量e,初始化默认为5.
       unsigned char f=5;       //定义一个变量f,初始化默认为5.

       unsigned char x=3;   //定义一个变量x,初始化默认为3.
       unsigned char y=6;   //定义一个变量y,初始化默认为6.      
       unsigned char k=2;   //定义一个变量k,初始化默认为2.
       d*=6;     //相当于d=d*6;最后d的结果为30。
       e*=x;     //相当于e=e*x;最后e的结果为15。
       f*=2*y*k; //相当于f=f*(2*y*k);最后f的结果为120。


【26.3   有没有“自乘1”的特殊写法?】

      之前在讲加法的自加和减法的自减运算时,还给大家介绍了它们另外一种特殊的简写方式。比如减法运算,当右边只有2减数,当一个减数是“保存变量”,另一个是常数1时,格式如下:
       “保存变量”=“保存变量”-1;

       这时候,可以把上述格式简写成如下两种格式:
       “保存变量”--;
      --“保存变量”;

      这两种格式也是俗称的“自减1”操作。比如:
       g--;  //相当于g=g-1或者g-=1;
       --h;  //相当于h=h-1或者h-=1;

      那么,本节所讲的自乘运算,有没有“g**”或者“**h”这种特殊的“自乘1”写法?答案很明显,C语言里没有“自乘1”这种特殊写法。因为任何一个数“自乘1”还是等于它本身,所以在乘法运算中这种特殊写法就没有存在的意义。多说一句,如果某天有朋友在某个地方看到“**h”这类语句,它的本意跟“自乘”没关系,而是跟C语言的另一块知识点“指针”有关。

【26.4   乘法的溢出。】

      乘法的溢出规律跟加减法的溢出规律是一样的。举一个例子如下:
      unsigned char m=30;
      unsigned char n=10;
      unsigned char a;
      a=m*n;  

      分析:m与n相乘,相当于30乘以10,运算结果是300(十六进制是0x012c)保存在一个隐藏中间变量,根据前面加减法运算的规律,我猜测这个隐藏中间变量可能是unsigned int类型,然后再把这个中间变量赋值给单字节变量a,a只能接收十六进制的低8位字节0x2c,所以运算后a的数值由于溢出变成了十六进制的0x2c(十进制是44)。由于乘法的溢出规律跟加减法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了减少溢出的现象,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。

【26.5   例程练习和分析。】

       现在编写一个程序来验证刚才讲到的连乘和自乘简写:
       程序代码如下:

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

void main() //主函数
{
    unsigned char a;      
    unsigned char b;      
unsigned char c;      
    unsigned char d=5;      //定义一个变量d,初始化默认为5.
    unsigned char e=5;      //定义一个变量e,初始化默认为5.
    unsigned char f=5;      //定义一个变量f,初始化默认为5.

    unsigned char x=3;      //定义一个变量x,初始化默认为3.
    unsigned char y=6;      //定义一个变量y,初始化默认为6.        
     unsigned char k=2;     //定义一个变量k,初始化默认为2.

     //第1个知识点:连乘。
     a=2*5*3;              //乘数全部是常量。a的结果为30。
     b=k*x*y;              //乘数全部是变量。b的结果为36。
     c=x*5*y;              //乘数,有的是常量,有的是变量。c的结果为90。

     //第2个知识点:自乘的简写。
     d*=6;                 //相当于d=d*6;最后d的结果为30。
     e*=x;                 //相当于e=e*x;最后e的结果为15。
     f*=2*y*k;             //相当于f=f*(2*y*k);最后f的结果为120。

     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。
     View(c);              //把第3个数c发送到电脑端的串口助手软件上观察。
     View(d);              //把第4个数d发送到电脑端的串口助手软件上观察。
     View(e);              //把第5个数e发送到电脑端的串口助手软件上观察。
     View(f);              //把第6个数f发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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


       在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:30
十六进制:1E
二进制:11110

第2个数
十进制:36
十六进制:24
二进制:100100

第3个数
十进制:90
十六进制:5A
二进制:1011010

第4个数
十进制:30
十六进制:1E
二进制:11110

第5个数
十进制:15
十六进制:F
二进制:1111

第6个数
十进制:120
十六进制:78
二进制:1111000


分析:        
       通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【26.6   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
146
jianhong_wu|  楼主 | 2016-7-17 14:09 | 只看该作者
第二十七节:整除求商。
第二十七节_pdf文件.pdf (83.7 KB)
【27.1   什么叫整除?】

       最小的细分单位是“1”的除法运算就是整除,“1”不能再往下细分成小数点的除法运算就是整除。比如:
       10除以4,商等于2.5。------(带小数点,这个不是整除)
       10除以4,商等于2,余数是2。------(这才是整除)
       什么时候带小数点,什么时候是整除?取决于参与运算的变量类型。标准的C语言中,其实远远不止我前面所说的unsigned char ,unsigned int ,unsigned long这三种类型,比如还有一种叫浮点数类型的float,当参与运算的变量存在float类型时,就可能存在小数点。关于小数点的问题以后再讲,现在暂时不深入讲解,现在要知道的是,unsigned char ,unsigned int ,unsigned long这三种变量类型的除法都是属于整除运算,不带小数点的。

【27.2   整除的运算符号是什么样子的?】

       10除以4,商等于2,余数是2,这个整除的过程诞生了两个结果,一个是商,一个是余数,与此对应,整除就诞生出两个运算符号,你如果想计算结果返回商就用“整除求商”的符号“/”,你如果想计算结果返回余数就用“整除求余”的符号“%”。咋一看,整除运算中用到的两个符号“/”和“%”都不是我们日常生活中熟悉的除号“÷”,我个人猜测是因为“÷”这个符号在电脑键盘上不方便直接输入,因此C语言的语法规则选用“/”和“%”作为整除的运算符号。

【27.3   整除求商“/”。】

        整除求商的通用格式:
        “保存变量”=“被除数” /  “除数1” /  “除数2”... /  “除数N”;

        跟之前讲的加减运算一样,赋值符号“=”左边的“保存变量”必须是变量,右边的可以是变量和常量的任意组合。如果右边只有两个参与运算的数据,就是整除求商的常见格式。
        整除求商的常见格式:
        “保存变量”=“被除数” /  “除数” ;

        现在深入分析一下整除求商的运算规律。

       (1)当除数等于0时。
         我们都知道,数**算的除数是不允许等于0的,如果在51单片机中非要让除数为0,商会出现什么结果?我测试了一下,发现有一个规律:在unsigned char的变量类型下,如果“除数”是变量的0,商等于十进制的255(十六进制是0xff)。如果“除数”是常量的0,商等于十进制的1。比如:
    unsigned char a;
    unsigned char b;
    unsigned char y=0;
    a=23/y;  //除数变量y里面是0,那么a的结果是255(十六进制的0xff)。
    b=23/0;  //除数是常量0,那么b的结果是1。

        平时做项目要尽量避免“除数是0”的情况,离它越远越好,但是既然除数不能为0,为什么我非要做“除数为0”时的实验呢?意义何在?这个实验的意义是,虽然我知道除数为0时会出错,但是我不知道这个错到底严不严重,会不会导致整个程序崩溃,当我做了这个实验后,我心中的石头才放下了,万一除数为0时,最多只是运算出错,但是不至于整个程序会崩溃,这样我心里就有了一个底,当哪天我某个程序崩溃跑飞时,我至少可以排除了“除数为0”这种情况,引导我从其它方面去找bug。

       (2)当被除数小于除数时。商等于0。比如:
    unsigned char c;
    c=7/10;   //c的结果是0。


        (3)当被除数等于除数时。商等于1。比如:
    unsigned char d;
    d=10/10;  //d的结果是1。


        (4)当被除数大于除数时。商大于0。比如:
    unsigned char e;
    unsigned char f;
    e=10/4;  //e的结果是2,大于0。
    f=10/3;  //f的结果是3,大于0。


【27.4   整除求商的自除简写。】

         当被除数是“保存变量”时,存在自除运算的简写。
          “保存变量”=“保存变量” /  “除数” ;

         上述自除运算的简写如下:
         “保存变量” / =“除数” ;

          比如:
    unsigned char e;
    g/=5;  //相当于g=g/5;


【27.5   整除求商有没有“自除1”的特殊写法?】

          加减法有自加1“++g”和自减1“g--”的特殊写法,但是除法不存在这种自除1的特殊写法,因为一个数除以1还是等于它本身,所以自除1没有任何意义,因此C语言语法中没有这种写法。

【27.6   整除求商的溢出。】

          除法的溢出规律跟加法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了避免一不小心就溢出的问题,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。

【27.7   例程练习和分析。】

          现在编写一个程序来验证刚才讲到的整除求商:
          程序代码如下:

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

void main() //主函数
{
     unsigned char a;
     unsigned char b;
     unsigned char c;
     unsigned char d;
     unsigned char e;
     unsigned char f;
     unsigned char g=10;  //初始化为10
     unsigned char y=0;   //除数变量初始化为0。

     //(1)当除数等于0时。
     a=23/y;
     b=23/0;  //这行代码在编译时会引起一条警告“Warning”,暂时不用管它。

     //(2)当被除数小于除数时。
     c=7/10;

     //(3)当被除数等于除数时。
     d=10/10;

     //(4)当被除数大于除数时。
     e=10/4;
     f=10/3;

     //(5)整除求商的简写。
     g/=5;  //相当于g=g/5;

     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。
     View(c);              //把第3个数c发送到电脑端的串口助手软件上观察。
     View(d);              //把第4个数d发送到电脑端的串口助手软件上观察。
     View(e);              //把第5个数e发送到电脑端的串口助手软件上观察。
     View(f);              //把第6个数f发送到电脑端的串口助手软件上观察。
     View(g);              //把第7个数g发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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


          在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:255
十六进制:FF
二进制:11111111

第2个数
十进制:1
十六进制:1
二进制:1

第3个数
十进制:0
十六进制:0
二进制:0

第4个数
十进制:1
十六进制:1
二进制:1

第5个数
十进制:2
十六进制:2
二进制:10

第6个数
十进制:3
十六进制:3
二进制:11

第7个数
十进制:2
十六进制:2
二进制:10


分析:        
          通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【27.8   如何在单片机上练习本章节C语言程序?】

          直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
147
whirt_noob| | 2016-7-23 16:16 | 只看该作者
后面还会有更新吗

使用特权

评论回复
148
jianhong_wu|  楼主 | 2016-7-24 11:06 | 只看该作者
本帖最后由 jianhong_wu 于 2016-7-24 11:20 编辑

第二十八节:整除求余。
第二十八节_pdf文件.pdf (79.55 KB)
【28.1   整除求余“%”。】

       上一节讲到,求商求余都是属于整除运算,区别是:求商返回商,求余返回余,求商是“/”,求余是“%”。求余的运算符号恰好就是我们平时常用的百分号“%”,之所以选择百分号作为求余的运算符号,我猜测是因为,在小于100%的数据中,如果我们仔细回味一下百分号的分子与分母的关系,其实就隐含了一层淡淡的求余的味道。

       整除求余的通用格式:
       “保存变量”=“被除数”% “除数1” % “除数2”...%  “除数N”;

        跟之前讲的加减运算一样,赋值符号“=”左边的“保存变量”必须是变量,右边的可以是变量和常量的任意组合。如果右边只有两个参与运算的数据,就是整除求余的常见格式。
       整除求余的常见格式:
       “保存变量”=“被除数” % “除数” ;      

       现在深入分析一下整除求余的运算规律。

     (1)当除数等于0时。
       我们都知道,数**算除数是不允许等于0的,如果在单片机中非要让除数为0,余数会出现什么结果?我在keil的C51编译环境试过,发现有一个规律:如果除数是变量的0,那么余数等于被除数。如果除数是常量的0,那么余数等于1。还有一种特殊的情况是编译不通过的,这种情况是“当被除数是变量,而除数是常量的0”。比如:
unsigned char a;
unsigned char b;
unsigned char k=10;
unsigned char y=0; //除数初始化为0

a=23%y;  //除数变量y里面是0,a的结果等于被除数23。
b=23%0;  //除数是常量0,b的结果是1。
b=k%0;   //这种特殊情况编译不通过:被除数是变量,而除数是常量的0。

       平时做项目要尽量避免“除数是0”的情况,离它越远越好,但是既然除数不能为0,为什么我非要做“除数为0”时的实验呢?意义何在?这个实验的意义是,虽然我知道除数为0时会出错,但是我不知道这个错到底严不严重,会不会导致整个程序崩溃,当我做了这个实验后,我心中的石头才放下了,万一除数为0时,最多只是运算出错,但是不至于整个程序会崩溃,这样我心里就有了一个底,当哪天我某个程序崩溃跑飞时,我至少可以排除了“除数为0”这种情况,引导我从其它方面去找bug。

      (2)当被除数小于除数时。余数等于被除数本身。比如:
    unsigned char c;
c=7%10;  //c的结果是7。


      (3)当被除数等于除数时。余数等于0。比如:
    unsigned char d;
d=10%10;  //d的结果是0。


      (4)当被除数大于除数时。余数必然小于除数。比如:
unsigned char e;
    unsigned char f;
e=10%4;  //e的结果是2。
f=10%3;  //f的结果是1。


      (5)当除数等于1时。余数必然等于0。
    unsigned char g;
g=7%1;  //g的结果是0。


【28.2   整除求余的自除简写。】

        当被除数是“保存变量”时,存在自除求余的简写。
        “保存变量”=“保存变量” %  “除数” ;

        上述自除求余的简写如下:
        “保存变量” % =“除数” ;

         比如:
    unsigned char h=9;
    h%=5;  //相当于h=h%5; 最后余数的计算结果是4。


【28.3   整除求余有没有“自除1”的特殊写法?】

        加减法有自加1“++g”和自减1“g--”的特殊写法,但是求余的除法不存在这种自除1的特殊写法,因为任何一个数除以1的余数必然等于0,所以求余的自除1没有任何意义,因此C语言语法中没有这种特殊写法。

【28.4   整除求余的溢出。】

        不管是求商还是求余,除法的溢出规律跟加法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了避免一不小心就溢出的问题,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。

【28.5   例程练习和分析。】

        现在编写一个程序来验证刚才讲到的整除求余:
        程序代码如下:
/*---C语言学习区域的开始。-----------------------------------------------*/

void main() //主函数
{
     unsigned char a;
     unsigned char b;
     unsigned char c;
     unsigned char d;
     unsigned char e;
     unsigned char f;
     unsigned char g;  
     unsigned char h=9;  //初始化为9。

     unsigned char k=10;  //初始化为10。
     unsigned char y=0; //除数变量初始化为0。

         //(1)当除数等于0时。
     a=23%y;
     b=23%0;
   //  b=k%0;  //这种特殊情况编译不通过:“被除数”是变量,而“除数”是常量的0。

         //(2)当被除数小于除数时。
     c=7%10;

         //(3)当被除数等于除数时。
     d=10%10;

         //(4)当被除数大于除数时。
     e=10%4;
     f=10%3;

         //(5)当除数等于1时。
     g=7%1;

         //(6)自除求余的简写。
     h%=5;  //相当于h=h%5;

     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。
     View(c);              //把第3个数c发送到电脑端的串口助手软件上观察。
     View(d);              //把第4个数d发送到电脑端的串口助手软件上观察。
     View(e);              //把第5个数e发送到电脑端的串口助手软件上观察。
     View(f);              //把第6个数f发送到电脑端的串口助手软件上观察。
     View(g);              //把第7个数g发送到电脑端的串口助手软件上观察。
     View(h);              //把第8个数h发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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


        在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:23
十六进制:17
二进制:10111

第2个数
十进制:1
十六进制:1
二进制:1

第3个数
十进制:7
十六进制:7
二进制:111

第4个数
十进制:0
十六进制:0
二进制:0

第5个数
十进制:2
十六进制:2
二进制:10

第6个数
十进制:1
十六进制:1
二进制:1

第7个数
十进制:0
十六进制:0
二进制:0

第8个数
十进制:4
十六进制:4
二进制:100


分析:        
         通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【28.6   如何在单片机上练习本章节C语言程序?】

         直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。


使用特权

评论回复
149
梦娇| | 2016-7-25 14:33 | 只看该作者
女孩子能学会吗

使用特权

评论回复
150
男人怕胖| | 2016-7-25 16:13 | 只看该作者
mark

使用特权

评论回复
151
liusiyi621| | 2016-7-25 22:10 | 只看该作者
有这种前辈真是后来者之福啊!感谢楼主无私奉献!

使用特权

评论回复
152
jianhong_wu|  楼主 | 2016-7-31 11:59 | 只看该作者
第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
第二十九节_pdf文件.pdf (79.95 KB)
【29.1   先余后商。】

       求商求余除了数**算外,在实际单片机项目中还有一个很常用的功能,就是提取某个数的个十百千位。提取这些位有什么用呢?用途可大了,几乎凡是涉及界面显示的项目都要用到,比如数码管的显示,液晶屏的显示。提取某个数的个十百千位是什么意思呢?比如8562这个数,提取处理后,就可以得到千位的8,百位的5,十位的6,个位的2。这里提到的“个,十,百,千”位只是一个虚数,具体是多少应该根据实际项目而定,也有可能是“个,十,百,千,万,十万,百万...”等位,总之,提取的思路和方法都是一致的。下面以8562这个数为例开始介绍提取的思路和方法。

       第一步:先把8562拆分成8562,562,62,2这四个数。怎么拆分呢?用求余的算法。比如:
8562等于8562%10000;
562等于8562%1000;
62等于8562%100;
2等于8562%10;


       第二步:再从8562,562,62,2这四个数中分别提取8,5,6,2这四个数。怎么提取呢?用求商的算法。比如:
8等于8562/1000;
5等于562/100;
6等于62/10;
2等于2/1;


       第三步:最后,把第一步和第二步的处理思路连写在一起如下:
8等于8562%10000/1000;
5等于8562%1000/100;
6等于8562%100/10;
2等于8562%10/1;


       仔细观察,上述处理思路的规律感特别清晰,我们很容易发现其中的规律和原因,如果要提取“万,十万,百万...”的位数,也是用一样的思路。另外,多说一句,根据我的经验,有一些单片机的C编译器可能不支持long类型数据的求余求商连写在一起,那么就要分两步走“先求余,再求商”,分开来操作。比如:
       unsigned char a;
       a=8562%10000/1000;  //提取千位。

       分成两步走之后如下:
       unsigned char a;
       a=8562%10000;     
       a=a/1000;          //提取千位。


      提取其它位分两步走的思路也是一样,不多说。


【29.2   先商后余。】

        刚才讲到了“先余后商”的提取思路,其实也可以倒过来“先商后余”,也就是先求商再求余数。下面还是以8562这个数为例。

        第一步:先把8562拆分成8,85,856,8562这四个数。怎么拆分呢?用求商的算法。比如:
8等于8562/1000;
85等于8562/100;
856等于8562/10;
8562等于8562/1;


        第二步:再从8,85,856,8562这四个数中分别提取8,5,6,2这四个数。怎么提取呢?用求余的算法。比如:
8等于8%10;
5等于85%10;
6等于856%10;
2等于8562%10;


         第三步:最后,把第一步和第二步的处理思路连写在一起如下:
8等于8562/1000%10;
5等于8562/100%10;
6等于8562/10%10;
2等于8562/1%10;


       上述的规律感也是特别清晰的。

【29.3   “先余后商”和“先商后余”哪家强?】

       上面讲了“先余后商”和“先商后余”这两种思路,到底哪种思路在实际项目中更好呢?其实我个人倾向于后者的“先商后余”,为什么呢?请看这个例子,以3100000000这个数为例,要提取该数的“十亿”位3。

       第一种:用“先余后商”的套路如下:
3等于3100000000%10000000000/1000000000;

       这里出现了一个问题,我们知道,unsigned long类型最大的数据是0xffffffff,转换成十进制后最大的数是4294967295,但是上面出现的10000000000这个数比unsigned long类型最大的数据4294967295还要大,这个就会引来我个人的担忧,C编译器到底会怎么处理,很有可能会出现意想不到的错误,至少会让我感到心里不踏实。当然,也许会有一些朋友说,这个是多虑的,最高位完全可以把求余这一步省略,这个说法也对,但是作为一种“套路”,我还是喜欢“套路”的对称感,“套路”之所以成为“套路”,是因为有一种对称感。下面再看看如果用“先商后余”的思路来处理,会不会出现这个担忧。

       第二种:用“先商后余”的套路如下:
3等于3100000000/1000000000%10;

       这一次,上面出现的1000000000这个数比unsigned long类型最大的数据4294967295小,所以没有刚才那种担忧,也维护了“套路”的对称感。所以我在实际项目中喜欢用这种方法。

【29.4   例程练习和分析。】

       现在编写一个程序来验证刚才讲到的两种思路:
       程序代码如下:
/*---C语言学习区域的开始。-----------------------------------------------*/

void main() //主函数
{
     unsigned char a; //千位
     unsigned char b; //百位
     unsigned char c; //十位
     unsigned char d; //个位

     unsigned char e; //千位
     unsigned char f; //百位
     unsigned char g; //十位  
     unsigned char h; //个位  

     //x初始化为8562,必须是unsignd int类型以上,不能是char类型,char最大范围是255。
     unsigned int  x=8562;  //被提取的数

         //第一种:先余后商。
         a=x%10000/1000;  //提取千位
         b=x%1000/100;    //提取百位
         c=x%100/10;      //提取十位
         d=x%10/1;        //提取个位

         //第二种:先商后余。
         e=x/1000%10;     //提取千位
         f=x/100%10;      //提取百位
         g=x/10%10;       //提取十位
         h=x/1%10;        //提取个位

     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。
     View(c);              //把第3个数c发送到电脑端的串口助手软件上观察。
     View(d);              //把第4个数d发送到电脑端的串口助手软件上观察。
     View(e);              //把第5个数e发送到电脑端的串口助手软件上观察。
     View(f);              //把第6个数f发送到电脑端的串口助手软件上观察。
     View(g);              //把第7个数g发送到电脑端的串口助手软件上观察。
     View(h);              //把第8个数h发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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


       在电脑串口助手软件上观察到的程序执行现象如下:
开始...

第1个数
十进制:8
十六进制:8
二进制:1000

第2个数
十进制:5
十六进制:5
二进制:101

第3个数
十进制:6
十六进制:6
二进制:110

第4个数
十进制:2
十六进制:2
二进制:10

第5个数
十进制:8
十六进制:8
二进制:1000

第6个数
十进制:5
十六进制:5
二进制:101

第7个数
十进制:6
十六进制:6
二进制:110

第8个数
十进制:2
十六进制:2
二进制:10


分析:        
        通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【29.5   如何在单片机上练习本章节C语言程序?】

        直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
153
sunhq02| | 2016-8-2 09:27 | 只看该作者
虽然楼主精神可嘉, 但
这是吃力不讨好的事情
也不是真正的开源

使用特权

评论回复
154
datouyuan| | 2016-8-2 10:28 | 只看该作者
必须支持楼主!!!!

使用特权

评论回复
155
jianhong_wu|  楼主 | 2016-8-8 17:08 | 只看该作者
本帖最后由 jianhong_wu 于 2016-8-8 17:10 编辑

第三十节:逻辑运算符的“与”运算。
第三十节_pdf文件.pdf (71.26 KB)
【30.1   “与”运算。】

      不管是十进制还是十六进制,单片机底层的运算都是以二进制的形式进行的,包括前面章节的加减乘除运算,在单片机的底层处理也是以二进制形式进行。只不过加减乘除我们平时太熟悉了,以十进制的形式口算或者笔算也能得到正确的结果,所以不需要刻意把十进制的数据先转换成二进制,然后再模拟单片机底层的二进制运算。但是本节的逻辑“与”运算,在分析它的运算过程和规律的时候,必须把所有的数据都转化成二进制才能进行分析,因为它强调的是二进制的位与位之间的逻辑运算。我们知道,二进制中的每一位只能是0或者1,两个数的“与”运算就是两个数被展开成二进制后的位与位之间的逻辑“与”运算。
      “与”运算的运算符号是“&”。运算规律是:两个位进行“与”运算,只有两个位都同时是1运算结果才能等于1,,否则,只要其中有一位是0,运算结果必是0.比如:
      0&0等于0。
      0&1等于0。
      1&0等于0。
      1&1等于1。


      注意,上述的0和1都是指二进制的0和1。


      现在举一个完整的例子来分析“与”运算的规律。有两个unsigned char类型的十进制数分别是12和9,求12&9的结果是多少?分析步骤如下:

      第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
      十进制12的二进制格式是:00001100。
      十进制9的二进制格式是: 00001001。



      第二步:二进制数右对齐,按上下每一位进行“与”运算。
      十进制的12       ->     00001100    
      十进制的9        ->    &00001001
      “与”运算结果是  ->    00001000



       第三步:把二进制的00001000转换成十六进制是:0x08。转换成十进制是8。所以12&9的结果是8。

       上述举的例子只能分析“与”运算的规律,并没有看出“与”运算的意义所在。“与”运算有啥用途呢?其实用途很多,最常见的用途是可以指定一个变量二进制格式的某位清零,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
       想让第0位清零,其它位保持不变,只需跟十六进制的0xfe相“与”:b=b&0xfe。
       想让第1位清零,其它位保持不变,只需跟十六进制的0xfd相“与”:b=b&0xfd。
       想让第2位清零,其它位保持不变,只需跟十六进制的0xfb相“与”:b=b&0xfb。
       想让第3位清零,其它位保持不变,只需跟十六进制的0xf7相“与”:b=b&0xf7。
       想让第4位清零,其它位保持不变,只需跟十六进制的0xef相“与”:b=b&0xef。
       想让第5位清零,其它位保持不变,只需跟十六进制的0xdf相“与”:b=b&0xdf。
       想让第6位清零,其它位保持不变,只需跟十六进制的0xbf相“与”:b=b&0xbf。
       想让第7位清零,其它位保持不变,只需跟十六进制的0x7f相“与”:b=b&0x7f。
       根据上述规律,假设b原来等于十进制的85(十六进制是0x55,二进制是01010101),要想把此数据的第0位清零,只需b=b&0xfe。最终b的运算结果是十进制是84(十六进制是0x54,二进制是01010100)。把它们展开成二进制格式的运算过程如下:
      十进制的85       ->     01010101    
      十六进制的0xfe   ->    &11111110
       “与”运算结果是  ->     01010100



【30.2   例程练习和分析。】

    现在编写一个程序来验证刚才讲到的“与”运算:
程序代码如下:

/*---C语言学习区域的开始。-----------------------------------------------*/
void main() //主函数
{
     unsigned char a;
     unsigned char b=85;  //十六进制是0x55,二进制是01010101。
     a=12&9;
     b=b&0xfe;   
     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。
     while(1)  
     {
     }
}
/*---C语言学习区域的结束。-----------------------------------------------*/

   在电脑串口助手软件上观察到的程序执行现象如下:


开始...
第1个数
十进制:8
十六进制:8
二进制:1000



第2个数
十进制:84
十六进制:54
二进制:1010100



分析:        
      通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【30.3   如何在单片机上练习本章节C语言程序?】

      直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。



使用特权

评论回复
156
minDragon| | 2016-8-8 22:41 | 只看该作者
鸿哥威武

使用特权

评论回复
157
wupin12345| | 2016-8-9 15:14 | 只看该作者
前辈辛苦,有没有模数转换显示的程序范例

使用特权

评论回复
158
杉侠| | 2016-8-11 16:21 | 只看该作者

使用特权

评论回复
159
jianhong_wu|  楼主 | 2016-8-14 13:50 | 只看该作者
第三十一节:逻辑运算符的“或”运算。
第三十一节_pdf文件.pdf (64.15 KB)
【31.1   “或”运算。】

      “或”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。两个数的“或”运算就是转换成二进制后每一位的“或”运算。
      “或”运算的符号是“|”。运算规律是:两个位的“或”运算,如果两个位都是0,那么运算结果才是0,否则只要其中有一位是1,那么运算结果必定是1。比如:
      0|0等于0。
      0|1等于1。
      1|0等于1。
      1|1等于1。


      现在举一个完整的例子来分析“|”运算的规律。有两个unsigned char类型的十进制数分别是12和9,求12|9的结果是多少?分析步骤如下:

      第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第13,14,15节的内容。
      十进制12的二进制格式是:00001100。
      十进制9的二进制格式是: 00001001。



      第二步:二进制数右对齐,按上下每一位进行“或”运算。
      十进制的12       ->     00001100    
      十进制的9        ->    |00001001
      “或”运算结果是  ->    00001101



      第三步:把二进制的00001101转换成十六进制是:0x0D。转换成十进制是13。所以12|9的结果是13。

      上一节讲的“与”运算最常见的用途是可以指定一个变量的某位清0,而本节的“或”运算刚好相反,“或”运算最常见的用途是可以指定一个变量的某位置1,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
       想让第0位置1,其它位保持不变,只需跟十六进制的0x01相“或”:b=b|0x01。
       想让第1位置1,其它位保持不变,只需跟十六进制的0x02相“或”:b=b|0x02。
       想让第2位置1,其它位保持不变,只需跟十六进制的0x04相“或”:b=b|0x04。
       想让第3位置1,其它位保持不变,只需跟十六进制的0x08相“或”:b=b|0x08。
       想让第4位置1,其它位保持不变,只需跟十六进制的0x10相“或”:b=b|0x10。
       想让第5位置1,其它位保持不变,只需跟十六进制的0x20相“或”:b=b|0x20。
       想让第6位置1,其它位保持不变,只需跟十六进制的0x40相“或”:b=b|0x40。
       想让第7位置1,其它位保持不变,只需跟十六进制的0x80相“或”:b=b|0x80。
       根据上述规律,假设b原来等于十进制的84(十六进制是0x54,二进制是01010100),要想把此数据的第0位置1,只需b=b|0x01。最终b的运算结果是十进制是85(十六进制是0x55,二进制是01010101)。把它们展开成二进制格式的运算过程如下:
       十进制的84       ->     01010100    
       十六进制的0x01   ->    |00000001
       “或”运算结果是  ->     01010101



【31.2   例程练习和分析。】

       现在编写一个程序来验证刚才讲到的“或”运算:
程序代码如下:
/*---C语言学习区域的开始。-----------------------------------------------*/

void main() //主函数
{
     unsigned char a;
     unsigned char b=84;  //十六进制是0x54,二进制是01010100。

     a=12|9;
     b=b|0x01;   

     View(a);              //把第1个数a发送到电脑端的串口助手软件上观察。
     View(b);              //把第2个数b发送到电脑端的串口助手软件上观察。

     while(1)  
     {
     }
}

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



       在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:13
十六进制: D
二进制:1101

第2个数
十进制:85
十六进制:55
二进制:1010101



分析:        
       通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。

【31.3   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
160
damoyeren| | 2016-8-15 09:25 | 只看该作者
持续关注,希望写书的时候从工程实际的角度出发。

使用特权

评论回复
发新帖 本帖赏金 72.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则