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:
- it should be clear that your functions AND interrupt handlers are following calling convention
- 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 correspondingpop
after a conditional branch as it makes it a lot easier to lose track of what you’re doing with the stack
- This means that if you have a
- 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
- check for interrupt handler changes
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.