Outline#
Before you attend this week’s lab, make sure:
-
you can read and write basic assembly code: programs with registers, instructions, labels and branching
-
you’re familiar with the basics of functions in the lectures
In this week’s lab you will:
-
write functions (subroutines) to break your program into reusable components
-
pass data in (parameters) and out (return values) of these functions
-
keep the different parts of your code from interfering with each other (especially the registers) using the stack
We will be using the lab-09
folder of a new lab pack repository. Make sure you
fork and clone the new lab pack.
Introduction#
In this week’s lab you will learn how to use functions to simplify accessing the lights on your microbit and how to work with data structures.
Interlude: Functions#
Functions are (usually reusable) blocks of code that have been designed to perform a specific task. Using functions allows us to break a big task (e.g. calculating the mark of a set of students) into smaller ones, which in turn can often be broken down into even smaller ones (e.g. setting and clearing a bit, potentially). How fine-grained you should break things down is a design choice that you get to make when you design your program.
The general pattern for functions look like this:
main:
@ put arguments in registers
@ mov r0, ...
bl foo @ call function foo
@ continue here after function returns
@ ...
.type foo, %function @ optional, telling compiler foo is a function
@ args:
@ r0: ...
@ result: ...
foo:
@ does something
bx lr @ return to "caller"
.size foo, .-foo @ optional, telling compiler the size of foo
You will notice that this looks very much like the label stuff we did in the basic machine code lab—and you’d be right. Since functions are just blocks of instructions, labels are used to mark the start of functions.
The only difference between a function and the “branch to label” code you’ve
written already in this course (with b
or perhaps a conditional
branch) is that with a function we want to return back to the
caller (e.g. the main
function) code; we branch with bl
but we want to
“come back” when we’re done with the function instructions.
That’s why bl foo
and bx lr
are used in the code template above instead of
just b foo
.
The bl foo
instruction:
- records the address of the next instruction (i.e. the next value of
pc
) in the link register (lr
), and - branches to the label (
foo
)
The bx lr
instruction
- branches to the memory address stored in the
lr
register
So, together these two instructions enable branching to a function (bl foo
)
and branching back (bx lr
) afterwards.
The .type
and .size
directive are optional—together they tell the compiler
that the label is a function, and what the size of the function is (.-foo
means current position minus the position of the label foo
). They are
essential for the disassembly view to work correctly for the function. They
also slightly change what value the label has in instructions like ldr r0, =main
.
If you’d like to add these annotations to your functions, check out the Tips and Tricks page and what it says about VSCode snippets, which are very convenient for this :).
Task 1: LEDs and Functions#
This lab picks up from the end of last week’s “blinky” lab. If you didn’t finish the lab tasks from last week, start with those before moving on to this week’s work.
At the end of last week’s lab you should have felt a warm glow of satisfaction—let there be light! But you might have noticed that a few of the steps you had to go through were pretty repetitive. For every step in blinking an LED, you were really doing one of two things:
- setting a specific bit at an offset from a base address, or
- clearing a specific bit at an offset from a base address
Wouldn’t it be good if we could “factor out” the common parts of those two
tasks, so that the code is simpler and clearer? We can do that with
functions (something you should have seen in the lectures). Remember that a
function starts with a label, and ends with the bx lr
instruction. To “call”
a function you use the bl
instruction to branch to the function’s label.
set_bit_0x50000000_0x514_21:
@ code to set bit 21 of word at offset 0x514 of 0x50000000
bx lr
main:
@ ...
bl set_bit_0x50000000_0x514_21
b main
When you start to use functions, the usefulness of the step over vs step
in buttons in the debugger toolbar starts to become clear. When the debugger
is paused at a function call (i.e. a bl
instruction) then step over will
branch, do the things without pausing, and then pause when the function
returns, while step in will follow the branch, allowing you to step
through the called function as well. Sometimes you want to do one, sometimes you
want to do the other, so it’s useful to have both and to choose the right one
for the job.
If you’re confused about what this section is referring to, ask your neighbour
/ tutor to point them out to you.
We’ve seen some students find that “stepping in” to a function does not work
unless if the function has the .type
and .size
annotations described earlier.
If this is the case for you, see the info box right before the Task 1 heading.
Update your top-left-LED-turning-on program from last week by writing a few
functions using the name pattern set_bit_<base>_<offset>_<index>
and
clear_bit_<base>_<offset>_<index>
following the example above. Your main
function is just a series of bl
instructions (and “spins” in an infinite loop
at the end).
Arguments / Parameters#
You’ve now modularised your code (broken it up into smaller, re-usable parts),
but it’s still pretty repetitive. There’s a lot of repeated code between the
set_bit_xxxx
functions.
The only difference between these repeated versions is the difference in inputs.
Therefore we can pass arguments to functions to parameterise those
functions, so that we just have one set_bit
function that we call with
different “inputs”.
As discussed in the lecture on functions, we leave values in r0
-r3
before calling bl
to act as “inputs” for our functions. Consider the following
sum_x_y
function:
main:
mov r0, 3 @ first argument, x
mov r1, 2 @ second argument, y
bl sum_x_y @ call sum_x_y(3, 2)
@ get result back in r0
.type sum_x_y, %function
@ Sums 2 values
@ args:
@ r0: x
@ r1: y
@ returns:
@ r0: x + y
sum_x_y:
add r0, r1
bx lr
.size sum_x_y, .-sum_x_y
The function adds the values in r0
and r1
and puts the result in r0
. So
the values in r0
and r1
are arguments (or parameters—same concept,
different name). We can just leave the numbers we want to add in r0
and r1
,
call the function sum_x_y
, and expect the result to be in r0
after it
finishes.
Did you notice something “underhanded” going on between the caller (main
) and
the callee (sum_x_y
)? There is an implicit contract/agreement as to:
- which registers hold the input arguments, and
- which registers hold the result
This is called calling convention, a set of rules that all function calls are expected to adhere to. It is generally CPU architecture and programming language defined.
Calling convention is super important, as such, it has its own page. Go and have a read of it now and then continue once you’re done. If you have any questions about this then ask your tutor.
Note that there are some comments before the function about where the arguments are placed. It’s a good idea to document what these registers are expected to hold for readability and clarity’s sake.
Parameterise your set_bit
and clear_bit
functions so that they each take
three arguments: base address, offset and bit index. Modify your main
function so that turning an LED on and off is as easy as calling your
set_bit
or clear_bit
functions with the right arguments. Copy the code into
tasks/task-1.S
. Commit and push your changes with the message “completed task
1”.
You may or may not have noticed that we haven’t told you to store the lr
register onto
the stack–that’s cause you’re creating what are called “leaf” functions. These
leaf functions don’t call other functions, so don’t need to worry about
having lr
overwritten.
Interlude: Nested Functions#
Now that we have had a taste of functions and parameters, we need to talk about nesting
functions. Let’s consider a toy example, we have 2 functions, one called double
and another
called triple
. Quite lazily, we have decided that our triple
function will use the double
function to compute its output. Our first attempt at writing these functions looks like so:
.type double, %function
@ Doubles a given value
@ args:
@ r0: value
@ returns:
@ r0: value * 2
double:
add r0, r0
bx lr
.size double, .-double
.type triple, %function
@ Triples a given value
@ args:
@ r0: value
@ returns:
@ r0: value * 3
triple:
bl double
add r0, r0
bx lr
.size triple, .-triple
There are a few issues with the way we have done things in our first attempt, can you identify what they are? You can think about it a bit before moving on to read the next part.
Here is a diagram of the flow of our program as it stands.
We can see that things are going okay (although we have an incorrect value after the add
line in triple
)
until we try to return from triple
. Instead of ending up at the nop
in main
we instead return to the
add
line in triple
.
If you thought that this would happen then congratulations! you’re absolutely right. The
reason for this is that we overwrote the value of our link register when we
called double
.
When we call bl
, we save the address of the instruction following it into the lr
register. This poses
an issue when we want to have nested functions (functions that call other functions) because we lose the address
to return to when we’re finished. We can get around this by utilising the stack.
The Stack#
If you already have a good grasp of what the stack is and how it works, you can skip this section.
By convention: the value of the sp
(stack pointer) is an address in the SRAM region of the
address space (like with the .data
section). Basically, it’s memory you can use to get things done and as
long as you maintain good stack practice then you won’t have to worry about interfering with or breaking
other areas of your program.
Common things that get stored on the stack include:
- “saving” values in registers which would otherwise be overwritten (e.g.
lr
) - passing parameters/returning values between function calls
- temporary / local variables
It’s called the stack because (in general) it’s used like a first-in-last-out (FILO) stack “data structure” with two main operations: push a value on to the stack, and pop a value off the stack.
Stack Pointer in Memory#
More About the Stack Pointer#
- the value (remember, it’s a memory address) in
sp
changes as your program runs -
sp
can either point to the last “used” address used (full stack) or the first “unused” one (empty stack) - you (usually) don’t care about the absolute
sp
address, because you use it primarily for offset (or relative) addressing - stack can “grow” up (ascending stack) or down (descending stack)
- in ARM Cortex-M (e.g., your microbit) the convention is to use a full descending stack starting at the highest address in the address space which points to actual RAM1.
Using the Stack#
So how do we actually use the stack? Well we can treat sp
just like any other register containing
a memory address.
Storing
@ Put a value in r2 that we want to store on the stack
mov r2, 0xABC
@ The following are all equivalent for storing r2 on the (full descending) stack.
@ Pre-offset based (expanded)
sub sp, sp, 4 @ decrease sp by 4 to point to the first "empty" spot
str r2, [sp] @ store r2 at new sp
@ Pre-offset based
str r2, [sp, -4]! @ sp := sp - 4, then store r2 at new sp value
@ (the ! makes the offset persist in the register
@ contained in the [ ])
Loading
@ Assume that the sp is currently pointing to an address that
@ contains a value we want to load into r3
@ The following sections are all equivalent for loading a value
@ into r3 and "removing" it from the stack.
@ Post-offset based (expanded)
ldr r3, [sp] @ store the value from sp into r3
add sp, sp, 4 @ increase sp by 4 to remove value we just loaded
@ Post-offset based
ldr r3, [sp], 4 @ load value from sp into r3, then sp := sp + 4
All of the above options for loading remove the value from the stack, but what does that actually mean? Is the value unrecoverable?
Fixing Our Nested Function#
With our new knowledge of how the stack works, we can fix the issues that we identified previously:
- we were overwriting the
lr
(link register) when we made our nested function call - we were losing our value needed to perform the final addition in
triple
double
is a leaf function (doesn’t make any nested calls), so no modifications are needed for this
function.
.type double, %function
@ Doubles a given value
@ args:
@ r0: value
@ returns:
@ r0: value * 2
double:
add r0, r0
bx lr
.size double, .-double
.type triple, %function
@ Triples a given value
@ args:
@ r0: value
@ returns:
@ r0: value * 3
triple:
str lr, [sp, -4]! @ Store the link register on the stack
str r0, [sp, -4]! @ Store the value to triple on the stack
bl double
ldr r1, [sp], 4 @ Load the original value to triple into r1
add r0, r1 @ Add the doubled value with the original value
ldr lr, [sp], 4 @ Load the original link register value
bx lr
.size triple, .-triple
These changes result in the following execution flow:
We can see now that by using the stack, we have been able to save the correct
return address (the nop
in main
) of our nested function triple
.
Here is a diagram of how the stack changes with the execution of triple
(where the first stack diagram is the stack view when triple
is called, and the
following stack diagrams are the way the stack looks after executing the linked
instruction):
Task 2: Blink with Functions#
Now that we have our set and clear functions, we can revisit our blink function from the previous lab. Previously you would have:
- turned an LED on
- delayed for some amount of time
- turned an LED off
- delayed for some amount of time
- looped
Our aim is to extract this behavior into a function to blink any LED for us using parameters.
In that lab, “turning on/off an LED” was as easy as setting a single row’s OUT value, but in general you need to be careful that this doesn’t also turn on LEDs in the same row, either by explicitly disabling those columns by clearing their DIR bit or setting their OUT bit. It worked out for us in the previous lab however because the other columns had 0 as their DIR bits by default and we did not set them.
We will create a function called blink_row
which turns a single row on, delays, then turns it off and
delays. You can then use this to blink individual LEDs.
Copy the following code into your main.S
file:
.syntax unified
.global main
.type main, %function
main:
@ fill in the rest of the instructions here!
nop
@ infinite catch loop
inf_loop:
nop
b inf_loop
.type blink_row, %function
@ args:
@ r0: base address
@ r1: offset
@ r2: bit index
@ r3: delay amount
@ note: the bit index is assumed to refer to a row and not a column
blink_row:
@ save link register, arguments on the stack
@ call set_bit with correct arguments
@ call delay with correct arguments
@ call clear_bit with correct arguments
@ call delay with correct arguments
@ clean up stack, restore link register and return
bx lr
.size blink_row, .-blink_row
Complete the above code to the given spec. Then fill in main
to use your blink_row
function
to blink the top left LED, then the bottom left LED, repeat.
Almost the same code is sufficient for blinking a column, but the problem is that a column’s OUT bit
is flipped in its meaning to the row bit; the column’s LEDs are only on if the column OUT bit is 0,
If you feel adventurous, try making a blink_col
function and instead blink the top left and top right
LEDs instead.
Copy the code into tasks/task-2.S
. Commit and push your changes with
the message “completed task 2”.
Task 3: FizzBlink#
For this task, you’ll make LED blinking more interesting by writing an
ARM assembly version of the classic
FizzBuzz children’s game (and a common
programming interview
question). There’s two
differences. Firstly, instead of printing "fizz"
or "buzz"
to the screen
(which you can’t do anyway, since we’re not running on the computer, you’re
running on the microbit) you’ll blink two LEDS on the board. So this new
version is called FizzBlink. Secondly, the classic FizzBuzz requires
testing divisibility by 3 and 5, which is difficult when working with binary numbers;
we’ve changed it so you are testing divisibility by 2 and 8 instead.
Modify your program to:
- count up from
0
to100
in increments of1
- call a function called
fizzbuzz
with the following properties:- takes an argument in
r0
- returns a result in
r0
, where the result is-
0
: if the argument is not divisible -
1
: if the argument is divisible by 2 (and not 8) -
2
: if the argument is divisible by both 2 and 8
-
- takes an argument in
- using the return result of the
fizzbuzz
function:- if the result is not divisible by
2
or8
, then display nothing (turn the lights) off for some period of time (use your delay function) - if the result is divisible by
2
, blink the top left light for some period of time - if the result is divisible by
8
, blink the bottom left light for some period of time.
- if the result is not divisible by
You may have noticed that the fizzbuzz
function uses r0
as both argument and return
value. This is in line with the ARM calling convention. Make sure to also use your blink_row
function from the previous task.
Why do we not need a case for when the argument is divisible by 8 and not 2?
There’s an easy way to determine whether a number is divisible by 2. If the
least significant bit of a binary number is 0
then it is divisible by 2 (since
it is an even number). Similarly, a number is divisible by 8 when the three
least significant bits are all 0 (e.g. the binary representation ends in 000
).
Think about why this is the case, and how you can use the tst
instruction
(see the cheat sheet)
to check divisibility in your fizzbuzz
function.
Copy the code into tasks/task-3.S
. Commit and push your changes with
the message “completed task 3”.
Task 4: A Basic Calculator#
Imagine you have a friend who’s a teacher (or a university lecturer!) and is stressed out at the end of semester. They’ve finished marking all of their student’s assignments and exams, but the marks for these individual pieces of assessment are just scribbled around your friend’s apartment on whatever piece of paper (or wall) was closest at the time.
There’s only so much you can do to help your friend out, but one thing you can do is to help them add up the marks for each student’s assignments and exam to calculate their final mark for the semester.
The assessment for your friend’s class had 3 items:
- 2 assignments, marked out of 100 but worth 25% of the total mark each
- 1 final exam, marked out of 100 but worth 50% of the total mark
As an example, here’s the marks for one student which your friend found written on a banana on the floor of his lounge room:
student id | assignment 1 | assignment 2 | final exam |
---|---|---|---|
s1 | 66 | 73 | 71 |
Your job in this exercise is to write a calculate_total_mark
function which
takes three parameters (assignment 1 score, assignment 2 score and exam
score) and calculates the total mark. Be careful to take account of the “number
of marks vs percentage of total mark” for each item (the maths here really isn’t
tricky, but you still have to take it into account)
In this lab we’ll talk a lot about “calling functions” because that’s something you’re familiar with from higher-level programming languages. However, it’s important to remember that functions aren’t some new magical thing, they’re just a matter of using the instructions you already know in a clever way.
To complete this exercise, your program should:
- store the individual marks somewhere
- calculate the total mark
- put the result somewhere
- continue executing from where it left off before the
calculate_total_mark
function was called
The key to packaging up a bunch of assembly instructions into a callable function is
using the link register (lr
) to remember where you branched from, and the bx lr
instruction to jump back (or return) when you’re done.
Here’s a partial template (although you’ll have to replace the ??
s with actual
assembly code for it to run):
main:
@ set up the arguments
mov r0, ?? @ ass1 mark
mov r1, ?? @ ass2 mark
mov r2, ?? @ final exam mark
@ call the function
bl calculate_total_mark
@ go to the inf_loop
b inf_loop
@ infinite catch loop
inf_loop:
nop
b inf_loop
calculate_total_mark:
@ do stuff with the arguments
@ ...
@ put the result in r0
mov r0, ??
@ go back to where the function was called from
bx ??
Starting with the code above, commit your a program which calculates the mark
for student s1
(see their marks in the table above),
then moves into an infinite loop. Copy the code into tasks/task-4.S
. Commit
and push your changes with the message “completed task 1”.
Task 5: Turning Marks Into Grades#
Your teacher friend is stoked with your solution but needs more help. They need to give a letter (A to F) grade to each student based on the following formula:
90–100 | 80–89 | 70–79 | 60–69 | 50–59 | 0–49 |
---|---|---|---|---|---|
A | B | C | D | E | F |
You tell your friend to relax—you can write another function which can do this.
In this exercise you need to write a second function called grade_from_mark
which
- takes a numerical mark (0–100) as input parameter
-
returns a value represending a letter grade (you can encode the “grade”
however you like, but the hex values
0xA
to0xF
might be a nice choice)
There are a few ways to do this—you could generate results by doing a series of comparison tests against the different score cut-offs, but also remember that our input is a number and our output is really just a number as well. Discuss with your partner: is there a numerical transformation (a simple formula) that turns an overall mark into a grade? What are the edge cases of this formula? Are there downsides to using a “closed form solution” rather than a series of checks?
Add a grade_from_mark
function to your program as described above. In your
program, demonstrate that it returns the correct grade for the following inputs:
(15
, 99
, 70
, 3
). Copy the code into tasks/task-5.S
. Commit and push
your changes with the message “completed task 2”.
Are there any other input values which are important to check? How does your function handle “invalid” input?
If you’re feeling adventurous, modify your program to call grade_from_mark
,
then store the result to memory in the .data
section using the ASCII encoding.
Task 6: Putting it Together#
In this exercise, you need to write a function called calculate_grade
which
combines these two steps: it takes the raw marks on the individual assessment
items and returns a grade.
Write a calculate_grade
function which calls (i.e. bl
s) the
calculate_total_mark
function and use it to calculate the grades of the
following students:
student id | assignment 1 | assignment 2 | final exam |
---|---|---|---|
s2 | 58 | 51 | 41 |
s3 | 68 | 81 | 71 |
s4 | 88 | 91 | 91 |
Combining these two functions is not too complicated, but remember to save your link register!
Submit a program which uses calculate_grade
to calculate the mark of student
s4
. Copy the code into tasks/task-6.S
. Commit and push your changes with the
message “completed task 6”.
Task 7: Recursive Functions#
A recursive function is one which calls itself, usually passing different arguments. This is useful when a task can be broken down into doing a smaller version of the task several times, and then combining the results. If you have done COMP1100 you should be very familiar with this idea; if not, feel free to ask a tutor. Alternatively, let this jolly englishman walk you through it.
We will implement a recursive function fibonacci
that takes one argument, n
(say it’s passed in r0
),
and returns the n
th number in the Fibonacci sequence. In other words:
-
If
n
equals0
or1
, return1
(the first and second Fibonacci numbers; because we’re computer scientists, we’re indexing from 0.) -
Otherwise, since each number is the sum of the previous two numbers in the sequence, return
fibonacci (n-1) + fibonacci (n-2)
. In other words, recursively callfibonacci
with the argumentsn-1
andn-2
, sum the results, and return that.
Since each recursive call is with an input that is strictly smaller than before, the recursive calls
will eventually stop when the input becomes 0
or 1
and the program will start backing out of
recursive calls again.
Again, you need to use the stack to not only store your old link registers but also the parameters you are passing into functions so the registers don’t interfere with each other.
Your code should be something like this:
@ inputs:
@ r0: n, the index of Fibonacci sequence to calculate
@ returns:
@ r0: the nth value of the Fibonacci sequence
fibonacci:
@ ...
bl fibonacci @ recursive call
@ ...
bx lr
Write a recursive function that calculates Fibonacci as described. Copy the code into tasks/task-7.S
.
Commit and push your changes with the message “completed task 7”.
Discuss with your lab neighbor—what are the pros and cons of having recursive calls in a function? Hint: think about how each recursive call affects the stack.
Extension tasks
The following tasks are beneficial for you to complete to further practice the use of functions as well as general assembly coding, but are not necessary to complete; you may do them in your own time.
Extension (Task 8): Arrays as Arguments#
One of the tutors has heard about the good work you’ve been doing for your teacher friend and they have asked you to help them. Fortunately, they are more organized than the teacher and have provided you with a collection of the students results in an array
main:
ldr r0, =results
bl calculate_lab_grades
nop
b main
@ ...
@ input:
@ r0: address of start of mark array with format,
@ .word size of array
@ .word a1, a2, final, 0
@ output:
@ .word a1, a2, final, grade
@ ...
calculate_lab_grades:
@ ...
bx lr
@ ...
.data
results:
@ Length of array: 6
.word 6
@S1
.word 50, 50, 40, 0
@S2
.word 77, 80, 63, 0
@S3
.word 40, 50, 60, 0
@S4
.word 80, 82, 89, 0
@S5
.word 80, 85, 77, 0
@S6
.word 91, 90, 95, 0
Write the calculate_lab_grades
function to iterate over the students results
array
- load the students results in to the registers
- calculate their final grade using your
calculate_grade
function (the original one, not the self assessment version) - store the final grade in the empty word at the end of each entry, eg.
@SX .word 20, 40, 58, 0 @ <--- here
- repeat for the length of the array
- return using
bx lr
If you’ve implemented it correctly, your memory at the results array should look like this afterwards:
note: the final grades are stored in the 00 offset column, starting from 20000010
Copy your program to add the grades to the array into tasks/task-8.S
. Commit and push
your changes with the message “completed task 9”.
The values in this code are stored in memory using .word
s which are 32 bits
(4 bytes) in size, yet no entry needs more than a byte, can you rework your code and
the array to reduce its size in memory?
Extension (Task 9): Morse Code 1#
The following 3 tasks are an extension and application of everything you’ve already done so far in the lab, so if you don’t finish them in time, don’t stress. They exist here to better help you solidify your understanding of what you’ve learnt so far.
Morse code is a simple communication protocol which uses “dots” and “dashes” to represent the letters of the alphabet.
The dots and dashes can be represented in different ways—as dots or lines on a page, as short or long beeps coming out of a speaker, or hidden in a song on the radio to reach kidnap victims, or as short or long “blinks” of an LED on your microbit.
In this lab content the morse code will be represented visually using a sequence of .
(dot) and _
(dash) characters, but for this exercise, you’ll be sending
morse code signals by blinking an LED on your microbit in short (dot) and
long (dash) bursts. Here’s the full morse alphabet (courtesy of
Wikipedia).
The Task#
Your task is to use / modify the functions you wrote earlier in the lab (Task 1 and 2) to
write three new functions in your main.S
file:
-
blink_dot
, which blinks an led (or leds) for a short period of time (say0x400000
cycles—we’ll call this the “dot length”) and then pauses (delays) for one dot length before returning -
blink_dash
, which blinks the led for three times the dot and then pauses (delays) for one dot length before returning -
blink_space
, which doesn’t blink an LED, but pauses (delays) for seven dot lengths before returning
Once you’ve written those functions, write a main
loop which blinks out the
sequence ... _ _ _
on an endless repeat.
Copy the code into tasks/task-9.S
then commit and push your changes to GitLab.
Extension (Task 10): Morse Code 2 - A Morse Data Structure#
Now it’s time for the actual morse code part. In morse code, each letter (also
called a codepoint) is encoded using up to five dots/dashes. For example,
the codepoint for the letter B has 4 dots/dashes: _...
while the codepoint for
the letter E is just a single dot .
. You could store this in memory in several
different ways, but one way to do it is to use a data structure which looks like
this:
Each “slot” in the data structure is one full word (32 bits/4 bytes), so the total size of the codepoint data structure is 4*6=24 bytes. The first word is an integer which gives the total number of dots/dashes in the codepoint, while the remaining 5 boxes contain either a 0 (for a dot) or a 1 (for a dash).
What will the address offsets for the different slots be? Remember that each box is one 32-bit word in size, but that memory addresses go up in bytes (8 bits = 1 byte).
Here are a couple of examples… codepoint B (_...
):
and codepoint E (.
)
In each case, the “end” slots in the data structure might be unused, e.g. if the codepoint only has 2 dots/dashes then the final 3 slots will be unused, and it doesn’t matter if they’re 0 or 1. These slots are coloured a darker grey in the diagrams. (If this inefficiency bums you out, you’ll get a chance to fix it in the Extra Tasks section after the main exercises.)
Your job for this task is to write a function which takes (as a parameter) the base address (i.e. the address of the first slot) of one of these morse data structures and “blinks out” the codepoint using an LED.
As a hint, here are the steps to follow:
-
pick any character from the morse code table in the previous task
-
store that character in memory (i.e. use the
.data
section) using the morse codepoint data structure shown in the pictures above -
write a
blink_codepoint
function which:- takes the base address of the data structure as an argument in
r0
- reads the “size” of the codepoint from the first slot
- using that size information, loops over the other slots to blink out the
dots/dashes for that codepoint (use the
blink_dot
andblink_dash
functions you wrote earlier) - when it’s finished all the dots/dashes for the codepoint, delays for 3x dot length (the gap between characters)
- takes the base address of the data structure as an argument in
Since the blink_codepoint
function will call a bunch of other functions, make
sure you use the stack to keep track of values you care about. If your program’s
not working properly, make sure you’re not relying on something staying in r0
(or any of the scratch registers) between function calls!
Write a program which uses the morse data structure and your blink_codepoint
function to blink out the first character of your name on infinite repeat.
Copy the code into tasks/task-10.S
then commit and push your changes to GitLab.
Extension (Task 11): Morse Code 3 - ASCII to Morse Conversion#
The final part of today’s lab is to bring it all together to write a program which takes an input string (i.e. a sequence of ASCII characters) and blinks out the morse code for that string.
To save you the trouble of writing out the full morse code alphabet, you can
copy-paste the following code into your editor. It also includes a place to
put the input string (using the .asciz
directive).
.data
input_string:
.asciz "INPUT STRING"
@ to make sure our table starts on a word boundary
.align 2
@ Each entry in the table is 6 words long
@ - The first word is the number of dots and dashes for this entry
@ - The next 5 words are 0 for a dot, 1 for a dash, or padding
@ (value doesn't matter)
@
@ e.g., 'G' is dash-dash-dot. There are 2 extra words to pad the
@ entry size to 6 words
morse_table:
.word 2, 0, 1, 0, 0, 0 @ A
.word 4, 1, 0, 0, 0, 0 @ B
.word 4, 1, 0, 1, 0, 0 @ C
.word 3, 1, 0, 0, 0, 0 @ D
.word 1, 0, 0, 0, 0, 0 @ E
.word 4, 0, 0, 1, 0, 0 @ F
.word 3, 1, 1, 0, 0, 0 @ G
.word 4, 0, 0, 0, 0, 0 @ H
.word 2, 0, 0, 0, 0, 0 @ I
.word 4, 0, 1, 1, 1, 0 @ J
.word 3, 1, 0, 1, 0, 0 @ K
.word 4, 0, 1, 0, 0, 0 @ L
.word 2, 1, 1, 0, 0, 0 @ M
.word 2, 1, 0, 0, 0, 0 @ N
.word 3, 1, 1, 1, 0, 0 @ O
.word 4, 0, 1, 1, 0, 0 @ P
.word 4, 1, 1, 0, 1, 0 @ Q
.word 3, 0, 1, 0, 0, 0 @ R
.word 3, 0, 0, 0, 0, 0 @ S
.word 1, 1, 0, 0, 0, 0 @ T
.word 3, 0, 0, 1, 0, 0 @ U
.word 4, 0, 0, 0, 1, 0 @ V
.word 3, 0, 1, 1, 0, 0 @ W
.word 4, 1, 0, 0, 1, 0 @ X
.word 4, 1, 0, 1, 1, 0 @ Y
.word 4, 1, 1, 0, 0, 0 @ Z
The main addition you’ll need to make to your program to complete this exercise
is a morse_table_index
function which takes a single
ASCII character as input, and returns the
base address of the corresponding codepoint data structure for that character
(which you can then pass to your blink_codepoint
function).
For example, the letter P is ASCII code 80
,
and the offset of the P codepoint data structure in the table above is 15 (P is
the 16th letter) times 24 (size of each codepoint data structure) equals 360 bytes.
So, your main program must:
- loop over the characters in the input string (
ldrb
will be useful here) - if the character is
0
, you’re done - if the character is not
0
:- calculate the address of the morse data structure for that character
- call the
blink_codepoint
function with that base address to blink out the character - jump back to the top of the loop and repeat for the next character
If you like, you can modify your program so that any non-capital letter (i.e.
ASCII value not between 65 and 90
inclusive) will get treated as a space (blink_space
).
Write a program which blinks out your name in morse code.
Copy the code into tasks/task-11.S
then commit and push your changes to GitLab.
Extra Tasks#
Morse Extensions#
There are many ways you can extend your morse program. Here are a few things to try (pick which ones interest you—you don’t have to do them in order):
- can you modify your program to accept both lowercase and uppercase ASCII input?
- the current
morse_table
doesn’t include the numbers 0 to 9; can you modify your program to handle these as well? - can you remove the need for the number of dots/dashes in each table entry altogether?
- this is far from the most space-efficient way to store the morse codepoints, can you implement a better scheme?
LED Library#
Combine what you’ve learned over this and the previous lab to create some LED utility functions. How could you parameterize the functions to make them the most useful and reduce similar code?
-
The address space is the set of all valid addresses
So on a machine with 32-bit addresses (like your microbit) that’s \(2^{32} = 4294 967 296\) different addresses
So you can address about 4GB of memory (is that a lot?) ↩