Recall from the previous lab, a function call implies a transfer (branch, jump) to an address representing the entry point of the function, causing execution of the body of the function. When the function is finished, another transfer occurs to resume execution at the statement following the original call. The first transfer is the function call (for invocation); the second transfer is the return. Together, this constitutes the processors call-return mechanism for subroutine execution.
During the subroutine execution, the corresponding code would likely need to use some registers. We would NOT want that code to override the information stored in the registers by the caller.
How to determine which registers are safe to use?
The best practice is to protect the callers registers by saving the information stored there on the stack. After the subroutine completes execution, that information can be easily restored. In other words, we can back-up the registers using the stack before executing the subroutine and restore them afterwards.
Who protects the registers, the caller or the subroutine?
Arguably, the most convenient option is for the subroutine to protect any registers that it uses.
Following is an example of a safe subroutine which does not produce any undesirable side effects. This subroutine copies a c-string from the program memory location labeled msg to the data memory location labeled msg_copy.
Example: Create an Assembler project in Atmel Studio 7 and replace the contents of its main.asm with the program provided in copy_string.asm file on conneX. Build the program and debug it. Observe that the initial values in r16 and r30 are changed while the subroutine executes, but then they are restored before the program reaches the end-loop (done: rjmp done). Verify the contents of the stack as the program executes and the values are pushed onto it.
Below is the diagram of the internal data memory (SRAM) when lpm r16, Z+ is executed for the first time.
Address | content | details | notes |
0x0000 ~0x001F | General Purpose Registers | ||
0x0020 ~0x005F | 64 I/O Registers | ||
0x0060 ~0x01FF | 416 extended I/ORegisters | ||
0x0200 | .DSEG | ||
0x21F7 | <-SP | 0x21F7 is stored in the stack pointer register | |
0x21F8 | R16 | saved register | In the subroutine copy_string, the registers to be used by the subroutine are pushed onto the stack to protect them from being changed. |
0x21F9 | XL (R26) | saved register | |
0x21FA | XH (R27) | saved register | |
0x21FB | ZL (R30) | saved register | |
0x21FC | ZH (R31) | saved register | |
0x21FD | high(ret) | return address | The CPU pushes the return address onto the stack automatically when call copy_string is executed. |
0x21FE | mid(ret) | return address | |
0x21FF | low(ret) | return address |
Recall that ATmega2060 has 256KB of word-addressable program memory, and thus the program counter (PC) needs to count as high as 128K word addresses (217 words) (refer to page 7, Table 2-1 of the datasheet). Therefore, the PC register uses 3 bytes, which are stored on the stack when a call instruction executes. Then, the called subroutine stores the protected registers on top of the return address. After the registers are restored, the return address is on the top of the stack again when the ret instruction executes and writes the top three bytes on the stack to the PC register, thereby returning the control flow back to the instruction which immediately follows the initial call instruction.
- Passing (and returning) parameters via the stack.
There are two ways to pass parameters to a subroutine: by value and by reference. Below are two examples illustrating each method.
Example 1 (pass by value):
Consider the following C function that takes two 8-bit unsigned integers as parameters, multiplies them, and returns the result.
uint16_t multiply(uint8_t multiplicand, uint8_t multiplier) {
uint16 result = multiplicand * multiplier;
return result;
}
An equivalent Assembly function is provided in the multiply_by_value.asm file on conneX. Here, the caller (main program) pushes the 8-bit values of the two parameters onto the stack and also reserves the required 16-bit space on the stack for the return value. The function protects all registers that it uses, retrieves the parameters from the stack, multiplies the integers, and stores the result in the reserved location on the stack before returning.
Download the multiply_by_value.asm file and put it in the same location as main.asm for the project that you started earlier in this lab. Then, add the file to your project by right-clicking the lab in the Solution Explorer window and choosing Add->Existing item:
Then, right-click the file and choose Set As EntryFile:
Only one file can be set as EntryFile at any time; this indicates which Assembly file will be built and debugged by the Atmel Studio.
Build and debug the solution. Observe the stack memory and SP as you step through the program. For convenience, use a breakpoint at the instruction immediately after the multiply loop and the debuggers run command to skip over the loop execution. Below is the stack frame diagram of the stack contents when the instruction in ZH, SPH is executed. An equivalent diagram is also available in the comments right before the function code in the multiply_by_value.asm file.
Address | content | details | notes |
0x0200 | .DSEG | ||
0x21F1 | <-SP | 0x21F1 is stored in the stack pointer register | |
0x21F2 | zero | saved register R0 | The registers to be used by the multiply subroutine are pushed onto the stack at the very beginning of the subroutine to protect their values from being changed by the subroutine. |
0x21F3 | result_low | saved register R4 | |
0x21F4 | result_high | saved register R3 | |
0x21F5 | multiplier | saved register R2 | |
0x21F6 | multiplicand | saved register R1 | |
0x21F7 | ZH | saved register R30 | |
0x21F8 | ZL | saved register R31 | |
0x21F9 | high(ret) | return address | The CPU pushes the return address (the address of the next command) onto the stack automatically when call add_num is executed. |
0x21FA | mid(ret) | return address | |
0x21FB | low(ret) | return address | |
0x21FC | 0x00 | result_high | |
0x21FD | 0x00 | result_low | |
0x21FE | 0xCD | parameter 2 | The caller (in this case the main program) pushes these parameters onto the stack. |
0x21FF | 0xAB | parameter 1 |
Example 2 (pass by reference):
Below is the stack frame diagram for a very similar multiplication function as above, except instead of the values, their corresponding memory addresses are passed to the multiply subroutine.
Address | content | details | notes |
0x0200 | .DSEG | ||
<-SP | 0x21F1 is stored in the stack pointer register | ||
0x21EE | zero | saved register R0 | The registers to be used by the multiply subroutine are pushed onto the stack at the very beginning of the subroutine to protect their values from being changed by the subroutine. |
0x21EF | result_low | saved register R4 | |
0x21F0 | result_high | saved register R3 | |
0x21F1 | multiplier | saved register R2 | |
0x21F2 | multiplicand | saved register R1 | |
0x21F3 | YH | saved register R28 | |
0x21F4 | YL | saved register R29 | |
0x21F5 | ZH | saved register R30 | |
0x21F6 | ZL | saved register R31 | |
0x21F7 | high(ret) | return address | The CPU pushes the return address (the address of the next command) onto the stack automatically when call add_num is executed. |
0x21F8 | mid(ret) | return address | |
0x21F9 | low(ret) | return address | |
0x21FA | 0x02 | address of answer | The caller (in this case the main program) pushes these addresses onto the stack in big-endian format (most significant byte in the lower memory location than the least significant byte). |
0x21FB | 0x02 | address of answer | |
0x21FC | 0x02 | address num2 | |
0x21FD | 0x01 | address num2 | |
0x21FE | 0x02 | address num1 | |
0x21FF | 0x00 | address num1 |
As in the previous example, download the multiply_by_reference.asm file, put it in the same location as main.asm and the multiply_by_value.asm files, add it to your Atmel Studio solution, and set it as EntryFile. Build and debug the solution. Observe the stack memory and SP as you step through the program. The stack frame diagram above illustrates the stack contents when the instruction in ZH, SPH is executed. An equivalent diagram is also available in the comments right before the function code in the multiply_by_value.asm file.
You may find it helpful to use the stack_frame.docx file provided on conneX or a piece of scrap paper for designing and tracing the stack when answering the questions below.
- For this exercise you will write a modified version of the copy_string function in the main.asm file (from the first section of this lab). First, design the stack for it and then modify this function to take the program memory address and the data memory address via the stack instead of using a pre-defined memory location as it does now. This way you can re-use this code in the future to copy any string(s) from program memory to data memory.
- Extend the program from the previous exercise. First, design the stack for a new function called string_length, and then write the function. This function should take a parameter via the stack (in big-endian format) which contains the data memory address where a c-string resides. The function should count the number of characters in that string and return it by via the stack. Remember, the caller has to reserve the space on the stack for the return value.
- Use the Word file provided on conneX or a blank piece of paper and trace the stack for the recursive function in the triangle_number.asm file, which is located on conneX, from the beginning until the program finishes by reaching the end-loop (done: rjmp done). What does the program do?
Reviews
There are no reviews yet.