Problem creating a subroutine that converts hexadecimal numbers to corresponding ASCII values

58 Views Asked by At

I have an assignment in school and these are my instrucions:

" The variable mytime is initialized to 59:57, to be read as 59 minutes and 57 seconds. The subroutine tick increments the contents of the mytime variable, once for each iteration of the loop (from main: to b main). The variable mytime stores time-info as four separate digits. Each digit uses four bits, allowing values from 0 through 9. The values 10 through 15 are also technically possible, but are not used."

" You will now write a function that converts time-info into a string of printable characters, with a null-byte as an end-of-string-marker. At the end of file timetemplate.asm, add an assembly- language subroutine with the following specification. Name: The subroutine must be called time2string. Parameters (two): Register $a0 contains the address of an area in memory, suitably large for the output from time2string. The 16 least significant bits of register $a1 contains time-info, organized as four NBCD-coded digits of 4 bits each. All other bits in register $a1 can have any value and must be ignored. Example: register $a0 can contain the address 0x100100017, and register $a1 can contain the value 0x00001653. Return value: None. Required action: The following sequence of six characters must be written to the area in memory pointed to by register $a0.

  1. Two ASCII-coded digits showing the number of minutes, according to the two more significant NBCD-coded digits of the input parameter. Example: '1', '6' (ASCII 0x31, 0x36).
  2. A colon character (ASCII :, code 0x3A).
  3. Two ASCII-coded digits showing the number of seconds, according to the two less significant NBCD-coded digits of the input parameter. Example: '5', '3' (ASCII 0x35, 0x33).
  4. A null byte (ASCII NUL, code 0x00). Notes:You must use the function hexasc to convert each NBCD-coded digit into the corresponding ASCII code. Use the sb instruction to store each byte at the destination. The macros PUSH and POP are useful for saving and restoring registers."

This my main function:

.macro  PUSH (%reg)
    addi    $sp,$sp,-4
    sw  %reg,0($sp)
.end_macro

.macro  POP (%reg)
    lw  %reg,0($sp)
    addi    $sp,$sp,4
.end_macro

    .data
    .align 2
mytime: .word 0x5957
timstr: .ascii "text more text lots of text\0"
    .text
main:
    # print timstr
    la  $a0,timstr
    li  $v0,4
    syscall
    nop
    # wait a little
    li  $a0,2
    jal delay
    nop
    # call tick
    la  $a0,mytime
    jal tick
    nop
    # call your function time2string
    la  $a0,timstr
    la  $t0,mytime
    lw  $a1,0($t0)
    jal time2string
    nop
    # print a newline
    li  $a0,10
    li  $v0,11
    syscall
    nop
    # go back and do it all again
    j   main
    nop 

And the subroutine that i wrote:

time2string:
    PUSH($ra)
    PUSH($a0)
                
    lw $a1, 0($a1)       # Load Argument i.e 0x5957
    andi $t1, $t0, 0xF000    # Extract the MSB of hex (Minutes) 
    srl $t2, $t1, 12     # Right - shift the MSB of hex by 12 (Minutes)
    
    jal hexasc      # Convert the MSB to ASCII
    sw $v0, 0($a0)      # Store the ASCII Character at the memory adress
    addi $a0, $a0, 4    # Move to the next memory adress
    
    andi $t3, $t0, 0x0F00   # Extract the LSB of hex (Minutes)
    srl $t3, $3, 8      # Right - shift the LOW bits (Minutes)
    
    jal hexasc      # Convert LSB to ASCII
    sw $v0, 0($a0)      # Store ASCII character at the memory adress
    addi $a0, $a0, 4    # Move to the next memory adress
    
    li $v0, 0x3A        # Load ASCII Value for ':'
    jal hexasc      # Convert to ':' 
    addi $a0, $a0, 1    # Move to the next memory adress
    
    andi $t4, $t0, 0x00F0   # Extract the MSB of hex (Seconds) 
    srl $t4, $t4, 4     # Right-Shift so that hexasc can convert to ASCII
    
    jal hexasc      # Convert MSB to ASCII
    sw $v0, 0($a0)      # Store the result
    addi $a0, $a0, 4    # Move to the next memory adress
    
    andi $t5, $t0, 0x000F   # Extract the LSB of hex (Seconds) 
    jal hexasc      # Convert MSB to ASCII
    sw $v0, 0($a0)      # Store the result
    
    POP($a0)
    POP($ra)
    
    jr $ra
    nop

The subroutine "Hexasc", I also created which works fine. It works by taking the LS4B of an adress and converting those bits 0-9 to ASCII 0-9. And bits with values 10-15 = A - F.

My problem is that I don't know how to make use of the PUSH and POP macros and I also get this error message after running the program instruction by instruction: Error in C:\Labb 1\timetemplate_oklar.asm line 103: Runtime exception at 0x00400120: address out of range 0x00005958 I'm a complete beginner with MIPS, and I really don't understand what I'm supposed to do.

1

There are 1 best solutions below

1
Erik Eidt On

As far as debugging the exception goes, you need to identify what machine code instruction is at memory location 0x00400120.

Then why it is accessing location 0x05958.

My guess is a type mismatch — your datum mytime: .word 0x5957 — is very close in value to the offending access location, so the logic error is probably a type mismatch.  The offending code is dereferencing a value that is not a pointer.


You need to study the calling convention, if that is the intent of the assignment — to follow the standard calling convention.

There are several different kinds of registers:

  • argument registers
  • scratch registers
  • return value registers
  • return address register
  • stack pointer register

The argument registers are used to pass parameters.  So, if a function has a signature such as int time2string (char *where, int value ) that means that

  • $a0 is used to pass where
  • $a1 is used to pass value
  • $ra is used to pass return address

Further, since these three registers are scratch (aka call clobbered), we cannot expect them to survive further function calling from within the body of time2string (which calls hexasc).  This means that if you need the values initially passed to time2string in any of those registers after a nested call (e.g. to hexasc), you have to save them somewhere first.  This is where the PUSH and POP come in, though these macros are reflective of x86 32-bit calling conventions rather than how things are commonly done on MIPS or RISC V.

For an aside, some instructors like to work with these concepts (push & pop) and though they do work, they are not actually used like this on MIPS or RISC V in practice (however, other architectures, namely x86 do this).  On these RISC processors, stack frames sizes are computed once for the whole function and allocated (and deallocated) en masse rather than by individual pushes (and pops).  To digress further, those macros should be using addiu instead of addi because pointer arithmetic is unsigned arithmetic and you don't want signed overflow checking there.

Pushing $ra at the beginning of time2string makes sense: the function will repurpose $ra to make the call to hexasc, and yet will still need the original incoming value of $ra to return to its caller (via the jr $ra at the end).

Pushing the other two also makes sense.  Since they were pushed, they also have to be popped, though only $ra value is needed, not $a0 or $a1.  However, while the value of $a0 and $a1 do not need to restored (the caller doesn't care about those, they were scratch anyway) the popping of those other two is needed to restore the stack pointer to where it must go — if you have to use these macros (without these macros, we would simply reload $ra from the stack location and then pop all these with one addiu).

Ok, so then continue to follow the calling convention for the nested call to hexasc:

  • pass parameters in the argument registers as per the signature of hexasc
  • expect scratch registers to be clobbered by the call
  • expect return value in $v0.

So, you'll have to pop $a0 and/or $a1 after a call to hexasc before you can use them (as in for the sw $v0, 0($a0) and the addiu $a0, $a0, 4.  Before the next call to hexasc, you'll also have to repush them.


Note in one case you're adding 4 to $a0 and another adding 1.  This is suspicious in that adding for is done for word pointers, while 1 for byte pointers and counters.