C51中的函数指针

发布者:温暖心情最新更新时间:2021-10-28 来源: eefocus关键字:C51  函数指针  8051架构 手机看文章 扫描二维码
随时随地手机看文章

概述

函数指针是C编程语言众多难懂的特性之一。由于C编译器对关于8051架构的独特要求,函数指针和可重入函数需要克服更大的挑战。这主要是因为函数参数传递的方式。


通常,(对于大多数非8051的芯片),函数参数是在栈上以压入和弹出的汇编指令来完成。由于8051的栈大小有限(仅128字节,某些设备上更低至64字节),函数参数传递必须用不同的技术来传递。


英特尔为8051推出PL/ML-51编译器时,他们引入了将参数存储在固定内存位置的技术。当链接器被调用时,它会建立程序的调用树,找出哪些函数参数是相互独立的,然后覆盖它们。这就是链接器OVERLAY指令的开始。


由于PL/M-51不支持函数指针,所以从未出现间接函数调用的问题。但是,关于C,问题更多。链接器如何“知道”哪些内存被间接函数使用?你又如何添加间接调用的函数进入调用树?


这篇文档解释如何在C51程序中有效地使用函数指针。一些示例被用来阐释讨论的问题和解决方案。

具体来说,就是这下面这些被讨论的主题:


转换常量为一个指针

声明函数指针

C51中函数指针的问题

用OVERLAY指令修改调用树

可重入函数指针

固定地址的指针

你可以轻松地将数字地址转换成函数指针。这样去做有很多原因。例如,你可能需要不用触发CPU的复位线就复位目标和应用程序。你能够用地址为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。这不是必须的,如果我们只是想转换0x000到一个函数指针。但是,由于我们将要调用这个函数,这些括号是必需的。


转换数字常量至一个指针并不像通过指针调用一个函数。为此,我们必须指定一个参数列表。那就是在这一行末尾的()。


注意,在这个表达式中的所有括号都是必需的。括号分组和优先级也是重要的。上面的指针和指向带参数函数的指针之间的唯一的不同是数据类型和参数列表。例如,这下面的函数调用…


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


…调用一个地址为0x8000的函数,接收3个int参数并且返回long。


无参数的函数指针

函数指针是一个指向函数的变量。这个变量的值是函数的地址。例如,下列函数指针的声明…


void (*function_ptr) (void);


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


(*function_ptr) ();


由于function_ptr指向的函数没有一个参数被传递。这也是为什么参数列表为空。function_ptr的地址可以被赋值,当它被声明的时候。


void (*function_ptr) (void) = another_function;


或者,它也可以在程序执行中被赋值。


function_ptr = and_another_function;


重要的是要注意,你必须给函数指针赋值一个地址。如果你没有这样做,这指针的值可能是0(如果你幸运的话)或者它可能是完全不确定的值,具体取决于你使用数据内存的方式。如果一个指针没有初始化,当你间接通过它调用函数时,你的程序可能崩溃。


要声明一个有返回类型的函数指针,你必须在声明时指明返回类型。例如,下面的声明将上面的声明改为一个指向返回float类型的函数。


float (*function_ptr) (void) = another_function;


很简单。只要记住括号在哪里就行了。


带参数的函数指针

带参数的函数指针和不带参数的函数指针类似。例如:


void (*function_ptr) (int, long, char);


…是一个带有int,long和char作为参数的函数指针。用下面的代码调用function_ptr指向的函数。


(*function_ptr)(12, 34L, 'A');


注意,函数指针可能只能指定带3个或更少参数的函数。这是因为间接调用函数的参数必须驻存寄存器。有关使用多于3个参数的函数指针,参见可重入函数。


使用函数指针的注意事项

如果你在C51程序中使用函数指针,这里有几个你必须注意的事项。


参数列表限制

通过函数指针传递到函数的参数必须全部填充进寄存器。最多3个参数可以自动在在寄存器中传递。不要认为任意3个数据类型都可以。


