Keil C51编译及连接技术

时间:2024-04-16 12:06:46

  主要介绍Keil C51的预处理方法如宏定义、常用的预处理指令及文件包含指令,C51编译库的选择及代码优化原理,C51与汇编混合编程的方法与实现以及超过64KB空间的地址分页方法的C51实现。

教学目标

  1.了解Keil C51的预处理方法,主要有宏定义#define、常用的预处理指令#define、#error、#if 、#else、#elif、#endif、#ifdef、#ifndef 、#undef 、#line 、#pragma 及文件包含指令#include。
  2.了解small、compact、large三种编译模式信代码优化方法。
  3. 掌握C51模块内部调用汇编程序的三种方法,理解C51模块与汇编模块之间的接口规则,主要有C51函数名与汇编程序名的转换规则、C51函数及其相关段的命名规则、C51函数的参数传递规则。
  4.理解Keil C51的Bankswitch原理,了解Keil C51的三种分页方式、公共空间的概念,掌握BL51有关分页的配置,以PSD813F2为硬件基础,掌握Keil C51分页的实现过程,能进行分页的程序设计。 
  一、预处理器
  1.1宏定义
宏定义格式如下:#define 名称 替换文字
宏是一种简单的替换,在程序中凡是出现“名称”之处均被“替换文字”替代,替换文字可以是数字,也可以是字符串。
  1.2预处理指令
由ANSI C的标准规定, 预处理指令主要包括: #define、#error、#if 、#else、#elif、#endif、#ifdef、#ifndef 、#undef 、#line 、#pragma。

  #define 指令
  例如:#define PI 3.1415926 凡是出现“PI”的地方均以“3.1415926”替换。宏的出现有助于提高程序的可读性及书写方便性,也有助于程序的调试。但是,出现在引号中的字符串是不能替换的。如printf(“PI\n”);该语句运行后输出的是:PI而不是3.1415926。

  define注意事项:
1)在宏定义语名后没有“;”;
2)在C51程序中习惯上用大写字符作为宏替换名,常放在程序开头;
3)宏定义还有一个特点,就是宏替换名可以带有形式参数,在程序中用到时,实际参数会代替这些形式参数。
例如: #define MAX(x, y) (x>y)?x:y

main()
{
    , j=;
    printf("The Maxmum is %d", MAX(i, j);
} 

上例宏定义语句的含义是用宏替换名MAX(x, y)代替x, y中较大者,同样也可定义:
#define MIN(x, y) (x<y)?x:y
表示用宏替换名MIN(x, y)代替x, y中较小者。
4)#define命令在程序之外,其有效范围为定义命令之后到源文件结束,但是可以用#undef命令终止宏定义的作用域。如:

#define PI 3.1415926
void main(void)
{
    ……
}
#undef PI

PI的范围从#define 开始到#undef PI 结束。

  #if、#else、#endif指令
#if、#els和#endif指令为条件编择指令, 它的一般形式为:

#if 常数表达式
    语句段;
#else
    语句段;
#endif 

上述结构的含义是: 若#if指令后的常数表达式为真, 则编译#if到#else 之间的程序段; 否则编译#else到#endif之间的程序段。
例如:

#define MAX 200
main()
{
    #if MAX>999
        printf("compiled for bigger\n");
    #else
        printf("compiled for small\n");
    #endif
} 

  #undef指令
#undef指令用来删除事先定义的宏定义, 其一般形式为:
#undef 宏替换名
例如:

#define TRUE 1
...
#undef TURE

#undef主要用来使宏替换名只限定在需要使用它们的程序段中。

  #include
文件包含是指用#include通常用来将一些常用的宏定义或变量定义所在的源文件包含到“#include”所在的文件中来。
通常格式如下:
#include“filename” 或 #include<filename>
在编译预处理时,将#include命令进行文件包含处理,即将filename中的全部内容复制插入到该指令处,减少重复劳动。通常引号包含的文件与现文件在同一个位置,否则用“<>”括起来。
在源文件开始处一般都要用一些#include指令,如

