本帖最后由 7788281 于 2012-10-27 22:45 编辑
19、KEIL的混合编程操作这一篇来讲讲混合编程的问题,在网上找了一下,讲混合编程的文件章也有不少,但进行实例操作讲解的不多也不完整,本来书上混合编程的内容看着就让人觉得抽象难懂,再没有个实际操作图例,就很让人觉得云里雾里。在这里我就针对KEIL做个混合编程的实例的文章希望对初学者有所帮助。先搞清几个问题。
①混合编程的必要性:也就是为什么需要混合编程,初学者一定会觉得,我C用的好好的为什么要混进汇编呢,不是自找麻烦吗?其实不然,最简单的例子就是延时子程序,用C写的话连你自己也不知道几层的循环后确切地用多少时间吧?但用汇编写你就能很准确地计算出要延时的时间。还有当你要对那些时序要求很高IC模块或步进电机行操作时用汇编来写就能做到操控的直接与精准。
②在进行实际操作前要弄清C与汇编之间的调用关系,C的函数大家都会用了,主要分为无反回参数的和有反回参数的,例如 void delay(void);就是无反回参数的,int readdata(void);就是有返回参数的。还有就是有参数传递和无参数传递的,void delay(void);就是无参数传递的,unsigned int add(unsigned char aa,unsigned char bb);就是有参数传递的函数。在教材上讲起C与汇编的混合编程就会说起寄存器最多传递三个函数,这样可以产生高效代码。
在参数返回时寄存器的传递规律为:
下面我们用实际的混合编程操作来讲讲如何实现函数的调用及参数的传递。
打开KEIL,我的用的版本是绿色免安装2.0中文版,编译器为7.0:无程序代码长度限制。现在有3.0版也是绿色免安装版本,好处是已支持双字节中文注释,但是英文版。用哪个版本都无所谓,只要用着习惯功能够用就行。
下面是版本信息:
在网上经常有朋友说为什么我下载了KEIL解压出目录后运行却不能编译呢,老是报告出错:
--- Error: can't execute 'E:old_pctxz001单片机c51KEIL4C51BINC51.EXE'
--- 错误: 不能执行 'E:old_pctxz001单片机c51KEIL2_70Keil2C51BINC51.EXE'
这是由于编译时,C51.exe编译器没能在你给出的路径上找到。你需要修改路径。
在选择KEIL的菜单栏“工程”--“文件扩展名、书籍和编译环境属性”--“环境设置”的如下图:
看到上图的“使用TOOLS.INI设定”前的钩了吗?对了,它是按照你TOOLS.INI里给出的路径去找的。因此的得打开那个tools.ini文件修改它。KEIL的目录结构一般是这样的:
我们KEIL软件运行主程序uvision2是在目录UV2里,而那个设置文件TOOLS.INI文件是在它的上一级目录Keil里,见上图。用记事本打开这个TOOLS.INI文件:
看见红笔圈出的[C51]下的路径了吗?将它修改正确指向你硬盘上KEIL下C51目录,存盘,运行KEIL。就可以正确编译了。(废话又多了。。。)好!言归正传。
我们在KEIL里创建一个新的工程TEST1。在这个工程里我们添加了两个文件,main.c和delay.c,程序如下:
文件main.c:
#include
extern void delay(void);
main(void)
{
delay();
}
文件delay.c
#define uchar unsigned char
void delay(void)
{ uchar i;
for(i=255,i>0,i--);
}
可以看出,这两个文件里的程序很简单,主程序里先定义了一个外部函数delay();然后就调用了这个无参数函数。而文件delay.c里也就是用for循环做了255次循环。
下面我们先进行编译,调试让程序正确,通过编译。然后我们选择左边工程窗口,选中文件delay.c,鼠标右击它出现下图。
选择“文件'delay.c'属性”后如下图:
见上图,有“产生汇编文件”和“汇编源代码文件”两项前的钩选框是灰色的,分别点击它们两次使它呈黑色钩选状态。如下图。
点击下面的确认钮,回到主界面。这时你再进行一次全部的重新编译,就会发现在你建立这个工程的目录下将多产生一个delay.src文件。
用记事本打开这个delay.src文件。发现它就是一个汇编文件。
; .delay.SRC generated from: delay.c
; COMPILER INVOKED BY:
; E:old_pctxz001单片机c51KEIL2_70KeilC51BINC51.EXE delay.c BROWSE DEBUG OBJECTEXTEND SRC(.delay.SRC)
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
; #define uchar unsigned char
; void delay(void)
RSEG ?PR?delay?DELAY
delay:
USING 0
; SOURCE LINE # 2
; { uchar i;
; SOURCE LINE # 3
; for(i=255;i>0;i--);
; SOURCE LINE # 4
;---- Variable 'i?040' assigned to Register 'R7' ----
MOV R7,#0FFH
?C0001:
DJNZ R7,?C0001
; }
; SOURCE LINE # 5
?C0004:
RET
; END OF delay
END
可以看出原来的C程序都变成了汇编的注释了。我们将注释都去掉。
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
USING 0
MOV R7,#0FFH
?C0001:
DJNZ R7,?C0001
?C0004:
RET
END
现在看看是不是很简呢。在标号delay:前是程序的说明,就是定义函数的名字,将代码放在哪里等,看不懂也没关系,别乱改它就行。从delay:标号后就是汇编的程序部分了。里面的标号最好也别乱改。添加你要操作的程序就行了,好!我们先不改动程序,就将上面十行汇编别存为delay.asm文件。回到KEIL界面,我们在工程窗里(是KEIL主界面左边的工程窗口而不是在工程目录里)的将delay.c删除。然后再添加上delay.asm程序,如下图:
这样,你再进行编译,你会发现你已经通过了混合编程的编译,虽然这次你对程序的功能什么都没有改变,但你已经知道如何做出一个C程序调用汇编子程序的例子了。下面我们可以对这个汇编了程序进行一些修改看它是否仍能很好的工作。
今天我们就来对那个汇编的delay子程序进行修改,为了让运行的结果能显示出来,我先加进一个LCD的显示子程序12864put.c。
我们先修改主程序如下:
//****************
// 主函数
//****************
main(void)
{ uchar aa,bb;
TMOD=0x01;//定义T0为模式1即16位计数方式
TH0=0;//将计数器高位初值清0
TL0=0;//将计数器低位初值清0
TR0=1;//计数器开始计数
//delay(); //调用汇编的子函数
TR0=0;//停止计数
aa=TH0;//把计数的值高位交给aa
bb=TL0;//把计数的值低位交给aa
LcmInit();//初始化LCD12864
LcmClear();//清屏LCD
LcmPutstr( 0,28,"C&A TEST" );//显示
LcmPutstr( 3,0,"TH0:" );
LcmPutstr( 3,24,uchartostr(aa) );
LcmPutstr( 3,46,"TL0:");
LcmPutstr( 3,70,uchartostr(bb) );
LcmPutstr( 5,0,"BLOG:http://" );
LcmPutstr( 6,18,"hi.baidu.com/txz01" );
LcmPutstr( 7,8,"Email:TXZ001@139.com" );
看见上面的程序了吗?我用了T0在调用汇编子函数delay()前开始计数,调用完后就关掉,然后看计数器内的计数值来知道我们这个子函数的精确程度。我先把delay()函数给注释掉,看看开始计数后就立即关掉要用去多少时间。结果显示为1,就是说用了一个脉冲的时间。12M的晶振就是一微秒。见下图:
看到没有,用了TR0=1;TR0=0;本身就用去了一个脉冲。好!现在我们将那个调用汇编子函数delay()语句启用,但我将汇编内的语句给清空。也就是说我把delay.asm这个子程序让它什么也没做。是个空函数,看它要用掉几个脉冲时间。汇编程序如下:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
RET
END
看到了吗?标号delay:下面什么也没有了,直接就RET返回了。好!编译,烧写,运行!如下图:
结果是用了5个脉冲,其中一个是调用计数器本身用的,也就是说调用一个空函数用了4个脉冲时间。好!我们再来修改一下汇编程序:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
mov r7,#100
djnz r7,$
RET
END
在标号delay:下面我加了两行,我们计算一下,第一行MOV r7,#100要用一个机器周期,也就是一个脉冲。第二行djnz r7,$要循环100次每次用2个机器周期,这样算来共是201个脉冲再加上刚才我们计算过的调用函数要4个脉冲和开关计数器用1个,总共是206个。编译,烧写,运行!
看来计算的没错呀!我们再循环多些:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
mov r7,#100 ;1
loop:mov r6,#50 ;100
djnz r6,$ ;50×100×2
djnz r7,loop ;100×2
RET
END
这次的计算应该是1+100+50×100×2+100×2+5=10306。再次编译烧写运行!
高位数值为40,低位数值为66,则总数=40×256+66=10306。精准吧!好了!无参函数的调用就讨论到此。
下面接着说说带参数据函数的调用:
我们重新建立一个目录TEST2(因为一个项目有很多个文件如果都放在一个目录里会很混乱,以后想挪到U盘带到其它机子上用时就很困难了),建立新的项目test2.Uv2,里面还是main.c主程序和12864put.c显示子程序:
主函数main()如下:
#include
#include
#define uchar unsigned char
#define uint unsigned int
extern void LcmClear( void ); //清屏
extern void LcmInit( void ); //初始化
extern void LcmPutstr( uchar row,uchar y,uchar * str ); //在设定位置显示字符串
//row:是LCD的行数(0-7)
//y:是LCD的列数(0-127)
//str:是字符串的首地址
extern uint add(uchar aa,uchar bb);
extern void inttostr(uint intval,uchar data * str);
uchar str[6];//定义四个字节空间用来存放数值转换成的字符值
//****************
// 主函数
//****************
main(void)
{ uchar aa,bb;
uint cc;
aa=145;
bb=236;
cc=add(aa,bb);
LcmInit();//初始化LCD12864
LcmClear();//清屏LCD
LcmPutstr( 0,28,"C&A TEST" );//显示
inttostr(aa,str);
LcmPutstr( 3,0,str );
LcmPutstr( 3,18," + " );
inttostr(bb,str);
LcmPutstr( 3,36,str);
LcmPutstr( 3,54," = ");
inttostr(cc,str);
LcmPutstr( 3,72,str);
//LcmPutstr( 3,46,"TL0:");
//LcmPutstr( 3,70,uchartostr(bb) );
LcmPutstr( 5,0,"BLOG:http://" );
LcmPutstr( 6,18,"hi.baidu.com/txz01" );
LcmPutstr( 7,8,"Email:TXZ001@139.com" );
while(1);
}
项目中还有uinttostr.c是无符号整型转字符串子程序和我们要做汇编调用的这个有返回参数有传递参数的子程序add.c,子程序add.c如下。
#define uchar unsigned char
#define uint unsigned int
uint add(uchar aa,uchar bb)
{
uint cc;
cc=aa+bb;
return(cc);
}
我们主要目的是为了表达清楚怎样在C程序里去调用汇编子函数,所以程序还是很简单,就是把主程序传过来的无符号字符型变量aa和bb相加,相加的结果交给无符号整型变量cc返回给主程序。编译前我们还是点取add.c文件属性,让它产生src文件。上面的图已显示了编译的过程信息。现在我们打开这个add.src文件:
; .add.SRC generated from: add.c
; COMPILER INVOKED BY:
; E:old_pctxz001单片机c51KEIL2_70KeilC51BINC51.EXE add.c BROWSE DEBUG OBJECTEXTEND SRC(.add.SRC)
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
; #define uchar unsigned char
; #define uint unsigned int
;
; uint add(uchar aa,uchar bb)
RSEG ?PR?_add?ADD
_add:
USING 0
; SOURCE LINE # 4
;---- Variable 'bb?041' assigned to Register 'R5' ----
;---- Variable 'aa?040' assigned to Register 'R7' ----
; {
; SOURCE LINE # 5
; uint cc;
; cc=aa+bb;
; SOURCE LINE # 7
MOV A,R5
ADD A,R7
MOV R7,A
CLR A
RLC A
MOV R6,A
;---- Variable 'cc?042' assigned to Register 'R6/R7' ----
; return(cc);
; SOURCE LINE # 8
; }
; SOURCE LINE # 9
?C0001:
RET
; END OF _add
END
我们还是将注释的部分删去,这样便于我们分析:
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
RSEG ?PR?_add?ADD
_add:
USING 0
MOV A,R5
ADD A,R7
MOV R7,A
CLR A
RLC A
MOV R6,A
RET
END
现在我们首先来看函数名,上面我们讲过的那个无参数函数delay()的调用,产生的汇编子函数名就是delay,而这次我我们原来C的函数名add变成了汇编的_add。前面多了个下划线,这就是有参数函数的特征。C语言函数名转变为汇编函数名的规律为:无参数传递时void func(void)----FUNC。寄存器参数传递时char func(char)----_FUNC。再入函数使用时void func(void) reentrant----_?FUNC。
不过这些名字的变化规律记没记住好象关系并不大。我们想要用到汇编调用时,就先用C做个假函数然后产生汇编文件名字就自然出来,并不用我们去管它的命名,然后去修改成我们想做的汇编程序就行了。
但是,这参数传递的位置规律就必须得知道,否则你就无法使用这个汇编了,我们看上面的汇编程序,第一句是将寄存器R5的值传到A中,第二句将A与寄存器R7相加,第三句将相加的结果A的值传给R7,后面的几句是将刚才相加的进位值C,传给R6,然后返回。对照本篇最上面给的那两张表我们可以看出C子函数add.c的第一个参数aa被传到了汇编的R7,第二个参数bb被传到了R5,将它们相加后,返回值的低位交给了R7,高位交给了R6。完全符合参数传递表和返回值表所述。下面我们将汇编子程序另存为asm文件后替换掉原来的C子程序:
编译、烧写后运行:
这个加法函数我们没有改动任何参数当然运行起来是不会错的,下面我们将在汇编里将它改成乘法试试,
将标号_add:下面的语句全都改掉,程序如下?
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
RSEG ?PR?_add?ADD
_add:
USING 0
MOV A,R7
MOV B,R5
MUL AB ;A与B相乘,乘积的高位值在B中,低位值在A中
MOV R7,A ;将低位值传给R7
MOV R6,B ;将高位值传给R6
RET
END
上面的改动我们已将原来的加法依照寄存器的传递规律改为乘法函数,看看是否还能正常运行并正确,改完后仍编译烧写运行:
哈哈!完全正确。
总结:
我们可以经常性地采用在C中建立简单的子函数,转成汇编后看它的操作方法和传递规律,慢慢地熟悉掌握和运用如何在C中调用汇编函数
20、AT89C2051烧写器的制做与调试 现在都用S52了,还用C2051干嘛!价格也差不多。但是C2051的体积要比S51、S52小很多,而且引脚只有20只,在一些简单的控制中,这些引脚已足够了,小的体积更具有优势些。但目前好像还没有支持在线编程(ISP)的S2051。因此试着做个C2051的编程器,更主要的是与大家分享一下在电路制做过程中硬件、软件的除错技巧,尽量做到一次成功的经验和方法。
在网上找了一番,图很多,大同小异。上一张最通用的图:
上图是可以烧写很多器件的电路,它的PCB板在网上也很容易找。但因为是实验制做,没用PCB,就用万用板吧。由于宽体的器件都有支持ISP的器件,所以我去掉了40脚宽体IC座,只留了个20脚窄体IC座用来烧写C2051,修改电路后电路图如下:
元件清单如下:
1/8w 4.7k 6 (一共6个)
560r 3 (一共3个)
2k2 1
4.7 欧姆 1 串联于供电电路起保护作用
10k排阻 1 用于cpu p0口上拉
1n4148 2 用于vpp高压供给 (注意4148 的一头是黑色的)
12v 1 用于vpp高压供给 (注意12V 的一头是白色的不要搞混)
4.7uf/50v 8 所有的电解都用同一个型号的, 注意方向哦
104 2 用于电源滤波
22p 2 用于晶体电路
11.0592 1 晶体
2n5401 1 注意看型号和插入的方向, 不要错!
2n5551 2
发光管 2 3mm白发红(用于电源指示和通信指示)(注意方向)
db9 1 串口头
20pin 卡座 1 烧写 2051等用
40pin ic座 1 插入监控用89s51
16pin ic座 1 插入 max232芯片
u***座 1 供电用
串口电缆 1 通信用
u*** 电缆 1 供电用
At89s51 1 用于监控
max232cpe 1 通信用集成电路
pcb 1 电路板
实际板子今天已做好,先秀一下吧。接着再讲详细调试方法和步骤:
(点击后看大图)
这个电路对于初学者可能复杂了一点,它分为几个部分,如果你先将电路板规划好,然后把元件一次都焊上,再进行调试,成功的概率很小。那应该怎么做呢?先规划一下元件的位置布局,然后分几个单元,一个单元做好后进行调试,正常后再进行下一个单元的制做调试。我的规划如下,先是电源部分包括那个电源指示LED:
先将USB座焊上,确认无短路后插上USB电源,用万用表找出A、B两点中哪个是5v的正极,哪个是地。然后去掉电源将剩下的几个电容、电阻和LED焊上,再确认无短路后接上电源,LED应亮起。我是将LED的限流电阻改为了2K,因为我看它已够亮了。电流1.5毫安。实物如右边红色圈出的图。
第二部分是MAX232,包括那个+12v电压稳压电路。MAX232是TTL转RS232专用IC。它内部提供了一个正10v,和负10v的升压电路。我们正是利用了它的升压电路又获得了个+12v的电源。
我们先将9针串口焊上,但不接上MAX232。在你规划的MAX232区,按上图左侧电路将MAX232座和四个电容焊好。检查无短路后,插上MAX232,接上电源,在A点应能测到+9v-+10v的电压。在B点应有-9v--10v的电压,将10脚接地,那7脚就应该是+10V。若10脚接+5v,那么7脚就变成-10v。这样MAX232就正常了。将串口的2脚接MAX232的7脚。串口的3脚接MAX232的8脚。接上电将MAX232的10脚分别接地和+5v看看串口的2脚是否跟刚才一样变化。以上的调试正确后,将上图右边的12V稳压电路焊上。如下图:
用万用表在稳压管两端应测得稳定的+12V电压。这样你的这个串口转换和12v电源电路就调试正常了。
第三部分是12V烧写控制部分:电路和实物图如下:
这部分电路焊好后,接上第二部分调试正常的+12v和电源+5V。先测量电路上+12v接入点和电源电压+5v是否正常确。再测量上图的A点也应该是+5v。然后你把VO13用导线接+5V,那么A点是+9v-+11v就对了。好!断开VO13,再将V014用导线接+5v,A点应该是接近0。这样12V烧写控制电路也调试正确了。
下面就是S51部分的电路,如下图:
这部分电路包括P0口的上拉排阻和那个接收指示LED接在S51的10脚。别忘了将S51的第31脚(EA / VP)也接电源+5v。先确认将MAX232的第9脚与S51第10脚的连接断开(因为MAX232在调试正常后9脚就输出+5v高平会点亮接收指示LED,妨碍S51工作性能的检查)。检查电路无误后插上S51片子接上电源,静态电流应在9-12mA左右。好!打开KEIL写个测试程序:
#include
#define uchar unsigned char
main(void)
{
uchar i,j,k;
while(1)
{
for(i=0;i<1;i++)
{
for(j=0;j<255;j++)
for(k=0;k<255;k++);
}
P3_0=~P3_0;
}
}
这个程序就是让S51第10脚(P3.0)接的那个LED闪烁。能闪烁就说明晶振起振,S51电路工作正常了,编译,然后用ISP将这个程序写进S51片内,再插到我们做好的2051烧写器上。接上电源,我的电路立即就正常工作了,LED在闪烁。如果LED无反应,你就得检查你的S51电路是否连接正常,晶振是否焊接正确等。
S51电路正常后,我们就要将刚才断开的MAX232的第9脚和S51的第10脚焊上。这时我们再编个测试程序:
#include
#include
void main(void)
{
SCON=0x50;
TMOD=0x20;
PCON=0x80;
TCON=0x40;
TH1=0xfd;//比特率19200
TL1=0xfd;
ti=1;
TR1=1;
while(1)
{
printf("hello World!n");
}
}
这个程序就是打开单片机S51的串口,不断的送出字符串“hello World"。编译成HEX文件,再用ISP将程序写进S51片内。然后插到2051烧写板上。将串口线接到PC上。打开PC,运行串口调试器:
将红圈画出位置的波特率设为19200。确认为COM1口。然后将2051烧写器的电源给上,我的PC串口调试器上立即就出现了“hello World”。
这样,与PC的串口通讯就调试成功了。如果你没有出现正确结果,而上面几步的调试又是正常的,那就是从PC串口端到你S51的接线有错误了。仔细检查你的接线,会找到错误的。
上面几步都正常后,就是将这几步调试正常的电路整合了。将12v烧写控制端VPP接到2051的第1脚,VO13、VO14分别接到S51的第13、14脚。S51的第15脚VST接到2051的第9脚。S51的第1脚(P1.0)接到2051的第11脚。S51的5、6、7、8分别接到2051的第5、6、7、8脚。S51的P0口(第32-39脚)分别接到2051座的第12-19脚。别忘了将2051座的电源,地接上。
好!检查无误后,接上电源再试一次串口通讯是否正常(以防在焊接后焊错使S51不工作)后。拔下S51,用ISP将那个E51Pro.HEX的驱动固件写进S51,再将它插到40脚S51座上,在20脚IC座上插上AT89C2051,连好PC串口,PC上运行那个Easy 51Pro V2.0,然后将2051烧写器给上电源。在PC软件上先择AT89C2051,点击检测器件,我的,1e 21 FF。然后试着打开一个HEX文件,点自动完成,它就一路完成器件的检测、擦除、写入,校验等工作,如下图:
如果你的器件没找到,但前面的串口通迅等调试是正常的,那么你最后一步的那些接线可能有误。仔细检查你的接线,及C2051座的电源、接地是否正常。
好了,掸一掸尘土,大功告成。终于可以闪到一边喝口茶凉快会儿去了!也给点掌声啊!容易嘛?我。。。!
总结:养成逐步调试的习惯,对电路的一次成功是非常有益的。有了问题对除错的范围也可以缩至最小。
21、时钟IC_DS1302的应用之一……基础知识 在网上看了很久,发现初学者最有兴趣的就是DS1302时钟电路,也很自然,它是个做出来就让你觉得最实用的电路了,但实际上制做上并不简单,首先你要让你的显示部分(不管是数码管还是LCD)调试通过。然后把DS1302接好,调试正确了才能在成功显示时间和日期。下面我们就来说说DS1302的用法。
DS1302的图如下:
DS1302是美国DALLAS公司推出的一种高性能、低功耗的实时时钟芯片,附加31字节静态RAM,采用SPI三线接口与CPU进行同步通信,并可采用突发方式一次传送多个字节的时钟信号和RAM数据。实时时钟可提供秒、分、时、日、星期、月和年,一个月小与31天时可以自动调整,且具有闰年补偿功能。工作电压宽达2.5~5.5V。采用双电源供电(主电源和备用电源),可设置备用电源充电方式,提供了对后背电源进行涓细电流充电的能力。
下面是标准的接线电路图:
各引脚功能如下:
引脚号 名称 功能
① Vcc2 主电源
②、③ X1,X2 接32768Hz晶振
④ GND 地线
⑤ RST 复位
⑥ I/0 数据输入输出
⑦ SCLK 串行时钟
⑧ Vccl 后备电源
DS1302有关日历、时间的寄存器共有12个,其中有7个寄存器(读时81h~8Dh,写时80h~8Ch)是存放秒、分,小时、日、月、年、周数据的,存放的数据格式为BCD码形式
它的内部时间寄存器如下:
这张表呢是DS1302内部的7个与时间、日期有关的寄存器图和一个写保护寄存器,我们要做的就是将初始设置的时间、日期数据写入这几个寄存器,然后再不断地读取这几个寄存器来获取实时时间和日期。这几个寄存器的说明如下:
1、秒寄存器(81h、80h)的位7定义为时钟暂停标志(CH)。当初始上电时该位置为1,时钟振荡器停止,DS1302处于低功耗状态;只有将秒寄存器的该位置改写为0时,时钟才能开始运行。
2、小时寄存器(85h、84h)的位7用于定义DS1302是运行于12小时模式还是24小时模式。当为高时,选择12小时模式。在12小时模式时,位5是 ,当为1时,表示PM。在24小时模式时,位5是第二个10小时位
3、控制寄存器(8Fh、8Eh)的位7是写保护位(WP),其它7位均置为0。在任何的对时钟和RAM的写操作之前,WP位必须为0。当WP位为1时,写保护位防止对任一寄存器的写操作。也就是说在电路上电的初始态WP是1,这时是不能改写上面任何一个时间寄存器的,只有首先将WP改写为0,才能进行其它寄存器的写操作。
下面来说说如果对DS1302进行读写:
上面的电路图可以看出,除了电源和接地,DS1302只有三根线和单片机连接,SCLK、I/O和RST(有的也写成CE),先看时序图:
DS1302的数据读写是通过I/O串行进行的。当进行一次读写操作时最少得读写两个字节,第一个字节是控制字节,就是一个命令,告诉DS1302是读还是写操作,是对RAM还是对CLOK寄存器操作,以及操作的地址。第二个字节就是要读或写的数据了。
我们先看单字节写:在进行操作之前先得将CE(也可说是RST)置高电平,然后单片机将控制字的位0放到I/O上,当I/O的数据稳定后,将SCLK置高电平,DS1302检测到SCLK的上升沿后就将I/O上的数据读取,然后单片机将SCLK置为低电平,再将控制字的位1放到I/O上,如此反复,将一个字节控制字的8个位传给DS1302。接下来就是传一个字节的数据给DS1302,当传完数据后,单片机将CE置为低电平,操作结束。
单字节读操作的一开始写控制字的过程和上面的单字节写操作是一样,但是单字节读操作在写控制字的最后一个位,SCLK还在高电平时,DS1302就将数据放到I/O上,单片机将SCLK置为低电平后数据锁存,单机机就可以读取I/O上的数据。如此反复,将一个字节的数据读入单片机。读与写操作的不同就在于,写操作是在SCLK低电平时单片机将数据放到IO上,当SCLK上升沿时,DS1302读取。而读操作是在SCLK高电平时DS1302放数据到IO上,将SCLK置为低电平后,单片机就可从IO上读取数据。
现在我们来看看控制字的内容:
位0就是读写位,当位0为1时,就是告诉DS1302,下面是进行读出操作,而当位0为0时就是写入操作。
位0-位5是要进行操作的DS1302寄存器地址。
位6就是告诉DS1302,是要对RAM进行操作还是对CLK寄存器进行操作,0就是对时间寄存器操作,一般我们都是对时间寄存器进行操作。
位7就是固定的1。为什么是1呢。还记得上面说的单字节读操作吗?在写控制字的最后一个位也就是位7时,DS1302已将它的寄存器数据位0放到IO上了,要是控制字的位7是0的话,DS1302就无法将它的随后的数据放到IO上了。
这样你现在就知道为什么控制字80H是写秒寄存器,而80H是读秒寄存器了吧!80H换成二进制就是10000000。而81H的二进制就是10000001,一个是写操作,另一个是读操作嘛!
好!我们现在来总结一下,如何对DS1302进行操作。
①首先要通过8eH将写保护去掉,这样我们才能将日期,时间的初值写时各个寄存器。
②然后就可以对80H、82H、84H、86H、88H、8AH、8CH进行初值的写入。同时也通过秒寄存器将位7的CH值改成0,这样DS1302就开始走时运行了。
③将写保护寄存器再写为80H,防止误改写寄存器的值。
④不断读取80H-8CH的值,将它们格式化后显示到LCD或数码管上。
22、时钟IC_DS1302应用之二……实际制做 这个电路本身还是很简单的,见下图:
元件就是DS1302、一个晶振,一个104的瓷片电源滤波电容。和98ATS52连接的线有电源5V(Vcc)、接地(GND)、SCLK、I/O、CE(RST)。共5根线。
按上面的电路图焊好后别忘了检测一直是否有短路,我检测了我的电路,电源和地之间的静态电流为50uA。
接上单片机后如图:
软件部分的编写,首先得把显示部分调试好,我用的是LCD12864,是已经调试好的可调用子程序12863put.c
下面DS1302程序部分,是根据网上用的非常多的一个DS1302子程序修改的:
/**************************/
/* ds1302实时时钟C程序 */
/**************************/
#include < reg52.h>
#define uchar unsigned char
***it T_CLK = P1^0; /*实时时钟时钟线引脚 */
***it T_IO = P1^1; /*实时时钟数据线引脚 */
***it T_RST = P1^2; /*实时时钟复位线引脚 */
***it ACC0=ACC^0;
***it ACC7=ACC^7;
void Init1302(void);
void v_W1302(uchar ucAddr, uchar ucDa);
uchar uc_R1302(uchar ucAddr);
void v_Set1302(uchar *pSecDa);
void v_Get1302(uchar ucCurtime[]);
/******************************************
* 名称: v_RTInputByte
* 说明:
* 功能: 往DS1302写入1Byte数据
* 调用:
* 输入: ucDa 写入的数据
* 返回值: 无
******************************************/
void v_WTInputByte(uchar ucDa)
{
uchar i;
ACC= ucDa;
for(i=8; i>0; i--)
{
T_IO = ACC0; //相当于汇编中的 RRC
T_CLK = 1;
T_CLK = 0;
ACC =ACC>> 1;
}
}
/*****************************************
* 名称: uchar uc_RTOutputByte
* 说明:
* 功能: 从DS1302读取1Byte数据
* 调用:
* 输入:
* 返回值: ACC
******************************************/
uchar uc_RTOutputByte(void)
{
uchar i;
for(i=8; i>0; i--)
{
ACC = ACC>>1; //相当于汇编中的 RRC
ACC7 = T_IO;
T_CLK = 1;
T_CLK = 0;
}
return(ACC);
}
/********************************************
* 名称: v_W1302
* 说明: 先写地址,后写数据
* 功能: 往DS1302写入数据
* 调用: v_RTInputByte()
* 输入: ucAddr: 控制字, ucDa: 要写的数据
* 返回值: 无
*********************************************/
void v_W1302(uchar ucAddr, uchar ucDa)
{
//OE=0;
T_RST = 0;
T_CLK = 0;
T_RST = 1;
v_WTInputByte(ucAddr); /* 地址,命令 */
v_WTInputByte(ucDa); /* 写1Byte数据*/
T_CLK = 1;
T_RST =0;
//OE=1;
}
/*********************************************
* 名称: uc_R1302
* 说明: 先写地址,后读数据
* 功能: 读取DS1302某地址的数据
* 调用: v_RTInputByte() , uc_RTOutputByte()
* 输入: ucAddr: 控制字
* 返回值: ucDa :读取的数据
**********************************************/
uchar uc_R1302(uchar ucAddr)
{
uchar ucDa;
//OE=0;
T_RST = 0;
T_CLK = 0;
T_RST = 1;
v_WTInputByte(ucAddr); /* 地址,命令 */
ucDa = uc_RTOutputByte(); /* 读1Byte数据 */
T_CLK = 1;
T_RST =0;
// OE=1;
return(ucDa);
}
/***************************************
*
* 名称: v_Set1302
* 说明:
* 功能: 设置初始时间
* 调用: v_W1302()
* 输入: pSecDa: 初始时间数组首地址。
* 返回值: 无
****************************************/
void v_Set1302(uchar *pSecDa)
{
uchar i;
uchar ucAddr = 0x80;
v_W1302(0x8e,0x00); // 控制命令,WP=0,允许写操作
for(i =7;i>0;i--)
{
v_W1302(ucAddr,*pSecDa); //秒 分 时 日 月 星期 年
pSecDa++;
ucAddr +=2;
}
v_W1302(0x8e,0x80); // 控制命令,WP=1,写保护
}
/**********************************************
* 名称: v_Get1302
* 说明:
* 功能: 读取DS1302当前时间
* 调用: uc_R1302()
* 输入: ucCurtime: 保存当前时间数据的数组地址
* 返回值: 无
***********************************************/
void v_Get1302(uchar ucCurtime[])
{
uchar i;
uchar ucAddr = 0x81;
for (i=0;i<7;i++)
{
ucCurtime = uc_R1302(ucAddr);//格式为: 秒 分 时 日 月 星期 年
ucAddr += 2;
}
}
/*******************************************
* 名称: Init1302
* 说明:
* 功能: 初始化DS1302
* 调用:
* 输入:
* 返回值: 无
*******************************************/
void Init1302(void)
{
v_W1302(0x8e,0x00); //控制写入WP=0
v_W1302(0x90,0xa5); //辅助电源充电命令
v_W1302(0x80,0x00); //写秒
v_W1302(0x82,0x59); //写分
v_W1302(0x84,0x10); //写时
v_W1302(0x86,0x07); //写日
v_W1302(0x88,0x05); //写月
v_W1302(0x8a,0x04); //写星期
v_W1302(0x8c,0x09); //写年
v_W1302(0x8e,0x80); //写保护WP=1
}
下面是主程序部分DS1302main.c,将DS1302、LCD12864子程序整合在一起。
#include
#define uchar unsigned char
#define uint unsigned int
extern void LcmClear( void ); //清屏
extern void LcmInit( void ); //初始化
extern void LcmPutstr( uchar row,uchar y,uchar * str ); //在设定位置显示字符串
extern void Init1302(void);//初始化DS1302
extern void v_Get1302(uchar ucCurtime[]);//获取DS1302内的7个字节时间数据存入数组中
uchar getTimebuf[7];//用于存放获取的时间数据
uchar setTimebuf[7];//用于存放要设置的时间日期数据
uchar time[]={" : : "};//时间格式字符串
uchar date[]={"20 - - "};//日期格式字符串
uchar daylist[]={"SunMonTueWedThuFriSat"};//星期字符列表
uchar day1[]={" "};//星期格式字符串
//****************
// 主函数
//****************
void Main( void )
{ uchar i,j,k;
Init1302();//初始化DS1302
LcmInit(); //初始化LCD12864
LcmClear();
LcmPutstr( 0,28,"DS1302 TEST" );
LcmPutstr( 3,24,"");
LcmPutstr( 5,0,"BLOG:http://" );
LcmPutstr( 6,18,"hi.baidu.com/txz01" );
LcmPutstr( 7,8,"Email:TXZ001@139.com" );
while(1)
{
v_Get1302(getTimebuf);//获取DS1302内7个时间日期数据存入数组getTimebuf[].
time[6]=(getTimebuf[0])/16+48;//格式化时间秒
time[7]=(getTimebuf[0])%16+48;
time[3]=(getTimebuf[1])/16+48;//格式化时间分
time[4]=(getTimebuf[1])%16+48;
time[0]=(getTimebuf[2])/16+48;//格式化时间小时
time[1]=(getTimebuf[2])%16+48;
date[8]=getTimebuf[3]/16+48;//格式化日期日
date[9]=getTimebuf[3]%16+48;
date[5]=getTimebuf[4]/16+48;//格式化日期月
date[6]=getTimebuf[4]%16+48;
date[2]=getTimebuf[6]/16+48;//格式化日期年
date[3]=getTimebuf[6]%16+48;
day1[0]=daylist[(getTimebuf[5]%10)*3];//格式化星期
day1[1]=daylist[(getTimebuf[5]%10)*3+1];
day1[2]=daylist[(getTimebuf[5]%10)*3+2];
LcmPutstr( 2,0,date);//显示日期
LcmPutstr(2,96,day1);//显示星期
LcmPutstr( 3,36,time);//显示时间
for(i=0;i<5;i++)
for(j=0;j<255;j++)
for(k=0;k<255;k++);
}
}
用Keil将程序编译:
生成HEX文件后下载到AT89S52板上,运行,如下图:
结果并不好,时间数据在乱跳,但我发现秒数基本是一秒一跳,分钟也是一分一跳的,就是说DS1302在正常工作,说明CH修改已正常,只是读取数据不正常。在分析了软件部分都正常后,决定将数据线缩短。因为原来的数据线大约有10多CM。再加上从接口到52芯片的转接线总共大约有30CM长了。因此得将DS1302的数据线剪短,再直接插在52芯片的接口P1上试试。如下图:
上面这张图是示意图,是晚上补做的图,没插电。其结果是基本上能看出是在我设定的时间初值上每秒跳一下,但还是很频繁地有乱跳现象。我于是就想那么短的线难道数据还不稳?莫非是阻抗太高了,使数据线容易受到干扰?我想起那个标准电路中三路数据线是有上拉10K的电阻的,当时我想反正常P1内部也有上拉电阻,就没外接电阻了。现在想起来是不是P1内部的上拉电阻值太大了,使读取数据受外界干扰。我想起我P0上外接的不就是10K的排阻吗?于是将DS1302的数据线插到了P0口的P0.0、P0.1、P0.2上,在软件里修改了数据线接口的定义,然后烧写运行。哈哈!现在就非常稳定了。
总结,其实DS1302的数据引线略长一点是没关系的,但那三根数据线上的上拉电阻一定得要,否则阻抗太高就很容易造成数据传输不稳定,受到外界干扰,造成显示数据乱跳的现象。
DS1302的调试不是很容易,因为当你做好了电路,写好了软件烧写运行后,如果DS1302没有任何反应。你就不太好判断问题出在哪,是晶振不起振还是程序改写秒寄存器CH没成功?我就因此换过晶振,换过DS1302,反复修改过程序。因为DS1302在初始接上电源时晶振是不起振的,就无法检测晶振电路的好坏。这是这个电路的难点。
边学边秀单片机1:https://bbs.elecfans.com/jishu_286355_1_1.html
边学边秀单片机2:https://bbs.elecfans.com/jishu_286362_1_1.html
边学边秀单片机3:https://bbs.elecfans.com/jishu_286363_1_1.html
边学边秀单片机4:https://bbs.elecfans.com/jishu_286367_1_1.html
边学边秀单片机5:https://bbs.elecfans.com/jishu_286368_1_1.html
边学边秀单片机6:https://bbs.elecfans.com/jishu_286369_1_1.html
边学边秀单片机7:https://bbs.elecfans.com/jishu_286370_1_1.html
边学边秀单片机8:https://bbs.elecfans.com/jishu_286371_1_1.html
|