由于C51至少可以通过寄存器传递3个参数。除非函数指定了更多参数,否则使用内存空间传递参数不是什么问题。如果是这种情况,你可以合并参数进一个结构体,然后传递指数结构体的指针。如果这不可接受,你可以使用可重入函数(参见下文)


调用树的保存

C51工具链不会将函数参数压入堆栈(除非可重入函数被调用)。相反,函数参数和自动变量(局部变量)是存储在寄存器中或在固定的内存位置。这会防止函数的可重入。例如,如果一个函数调用它自己,它将覆盖它自己的参数或局部变量。这个重入的问题通过reentrant关键字解决(参见下文)。另一个非可重入的副作用是函数指针能够,而且经常带来实现的问题。


为了保存尽可能多的数据空间,链接器执行调用树分析来确定一些内存空间是否要以安全地被覆盖。

例如,如果你的应用程序包括main函数、函数a、函数b和函数c;然后如果main调用a,b,c;并且a,b,c没有调用其他函数(也没有调用彼此);然后关于你的应用程序的调用树如下:


MAIN

+--> A

+--> B

+--> C


然后,被A,B和C使用的内存可以被安全覆盖。


当调用树不能被正确的构建时,关于函数指针的问题就出现了。这原因是链接器不能确定函数指针是引用哪个函数。这里没有自动的方法来解决这个问题。但是这里有一个手动的,尽管有点麻烦。


这下面的两个源文件帮助说明问题并且使得解决方案更容易理解。这第一个源文件FPCALLER.C,包含一个函数,这个函数通过函数指针(fptr)调用另外一个函数。


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

{

unsigned char i;

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

{

(*fptr) (i);

}

}


第二个源文件FPMAIN.C,包含main C函数,也包括通过func_caller(上面定义)间接调用的函数。注意,main调用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) ;

}


上述两个文件编译没有错误。它们链接也没有错误。这下面的调用是通过链接器在map文件中产生的。


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 C函数,那是?PR?MAIN?FPMAIN段。这个段名的组成部分可以被解码为:PR是PRogram内存,MAIN是函数名,FPMAIN是函数定义所在的源文件的名字。


MAIN函数调用FUNC和FUNC_CALLER(通过调用树)。注意,这不是正确的。MAIN从没有调用FUNC。但是它的确传递了FUNC的地址给FUNC_CALLER。同样要注意,通过调用树,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 C51编译器和链接器不起作用了吗?


这个问题的原因是函数指针。无论你什么时候使用函数指针,你一直会有类似的问题。幸运的是,它们很容易被修复。OVERLAY链接指令可以让你在调用树中指定函数是如何链接到一起的。


为了修正上面的调用树,针对FUNC的调用必须从MAIN函数移出,并且在FUNC_CALLER中插入对FUNC的调用。这下面的OVERLAY命令正是这样做的。


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

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


为了移除或插入对调用树的引用,需要首先指定调用者然后是被调用者。波浪线(~)是移除引用或调用,感叹号(!)是添加引用或调用。例如,?PR?MAIN?FPMAIN ~ ?PR?_FUNC?FPMAIN移除从MAIN中对FUNC的调用。


通过OVERLAY指令修正调用树的链接命令被调整之后,map文件中显示如下信息:


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和FUNC_CALLER被分离在不同的空间(不再覆盖了)。


函数指针表

下面的内容是典型的函数指针表定义:


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


如果你的main C函数通过fp_tab调用函数,那么这链接map将出现如下信息:


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


通过表来调用这3个函数,func1,func2,func3,看起来像通过?C_INITSEG调用。但是,这并不是正确的。?C_INITSEG是初始化你代码变量的例程。这些函数只是在初始化代码中被引用,因为函数指针表是通过这些函数的地址初始化的。


注意,这些通过main C函数,同样还有func1,func2,func3作为变量被用的起始区域都是起始于0008h。这是无法运行的,因为main C函数调用func1、func2和fun3(通过函数指针表)。并且,在func1等函数中被用到的变量会覆盖那些在main中被用到的。


