关于单片机上for循环中运用ACC的隐蔽错误

2020-02-05 09:13发布

最近写了几个程序,一个是用51单片机读取模数传感器adc0832的电压值,一个是读取ds1302的时间值,结果都出现了读数一直为0的情况。我调试了近一个星期,修改了一个我认为不可能会错的句子,程序运行成功了,这才发现了一个极其隐蔽的错误。(我用的是xp系统,用keil4软件编译)

     先上代码:第一个为错误代码,第二个为正确代码。这是用来向ds1302芯片写入命令或数据的函数。实现把8位的数据dat一位一位地写入ds1302的io口。其中ACC0为ACC的第0位。

  
view plaincopy to clipboardprint?
·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001.//for(i=0;i<8;i++),ACC版   
02.  
03.void ds1302_input(uchar dat)   
04.{   
05.   uchar i;      //uchar= unsigned char   
06.  ACC = dat;   //ACC为51的累加器   
07.   for(i=0;i<8;i++)   
08.   {   
09.    DS1302_SCLK = 0;    //ds1302的时钟引脚   
10.    DS1302_IO = ACC0;    //ds1302的io口   
11.    DS1302_SCLK = 1;     //上升沿写入数据   
12.    ACC >>= 1;       //第一个不移动   
13.   }   
14.}  
//for(i=0;i<8;i++),ACC版

void ds1302_input(uchar dat)
{
   uchar i;      //uchar= unsigned char
  ACC = dat;   //ACC为51的累加器
   for(i=0;i<8;i++)
   {
           DS1302_SCLK = 0;    //ds1302的时钟引脚
        DS1302_IO = ACC0;    //ds1302的io口
        DS1302_SCLK = 1;     //上升沿写入数据
        ACC >>= 1;       //第一个不移动
   }
}

view plaincopy to clipboardprint?
·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001.//for(i=8;i>0;i--),ACC版   
02.  
03.void ds1302_input(uchar dat)   
04.{   
05.   uchar i;   
06.  ACC = dat;   
07.   for(i=8;i>0;i--)   
08.   {   
09.    DS1302_SCLK = 0;   
10.    DS1302_IO = ACC0;   
11.    DS1302_SCLK = 1; //上升沿写入数据   
12.    ACC >>= 1;     // 第一个不移动   
13.   }   
14.}  
//for(i=8;i>0;i--),ACC版

void ds1302_input(uchar dat)
{
   uchar i;
  ACC = dat;
   for(i=8;i>0;i--)
   {
           DS1302_SCLK = 0;
        DS1302_IO = ACC0;
        DS1302_SCLK = 1; //上升沿写入数据
        ACC >>= 1;          //        第一个不移动
   }
}

     认真对比这两个代码,可能会觉得没区别,而且这两个代码都可以通过编译(加上reg52.h和一些宏定义)。我也是一直认为for()这里边没有错误,结果。。。试着修改时钟信号,增加延时之类的,调了好久还是错,严重打击我的自信心。这两个代码的区别就只有for(i=0;i<8;i++)和for(i=8;i>0;i--)了。学过c语言的人都知道,这两个句子都是实现一个8次的循环,功能一模一样。怎么会因为这个句子的区别就导致单片机控制的错误呢?神奇!

    接着我试着把错误程序中的ACC改为51芯片的寄存器B,烧录进单片机,程序运行成功,跟“for(i=8;i>0;i--),ACC版”一样,lcd在很嚣张地显示着正确的时间( for(i=0;i<8;i++),ACC版lcd的时间显示为0)。附:
