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 have read the laboratory text below
In this weekβs lab you will:
- learn how to load and store withΒ
ldrΒ andΒstr - blink the LEDs by reading & writing special hardware registers
- useΒ functionsΒ to structure your code and remove duplication
Hereβs a table of contents for this weekβs lab, just in case you need to jump around.
Introduction
In this weekβs lab youβll read and write some of the special hardware registers on your discoboard to seeΒ LEDΒ βoutputβ from your program. Seeing stuff happen in the real world is a big part of the fun of microcontrollers, so this is going to be fun.
Youβll also start to see more clearly the connections between what weβve been covering in this course and the higher-level programming languages youβre used to, with theirΒ ifΒ statements,Β forΒ loops, and other niceties. This process of βdemystifyingβ programming is a big part of what this course is about, so take the time to reflect on what youβre doing and how it fits in with what you know and do in other programming situations.
Exercise 0: bit shifting practice
Plug in your discoboard, fork & clone theΒ lab 6 templateΒ to your machine, and open theΒ src/main.SΒ file as usual.
The stuff in Exercise 0 is all stuff weβve covered already (in some cases as far back as week 2) but itβs sometimes good to have a warm-up/refresher. Feel free to jump straight toΒ Exercise 1Β if youβre feeling adventurous, but thereβs no shame in taking your time and working through the warm-up stuffβpractice makes perfect!
Bit-shifting & logic ops
You will need to use some logic operations in this lab, in particular, setting (setΒ 1) and clearing (setΒ 0) bits. This warm up exercise gives you a chance to practice bit shifting and using logic operations to set and clear bits.
Edit yourΒ main.SΒ file so that it looks like the following. Donβt forget the requiredΒ .syntax unifiedΒ andΒ .global mainΒ statements at the top of the file.
main:
ldr r0, =0xcafe0000
ldr r1, =0xffff
@ your code goes here
@ when it's all done, spin in an infinite loop
loop:
nop
b loop
| Decimal | |
| Hex | |
| Binary |
UsingΒ onlyΒ the instructions in theΒ LogicΒ andΒ Shift/RotateΒ subsections of the cheat sheet (but as many registers as you need) write a program which putsΒ allΒ of the following values into the listed registers. Use the cheat sheet and the converter widget to help you outβdraw βbit patternβ pictures on a piece of paper if it helps.
0xcafeffffΒ intoΒr30xcafeΒ intoΒr40xcaff0000Β intoΒr50xc0fe0000Β intoΒr6
These shouldnβt require heaps of codeβjust a couple of instructions for each. Remember theΒ stuff youβveΒ done inΒ previous labs.
Did you needΒ bothΒ of the βstartingβ values (e.g.Β 0xcafe0000Β andΒ 0xffff) or could you have got the job done with only some of them? Which ones are essential?
Can you do each task with only one instruction each?
Exercise 1: using labels, ldr, and str
Labels and loading arbitrary numbers into registers
Labels (Page 24)Β are the symbols in your source code followed by a colon (:), e.g.Β main:. Youβve probably already got an intuitive feel for how they work: you put them in your code wherever you like, and when you want to branch to that part of the program you put the label in as the βdestinationβ part of the branch instruction. Hereβs an example:
loop:
@ do stuff
b loop @ branch back to the "loop" label
In theΒ week 3 labΒ you even used conditional branches to only branch under certain conditions (i.e., if certain flags were set). And we covered this in theΒ week 4 lecturesΒ as wellβincluding how to turn βmathematicalβ conditional expressions (e.g.Β x >= -45) into sequences of assembly instructions.
But what are labels, really? Add this code to your program (under theΒ mainΒ label):
ldr r0, =main
After you step through this line, whatβs inΒ r0? You might be wondering what theΒ =Β sign is doing in your program. Remember from your week 2 lab that instructions are stored in memory with various encodings (some are 16-bit, some are 32-bit) and that when you use anΒ immediate valueΒ constant (e.g.Β 42) in an instruction which supports it then the bit pattern forΒ 42Β (which isΒ 0b101010) is storedΒ insideΒ that instruction.
This means that if you need to include a constant which is 32 bits long (e.g.Β 0xABCD1234) then you canβt fit it in the instruction. You may have run into this problem alreadyβthe error message will be something like
Error: invalid constant (ffffffffabcd1234) after fixup
and what it means is that the constant value youβre using is too big (too many bits) for the instruction youβre trying to fit it inside.
If youβre interested in how exactly the ARM instruction set deals with this problem, and which constantsΒ canΒ be stored inside a 32-bit instruction, thenΒ hereβsΒ an interesting blog post. Recall we use the Thumb-2 instruction set, so itβs not exactly the same on the discoboard (see section A5.3.2 in the reference manual for how they work on the discoboard).
Instead, you can play with the following widget to see how our board encodes a shift. The top input is the 12-bit encoded value. The second input is the 32-bit expanded output. Below this, the longer table is the binary representation of the output. An orange colour represents one of the 4 βspecialβ shifts, while blue means it is a regular shifted byte. Similarly, the shorter table shows the parts of the encoded input, with the shift in purple and the data in green. Note how bit 7 of the encoded input is conditionally part of the shift or data, depending on bits 10 and 11. Each bit in the binary table can also be toggled by clicking it.
Because this is a bit of a pain, the assembler provides a special syntax for storing larger values in registers. Itβs based around theΒ ldrΒ (load register) instruction, and if you prefix the constant with anΒ =Β sign then the assembler will generate the code to load the full value into the register, as we discussed in theΒ week 4 lectures.
Have a look at these two lines of assembly code:
mov r0, 0xFFF
ldr r0, =0xFFF
will they result in the same assembly instructions when uploaded & running on your discoboard? How might you check?Β Hint:Β the disassembler is your friend π.
Can you think of anyΒ otherΒ lines of assembly code (apart from the two above) which will be assembled into the same machine instruction(s)?
So how does this relate to theΒ ldr r0, =mainΒ instruction above? Well, the answer is that the labels in your program are just valuesβtheyβre the addresses (in your boardβs memory space) of the instruction which occurs after them in the program. After the linker figures out exactly which address each label points to, it βreplacesβ them in the program, so that
ldr r0, =main
becomesΒ something like
ldr r0, =0x80001c8
or whatever address theΒ mainΒ label ends up pointing to (which will change every time your program changes).
And sinceΒ 0x80001c4Β (or whatever it is) is just a bit pattern in a register, you can do the usual arithmetic/logic stuff you can do with any values in registers:
Write a small program which calculates the size (in memory) of theΒ movs r3, 1Β instruction and stores the result inΒ r0.
Exercise 2: the load-twiddle-store pattern
TheΒ load-twiddle-storeΒ pattern is a fundamental pattern in making your discoboard do useful work. The basic idea is this:
- load some data from memory into a register
- operate on (βtwiddleβ) the value in the register (e.g. with anΒ
addΒ orΒandΒ instruction) - store this new value from the register back into memory

