当前位置: 首页 > 工具软件 > SDCC > 使用案例 >

c语言扩展编译器,TKStudio内置51编译器SDCC对C语言的扩展(2)

田文景
2023-12-01

函数1. 函数参数和局部变量

函数里的自动(局部)变量和参数可以分配到堆栈上或者数据空间里。SDCC编译器默认的做法是分配这些变量到内部RAM中(小模式)或者外部RAM

(中模式或大模式),即SDCC把函数参数和局部变量分配在固定的地址,每次调用该函数就直接从固定的地址去取参数和局部变量的值,这与其它的编译器不一样,主要是因为8051数据存储空间RAM有限,不能支持大的堆栈。这实际使它们类似于静态局部变量,这样默认函数都是不可重入函数(有关可重入函数的概念将在下节中讲述)。

它们可以分配到栈,使用--stack-auto选项或者使用#pragma

stackauto或者在函数声明里使用下节将要讲述的reentrant关键字。

例如:

unsigned char foo(char i) __reentrant

{

...

}

由于8051上栈的空间有限,因此,关键字reentrant或者--stack-auto选项应该尽量少用。

注意,关键字reentrant仅仅意味参数和局部变量将被分配到栈,并不是指函数是可重入的。

如果局部变量被指定存储类型和使用绝对地址分配,编译器将按指定的存储类型为局部变量分配空间,而不再是在堆栈空间中分配,这一点需要注意。例如:

unsigned char foo()

{

__xdata unsigned char i;

__bit bvar;

__data __at (0x31) unsigned char j;

...

}

如上例子,变量i将被分配到外部的RAM中,

bvar变量被分配到位可寻址空间以及j变量被分配到内部的RAM中。当使用--stack-auto选项编译或者声明函数时指定reentrant关键字,这将会被当作静态变量被处理。

无论怎样,参数不允许指定任何的存储类型(参数的存储类型会被忽略),它们的空间分配受使用的内存模式跟reentrancy选项所决定。

2.可重入函数可重入函数指的是函数可以被多个任务同时调用或者被递归调用,而不必担心会破坏数据。也就是说,可重入型函数在任何时候都可以被中断执行,过一段时间以后又可以继续运行,而不会因为在函数中断的时候被其他的任务重新调用,影响函数中的数据。可重入函数可以被递归调用,可同时被两个或多个进程调用。可重入函数经常在实时应用或在中断和非中断,必须共用一个函数的情况下被使用。在一般的编译器中,下面的两个例子可以比较可重入型函数和非可重入型函数:

程序清单 2 4可重入型函数

void swap(int *x, int *y)

{

int temp;

temp=*x;

*x=*y;

*y=temp;

}

程序清单 2 5非可重入型函数

int temp;

void swap(int *x, int *y)

{

temp=*x;

*x=*y;

*y=temp;

}

程序清单 2 4中使用的是局部变量temp 作为变量,通常的C

编译器,把局部变量分配在栈中。所以,多次调用同一个函数,可以保证每次的temp 互不受影响。而程序清单 2 5中temp

定义的是全局变量,多次调用函数的时候,必然受到影响。代码的可重入性是保证完成多任务的基础,除了在C

程序中使用局部变量以外,还要C编译器的支持。非常遗憾的是在SDCC编译器中,上面两个函数都是不可重入的。为什么呢?其实上一个小节已经说得非常清楚了,在SDCC中,参数和局部变量都被分配在固定的地址,它们相当于静态变量,所有每次调用都会修改它的值。那么如果要在SDCC支持可重入函数需要怎样做呢?除了上面给出的条件,还应该满足下面的条件:

1. 函数的声明要加上采用SDCC的一个扩展关键reentrant,函数的声明形式为:

函数类型 函数名(形式参数) reentrant/__reentrant

{

}

或者使用--stack-auto选项编译函数,不过不推荐这样做,因为它会把所有的函数参数和局部变量都放入到堆栈中,而堆栈本身就比较小。