view plaincopy to clipboardprint?
·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001.//for(i=0;i<8;i++),寄存器B版   
02.void ds1302_input(uchar dat)   
03.{   
04.   uchar i;   
05.  B = dat;   
06.   for(i=0;i<8;i++)   
07.   {   
08.        DS1302_SCLK = 0;   
09.        DS1302_IO = B0;   
10.        DS1302_SCLK = 1; //上升沿写入数据   
11.        B >>= 1;  //第一个不移动   
12.   }   
13.}  
//for(i=0;i<8;i++),寄存器B版
void ds1302_input(uchar dat)
{
   uchar i;
  B = dat;
   for(i=0;i<8;i++)
   {
                   DS1302_SCLK = 0;
                DS1302_IO = B0;
                DS1302_SCLK = 1; //上升沿写入数据
                B >>= 1;  //第一个不移动
   }
}

   这样就知道原因了,使用for(i=0;i<8;i++)的运算中可能有累加器ACC参与了,导致修改了ACC的值,使写入的命令出现错误。但为什么for(i=8;i>0;i--)就没有ACC的参与呢?一个大大的问号。基于我调试了一个星期的程序,皆因为这一个神奇的错误,我实在不甘心,决定研究到底。于是,分别查看了这三个程序代码用 keil4 编译后得到的 汇编代码。(学过汇编就是爽啊,哈)
   
view plaincopy to clipboardprint?
01.;for(i=0;i>8;i++),ACC版     汇编中“;”后的这行句子是注释   
02.RSEG  ?PR?_ds1302_input?DS1302   
03._ds1302_input:   
04.    USING   0   
05.;---- Variable 'dat?141' assigned to Register 'R7' ----   
06.    MOV     A,R7   ;实参值dat的数据存储到r7寄存器,把r7的值赋给累加器ACC   
07.    CLR     A      ;把ACC清零(就是因为这句子导致程序有问题)   
08.    MOV     R7,A   ;把ACC的值赋给R7   
09.?C0005:   
10.    CLR     DS1302_SCLK  ;时钟线sclk清零   
11.    MOV     C,ACC0       ;把ACC第0位赋值到进位寄存器C中   
12.    MOV     DS1302_IO,C  ;把c赋值给IO口,改变单片机的引脚值   
13.    SETB    DS1302_SCLK  ;时钟线sclk置1   
14.    CLR     C            ;进位寄存器c清零   
15.    RRC     A            ;ACC右移一位   
16.    INC     R7           ;寄存器R值加1   
17.    CJNE    R7,#08H,?C0005  ;r7的值与8比较,不相等跳转到?c005:循环   
18.?C0008:   
19.    RET  ;子函数返回   
;for(i=0;i>8;i++),ACC版     汇编中“;”后的这行句子是注释
RSEG  ?PR?_ds1302_input?DS1302
_ds1302_input:
        USING        0
;---- Variable 'dat?141' assigned to Register 'R7' ----
        MOV          A,R7   ;实参值dat的数据存储到r7寄存器,把r7的值赋给累加器ACC
        CLR          A      ;把ACC清零(就是因为这句子导致程序有问题)
        MOV          R7,A   ;把ACC的值赋给R7
?C0005:
        CLR          DS1302_SCLK  ;时钟线sclk清零
        MOV          C,ACC0       ;把ACC第0位赋值到进位寄存器C中
        MOV          DS1302_IO,C  ;把c赋值给IO口,改变单片机的引脚值
        SETB         DS1302_SCLK  ;时钟线sclk置1
        CLR          C            ;进位寄存器c清零
        RRC          A            ;ACC右移一位
        INC          R7           ;寄存器R值加1
        CJNE         R7,#08H,?C0005  ;r7的值与8比较,不相等跳转到?c005:循环
?C0008:
        RET  ;子函数返回       


view plaincopy to clipboardprint?
01.;for(i=8;i>0;i--)ACC版   
02.RSEG  ?PR?_ds1302_input?DS1302   
03._ds1302_input:   
04.    USING   0   
05.           
06.;---- Variable 'dat?040' assigned to Register 'R7' ----   
07.    MOV     A,R7     ;把实参值dat赋给寄存器ACC   
08.;---- Variable 'i?041' assigned to Register 'R7' ----   
09.    MOV     R7,#08H  ;把8赋值给寄存器R7   
10.?C0001:   
11.    CLR     DS1302_SCLK  ;此处跟i=0版的一样   
12.    MOV     C,ACC0   
13.    MOV     DS1302_IO,C   
14.    SETB    DS1302_SCLK   
15.    CLR     C   
16.    RRC     A   
17.    DJNZ    R7,?C0001   ;r7的值减1,不是零跳转到?c001,循环      
18.?C0004:   
19.    RET      
;for(i=8;i>0;i--)ACC版
RSEG  ?PR?_ds1302_input?DS1302
_ds1302_input:
        USING        0
               
