April 28, 2026

Allwinner H5 network driver -- the aarch64 MMU

As near as I can tell the aarch64 MMU and the 32 bit MMU are essentially identical.

The following (chapter D8) in the ARM documentation gives the VMSA (virtual memory system architecture).

Also useful (for me as a refresher) are these:

A peek at what U-Boot handed us

After writing code to dump the basic MMU registers, I see this:

TTBR0_EL1 = 0000000000000000
TTBR1_EL1 = 0000000000000000
TCR_EL1 = 0000000000000000

TTBR0_EL2 = 000000007fff0000
TCR_EL2 = 0000000080803520
The MMU is not set up for EL1.

The TTBR0_EL2 register is pointing to a page table at the top of our 1G address space, and it is no bigger than 64K in size.

Both TTBR0 and TTBR1 exist (for EL1) with the idea that TTBR1 will hold the address mapping for the operating system, and TTBR0 will hold the address mapping for the current user process.

The TCR register

The TCR has a multitude of fields.

 TCR:ips = 00000000
 TCR:t1sz = 00000000
 TCR:t0sz = 00000020

The IPS bits (34:32) are 000 indicating a 4G (32 bit) intermediate physical address size.

the t1sz field is zero, which it should be as there is no TTBR1 register for EL2.

The intent of having two pointers (TTBR0 and TTBR1) for el1 is that TTBR1 would hold the constant mapping for the kernel, which TTBR0 would be used for the often changing map for the user process. I am told that linux never does this. Linux ignores TTBR1 and handles everything with TTBR0. Of course we are not dealing with linux, but this is interesting.

The page size is set by these fields:

 TCR:tg0 = 00000000
 TCR:tg1 = 00000002
The tg1 value doesn't matter as we aren't using TTBR1 (and don't have TTBR1 at EL2).
The tg0 value of 0 indicates a 4K page size, which should be no surprise.

The table itself

Here are the first few entries. Each entry is 64 bits.
PTE-7fff0000 0000000000000401
PTE-7fff0008 0000000040000711
PTE-7fff0010 0000000080000711
PTE-7fff0018 00000000c0000711
PTE-7fff0020 0000000000000000 (invalid)
 -- lots of zero entries.
PTE-7fff1000 0000000000000401
PTE-7fff1008 0000000040000711
PTE-7fff1010 0000000080000711
PTE-7fff1018 00000000c0000711
 -- lots of zero entries.
 -- just garbage from here on
PTE-7fff2000 fffffffffff7ffff
PTE-7fff2008 ffffffffffffffff
PTE-7fff2010 ffffffffffffffff
PTE-7fff2018 ffffffffffefff7f
PTE-7fff2020 fffdffffffffffff

So the first level table is 4K bytes in size, so it has 512 entries. Each entry maps 1G, so the entire L1 table maps 512G. For our processor (the h5) we thus need only the first 4 of these entries. The rest are zero (invalid) and accessing those addresses will cause a fault.

The two lowest bits are important.
If the lowest bit is zero, the PTE is invalid.
If the two low bits are 01 it is a block descriptor.
If the two low bits are 11 it is a table descriptor.

It took some time for me to get a grip on what is going on here. There are two identical page tables, each with 512 entires. Only the first 4 are valid and each maps 1G. There are two page tables because the second is an "emergency" page table that gets switched to while the first one gets screwed with.

There are mysteries here that I don't understand yet. We have a 4K page size, yet each of these entries is a final PTE and yet maps 1G.

U-boot setup code

This little gem can be found in arch/arm/mach-sunxi/board.c
static struct mm_region sunxi_mem_map[] = {
    {
        /* SRAM, MMIO regions */
        .virt = 0x0UL,
        .phys = 0x0UL,
        .size = 0x40000000UL,
        .attrs = PTE_BLOCK_MEMTYPE(MT_DEVICE_NGNRNE) |
             PTE_BLOCK_NON_SHARE
    }, {
        /* RAM */
        .virt = 0x40000000UL,
        .phys = 0x40000000UL,
        .size = CONFIG_SUNXI_DRAM_MAX_SIZE,
        .attrs = PTE_BLOCK_MEMTYPE(MT_NORMAL) |
             PTE_BLOCK_INNER_SHARE
    }, {
        /* List terminator */
        0,
    }
};
struct mm_region *mem_map = sunxi_mem_map;
The important value CONFIG_SUNXI_DRAM_MAX_SIZE is in include/generated/autoconf.h and is:
#define CONFIG_SUNXI_DRAM_MAX_SIZE 0xC0000000
So, U-boot should set up 2 regions in the mmu mapping. First a 1G region to hold device registers, then a 3G region for DRAM.


Have any comments? Questions? Drop me a line!

Tom's electronics pages / tom@mmto.org