2. 函数中的局部变量不能指定存储类型或者使用绝对地址,因为该局部变量不会放到堆栈中,而是根据指定的存储类型分配到固定的地址。

另外,在可重入函数里允许使用位变量参数和非静态的局部位变量。在位空间中,高效率的使用方法是限制在8个位寄存器内。它们将被当作普通寄存器一样单字节地压栈和出栈。

3. 覆盖为了减少内部RAM空间的使用,对非可重入函数,SDCC通过使用覆盖来处理函数的参数和局部变量。如果在小存储模式下函数没有被其他函数调用并且函数是不可重入的,那么函数的参数和局部变量将被分配在一个可覆盖的段中。如果显式的指定了一个变量的存储类型,该变量将不会被覆盖。

注意:编译器(而不是连接器)决定数据项是否可覆盖。如果是不可重入函数,被中断服务例程调用的函数应该在函数定义前加上#pragma

nooverlay。

另外需要注意的是编译不会对内嵌汇编做任何处理,因此,编译器可能错误地分配函数的局部变量和参数到可覆盖段,如果内嵌汇编代码调用其它的C语言函数而该函数也可能使用可覆盖段。如果那样的话#pragma

nooverlay应该被使用。函数参数和变量如果包含16位或者32位的乘法或者除法将是不可覆盖的。

例如:

#pragma save

#pragma nooverlay

void set_error(unsigned char errcd)

{

P3 = errcd;

}

#pragma restore

void some_isr () __interrupt (2)

{

...

set_error(10);

...

}

如果#pragma

nooverlay不存在的话,在上面的例子中函数set_error的参数errcd将被分配到可覆盖的段中,当中断服务例程调用该函数时这将导致不可知的运行结果。#pragma

nooverlay确保函数参数和局部变量将是不可覆盖的。

4. 中断服务例程SDCC编译器器支持在C语言源程序中直接编写8051单片机的中断服务例程程序,从而减轻了采用汇编编写中断服务程序的繁琐。为了满足在C语言程序中直接编写中断服务例程的需要,SDCC对函数的定义进行了扩展,增加了一个扩展关键字interrupt。它是函数定义时的一个选项,加上它就代表函数被定义为中断服务函数。中断服务函数定义的一般形式如下:

void 函数名(void)[interrupt/__ interrupt n] [using/__using n]

注意:中断服务函数不能有参数和返回值。如:

void timer_isr (void) __interrupt (1) __using (1)

{

...

}

在interrupt关键字后面可选的数字是该例程将要服务的中断号。当它存在时,编译器将在中断向量表中为该指定的中断号插入一条CALL指令来调用该例程。如果在工程中包含了多个源文件,中断服务例程能够出现在任何源文件中,但是该ISR的原型必须出现在或者#included包含main函数的文件。可选关键字using被用来告诉编译器当为该函数产生代码时使用指定的寄存器组。

如果中断服务例程被定义成没有使用一个寄存器组或者使用寄存器组0(using

0),编译器将保存所有被使用的寄存器到堆栈的入口之上并且在退出时恢复。然而如果中断服务例程调用其它的函数,整个寄存器组将被保存到堆栈中。该方案对使用非常少的寄存器的小中断服务例程是非常有利。

如果中断服务例程被定义为使用一个专门的寄存器组,只需要保存和恢复a, b, dptr &

psw就可以了。如果中断服务例程调用其它的函数(使用另外的一个寄存器组),被调用函数整个寄存器组将被保存到堆栈中。该方案推荐在大的中断服务例程中使用。

标准8051/8052的中断号和相应的地址,以及描述如表 2 2 所示。SDCC将自动调整指定的最大中断号。

中断号 描述 向量地址

0 外部中断0(INT0) 0x0003

1 定时器0中断 0x000b

2 外部中断1(INT1) 0x0013

3 定时器1中断 0x001b

4 串口中断 0x0023

5 定时器2中断(8052) 0x002b

… … …

n 0x0003 +

8*n