#include “stdio.h”
#include “reg51.h” 

如用户自定义了一个字库文件”kzk.h”,在源文件开始用#include “hzk.h”,之后,则可以在源程序中使用字库。

  二、C51编译库及代码优化技术

  2.1 C51编译库
  C51编译器包含6个不同的编译库,如表12-2-1可根据不同需要进行优化 , C51 编译器包含的库模块,都有源代码,对它们可作与硬件相关的修改。用户改变对于现有硬件输入和输出结构的两个模块,就可修改所有库函数,同样也可以重新很快地构造如“printf”和“puts”函数系统默认串口为输出设备,但用户可修改为LCD 显示。

Keil C51编译及连接技术

  2.2 代码优化
C51可将即使有经验的程序员编制的代码进行优化,用户可选6个优化级,C51的所有优化方法如下:
  1)一般优化:
常数折迭:发生在一个表达式或地址计算中的几个常数值组合为一个常数;
跳转优化:跳转转到最终的目标地址,以提高程序效率;
死码消除:不可执行代码(死码)可从程序中去掉;
寄存器变量:只要有可能,自动变量和参量放入寄存器中;
通过寄存器传递参数:寄存器中可传递最多三个参数;
全局公共子式消除:相同的子表达式或地址计算(多次发生在同一函数中)将被识别出来,并且只要有可能,将只计算一次。
  2)基于8051 的优化
窥孔(PEEPHOLE)优化:只要能节省存贮空间或执行时间,复杂的运算都将化简。
访问优化:常数和变量直接包含在操作中。
数据覆盖:函数的数据和位移被标记为OVERLAYABLE,被L51 用其它数据和位覆盖。
CASE/SWITCH 优化:SWITCH/CASE 语句优化为一个跳转或一串跳转。
  3)代码生成选项:
OPTMIZE(SIZE):共同的“C”操作被子程序代替:程序码长被压缩。
NOAREGS:不使用绝对寄存器访问,程序代码在这种方式下独立于寄存器组。
NOREGPARMS:参数传递总是在本数据段完成,程序代码与早期C51 版本兼容。

  三、C51与ASM混合编程技术
  3.1 混合编程意义
通常用C51来编写主程序。然而,在一些时序要求严格的采用汇编程序设计具有更高的效率,因此要求在C程序中调用一些用汇编语言编写的子程序。
另一方面,在以汇编语言为主体的程序开发过程中,如果涉及到复杂的数学运算,往往需要借助C51提供的运算库函数和强大的数据处理能力,这就要求在汇编中调用C51函数。
  3.2 C51模块内的汇编接口
模块内接口通常是指在C函数内部插入汇编代码,也称内嵌汇编语句 。
在线汇编提供了能直接读写硬件的能力,如读写中断控制允许寄存器等,但编译器并不检查和分析在线汇编语言,插入在线汇编语言改变汇编环境或可能改变C变量的值可能导致严重的错误。
  方法一、直接在函数体内的每个汇编语句前加?“asm”预编译指令。

void reset_data(void)
{
    asm    MOV R1,#0AH
    asm    LOOP: INC A
    asm    DJNZ R0,LOOP
    return;
} 

  方法二、把asm作为关键字后续汇编用大括号括起来即可。

void reset_data(void)
{
    asm
    {
        MOV R1,#0AH
        LOOP: INC A
        DJNZ R0,LOOP
    }
    return;
}

  方法三、在C模块内通过语句“#pragma”嵌入汇编代码。

void reset (void)
{
    #pragma asm
        MOV R1,#0AH
        LOOP: INC A
        DJNZ R0,LOOP
    #pragma endasm
    return;
}

  Keil C51编译环境相关设置
  将嵌有汇编语句源文件加入要编译的项目文件,将光标指向此文件,选择右键菜单“option?for?file?'asm.c'”,将属性单“properties”中的“Generate?Assembler?SRC?File”与“Assemble?SRC?File”两项设置成黑体的“√”,将“Link?Public?Only”的“√”去掉,再编译即可。用此方法可以在C51源代码的任意位置用#pragma?asm和#pragma?endasm嵌入汇编语句。如果编译时未用SRC指令,则C51中的汇编代码会被编译器忽略。