当你使用函数指针表时,C51编译器和BL51链接器组合工作,让覆盖函数变量空间变得很容易。但是,你必须恰当地声明函数指针表。如果你这样做,你能够避免使用OVERLAY指令。这下面的是一个函数指针表定义,C51和BL51可以自动处理。


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


注意,这唯一不同的是表格存储在代码空间。

现在,链接map显示如下:


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可以为你让所一切在调用树中正确地链接。


函数指针建议和技巧

这里有一些函数指针的技巧,可以使得你让事情变得更容易。


用指定内存指针

将函数指针从通用指针转换成指定内存指针。这将为每个指针节省1个字节。到目前为止,示例使用的都是通用函数指针。由于函数仅仅驻留在代码内存(在8051上),因此可以将函数声明为Code类型指针来节省1个字节。例如:


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


如果你在你的函数指针声明中选择包含一个code关键字,那么请确保所有的地方都这样用。如果你声明一个3个字节的函数指针,

并且传递指定内存2个字节的函数指针,糟糕的事情就会发生!


可重入函数和指针

Keil C51为可重入的函数提供reentrant关键字。可重入函数期望参数是通过模拟栈来传递。

栈是在用于small memory model的IDATA、用于compact memory model的PDATA或用于large memory model的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 model的可重入函数,使用large和reentrant关键字。


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

{

}


要声明一个指定可重入函数的指针,你必须同样使用reentrant关键字。


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

1

声明可重入函数指针和非可重入函数指针并没有太多不同。


当使用可重入函数指针时,因为参数必须压入模拟栈,所以更多的代码会产生。但是,没有链接控制需要指定,你也不必须混乱地使用OVERLAY指令。


如果你使用间接调用地方式传递多于3个参数到函数,那么可重入函数指针是需要的。


结论

如果你注意链接器调用树并且确保使用OVERLAY修正任何不一致的情况,那么函数指针是非常有用的并且也不是特别难用。


关键字:C51  函数指针  8051架构 引用地址:C51中的函数指针

上一篇:C51简介及Keil的使用
下一篇:Keil C51 Code Banking

推荐阅读最新更新时间:2024-11-08 14:58

ISD4004语音芯片C51驱动程序源代码
ISD4004语音芯片C51驱动程序源代码 /*spi isd4004.h*/ #include reg51.h #include intrins.h sbit _cs = p0^0; sbit _sclk= p0^3; sbit _mosi= p0^1; sbit _miso= p0^2; sbit _rac = p0^4; sbit _int = p0^5; void delay(unsigned int i) //延时程序 { while(i--); } void stopmode() //停止 { unsigned char m,i,j; _cs=1; _sclk=0; _cs=0; m=0x30; for(i=0;i
[单片机]
C51系列单片机设计物体分级设备的测量光幕
摘要:首先介绍了光幕测量高度的原理,给出了高度测量光幕的一种实现方法,分析了由该方法设计的系统结构和主要性能。从而彻底解决了相邻通路间的干扰,提高了测量精度。 关键词:单片机;测量光幕;分级 1 引言 光幕是电子测量系统中应用比较多的一种设备。利用光幕可以测量恒速传送带上的物体高度、长度或宽度等一系列数据,以便为后面的电子系统提供相应的参数。本文给出了一种利用单片机实现物体高度测量的光幕测量方法。 2 光幕测量物体高度的基本原理 图1所示是一个用普通光幕测量物体高度的测试原理结构示意图。图中,光幕的一边等间距安装有多个红外发射管,另一边相应的有相同数量同样排列的红外接收管,每一个红外发射管都对应有一个相应的红外接收管,且
[应用]
c51中的bit,char的强制类型转换
c51中的bit,char的强制类型转换。 data为非0,,bit强制后,为1。否则为0 data的在char强制后取后8位。 (1)强制转换有什么好处? 例如: unsigned char x ; sbit SDA = P1^0 ; 1)、要用x来记录p1.0的值,可以用: for(i=0;i 8;i++) { x |= (unsigned char )SDA ; x =1; } 2)、要用SDA来一位一位传送x的值,可以: for(i=0;i 8;i++) { SDA = (bit)(x&0x80); x =1; } 在进行强
[单片机]
Keil c51指针变量
单片机c语言支持一般指针(Generic Pointer)和存储器指针(Memory_Specific Pointer)。 1. 1. 一般指针 一般指针的声明和使用均与标准C相同,不过同时还能说明指针的存储类型,例如: long * state;为一个指向long型整数的指针,而state本身则依存储模式存放。 char * xdata ptr;ptr为一个指向char数据的指针,而ptr本身放于外部RAM区,以上的long,char等指针指向的数据可存放于任何存储器中。 一般指针本身用3个字节存放,分别为存储器类型,高位偏移,低位偏移量。 2. 2. 存储器指针 基于存储器的指针说明时即指定了存贮类
[单片机]
C51的基础 9 《 指针、结构、联合和枚举 》
指针、结构、联合和枚举 本节专门对第二节曾讲述过的指针作一详述。并介绍Turbo C新的数据类型: 结构、联合和枚举, 其中结构和联合是以前讲过的 五种基本数据类型(整型、浮点型、字符型、指针型和无值型)的组合。枚举是一个被命名为整型常数的集合。最后对类型说明 (typedef)和预处理指令作一阐述。 指 针(point) 学习Turbo C语言, 如果你不能用指针编写有效、正确和灵活的程序, 可以认为你没有学好C语言。指针、地址、数组及其相互 关系是C语言中最有特色的部分。规范地使用指针,可以使程序达到简单明了, 因此, 我们不但要学会如何正确地使
[单片机]
C51---示波器---2ms方波
/* //查询方式,输出方波 #include sbit p1_0=P1^0; void main() { p1_0=1; TMOD=0x01;//选择定时器0,工作方式为1 TH0=(65535-1000)/256; TL0=(65536-1000)%256; TR0=1;//启动定时器0 p1_0=0; while(1) { if(TF0==1) { p1_0=~p1_0; TF0=0;//清空中断标志位 TH0=(65535-1000)/256; TL0=(65536-1000)%256; TR0=1;//启动定时器0 } } } */ //中断方式,输出方波 #i
[单片机]
C51---示波器---2ms方波
C51单片机————定时器计数器
51单片机定时器/计数器 定时(定时控制、测量、延时。。。)和计数(对外部事件统计数目)。 软件定时|数字电路定时|可编程定时/计数器 它们都是怎么实现的呢? 额! 你猜 。。。 软件定时 机器执行一个程序,这个程序没有其他用处,只是为了延时!以达到定时的目的。 数字电路硬件定时 555之类的器件,完成定时 可编程定时/计数器 硬件定时,但是是可编程的,可以通过软件初始化设置定时的要求。 1.结构 定时/计数器实质上是一个加一计数器,可以工作在两种方式里,实际上都是对脉冲计数,只是说脉冲的来源不一样而已! 1. 定时 1. 加一计数。 2. 脉冲来自振荡器的12分频后的脉冲(Fosc/12),简单的
[单片机]
<font color='red'>C51</font>单片机————定时器计数器
用Keil C51开发串行口
MCS-51单片机的串行口具有两条独立的数据线 发送端TXD和接收端RXD,它允许数据同时往两个相反的方向传输。一般通信时发送数据由TXD端输出,接收数据由RXD端输入。 MCS-51单片机的串行口既可以用于网络通信,亦可实现串行异步通信,还可以用作同步移位寄存器。如果在串行口的输入输出引脚上加上电平转换器,就可方便地构成标准的RS-232接口。 MCS-51单片机的串行接口是一个全双工通信接口,它有两个物理上独立的接收、发送缓冲器SBUF,可以同时发送和接收数据。但是发送缓冲器只能写入,不能读出;接收缓冲器只能读出,不能写入。两个缓冲器共用一个地址(99H)。 1 数据通信的基本概念 常用于数据通信的传输方式有单工、半双工、
[单片机]
用Keil <font color='red'>C51</font>开发串行口
小广播
设计资源 培训 开发板 精华推荐

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

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

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

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