51单片机的仿真栈(模拟栈/可重入栈)

51单片机的仿真栈(又叫模拟栈、或者可重入栈)。

首先来看,51的系统栈(又叫系统栈,或者硬件栈),就是SP所指向的栈,他是一个满增栈(注释1),位于片内RAM的128 bytes之中,上电之后系统堆栈指针SP的初值等于多少呢?这个要从51的启动文件来分析,启动文件中有这样的汇编代码:

?STACK SEGMENT IDATA ;定义一个片内数据段,段名:?STACK

RSEG ?STACK ;选择之前定义过的一个可重定位的段?STACK,下面的汇编语句将会被放置到该段,直到遇到下一个段定位指令,例如CSEG/RSEG。

DS 1 ;预留存储区命令。声明先占用一个字节的空间,在编译时,这个预留的空间不会被其他变量所使用。在这里的意义是,给硬件栈分配1个byte(实际这样是有问题的,应该为硬件栈预留更多空间)

还有:

MOV SP,#?STACK-1

由上可见,SP被初始化为#?STACK-1,在#?STACK地址处,DS指令预留了N个字节的空间,这些空间就是硬件栈的空间

但启动文件的代码中,DS 1相当于只给硬件栈预留了1个字节,这实际上会出问题,原因如下:片内RAM中会有多个数据段,只要使用XX SEGMENT IDATA指令即可在片内RAM中声明一个数据段XX,如果整个工程程序中,声明了多个数据段,?STACK数据段就只是片内RAM中众多数据段中的一个,如果只给?STACK段预留1个字节,而?STACK数据段后面又有别的数据段,那么我们的硬件栈就只有1个字节了,一旦发生中断,CPU寄存器自动入栈立即导致栈溢出,溢出后踩了别的变量的内存,程序基本崩溃;对于这个问题,keil是这样处理的:keil在链接阶段总是把?STACK数据段链接为片内RAM中的最后一个数据段,即使我们只给他预留了1个字节,那也不要紧,反正该段后面没有别的变量占用,只要SP别超出0X7F(片内RAM地址的上限)就行了。通过观察.m51(map文件)我们发现,keil确实是把?STACK数据段放到了片内RAM的最后,下面是某个51工程生成的map文件摘抄:

* * * * * * * D A T A M E M O R Y * * * * * * *

REG 0000H 0008H ABSOLUTE "REG BANK 0"

DATA 0008H 0002H UNIT ?C?LIB_DATA

IDATA 000AH 000DH UNIT ?ID?UCOS_II

0017H 0009H *** GAP ***

BIT 0020H.0 0000H.1 UNIT ?BI?SERIAL

0020H.1 0000H.7 *** GAP ***

IDATA 0021H 0041H UNIT ?STACK ; 作者注:就是这一行!

* * * * * * * X D A T A M E M O R Y * * * * * * *

XDATA 0000H 080EH UNIT ?XD?SERIAL

XDATA 080EH 0804H UNIT ?XD?MAIN

XDATA 1012H 0490H UNIT ?XD?UCOS_II

XDATA 14A2H 005CH UNIT _XDATA_GROUP_

为避免系统栈不够用,一个比较稳妥的办法就是,用汇编指令DS给?STACK数据段预留更多的空间,上面这个51工程中在另一个汇编文件中又给?STACK数据留出了40H个字节,这样总共就有41H个字节了。这样做的好处是可以在编译链接阶段即可排查堆栈错误,举个例子: 假设片内RAM中的数据段有很多,以至于,除了?STACK数据段之外,片内RAM只剩2个字节了,而?STACK数据段我们只默认采用了启动文件中的配置预留一个字节,这样编译没有任何问题,keil给编译通过了,但是运行过程中系统栈只有2个字节,肯定是分分钟就发生栈溢出,然后崩溃;假设片内RAM中的数据段有很多,以至于,除了?STACK数据段之外,片内RAM只剩2个字节了,而如果我们给?STACK数据段用DS指令分配40H个字节,这样keil在编译时就会发现51的片内RAM不足而报错,无法编译,从而在编译链接阶段帮助我们发现堆栈问题。

