打印

从51到ARM编程风格上的改变

[复制链接]
3593|16
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
z_no1|  楼主 | 2011-11-25 14:08 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
以前在51上编程,局部变量是不敢设多了东西,也就是几个循环用的i,j,k,num啥的,大数组都是预先分配好的全局变量,函数基本都是void up_proc(void)程序写大了容易不知道这个全局变量在那调用过,现在除了非要全局的,一律局部变量伺候,uint32_t up_proc(uint32_t *sp,uint32_t *pp).看上去清楚多了。
沙发
香水城| | 2011-11-25 15:01 | 只看该作者
嗬嗬,局部变量还是以少用为好。

全局变量也要少用,但可以使用静态变量(static)。

使用特权

评论回复
板凳
z_no1|  楼主 | 2011-11-25 15:08 | 只看该作者
应该是能用局部变量尽量用局部,实在不行才用全局。

使用特权

评论回复
地板
香水城| | 2011-11-25 15:37 | 只看该作者
应该是能用局部变量尽量用局部,实在不行才用全局。
z_no1 发表于 2011-11-25 15:08


一般局部变量是分配到堆栈中,如果无节制地使用局部变量,很可能造成堆栈溢出,而且一旦出错,很难找到原因。

使用特权

评论回复
5
z_no1|  楼主 | 2011-11-25 16:32 | 只看该作者
我的方式就是把堆栈设大,把原本是做静态局部变量的空间都划到堆栈里去,准确的说是划到栈里,堆基本少用,这样代码清楚,实际上对RAM的利用率也高,毕竟堆栈可以说是复用的。RAM里的空间最低端是全局变量,然后是堆,然后是栈,特别是对STM这种RAM在片内的,提高利用率。

使用特权

评论回复
6
z_no1|  楼主 | 2011-11-25 16:36 | 只看该作者
这样程序便于模块化

使用特权

评论回复
7
李富贵| | 2011-11-25 19:47 | 只看该作者
我觉得最大区别是STM32可以用RTOS,以前51的时候试过RTX51Full,太TMD耗费资源了,得不偿失。上了STM32以后马上用上ucos、freertos这些,整个程序思路清晰明确多了。STM32的SVC、PendSV这些特性就是为了让你用RTOS的。

使用特权

评论回复
8
gfkdliling| | 2011-11-25 21:17 | 只看该作者
嗬嗬,局部变量还是以少用为好。

全局变量也要少用,但可以使用静态变量(static)。
香水城 发表于 2011-11-25 15:01

香水版主,请问全局变量用多了会出什么问题呢?

使用特权

评论回复
9
香水城| | 2011-11-25 21:48 | 只看该作者
香水版主,请问全局变量用多了会出什么问题呢?
gfkdliling 发表于 2011-11-25 21:17


用太多了,程序大了以后,自己很容易逻辑混乱。

使用特权

评论回复
10
armmage| | 2011-11-26 10:48 | 只看该作者
全局变量用 STATIC修饰  把范围定在本文件内就可以了  在其他文件看来是不存在的 就算重名也没关系
全局变量调试的时候看比较方便

使用特权

评论回复
11
bingzhongbing| | 2011-11-26 12:38 | 只看该作者
看看了

使用特权

评论回复
12
youku| | 2011-11-26 12:45 | 只看该作者
一般局部变量是分配到堆栈中,如果无节制地使用局部变量,很可能造成堆栈溢出,而且一旦出错,很难找到原因。
香水城 发表于 2011-11-25 15:37


嗯?IDE不检测?不会提示溢出???

使用特权

评论回复
13
airwill| | 2011-11-26 12:50 | 只看该作者
综合 STM32 类 ARM 的优势. 由于CPU 有多个通用寄存器, 这样少量的局部变量是分配在寄存器里, 不会占用内存和堆栈的. 而且这样使用的话, 代码的效率也很高. 另外, arm 内核推荐 R0~R3,R12 做零时变量(调用子程序会受影响), R4~R11局部变量(调用子程序不会影响), 所以stm32 系统适合编程方法是: 把大的任务尽量化整为零, 用多个子程序来完成.
这样做, 虽然增加了子程序来回调用和堆栈操作的开销, 但可以尽可能减少在堆栈中分配局部变量, 减少运算处理时的内存读写, 从而有利于提高代码执行效率.

