Keil C51 中的函数指针和再入函数

发布者:中原读书客最新更新时间:2015-09-15 来源: eefocus关键字:Keil  C51  函数指针  再入函数 手机看文章 扫描二维码
随时随地手机看文章
概述

函数指针是C语言中几个难点之一。由于8051的C编译器的独特要求,函数指针和再入函数有更多的挑战需要克服。主要由于函数变量的传递。

典型的(绝大部分8051芯片)函数变量通过堆栈的入栈和出栈命令来传递。因为8051只有有限的堆栈空间(128字节或更少的64字节),函数变量必须通过不同的方式进行传递。

8051的PL/M-51编译器,介绍在固定的存储空间存储变量的方式。当使用连接器时,程序建立一个调用树,计算出函数变量的互斥空间,然后覆盖它们。这就是连接器的“OVERLAY”指令。

因为PL/M-51不支持函数指针,所以不能实现间接函数调用。然而,C语言中存在这样的问题。连接器知道哪块空间用于存储间接函数的变量。怎样间接加入函数进入调用树?

本文解释在C51编程中,怎样有效使用函数指针。特别地,讨论如下几个话题:

分配常量地址给一个指针;

定义函数指针;

C51中函数指针问题;

使用OVERLAY指令确定调用树;

再入函数的指针; 

固定地址的指针

   你很容易的给函数指针分配一个数字地址。有许多原因需要这样做。例如,你需要复位目标。你可以设置函数指针为0000H去实现。

   你可以使用标准C语言的类型映射特点,映射0X0000指针指向地址0的函数。例如,当你编译如下C代码….

   ((void (code *) (void))0x0000) ();

   …编译器产生如下如下代码:

                ;FUNCTION main (BEGIN) 

                                          ;SOURCE   LINE  #3

0000  120000    LCALL   00H

                                          ;SOURCE   LINE  #4

0003  22        RET

                ; FUNCTION main (END)

   这正是我们期望的:LCALL  0