继续上面的问题,SP复位后的初值是多少,SP复位后等于0X07,但是立即就被启动文件通过语句MOV SP,#?STACK-1给改掉了,所以在进入main函数时SP的值是启动文件修改后的值,也即#?STACK-1(注,很好理解,这里-1是满增栈的特性),那么#?STACK的值又是多少呢?看上面的汇编语句?STACK SEGMENT IDATA,这一句声明?STACK段为一个可重定位的段,也就是说,?STACK段的首地址(#?STACK)在编译器进行程序链接时才能确定下来,也就是说,#?STACK的值是在链接时由编译器自动分配的,编译阶段不分配。仍然以上面摘抄的这段map文件为例,我们发现,?STACK段的起始地址是0021H,也就是说,#?STACK就等于21H。

仿真栈是keil为51生成可重入函数时用的(通过给函数使用关键词 REENTRANT限定,可使该函数具备可重入特性),对于STM32来说,默认生成的函数(不含全局变量和静态局部变量的函数)就是可重入的,而keil为51生成的函数,即使这个函数不含全局变量和静态局部变量,默认情况下keil也不会把这个函数汇编成可重入的,我认为keil主要是考虑到51的片内RAM匮乏,在不外接RAM的情况下,函数如果被编译为可重入的,可重入函数的执行需要占用一定的栈空间(尤其是由可重入函数嵌套调用产生的长的调用链,所需的栈更多)。

可重入函数在执行过程中是需要使用栈的,那么51的可重入函数使用的栈在哪呢?是SP指向的那个系统栈吗?答案是:不是。下面是解释:

当我们给51外扩了大的片外RAM时,就不用担心RAM不够的问题了,但是还有一个问题,系统栈指针SP只能寻址0~7FH共128字节的空间,可重入函数肯定不允许被编译成使用系统栈,否则,就算外扩了RAM,这个外扩RAM又无法供系统栈来使用,外扩RAM就没有意义了,所以keil为51打造了一个仿真栈的概念,keil在启动文件中声明了一个1或2字节的变量作为栈指针,这个栈指针的名字和大小根据编译模式的不同而不同,以大编译模式(注释2)为例,大编译模式下,启动文件中的XBPSTACK常量需要程序员手动设置为1,这样启动文件中使用到的条件编译,将会引用到一个2字节的仿真栈指针?C_XBP,由于keil把仿真栈作为满减栈,所以这个仿真栈指针?C_XBP被初始化为片外RAM地址的最大值加1,若我们外接了一个64K的片外RAM,该RAM的最大地址是0XFFFF,那么栈指针?C_XBP被初始化为0XFFFF+1=溢出为0x0000。再举一个小编译模式的例子,小编译模式是用来给没有外扩RAM的51用的,这样51只能使用片内0~127共128字节的RAM(这128RAN中还有一部分是Rn等,留给程序可用的RAM就更少了),在小编译模式下,keil给51生成的仿真栈指针名叫?C_IBP,同时需要程序员手动把IBPSTACK常量设置为1,指针?C_IBP的初值被初始化为可用RAM的最大地址(127)加1,也即0x7f+1。关于小编译模式small、压缩编译模式compact、大编译模式large在堆栈处理上方面的不同,可参考这篇文章点击打开链接,如果链接挂了,可自行搜索:《Keil模式设置和编程事项》。

注释1:满增栈,满指的是SP总是指向最后一个入栈的字节的地址,增指的是每入栈一次,SP变大。相应的,还有空增栈、空减栈、满减栈,空指的是SP总是指向栈中下一个空闲位置的地址。

注释2:如何选择大编译模式:以keil5为例,依次选择->魔术棒->Target选项卡,Memory Model选择Large:var...,Code Rom Size选择Large....

附:举一个不可重入函数使用中可能发生的陷阱,假设有分别有如下两个函数,第一个可重入,第二个不可重入