Keil C51编译及连接技术

3.3 C51与汇编的模块间接口
1、C51函数名与汇编程序名的转换规则
C51程序模块编译成目标文件后,其中的函数名依据其定义的性质不同会转换为不同的函数名,因此在C51和汇编程序的相互调用时要求汇编程序必须服从这种函数名的转换规则。C51中函数名转换规则如表12-3-1所列。

Keil C51编译及连接技术
2、C51函数及其相关段的命名规则
一个C51源程序模块被编译后,其中的每一个函数以“?PR?函数名?模块名”为名的命名规则被分配到一个独立的CODE段。
如:模块“FUNC51”内包含一个名为“func”的函数,则其CODE段的名字是“?PR?FUNC?FUNC51”。
如果一个函数包含有data和bit对象的局部变量,编译器将按“?函数名?BYTE和?函数名?BIT”命令规则建立一个data和bit段,它们代表所要传递参数的起始位置,其偏移值为零。这些段是公开的,因而它们的地址可被其它模块访问。另外,这些段被编译器赋予“OVERLAYABLE”标志,故可被L51连接/定位器作覆盖分析。
依赖于所使用的存储器模式,这些段的段名按表12-3-2所列规则命名,在相互调用时,汇编语言必须服从C51有关段名的命名规则。
各种存储器模式下函数相关段名的命名规则

Keil C51编译及连接技术
3、C51函数的参数传递规则
C51函数和汇编接口的关键在于C51函数的参数传递规则。
Keil C51具有特定的参数传递规则。
Keil C51函数最多可通过CPU寄存器传递三个参数,表12-2-3是利用寄存器传递参数的规则。
如果参数较多而使得寄存器不够用时,部分参数将在固定的存储区域内传送,这种混合的情况有时会令程序员在弄清每一个参数的传递方式时发生困难。
如果在源程序中选择了编译控制命令“#pragma NOREGPARMS”,则所有参数传递都发生在固定的存储区域,所使用的地址空间依赖于所选择的存储器模式。这种参数传递技术的优点是传递途径非常清晰,缺点是代码效率不高,速度较慢。
当函数具有返回值时,也需传递参数,这种返回值参数的传递均是通过CPU内部寄存器完成,其传递规则如表12-3-4所示。
Keil C51编译及连接技术
参数传递规则
参数传递规则的例子

func1(int a):“a”在R6/R7中传递;
func2(int a,int b,int *c):“a”和“b”在R6/R7和R4/R5中传递,“c”在R1/R2/R3中传递 ;
func3(long a,long b):“a”在R4/R5/R6/R7中传递,“b”在参数段中传递 ;  

SRC是一个十分有用的编译控制命令,它可令C51编译器将一个C源文件编译成一个相应的汇编源文件,而不是目标文件,在这个汇编文件中,我们可清楚地看到每一个参数的传递方法。
示例剖析

#include<reg51.h>
#define uchar unsigned char
uchar func(uchar x,uchar y);    /*函数func 原型声明*/
void main(void)                    /* 主函数 */
{
    func(,);                    /* 调用函数func */
}
uchar func(uchar x,uchar y)        /* 函数func */
{
    return (x/y);                /* 计算x/y并返回结果 */
} 

汇编结果

    RSEG  ?PR?_func?C12_2_1A  ; 寄存器传递参数
_func:    ; 函数func代码段起始
    USING     ;使用第0组寄存器
            ; SOURCE LINE # 8
;---- Variable 'y?141' assigned to Register 'R5' ---- ;输入参数y由R5传递
;---- Variable 'x?140' assigned to Register 'R7' ----;输入参数x由R7传递
; {
            ; SOURCE LINE # 9
; return (x/y);     /* 计算x/y并返回结果 */
            ; SOURCE LINE # 10
    MOV      A,R7
    MOV      B,R5
    DIV      AB
    MOV      R7,A                     ; 返回数据由R7传递
; }
            ; SOURCE LINE # 11
