Changing Exception levels

Switching between Exception levels is done by returning from an exception. However, we must also have something to return to. In startup.s we define a function:

.global el1_entry_aarch64
    .type el1_entry_aarch64, "function"

el1_entry_aarch64:
  // we can use the same vector table in this example, but in general
  // each combination of Exception level, Security state, and Execution state
  // will need a new vector table
  LDR      x0, =vectors
  MSR	   VBAR_EL1, x0

  //we must ensure that floating point register accesses are not trapped
  //since the c library for AArch64-v8A uses them
  MOV      x0, #(0x3 << 20)
  MSR      CPACR_EL1, x0

  // ensures that all instructions complete before
  ISB
  // Branch to scatter loading and C library init code
  .global  __main
  B        __main

We now have a label that the ERET can branch to. This means that we can modify the start64 function, as shown in the following code:

boot:
  ADRP x0, Image$$STACK_EL3$$ZI$$Limit // get stack address
  MOV  sp, x0

  // NB, CODE OMITTED

  // Configure SCR_EL3
  // -----------------
  MOV      w1, #0              // Initial value of register is unknown
  ORR 	   w1, w1, #(1 << 11)  // set ST bit (disable trapping of timer control registers)
  ORR      w1, w1, #(1 << 10)  // set RW bit (next lower EL in aarch64)
  ORR      w1, w1, #(1 << 3)   // Set EA bit (SError routed to EL3)
  ORR      w1, w1, #(1 << 2)   // Set FIQ bit (FIQs routed to EL3)
  ORR      w1, w1, #(1 << 1)   // Set IRQ bit (IRQs routed to EL3)
  MSR      SCR_EL3, x1

  // NB, CODE OMITTED

  // Initialize SCTLR_EL1
  // --------------------
  // SCTLR_EL1 has an unknown reset value and must be configured
  // before we can enter EL1
  MSR 	   SCTLR_EL1, xzr

  LDR      x0, =el1_entry_aarch64
  LDR	   x1, =AArch64_EL1_SP1
  MSR 	   ELR_EL3, x0 // where to branch to when exception completes
  MSR      SPSR_EL3, x1 // set the program state for this point to a known value

  BL       gicInit
  
  ERET

In the preceding code, the following operations are performed:

  1. Define a new stack pointer for the current exception level. In the previous guides (Building your first embedded image, Retargeting embedded output to UART, and Creating an event-driven embedded image), we relied on the Arm C libraries to initialize the stack pointer. Because we have moved our branch to __main, this will only initialize a stack pointer for EL1. We also add the line STACK_EL3 +0 ALIGN 64 EMPTY 0x4000 {} in scatter.txt to define the stack in memory.
  2. Disable trapping of the timer register accesses, because the processor will be in EL1 when the timer interrupt is generated.
  3. Set the next lower Exception level, Secure EL1, to the 64 bit Execution state.
  4. Ensure the System Control Register, SCTLR_EL1, is zero initialized, and set the Exception Link Register, ELR_EL3, and Saved Program State Register, SPSR_EL3, to the desired address and state at EL1.
    Note: SPSR_EL3 is responsible for controlling the Exception level that the processor enters after the ERET, while ELR_EL3 merely specifies the address to return to.
  5. Move the branch to gicInit here. Because this function modifies registers accessible at EL3 only, it cannot be placed in the main() function, because that function is now at EL1.

Building and running this code will send a "hello world" and interrupt message, as we saw in the previous guides in this series.

Previous Next