carmkernelxilinxmmu

Armv7 MMU on the ZYNQ-7000 not starting the virtualization


Introduction

I have been trying to evaluate the MMU functionality for the Xilinx zynq7000 soc that contains 2 A9-Cortex processors.

Firstly I have tried using the xil_mmu.h library, however after reading the functions that were implemented there realised that the mapping does not allow real P to V or V to P translation. Further more this library does not include any notion of granularity and only supports 1M page table entries. Lets analyse a function from this library to illustrate my concerns: xil_mmu.c

void Xil_SetTlbAttributes(INTPTR Addr, u32 attrib)
{
    u32 *ptr;
    u32 section;

    section = Addr / 0x100000U;
    ptr = &MMUTable;
    ptr += section;
    if(ptr != NULL) {
        *ptr = (Addr & 0xFFF00000U) | attrib;
    }

    Xil_DCacheFlush();

    mtcp(XREG_CP15_INVAL_UTLB_UNLOCKED, 0U);
    /* Invalidate all branch predictors */
    mtcp(XREG_CP15_INVAL_BRANCH_ARRAY, 0U);

    dsb(); /* ensure completion of the BP and TLB invalidation */
    isb(); /* synchronize context on this processor */
}

As you can tell the section variable (one of the 4096 1MB sections in a 4GB address space) is chosen in terms of the physical address, so for instance there is no way that I can map a physical address 0x100000 to correspond to a virtual 0x200000 or other.

Own implementation

Seeing the lack of modularity with this approach (basically no real virtualization) I decided to implement an extension to the basic xil_mmu library.

/*
 *  Function to flush, invalidate the TLBs and synchronize the processors
 */

static void hwi_mmu_picoZed_7010_sync()
{
    Xil_DCacheFlush();

    mtcp(XREG_CP15_INVAL_UTLB_UNLOCKED, 0U);
    /* Invalidate all branch predictors */
    mtcp(XREG_CP15_INVAL_BRANCH_ARRAY, 0U);

    dsb(); /* ensure completion of the BP and TLB invalidation */
    isb(); /* synchronize context on this processor */
}

/*
 *  This function sets an entry in the L1 table given the attributes
 */
static void hwi_mmu_picoZed_7010_set_table_entry(uint32_t entry, uint32_t properties)
{
    // Manage entries out of bound
    // You can add some type of fault here
    if (entry > 4095)
        return;
    // Get one of the 4096 entries
    uintptr_t *new_entry = (uintptr_t *)&MMUTable + entry;
    if (new_entry)
        *new_entry = properties;
}

// Wrapper functions calling hwi_mmu_picoZed_7010_set_table_entry for specific entry types

// Sets a section in the L1 table with the given properties and the physical address
// bits [31:20] of the address
void hwi_mmu_set_L1_Section(uint32_t addr, uint32_t entry, uint32_t properties)
{
    // Verify that this is effectively a section
    if (!IS_ENTRY_SECTION(properties))
        return;
    hwi_mmu_picoZed_7010_set_table_entry(entry, (addr & 0xFFF00000) | properties);
}

As well as some macros to easier set the required memory attributes


#    define SECTION_ENTRY 0x00000002
#    define IS_ENTRY_SECTION(entry) (((entry)&0x04000003) == SECTION_ENTRY)

// NONE for Privileged, NONE for User
#    define SECTION_AP_PERMISSION_FAULT 0x0000
// R/W for Privileged, NONE for User
#    define SECTION_AP_PRIVILEDGE_ACCESS 0x0400
// R/W for Privileged, R for User
#    define SECTION_AP_USER_NO_WRITE 0x0800
// R/W for Privileged, R/W for User
#    define SECTION_AP_FULL_ACCESS 0x0C00
// R for Privileged, NONE for User
#    define SECTION_AP_PRIVELEDGE_READ 0x8400
// R for Privileged, R for User
#    define SECTION_AP_READ_ONLY 0x8800

// Basically non cacheable
#    define SECTION_STRONGLY_ORDERED 0x0000
// Write to cache only (memory is stale) and allocate new entries
#    define SECTION_SHAREABLE_DEVICE 0x0004
// Write to both cache and memory
#    define SECTION_WRITE_THROUGH_NO_ALLOC_ON_WRITE 0x0008
// Write to cache only (memory is stale) no allocation
#    define SECTION_WRITE_BACK_NO_ALLOC_ON_WRITE 0x000C
// Non cacheable in Normal memory
#    define SECTION_NON_CACHEABLE 0x1000
// Cacheable in normal memory
#    define SECTION_CACHEABLE 0x100C
// Non shareable device specific memory
#    define SECTION_NON_SHAREABLE 0x2000

#    define SECTION_SHARE_BIT_SET 0x00010000
#    define SECTION_SHARE_BIT_CLEAR 0x00000000

#    define SECTION_NON_GLOBAL_SET 0x02000000
#    define SECTION_NON_GLOBAL_CLEAR 0x00000000

#    define SECTION_EXECUTE_NEVER_SET 0x00000010
#    define SECTION_EXECUTE_NEVER_CLEAR 0x00000000

All of these macros are based upon the UG585 trm: Chapter 3, Figure 3-5

L1 Page Table entry format

The problem

Here is the code I have tried to write to test the MMU

#include "hwi_mmu_picoZed_7010.h"
#include <stdint.h>
#include <xil_mmu.h>




int main(void) {


    Xil_EnableMMU();

    uint32_t addr1 = 0x6400000;
    uint32_t addr0 = 0x6300000;

    hwi_mmu_set_L1_Section(addr1, 0x63, SECTION_ENTRY | SECTION_SHARE_BIT_SET | SECTION_AP_FULL_ACCESS | SECTION_SHAREABLE_DEVICE);
    hwi_mmu_sync();
    hwi_mmu_set_L1_Section(addr0, 0x64, SECTION_ENTRY | SECTION_SHARE_BIT_SET | SECTION_AP_FULL_ACCESS | SECTION_SHAREABLE_DEVICE);
    hwi_mmu_sync();


    *((uint32_t *)(uintptr_t)addr1) = 123;
    return 1;
}

As you can tell the test is quite simple, I set the entry at 0x63M to translate to 0x64M and vice versa, after that I write a value at addr1 (0x6400000) and of course I expect it to be written at addr0 (0x6300000).

However it still writes at 0x6400000 like if the MMU has never been activated.

Looking at CP15 C1 register I can tell that it is however active! I am clueless as to why my code does not work as intended, am I missing something in the initialization of the MMU?


Solution

  • The MMU is working correctly and the memory dump is simply reporting addresses from the point of view of the processor. That is, the addresses in the memory dump are virtual, rather than physical addresses.

    It is possible to verify that the MMU is working correctly by mapping two different virtual addresses to the same physical page and observing that modifications at one address are visible at the other address.