Outline#

Before you attend this week’s lab, make sure:

  1. you can read and write basic assembly code: programs with registers, instructions, labels and branching

  2. you’ve completed the week 7 and week 8 labs

  3. you’re familiar with the basics of functions in the lectures

In this week’s lab you will:

  1. write functions (subroutines) to break your program into reusable components

  2. pass data in (parameters) and out (return values) of these functions

  3. 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:

  1. records the address of the next instruction (i.e. the next value of pc) in the link register (lr), and
  2. 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.

Incorrect Flow Diagram

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#

Stack pointer 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. Address Space Diagram

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:

Example Correct 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):

Example Correct Flow

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”.

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:

  1. count up from 0 to 100 in increments of 1
  2. 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
  3. using the return result of the fizzbuzz function:
    • if the result is not divisible by 2 or 8, 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.

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.

What a mess!

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:

  1. 2 assignments, marked out of 100 but worth 25% of the total mark each
  2. 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:

  1. store the individual marks somewhere
  2. calculate the total mark
  3. put the result somewhere
  4. 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 to 0xF 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. bls) 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 nth number in the Fibonacci sequence. In other words:

  1. If n equals 0 or 1, return 1 (the first and second Fibonacci numbers; because we’re computer scientists, we’re indexing from 0.)

  2. 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 call fibonacci with the arguments n-1 and n-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

  1. load the students results in to the registers
  2. calculate their final grade using your calculate_grade function (the original one, not the self assessment version)
  3. store the final grade in the empty word at the end of each entry, eg.
    @SX
    .word 20, 40, 58, 0 @ <--- here
    
  4. repeat for the length of the array
  5. return using bx lr

If you’ve implemented it correctly, your memory at the results array should look like this afterwards:

final grades in memory

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 .words 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).

Morse code alphabet

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:

  1. blink_dot, which blinks an led (or leds) for a short period of time (say 0x400000 cycles—we’ll call this the “dot length”) and then pauses (delays) for one dot length before returning

  2. blink_dash, which blinks the led for three times the dot and then pauses (delays) for one dot length before returning

  3. 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:

Morse data structure

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 (_...):

Morse data B

and codepoint E (.)

Morse data 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:

  1. pick any character from the morse code table in the previous task

  2. store that character in memory (i.e. use the .data section) using the morse codepoint data structure shown in the pictures above

  3. 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 and blink_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)

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:

  1. loop over the characters in the input string (ldrb will be useful here)
  2. if the character is 0, you’re done
  3. 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):

  1. can you modify your program to accept both lowercase and uppercase ASCII input?
  2. the current morse_table doesn’t include the numbers 0 to 9; can you modify your program to handle these as well?
  3. can you remove the need for the number of dots/dashes in each table entry altogether?
  4. 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?

  1. 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?) 

bars search times arrow-up