|
用C196编译器语言开发196系列单片机的代码
|
Developing Codes in 196 Series Microcontrollers
Using C196 Complier
|
■珠海市阿尔法有限公司 李天华
|
随着单片机应用的发展,单片机配套的C语言编译器也用的越来越普遍。进入中国市场较早的Intel MCS-51系列单片机,目前第三方的C语言开发工具C51已用的很多,而同样进入中国市场较早的MCS-96系列单片机,虽然目前用的不如8位机较普遍,但也占据了相当一部分市场,其优越的性能,不是8位机可以比拟的。笔者想就最近用C196的一些经验对它作一个介绍,其中涉及到和C51不一样的地方,也作了一些对比。
目前市场上见到的196系列单片机的仿真器大多都支持Tasking Software Inc的C196 V5.0r3的编译格式,因此本文主要以这个版本为基础介绍一下C196的特点。
C196的语言特点
C196的C语言按ANSI C来定义,它的数据也遵从标准C的数据结构。
C196的数据结构是以数据类型的形式出现的。C196编译器支持的数据类型有无符号字符(unsigned char)、有符号字符(signed
char)、无符号整形(unsigned int)、有符号整形(signed int)、无符号长整型(unsigned long)、有符号长整型(signed
long)、浮点(float)和指针类型等。和C51不同的是,由于196系列单片机不能位寻址,故C196也就没有位类型(bit)的扩展。
C196的C语言变量分为局部变量和全局变量,所有变量在使用前必须先定义后使用。局部变量在函数内部说明。当程序退出时,局部变量占用的空间释放,局部变量也就失去意义;全局变量则是在任何函数之外说明的、可被任意模块使用的、在整个程序执行期间都保持有效的变量。C196的变量类型与数据类型是相同的,可以在变量前加上修饰符,如:int、char、float等。
同C51一样,C196编译器对局部变量及传递参数使用RAM覆盖技术,其内部RAM的利用效率非常高。
寄存器分配
寄存器分配如下所示:
专用寄存器(SFRs):片内存储单元00H-17H是I/O控制寄存器或其它外设专用寄存器。根据单片机型号的不同,还有其他一部分专用寄存器定位在空间1F00H-1FFFH,具体的专用寄存器定位空间在寄存器目标文件xx_sfrs.obj中定义,而在头文件xx_sfrs.h声明,这一点是和C51不一样的地方。
堆栈指针寄存器(SP):存储当前堆栈的栈底地址。需要注意的是,196系列单片机的堆栈是向下排列的,而51系列单片机的堆栈是向上排列的。
临时寄存器(Temporary registers)TMPREG0(或TMPREG8):在缺省情况下,编译器将地址1CH-23H作为临时寄存器变量TMPREG0使用。它是双长字类型变量,缺省变量属性为NULL,因而可以和任何类型的变量和函数匹配。它相当于51系列中的累加器和R0-R7寄存器,用于中间计算结果存放或放置有返回值函数的返回值。当然也可以根据需要通过设置编译控制选项extratmp来设定允许使用额外的临时寄存器TMPREG8(缺省分配地址到2BH)或设置编译控制选项tmpreg,将临时寄存器定位在其他存储空间。
通用寄存器:24H-0FFH为内部通用寄存器,用于分配全局变量和函数内部的局部变量,可以直接访问或间接访问。根据单片机型号的不同,有些单片机还有0100H-01FFH共256个字节(或更多)的附加寄存器。这部分寄存器只能间接访问或通过窗口方式访问。
帧指针寄存器(Frame Pointer):编译时分配给FRAME01临时变量,主要使用在函数内部的临时变量。
内部特殊功能寄存器的访问
C196编译器要求对内部特殊功能寄存器访问前应先定义后访问,对特殊功能寄存器的定义有两种方法。一种方法是在程序中直接定义,将寄存器定义为可变变量(volidata),并用编译控制项#pragma
locate定义出寄存器的实际内存位置,如下:
volatile unsigned char int_mask;
#pragma locate(int_mask=0x08)(注意这是预处理控制命令,后面不加分号)
这样定义完以后,在程序中就可以直接引用这个变量。这种方法虽然比较直接,但有时用到的寄存器比较多,程序就会变得非常庞大,也不利于模块化。另一种方法是把这些寄存器定义在一个头文件中,并且声明这一变量为外部类型(external),然后在库文件或一个目标文件中定义地址,并用预处理命令#include将这个头文件包含在自己的程序中即可。当然,一般情况下,程序员都不用自己去定义这些头文件。由于不同的单片机内部使用的专用寄存器不同,编译器都带有对应不同单片机的寄存器头文件xx_sfrs.h和相应的目标文件xx_sfrs.obj,其中xx为单片机类型。
中断处理过程
用单片机做开发时,中断过程处理是系统开发的一个重要问题,196系列单片机也不例外。由于C196编译器支持用C语言直接开发中断过程,所以减轻了用汇编语言开发中断过程的繁琐工作,提高了开发系统的效率。C196编译器用编译控制项interrupt来声明一个函数为中断函数。其格式为:
#pragma interrupt(function[=n])。
function为中断函数名,n为中断号或中断矢量地址。中断函数必须是无返回值类型的(void),并且不能带参数。中断定义声明必须放在函数定义之前,中断控制编译控制选项会控制编译器自动产生用于保存和恢复寄存器所需要的特别的汇编程序代码。编译器对每个中断函数产生一个中断矢量入口,对8096系列矢量号为0-7,对80196系列矢量号为0-9和24-31。中断号和中断矢量地址是一一对应的,可以由计算的方法由中断号确定中断矢量表,即为中断矢量的基地址(为2000H)加上中断号乘以2,相应的中断矢量首地址为2000H-2012H和2030H-203EH。当然,对于不同的96系列单片机,其每个中断号所对应的中断函数功能是不一样的。
需要说明的是,C中断函数调用产生的代码要比一般的非中断函数多。首先,C编译器要产生代码用于保存程序状态字(PSW),而且由于所有的中断函数被认为是可重入的,所以编译器还要产生代码保存和恢复被中断函数使用的寄存器。而如果在中断函数中调用其他函数时,编译器则要将所有用到的临时寄存器都产生代码压栈和出栈。这样更加大了中断资源的开销,因此最好不要在中断函数中再调用其他函数。而51系列单片机在中断函数中可以使用寄存器组切换(usingr)来高效使用内部寄存器,而当在中断函数中调用其他函数时,要求被调用的函数所使用的寄存器组与中断函数相同就可以了,因此在这一点上,C196编译器所产生的代码要比C51的多出一部分压栈和出栈代码。
PTS中断控制处理
C196编译器支持用C语言直接开发PTS中断处理。C196编译器用编译控制项pts来声明一个PTS控制块。其格式为:
#pragma pts(struct_name=vector)
其中struct_name为定义的控制块名称,vector为PTS中断矢量或中断矢量地址。用#pragma locate 可以将PTS控制块装载到PTS矢量地址中。用locate定义PTS控制块在内部RAM空间中,这一地址要求能被8整除,tasking公司的C196编译器在xx_funs.h头文件中对各种PTS控制块的类型作了定义。例如:下面为一个PTS块传递初始化定义:
#definePTS_BLOCK_BASE 0x01a0 //定义PTS控制块首地址
BlockTran_ptscb Block_CB_4;//定义一个PTS控制块结构
#pragmalocate(Block_CB_4= PTS_BLOCK_BASE)//将PTS控制块定义在地址0x100
#pragma pts(Block_CB_4=4) //定义PTS中断矢量号
函数调用时参数的传递
用过C51的都知道,C51是通过R0-R7寄存器传递形式参数和返回值,而C196编译器则采用堆栈传递形式参数和返回值。如果传递参数较多,相应堆栈深度就加大。
从实际例子的汇编代码可以看出,参数传递时进栈及出栈占用了大量的代码和执行时间,因此在用C196编写程序时,对执行时间有要求的地方,应尽量减少子程序的调用及嵌套。
C196语言和汇编语言的混合编程
一般情况下,主程序都是用C语言编写,C语言与汇编语言最大的区别就在于汇编程序执行效率较高些。因为C语言首先要用C编译器生成汇编代码,在不少情况下,C编译器生成的汇编代码不如用手工生成的汇编代码效率高。在C196中,可以用两种办法在C程序中调用汇编程序,一是在C语言中直接嵌入汇编代码,如下所示:
void init_interrupt(void)//初始化中断屏蔽字
{
asm
{
DI;
DPTS;
}
int_mask &= 0;
int_mask1 &= 0;
int_pend &= 0;
int_pend1 &= 0;
wsr = 0x3e;
pi_mask_3e = 0;
}
这种嵌入汇编不是完整意义上的汇编,是一种伪汇编指令,仅支持部分汇编代码。例如,这种伪汇编不支持条件汇编,不支持定位指令(cseg)和符号定义指令(equ)。在汇编中,16进制必须采用C语言的格式,例如16必须用0x10表示。而且在汇编中也不能定义新的变量,每一条汇编语句都要加分号结尾。
另一种方法是将汇编作为一个独立的模块,用汇编编译器生成目标文件,然后用连接器和C语言生成的其它模块的目标文件连接在一起。如果变量要公用时,则在另一个模块中说明为外部类型。这种方法比较麻烦一些,但是如果程序员对某一模块的执行效率要求较高时,可以采取这种办法。下面是一个C语言调用汇编语言的例子。
C语言程序为:
unsigned int temp1;
extern void delay1(void);
void main(void)
{
temp1 = 0x10;
delay1(); //调用汇编模块
}
被调用的汇编语言程序为:
EXTRN temp1
PUBLIC delay1
rseg
temp0: dsw 1
num: dsw 1
cseg
delay1:
ld num,temp1
s1:
clr temp0
sjmp s3
s2:
inc temp0
s3: cmp temp0,#0940h
jnc s2
djnz num,s1
ret
end
从上面的例子可以看出,被调用的delay1函数在汇编模块中必须说明为公共变量(PUBLIC),而在调用它的C语言模块中必须说明为外部函数(extern)。同样在汇编模块中使用由C语言定义的全局变量temp1时,必须先声明为外部变量(EXTRN)。
编译连接
C196在编译连接时,其控制参数的正确设置非常重要。需要注意的是,在编译时,应根据所用单片机型号的不同,采用编译控制项#pragma
model(xx)。其中,xx为单片机型号,如kb、kc、mc等。在连接时需要将对应的单片机的特殊寄存器目标文件xx_sfrs.obj和启动目标文件cstart.obj连接进去。
注意事项
使用C196时,为了更有效的利用资源,应注意以下几点:
(1)尽量使用无符号数和字节变量。
(2)尽量使用局部变量。编译器可自动将某些局部变量声明为寄存器变量,并且采用静态覆盖技术,大大提高了使用效率。
(3)在寄存器资源允许的情况下,对某些对执行效率要求较高的平级无相互调用函数中用到的内部变量,可将其定义为全局临时变量,编程时覆盖使用,这样可减少很多编译代码。而对于中断函数内部用到的变量,可用全局变量。
(4)应经常观看经正确编译连接后产生的映象文件(.M96文件),在该文件中详细列出了分配给变量和代码的地址和生成代码的大小等信息。使用者可了解代码是否优化,变量分配是否合理,堆栈是否溢出等。
(5)对于有一定汇编经验的人在开始使用C196时,应多注意观看经编译后产生的汇编源代码,从而写出高效简洁的C源代码。
结论
经过作者在开发项目过程中的实践,C196编译器产生的代码在有些时候虽然比较繁琐,但结构和逻辑性很强,开发效率大大提高,调试与维护都很方便。不论是从程序的开发速度、软件质量还是从程序的可维护性和可移植性上讲,C196的优点绝非汇编语言所能比拟的。
|
|