EL3: Multiple levels of table
This section of the guide walks through an example with two levels of translation. The single-level table at EL3 example used a single level 1 table. This means that all mappings were using 1GB blocks. For a simple system, this kind of course grain mapping is appropriate. However, many systems need more fine grain mappings, which is achieved by using multiple levels of tables.
In the examples package, the files are in:
Generate the level 1 table
The first steps of this example are the same as the single-level table at EL3 example. As in the single-level table at EL3 example, the
MAIR is populated with the three Types that the example uses. The
TCR is configured to select a 4KB granule and a starting level of translation is L1.
This example differs from the single-level table at EL3 example at the point of table generation. The following code generates the L1 table:
// // Generate L1 table // LDR x1, =tt_l1_base // Address of L1 table // : 0x0000,0000 - 0x3FFF,FFFF LDR x0, =TT_S1_DEVICE_nGnRnE // Entry template // AP=0, RW // Don't need to OR in address, as it is 0 STR x0, [x1] // : 0x4000,0000 - 0x7FFF,FFFF LDR x0, =TT_S1_DEVICE_nGnRnE // Entry template // AP=0, RW ORR x0, x0, #0x40000000 // 'OR' template with base physical address STR x0, [x1, #8] // : 0x8000,0000 - 0xBFFF,FFFF (DRAM on the VE and Base Platform) LDR x2, =tt_l2_base // Get address of L2 table LDR x0, =TT_S1_TABLE // Entry template for pointer to next level table ORR x0, x0, x2 // Combine template with L2 table Base address STR x0, [x1, #16] // Write template into entry table
As in the single-level table at EL3 example, this example uses templates that are defined at the start of the file to create each entry. The first two entries are the same as the single-level table at EL3 example. These entries create two 1GB block mappings with Device type.
The third entry is different. Instead of mapping a 1GB block, this entry points to a level 2 table. This level 2 table divides the 1GB block in to 512 2MB blocks. To do this, the example uses another template,
Where does the address for the level 2 table (
tt_l2_base) come from?
Like the level 1 table, the example uses a
fill directive to allocate a 4KB region of memory to hold the table. Here is the code that allocates the level 1 and 2 tables:
.align 12 .global tt_l1_base tt_l1_base: .fill 4096 , 1 , 0 .global tt_l2_base tt_l2_base: .fill 4096 , 1 , 0
The example uses the
fill directive to pre-fill the memory allocated for the tables with zeros. A value of zeros gives a fault. In a real system, code writes these zeros at run-time. However, pre-filling the zeros is useful for test code, to reduce simulation or emulation time.
Note: The level 2 table also needs to be size aligned, which is 4KB aligned in this example. In this example, the table is aligned because it immediately follows another size-aligned 4KB structure.
Generate the level 2 tables
The following code generates the level table 2:
// // Generate L2 table // … LDR x0, =tt_l2_base // Address of first L2 table // The L2 table covers the address range: // 0x8000_0000 - 0xBFFF_FFFF // // This example only populates entry 0, which covers: // 0x8000_0000 - 0x801F_FFFF LDR x1, =tt_l2_base // Address of L1 table LDR x0, =TT_S1_NORMAL_WBWA // Entry template ORR x0, x0, #TT_S1_INNER_SHARED // 'OR' with inner-shareable attribute // AP=0, RW ORR x0, x0, #0x80000000 // 'OR' template with base physical address STR x0, [x1] DSB SY
The level 2 table in this example covers the virtual address range
0xBFFF_FFFF, which is the third gigabyte of the virtual address space. The table contains 512 entries, and each entry describes 2MB of virtual address space.
The example populates the first entry of the level 2 table with an entry for a Normal/Cacheable block. This entry corresponds to the first 2MB of address space that is covered by the L2 table,
Overview of the configured virtual address space
The result of the translation tables is shown in the following diagram:
The table walk
Like in the single-level table at EL3 example, let’s look at the TARMAC trace showing the first table walk after the MMU is enabled:
81 clk TTW ITLB LPAE 1:1 000080002010 0000000080003003 : TABLE PXN=0 XN=0 AP=0 NS=0 ADDR=0x0000000080003000 81 clk TTW ITLB LPAE 1:2 000080003000 0000000080000705 : BLOCK ATTRIDX=1 NS=0 AP=0 SH=3 AF=1 nG=0 16E=0 PXN=0 XN=0 ADDR=0x0000000080000000 81 clk TLB FILL FVP_Base_AEMv8A_AEMv8A.cluster0.cpu0.UTLB 2M 0x80000000 EL3_s, nG asid=0:0x0080000000 Normal InnerShareable Inner=WriteBackWriteAllocate Outer=WriteBackWriteAllocate xn=0 pxn=0 ContiguousHint=0
In this example, unlike the single-level table at EL3 example, there are now two ITLB lines. The first reports 1:1, which refers to a stage 1 table entry. The trace shows that level 1 table entry fetched from memory points to a level 2 table.
The next line in the trace reports 1:2, which refers to a stage 1 level 2 table entry. This is entry is a Block descriptor, like we saw in the single-level table at EL3 example.
The final line shows the TLB entry being recorded. Because the translation came from a level 2 block, this time the size of the entry is recorded as 2MB.
Check your knowledge: This example shows a stage 1 translation, with two levels of table. What would you expect to see the trace for a stage 2 level 3 entry?