使用特权

评论回复
14
z_no1|  楼主 | 2011-12-15 09:08 | 只看该作者
关于可重入函数(可再入函数)和模拟堆栈(仿真堆栈)发布时间:2011-07-18 19:43:45  

作者:xzp21st  邮箱: tyter1223@163.com 撰文辛苦,转载请注明作者及出处

关键字:keilc51,模拟堆栈,可重入函数调用,参数传递,C?XBP,C?ADDXBP

摘要:本文较详细的介绍了keilc51可再入函数和模拟堆栈的一些概念和实现原理,通过一个简单的程序来剖析keilc51在大存储模式下可重入函数的调用过程,希望能为keilc51和在51系列单片机上移植嵌入式实时操作系统的初学者提供一些帮助。



1、  关于可重入函数(可再入函数)和模拟堆栈(仿真堆栈)

“可重入函数可以被一个以上的任务调用,而不必担心数据被破坏。可重入函数任何时候都可以被中断,一段时间以后又可以运行,而相应的数据不会丢失。”(摘自嵌入式实时操作系统uC/OS-II)

在理解上述概念之前,必须先说一下keilc51的“覆盖技术”。(采用该技术的原因请看附录中一网友的解释)

(1)局部变量存储在全局RAM空间(不考虑扩展外部存储器的情况);

(2)在编译链接时,即已经完成局部变量的定位;

(3)如果各函数之间没有直接或间接的调用关系,则其局部变量空间便可覆盖。

正是由于以上的原因,在Keil C51环境下,纯粹的函数如果不加处理(如增加一个模拟栈),是无法重入的。举个例子:

void TaskA(void* pd)

{

int a;

//其他一些变量定义



do{

//实际的用户任务处理代码

}while(1);

}



void TaskB(void* pd)

{

int b;

//其他一些变量定义



do{

func();

//其他实际的用户任务处理代码

}while(1);

}



void func()

{

int c;

//其他变量的定义



//函数的处理代码

}



在上面的代码中,TaskA与TaskB并不存在直接或间接的调用关系,因而它们的局部变量a与b便是可以被互相覆盖的,即它们可能都被定位于某一个相同的RAM空间。这样,当TaskA运行一段时间,改变了a后,TaskB取得CPU控制权并运行时,便可能会改变b。由于a和b指向相同的RAM空间,导致TaskA重新取得CPU控制权时,a的值已经改变,从而导致程序运行不正确,反过来亦然。另一方面,func()与TaskB有直接的调用关系,因而其局部变量b与c不会被互相覆盖,但也不能保证func的局部变量c不会与TaskA或其他任务的局部变量形成可覆盖关系。



根据上述分析我们很容易就能够判断出TaskA和TaskB这两个函数是不可重入的(当然,func也不可重入)。那么如何让函数成为可重入函数呢?C51编译器采用了一个扩展关键字reentrant作为定义函数时的选项,需要将一个函数定义为可重入函数时,只要在函数后面加上关键字reentrant即可。



       与非可重入函数的参数传递和局部变量的存储分配方法不同,C51编译器为可重入函数生成一个模拟栈(相对于系统堆栈或是硬件堆栈来说),通过这个模拟栈来完成参数传递和存放局部变量。模拟栈以全局变量?C_IBP、?C_PBP和?C_XBP作为栈指针(系统堆栈栈顶指针为SP),这些变量定义在DATA地址空间,并且可在文件startup.a51中进行初始化。根据编译时采用的存储器模式,模拟栈区可位于内部(IDATA)或外部(PDATA或XDATA)存储器中。如表1所示:



存储模式
       

栈指针
       

栈区域

Small
       

?C_IBP(1字节)
       

间接访问的内部数据存储器(IDATA),栈区最大为256字节

Compact
       

?C_PBP(1字节)
       

分页寻址的外部数据存储器(PDATA),栈区最大为256字节

Large
       

?C_XBP(2字节)
       

外部数据存储器(XDATA),栈区最大为64K

表1

注意:51系列单片机的系统堆栈(也叫硬件堆栈或常规栈)总是位于内部数据存储器中(SP为 8位寄存器,只能指向内部),而且是“向上生长”型的(从低地址向高地址),而模拟栈是“向下生长”型的。



2、  可重入函数参数传递过程剖析

在进入剖析之前,先简单讲讲c51函数调用时参数是如何传递的。简单来说,参数主要是通过寄存器R1~R7来传递的,如果在调用时,参数无寄存器可用或是采用了编译控制指令“NOREGPARMS”,则参数的传递将发生在固定的存储器区域,该存储器区域称为参数传递段,其地址空间取决于编译时所选择的存储器模式。利用51单片机的工作寄存器最多传递3个参数,如表2所示。



传递的参数
       

char、1字节指针
       

int、2字节指针
       

long、float
       

一般指针

第一个参数
       

R7
       

R6,R7
       

R4~R7
       

R1,R2,R3

第二个参数
       

R5
       

R4,R5
       

R4~R7
       

R1,R2,R3

第三个参数
       

R3
       

R2,R3
       


       

R1,R2,R3

表二

举两个例子:

func1(int a):“a”是第一个参数,在R6,R7中传递;

func2(int b,int c, int *d):“b”在R6,R7中传递,“c”在R4,R5中传递,“*d”则在R1,R2,R3中传递。

至于函数的返回值通过哪些寄存器或是什么方法传递这里就不说了,大家可以看看c51的相关文档或是书籍。



好了,接下来我们开始剖析一个简单的程序,代码如下:

int fun(char a, char b, char c, char d ) reentrant  //为了分析简单,参数都是char型;

{

       int j1,j2;



       j1 = a + b + c +d;

       j2 = j1 + 10;

       return j2;

}



main()

{

       int i;

       i = fun(1,2,3,4);      

}



程序很简单,废话少说,下面跟我一起看看c51翻译成的汇编语言是什么样子的。   

main()

{

int i;

i = fun(1,2,3,4);         

MOV      DPTR,#0xFFFF        ; 模拟栈指针C?XBP最初指向0xFFFF+1

LCALL    C?ADDXBP(C:00A6)       ;调用C?ADDXBP子程序,调整模拟栈指针C?XBP

;指向0xFFFF

MOV      A,#0x04                         ;无寄存器可用,第四个参数直接压入模拟栈

MOVX     @DPTR,A                     ;

MOV      R3,#0x03                        ;参数3通过R3传递,见表2

MOV      R5,#0x02                        ;参数2过R5传递,见表2

MOV      R7,#0x01                        ;参数1通过R7传递,见表2

LCALL    fun(C:0003)            ;调用fun函数

MOV      DPTR,#C_STARTUP(0x0000) ; fun函数返回值(int型)通过R6,R7传递回来

;并存储在外部数据存储器0x0000和0x0001处

;(int型为两个字节)

MOV      A,R6

MOVX     @DPTR,A

INC      DPTR

MOV      A,R7

MOVX     @DPTR,A

}

RET      ;main返回



说明:模拟栈指针最初在startup.a51中初始化为0xFFFF+1;由以上汇编代码可以看出参数是从右往左扫描的。



接下来看看fun的汇编代码:(很长,大家耐心看吧,有些可以跳过的)



C:0003

MOV      DPTR,#0xFFFF      

LCALL    C?ADDXBP(C:00A6)       ;调整模拟栈指针C?XBP=C?XBP-1

MOV      A,R3

MOVX     @DPTR,A                     ;R3中的值(参数3)压入模拟栈