?C0002:
    RET
; END OF _func
END      

函数名func前有一个前缀字符“_”,这表明该函数采用寄存器传递参数,寄存器R7和R5用来传递参数,计算结果经R7返回。

3.4 应用实例
AD7705的写操作子程序,并给出C51调用格式。

Keil C51编译及连接技术

NAME   AD7705
    PUBLIC  _WRITE
   ?PR?WRITE?AD7705 SEGMENT CODE
   RSEG ?PR?WRITE?AD7705
_WRITE:MOV A,R7    ;;;指令由R7传入
      MOV R1,#08H
    ;;;SCLK_0
      NOP
      RLC A
      ,C  ;;;Din
      NOP
        ;;;SCLK_1
      NOP
      NOP
      DJNZ R1,LOOP01
      RET

在调用WRITE函数之前先声明该函数为为extern函数,格式如下:extern void WRITE(unisigned char);
C51调用该子程序的格式为 void WRITE(unsigned char command)

四、Keil C51环境下的分页技术
4.1 Keil C51的Bankswitch原理
Keil的BL51使用页面寻址的方式来增加代码空间。EPROM被分页,每页的大小以及在页间进行跳转的方式取决于具体的应用,可以由用户自行设置。
1、Keil C51的分页方式
Keil C51支持的分页方式有三种:
1)利用单片机I/O口线。通常默认是P1口,采用1条P1口线时, 分组数为2,采用5条P1口线时,最多可分为32个代码组,剩余的P1口线也可用于其他用途。
2)利用片外数据存储器(XDATA)空间实现分页。指定一个XDATA端口字节实现分页操作,该字节中的剩余位,不能再用于其他目的。
3)用户自定义方式实现分页。
根据PSD系列器件的特性,本文介绍的是其中的第二种。采用的分页方式、页数和地址在L51_BANK.A51中进行配置。
2、公共空间
公共空间:分页的所有页面均要留有一个相同的存储区域,这个区域包括中断向量、中断功能函数、调用其它EPROM页面的函数、C51库函数、在页面间跳转的代码和被多个页面代码使用的常量及变量.
通常在每页面的底部留作公共空间,保存公共代码。公共空间是处理器在任何时候都能够寻址的。
公共空间是占总存储空间的,若每个页面总存储容量为64KB,公共空间需要10KB存储空间,则该页面给用户实际使用的空间为54KB。若用户使用的页面数为8页,则用户最大使用的代码空间为8×(64-10)+10=442KB。通常页切换需要大约50个机器周期和2字节的堆栈空间。
3、BL51的配置
需要对L51_BANK.A51文件预先进行相关参数的设置,该文件是BL51连接定位器的一部分。
主要有以下几项配置内容:
1)?B_NBANKS EQU X 。定义最大页数,BL51 最大可定义为32页,X值可取为2、4、8、16和32。
2)?B_MODE EQU Y。设置KeilC51的分页方式。Y取值为0、1、4。含义如下:0—通过8051单片机的I/O口进行分组切换;1—通过XDATA存储器单元进行分组切换;4—用户自定义切换方式。
3)?B_RTX EQU Z。设置系统是否使用RTX-51 FULL实时操作系统。Z取值为0、1,分别代表不使用RTX-51及使用RTX-51实时系统。
其它如?B_VAR_BANKING的设置通常使用系统的默认值。
4.2 基于PSD813F2的分页硬件设计
PSD813F2内置128KB闪存,分8个页面,每页16KB,内置可选的32KB启动存储器、2KB的SRAM、27个I/O端口、电源管理单元、40个可编程逻辑宏单元CPLD,通过JTAG串行接口允许在系统编程整个器件。适用于8031、MC68HC11、Dallas、Z80等20余种单片机。

Keil C51编译及连接技术
4.3分页的实现过程及实例
1 PSD Soft配置
1)在Page Register Definition步骤中选择pgr0,只分两页,如图12-4-2所示。在Chip Select Equation n步骤中设置rs0为0页,地址0000H~07ffH;csiop地址4000H~40ffH;fs0、fs1、fs2、fs3设置为0页,地址分别为0000H~3fffH(如图12-4-3所示)、4000H~7fffH、8000H~bfffH、c000H~ffffH,fs4、fs5、fs6、fs7设置为1页。

