*** Variables ***
${a0}                               0xa
${a1}                               0xb
${a2}                               0xc
${PRIV_ALL}                         7
${PRIV_READWRITE}                   3
${PRIV_WRITE_ONLY}                  2
${PRIV_EXEC_ONLY}                   4
${PRIV_NONE}                        0
${START_PC}                         0x0
${-1u64}                            0xFFFFFFFFFFFFFFFF

*** Keywords ***
Create Platform
    Execute Command                 using sysbus
    Execute Command                 mach create "risc-v"

    Execute Command                 machine LoadPlatformDescriptionFromString "clint: IRQControllers.CoreLevelInterruptor @ sysbus 0x44000000 { frequency: 66000000 }"
    Execute Command                 machine LoadPlatformDescriptionFromString "cpu: CPU.RiscV32 @ sysbus { timeProvider: clint; cpuType: \\"rv32gc\\" }"
    Execute Command                 machine LoadPlatformDescriptionFromString "mem: Memory.MappedMemory @ sysbus 0x0 { size: 0x100000 }"

    Execute Command                 cpu PC ${START_PC}

Create ARM Platform
    [Arguments]                     ${type}=cortex-a53-gicv2
    Execute Command                 using sysbus
    Execute Command                 mach create "${type}"

    Execute Command                 machine LoadPlatformDescriptionFromString "using \\"platforms/cpus/${type}.repl\\"; smmu: MemoryControllers.ARM_SMMUv3 @ sysbus 0xfd800000 { context: cpu }; cpu: @ { sysbus; smmu 29 }"
    Execute Command                 machine LoadPlatformDescriptionFromString "highmem: Memory.MappedMemory @ sysbus 0x100000000 { size: 0x1000 }"
    Execute Command                 include @tests/unit-tests/external-mmu.py

    Execute Command                 cpu PC ${START_PC}

Expect Value Read From Address
    [Arguments]                     ${address}  ${value}
    Execute Command                 cpu PC ${START_PC}
    Execute Command                 cpu SetRegister ${a0} ${address}
    Execute Command                 sysbus WriteDoubleWord ${START_PC} 0x52583  # lw a1, 0(a0)

    Execute Command                 cpu Step

    ${val}=                         Execute Command   cpu GetRegister ${a1}
    Should Be Equal As Integers     ${val}  ${value}

Write Range With Doublewords
    [Arguments]                     ${start_addr}  ${length}  ${value}
    ${end_addr}=                    Evaluate  ${start_addr}+${length}
    ${bytesPerDoubleword}=          Evaluate  4
    FOR   ${addr}  IN RANGE         ${start_addr}  ${end_addr}  ${bytesPerDoubleWord}
        Execute Command             sysbus WriteDoubleWord ${addr} ${value}
    END

Define Window Using CPU API
    [Arguments]                     ${start_addr}  ${end_addr}  ${addend}  ${priv}
    Execute Command                 cpu EnableExternalWindowMmu true
    ${window_index}=                Execute Command  cpu AcquireExternalMmuWindow ${PRIV_ALL}
    Execute Command                 cpu SetMmuWindowStart ${window_index} ${start_addr}
    Execute Command                 cpu SetMmuWindowEnd ${window_index} ${end_addr}
    Execute Command                 cpu SetMmuWindowAddend ${window_index} ${addend}
    Execute Command                 cpu SetMmuWindowPrivileges ${window_index} ${priv}
    Return From Keyword             ${window_index}

Define Typed Window Using CPU API
    [Arguments]                     ${start_addr}  ${end_addr}  ${addend}  ${priv}  ${type}
    Execute Command                 cpu EnableExternalWindowMmu true
    ${window_index}=                Execute Command  cpu AcquireExternalMmuWindow ${type}
    Execute Command                 cpu SetMmuWindowStart ${window_index} ${start_addr}
    Execute Command                 cpu SetMmuWindowEnd ${window_index} ${end_addr}
    Execute Command                 cpu SetMmuWindowAddend ${window_index} ${addend}
    Execute Command                 cpu SetMmuWindowPrivileges ${window_index} ${priv}
    Return From Keyword             ${window_index}