MOV      DPTR,#0xFFFF              

LCALL    C?ADDXBP(C:00A6)      ;调整模拟栈指针C?XBP=C?XBP-1

MOV      A,R5

MOVX     @DPTR,A            ;R5中的值(参数2)压入模拟栈

MOV      DPTR,#0xFFFF      

LCALL    C?ADDXBP(C:00A6)       ;调整模拟栈指针C?XBP=C?XBP-1

MOV      A,R7

MOVX     @DPTR,A                     ;R7中的值(参数1)压入模拟栈

MOV      DPTR,#0xFFFC

LCALL    C?ADDXBP(C:00A6)   ;继续调整模拟栈指针C?XBP=C?XBP-4,为放两个

;局部int变量做准备

j1 = a + b + c +d;

MOV      DPTR,#0x0005

LCALL    C?XBPOFF(C:00CA)    ;通过C?XBP的值调整DPTR使其指向模拟栈中第

;一个参数,此时DPTR=0xFFFF

;注意:C?XBPOFF不改变C?XBP的值

MOVX     A,@DPTR

MOV      R7,A                 ;取出参数1

MOV      A,R7

。。。。。。

。。。。。。;省略,完成取参数2,取参数3,取参数4并相加

。。。。。。

MOV      DPH(0x83),?C_XBP(0x08)

MOV      DPL(0x82),0x09        ;0x09就是?C_XBP+1

MOV      A, R6

MOVX     @DPTR,A

INC      DPTR

MOV      A,R7

MOVX     @DPTR,A         ;计算结果j1压入模拟栈



j2 = j1 + 10;

。。。。。。

。。。。。。

。。。。。。;省略,完成j2=j1+10,并把计算结果j1压入模拟栈



return j2;

MOV      DPH(0x83),?C_XBP(0x08)

MOV      DPL(0x82),0x09

INC      DPTR

INC      DPTR

MOVX     A,@DPTR

MOV      R6,A

INC      DPTR

MOVX     A,@DPTR

MOV      R7,A        ;从模拟栈取出j2送入R6,R7

}   

MOV      DPTR,#?C_XBP(0x0008)      

LCALL    C?ADDXBP(C:00A6)              ;fun要返回,释放模拟栈,使C_XBP指向0xffff

RET  



说明:模拟栈结构如下

参数4

参数3

参数2

参数1

j1低字节

j1高字节

J2低字节

J2高字节



接下来说明两个重点子函数C_ADDXBP和C_XBPOFF

                 C?ADDXBP:

MOV      A,0x09                  ;0x09即为C_XBP

ADD      A,DPL(0x82)                      ;以下到第一个RET之前即完成:C_XBP+DPTR

MOV      DPL(0x82),A

MOV      A,?C_XBP(0x08)

ADDC     A,DPH(0x83)

MOV      DPH(0x83),A

CJNE     A,?C_XBP(0x08),C:00B9

MOV      0x09,DPL(0x82)

RET



C:00B9   

JBC      EA(0xA8.7),C:00C2  ;中断开着吗?开着就把它关了(清0),然后跳到C:00C2

MOV      0x09,DPL(0x82)    ;中断本来就关着,安全,下面的行动不会被打断,把新

;的模拟栈指针赋给C_XBP

MOV      ?C_XBP(0x08),A

RET  



C:00C2  

MOV      0x09,DPL(0x82)  

MOV      ?C_XBP(0x08),A

SETB     EA(0xA8.7)        ;开中断

RET     

                 C?XBPOFF:  ;此函数的功能一看就明白,即完成DPTR=C_XBP+DPTR

MOV      A,0x09            

ADD      A,DPL(0x82)

MOV      DPL(0x82),A

MOV      A,?C_XBP(0x08)

ADDC     A,DPH(0x83)

MOV      DPH(0x83),A

RET     



终于到尾声了,最后重点说明啦~~~

模拟堆栈是向下生长的,C_XBP最初等于0xffff+1,那么请看下面这句

