Skip to content

Instantly share code, notes, and snippets.

@petemoore
Last active February 20, 2024 15:35
Show Gist options
  • Save petemoore/4a17f701d2ad57025a1f6ab90a66ce33 to your computer and use it in GitHub Desktop.
Save petemoore/4a17f701d2ad57025a1f6ab90a66ce33 to your computer and use it in GitHub Desktop.
rpi3 bare metal interrupt example

This is an example to demonstrate how to have a timer execute in EL1 on a raspberry pi 3 in bare metal assembly.

Created demo to support the user question in https://forums.raspberrypi.com/viewtopic.php?t=365600

Generate kernel with:

$ aarch64-none-elf-as -o rpi3-interrupt-demo.o rpi3-interrupt-demo.s 
$ aarch64-none-elf-ld --no-warn-rwx-segments -N -Ttext=0x0 -o rpi3-interrupt-demo.elf rpi3-interrupt-demo.o
$ aarch64-none-elf-objcopy --set-start=0x0 rpi3-interrupt-demo.elf -O binary rpi3-interrupt-demo.img

If you get an error when linking like:

undefined reference to `no symbol'

then you have probably hit https://sourceware.org/bugzilla/show_bug.cgi?id=31228. If so, just add _start to the literal value of the adrp instruction (e.g. adrp x1, 0x3f00b000 + _start). _start has value zero, and thus has no effect, but it works around the linker bug which expects a symbol.

Prepare config.txt as:

disable_commandline_tags=1
kernel_old=1
arm_64bit=1
kernel=rpi3-interrupt-demo.img

Download firmware with:

$ for file in LICENCE.broadcom bootcode.bin start.elf start4.elf; do curl -fL "https://github.com/raspberrypi/firmware/blob/13691cee95902d76bc88a3f658abeb37b3c90b03/boot/${file}?raw=true" > "${file}"; done

Copy all files to root partition of SD card:

  • LICENCE.broadcom
  • bootcode.bin
  • config.txt
  • rpi3-interrupt-demo.img
  • start.elf
  • start4.elf

Alternatively, run the demo under QEMU:

$ qemu-system-aarch64 -M raspi3b -kernel rpi3-interrupt-demo.elf -serial null -serial stdio

Note, the demo just writes x to UART with each timer interrupt. To see this working on a real rpi3, you will need to e.g. connect a USB/Serial device to view the output on a different computer.

.global _start
.arch armv8-a
.cpu cortex-a53
.set AUX_IRQ, 0x0000 // Auxiliary Interrupt Status
.set AUX_ENABLES, 0x0004 // Auxiliary Enables
.set AUX_MU_IO_REG, 0x0040 // Mini Uart I/O Data
.set AUX_MU_IER, 0x0044 // Mini Uart Interrupt Enable
.set AUX_MU_IIR, 0x0048 // Mini Uart Interrupt Identify
.set AUX_MU_LCR, 0x004C // Mini Uart Line Control
.set AUX_MU_MCR, 0x0050 // Mini Uart Modem Control
.set AUX_MU_LSR, 0x0054 // Mini Uart Line Status
.set AUX_MU_MSR, 0x0058 // Mini Uart Modem Status
.set AUX_MU_SCRATCH, 0x005C // Mini Uart Scratch
.set AUX_MU_CNTL, 0x0060 // Mini Uart Extra Control
.set AUX_MU_STAT, 0x0064 // Mini Uart Extra Status
.set AUX_MU_BAUD, 0x0068 // Mini Uart Baudrate
# ------------------------------------------------------------------------------
# See "BCM2837 ARM Peripherals" datasheet pages 90-104:
# https://cs140e.sergio.bz/docs/BCM2837-ARM-Peripherals.pdf
# ------------------------------------------------------------------------------
.set GPFSEL1, 0x0004 // GPIO Function Select 1
.set GPPUD, 0x0094 // GPIO Pin Pull-up/down Enable
.set GPPUDCLK0, 0x0098 // GPIO Pin Pull-up/down Enable Clock 0
.text
.align 2
_start:
mrs x0, mpidr_el1 // x0 = Multiprocessor Affinity Register value.
ands x0, x0, #0x3 // x0 = core number.
b.ne sleep // Put all cores except core 0 to sleep.
ldr x0, =0x30d00800
msr sctlr_el1, x0 // Update "System Control Register (EL1)":
// set RES:1 bits (11, 20, 22, 23, 28, 29)
// disable caches (bits 2, 12)
mov x0, 0x80000000
msr hcr_el2, x0 // Update "Hypervisor Configuration Register":
// set bit 31 => execution state for EL1 is aarch64
mrs x0, currentel
and x0, x0, #0x0c
cmp x0, #0x0c
b.ne 1f
##################################################
# We are in EL3 (kernel_old=1 in config.txt)
# Move from EL3 to EL1 directly (skip EL2)
mov x0, 0x00000431
msr scr_el3, x0 // Update "Secure Configuration Register":
// set bit 0 => EL0 and EL1 are in non-secure state
// set RES:1 bits 4, 5
// set bit 10 => EL2 is aarch64, EL2 controls EL1 and EL0 behaviors
mov x0, 0x000001c5
msr spsr_el3, x0 // Update "Saved Program Status Register (EL3)":
// set bit 0 => dedicated stack pointer selected on EL switch to/from EL3
// set bit 2 (and clear bit 3) => drop to EL1 on eret
// set bit 6 => mask (disable) error (SError) interrupts
// set bit 7 => mask (disable) regular (IRQ) interrupts
// set bit 8 => mask (disable) fast (FIQ) interrupts
adr x0, 2f // Address of label 2: below
msr elr_el3, x0 // Update Exception Link Register (EL3):
// set to return to address of label 2: below
eret
# Move from EL3 to EL1 completed
##################################################
1:
##################################################
# We are in EL2 (kernel_old=1 /not/ in config.txt)
# Move from EL2 to EL1
mov x0, 0x000001c5
msr spsr_el2, x0 // Update "Saved Program Status Register (EL2)":
// set bit 0 => dedicated stack pointer selected on EL switch to/from EL2
// set bit 2 (and clear bit 3) => drop to EL1 on eret
// set bit 6 => mask (disable) error (SError) interrupts
// set bit 7 => mask (disable) regular (IRQ) interrupts
// set bit 8 => mask (disable) fast (FIQ) interrupts
adr x0, 2f
msr elr_el2, x0 // Update Exception Link Register (EL2):
// set to return address after this `eret`
eret
# Move from EL2 to EL1 completed
##################################################
2:
# We are in EL1
mov sp, #0x01000000
bl uart_init // Initialise UART interface.
bl irq_vector_init
dsb sy // TODO: Not sure if this is needed at all, or if a less aggressive barrier can be used
bl timer_init
dsb sy // TODO: Not sure if this is needed at all, or if a less aggressive barrier can be used
bl enable_ic
dsb sy // TODO: Not sure if this is needed at all, or if a less aggressive barrier can be used
bl enable_irq
sleep:
wfi // Sleep until woken.
b sleep // Go to sleep; it has been a long day.
.align 2
irq_vector_init:
adr x0, vectors // load VBAR_EL1 with
msr vbar_el1, x0 // vector table address
ret
enable_irq:
msr daifclr, #2
ret
enable_ic:
mov w0, #0x00000002
adrp x1, 0x3f00b000
str w0, [x1, #0x210] // [0x3f00b210] = 0x00000002
ret
handle_irq:
stp x29, x30, [sp, #-16]! // Push frame pointer, procedure link register on stack.
mov x29, sp // Update frame pointer to new stack location.
adrp x1, 0x3f00b000
ldr w0, [x1, #0x204] // w0 = [0x3f00b204]
cmp w0, #2
b.ne 1f
bl handle_timer_irq
1:
ldp x29, x30, [sp], #16 // Pop frame pointer, procedure link register off stack.
ret
timer_init:
adrp x0, 0x3f003000
ldr w1, [x0, #0x04]
mov w2, #0x20000
add w1, w1, w2
str w1, [x0, #0x10] // [0x3f003010] += [0x3f003004] + 0x20000 (rpi3)
ret
handle_timer_irq:
stp x29, x30, [sp, #-16]! // Push frame pointer, procedure link register on stack.
mov x29, sp // Update frame pointer to new stack location.
bl timer_init // [0x3f003010] += [0x3f003004] + 200000 (rpi3)
mov w1, #0x02
str w1, [x0] // [0x3f003000] = 2 (rpi3)
bl timed_interrupt
ldp x29, x30, [sp], #0x10 // Pop frame pointer, procedure link register off stack.
ret
timed_interrupt:
stp x29, x30, [sp, #-16]! // Push frame pointer, procedure link register on stack.
mov x29, sp // Update frame pointer to new stack location.
mov x0, 'x' // Write 'x' to UART
bl uart_send
ldp x29, x30, [sp], #0x10 // Pop frame pointer, procedure link register off stack.
ret
# ------------------------------------------------------------------------------
# Initialise the Mini UART interface for logging over serial port.
# Note, this is Broadcomm's own UART, not the ARM licenced UART interface.
# ------------------------------------------------------------------------------
.align 2
uart_init:
adrp x1, 0x3f215000
ldr w2, [x1, AUX_ENABLES] // w2 = [AUX_ENABLES] (Auxiliary enables)
orr w2, w2, #1
str w2, [x1, AUX_ENABLES] // [AUX_ENABLES] |= 0x00000001 => Enable Mini UART.
str wzr, [x1, AUX_MU_IER] // [AUX_MU_IER_REG] = 0x00000000 => Disable Mini UART interrupts.
str wzr, [x1, AUX_MU_CNTL] // [AUX_MU_CNTL_REG] = 0x00000000 => Disable Mini UART Tx/Rx
mov w2, #0x6 // w2 = 6
str w2, [x1, AUX_MU_IIR] // [AUX_MU_IIR_REG] = 0x00000006 => Mini UART clear Tx, Rx FIFOs
mov w3, #0x3 // w3 = 3
str w3, [x1, AUX_MU_LCR] // [AUX_MU_LCR_REG] = 0x00000003 => Mini UART in 8-bit mode.
str wzr, [x1, AUX_MU_MCR] // [AUX_MU_MCR_REG] = 0x00000000 => Set UART1_RTS line high.
mov w2, #0x0000010e
str w2, [x1, AUX_MU_BAUD] // [AUX_MU_BAUD_REG] = 0x0000010e (rpi3)
// => baudrate = system_clock_freq/(8*([AUX_MU_BAUD_REG]+1))
// (as close to 115200 as possible)
adrp x4, 0x3f200000 // x4 = GPIO base = 0x3f200000 (rpi3)
ldr w2, [x4, GPFSEL1] // w2 = [GPFSEL1]
and w2, w2, #0xfffc0fff // Unset bits 12, 13, 14 (FSEL14 => GPIO Pin 14 is an input).
// Unset bits 15, 16, 17 (FSEL15 => GPIO Pin 15 is an input).
orr w2, w2, #0x00002000 // Set bit 13 (FSEL14 => GPIO Pin 14 takes alternative function 5).
orr w2, w2, #0x00010000 // Set bit 16 (FSEL15 => GPIO Pin 15 takes alternative function 5).
str w2, [x4, GPFSEL1] // [GPFSEL1] = updated value => Enable UART 1.
str wzr, [x4, GPPUD] // [GPPUD] = 0x00000000 => GPIO Pull up/down = OFF
mov x5, #0x96 // Wait 150 instruction cycles (as stipulated by datasheet).
1:
subs x5, x5, #0x1 // x0 -= 1
b.ne 1b // Repeat until x0 == 0.
mov w2, #0xc000 // w2 = 2^14 + 2^15
str w2, [x4, GPPUDCLK0] // [GPPUDCLK0] = 0x0000c000 => Control signal to lines 14, 15.
mov x0, #0x96 // Wait 150 instruction cycles (as stipulated by datasheet).
2:
subs x0, x0, #0x1 // x0 -= 1
b.ne 2b // Repeat until x0 == 0.
str wzr, [x4, GPPUDCLK0] // [GPPUDCLK0] = 0x00000000 => Remove control signal to lines 14, 15.
str w3, [x1, AUX_MU_CNTL] // [AUX_MU_CNTL_REG] = 0x00000003 => Enable Mini UART Tx/Rx
ret // Return.
# ------------------------------------------------------------------------------
# Send a byte over Mini UART
# ------------------------------------------------------------------------------
# On entry:
# x0: char to send
# On exit:
# x1: [aux_base] = 0x3f215000 (rpi3)
# x2: Last read of [AUX_MU_LSR_REG] when waiting for bit 5 to be set
uart_send:
adrp x1, 0x3f215000
1:
ldr w2, [x1, AUX_MU_LSR] // w2 = [AUX_MU_LSR_REG]
tbz x2, #5, 1b // Repeat last statement until bit 5 is set.
strb w0, [x1, AUX_MU_IO_REG] // [AUX_MU_IO_REG] = w0
ret
.macro handle_invalid_entry type
kernel_entry
mov w0, 'A'
add x0, x0, #\type
bl uart_send
b sleep
.endm
.macro ventry label
.align 7
b \label
.endm
.macro kernel_entry
sub sp, sp, #16 * 16
stp x0, x1, [sp, #16 * 0]
stp x2, x3, [sp, #16 * 1]
stp x4, x5, [sp, #16 * 2]
stp x6, x7, [sp, #16 * 3]
stp x8, x9, [sp, #16 * 4]
stp x10, x11, [sp, #16 * 5]
stp x12, x13, [sp, #16 * 6]
stp x14, x15, [sp, #16 * 7]
stp x16, x17, [sp, #16 * 8]
stp x18, x19, [sp, #16 * 9]
stp x20, x21, [sp, #16 * 10]
stp x22, x23, [sp, #16 * 11]
stp x24, x25, [sp, #16 * 12]
stp x26, x27, [sp, #16 * 13]
stp x28, x29, [sp, #16 * 14]
str x30, [sp, #16 * 15]
.endm
.macro kernel_exit
ldp x0, x1, [sp, #16 * 0]
ldp x2, x3, [sp, #16 * 1]
ldp x4, x5, [sp, #16 * 2]
ldp x6, x7, [sp, #16 * 3]
ldp x8, x9, [sp, #16 * 4]
ldp x10, x11, [sp, #16 * 5]
ldp x12, x13, [sp, #16 * 6]
ldp x14, x15, [sp, #16 * 7]
ldp x16, x17, [sp, #16 * 8]
ldp x18, x19, [sp, #16 * 9]
ldp x20, x21, [sp, #16 * 10]
ldp x22, x23, [sp, #16 * 11]
ldp x24, x25, [sp, #16 * 12]
ldp x26, x27, [sp, #16 * 13]
ldp x28, x29, [sp, #16 * 14]
ldr x30, [sp, #16 * 15]
add sp, sp, #16 * 16
eret
.endm
.text
/*
* Exception vectors.
*/
.align 11
vectors:
ventry sync_invalid_el1t // Synchronous EL1t
ventry irq_invalid_el1t // IRQ EL1t
ventry fiq_invalid_el1t // FIQ EL1t
ventry error_invalid_el1t // Error EL1t
ventry sync_invalid_el1h // Synchronous EL1h
ventry el1_irq // IRQ EL1h
ventry fiq_invalid_el1h // FIQ EL1h
ventry error_invalid_el1h // Error EL1h
ventry sync_invalid_el0_64 // Synchronous 64-bit EL0
ventry irq_invalid_el0_64 // IRQ 64-bit EL0
ventry fiq_invalid_el0_64 // FIQ 64-bit EL0
ventry error_invalid_el0_64 // Error 64-bit EL0
ventry sync_invalid_el0_32 // Synchronous 32-bit EL0
ventry irq_invalid_el0_32 // IRQ 32-bit EL0
ventry fiq_invalid_el0_32 // FIQ 32-bit EL0
ventry error_invalid_el0_32 // Error 32-bit EL0
.align 2
sync_invalid_el1t:
handle_invalid_entry 0
irq_invalid_el1t:
handle_invalid_entry 1
fiq_invalid_el1t:
handle_invalid_entry 2
error_invalid_el1t:
handle_invalid_entry 3
sync_invalid_el1h:
handle_invalid_entry 4
el1_irq:
kernel_entry
bl handle_irq
kernel_exit
fiq_invalid_el1h:
handle_invalid_entry 6
error_invalid_el1h:
handle_invalid_entry 7
sync_invalid_el0_64:
handle_invalid_entry 8
irq_invalid_el0_64:
handle_invalid_entry 9
fiq_invalid_el0_64:
handle_invalid_entry 10
error_invalid_el0_64:
handle_invalid_entry 11
sync_invalid_el0_32:
handle_invalid_entry 12
irq_invalid_el0_32:
handle_invalid_entry 13
fiq_invalid_el0_32:
handle_invalid_entry 14
error_invalid_el0_32:
handle_invalid_entry 15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment