/**
 * @file        scp_direct.c
 * @brief       Low-level SCP primitives.
 *
 * Copyright (c) 2021-2022, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * * Neither the name of NVIDIA CORPORATION nor the names of its
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#if !NVRISCV_FEATURE_SCP
#error "Attempting to build scp_direct.c but the SCP driver is disabled!"
#endif // NVRISCV_FEATURE_SCP


/* Compiler headers */
#include <stdbool.h>                    // for bool, true
#include <stdint.h>                     // for uint32_t, uintptr_t, etc.

/* Early FSP headers */
#include <misc/ct-assert.h>             // for HEADER_CHECK
#include <misc/nvmisc_drf.h>            // for DRF macros

/* Hardware headers */

/* Late FSP headers */
#include <cpu/io.h>                     // for local_read, local_write
#include <cpu/iopmp.h>                  // for iopmp_has_error
#include <error/common-errors.h>        // for error_t, common error codes
#include <fbdma/memutils.h>             // for memutils_riscv_pa_to_target_offset
#include <misc/macros.h>                // for INLINE_RFD

/* Module-specific FSP headers */
#include <scp/scp-errors.h>             // for SCP error codes
#include <scp/scp_common.h>             // for scp_register_index_t
#include <scp/scp_direct.h>             // for scp_transfer_size_t, etc.
#include <scp/scp_private.h>            // for scp_pvt_*

/*
 * Compile-time check for FSP header files
 *   Each FSP header file contains a signature unique to that file, and the
 *   FSP project.  The HEADER_CHECK macro (contained in misc/ct-assert.h) can
 *   check for this signature.  If it does not exist, then the build will
 *   abort.
 *
 *   This is a trap for projects which have their own include files of the
 *   same names, but different contents.  This trap ensures that only the
 *   files from the FSP project, are built into the FSP source code.
 */
HEADER_CHECK(FSP__MISC__NVMISC_DRF_H)
HEADER_CHECK(FSP__CPU__IO_H)
HEADER_CHECK(FSP__CPU__IOPMP_H)
HEADER_CHECK(FSP__ERROR__COMMON_ERRORS_H)
HEADER_CHECK(FSP__FBDMA__MEMUTILS_H)
HEADER_CHECK(FSP__MISC__MACROS_H)
HEADER_CHECK(FSP__SCP__SCP_ERRORS_H)
HEADER_CHECK(FSP__SCP__SCP_COMMON_H)
HEADER_CHECK(FSP__SCP__SCP_DIRECT_H)
HEADER_CHECK(FSP__SCP__SCP_PRIVATE_H)


/**
 * @brief Initiates a single SCPDMA transfer.
 *
 * @pre The SCP hardware must have been properly initialized.
 *
 * @param[inout] offset    The IMEM/DMEM offset for the transfer. Ignored for
 *                         shortcut transfers (32-bit).
 *
 * @param[in]    suppress  Whether to avoid issuing a corresponding push/fetch
 *                         instruction for this transfer (ENABLE / DISABLE).
 *
 * @param[in]    shortcut  Whether to enable the shortcut path for direct
 *                         transfer to FB/SYSMEM. Only valid for writes (ENABLE
 *                         / DISABLE).
 *
 * @param[in]    imem      Whether to target IMEM rather than DMEM. Ignored for
 *                         shortcut transfers (TRUE / FALSE).
 *
 * @param[in]    write     Indicates transfer direction. Here "write" means a
 *                         transfer from memory to the SCP unit (TRUE / FALSE).
 *
 * @param[in]    size      The number of bytes to transfer. Must be a valid
 *                         scp_transfer_size_t value, or zero for non-suppressed
 *                         transfers.
 *
 * @param[in]    gpr       The SCP register to target in the corresponding push
 *                         /fetch instruction. Must be a valid
 *                         scp_register_index_t value, or zero for suppressed
 *                         transfers.
 */
