实现对8051的功能调用 [英] Implementing function calls for 8051

查看:46
本文介绍了实现对8051的功能调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设您有一个没有外部RAM的8051微控制器.内部RAM为128字节,您大约有80字节可用.并且您想编写一种用于堆栈语言的编译器.

假设您要编译RPN表达式 2 3 + .8051具有本机 push pop 指令,因此您可以编写

  push#2推#3 

然后您可以将 + 实施为:

  pop A;将2弹出到寄存器A中弹出B;将3弹出到寄存器B中加A,B;A = A + B推A;将结果压入堆栈 

简单,对吧?但是在这种情况下, + 被实现为嵌入式程序集.如果您想重用此代码并将其放入子例程,该怎么办?幸运的是,8051具有 lcall ret 指令. lcall LABEL 将返回地址压入堆栈并跳转到LABEL,而 ret 返回到堆栈顶部指定的地址.但是,这些操作会干扰我们的堆栈,因此,如果我们执行 lcall 跳转到我们的 + 实现,则第一条指令 pop A 将弹出返回地址,而不是我们要操作的值.

在一种我们事先知道每个函数的参数数量的语言中,我们可以将几个值重新排列在堆栈的顶部,然后将参数放在堆栈的顶部,然后将返回地址进一步往下推.但是对于基于堆栈的语言,我们不知道每个函数将采用多少个参数.

那么,在这种情况下可以采取什么方法来实现函数调用?

以下是8051指令集的描述: http://sites.fas.harvard.edu/~phys123/8051_refs/8051_instruc_set_ref.pdf

解决方案

这是一台非常有限的机器.

好的,最大的问题是您想使用堆栈"来保存操作数,但它也保存返回地址.因此可以解决:将返回地址移开,并在完成后放回去.

您的示例:

 推送#2推#3调用my_add...myadd:流行r6;保存寄信人地址流行音乐r7弹出一个流行音乐b加a,b推一个推r7推r8退回 

我的猜测是保存寄信人地址",恢复寄信人地址"将非常普遍.我不知道如何对保存返回地址"进行空间优化,但是您可以使大多数子例程的末尾通用:

  myadd:流行r6;保存寄信人地址流行音乐r7弹出一个流行音乐b加a,bjmp push_a_return...;编译器常用代码库:push_ab_return:;由在AB中返回答案的子例程使用推bpush_a_return:;由子程序返回A中的答案使用推一个返回: ;由未在寄存器中产生结果的子例程使用推r7推r6退回push_b_return:;由在B中计算答案的子例程使用推bjmpshort返回 

但是,您的大部分麻烦似乎是坚持要将操作数推入堆栈.然后,您将遇到寄信人地址的问题.您的编译器当然可以解决这个问题,但是您遇到麻烦的事实表明您应该做其他事情,例如,如果可以帮助的话,不要将操作数放在堆栈上.

相反,您的编译器还可以生成面向寄存器的代码,并在可能的情况下尝试将操作数保留在寄存器中.毕竟,您可以轻松访问8个(我认为)R0..R7和A和B.

因此,您应该做的是首先弄清楚所有操作数(均由原始程序员命名,以及编译器需要的临时变量(例如3地址代码)和操作都包含在代码中),其次,应用一些操作数.寄存器分配的一种(查找寄存器着色为一个很好的示例),以确定哪些操作数将在R0..R7中,应用相同的技术将未分配给寄存器的命名变量分配给您直接寻址的对象(将它们分配给位置8)例如,"top"(临时),还有第三次临时空间,您需要为其分配一些额外空间(将其位置"top"指定为"top"至64),这将在生成临时空间时将其余空间(位置65至127)强制放入堆栈中(坦率地说,除非您的程序对于8051来说太大,否则我会怀疑这种方案会导致很多堆栈堆积.)

一旦每个操作数都有一个指定的位置,那么代码生成就很容易.如果已在寄存器中分配了操作数,则可以根据需要使用A,B和算术运算来计算它,或者按照三个地址指令的指示使用MOV来填充或存储该操作数.

如果操作数在堆栈上,则将其弹出到A或B中;如果在顶部,则将其弹出;如果将其深层"嵌套在堆栈中,则可能需要进行一些奇特的寻址才能到达其实际位置.如果生成的代码在被调用的子例程中,并且操作数在堆栈中,请使用返回地址保存技巧;否则,请执行以下步骤:如果R6和R7忙,则将返回地址保存在另一个寄存器组中.您可能只需要在每个子例程中最多保存一次返回.

如果堆栈由交错的返回地址和变量组成,则编译器可以实际计算所需变量的位置,并使用堆栈指针中的复杂索引来获取该变量.只有当您处理多个嵌套函数调用时,才会发生这种情况.大多数C实现都不允许这样做(GCC允许).因此,您可以取缔这种情况,也可以根据自己的野心决定处理.

所以对于程序(C风格)

 字节X = 2;字节Y = 3;{字Q = X * Y;呼叫W()}字节S;W(){S = Q;} 

我们可能会分配(使用寄存器分配算法)

  X到R1Y到位置17Q到堆栈S到R3 

并生成代码

  MOV R1,2MOV A,3MOV 17,AMOV A,17MOV B,AMOV A,R1UL推A;Q生活在堆栈上推B呼叫WPOP A;不再需要QPOP B...W:流行音乐R6流行音乐R7POP APOP BMOV R3,BJMP PUSH_AB_RETURN 

您几乎可以获得与此相关的合理代码.(这很有趣).

Say you have an 8051 microcontroller with no external RAM. Internal RAM is 128 bytes, and you have around 80 bytes available. And you want to write a compiler for a stack language.

Say you want to compile an RPN expression 2 3 +. 8051 has native push and pop instructions, so you can write

push #2
push #3

Then you can implement + as:

pop A     ; pop 2 into register A
pop B     ; pop 3 into register B
add A, B  ; A = A + B
push A    ; push the result on the stack

Simple, right? But in this case + is implemented as an inline assembly. What if you want to reuse this code, and put it into a subroutine? Fortunately, 8051 has lcall and ret instructions. lcall LABEL pushes the return address onto the stack and jumps to the LABEL, while ret returns to the address specified at the top of the stack. However, these operations interfere with our stack, so if we do lcall to jump to our implementation of + the first instruction pop A will pop the return address, instead of the value that we want to operate on.

In a language where we knew the number of arguments for each function in advance we could rearrange the few values on top of the stack and put the arguments on top of the stack, and push the return address further down. But for a stack-based language we don't know how many arguments each function will take.

So, what are the approaches one can take to implement function calls in these circumstances?

Here's the 8051 instruction set description: http://sites.fas.harvard.edu/~phys123/8051_refs/8051_instruc_set_ref.pdf

解决方案

This is a pretty limited machine.

OK, the biggest problem is that you want to use the "stack" to hold operands, but it also holds return addresses. So the cure: move the return address out of the way when it is in the way, and put it back when done.

Your example:

    push #2
    push #3
    lcall   my_add
    ...

myadd:
    pop r6     ; save the return address
    pop r7
    pop a
    pop b
    add a, b
    push a
    push r7
    push r8
    ret

My guess is that the "save return address" , "restore return address" are going to be pretty common. I don't know how to space-optimize the "save return address", but you could make the tail end of most subroutines common:

myadd:
    pop r6     ; save the return address
    pop r7
    pop a
    pop b
    add a, b
    jmp  push_a_return

    ...

 ; compiler library of commonly used code:
 push_ab_return: ; used by subroutines that return answer in AB
     push b
 push_a_return: ; used by subroutines that return answer in A
     push a
 return: ; used by subroutines that don't produce a result in register
     push r7
     push r6
     ret

 push_b_return: ; used by subroutines that compute answer in B
     push b
     jmpshort return

However, much of your trouble seems to be the insistence that you are going to push operands onto the stack. Then you have trouble with return addresses. Your compiler can certainly handle that, but the fact that you are having trouble suggests you should do something else, e.g., don't put the operands on the stack if you can help it.

Instead, your compiler could also generate register-oriented code, trying to keep operands in registers whenever possible. After all, you have 8 (I think) R0..R7 and A and B as easily accessible.

So what you should do, is to first figure out what all the operands (both named by the original programmer, and temporaries the compiler needs [say for 3-address code] and operations are in your code. Second, apply some kind of register allocation (look up register coloring for a nice example) to determine which operands will be in R0..R7, apply the same technique to allocate the named variables not assigned to registers to your directly addressable (assign them to locations 8-'top', say), and a third time for temporaries for which you have some additional space (assign them locations 'top' to 64). THis forces the the rest into the stack, as they are generated, having locations 65 to 127. (Frankly, I doubt you'll end up with many in the stack with this scheme unless your program is just too big for the 8051).

Once every operand has an assigned location, code generation is then easy. If an operand has been allocated in a register, either compute it using A, B and arithmetic as appropriate, or a MOV to fill or store it as the three address instruction indicates.

If the operand is on the stack, pop it into A or B if on top; you might to do some fancy addressing to reach its actual location if it is nested "deeply" in the stack. If the generated code is in the called subroutine and an operand is on the stack, use the return address saving trick; if R6 and R7 are busy, save the return address in another register bank. You likely only have to save the return at most once per subroutine.

If the stack consists of interleaved return addresses and variables, the compiler can actually compute where the desired variable is, and use complex indexing from the stack pointer to get to it. That will only happen if you address across multiple nested function calls; most C implementations don't allow this (GCC does). So you can outlaw this case, or decide to handle it depending on your ambition.

So for the program (C style)

 byte X=2;
 byte Y=3;
 { word Q=X*Y;
   call W()
 }     

 byte S;

  W()
    { S=Q; }

we might assign (using the register allocation algorithm)

 X to R1
 Y to location 17
 Q to the stack
 S to R3

and generate code

 MOV R1,2
 MOV A, 3
 MOV 17, A
 MOV A, 17
 MOV B, A
 MOV A, R1
 MUL
 PUSH A   ; Q lives on the stack
 PUSH B
 CALL W
 POP  A   ; Q no longer needed
 POP  B
 ...

 W:
 POP R6
 POP R7
 POP A
 POP B
 MOV R3, B
 JMP PUSH_AB_RETURN

You almost get reasonable code with this. (That was fun).

这篇关于实现对8051的功能调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