EL1 Single-level table
In this section of the guide, we recreate the single-level table at EL3 example, this time running at EL1 in Non-secure state. The single-level table at EL3 and multiple-level table examples run at EL3.
In the examples package, the files are in
The Processing Element (PE) always comes out of reset in the highest implemented Exception level. For our test system, the highest implemented Exception level is EL3. The example therefore needs to include code to switch from EL3 to EL1. Before changing the Exception level, we need to carry out some configuration at EL3.
The first register the example configures is the Secure Configuration Register (
SCR_EL3), as you see in the following code:
// Configure SCR_EL3 // ------------------ MOV x0, #1 // NS=1 ORR x0, x0, #(1 << 1) // IRQ=1 IRQs routed to EL3 ORR x0, x0, #(1 << 2) // FIQ=1 FIQs routed to EL3 ORR x0, x0, #(1 << 3) // EA=1 SError routed to EL3 ORR x0, x0, #(1 << 8) // HCE=1 HVC instructions are enabled ORR x0, x0, #(1 << 10) // RW=1 Next EL down uses AArch64 ORR x0, x0, #(1 << 11) // ST=1 Secure EL1 can access timers MSR SCR_EL3, x0
There are many settings in
SCR_EL3. Two settings are important for this example:
- Controls whether lower Exception levels are Secure or Non-secure
- Controls whether the next Exception level uses AArch64 or AArch32
The example sets both bits. This means that lower Exception levels are Non-secure and that EL2 uses AArch64.
We also need to configure the Hypervisor Configuration Register (
HCR_EL2). In a real software stack, code running in EL2 would do this. However, to keep the example simple, these registers are programmed from EL3 instead. The code to configure HCR_EL2 is shown here:
// Configure HCR_EL2 // ------------------ ORR w0, wzr, #(1 << 3) // FMO=1 ORR x0, x0, #(1 << 4) // IMO=1 ORR x0, x0, #(1 << 31) // RW=1 NS.EL1 is AArch64 // TGE=0 Entry to NS.EL1 is possible // VM=0 Stage 2 MMU disabled MSR HCR_EL2, x0
Like with the
SCR_EL3, HCR_EL2 contains many controls. For this example, like with the single-level table at EL3 and multiple-level table examples, we are most interested in two settings:
- Controls whether EL1 uses AArch64 or AArch32
- Enables/disables stage 2 translation at EL1 and EL0
The example disables stage 2 and sets EL1 to use AArch64.
There are other settings in EL2 registers that we need to configure before switching to EL2. These are shown in the following code:
// Set up VMPIDR_EL2/VPIDR_EL1 // --------------------------- MRS x0, MIDR_EL1 MSR VPIDR_EL2, x0 MRS x0, MPIDR_EL1 MSR VMPIDR_EL2, x0 // Set VMID // --------- // Although we are not using stage 2 translation, NS.EL1 still cares // about the VMID MSR VTTBR_EL2, xzr // Set SCTLRs for EL1/2 to safe values // ------------------------------------ MSR SCTLR_EL2, xzr MSR SCTLR_EL1, xzr
Reads of the
MIDR_EL2 registers at NS.EL1 return virtual values. The registers which hold these virtual values,
VPIDR_EL2, do not have defined reset values. Software should initialize these registers before entering EL1 for the first time. For this example, we are not using virtualization. This means that we can copy the physical values.
Even though stage 2 is disabled, EL1 still uses a Virtual Machine Identifier (VMID). It is good practice to set this to a known value, before entering EL1. This is particularly important when working in a multi-core environment. All the PEs that run within the same NS.EL0/1 translation regime need to use the same VMID.
Finally, there are separate System Control Registers (
SCTLR_ELx) for EL3, EL2, and EL1. Only the
SCTLR_ELx for the highest implemented Exception level has a known reset value. Software must set the
SCTLR_ELx registers for lower Exception levels to known or safe values before entering those Exception levels. This example sets them to 0, which ensures that the MMU for that Exception level is disabled.
Now that the minimum configuration is performed, control pass to NS.EL1, as you see in the following code:
ADR x0, el1_entry MSR ELR_EL3, x0 LDR x0, =AArch64_EL1_SP1 MSR spsr_el3, x0 ERET // ------------------------------------------------------------ // Enter EL1 // ------------------------------------------------------------ el1_entry:
The only way to move to a lower Exception level is to perform an exception return. Normally, the exception return information is generated as part of taking an exception.. Because this is the part of boot the example instead creates the required information and populates the registers. The example sets the Saved Processor State Register (
SPSR_ELx) to indicate EL1 using AArch64, and the Exception Link Register (
ELR_ELx) to point to the start of the EL1 code. The
ERET instruction then performs the Exception return.
Configure the MMU at EL1
Now that execution has entered EL1, the next step is to configure the MMU. The steps are the same as in the first example, but this time we use
_EL1 registers instead of
_EL3 registers. For examples, let's look at the following code:
// Set the Base address // --------------------- LDR x0, =tt_l1_base MSR TTBR0_EL1, x0 // Set up memory attributes // ------------------------- // This equates to: // 0 = b01000100 = Normal, Inner/Outer Non-Cacheable // 1 = b11111111 = Normal, Inner/Outer WB/WA/RA // 2 = b00000000 = Device-nGnRnE MOV x0, #0x000000000000FF44 MSR MAIR_EL1, x0
Unlike EL3, in EL1/0 there are two virtual address regions: one at the bottom of the address space and another at the top of the address space. This is illustrated in the following diagram:
By convention, the lower address region is called User space and the upper region is called Kernel space. However, this is only a convention and you will not see these names used in the Architecture Reference Manual.
For this example, we only configure the lower region. We disable the upper region, using a control in the Translation Control Register (
TCR_ELx). The code for this is shown here:
// Set up TCR_EL1 // --------------- MOV x0, #0x19 // T0SZ=0b011001 Limits VA space to 39 bits ORR x0, x0, #(0x1 << 8) // IGRN0=0b01 Walks to TTBR0 are Inner WB/WA ORR x0, x0, #(0x1 << 10) // OGRN0=0b01 Walks to TTBR0 are Outer WB/WA ORR x0, x0, #(0x3 << 12) // SH0=0b11 Inner Shareable ORR x0, x0, #(0x1 << 23) // EPD1=0b1 Disable table walks from TTBR1 // TBI0=0b0 // TG0=0b00 4KB granule for TTBR0 // A1=0 TTBR0 contains the ASID // AS=0 8-bit ASID // IPS=0 32-bit IPA space MSR TCR_EL1, x0
EPDn bits enable or disable walks from the lower region (
EPD0) and the upper region (
EPD1). The example only configures the lower region (
EPD1==0). Walks to the upper region are disabled (
EPD1==1). Because table walks to the upper region are disabled, the example does not need to provide a table pointer in
The code to generate the translation tables is unchanged from the single-level table at EL3 example.
Non-secure translation regimes
The example in this section of the guide runs in NS.EL1. The translation tables in this example are identical to the translation tables at EL3 in the single-level table at EL3 example. Does this mean that the resulting mappings are the same?
The answer is no. There is an important difference between Secure and Non-secure translation regimes. A Secure translation regime maps virtual addresses to Secure or Non-secure physical addresses that are controlled by the
NS bit in the table entries. A Non-secure translation regime only maps to Non-secure physical addresses. The
NS bit in the table entries is ignored.
In the single-level table at EL3 example and this example, the
NS bit in the table entries is b0 (Secure). At EL3, this causes the outputted address to be Secure. In NS.EL1 the
NS bit is ignored, and the outputted address is Non-secure.
Note: On a real system, it is very unlikely that you could run both these two examples, because the memory at physical address
0x8000_0000 would either be Secure or Non-secure. The memory system of a real system would not allow both kinds of access to the memory. However, the FVP model that is used for these examples allows us to control which types of accesses are permitted to DRAM using model parameters.
The table walk
Tracing this example gives a very similar result to the single-level table at EL3 example, as you see in the following code:
93 clk IT (93) 80000174 d5181000 O EL1h_n : MSR SCTLR_EL1,x0 93 clk R SCTLR_EL1 00000000:00001005 93 clk CACHE FVP_Base_AEMv8A_AEMv8A.cluster0.cpu0.l1dcache LINE 0100 ALLOC 0x000080002000_NS 93 clk CACHE FVP_Base_AEMv8A_AEMv8A.cluster0.l2_cache LINE 0800 ALLOC 0x000080002000_NS 93 clk TTW ITLB LPAE 1:1 000080002010 0000000080000705 : BLOCK ATTRIDX=1 NS=0 AP=0 SH=3 AF=1 nG=0 16E=0 PXN=0 XN=0 ADDR=0x0000000080000000 93 clk TLB FILL FVP_Base_AEMv8A_AEMv8A.cluster0.cpu0.UTLB 1G 0x80000000_NS EL1_n vmid=0:0x0080000000_NS Normal InnerShareable Inner=WriteBackWriteAllocate Outer=WriteBackWriteAllocate xn=0 pxn=0 ContiguousHint=0 93 clk CACHE FVP_Base_AEMv8A_AEMv8A.cluster0.cpu0.l1icache LINE 000a ALLOC 0x000080000140_NS 93 clk CACHE FVP_Base_AEMv8A_AEMv8A.cluster0.l2_cache LINE 0050 ALLOC 0x000080000140_NS
There are, however, some important differences between the single-level table at EL3 example and this example, starting with the MSR, as this code shows:
93 clk IT (93) 80000174 d5181000 O EL1h_n : MSR SCTLR_EL1,x0
TARMAC records the Exception level and Security state that instructions were executed in. In the previous examples, this was
EL3h_s, but this example reports
EL1h_n. This means that EL1 is in Non-secure state.
The created TLB entry is also different, as this code shows:
93 clk TLB FILL FVP_Base_AEMv8A_AEMv8A.cluster0.cpu0.UTLB 1G 0x80000000_NS EL1_n vmid=0:0x0080000000_NS Normal InnerShareable Inner=WriteBackWriteAllocate Outer=WriteBackWriteAllocate xn=0 pxn=0 ContiguousHint=0
The trace shows the TLB entry recording the translation regime (
EL1_n). The trace also shows the VMID being stored (
vmid=0). As explained in Enter NS.EL1, even when stage 2 translation is disabled, the VMID is still recorded for the Non-secure EL1 translation regime.