Define Window In Peripheral
    [Arguments]                     ${periph}  ${window_index}  ${start_addr}  ${end_addr}  ${addend}  ${priv}
    ${offset}=                      Evaluate  4 * ${window_index}
    ${start_register}=              Evaluate  0x0 + ${offset}
    ${end_register}=                Evaluate  0x400 + ${offset}
    ${addend_register}=             Evaluate  0x800 + ${offset}
    ${priv_register}=               Evaluate  0xC00 + ${offset}
    Execute Command                 ${periph} WriteDoubleWord ${start_register} ${start_addr}
    Execute Command                 ${periph} WriteDoubleWord ${end_register} ${end_addr}
    Execute Command                 ${periph} WriteDoubleWord ${addend_register} ${addend}
    Execute Command                 ${periph} WriteDoubleWord ${priv_register} ${priv}

*** Test Cases ***
Setting MMU Window Parameters Before Enabling Throws
    Create Platform
    Run Keyword And Expect Error    KeywordException: *There was an error executing command 'cpu SetMmuWindowAddend 0 256'External MMU not enabled*
    ...  Execute Command                 cpu SetMmuWindowAddend 0 0x100

Using Too High MMU Window Index Throws
    Create Platform
    Execute Command                 cpu EnableExternalWindowMmu true
    Run Keyword And Expect Error    CpuAbortException: Failed to find external MMU window with ID 256*
    ...  Execute Command            cpu SetMmuWindowAddend 256 0x100

Read/Write From Address Outside The Defined MMU Windows Throws
    Create Platform
    Execute Command                 cpu EnableExternalWindowMmu true
    Create Log Tester               0
    Execute Command                 sysbus WriteWord 0x2000 0x1234
    Expect Value Read From Address  0x10000  0x0
    Wait For Log Entry              MMU fault - the address 0x0 is not specified in any of the existing ranges

Window Can Handle Only One Type Of Access
    Create Platform
    Execute Command                 cpu EnableExternalWindowMmu true
    Create Log Tester               0
    Define Typed Window Using CPU API    0x0000  0x1000  0x0  ${PRIV_EXEC_ONLY}  ${PRIV_EXEC_ONLY}
    Define Typed Window Using CPU API    0x0000  0x1000  0x1000  ${PRIV_READWRITE}  ${PRIV_READWRITE}
    Execute Command                 sysbus WriteWord 0x1000 0x0124
    Expect Value Read From Address  0x0  0x0124

Is Able To Retrive The Properties
    Create Platform

    ${window}=                      Define Window Using CPU API  0x1000  0x2000  0x100  ${PRIV_ALL}
    ${range_start}=                 Execute Command   cpu GetMmuWindowStart ${window}
    ${range_end}=                   Execute Command   cpu GetMmuWindowEnd ${window}
    ${addend}=                      Execute Command   cpu GetMmuWindowAddend ${window}
    ${priv}=                        Execute Command   cpu GetMmuWindowPrivileges ${window}
    Should Be Equal As Integers     0x1000  ${range_start}
    Should Be Equal As Integers     0x2000  ${range_end}
    Should Be Equal As Integers     0x100   ${addend}
    Should Be Equal As Integers     ${PRIV_ALL}  ${priv}

Can Reset The Windows
    Create Platform

    ${window1}=                     Define Window Using CPU API     0x1000  0x2000  0x100  ${PRIV_ALL}
    ${window2}=                     Define Window Using CPU API     0x2000  0x3000  0x100  ${PRIV_ALL}
    ${window3}=                     Define Window Using CPU API     0x3000  0x4000  0x100  ${PRIV_ALL}
    ${number_of_windows}=           Execute Command    cpu ExternalMmuWindowsCount
    Should Be Equal As Integers     3  ${number_of_windows}

    Execute Command                 cpu ResetMmuWindow ${window2}
    ${number_of_windows}=           Execute Command    cpu ExternalMmuWindowsCount
    Should Be Equal As Integers     2  ${number_of_windows}

    # Surrounding windows should remain untouched
    ${range_start}=                 Execute Command    cpu GetMmuWindowStart ${window1}
    ${range_end}=                   Execute Command    cpu GetMmuWindowEnd ${window1}
    ${addend}=                      Execute Command    cpu GetMmuWindowAddend ${window1}
    ${priv}=                        Execute Command    cpu GetMmuWindowPrivileges ${window1}
    Should Be Equal As Integers     0x1000  ${range_start}
    Should Be Equal As Integers     0x2000  ${range_end}
    Should Be Equal As Integers     0x100  ${addend}
    Should Be Equal As Integers     ${PRIV_ALL}  ${priv}

    ${range_start}=                 Execute Command    cpu GetMmuWindowStart ${window3}
    ${range_end}=                   Execute Command    cpu GetMmuWindowEnd ${window3}
    ${addend}=                      Execute Command    cpu GetMmuWindowAddend ${window3}
    ${priv}=                        Execute Command    cpu GetMmuWindowPrivileges ${window3}
    Should Be Equal As Integers     0x3000  ${range_start}
    Should Be Equal As Integers     0x4000  ${range_end}
    Should Be Equal As Integers     0x100  ${addend}
    Should Be Equal As Integers     ${PRIV_ALL}  ${priv}