表 2 2 中断矢量地址

中断服务例程导致了一些非常有趣的bugs:

 一般的中断缺陷:变量没有被声明为volatile

如果一个中断服务例程改变变量,而其它函数也需要访问该变量,那么这些变量必须被声明为volatile.有关信息请查看http://en.wikipedia.org/wiki/Volatile_variable。

 一般中断缺陷:非原子访问

如果访问的这些变量不是原子性的(如处理器需要不只一条指令来访问该变量并且中断可能会发生在访问这些变量的过程中),在访问期间中断必须被禁止来避免不一致的数据。访问16位或者32位变量明显的在8位CPU中不是原子操作并且必须关闭中断来进行保护。尽管使用8位变量,但也不一定会安全可靠。例如:8051中貌似无害的”flags

|= 0x80;” 如果flags驻留在xdata中,也并不是原子操作。在一个中断服务例程中放入”flags |=

0x40;”语句也许发生错误如果该中断发生在错误时间。”counter +=

8;”在8051中不是原子操作,即使counter分配在内部数据寄存器中。像这样的Bugs很难重现并且会产生非常多的麻烦。

 一般中断缺陷:堆栈溢出

返回地址和中断服务例程使用的寄存器被保存在堆栈,因此必须要有足够的堆栈空间。如果发生中断溢出变量或者寄存器(甚至返回地址本身)将被破坏。中断溢出最可能发生在深度子函数调用而这时堆栈已经使用了大量空间来存放返回地址。

 一般中断缺陷:使用非可重入函数

这儿特别要注意:int(16位)和long(32位)整数除法,乘法以及取模,还有浮点数操作都是通过使用扩展支持例程实现的。如果一个中断服务例程需要对上面这些进行操作,那么支持例程必须在使用--stack-auto选项情况下重新编译并且源文件需要使用--int-long-reent编译选项来编译。

注意:ANSI

C要求的类型提升能在程序员不知道的情况下导致16位例程的使用。不推荐在中断服务函数中调用其它函数,尽量避免该操作。注意:当一些函数需要被中断服务例程调用时,如果该函数不是可重入的,应该要在它定义的前面使用#pragma

nooverlay。此外,不可重入函数不应该在主程序中调用,因为中断服务例程随时可能被调用。同时也不能被低优先级中断服务函数调用,高优先级中断可能打断它。如果所有参数都通过寄存器传递应该在函数中使用信号量或者把函数声明为临界的。

在编写和调试中断服务例程时,必须要小心仔细的核对是否发生了上面所说的BUG。

5. 函数使用私有寄存器组

51体系结构支持快速切换寄存器组。SDCC通过在函数声明后面加上using这个属性(告诉编译器使用除了默认组0以外的寄存器组)来支持这个特性。

如:

void quitswap(char a) using

2

{

}

它应该被用在中断函数中。这在大多数情况下,使所产生的中断服务代码更有效率,因为它不需要保存寄存器到堆栈。using属性,将不会影响非中断代码的生成。中断函数使用非0寄存器组将假设它可以任意使用该寄存器组并且不需要保存。因为8051和其同类支持高优先级中断可以打断低优先级中断,那么如果一个高优先级中断ISR跟一个低优先级中断ISR使用同一个寄存器组,将会产生错误。为了防止发生这种情况,高优先级中断ISR不应该跟低优先级中断ISR使用同一个寄存器组。当然同高优先级中断ISR使用一个寄存器组和同低优先级中断ISR使用另外的一个寄存器组是可以的。如果能够动态改变中断服务程序优先级,建议使用默认0寄存器组,这样可能更好一点。在中断服务程序中如果没有调用其它函数是非常有效的。如果在中断服务程序中调用了其它函数,而这些函数又跟中断服务程序使用同样的一个寄存器组,这样也是非常有效的。然而如果在中断服务程序中调用了其它函数,而这些函数使用了另外的寄存器组,那将非常糟糕的。