#define SCP_TRANSFER(offset, suppress, shortcut, imem, write, size, gpr) do { \
    INLINE_RFD(CERTC, DEVIATE, INT31_C, "Bug: TODO, DR: TODO - NVRISCV_RISCV_AMAP_IMEM/DMEM_END < UINT32_MAX") \
    uint32_t local_offset = (uint32_t) offset;                             \
    local_write(NV_PRGNLCL_RISCV_SCPDMATRFMOFFS,                            \
        DRF_NUM(_PRGNLCL_RISCV, _SCPDMATRFMOFFS, _OFFS, local_offset));    \
                                                                           \
    local_write(NV_PRGNLCL_RISCV_SCPDMATRFCMD,                              \
        DRF_DEF(_PRGNLCL_RISCV, _SCPDMATRFCMD, _SUPPRESS, _##suppress)  |  \
        DRF_DEF(_PRGNLCL_RISCV, _SCPDMATRFCMD, _SHORTCUT, _##shortcut)  |  \
        DRF_DEF(_PRGNLCL_RISCV, _SCPDMATRFCMD, _IMEM,     _##imem    )  |  \
        DRF_DEF(_PRGNLCL_RISCV, _SCPDMATRFCMD, _WRITE,    _##write   )  |  \
        DRF_NUM(_PRGNLCL_RISCV, _SCPDMATRFCMD, _SIZE,     size       )  |  \
        DRF_NUM(_PRGNLCL_RISCV, _SCPDMATRFCMD, _GPR,      gpr        )  |  \
        DRF_DEF(_PRGNLCL_RISCV, _SCPDMATRFCMD, _CCI_EX,   _SCPDMA    )     \
    );                                                                     \
} while(0)


/**
 * @brief Tracks load/store failures for scp_check_dma() to report later.
 */
static error_t s_transfer_status = E_SUCCESS;


//
// Prototypes for local helper functions.
// See definitions for details.
//
static inline error_t   s_scp_Get_Error(void);
static inline void      s_scp_Clear_Error(void);


/**
 * @copydoc scp_Start_Direct
 */
error_t
scp_Start_Direct(void)
{
    // Verify driver state before proceeding further.
    if (!g_scp_state.b_initialized)
    {
        return E_SCP_NOT_READY;
    }

    // Skip entry if already in direct mode.
    if (g_scp_state.b_direct_mode_active)
    {
        return E_SCP_NOTHING_TO_DO;
    }

    //
    // The high-level SCP interfaces are written such that the SCP GPRs should
    // already be cleared and unlocked, the sequencers should already be empty,
    // any SCPDMA errors should already be handled, and the SCP unit should
    // already be idle before this function is even called. Therefore, we have
    // nothing to do here besides updating the internal driver state.
    //
    g_scp_state.b_direct_mode_active = true;

    // Direct mode was started successfully.
    return E_SUCCESS;
}

/**
 * @copydoc scp_Stop_Direct
 */
error_t
scp_Stop_Direct(void)
{
    // Verify driver state before proceeding further.
    if (!g_scp_state.b_initialized)
    {
        return E_SCP_NOT_READY;
    }

    // Skip exit if not in direct mode in the first place.
    if (!g_scp_state.b_direct_mode_active)
    {
        return E_SCP_NOTHING_TO_DO;
    }

    // Verify access to lockable SCP registers/fields.
    if (scp_pvt_Is_Access_Restricted())
    {
        return E_SCP_INVALID_LOCK_STATE;
    }

    //
    // Wait for idle before issuing the pipe reset. Otherwise, we may end up
    // corrupting the result of some prior operation that's still pending.
    //
    scp_wait_dma();

    //
    // Do a full pipe reset. This ensures that the SCP unit isn't in some weird
    // state (e.g. stalled, in the middle of a sequencer load, etc.).
    //
    scp_pvt_Reset_Pipeline();

    //
    // Clear the sequencers to prevent anything accidentally carrying forward
    // into higher-level operations.
    //
    scp_pvt_Reset_Sequencers();

    //
    // Clear any pending SCPDMA errors the application may have neglected to
    // deal with (or else we might get false failures from scp_pvt_Clear_All()).
    // Ignore result explicitly to avoid coverity errors.
    //
    (void) scp_check_dma(true);

    // Clear and unlock the GPRs.
    error_t clear_status = scp_pvt_Clear_All();
    if (clear_status != E_SUCCESS)
    {
        return clear_status;
    }

    // Update internal state.
    g_scp_state.b_direct_mode_active = false;

    // Direct mode was stopped successfully.
    return E_SUCCESS;
}

/**
 * @copydoc scp_load
 */
void
scp_load
(
    uintptr_t               source_pa,
    scp_register_index_t    reg_index
)
{
    riscv_mem_target_t  mem_target;
    uint64_t            mem_offset;

    //
    // Determine which memory region source_pa points to and obtain the offset
    // of source_pa into said region.
    //
    if (memutils_riscv_pa_to_target_offset(source_pa, &mem_target, &mem_offset)
            != E_SUCCESS)
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
    // Issue a transfer from IMEM.
    else if (mem_target == RISCV_MEM_TARGET_IMEM)
    {
        // We assume that IMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, DISABLE, DISABLE, TRUE, TRUE, 0U, reg_index);
    }
    // Issue a transfer from DMEM.
    else if (mem_target == RISCV_MEM_TARGET_DMEM)
    {
        // We assume that DMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, DISABLE, DISABLE, FALSE, TRUE, 0U, reg_index);
    }
    // Invalid memory region.
    else
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
}

/**
 * @copydoc scp_store
 */
void
scp_store
(
    scp_register_index_t    reg_index,
    uintptr_t               dest_pa
)
{
    riscv_mem_target_t  mem_target;
    uint64_t            mem_offset;

    //
    // Determine which memory region dest_pa points to and obtain the offset
    // of dest_pa into said region.
    //
    if (memutils_riscv_pa_to_target_offset(dest_pa, &mem_target, &mem_offset) !=
        E_SUCCESS)
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
    // Issue a transfer to IMEM.
    else if (mem_target == RISCV_MEM_TARGET_IMEM)
    {
        // We assume that IMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, DISABLE, DISABLE, TRUE, FALSE, 0U, reg_index);
    }
    // Issue a transfer to DMEM.
    else if (mem_target == RISCV_MEM_TARGET_DMEM)
    {
        // We assume that DMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, DISABLE, DISABLE, FALSE, FALSE, 0U, reg_index);
    }
    // Issue a transfer to external memory (shortcut).
    else if ((mem_target == RISCV_MEM_TARGET_FBGPA) ||
             (mem_target == RISCV_MEM_TARGET_SYSGPA))
    {
        SCP_TRANSFER(0U, DISABLE, ENABLE, FALSE, FALSE, 0U, reg_index);
    }
    // Invalid memory region.
    else
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
}

/**
 * @copydoc scp_queue_read
 */
void
scp_queue_read
(
    uintptr_t           source_pa,
    scp_transfer_size_t size
)
{
    riscv_mem_target_t  mem_target;
    uint64_t            mem_offset;

    //
    // Determine which memory region source_pa points to and obtain the offset
    // of source_pa into said region.
    //
    if (memutils_riscv_pa_to_target_offset(source_pa, &mem_target, &mem_offset)
            != E_SUCCESS)
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
    // Queue a transfer from IMEM.
    else if (mem_target == RISCV_MEM_TARGET_IMEM)
    {
        // We assume that IMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, ENABLE, DISABLE, TRUE, TRUE, size, 0U);
    }
    // Queue a transfer from DMEM.
    else if (mem_target == RISCV_MEM_TARGET_DMEM)
    {
        // We assume that DMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, ENABLE, DISABLE, FALSE, TRUE, size, 0U);
    }
    // Invalid memory region.
    else
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
}

/**
 * @copydoc scp_queue_write
 */
void
scp_queue_write
(
    uintptr_t           dest_pa,
    scp_transfer_size_t size
)
{
    riscv_mem_target_t  mem_target;
    uint64_t            mem_offset;

    //
    // Determine which memory region dest_pa points to and obtain the offset
    // of dest_pa into said region.
    //
    if (memutils_riscv_pa_to_target_offset(dest_pa, &mem_target, &mem_offset) !=
        E_SUCCESS)
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
    // Queue a transfer to IMEM.
    else if (mem_target == RISCV_MEM_TARGET_IMEM)
    {
        // We assume that IMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, ENABLE, DISABLE, TRUE, FALSE, size, 0U);
    }
    // Queue a transfer to DMEM.
    else if (mem_target == RISCV_MEM_TARGET_DMEM)
    {
        // We assume that DMEM does not exceed 2^32 bytes in size.
        SCP_TRANSFER(mem_offset, ENABLE, DISABLE, FALSE, FALSE, size, 0U);
    }
    // Queue a transfer to external memory (shortcut).
    else if ((mem_target == RISCV_MEM_TARGET_FBGPA) ||
             (mem_target == RISCV_MEM_TARGET_SYSGPA))
    {
        SCP_TRANSFER(0U, ENABLE, ENABLE, FALSE, FALSE, size, 0U);
    }
    // Invalid memory region.
    else
    {
        // Track for scp_check_dma() to report later.
        s_transfer_status = E_SCP_INVALID_BASE;
    }
}

/**
 * @copydoc scp_check_dma
 */
error_t
scp_check_dma
(
    bool b_clear
)
{
    error_t final_status;

    // Give first precedence to errors cached from prior load/store operations.
    if (s_transfer_status != E_SUCCESS)
    {
        final_status = s_transfer_status;
    }
    else
    {
        // Next, check for errors flagged by the SCPDMA hardware.
        final_status = s_scp_Get_Error();

        // Finally, check for general IO-PMP errors.
        if ((final_status == E_SUCCESS) && iopmp_has_error())
        {
            final_status = E_FAULT;
        }
    }

    //
    // Clear any errors if requested. Note that we don't clear IO-PMP errors as
    // the user may want to investigate them in more detail.
    //
    if (b_clear)
    {
        s_transfer_status = E_SUCCESS;
        s_scp_Clear_Error();
    }

    return final_status;
}

///////////////////////////////////////////////////////////////////////////////

/**
 * @brief Checks for and returns any pending SCPDMA errors.
 *
 * @pre The SCPDMA hardware must be idle.
 *
 * @retval E_SUCCESS                if no errors were reported.
 *
 * @retval E_SCP_INVALID_ADDRESS    if an SCPDMA transfer was initiated with an
 *                                  offset that is not aligned to its size.
 *
 * @retval E_PERM                   if a CCI secret instruction was initiated
 *                                  with a hardware-secret index that the
 *                                  application does not have permission to
 *                                  use.
 */
static inline error_t
s_scp_Get_Error(void)
{
    const uint32_t reg = local_read(NV_PRGNLCL_RISCV_SCPDMAPOLL);

    // Check for bad alignment.
    if (FLD_TEST_DRF(_PRGNLCL_RISCV, _SCPDMAPOLL, _ERROR_CODE,
        _NOT_SIZE_ALIGNED, reg))
    {
        return E_SCP_INVALID_ADDRESS;
    }
    // Check for bad secret index.
    else if (FLD_TEST_DRF(_PRGNLCL_RISCV, _SCPDMAPOLL, _ERROR_CODE,
        _SECRET_NOT_ALLOWED, reg))
    {
        return E_PERM;
    }
    // No errors reported.
    else
    {
        return E_SUCCESS;
    }
}

/**
 * @brief Clears any pending SCPDMA errors.
 *
 * @pre The SCPDMA hardware must be idle.
 */
static inline void
s_scp_Clear_Error(void)
{
    local_write(NV_PRGNLCL_RISCV_SCPDMAPOLL,
        FLD_SET_DRF_NUM(_PRGNLCL_RISCV, _SCPDMAPOLL, _ERROR_CLR, 1U,
            local_read(NV_PRGNLCL_RISCV_SCPDMAPOLL)));
}