Read/Write Uses The Proper Addend
    Create Platform
    Define Window Using CPU API     0x0000  0x1000  0x0  ${PRIV_ALL}
    Define Window Using CPU API     0x10000  0x11000  0x1000  ${PRIV_ALL}
    Execute Command                 sysbus WriteWord 0x10000 0xFFFF
    Execute Command                 sysbus WriteWord 0x11000 0x0124
    Expect Value Read From Address  0x10000  0x0124

Throws On Ranges Unaligned To The Page Size
    Create Platform
    Run Keyword And Expect Error    CpuAbortException: MMU ranges must be aligned to the page size (0x1000), the address 0x100 is not*
    ...  Define Window Using CPU API     0x100  0x1100  0x1000  ${PRIV_ALL}

Permissions Are Respected
    Create Platform
    Create Log Tester               0
    Execute Command                 logLevel -1
    Define Window Using CPU API     0x0000  0x1000  0x0  ${PRIV_ALL}
    Define Window Using CPU API     0x1000  0x2000  0x100  ${PRIV_EXEC_ONLY}
    Write Range With Doublewords    0x1000  0x1FF  0xFFFFFFFF
    Expect Value Read From Address  0x1100  0x0
    Wait For Log Entry              External MMU fault at 0x1100

Fault Callback Works Only When Enabled
    Create Platform
    Create Log Tester               0.0001
    Execute Command                 logLevel 0 cpu
    Expect Value Read From Address  0x1100  0x0
    Should Not Be In Log            External MMU fault at 0x1100

Peripheral Can Be Attached To The Sysbus
    Create Platform
    Execute Command                 machine LoadPlatformDescriptionFromString "mmu1: Miscellaneous.ExternalWindowMMU @ sysbus 0x47000000 {cpu: cpu; numberOfWindows: 4}"
    Define Window In Peripheral     mmu1  0  0x0  0x1000  0x0  ${PRIV_EXEC_ONLY}
    Provides                        SingleMMU

Peripheral Can Be Configured Using The Registers Inteface
    Requires                        SingleMMU
    Define Window In Peripheral     mmu1  1  0x10000  0x11000  0x1000  ${PRIV_ALL}
    Execute Command                 sysbus WriteWord 0x10000 0xFFFF
    Execute Command                 sysbus WriteWord 0x11000 0x0124
    Expect Value Read From Address  0x10000  0x0124

CPU Can Have Two MMUs
    Create Platform
    Execute Command                 machine LoadPlatformDescriptionFromString "mmu1: Miscellaneous.ExternalWindowMMU @ sysbus 0x47000000 {cpu: cpu; numberOfWindows: 2}"
    Execute Command                 machine LoadPlatformDescriptionFromString "mmu2: Miscellaneous.ExternalWindowMMU @ sysbus 0x47001000 {cpu: cpu; numberOfWindows: 2}"
    Define Window In Peripheral     mmu1  0  0x0  0x1000  0x0  ${PRIV_EXEC_ONLY}
    Define Window In Peripheral     mmu2  0  0x0  0x1000  0x0  ${PRIV_EXEC_ONLY}
    Provides                        TwoMmus

Peripheral Throws Fault On Illegal Data Access
    Requires                        SingleMMU
    Create Log Tester               0
    Define Window In Peripheral     mmu1  1  0x0000  0x1000  0x0000  ${PRIV_ALL}
    Define Window In Peripheral     mmu1  2  0x1000  0x2000  0x0000  ${PRIV_NONE}
    Execute Command                 logLevel 0 mmu1

    Expect Value Read From Address  0x1100  0x0
    Wait For Log Entry              mmu1: MMU fault occured

