Lab 3 - Subroutines and C

The purpose of this lab is 1) to introduce subroutines and 2) to learn to integrate C-code and assembly code.

In this lab, you are given a main function in C (download here).

You will be required to write an assembly function called 'printn'. The definition of this function should look as follows:

void printn ( char *fmt, ... ) ;    /* Where ... is a comma-separated list of integers */

The first parameter is a string which defines two things i) how many numbers are to be printed as determined by the size of the string - note the end of a string is indicated by a zero ('\0') character, and ii) in which format will they be printed in (i.e. o - octal, h - hexadecimal, or d - decimal). The second and subsequent parameters are the integers you want to print. WARNING: the number of integers should match the size of the string.

For example, let's use printn to print the number 10 in octal, hexadecimal, and decimal. The C function call would look like:

printn("ohd",10,10,10);   /* In C, all strings are terminated with a zero byte */

The output should then be:

12 a 10

Along with the C main function are three helper functions to help you with the printing. There is a helper function for printing in each of octal, hexadecimal, and decimal. You are to call these functions from your assembly 'printn' function. The prototypes are below:

void printOct ( int val );
void printHex ( int val );
void printDec ( int val );

In summary, the main program will be responsible for calling your 'printn' function. Then your 'printn' function will need to call a proper print function from one of printOct, printHex, or printDec with the appropriate parameter, as shown in the diagram below:

You will have to determine how the registers and stack are used by the compiler (these conventions form part of the Application Binary Interface or ABI), and making your code consistent with it. Read the sections on "Register Usage" and "Stacks" starting on page 120 of the Nios II Processor Reference Handbook. Study the tables and diagrams there.

You can also use the "make disasm" command to view the output of the compiler. Even with an empty printn function, you can disassemble the program and view the compiler's output. The full command is shown in the Preparation section below, just change the "compile" to "disasm". A sample output is shown below:

C FunctionGenerated Assembly
#define  TEXT   "d"

int main ( )
{
  char* text = TEXT; 
  printn ( text, 16 );
  return 0;
} 
main: 
  addi  sp,sp,-4  /* allocated space in the stack */
  stw ra,0(sp)  /* push return address into the stack */
  movhi r4,257    /* calculate the location of the text */
  addi  r4,r4,-27204  /* string, then place it in r4 */
  movi  r5,16   /* move the value to be printed into r5 */
  call  printn    /* call printn subroutine */
  mov r2,zero   /* set return value to 0 */
  ldw ra,0(sp)  /* pop the return address off the stack */
  addi  sp,sp,4   /* de-allocate space from the stack */
  ret     /* return */

Note that the string parameter is passed as the address of the string in memory in r4, and that 16 gets passed as the 2nd parameter in r5. The example below gives a similar example but for more arguments. Examine it carefully, particularly the arguments that are pushed on the stack and in what order.

C FunctionGenerated Assembly
#define  TEXT   "ddoohh"

int main ( )
{
  char* text = TEXT; 
  printn ( text, 16,17,18,19,20,21 );
  return 0;
} 
main: 
addi    sp,sp,-16
stw     ra,12(sp)
movi    r2,19
stw     r2,0(sp)
movi    r2,20
stw     r2,4(sp)
movi    r2,21
stw     r2,8(sp)
movhi   r4,0
addi    r4,r4,0
movi    r5,16
movi    r6,17
movi    r7,18
call    0 
mov r2,zero ldw ra,12(sp) addi sp,sp,16

Don't forget to declare the printn label as a global symbol since it must be used by another file. Your code should look like this:
.global printn
printn:
  /*
  your code goes here
  */
  ret

Preparation

  1. Study the sections on Register Usage and just below it Stacks starting on page 120 (7-2) of the Nios II Processor Reference Handbook and ending on page 128 (7-10). Answer the following questions:
    1. Which registers are callee-saved?
    2. Which registers are caller-saved?
    3. What is the sp (or r27) register used for?
    4. What is the ra (or r31) register used for? Who saves it?
    5. Does the stack pointer point to the next empty spot, or to the last datum pushed onto the stack?
    6. What register(s) are used to pass parameters (arguments) to a function?
    7. What register(s) are used to return the results of a function?
  2. Write the printn function. Compile it with the following command (assuming you named your printn function lab3_printn.s):
    make SRCS="lab3_main.c lab3_printn.s" JTAGOBJ=jtag.o compile
  3. Assuming printn was called as follows, fill in the contents of the register and stack locations below as seen by the first instruction in your printn function:
    printn("ooohhhddd",8,9,10,11,12,13,14,15,16);
    r4r5r6r7ra
         

    0(sp) 4(sp) 8(sp) 12(sp)16(sp)20(sp)24(sp)
           

In the Lab

Demonstrate your working program on the DE2. To run the program type in the same make command as above, but change the "compile" to "run". To view the printed output, you must open a second Nios II Command Shell, go to your directory, and type "make terminal"