Twenty Three Hundred
Dr Charles Martin
Semester 1, 2022
You’ve now completed 45% of the assessment for COMP2300/6300!
midsem results, eta end of week 9.
assignment 2 coming out this week — a new challenge for you!
week 9 is an assignment pre-submission week!
don’t forget to go to your labs!!
This is going to be fun!
(This is assumed knowledge)
polling vs interrupts
main: @ do some work!
bl check_keyboard
bl check_mouse
bl check_camera
bl do_some_actual_work @ yeah!
bl check_usb_1
bl check_temperature
bl do_some_other_work @ woo!
bl check_usb_2
bl check_network
b main
Change our processor architecture to allow an external part to “interrupt” our fetch-decode-execute cycle!
Sequence of code is interrupted, and the CPU automatically branches to an “interrupt handler”.
main: @ do some work!
bl do_some_actual_work @ yeah!
bl do_some_other_work @ woo!
b main
keyboard_interrupt_handler:
@ do what's needed to check the keyboard
bx lr
But how does this work in practice? (we’ll find out in a minute…)
Polling: Simple to implement, easy to calculate latency, fastest for small number of devices (e.g., one)
BUT: Devices need to wait their turn, events/data could be missed, main
can get unmanageably long!
Interrupts: Enables pre-emptive scheduling, timer driven actions, transient hardware interactions, etc…
BUT: A little bit more work to set up, requires external hardware (“interrupt controller”) to encode external requests.
So far, your microbit has been running like clockwork (fetch-decode-execute, fetch-decode-execute, etc).
Now, an interrupt could happen at any time.
“An interrupt could happen at any time.”
What does this mean for your assumptions as a programmer?
Our computers need to respond to external events, as well as errors!
Interrupts happen all the time (keyboard?)
Breaks our assumptions about fetch-decode-execute cycle, need to follow a process to save our context, and get back to it later.
Everything is fine!
fetch-decode-execute ticking away!
Save PC and flags on the stack (the CPU hardware does this for you!!)
Now the PC is inside an interrupt handler (a function that has been set up previously to look after this interrupt)
The link register lr
has been set up with a special value!
push registers
declare local variables
..do some I/O..
..run some time critical code..
remove local variables
pop registers
Return from interrupt (bx lr
)
The “special” lr
gets the CPU to put everything back the way it was! (Clever!)
lr
has been set up with a special value!
bx lr
)You get to write it! (more on that later…)
But you don’t get to choose when it runs!
You’ve probably experienced an interrupt (exception) already!
What about “Usage Fault”?
ldr r0, =0x20000000
bx r0
N.B., the above code is broken on purpose! Your microbit will end up in the “Default_Handler”
Have a look in startup.S
:
.section .rodata.vtable @ (r)ead-(o)nly(data), the .vtable suffix is just a way to uniquely identify this section in the linker
.word _stack_end
.word Reset_Handler
.word NonMaskableInt_Handler
.word HardFault_Handler
.word MemManage_Handler
...
All the interrupt vectors are named here, and linked to a default handler.
Need to redefine one of the handler functions. E.g., for GPIOTE_IRQHandler
:
.global GPIOTE_IRQHandler
.type GPIOTE_IRQHandler, %function
GPIOTE_IRQHandler:
@ do some important stuff!
nop
bx lr
.size GPIOTE_IRQHandler, .-GPIOTE_IRQHandler
So I want to use the buttons…
Isn’t that similar to activating the LEDs?
The microbit’s buttons are connected to pin P0.14
(A) and P0.23
(B).
We need to configure the GPIOTE
module to listen on P0.14
, and configure the NVIC to make this happen. This is a little bit fussy!
Let’s do it.
@ 1: Configure GPIOTE_CONFIG[0] (Section 6.9.4.8 in nRF52833 reference manual)
@ mode = 1 (event), pin = 14 and port = 0 (P0.14 = Button A), polarity = 1 (LoToHi)
ldr r0, =GPIOTE_CONFIG0
ldr r1, =(1 | 14 << 8 | 0 << 13 | 1 << 16)
str r1, [r0]
@ 2: Enable Interrupt for GPIOTE[0] (id = 6) (S6.9.4.6 in nRF52833 reference manual)
ldr r0, =GPIOTE_INTENSET
ldr r1, =0b1
str r1, [r0]
@ 3: enable GPIOTE (interrupt #6 = NVIC_GPIOTE_ID) in NVIC_ISER0 (B3.4.4 in ARMv7-M Reference Manual)
ldr r0, =NVIC_ISER0
ldr r1, =(1 << 6) @ set the 6th bit since NVIC_GPIOTE_ID = 6
str r1, [r0]
.global GPIOTE_IRQHandler
.type GPIOTE_IRQHandler, %function
GPIOTE_IRQHandler:
@ interrupt code goes here
@ clear event
ldr r0, =GPIOTE_EVENTS_IN0
ldr r1, =0
str r1, [r0]
bx lr
.size GPIOTE_IRQHandler, .-GPIOTE_IRQHandler
What about the interrupt handler function and the AAPCS?
link register lr
?
status register xPSR
?
caller-save registers (r0
-r3
)?
let’s look at an interrupt handler and do some digging…
http://bob.cs.sonoma.edu/IntroCompOrg-RPi/chp-except.html
We just learned that an interrupt can stop our program at any time and run different code in the handler.
Returning from the handler puts our program back just the way it was before the interrupt.
we kind of can, folks!
Big Idea:
Have two (or more) main programs and use interrupts to swap in between them.
But what is a context?
The set of information that needs to be saved for a program to start up again after an interruption.
Would a program notice if it’s been switched out and back?
Could anything go wrong here?
What if both programs share data?
int G = 0;
G = 1; G = G + G; |
G = 2; G = G + G; |
G = 3; G = G + G; |
What’s the value of G at the end?
What’s the smallest G that could be produced? What’s the largest?
G: .word 0x00000000
ldr r4, =G mov r1, #1 str r1, [r4] ldr r4, =G ldr r2, [r4] add r2, r2, r2 str r2, [r4] |
ldr r4, =G mov r1, #2 str r1, [r4] ldr r4, =G ldr r2, [r4] add r2, r2, r2 str r2, [r4] |
ldr r4, =G mov r1, #3 str r1, [r4] ldr r4, =G ldr r2, [r4] add r2, r2, r2 str r2, [r4] |
What are the values of G that we can see now?
Fact 1: There are two roommates
Fact 2: When a roommate gets home, they open the fridge. If there is no milk, they go out to get some.
Now what is going to happen? What is the solution?
A “critical section” is a section where a program is accessing a shared resource.
We need a way to ensure that only one process can enter a critical section at a time.
This is called “mutual exclusion”.
int count = 0;
for (i = 1; i <= 100; i++) { count = count + 1; } |
for (i = 1; i <= 100; i++) { count = count - 1; } |
strex
and ldrex
An atomic operation is one that either happens completely, or not at all.
It’s indivisible.
There’s lots of operations we need to be atomic… (e.g., bank transfers)
(Photo by Dan Meyers on Unsplash)
“keep reading the lock (and setting it to 1) until you find it open”
This is what we have! Also called “load-link/store-conditional”.
A semaphore (Dijkstra, 1968) is a generalisation of our lock to handle common resources more generally.
We define a semaphore variable (S), and two operations:
Wait(S): if S > 0, then S := S - 1 and continue, else wait
Signal(S): S := S + 1, tell someone waiting to try again
So S could start at 1 (binary) or a higher number.
Concurrency is not an edge case! It’s needed all the time.
Multi-task, and multi-CPU systems require hardware support for concurrency.
Now you know the hardware operations that can support synchronisation implementations in your code.
In general, higher abstractions and safer solutions are provided; learn more in COMP2310!
ldr r0, =label_in_data_section
ldrex r1, [r0]
ldrex
loads r1
with the memory that r0
is pointing to, and sets the “local exclusive monitor” to exclusive.
It doesn’t do any checking of the exclusive monitor before doing so!
The local exclusive monitor is just 1-bit! Either “open” or “exclusive”.
(Multi-processor systems also have a “global exclusive monitor”… not covered here!)
ldr r0, =label_in_data_section
mov r1, 5
strex r2, r1, [r0]
cmp r2, 0
bne do_something_to_recover
strex
tries to store r1
in the memory that r0
is pointing to, but checks the exclusive monitor first.
If the store is allowed, r2
is set to 0, if it fails then r2
is set to 1.
Then what should we do?
strex
actually fail?Need to look in the ARMv7-M reference manual (Section A3.4 “Synchronisation and Semaphores”)
strex
is tagged exclusive in the local monitor; then store takes place (woo hoo!)strex
is NOT tagged exclusive; then it is “implementation defined” whether the store takes place (????).“Any ldrex
operation updates the tagged address to the most significant bits of the address… used for the operation.” (ARMv7-M reference manual)
Note that clrex
always clears the monitor, and that interrupt handlers run clrex
!
G. Taubenfeld. Concurrent programming, mutual exclusion (1965; Dijkstra), Encyclopedia of Algorithms (2008) 188–191 http://www.faculty.idc.ac.il/gadi/MyPapers/2008T-mutex.pdf
Wikipedia: Load-link and store conditional
Is there a smarter way to measure time on the microbit?
How about a function that tells you how much time has passed?
On an Arduino or in p5
or Processing you can call millis()
which returns how long your program has been running in milliseconds. How does that work?
@ returns: r0, number of milliseconds since starting
.type millis, %function
millis:
ldr r0, =milliseconds_count
ldr r0, [r0]
bx lr
.size millis, . - millis
.global SysTick_Handler
.type SysTick_Handler, %function
SysTick_Handler:
@ update the milliseconds count.
ldr r0, =milliseconds_count
ldr r1, [r0]
add r1, r1, #1
str r1, [r0]
bx lr
.data
milliseconds_count:
.word 0x0
@ enable SysTick - lower three bits of SYST_CSR (SysTick Control and Status Regsiter)
ldr r0, =ADR_SYST_CSR
ldr r1, =0b111 @ (enable systick, enable interrupt, set CPU as clock source)
str r1, [r0]
@ Store a reload value in SYST_RVR (Reload Value Register)
ldr r0, =ADR_SYST_RVR
ldr r1, =64000 @ 1ms @ 64MHz
str r1, [r0]