Keil C51编译及连接技术Keil C51编译及连接技术

二、Keil C51项目设置
1)建一个KeilC51的新项目,命名为如KC_PSD,在菜单Project选择中选择Option for Target “Target 1”选择项,按图12-4-4所示进行分页及地址配置。

Keil C51编译及连接技术
●由于PSD813F2片内从FLASH为32KB。故在off-chip code memory-Eprom设为0x0000~0x7fff。如果这32KB用户未使用,也可空着不填。
● 在PSD813F2的PSDSoft Express已配置2KB的SRAM,地址范围为0x0000~0x07ff,故在off-chip Xdata memory-Ram设为0x0000,长度为0x0800。
● 在PSD813F2的PSDSoft Express已配置256个字节的CSIOP空间,地址范围为0x1000~0x10ff, 故在off-chip Xdata memory-Ram设为0x4000,长度为0x0100。
● 在PSD813F2的主FLASH为128KB,共分为8个物理页,此处在逻辑上分为两页,每页64KB。因此在KeilC51中如图12-4-2所示分为2个Banks,地址范围为0x0000~0xffff
2)L51_Bank.A51的修改
在KeilC51的安装目录\KeilC51\C51\EXAMPLES\Bank_EX1中,把L51_Bank.A51文件的拷贝并加入到KC_PSD 项目并作如下修改:

A?B_NBANKS EQU  //定义最大页数(~),可为2、、、16和32。
?B_MODE EQU  //:通过8051单片机的I/O口进行分组切换,:通过XDATA存储器单元进行分组切换,:用户自定义切换方式。
?B_XDATAPORT EQU 40E0H //如果?B_MODE被定义为1,就通过XDATA口定义XDATA口引脚地址/位来映射开关定义XDATA口地址40E0H。 

3)新建主程序和各bank文件。 
示例
示例的项目主要是为演示BankSwitch,演示结果为单片机的P1.0引脚的发光二极管闪烁。
由主文件psd1.c调用psd2.c中的延时子程序Delay(),psd1.c 存放在第一页(64KB存储空间)中,psd2.c存放在第二页(64KB存储空间)中。
主程序psd1.c 源程序

#include<reg51.h>
extern void Delay(unsigned char d1);
sbit P1_0=P1^;
void main(void)
{
    Delay();
    )
    {
        P1_0=!P1_0;
        Delay();
    }
}

psd2.c 源程序

void Delay(unsigned char d2)
{
    unsigned char i,j,k;
    ;k<d2;k++)
    {
        ;i<;i++)
        ;j<;j++);
    }
}                                  

4)设置文件的属性。对于主文件psd1.c,选择Options for File,设置Code Bank为“bank#0”,设置Stop on Exit为“Not Specified”如图12-4-5所示。类似地,Psd2.c设置为Bank #1。L51_Bank.A51设置为“default”。
5)设置好后,进行编译,生成HEX文件KC_PSD.h00及KC_PSD.h01。
Keil C51编译及连接技术
3 编程
● 打开PSDSoft Express软件,在Merge MCU Firmware with PSD模块中设置各个FLASH页面的地址和文件名,如图12-4-6所示。

Keil C51编译及连接技术
主FLASH第0页(FS0),地址:0x0000-0x3fff,文件名:KC_PSD.hoo
主FLASH第1页(FS4),地址:0x0000-0x3fff,文件名:KC_PSD.ho1
MCS-51单片机的寻址空间突破64KB需要硬件的支持和软件保障。
利用PSD813F2芯片与MCS-51单片机的无缝连接,Keil C51的Bankswitch技术有效地实现了MCS-51单片机程序代码64KB空间的突破,解决了大容量存储器的系统如POS机、打印机或大量汉字显示系统等系统软件设计时的换页难题,降低了单片机软件设计人员的程序设计难度。