If youβve forgotten about βdataβ and βtextβ sections, you can review it in theΒ week 5 lab content.
Letβs now make use of a data section to store some (spoilers) data, and attempt to load-twiddle-store.
main:
ldr r1, =storage
@ your code starts here
.data
storage:
.word 2, 3, 0, 0 @ don't change this line
Starting with the code above, use theΒ load-twiddle-storeΒ pattern to change the first four data words toΒ 2Β 3Β 0Β 1Β instead ofΒ 2Β 3Β 0Β 0. Hint: first load theΒ storageΒ label using theΒ =Β instruction, then remember that you canΒ load and store with an offsetΒ from this base address (check the cheat sheet). Youβll probably also want to use the memory browser view (like you didΒ in week 2) to watch the values change in memory.
Exercise 3: hello, LED!
So what does all that stuff have to do with blinking the LEDs? Well, the answer is that thereβs a section of the discoboardβsΒ address space (Page 23)Β (0x40000000Β toΒ 0x5FFFFFFF) which is mapped to peripherals (as shown in the picture above). To interact with the LEDs, LCD, microphone etc. on the board you need to talk to the hardware by reading and writing to special memory locations in this memory range. To figure out exactly which addresses are mapped to which peripherals, you need to look at the discoboardΒ reference manual

One type of peripheral is aΒ General Purpose Input/OutputΒ pin. You can see them on your discoboard as little gold-coloured spikes sticking up out of the top and bottom of the board. Your discoboard has lots of them, and you can wire them up to other devices to make more sophisticated devices.
This exercise is pretty long, so here are the steps youβll go through to turn on the LED:
- enable the clock for correct LED GPIO pin
- set the pin to output mode
- set a bit in the pinβs data register to turn the LED on
Donβt worry if you donβt understand some of those termsβthe rest of this exercise will explain all the details.
Some of the ports on the discoboard are already connected to certain bits of hardware on the board. In theΒ discoboardΒ userΒ manualΒ Section 7.5Β User interface: LCD, joystick, LEDsΒ it says:
- LD4 user: the red LED is a user LED connected to the I/O PB2 of the STM32L476VGT6
- LD5 user: the green LED is a user LED connected to the I/O PE8 of the STM32L476VGT6
The first bullet point says that the red LED is connected to GPIO pinΒ PB2. This means that itβs connected toΒ pin 2Β ofΒ port B. Just a note that theΒ userΒ manual (short) is different from theΒ referenceΒ manual (long & detailed).
What port+pin do you think the green LED is connected to?
The GPIO pins are grouped into 8 ports (port A to port E) and each port has 16 pins (pin). Itβs worth pointing out that the pin numbering starts at 0, so the first pin in port A is PA0.