6. 临界函数与临界语句一个特定的关键字可以在一个块语句或者一个函数定义的时候关联进去,那就是critical。SDCC将在进入临界函数时生成禁止所有中断和在返回之前生成恢复中断使能到前面的状态的代码。嵌套的临界函数需要在每个调用时在堆栈中增加一个额外的字节。

int foo () __critical

{

...

}

临界属性可以跟其它属性一起使用,象可重入属性。

Critical关键字也可以在更局部的地方来禁止中断:

__critical{ i++; }

至少有一条语句应该包含在这个块中。

上面所说的临界,其实就是在进入和退出函数或者语句是关闭中断。中断也能够被直接关闭和打开(8051)使用下面的语句:

EA = 0; or: EA_SAVE = EA;

... EA = 0;

EA = 1; ...

EA = EA_SAVE;

其他的体系结构有分别的代码来打开和关闭中断。这样你就可以使用内置汇编宏来定义。如HC08:

#define CLI _asm cli _endasm;

#define SEI _asm sei _endasm;

。。。

注意:有时只是需要关闭一个特殊的中断源只要通过操作一个中断掩码寄存器就可以了,象定时器和串口中断。

通常禁止中断的时间尽可的要短。这将减少中断延迟(发生中断到执行中断处理程序的第一条指令的时间)和中断抖动(不同于最短和最长的中断延迟)到最小。这些真的有些不一样,例如串口中断不得不被服务在它的缓冲区被覆盖之前,因此它关心的是最大中断延迟,而它不必关注抖动。在一个数字信号到模拟信号的转换的喇叭驱动中,中断延迟数毫秒也许能够容忍,然而大量小的抖动是能够被听见的。

在一个中断例程中重新使能中断,并且在一些体系结构中可以定义两个(或者更多个)中断优先级。在一些体系结构中不支持中断优先级,但可以通过操作中断掩码寄存器和在中断例程中再使能中断来实现。

临界也是对资源的竞争使用,而在MSC51中可能非常容易使用其它方法实现。MSC51有位测试和清除指令。这些类型的指令经常被用在抢占式的多任务操作系统中。当一个例程要求使用一个数据结构(要求对它进行琐定),当该数据结构再次成为可用的时需要做一些修改并且然后释放该琐。

该指令也能够被使用如果中断和非中断代码竞争使用一个资源。在位测试和清除指令下中断不需要被关闭为了进行琐操作。

如果源代码使用下面的形式,SDCC产生自动这些指令:

volatile bit resource_is_free;

if (resource_is_free)

{

resource_is_free=0;

...

resource_is_free=1;

}

7. 内嵌汇编代码开始从一个小的C语言代码片段例子来展示MCS51如何使用内嵌汇编,访问变量,函数参数和外部数据存储器中的一个数组。

unsigned char __far __at(0x7f00) buf[0x100];

unsigned char head, tail;

void to_buffer( unsigned char c )

{

if( head != (unsigned char)(tail-1) ) !

buf[ head++ ] = c;

}

如果该代码片段(假设它保存在buffer.c中)被SDCC编译将产生相应的buffer.asm文件。现在buffer.c文件中定义一个新的函数to_buffer_asm(),该函数通过复制粘贴生成的代码,去掉不必要的注释和一些’:’.。在函数体的开始和结尾分别增加”_asm”

和”_endasm;”语句。

void to_buffer_asm( unsigned char c )

{

_asm

mov r2,dpl

;buffer.c if( head != (unsigned char)(tail-1) )

mov a,_tail

dec a

mov r3,a

mov a,_head

cjne a,ar3,00106$

ret

00106$:

;buffer.c buf[ head++ ] = c;

mov r3,_head

inc _head

mov dpl,r3

mov dph,#(_buf > > 8)

mov a,r2

movx @dptr,a

00103$:

ret

_endasm;

}

该新的buffer.c文件能够被编译,除了一个关于没有引用函数参数’c’的警告。现在手工优化该汇编代码并且插入#define

USE_ASSEMBLY (1)语句,最后的代码如下:

unsigned char __far __at(0x7f00) buf[0x100];

unsigned char head, tail;

#define USE_ASSEMBLY (1)

#if !USE_ASSEMBLY

void to_buffer( unsigned char c )

{

if( head != (unsigned char)(tail-1) )

buf[ head++ ] = c;

}

#else

void to_buffer( unsigned char c )

{

c; // to avoid warning: unreferenced function argument

_asm

; save used registers here.

; If we were still using r2,r3 we would have to push them

here.

; if( head != (unsigned char)(tail-1) )

mov a,_tail

dec a

xrl a,_head

; we could do an ANL a,#0x0f here to use a smaller buffer (see

below)

jz t_b_end$

;

; buf[ head++ ] = c;

mov a,dpl ; dpl holds lower byte of function argument

mov dpl,_head ; buf is 0x100 byte aligned so head can be used

directly

mov dph,#(_buf> >8)

movx @dptr,a

inc _head

; we could do an ANL _head,#0x0f here to use a smaller buffer (see

above)

t_b_end$:

; restore used registers here

_endasm;

}

#endif

内嵌汇编代码能够包含任何汇编程序可以解释的有效代码,包括任何汇编指示和注释行。你能够找到100多页的汇编使用手册从dcc/as/doc/asxhtm.html或者网站:

http://sdcc.svn.sourceforge.net/viewvcsdcc/trunk/sdcc/as/doc/asxhtm.html

.

如果使用了_asm ...

_endasm;关键字对,编译器将对代码中不做任何检查。特别是它将不知道哪些寄存器被使用了并且这些寄存器只能被手工的压入压出堆栈。这里推荐每条汇编指令(包括标签)使用独立的一行。当--peep-asm命令行选项被使用时,内嵌汇编代码将被窥孔优化器优化。

SDCC允许在内嵌汇编中使用有一些约束的标签。在内嵌汇编代码中的所有标签必须以nnnnn$形式定义,其中nnnnn是小于100的数(意味着在每个函数中内嵌代码最多可以100个标签)。

_asm

mov b,#10

00001$:

djnz b,00001$

_endasm ;

内嵌代码不能引用任何C语言中的标签,然而可以引用内嵌汇编中的定义的标签。

例如:

foo() {

_asm

; some assembler code

ljmp 0003$

_endasm;

clabel:

_asm

0003$: ;label (can be referenced by inline assembler only)

_endasm ;

}

换句话说,内嵌汇编代码能够访问在整个函数区域内的被内嵌汇编定义的标签。内嵌汇编定义的标签也不能被C语言的语句访问。

8. 无保护函数一个指定的关键字naked/__naked可以跟函数声明一起使用。naked函数阻止编译器为函数生成开始和结尾代码。这就意味着使用者将完全地掌握这些事情,如保存任何需要被保护的寄存器,选择合适的寄存器组,在最后产生返回指令等等。实际上它意味着函数的这部分代码必须使用内嵌汇编来写。这对可能有非常大开始和结尾代码的中断函数来说非常有用。例如,比较这两个函数生成的代码:

volatile data unsigned char counter;

void simpleInterrupt(void) __interrupt (1)

{

counter++;

}

void nakedInterrupt(void) __interrupt (2) __naked

{

_asm

inc _counter ; does not change flags, no need to save psw

reti ; MUST explicitly include ret or reti in _naked

function.

_endasm;

}

对于8051目标来说,产生的 simpleInterrupt如下:

_simpleInterrupt:

push acc

push b

push dpl

push dph

push psw

mov psw,#0x00

inc _counter

pop psw

pop dph

pop dpl

pop b

pop acc

reti

whereas nakedInterrupt looks like:

_nakedInterrupt:

inc _counter ; does not change flags, no need to save psw

reti ; MUST explicitly include ret or reti in _naked function

注意:最新的SDCC版本可能对simpleInterrupt()

和nakedInterrupt()已经产生相同的代码。

 类似资料: