Implementing function calls for 8051

不问归期 提交于 2019-12-07 10:10:51

问题


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


回答1:


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).



来源:https://stackoverflow.com/questions/25274153/implementing-function-calls-for-8051

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!