From theΒ reference manual:
Each general-purpose I/O port has four 32-bit configuration registers (GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR and GPIOx_PUPDR), two 32-bit data registers (GPIOx_IDR and GPIOx_ODR) and a 32-bit set/reset register (GPIOx_BSRR).
Donβt worry if this seems overwhelming, the main point is that each of these ports has a few dedicated configuration registersβsome are used to turn it on, some are used to set it up, and some are used to receive data (read the voltage on the pin as 0 or 1) or send data out (set the voltage to high or low). When it saysΒ GPIOx, that means that there exists a version for all the ports, so the port A version would be calledΒ GPIOA, etc.
These are not like the CPU registers youβve been using so far (e.g.Β r0Β orΒ r5). Instead, theyβre mapped to certain parts of the address space (so theyβre sometimes called memory-mapped registers). Read/write access to this register happens through load/store instructions to a specific memory address (as with pretty much everything in aΒ load/store architecture (Page 27).
The register for turning on the clock (step 1) is in theΒ Reset and Clock Control (RCC)Β section of the address space, which on your discoboard starts at memory addressΒ 0x40021000. The specific register which controls the clock for GPIO ports is theΒ RCC_AHB2ENRΒ 32-bit register, which lives at an offset ofΒ 0x4CΒ from the RCC base address and looks like this:

As you can see, the clock for GPIO port B (where your red LED is) is controlled through bit 1 (i.e. theΒ secondΒ bit from the right, because the rightmost bit is bit 0). You can have a look at Section 6.4.17 of the reference manual for all the gory details.
Why do we need to turn on a clock to even use the GPIO pins? Why arenβt they just permanently enabled to save us all the hassle? Hint: the discoboard is marketed as an βultra-low-power microcontrollerβ. Itβs designed to support being on battery power for long durations without recharging.
Note that in debug view you can conveniently see this information in theΒ PeripheralsΒ pane:Β 
This is your chance to see theΒ load-twiddle-storeΒ pattern from Exercise 1 in action. To turn on GPIO port B, you must:
- load: load the RCC base address into a register, then do an βoffsetβΒ
ldrΒ with theΒRCC_AHB2ENRΒ offset to read the current state of theΒRCC_AHB2ENRΒ register into a CPU register - twiddle: use bitwise operations to set the second-from-the-rightΒ
GPIOBENΒ bit to 1 while leaving the other bits unchanged - store: write the newΒ
RCC_AHB2ENRΒ value back to the memory address you read it from earlier
When we talk aboutΒ settingΒ a bit, that means that it should be equal toΒ 1, andΒ clearingΒ a bit means it should be equal toΒ 0.
So what does the code to perform these load-twiddle-store steps look like?
@ load r1 with the base address of RCC
ldr r1, =0x40021000
@ load r2 with the value of RCC_AHB2ENR
@ (note the 0x4C offset from the RCC base address)
ldr r2, [r1, 0x4C]
@ set bit 1 of this register by doing a logical or with 2
@ think: why does this work?
orr r2, 0b10
@ store the modified result back in RCC_AHB2ENR
str r2, [r1, 0x4C]
Why is theΒ loadΒ part of this process necessary? Why canβt you just store aΒ 2Β into theΒ RCC_AHB2ENRΒ register and be done with it?
You can paste the above code straight into your program. There are a couple more steps before you can actually turn the LED on, but theyβre basically the same load-twiddle-store pattern, except with different addresses and βtwiddlesβ.
Now itβs your turn: copy-paste a second copy of the code above as a starting point, but youβll need different load/store addresses and different twiddles. To set pin 2 of GPIO port B to output mode, you need to set theΒ MODE2Β bits (4 and 5) of the GPIO mode registerΒ GPIOB_MODER, which lives at an offset ofΒ 0x0Β from the GPIO base address ofΒ 0x48000400Β (see Section 7.4.1 of the manual for more info). Hereβs what theΒ GPIOB_MODERΒ looks like:

To configure pin 2 for output mode to power the LED, you need to ensure the mode bits for pin 2 (MODE2Β in the diagram) areΒ 01Β for output mode (i.e. clear bit 5, set bit 4).
This output mode configuration for the mode register bits is sufficient for turning the LEDs on and off, but there are many more ways to configure the GPIO pins. If youβre interested, have a look in Section 7.4 of the manual.
Your PB2 pin is now configured and ready to roll. The only thing left to do is to actually send an βonβ signal to it by setting aΒ 1Β into bit 2 (for pin 2) of the port B Output Data RegisterΒ GPIOB_ODR, which lives at offsetΒ 0x14Β from the GPIOB base address and looks like this:

Following the steps above, write a program which turns on the red LED on your discoboard, and push it up to your fork on GitLab.
The red and green LEDs are normally near the top left corner of the reset button. If you look closely, you can see the labelsΒ LD4Β andΒ LD5Β on the board there. The LEDs above the LCD screen are not the ones we are working with.
There are a few fiddly things which can go wrong here. If your LED isnβt coming on, talk with your neighbour/tutor about your program. Have you accidentally set the wrong bit (remember that the ports and bits are 0-indexed, so the rightmost bit is bit 0, not bit 1). Are you reading the existing register value correctly? Are you turning the bit on correctly? Are you writing it back to the right memory address? Step through the program with your partner to see what might be going wrong.
Once you can turn the red LED on, add code to turn the green one (PE8) as well. Commit & push your red+green program up to GitLab.
Exercise 4: functions review
Hey wow! You made it toΒ Exercise 4Β which is about theΒ halfway pointΒ for this lab! As we have both week 6 and week 7 for this lab, you can feel free to stop here and pick it up in week 7. Alternatively, if youβre preparing for week 6βs lab, make sure you understand everything up to this point. Of course if youΒ wantΒ to keep going, Iβm not going to stop you! Have fun! π
You should now feel a warm glow of satisfactionβlet there be light! But youβll also notice that a few of the steps you had to go through were pretty repetitive. For every step you just did, you were really doing one of two things:
- settingΒ a specific bit at an offset from a base address,Β or
- clearingΒ a specifc 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 remember from last week). Weβre going to take this quite slow, if youβre feeling confidentβyou canΒ skip to the second βpushβ boxΒ and work from there.
set_bit_0x48000400_0x14_2:
@ code to set bit 2 of word at offset 0x14 of 0x48000400
bx lr
main:
@ ...
bl set_bit_0x48000400_0x14_2
b main
Update yourΒ red+greenΒ program by writing a few functions using the name patternΒ set_bit_<base>_<offset>_<index>Β andΒ clear_bit_<base>_<offset>_<index>Β following the example above, so that 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 we learnt last week, 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
@ args:
@ r0: x
@ r1: y
@ result: r0
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. There is more to calling convention, and it will be covered in lecture and in the next lab.
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 the LED on and off is as easy as calling yourΒ set_bitΒ orΒ clear_bitΒ functions with the right arguments. Commit & push your newly-functional program to GitLab.
You might 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.
Exercise 5: blinky
In this exercise youβll add a simple loop into your program to blink one of the LEDs on and off. You can write aΒ delayΒ function to do thisβdo it now, and call it in-between your on and off instructions. Let the function take in an argument so that you can specify the number of steps to βdelayβ for.
There are a bunch of ways to do this, but one way is to
- subtract 1 from an input register,
- ifΒ the value isnβt zeroΒ thenΒ goto step 1,Β elseΒ return back to the caller.
Modify your program so that after the initial setup code, there is a loop which turns the LED on, delays a little while, turns it back off, delays a little again, then branches back to the top of the loop.
Once youβve done that, you should be able to blink the LEDs on your board to your heartβs content.
Exercise 6: FizzBlink
For the final exercise, youβll make LED blinking more interesting by writing an ARM assembly version of the classicΒ FizzBuzzΒ childrens game (and a commonΒ programming interview question). The only difference is that 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 discoboard) youβll blink the LEDS on the board. So this new version is calledΒ FizzBlink, I guess.
Modify your program to:
- count up fromΒ
0Β toΒ100Β in increments ofΒ1 - if the number is divisible byΒ
3, blink theΒ redΒ light for some period of time (use your delay function) - if the number is divisible byΒ
5, blink theΒ greenΒ light for some period of time - if the number is divisible by bothΒ
3Β andΒ5, blinkΒ bothΒ lights