int add5_re(char a1,char a2,char a3,char a4,char a5) REENTRANT

{

int sum;

sum=a1+a2+a3+a4+a5;

return sum;

}

int add5(char a1,char a2,char a3,char a4,char a5)

{

int sum;

sum=a1+a2+a3+a4+a5;

return sum;

}

这两个函数的形参以及局部变量分配等信息我们查阅.m51文件,分别如下(分号后面的注释是博主自己加上的):

[plain] view plain copy------- PROC _?ADD5_RE

x:0002H SYMBOL a1 ;注意,地址标号前为小x,指a1倍分配到了仿真栈中

x:0003H SYMBOL a2

x:0004H SYMBOL a3

x:0005H SYMBOL a4

x:0006H SYMBOL a5

------- DO

x:0000H SYMBOL sum

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

------- PROC _ADD5

D:0007H SYMBOL a1 ;R7

D:0005H SYMBOL a2 ;R5

D:0003H SYMBOL a3 ;R3

X:14ABH SYMBOL a4 ;注意地址标号前为大X,指外部RAM

X:14ACH SYMBOL a5

------- DO

D:0006H SYMBOL sum ;R6

我们发现,add5中的形参和局部变量a1/a2/a3/sum分到了Rn中,a4/A5分到了外部RAM xdata的绝对地址处,如果我们在main的调用链中和中断函数中都调用了add5这个函数,就会发生错误,假设恰好在main的调用链中执行add5时发生了中断,切换到中断函数中去执行add5,那么main调用链中的a1/a2/a3/sum因为被分到了Rn中,进入中断会切换register BANK,使得main调用链中的a1/a2/a3/sum没有被破坏,得以幸免,但是a4/a5因为被分配到了绝对地址中,在中断执行完add5以后,main链条中的add5的a4/a5肯定会被破坏!!

对于可重入的add5_re函数,即使main调用链和中断同时调用它也不会出现上述被破坏的情形,因为add5_re的形参和局部变量全部都被定义到了仿真栈中(见上述代码注释),main调用链中使用add5_re函数会申请栈空间,中断时add5_re又会申请新的栈空间。

还要注意的是,因为keil编译51程序时,使用了覆盖技术(不同函数的形参和局部变量可分时共享同一个绝对内存单元),这也有可能产生陷阱,假设这样一种情况:有一个函数func2( )的局部变量b在编译后被分配到了绝对xdata的地址14ABH处,和上文的add5的a4变量共享内存,这种情况下,即使 { func2( )仅在中断中被调用,main调用链中不调用func2( )}、且{ add5仅在main调用链中被调用,中断中不调用add5 },也会出问题,原因是显而易见的,如果在add5执行过程中发生中断,中断中使用过变量b之后,会破坏add5中的变量a4。究其原因在于,共享地址的编译方式生成的函数,只要分时调用就不会产生被破坏的情形,但是发生中断导致了分时机制被破坏,以至于产生了同时调用。

结论:中断中使用的函数,要么是可重入的,要么是该函数的局部变量全部是独享内存单元的。

原文地址:https://www.cnblogs.com/jikexianfeng/p/10327940.html

时间: 2024-10-29 10:27:52

51单片机的仿真栈(模拟栈/可重入栈)的相关文章

【数据结构】用C++编写栈及基本操作(包括入栈,出栈,获得栈顶,摧毁,清空等等)

//[数据结构]用C++编写栈及基本操作(包括入栈,出栈,获得栈顶,摧毁,清空等等) //头文件 #ifndef _SEQ_STACK_ #define _SEQ_STACK_ #include <iostream> using namespace std; template <class Type> class SeqStack { public: SeqStack(size_t sz=INIT_SIZE) { capacity = sz > INIT_SIZE ? sz

顺序栈:创建&amp;初始化、入栈、出栈、计算栈中有效数据长度、获取栈顶数据、清空栈、销毁栈