Peripheral Does Not Throw When no_page_fault Is Set
    Requires                        SingleMMU
    Create Log Tester               0
    Define Window In Peripheral     mmu1  1  0x0000  0x1000  0x0000  ${PRIV_ALL}
    Define Window In Peripheral     mmu1  2  0x1000  0x2000  0x0000  ${PRIV_NONE}
    Execute Command                 logLevel 0 mmu1

    # cpu TranslateAddress uses the cpu_handle_mmu_fault with no_page_fault set to 1
    Run Keyword And Expect Error
    ...   *Failed to translate address*
    ...   Execute Command  cpu TranslateAddress 0x1100 Read
    Should Not Be In Log            mmu1: MMU fault occured

Peripheal Throws On Illegal Instruction Fetch
    Create Platform
    Create Log Tester               0
    Execute Command                 machine LoadPlatformDescriptionFromString "mmu1: Miscellaneous.ExternalWindowMMU @ sysbus 0x47000000 {cpu: cpu; numberOfWindows: 4}"
    Define Window In Peripheral     mmu1  1  0x0000  0x1000  0x0000  ${PRIV_WRITE_ONLY}
    Execute Command                 logLevel 0 mmu1

    Expect Value Read From Address  0x2000  0x0
    Wait For Log Entry              mmu1: MMU fault occured

First MMU Throws On Fault In Its Window
    Requires                        TwoMmus
    Create Log Tester               0

    Execute Command                 logLevel 0 mmu1
    Execute Command                 logLevel 0 mmu2

    Define Window In Peripheral     mmu1  1  0x1000  0x2000  0x0000  ${PRIV_NONE}
    Execute Command                 sysbus WriteWord 0x1000 0x0124
    Expect Value Read From Address  0x1000  0x0

    Wait For Log Entry              mmu1: MMU fault occured
    Should Not Be In Log            mmu2: MMU fault occured

Second MMU Throws On Fault In Its Window
    Requires                        TwoMmus
    Create Log Tester               0

    Execute Command                 logLevel 0 mmu1
    Execute Command                 logLevel 0 mmu2

    Define Window In Peripheral     mmu2  1  0x2000  0x3000  0x1000  ${PRIV_WRITE_ONLY}
    Execute Command                 sysbus WriteWord 0x2000 0x0124
    Expect Value Read From Address  0x2000  0x0

    Wait For Log Entry              mmu2: MMU fault occured
    Should Not Be In Log            mmu1: MMU fault occured

Execution Stops On Fault
    Requires                        SingleMMU
    Write Range With Doublewords    0x0  0x1000  0x13 # Nop sled on whole page
    Execute Command                 cpu SetRegister ${a0} 0x1C
    Execute Command                 sysbus WriteDoubleWord 0x1C 0x52583  # lw a1, 0(a0)
    Execute Command                 sysbus WriteDoubleWord 0x20 0xd02503 # lw a2, 0(zero)
    Start Emulation
    Sleep 							0.1

    # Assert we are halted on the faulting insn
    ${pc}=                          Execute Command   cpu PC
    Should Be Equal As integers     ${pc}  0x1C
    # Assert that the second insn was not executed
    ${val}=                         Execute Command   cpu GetRegister ${a2}
    Should Be Equal As Integers     ${val}  0x0

Throws When Window Is Out Of Range
    Requires                        SingleMMU
    Run Keyword And Expect Error    *Address is outside of the possible range.*
    ...                             Define Typed Window Using CPU API  0x0  0x100001000  0x0  ${PRIV_ALL}  ${PRIV_ALL}

Works With The Last Page Of Memory
    Requires                        SingleMMU
    Define Typed Window Using CPU API  0x0  0x100000000  0x0  ${PRIV_EXEC_ONLY}  ${PRIV_EXEC_ONLY}
    Execute Command                 sysbus ReadWord 0xFFFFFFFF

SMMUv3 Can Translate Accesses From CPU
    Create ARM Platform            cortex-r52
    Execute Command                setup_smmu 29
    Execute Command                sysbus WriteDoubleWord 0x100000000 0xfeedface

    ${prog}=                       Catenate  SEPARATOR=\n
    ...                            // Identity mapping tested by the mere fact that this code can execute from 0x0.
    ...                            mov r0, 0x1000
    ...                            ldr r1, [r0]

    Execute Command                cpu AssembleBlock 0 "${prog}"
    Execute Command                cpu Step 2
    Register Should Be Equal       R1  0xfeedface