把一个数字常量映射成一个函数指针是一件很复杂的事情。下面关于上面的函数调用的各部分的描述,将帮助你怎样更好的使用它们。

  在上面的函数调用中,(void ( *) (void))是数据类型:一个不带参数且返回void的函数指针。

  0x0000是一个映射地址。经过类型映射,函数指针指向地址0x0000。注意我们把一个圆括号放在数据类型和0x0000后面。如果我们仅仅想映射0x0000成为函数指针,这是不必要的。然而,因为我们将引用这个函数,这些圆括号是必要的。

   映射一个数值常量成为指针和通过指针调用函数是不同的。为了实现这个,我们必须指定一个变量表。这就是为什么在此行的后面有一个()。

   注意上面表达式中的所有圆括号都是必须的。分组和优先级是很重要的。

   上面不带参数的函数指针和带参数的函数指针的唯一不同是数据类型和变量列表。例如,下面的函数调用…..

   ((long (code *) (int int int ) 0x8000)(1,2,3);

声明一个函数,地址在0x8000,接收3个int型参数,返回long型结果。

 

不带参数的函数指针

   指向函数的函数指针是可变的。函数的地址是一个可变的数值。例如,下面的函数指针的声明….

   void (*function_ptr) (void);

是一个调用function_ptr的指针。使用下面的代码调用function_ptr函数。

   (*function_ptr ) ();

因为函数没有参数传送,所以参数列表时空的。

当定义变量的时候,函数指针可以被分配地址:void (*function_ptr) (void) = another_fuction; 或者在程序执行过程中被分配,function_ptr = another_fuction;

   注意,必须分配一个地址给函数指针。如果没有分配,函数指针将有一个0值(如果你运气好),或者有一些你完全不知道的数值,依赖于你的数据存储区的使用情况。当你间接的调用一个函数通过函数指针,如果函数指针没有初始化,你的程序将是混乱的。

   为了声明一个带返回值的函数指针,在声明过程中你必须指定返回值的数据类型。例如,下面的声明改变了上面的函数指针的声明,返回一个float 数据。

float  (*function_ptr) (void) = another_fuction; 

 

带参数的函数指针

  带参数的函数指针与不带参数的函数指针是相似的。例如:

  void (*function_ptr) (int, long,char); 一个函数指针,带一个int参数,带一个long参数,带一个char参数。使用下面的代码调用函数。

  (*function_ptr) (12, 34L,‘A’);

注意,函数指针仅仅可以指向小于等于3个参数的函数。这是因为,间接调用函数时,参数必须保存在寄存器中。关于超过3个参数的函数指针的信息,在再入函数中介绍。

 

使用函数指针的附加说明

   如果你在C51中使用函数指针编程,有几个附加的说明你必须注意。

参数列表的限制

   通过函数指针传递参数给函数必须把所有的参数存入寄存器。在大部分情况下,3个参数能够自动通过寄存器传递。在C51的用户手册中能找到传递参数进入寄存器的运算法则。但是并不保证,任何的3个数据类型可以传递。

  因为C51在寄存器中传递3个参数,用于传递参数的存储空间是不被分配的,除非函数指向一个要求更多参数的函数。如果在那样的情况下,可以把参数混入一个结构体中,然后通过一个结构体指针传递参数。如果这样不可接受,你可以使用再入函数(看下面)。

 

调用树的保存

   C51不把函数参数压栈(除非使用再入函数)。函数参数和全局变量被存入寄存器或固定的存储空间。这样阻止函数的再入。例如,一个函数调用它自己,它将覆盖它自己的参数或存储空间。函数的再入问题通过关键字“reentrant”来解决。函数指针的非再入函数的副作用,在执行中出现问题。

   为了保护尽量多的数据空间,连接器执行调用树的性能分析,决定一些存储空间被安全的覆盖。例如,如果你的应用中包含main 函数,函数a,函数b,函数c,并且main函数调用a,b,c,但是a,b,c之间没有互相调用。在你应用中的调用树见出现如下:

MAIN

    +→ A

    +→ B

    +→ C

这样A,B,C的存储空间可以被安全的覆盖。

 

当调用树不能正确的建立,函数指针将带来问题。因为连接器不能决定函数之间的引用。在这个问题上,没有自动的解决方法。[page]

 

下面两个源文件将解答这个问题,使问题容易明白。第一个源文件FPCALLER.C,包括一个函数,它通过一个函数指针(fptr)调用另一个函数。

   void  func_caller(long (code *fptr) (unsigned int))

{

    unsigned char i;

    for(i=0;i<10;i++)

{

  (*ftpr)(i);

}

}

第二个源文件FPMAIN.C,包含C主函数和被func_caller调用的函数func。       注意main函数调用func_caller,把func的地址作为参数传递给func_caller。

  extern void func_caller (long (code *) (unsigned int));

int func (unsigned int count)

{

long j;

long k;

k = 0;

for (j = 0; j < count; j++)

    {

    k += j;

    }

return (k);

}

 

void main (void)

{

func_caller (func);

while (1) ;

}

 

上面的两个的源文件编译和链接都没有错误。通过连接器,调用树的映射文件如下:

  SEGMENT                             DATA_GROUP

  +--> CALLED SEGMENT             START    LENGTH

-------------------------------------------------

?C_C51STARTUP                       -----    -----

  +--> ?PR?MAIN?FPMAIN

?PR?MAIN?FPMAIN                    -----    -----

  +--> ?PR?_FUNC?FPMAIN

  +--> ?PR?_FUNC_CALLER?FPCALLER

?PR?_FUNC?FPMAIN                  0008H    000AH

?PR?_FUNC_CALLER?FPCALLER       0008H    0003H

 

 

在这个简单的例子中,许多信息可以从调用树里挖掘出来。?C_C51STARTUP段调用main函数的?PR?MAIN?FPMAIN,段名各部分解析:PR是代码存储区,MAIN是函数名,FPMAIN是定义函数所在的源文件名。

  MAIN函数调用FUNC和FUNC_CALLER(根据调用树)。注意这是错误的。MAIN函数没有调用FUNC函数,但是它传递FUNC函数的地址给FUNC_CALLER函数。同时注意,根据调用树FUNC_CALLER没有调用FUNC。这是因为FUNC_CALLER是通过函数指针间接调用FUNC。

  FPMAIN文件中的FUNC函数使用从0008H开始,长000AH字节的数据。FPCALLER文件中的FUNC_CALLER函数也使用从0008H开始,长0003H字节的数据。这是重要的。

  FUNC_CALLER函数使用的存储区从0008H开始,FUNC函数使用的存储区也是从0008H开始。因为FUNC_CALLER函数调用FUNC函数,又因为两个函数使用相同的存储区,这样就产生了问题。当FUNC函数被FUNC_CALLER函数调用时,存储区将被FUNC_CALLER破坏。这个问题是怎样产生的?是由Keil 51编译器产生还是由连接器产生?

  这个问题的原因是函数指针。当你使用函数指针时,你将总是遇到这样的问题。幸运的是,他们是容易被修改的。“OVERLAY”指令让你指定在调用树中,函数与其他函数是怎样连接的。

  为了修正上面显示的调用树,FUNC函数必须从MAIN函数中删除,同时FUNC函数必须插入到FUNC_CALLER函数中。下面用“OVERLAY”指令修改后如下:

  OVERLAY (?PR?MAIN?FPMAIN ~ ?PR?_FUNC?FPMAIN,

  ?PR?_FUNC_CALLER?FPCALLER ! ?PR?_FUNC?FPMAIN)

为了删除或插入相关的进入调用树,指定第一调用和第二调用。“~”符号用于删除相关的函数,“!”用于插入一个外部函数。例如?PR?MAIN?FPMAIN ~ ?PR?_FUNC?FPMAIN,意义是从MAIN函数中删除FUNC函数的调用。

  经过调整连接命令,包括用“OVERLAY”指令修正调用树,调整后的映射文件如下:

  SEGMENT                             DATA_GROUP

  +--> CALLED SEGMENT                START    LENGTH

-------------------------------------------------

?C_C51STARTUP                     -----    -----

  +--> ?PR?MAIN?FPMAIN

?PR?MAIN?FPMAIN                   -----    -----

  +--> ?PR?_FUNC_CALLER?FPCALLER

?PR?_FUNC_CALLER?FPCALLER         0008H    0003H

  +--> ?PR?_FUNC?FPMAIN

?PR?_FUNC?FPMAIN                  000BH    000AH

 

修正后的调用树中,FUNC_CALLER函数和FUNC函数使用独立存储空间。

 

函数指针列表

  下面是一个典型的函数指针列表的定义:

long (code *fp_tab []) (void) = { func1, func2, func3 };

 

如果你的MAIN函数中通过fp_tab调用歌函数,连接映射文件出现如下:

 

SEGMENT                          DATA_GROUP

  +--> CALLED SEGMENT          START    LENGTH

----------------------------------------------

?C_C51STARTUP                  -----    -----

  +--> ?PR?MAIN?FPT_MAIN

  +--> ?C_INITSEG

?PR?MAIN?FPT_MAIN              0008H    0001H

?C_INITSEG                     -----    -----

  +--> ?PR?FUNC1?FP_TAB

  +--> ?PR?FUNC2?FP_TAB

  +--> ?PR?FUNC3?FP_TAB

?PR?FUNC1?FP_TAB               0008H    0008H

?PR?FUNC2?FP_TAB               0008H    0008H

?PR?FUNC3?FP_TAB               0008H    0008H

三个函数通过列表被调用,FUNC1,FUNC2 和FUNC3被C_INITSEG调用。但是这是错误的,C_INITSEG按照常规的方式在程序中初始化。这些函数被引入初始化代码中,因为函数指针列表被初始化成这些函数的地址值。

   注意这些变量(FUNC1,FUNC2 和FUNC13)和MAIN函数的起始地址都是0008H。这样不能正常工作,因为MAIN函数调用FUNC1,FUNC2 和FUNC3(通过函数指针类表)。

   C51编译器和BL51连接器联合工作,当使用函数指针列表时,使得函数变量空间覆盖很容易。但是,你必须合理的声明指针列表。如果你这样做了,就可以避免使用“OVERLAY”指令。下面的函数指针列表的定义,C51和BL51可以自动处理:

  code long (code *fp_tab []) (void) = { func1, func2, func3 };

注意唯一不同的是存储列表在CODE空间。现在,连接映射文件如下:

  SEGMENT                          DATA_GROUP

  +--> CALLED SEGMENT          START    LENGTH

----------------------------------------------

?C_C51STARTUP                  -----    -----

  +--> ?PR?MAIN?FPT_MAIN

?PR?MAIN?FPT_MAIN              0008H    0001H

  +--> ?CO?FP_TAB

?CO?FP_TAB                     -----    -----

  +--> ?PR?FUNC1?FP_TAB

  +--> ?PR?FUNC2?FP_TAB

  +--> ?PR?FUNC3?FP_TAB

?PR?FUNC1?FP_TAB               0009H    0008H

?PR?FUNC2?FP_TAB               0009H    0008H

?PR?FUNC3?FP_TAB               0009H    0008H

  现在,初始化代码中没有引入FUNC1,FUNC2 和FUNC3。但是,MAIN函数中引入一个常数段FP_TAB。这是一个函数指针列表。因为函数指针列表引入了FUNC1,FUNC2 和FUNC3,所以调用树是正确的。

  只要把函数指针列表放在一个独立的源文件中,在调用树中,C51和BL51就能正确的连接。 [page]

函数指针的建议和技巧

  有些函数指针的应用技巧。
使用指定空间的指针

  把函数指针从一个普通的指针变成一个指定空间的指针。用一个字节保存指针。因为函数属于CODE存储区(在8051里),一个字节可以用来保存声明的函数指针作为CODE指针。例如:

  void (code *function_ptr) (void) = another_function;

如果你选择在你的函数指针声明中包含code关键字,就可以在任何地方使用它。如果你声明一个函数,它接收一个3字节的普通指针,通过指定空间传递,2字节函数指针,坏事将要产生。

 

再入函数和指针

  Keil C51 为函数的再入提供关键字“reentrant”。再入函数的参数通过模拟栈来传递。模拟栈对于small存储模式位于IDATA,对于compact存储模式位于PDATA,对于large存储模式位于XDATA。如果你使用再入函数,在STARTUP.A51中你必须初始化再入栈的指针。参考下面的启动代码:

  ;----------------------------------------------------------------------

 Reentrant Stack Initilization

;

 The following EQU statements define the stack pointer for reentrant

 functions and initialized it:

;

 Stack Space for reentrant functions in the SMALL model.

IBPSTACK      EQU     0         ; set to 1 if small reentrant is used.

IBPSTACKTOP   EQU     0FFH+1    ; set top of stack to highest location+1.

;

 Stack Space for reentrant functions in the LARGE model.

XBPSTACK      EQU     0         ; set to 1 if large reentrant is used.

XBPSTACKTOP   EQU     0FFFFH+1  ; set top of stack to highest location+1.

;

 Stack Space for reentrant functions in the COMPACT model.

PBPSTACK      EQU     0         ; set to 1 if compact reentrant is used.

PBPSTACKTOP   EQU     0FFFFH+1  ; set top of stack to highest location+1.

;----------------------------------------------------------------------

 

  你必须设置你使用的存储模式的堆栈和设置栈顶。当有入栈时,再入函数的栈指针减少(向下移动)。为了保护内部的数据区,有一个技巧就是把所有的再入函数放在一个独立的存储模式,像large或compact。

  用reentrant声明再入函数。

void reentrant_func (long arg1, long arg2, long arg3) reentrant

{

}

 

  用large和reentrant声明一个large模式的再入函数。

void reentrant_func (long arg1, long arg2, long arg3) large reentrant

{

}

 

声明一个再入函数的函数指针,必须使用reentrant关键字。

 void (*rfunc_ptr) (long, long, long) reentrant = reentrant_func;

再入函数的函数指针和非再入函数的函数指针没有许多不同。当使用再入函数指针时,会生成更多的代码,因为参数被压入模拟栈。然而,没有特殊的连接要求和不需要打乱“OVERLAY”指令。

  如果通过间接调用传递超过3个参数给函数,需要再入函数指针。

 

使用再入指针的注意事项

 

keil中的函数递归调用可分为两种情况,一种是普通函数递归,调用时,新调用函数的程序储存空间覆盖原来的相同函数调用的程序储存空间,使得原来的局部变量消失了;还有一种是再入函数(用reentrant说明)的递归,每次递归,keil为再入函数生成一个模拟栈,再入函数参数和局部变量被放在这模拟栈中,这样使得原来调用函数的局部变量就没有消失了,而新的调用函数参数和局部变量又可以继续。


再入函数的定义:


函数类型 [reentrent] 函数名 (形式参数)


例如:int [reentrent] fution(char n)


     if(n<1)return(1);


       else return(n*fution(n-1));


}