MOV      DPTR,#0xFFFF      

LCALL    C?ADDXBP(C:00A6)      

(0xffff+1)+0xffff = 0xffff

即C_XBP -1;



再看

MOV      DPTR,#0xFFFE      

LCALL    C?ADDXBP(C:00A6)

即C_XBP-2



再看

MOV      DPTR,#0xFFFE      

LCALL    C?ADDXBP(C:00A6)

即C_XBP-3

。。。

其实是这样:加0xffff相当与减1,加0xfffe相当与减2,加0xfffd相当于减4。。。。。。为啥,就不用说了吧:)



结束语:

经过了几天的研究,终于写了个总结报告,算是自己的一点小小成就吧,错误之处在所难免,希望能够同大家一起讨论问题,共同进步。





参考文献:

1、徐爱钧,彭秀华 《单片机高级语言C51windows环境编程与应用》电子工业出版社 2001

2、彭光红,构造一个51单片机的实时操作系统。



附录:

在其它环境下(比如PC,比如ARM),函数重入的问题一般不是要特别注意的问题.只要你没有使用static变量,或者指向static变量的指针,一般情况下,函数自然而然地就是可重入的.

但C51不一样,如果你不特别设计你的函数,它就是不可重入的.

引起这个差别的原因在于:一般的C编译器(或者更确切点地说:基于一般的处理器上的C编译器),其函数的局部变量是存放于堆栈中的,而C51是存放于一个可覆盖的(数据)段中的.

至于C51这样做的原因,不是象有些人说的那样,为了节约内存.事实上,这样做根本节约不了内存.理由如下:

1) 如果一个函数func1调用另一个函数func2,那么func1,func2的局部变量根本就不能是同一块内存.C51还是要为他们分配不同的RAM.这跟使用堆栈相比,节约不了内存.

2) 如果func1,func2不是在一个调用链上,那么C51可以通过覆盖分析,让它们的局部变量共享相同的内存地址.但这样也不会比使用堆栈节约内存.因为既然它们是在不同的调用链上,那么当其中一个函数运行时,那么另外一个函数必然不在其生命期内,它所占用的堆栈也已释放,归还给系统.

真实的原因(C51使用覆盖段作为局部变量的存放地的原因)是:

51的指令系统没有一个有效的相对寻址(变址寻址)的指令,这使得使用堆栈作为变量的代价太过昂贵.

使用堆栈存放变量的一般做法是:

进入函数时,保留一段堆栈空间,作为变量的存放空间,用一个可作为基址寻址的寄存器指向这个空间,通过加上一个偏移量,就可以访问不同的变量了.

例如: MOV EAX, [EBP + 14];X86指令

LDR R0, [R12, #14];ARM指令

都可以很好的解决这个问题.

但51缺少这样的指令.

*其实,51中还是有2个可变址寻址的指令的,但不适合访问堆栈的局部变量这样的场合.

MOVC A, @A+DPTR

MOVC A, @A+PC

所以,C51有个特别的关键字: reentrant 用来解决函数重入的问题.

使用特权

评论回复
15
z_no1|  楼主 | 2011-12-15 09:09 | 只看该作者
所以51写惯了,换成ARM风格要变。

使用特权

评论回复
16
幸福至上| | 2014-11-28 09:29 | 只看该作者
嗯,不错,Keil C51编译链接局部变量时,如果分析不存在串行调用关系,就会为他们的局部变量从同一个基地址开始分配变量地址,从编译生成的.m51文件中就可以看出来。所以如果要在Keil 51中使用一个可重入函数,则必须进行可重入声明。这也说明了,C语言编程是容易上手,但要真正的提高程序效率,避免很多BUG,对一些底层的东西进行了解是必要的。另外,不知道楼主有没有研究过Keil C51提供的动态分配函数:malloc,free,很想使用下,但最近看网上很多朋友说,该函数有BUG.

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

223

主题

2644

帖子

10

粉丝