Comment Your Code!#

I can’t stress this enough, please comment your code. ARM comments can be styled using the following:

@ for a single line comment
// also for a single line comment
/* for
   a
   multi-line
   comment */

And while you can comment on every single line like so:

get_order_cost:
  str lr, [sp, -4]! @ Store the link register on the stack
  ldr r1, [r0]      @ Load the customer id
  ldr r2, [r0, 4]   @ Load the items in the order
  str r1, [sp, -4]! @ Store the customer id on the stack 
  str r2, [sp, -4]! @ Store the # of items in the order on the stack 
  @ ...

it’s not always the most helpful, or the best use of your time. Instead, you would be better suited to commenting your functions and segments of your code, and resorting to line by line comments only when what you are doing is particularly tricky or unintuitive.

Here is an example for a function preamble, you don’t have to follow this exact format, however the main idea of covering the following is something you should do when writing your code:

  • What the function does
  • What arguments it takes and where they are
  • What does it return and where is that
  • Are the flags set (if relevant)
@ Reads the led current output state based on a given row 
@ and column index. 
@ --parameters--
@ r0: row index (0-4 with 0 being the top row)
@ r1: column index (0-4 with 0 being the left most column)
@ --return--
@ r0: bit<0> contains led state (1 == On, 0 == Off)
@ flags set
read_led:
@ ...

Functionize Your Code!#

Assembly programs can get very long, very quickly. This can lead to errors, bugs and just plain confusion. Something that you should always try and do is break your code down into logical blocks, or functions, that have a specific purpose.
This will make your life a lot lot lot easier when it comes to debugging your program, because you’ll be able to set breakpoints, use the debugger functions like ‘step over’ etc. and will be able to verify pieces of your code, without having to try and hold the whole program logic in your mind at once.

Follow Calling Convention!#

If you’re writing a function, actually make it a function. This means following calling convention. It’s super important and has its own page.

Maintain Consistent Indentation!#

I can’t tell you the amount of times that I have opened up a students code and seen something like this:

main:
nop
  adds r0, 1
  sub r1, 1
       mov r0, 2
       mov r1 , 3
mul r1, r1
@ ...

It just makes your code hard to read for no reason, instead try and maintain things on the same level, or indent blocks where it makes sense, like in a conditional or loop section

main:
  adds r0, 1
  sub r1, 1
  mov r0, 2
  mov r1, 3
  mul r1, r1
  @ ...

calc:
  mov r0, 0
  mov r1, 10
  calc_loop:
    add r0, 1
    @ ...
    cmp r0, r1
    blt calc_loop
  mov r0, r3
  bx lr

Keep Interrupt Handlers Short and To The Point!#

This has already been covered in lab 10 a bit, especially in task 6, but it is still a good general point that you need to abide by for good quality assembly code.

Interrupt handlers should be as short as they can be to accomplish their task. Whether that is modifying memory address(es) or interacting with MMIO, etc. They should only be doing that and then leaving with bx lr .

If you find that you are writing a significantly long loop, or you have your LED scanning code inside the interrupt handler, or your interrupt handler doesn’t finish with bx lr, then your design is probably wrong.

The job of the interrupt handler is to perform necessary tasks so that the main line of execution changes in some way in response, not to perform the different task itself.

In addition, remember to follow calling convention and save the link register! Interrupt handlers must be treated like a function to operate correctly.

Keep Your Code Structure Logical#

So what does this mean exactly? It is basically a combination of other rules and a general guideline for writing your programs. This relates to:

  • Functions
  • Calling Convention
  • Interrupts
  • etc.

What it is saying is that your code should should make logical sense:

  1. it should be clear that your functions AND interrupt handlers are following calling convention
  2. your code, and any functions called, should be being executed in the right locations, the right amount of times

For point 1, you can help this out by follwing a few rules:

  • Store necessary registers once at the start of a function and restore those same registers at the end of the function
  • If you do need to have a push / pop in the middle of a function, keep the push and pop on the same conditional area
    • This means that if you have a push, don’t put the corresponding pop after a conditional branch as it makes it a lot easier to lose track of what you’re doing with the stack
  • Always exit the function from the same location, even if you could have returned earlier, instead opt for a branch to an end_function label which performs the same clean up

Here is an example of a function / handler following the above rules

.global ExampleFnOrHandler
.type ExampleFnOrHandler, %function
ExampleFnOrHandler:
  push {r4 - r6, lr} @ Store necessary registers once at the start

  mov r5, 10
  subs r3, r5
  beq end_example

  @ don't have a push and a pop in different conditional areas, 
  @ make sure that after a push you will always execute the 
  @ corresponding pop
  push {r0} 
  mov r4, 1
  cmp r0, r4
  beq if_condition
  b else_condition

  if_condition:
    @ if code here
    mov r0, 1
    bl something
    b end_if
  else_condition:
    @ else code here
    mov r0, 2
    bl something_else
    b end_if
  end_if:
  pop {r0}

@ always have functions exit from the same spot, even if you could 
@ have exited earlier, this helps keep the stack consistent
end_example:
  pop {r4 - r6, lr} @ Restore those same registers at the end
  bx lr
.size ExampleFnOrHandler, .-ExampleFnOrHandler

If I were to take the exact same example, and instead do this:

.global ExampleFnOrHandler
.type ExampleFnOrHandler, %function
ExampleFnOrHandler:
  @ don't store necessary registers at the start

  push {r5}
  mov r5, 10
  subs r3, r5
  bne continue
  pop {r5}
  bx lr

continue:
  push {r4}
  mov r4, 1
  cmp r0, r4
  beq if_condition
  b else_condition
   
  if_condition:
    push {r0, lr}
    @ if code here
    mov r0, 1
    bl something
    b end_if
  else_condition:
    push {r0, lr}
    @ else code here
    mov r0, 2
    bl something_else
    b end_if
  end_if:
  pop {r0, lr}

end_example:
  pop {r5} 
  bx lr
.size ExampleFnOrHandler, .-ExampleFnOrHandler

Notice how its now a lot harder to track the fact that I am keeping the stack correct and saving registers as necessary? By following the previous example you will save yourself a load of headaches and make debugging your code a lot easier.

For point 2, this means thinking about what sections of your program should be doing. Generally it should work like so:

  • main
    • perform necessary setup
    • enter central_loop
  • central_loop
    • check for interrupt handler changes
      • make modifications if it has fired
    • perform base program executions (eg. controlling the LEDs on display)
    • loop central_loop
  • interrupt_handlers
    • perform a very small amount of code, modify memory values, etc.
    • don’t perform any heavy processing or heavy looping (eg. displaying LEDs in a loop)
    • return

By following this structure, you’re ensuring that:

  • Your code doesn’t break because you have performed setup more than once
  • Your code feels responsive because interrupt handlers are short and don’t take execution away from “main” for too long
  • Your code is able to perform its main operation all the time and is only briefly interrupted by handlers, which don’t break its execution

So how would this look? As a very brief and abstracted example:

main:
  @ Run setup once at the start
  bl init_leds
  bl init_interrupts

@ Begin a main central loop for displaying images
central_loop:
  @ First check if the button has been clicked 
  @ to see if modification to what we are currently doing 
  @ are necessary
  ldr r0, =interrupt_flags
  ldr r1, [r0]
  cmp r1, 1
  bne continue_loop
  @ Interrupt handler has fired
  @ reset the flag and do the necessary changes
  mov r1, 0
  str r1, [r0]
  @ ...
continue_loop:
  @ Perform the main body of work (displaying image)
  @ ...
  ldr r0, =current_display
  ldr r0, [r0]
  bl display_frame
  @ Loop back to the main central loop, not 'main'
  b central_loop

@ Interrupt handler is short and only modifies a memory value 
@ and whatever else to let you know it has occurred
ButtonHandler:
  push {lr}

  @ Flag that interrupt has occurred
  ldr r0, =interrupt_flags
  mov r1, 1
  str r1, [r0]

  @ rest of interrupt code as necessary
  @ ...

  bl clear_button_interrupts

  bl sync
  pop {lr}
  bx lr

Initialise peripherals only once#

With all of the peripherals, there is some code required to initialise them by setting certain config registers. This code only needs to be called once, so try to avoid re-initialising the peripheral every time you use it.

This is a common issue with the RNG peripheral in particular. You only need to set TASKS_START to 1 once; as the manual says, it then continually generates new random numbers. To make sure you’re reading a new random number, you can just use the VALRDY register provided; you do not need to set TASKS_START every time you read a new random number.

bars search times arrow-up