在keil C语言与汇编语言的混合编程中曾经遇到过的一些问题,写下来留作以后参考。
1、C语言中加入汇编语言模块的方法:
例子:
void func()
{
C语言代码……
#pragma asm
MOV R6,#23
DELAY2: MOV R7,#191
DELAY1: DJNZ R7,DELAY1
DJNZ R6,DELAY2
RET
#pragma endasm
C语言代码……
}
其中红色为C语言部分,绿色为嵌入的汇编语言部分。汇编部分需要用#pragma asm和#pragma endasm包起来
2、Keil提示“asm/endasm”出错的解决方法
如果只是像1中那样直接加入汇编代码的话,编译将会报错,错误如下:
compiling sendata.c...
sendata.c(81): error C272: 'asm/endasm' requires src-control to be active
sendata.c(87): error C272: 'asm/endasm' requires src-control to be active
Target not created
解决方法如下:
首先右键单击包含有汇编部分的c语言文件名,然后在如上图所示的菜单中选择带有红色方框的选项
在弹出的对话框中,将上图中红色方框选中的两项打上勾(默认的情况下,前面的勾是灰色的,要让这两项前的勾变为黑色的),点击确定。
3、?C_START等相关警告的处理
按照2中的方法处理完之后,再编译不会出现错误信息了,但是会出现如下的警告信息:
linking...
*** WARNING L1: UNRESOLVED EXTERNAL SYMBOL
SYMBOL: ?C_START
MODULE: STARTUP.obj (?C_STARTUP)
*** WARNING L2: REFERENCE MADE TO UNRESOLVED EXTERNAL
SYMBOL: ?C_START
MODULE: STARTUP.obj (?C_STARTUP)
ADDRESS: 000DH
处理方法如下:
在如上图所示的“Source Group 1”上点右键,在菜单中选择 “Add Files to Group 'Source Group 1' ”
找到你的KEIL安装目录,选择其中的“C51”目录下的“LIB”目录下的“C51S.LIB”文件,点击Add,然后Close即可。
注意,上图所示的文件选择框进入LIB目录下后,默认只显示.c文件,需要在“文件类型”中选择“Library file (*.lib)”,即可显示LIB文件了。
添加C51S.LIB到工程后,再次编译,警告信息消失。
linking...
Program Size: data=9.0 xdata=0 code=28
creating hex file from "sendata"...
"sendata" - 0 Error(s), 0 Warning(s).
4、寄存器冲突问题的解决
汇编程序块中常常会使用到51的通用寄存器,比如R0-R7。这种情况下可能会和C语言程序中已经使用到的R0-R7产生冲突,以至于产生一些非常隐蔽和古怪的错误。虽然有人说KEIL可以自动分配寄存器组使之不产生冲突。但是在我这里具体测试时还是有冲突的(或者是我的KEIL设置有问题?),下面是测试小程序:
/*************************************************************
*测试程序
*************************************************************/
#include
#include
typedefunsignedcharuint8;
uint8buf[16]=
{
0x55
};
uint8i=0;
/************************************************************
*初始化单片机相关寄存器
***********************************************************/
voidUartInit()
{
SCON=0x50;
TMOD|=0x21;
PCON|=0x80;
TH1=0xE8;
TL1=0xE8;
IE|=0x90;
TR1=1;
}
/**************************************************
*延时
***************************************************/
voiddelay()
{
#pragmaasm
MOVR6,#19
DELAY2: MOVR7,#18
DELAY1: DJNZR7,DELAY1
DJNZR6,DELAY2
RET
#pragmaendasm
}
/**********************************************
*向COM1发送一个字符
**********************************************/
voidSendChar(uint8byteToSend)
{
SBUF=byteToSend;
while(!TI);
TI=0;
}
/************************************************************
*读取一个字节
***********************************************************/
uint8read_byte()
{
uint8recvdata=8;
delay();//延时
returnrecvdata;
}
/**************************************************
*主程序
***************************************************/
intmain()
{
UartInit();//串口初始化
while(1)
{
buf[i++]=read_byte();
}
}
/**************************************************
*串口中断处理
***************************************************/
voidchuankou()interrupt4
{
if(RI)
for(i=0;i<sizeof(buf);i++)
{
SendChar(buf);
}
RI=0;
}
程序本身非常简单,一目了然。main函数的作用就是不断把read_byte()的返回值读入buf中,再待有串口中断时,将buf中的内容输出到串口。read_byte()函数也已经做了简化,返回值固定为8。
将这个程序在KEIL中编译,下载运行。奇怪的情况出现了,buf中的内容输出到串口总是0(输出理所当然应该是8)。
把delay();这一句注释掉后,程序就输出正常了。
delay();不过起到了一个延时作用而已,怎么可能改变到函数返回值呢?
看看read_byte()函数生成的汇编程序
USING 0
MOV R7,#08H
ACALL delay
RET
才晓得,原来read_byte()函数将返回值放到R7中,然后调用delay函数,再返回。后面的程序调用read_byte()返回值时,直接从R7中取数。
但是“ACALL delay”时,已经存放了#08H的R7,在延时中被递减到了0,这也是为什么buf中存放的内容都是0的缘故。
知道了原因就好办一些了,由于本人是菜鸟,单片机水平很有限,目前只想到了如下4种解决方法:
(1)避开C语言部分已经使用了的Rn
编汇编模块时,看看C语言部分生成的汇编程序,把那些C语言已经使用到的,且可能对汇编部分构成冲突的Rn避开就好了。比如上面的延时程序中,把R6、R7换成R3、R4,程序就正常了。
(2)用USING X + ARX的方式调用其他组Rn寄存器
51单片机有4组R0-R7的寄存器,据说main中一般使用的都是第0组。那么在汇编部分中使用其他的组就可以了,delay函数的内容可以改成如下这样:
voiddelay()
{
#pragmaasm
USING 2
MOVAR6,#19
DELAY2: MOVAR7,#18
DELAY1: DJNZAR7,DELAY1
DJNZAR6,DELAY2
RET
#pragmaendasm
}
但是注意,如果这样用的话,delay延时的长度可能需要重新计算。因为AR6和R6有所不同,以下是在网上找到的两段英文描述:
R0–R7Theeight8bitgeneralpurpose8051registersinthecurrentlyactiveregisterbank.
AMaximumoffourregisterbanksareavailable.
AR0–AR7RepresenttheabsolutedataaddressesofR0throughR7inthecurrentregisterbank.
Theabsoluteaddressfortheseregisterschangesdependingontheregisterbankthatiscurrentlyselected.
ThesesymbolsareonlyavailablewhentheUSINGassemblerstatementisgiven.
RefertotheUSINGassemblerstatementformoreinformationonselectingtheregisterbank.
TheserepresentationsaresuppressedbytheNOAREGSdirectiveofftheCx51compiler.
看这意思,似乎R0–R7是寄存器,而AR0–AR7是地址,所以“MOVAR6,#19”和“MOVR6,#19”所花的时钟周期数是不同的。我调一个采集数据的程序时,刚开始没有注意到这个问题,因此很奇怪为什么用AR6和R6,采集到的数据是不一样的,后来才反应过来它们延时不同。(关于51单片机的各条指令的时钟周期数可以在百度上搜到很多,这里就不列出了)
(3)对汇编部分中使用到的寄存器采用入栈保护
简单的说就是在汇编部分使用Rn之前,将它们的内容塞进栈中存起来,延时循环结束之后再从栈中取出来重新赋给它们。
比如上面的delay程序,可以改成这样:
voiddelay()
{
#pragmaasm
MOVA,R6
PUSHACC
MOVA,R7
PUSHACC
MOVR6,#19
DELAY2:MOVR7,#18
DELAY1: DJNZR7,DELAY1
DJNZR6,DELAY2
POPACC
MOVR7,A
POPACC
MOVR6,A
RET
#pragmaendasm
}
(4)使用RS0和RS1切换使用的寄存器组
51单片机中使用RS0和RS1来选择使用哪一组Rn,因此在汇编程序通过修改这两位的值,即可实现切换寄存器组。
上面的delay函数可修改为如下形式:
voiddelay()
{
#pragmaasm
SETBRS1
SETBRS0
MOVR6,#19
DELAY2:MOVR7,#18
DELAY1: DJNZR7,DELAY1
DJNZR6,DELAY2
CLRRS1
CLRRS0
RET
#pragmaendasm
}
进入汇编部分后选择采用第3组寄存器,退出汇编部分前更换回之前使用的第x组寄存器(在本程序中是第0组寄存器)
不过对于比较精细的延时的话,方法(3)和方法(4)这样或许会稍有些影响,毕竟增加了一些入栈出栈或者置位清零的语句。因此如果对延时精度要求较高,且采用方法(3)或(4)的话,计算延时的循环次数时,需要把入栈出栈或置位清零语句花费的时间也考虑进去。
另外,除上面四种方法之外,我还曾经尝试过以下方法,但是失败:
一种是在void delay()的函数定义后面加上 using 2,使之变成
void delay() using 2
{
................................
}
或者是在汇编中加上USING 2,使之变成
void delay()
{
#pragma asm
USING 2
................................
#pragma endasm
}
但是似乎都不行。。。