使用再入函数注意事项:


 1: 再入函数不能传送bit类型的参数,函数内部也不能定义局部位变量,不能有位操作。总之与位有关的定义和操作在再入函数中都不能实现。


2: 同一程序中可以有不同储存模式的再入函数,但是注意,任意模式的再入函数不能调用不同储存模式的再入函数,但可以调用不同储存模式的非再入函数。


 参数传递上,实际参数可以传递给间接调用的再入函数;非再入函数不能包含调用参数,因为那样会覆盖了原来的参数;但是,可以用全局变量来进行参数传递。


总结

  函数指针是非常有用的,并不是很困难的,如果你注意连接调用树,保证用“OVERLAY”指令修正一些冲突。

关键字:Keil  C51  函数指针  再入函数 引用地址:Keil C51 中的函数指针和再入函数

上一篇:C51的INTRINS.H详解
下一篇:keil c51中定义XDATA,CODE等类型的结构体方法

推荐阅读最新更新时间:2024-03-16 14:31

C51系统上实现YAFFS文件系统
随着NAND Flash存储器作为大容量数据存储介质的普及,基于NAND闪存的文件系统YAFFS(Yet Another Flash File System)正逐渐被应用到各种嵌入式系统中。本文将详细阐述YAFFS文件系统在C51系统上的实现过程。 1 NAND Flash的特点 非易失性闪速存储器Flash具有速度快、成本低、密度大的特点,被广泛应用于嵌入式系统中。Flash存储器主要有NOR和NAND两种类型。NOR型比较适合存储程序代码;NAND型则可用作大容量数据存储。NAND闪存的存储单元为块和页。本文使用的Samsung公司的K9F5608包括2 048块,每一块又包括32页,一页大小为528字节,依次分为2个256
[单片机]
Keil模式设置和编程的事项
因为大多数扩展功能都是直接针对8051系列CPU硬件的。大致有以下8类: 8051存储类型及存储区域 , 存储模式 , 存储器类型声明 , 变量类型声明 , 位变量与位寻址 ,特殊功能寄存器(SFR) ,C51指针 l 函数属性 具体说明如下(8031为缺省CPU)。 第一节KeilC51扩展关键字 C51 V4.0版本有以下扩展关键字(共19个): _at_ idata sfr16 alien interrupt small bdata large _task_ Code bit pdata using reentrant xdata compact sbit data sfr 第二节 内存区
[单片机]
单片机C51串口接收(中断)和发送例程
//这是一个单片机C51串口接收(中断)和发送例程,可以用来测试51单片机的中断接收 //和查询发送,另外我觉得发送没有必要用中断,因为程序的开销是一样 #include reg52.h #include string.h #define INBUF_LEN 4 //数据长度 unsigned char inbuf1 ; unsigned char checksum,count3; bit read_flag=0; void init_serialcomm(void) { SCON = 0x50; //SCON: serail mode 1, 8-bit UART, enab
[单片机]
深入理解指针函数
1.指针函数的定义 顾名思义,指针函数即返回指针的函数。其一般定义形式如下: 类型名 *函数名(函数参数表列); 其中,后缀运算符括号 () 表示这是一个函数,其前缀运算符星号 * 表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个 指向返回值为 的指针(地址), 类型名 表示函数返回的指针指向的类型 。 (函数参数表列) 中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下: int *pfun(int, int); 由于 * 的优先级低于 () 的优先级,因而pfun首先和后面的 (
[单片机]
keil使用详解
第一章 Keil C51开发系统基本知识 第一节 系统概述 Keil C51是美国Keil Software公司出品的51系列兼容单片机C语言软件开发系统,与汇编相比,C语言在功能上、结构性、可读性、可维护性上有明显的优势,因而易学易用。用过汇编语言后再使用C来开发,体会更加深刻。Keil C51软件提供丰富的库函数和功能强大的集成开发调试工具,全Windows界面。另外重要的一点,只要看一下编译后生成的汇编代码,就能体会到Keil C51生成的目标代码效率非常之高,多数语句生成的汇编代码很紧凑,容易理解。在开发大型软件时更能体现高级语言的优势。下面详细介绍Keil C51开发系统各部分功能和使用。 第二节 Keil C51单片
[单片机]
<font color='red'>keil</font>使用详解
《初学者C51自学笔记》之74HC573芯片及发光二极管闪烁
当LE为高电平时,D与Q的状态一样,同高同低;LE为低电平时,D是任意的状态而Q保持原来的状态;OE是低电平有效。 用总线方法使发光二极管闪烁 #include reg52.h /* 不精确延时 */ void delay(void) { unsigned int a; a=55400; while(a--); } void main() { while(1) { P0=0xfd;//11111101 delay(); P0=0xff;//11111111 delay(); } }
[单片机]
《初学者<font color='red'>C51</font>自学笔记》之74HC573芯片及发光二极管闪烁
Keil精确测量代码运行时间
在工程设置中“Debug”下,选右边硬件仿真,点下拉框选“J-LINK/J-TRACE”,再点“Setting” 在弹出窗口中“Debug”下,选“JTAG”或“SW”,在后面选好下载速率。 点到Trace”标签下,如果是选择的“SW” 则勾选“Enable”选项,在“Core”框中输入MCU实际工作时钟频率(就是单片机以什么频率来执行指令的,MDK会用它来计算时间),再勾选“Autodetect max SW0 Clock” 如果是选择的“JTAG”, 先勾选“Enable”,在“Core”中设好时钟频率,最后去掉刚才勾选的“Enable” 因为“JTAG”模式不支持“Trace”功能,不把“Enable”去掉,
[单片机]
<font color='red'>Keil</font>精确测量代码运行时间
基于C51单片机的手动计数器设计电路图
  利用 AT89S51 单片机来制作一个手动计数器,在 AT89S51 单片机的 P3.7 管脚接一个轻触开关,作为手动计数的按钮,用单片机的 P2.0 - P2.7 接一个共阴数码管,作为00-99计数的个位数显示,用单片机的 P0.0 - P0.7 接一个共阴数码管,作为 00 - 99 计数的十位数显示;硬件电路图如图所示。   1 . 把 “ 单片机系统 ” 区域中的 P0.0/AD0 - P0.7/AD7 端口用 8 芯排线连接到“ 四路静态数码显示模块 ” 区域中的任一个 a - h 端口上;要求: P0.0/A D0对应着 a , P0.1/AD1 对应着 b , …… , P0.7/AD7 对应着 h 。  
[单片机]
基于<font color='red'>C51</font>单片机的手动计数器设计电路图
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved