19、KEIL的混合编程操作
2009-03-01 17:44
这一篇来讲讲混合编程的问题,在网上了一下,讲混合编程的文件章也有不少,但进行实例操作讲解的不多也不完整,本来书上混合编程的内容看着就让人觉得抽象难懂,再没有个实际操作图例,就很让人觉得云里雾里。在这里我就针对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_pc\txz001\单片机c51\KEIL4\C51\BIN\C51.EXE' --- 错误: 不能执行 'E:\old_pc\txz001\单片机c51\KEIL2_70\Keil2\C51\BIN\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 <AT89X52.H> 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_pc\txz001\单片机c51\KEIL2_70\Keil\C51\BIN\C51.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:" ); LcmPutstr( 6,18,"hi.baidu/txz01" ); LcmPutstr( 7,8,"Email:TXZ001@139" ); 看见上面的程序了吗?我用了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 <AT89X52.H> #include <intrins.H> #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:" ); LcmPutstr( 6,18,"hi.baidu/txz01" ); LcmPutstr( 7,8,"Email:TXZ001@139" ); 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_pc\txz001\单片机c51\KEIL2_70\Keil\C51\BIN\C51.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中调用汇编函数。 修改于(2009.3.15) |
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论