;---- Variable 'dat?040' assigned to Register 'R7' ----
        MOV          A,R7     ;把实参值dat赋给寄存器ACC
;---- Variable 'i?041' assigned to Register 'R7' ----
        MOV          R7,#08H  ;把8赋值给寄存器R7
?C0001:
        CLR          DS1302_SCLK  ;此处跟i=0版的一样
        MOV          C,ACC0
        MOV          DS1302_IO,C
        SETB         DS1302_SCLK
        CLR          C
        RRC          A
        DJNZ         R7,?C0001   ;r7的值减1,不是零跳转到?c001,循环   
?C0004:
        RET         


     对比后,可以发现,出错的原因是for(i=0;i<8;i++)ACC版中,用ACC接收了实参(存储的为要写入的指令),然后在 for 循环前要给变量  “ i  "   赋值时,要用到ACC清零,再把ACC中的零赋给 R7 ("i"的值存储在R7)。这样的话,原来存储在ACC中的写入指令就被清零,自然会导致控制出现错误,最终没法读取ds1302芯片的时间,故显示为零。

    而在for(i=8;i>0;i--)ACC版中,也用ACC接收了实参的值,但在 for 循环前,给变量“ i ” 赋值时,赋值为8,不需要用到ACC,所以ACC一直是存储着实参中的指令,没有被清零,所以能够顺利地向ds1302发送指令,从而能够读取到时间。

   总结:

     因为用for(i=0;i<n;i++)类的指令会重复使用ACC而可能导致出错,而且,即使其它程序可能不会有这种用ACC来接收实参值的情况,但前者比for(i=n;i>0;i--)类的指令多了 CLR A  和 INC R7 两条指令,CJNE 指令又比较DJNZ指令多了一个字节的程序代码存储空间,在频率为12M的51单片机上体现为执行同样功能的程序,要多用2us,代码空间花多一字节。所以前者是毫无优势的,以后应养成用

for(i=n;i>0;i--)的习惯。

    请不要反驳我用了这么长的时间去研究,只能使单片机执行快2us,而说我钻牛角尖,只是因为,这个错误导致我整个程序无法正常运行,这不是一件小事。

   至于为什么要用到累加器ACC来接收实参,是因为后面的程序要把一个8位的实参一位一位地输出到一个io口,自定义一个变量的话,按位寻址好像比较麻烦,要经过一系列 位运算 ,或者用bit定义8个位(有好的方法请告诉我,哈),而且我写不出来。而用ACC的话,可以很轻易地操作ACC的任意一位,如ACC0,ACC7。在网上查了一下,好像还有一种方法是定义 一种叫 位域 的东东,我看的c语言的书都没介绍,所以还不是很了解。

/************************************************************/

刚刚想了一下,不用ACC 的方法,作一个位运算dat &0x01,修改如下:

view plaincopy to clipboardprint?
01.//for(i=0;i<8;i++),ACC版   
02.  
03.void ds1302_input(uchar dat)   
04.{   
05.   uchar i;      //uchar= unsigned char   
06.    for(i=0;i<8;i++)   
07.   {   
08.    DS1302_SCLK = 0;    //ds1302的时钟引脚   
09.    DS1302_IO = dat & 0x01;    //ds1302的io口   
10.    DS1302_SCLK = 1;     //上升沿写入数据   
11.    dat >>= 1;       //第一个不移动   
12.   }   
13.}   
//for(i=0;i<8;i++),ACC版

void ds1302_input(uchar dat)
{
   uchar i;      //uchar= unsigned char
    for(i=0;i<8;i++)
   {
    DS1302_SCLK = 0;    //ds1302的时钟引脚
    DS1302_IO = dat & 0x01;    //ds1302的io口
    DS1302_SCLK = 1;     //上升沿写入数据
    dat >>= 1;       //第一个不移动
   }
}



想到了这个方法后,觉得自己好白痴,以后都不用ACC了。

我也对比了用寄存器 B 时的情况,在这里不再赘述。想了解的可以找我探讨。
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。