/*    顺序栈的实现:    初始化    入栈    出栈    计算栈的有效数据长度    获取栈顶数据    清空栈    销毁栈*/ #include <stdio.h>#include <stdlib.h> #define ElemType int typedef struct __stackInfo{    ElemType *data;    unsigned int top;    unsigned int capacity;} stackInfo;/* 初始化

hdu1515 dfs栈模拟

Anagrams by Stack Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 1513    Accepted Submission(s): 690 Problem Description How can anagrams result from sequences of stack operations? There are tw

数据结构和算法之栈和队列一:两个栈模拟一个队列以及两个队列模拟一个栈

今天我们需要学习的是关于数据结构里面经常看到的两种结构,栈和队列.可以说我们是一直都在使用栈,比如说在前面递归所使用的的系统的栈,以及在链表倒序输出时介绍的自定义栈类Stack和使用系统的栈进行递归.那么,在这里我们就讲述一下这两个比较具有特色的或者说关系比较紧密的数据结构之间的互相实现问题. 一:两个栈模拟实现一个队列: 栈的特点是先进后出,然而队列的特点是先进先出. public class Queen(Stack s1,Stack s2){ //实现插入的方法 public void ad

栈的链式存储结构和入栈出栈操作

参考<大话数据结构>P98~99——栈的链式存储结构. 进栈: 出栈: 举个简单的例子: 代码和解释如下(VS2012测试通过): 1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 typedef string status;//用书上推荐的status返回是否成功,C++中的模板类string比字符数组char[]更方便 6 7 //栈的结点 8 //包含data,和指向下一个结点

小猪的数据结构辅助教程——3.1 栈与队列中的顺序栈

小猪的数据结构辅助教程--3.1 栈与队列中的顺序栈 标签(空格分隔): 数据结构 本节学习路线图与学习要点 学习要点 1.栈与队列的介绍,栈顶,栈底,入栈,出栈的概念 2.熟悉顺序栈的特点以及存储结构 3.掌握顺序栈的基本操作的实现逻辑 4.掌握顺序栈的经典例子:进制变换的实现逻辑 1.栈与队列的概念: 嗯,本节要进行讲解的就是栈 + 顺序结构 = 顺序栈! 可能大家对栈的概念还是很模糊,我们找个常见的东西来拟物化~ 不知道大家喜欢吃零食不--"桶装薯片"就可以用来演示栈! 生产的时

栈与队列问题1——出栈序列

问题描述:栈是常用的一种数据结构,有n个元素在栈顶端一侧等待进栈,栈顶端另一侧是出栈序列.你已经知道栈的操作有两种:push和pop,前者是将一个元素进栈,后者是将栈顶元素弹出.现在要使用这两种操作,由一个操作序列可以得到一系列的输出序列.请你编程求出对于给定的n,计算并输出由操作数序列1,2,…,n,经过一系列操作可能得到的输出序列总数. 分析:之前就有看过这种问题.就是火车进站问题,判断序列是否合法,当时是用STL栈做的.这个题只需统计次数,那么,方法就十分简便了,递归和动归都可以实现,当然

实现一个栈,要求实现Push(入栈)、Pop(出栈)、Min(返回最小值的操作)的时间复杂度为O(1)

具体实现如下: #include<iostream> #include<stack> #include<string> #include<assert.h> using namespace std; template<class T> class Stack { public: void Push(const T& x); void Pop(); T& Min(); void PrintS(); private: stack<

C++中栈的出栈,入栈规则:A,B,C,D,E

考题: 栈底至栈顶一次存放元素 ABCD 在第五个元素E入栈之前  栈中元素可以出栈,则出栈序列可能是_____a d___________. a.  ABCED b.  DBCEA   c.  CDABE   d.  DCBEA 分析: 1.假定进栈序列是从小到大排练的(即A<B<C<D<E),则出栈序列中不可能有  “大小中”这种序列,因为在“大数”出栈后,在栈中“中数”是在“小数”上面的,所以只能是先出“中数”再出“小数”2.出栈序列中如包含下列序列则是错误的:CAB,DAB