4
\$\begingroup\$

I'm "playing" with ARM assembly on a Raspberry Pi, reading various tutorials and posts to help - no, it's not a college assignment, I'm too old for college! I'm pleased with getting the following code to take a decimal value (between 0 and 255) and output the binary string equivalent to the screen using scanf() and printf() but curious to know though whether those more experienced would consider this a "naive" solution? Are there any "gotchas"? Any better ways of tackling the problem?

I reckon there should be a better way of declaring the string to hold the output and how the string is built from the LSB (using R9)

/* dectobin2.s                                                  */
/* ------------------------------------------------------------ */
/* Converts a decimal number to binary and outputs to screen    */
/* ============================================================ */

.data
.balign 4
value:      .word   0   
binStr:     .asciz  "00000000"
output:     .asciz  "%d in binary is %s\n"
title:      .asciz  "DECIMAL TO BINARY CONVERTER\n\n"
lr_temp:    .word   0
lr_local:   .word   0
prompt:     .asciz  "Enter a positive value (0-255) > "
pattern:    .asciz  "%d"

.text
@ function to convert given value into a binary string
decToBin:
    @ save the link register
    ldr     r2, =lr_local
    str     lr, [r2]
    @ set data values
    mov     r4, r0              @ value in r4 for processing
    mov     r9, #7              @ LSB position in output string
_loop:
    @ repeated division by 2 2
    movs    r4, r4, lsr #1      @ divide by 2, set carry
    bcs     _odd                @ if carry set, remainder is 1
_even:
    mov     r5, #0              @ remainder of 0
    b       _toString
_odd:
    mov     r5, #1              @ remainder of 1
_toString:  
    add     r5, #48             @ convert remainder to ASCII
    ldr     r1, =binStr         @ address of binary string
    strb    r5, [r1, r9]        @ store remainder in string at pos in r9
    sub     r9, r9, #1          @ move to next position in output string
    cmp     r4, #0              @ reached zero?
    ble     _endloop
    b       _loop

_endloop:
    @ restore the link register
    ldr     lr, =lr_local
    ldr     lr, [lr]
    bx      lr

.global main

main:
    @ store the link register
    ldr     r1, =lr_temp
    str     lr, [r1]
    
    @ print a title
    ldr     r0, =title          
    bl      printf
_getValue:
    @ get the value to convert
    ldr     r0, =prompt         @ prep the arguments for scanf
    bl      printf
    ldr     r0, =pattern
    ldr     r1, =value
    bl      scanf

    @ check for valid input (0-255)
    ldr     r0, =value
    ldr     r0, [r0]
    cmp     r0, #255            @ must be <= 255
    bgt     _getValue
    cmp     r0, #0              @ must be >= 0
    blt     _getValue
    
_isValid:
    @ call the function, value is in R0
    bl      decToBin
    
    @ display output
    ldr     r0, =output         @ get address of output string
    ldr     r1, =value          @ get the original value
    ldr     r1, [r1]            @ for first parameter
    ldr     r2, =binStr         @ get the binary string
    bl      printf              @ print the result

    @ restore the link register and exit
    ldr     lr, =lr_temp
    ldr     lr, [lr]
    bx      lr

.global printf      
.global scanf
```
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Here are a number of things that may help you improve your program.

Understand the calling convention

The procedure call standard for ARM says that the registers r0, r1, r2, and r3 can be used and not restored by a called function. Further, if we don't alter lr, there's no need to preserve and then restore it. These are all clues for making your program more efficient.

Know your instruction set

The code currently contains these lines:

    movs    r4, r4, lsr #1      @ divide by 2, set carry
    bcs     _odd                @ if carry set, remainder is 1
_even:
    mov     r5, #0              @ remainder of 0
    b       _toString
_odd:
    mov     r5, #1              @ remainder of 1
_toString:  
    add     r5, #48             @ convert

That is terribly inefficient. We can simplify considerably:

    mov     r5, #48             @ start with ASCII '0'
    movs    r4, r4, lsr #1      @ divide by 2, set carry
    adc     r5, #0              @ add zero + carry
    

Minimize branching

The code ends with this curious combination:

    ble     _endloop
    b       _loop
_endloop:

First, the comment suggests that we're looking for exactly zero and so we should be looking only at the zero flag (beq or bne) rather than doing a signed comparison with ble. Second, why not simply use a single instruction?

    beq     _loop

However, better, see the next suggestion.

Create reusable code

The code currently exits if the shifted value becomes zero. That works, but only once. In other words, if you use the routine to decode 255 and then again to decode the value 3, you will likely be surprised that your routine will falsely give the same binary string for both. The problem is that it relies on the binStr buffer to always contain a string of zeroes. A better approach is to explicitly set each byte of the buffer and instead use the counter you already have in r9. This shortens the code and makes it reusable.

    subs    r9, r9, #1  
    bpl     _loop

Move loop invariants outside the loop

Every iteration through the loop we load the r1 register with the address of binStr even though r1 is never changed within the loop. A more efficient approach would be to load r1 once outside the loop.

Document register usage

One of the keys to being a good assembly language programmer is to carefully manage register use. The most basic requirement is to keep track of how you're using registers, and a good way to do that, both for yourself and future readers of the code is to document the use. Based on the observations in the first suggestion above, here's what I used:

@ Register usage:  
@   r0 - passed value
@   r1 - pointer to current string digit
@   r2 - decrementing digit offset
@   r3 - the current digit

Results

Using all of these suggestions, the resulting routine is only nine instructions.

decToBin:
    ldr     r1, =binStr         @ output string location
    mov     r2, #7              @ start with the LSB 
_loop:
    mov     r3, #48             @ start with ASCII zero
    movs    r0, r0, lsr #1      @ divide by 2, set carry
    adc     r3, #0              @ add the extracted bit
    strb    r3, [r1, r2]        @ store remainder in string at pos in r9
    subs    r2, r2, #1          @ decrement counter
    bpl     _loop               @ end if it has gone negative
    bx      lr

Armed with these suggestions (pun intended!) see if you can perform a similar cleanup on the rest of the code. In particular, there is a much more efficient way to check if the input value is in the appropriate range (hint: movs r1, r0, lsr #8).

Also, just for fun, this answer was composed and tested entirely on a Raspberry Pi running on battery power.

Other ideas

A more generally useful routine would have the caller pass a buffer pointer and buffer length into which the program would put its result rather than using a fixed location. See if you can figure out how to modify the code to do that, and don't forget to place the terminating \0 character.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you Edward for the detailed comments, it's much appreciated. As I've done some further reading since posting I've now encountered the conditionals that can be suffixed to any of the instructions to remove the need to branch, as you've suggested - they're a nice touch! I shall revisit with your other tips - thank you. \$\endgroup\$
    – SimonH
    Commented Feb 22, 2021 at 16:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.