/**
 * @file
 *
 * @ingroup RTEMSBSPsARMCycVContrib
 */

/******************************************************************************
*
* alt_qspi.c - API for the Altera SoC FPGA QSPI device.
*
******************************************************************************/

/******************************************************************************
 *
 * Copyright 2013 Altera 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:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * 2. 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.
 * 
 * 3. The name of the author may not be used to endorse or promote products
 * derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "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 AUTHOR 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.
 * 
 ******************************************************************************/

#include <string.h>
#include <stdio.h>
#include <inttypes.h>
#include <bsp/hwlib.h>
#include <bsp/alt_clock_manager.h>
#include "alt_qspi.h"
#include <bsp/alt_qspi_private.h>
#include <bsp/socal/alt_qspi.h>
#include <bsp/socal/alt_rstmgr.h>
#include <bsp/socal/alt_sysmgr.h>
#include <bsp/socal/hps.h>
#include <bsp/socal/socal.h>

/////

// NOTE: To enable debugging output, delete the next line and uncomment the
//   line after.
#define dprintf(...)
// #define dprintf printf

/////

#define MIN(a, b) ((a) > (b) ? (b) : (a))

// qspi_clk operating frequency range.
#define ALT_QSPI_CLK_FREQ_MIN ((alt_freq_t)0)
#define ALT_QSPI_CLK_FREQ_MAX ((alt_freq_t)432000000)

// The set of all valid QSPI controller interrupt status mask values.
#define ALT_QSPI_INT_STATUS_ALL ( \
        ALT_QSPI_INT_STATUS_MODE_FAIL         | \
        ALT_QSPI_INT_STATUS_UFL               | \
        ALT_QSPI_INT_STATUS_IDAC_OP_COMPLETE  | \
        ALT_QSPI_INT_STATUS_IDAC_OP_REJECT    | \
        ALT_QSPI_INT_STATUS_WR_PROT_VIOL      | \
        ALT_QSPI_INT_STATUS_ILL_AHB_ACCESS    | \
        ALT_QSPI_INT_STATUS_IDAC_WTRMK_TRIG   | \
        ALT_QSPI_INT_STATUS_RX_OVF            | \
        ALT_QSPI_INT_STATUS_TX_FIFO_NOT_FULL  | \
        ALT_QSPI_INT_STATUS_TX_FIFO_FULL      | \
        ALT_QSPI_INT_STATUS_RX_FIFO_NOT_EMPTY | \
        ALT_QSPI_INT_STATUS_RX_FIFO_FULL      | \
        ALT_QSPI_INT_STATUS_IDAC_RD_FULL        \
        )

static uint32_t qspi_device_size = 0;

/////

static ALT_STATUS_CODE alt_qspi_device_status(uint32_t * status)
{
    // Read flag status register through STIG
    return alt_qspi_stig_rd_cmd(ALT_QSPI_STIG_OPCODE_RDSR, 0, 1, status, 10000);
}

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
static ALT_STATUS_CODE alt_qspi_N25Q_device_flag(uint32_t * flagsr)
{
    if (qspi_device_size < 0x4000000)
    {
        return ALT_E_SUCCESS;
    }

    // Read flag status register through STIG
    return alt_qspi_stig_rd_cmd(ALT_QSPI_STIG_OPCODE_RDFLGSR, 0, 1, flagsr, 10000);
}

// NOTE: This must be called after QSPI has been enabled. Communications with
//   the device will not happen until QSPI is enabled.
static inline ALT_STATUS_CODE alt_qspi_N25Q_enable(void)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    // Reset the volatile memory on the N25Q

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_stig_cmd(ALT_QSPI_STIG_OPCODE_RESET_EN, 0, 10000);
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_stig_cmd(ALT_QSPI_STIG_OPCODE_RESET_MEM, 0, 10000);
    }

    /////

    if (status == ALT_E_SUCCESS)
    {
        ALT_QSPI_DEV_INST_CONFIG_t cfg =
        {
            .op_code        = ALT_QSPI_STIG_OPCODE_FASTREAD_QUAD_IO,
            .inst_type      = ALT_QSPI_MODE_SINGLE, // RDID does not support QUAD.
            .addr_xfer_type = ALT_QSPI_MODE_QUAD,
            .data_xfer_type = ALT_QSPI_MODE_QUAD,
            .dummy_cycles   = 10
        };

        status = alt_qspi_device_read_config_set(&cfg);
    }

/*
    // CASE 157096: Investigate using QUAD for writes.
    if (status == ALT_E_SUCCESS)
    {
        ALT_QSPI_DEV_INST_CONFIG_t cfg =
        {
            .op_code        = ALT_QSPI_STIG_OPCODE_PP,
            .inst_type      = ALT_QSPI_MODE_SINGLE,
            .addr_xfer_type = ALT_QSPI_MODE_QUAD,
            .data_xfer_type = ALT_QSPI_MODE_QUAD,
            .dummy_cycles   = 0
        };

        status = alt_qspi_device_write_config_set(&cfg);
    }
*/

    return status;
}

static ALT_STATUS_CODE alt_qspi_N25Q_flag_wait_for_program(uint32_t timeout)
{
    // The flag status register is only available on the 512 Mib and 1 Gib
    // (64 MiB and 128 MiB) Micron parts.
    if (qspi_device_size < 0x4000000)
    {
        return ALT_E_SUCCESS;
    }

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    uint32_t time_out = timeout;
    uint32_t stat = 0;
    bool infinite = (timeout == ALT_QSPI_TIMEOUT_INFINITE);

    do
    {
        status = alt_qspi_device_status(&stat);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }
        if (!ALT_QSPI_STIG_SR_BUSY_GET(stat))
        {
            break;
        }
    }
    while (time_out-- || infinite);

    if (time_out == (uint32_t)-1 && !infinite)
    {
        status = ALT_E_TMO;
    }

    if (status == ALT_E_SUCCESS)
    {
        uint32_t flagsr = 0;

        do
        {
            status = alt_qspi_N25Q_device_flag(&flagsr);
            if (status != ALT_E_SUCCESS)
            {
                break;
            }
            if (ALT_QSPI_STIG_FLAGSR_PROGRAMREADY_GET(flagsr))
            {
                break;
            }
        }
        while (timeout-- || infinite);

        if (timeout == (uint32_t)-1 && !infinite)
        {
            status = ALT_E_TMO;
        }

        if (status == ALT_E_SUCCESS)
        {
            if (ALT_QSPI_STIG_FLAGSR_PROGRAMERROR_GET(flagsr))
            {
                status = ALT_E_ERROR;
            }
        }
    }
    return status;
}

static ALT_STATUS_CODE alt_qspi_N25Q_flag_wait_for_erase(uint32_t timeout)
{
    // The flag status register is only available on the 512 Mib and 1 Gib
    // (64 MiB and 128 MiB) Micron parts.
    if (qspi_device_size < 0x4000000)
    {
        return ALT_E_SUCCESS;
    }

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    uint32_t time_out = timeout;
    uint32_t stat = 0;
    bool infinite = (timeout == ALT_QSPI_TIMEOUT_INFINITE);

    do
    {
        status = alt_qspi_device_status(&stat);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }
        if (!ALT_QSPI_STIG_SR_BUSY_GET(stat))
        {
            break;
        }
    }
    while (time_out-- || infinite);

    if (time_out == (uint32_t)-1 && !infinite)
    {
        status = ALT_E_TMO;
    }

    if (status == ALT_E_SUCCESS)
    {

        uint32_t flagsr = 0;

        do
        {
            status = alt_qspi_N25Q_device_flag(&flagsr);
            if (status != ALT_E_SUCCESS)
            {
                break;
            }
            if (ALT_QSPI_STIG_FLAGSR_ERASEREADY_GET(flagsr))
            {
                break;
            }
        }
        while (timeout-- || infinite);

        if (timeout == (uint32_t)-1 && !infinite)
        {
            status = ALT_E_TMO;
        }

        if (status == ALT_E_SUCCESS)
        {
            if (ALT_QSPI_STIG_FLAGSR_ERASEERROR_GET(flagsr))
            {
                status = ALT_E_ERROR;
            }
        }
    }
 
    return status;
}
#endif

//
// A helper function which converts a ns interval into a delay interval for a given MHz.
// The +999 is there to round up the result.
//
static inline int alt_qspi_ns_to_multiplier(int ns, int mhz)
{
    return ((ns * mhz) + 999) / 1000;
}

ALT_STATUS_CODE alt_qspi_init(void)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;
    alt_freq_t qspi_clk_freq = 0;

    // Validate QSPI module input clocks.
    //  - pclk    - l4_mp_clk
    //  - hclk    - l4_mp_clk
    //  - ref_clk - qspi_clk

    // Check and validate the QSPI ref_clk which is connected to the HPS qspi_clk.
    if (status == ALT_E_SUCCESS)
    {
        if (alt_clk_is_enabled(ALT_CLK_QSPI) != ALT_E_TRUE)
        {
            status = ALT_E_BAD_CLK;
        }
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_clk_freq_get(ALT_CLK_QSPI, &qspi_clk_freq);
        if (status == ALT_E_SUCCESS)
        {
            if (qspi_clk_freq > ALT_QSPI_CLK_FREQ_MAX)
            {
                return ALT_E_BAD_CLK;
            }
        }
    }

    int qspi_clk_mhz = qspi_clk_freq / 1000000;

    /////

    // Take QSPI controller out of reset.
    alt_clrbits_word(ALT_RSTMGR_PERMODRST_ADDR, ALT_RSTMGR_PERMODRST_QSPI_SET_MSK);

    /////

    // Configure the device timing

    if (status == ALT_E_SUCCESS)
    {
        ALT_QSPI_TIMING_CONFIG_t timing_cfg =
        {
            .clk_phase  = (ALT_QSPI_CLK_PHASE_t)ALT_QSPI_CFG_SELCLKPHASE_RESET,
            .clk_pol    = (ALT_QSPI_CLK_POLARITY_t)ALT_QSPI_CFG_SELCLKPOL_RESET,
            .cs_da      = alt_qspi_ns_to_multiplier(ALT_QSPI_TSHSL_NS_DEF, qspi_clk_mhz),
            .cs_dads    = alt_qspi_ns_to_multiplier(ALT_QSPI_TSD2D_NS_DEF, qspi_clk_mhz),
            .cs_eot     = alt_qspi_ns_to_multiplier(ALT_QSPI_TCHSH_NS_DEF, qspi_clk_mhz),
            .cs_sot     = alt_qspi_ns_to_multiplier(ALT_QSPI_TSLCH_NS_DEF, qspi_clk_mhz),
            .rd_datacap = 1
        };

        dprintf("DEBUG[QSPI]: cs_da   = %" PRIu32 ".\n", timing_cfg.cs_da);
        dprintf("DEBUG[QSPI]: cs_dads = %" PRIu32 ".\n", timing_cfg.cs_dads);
        dprintf("DEBUG[QSPI]: cs_eot  = %" PRIu32 ".\n", timing_cfg.cs_eot);
        dprintf("DEBUG[QSPI]: cs_sot  = %" PRIu32 ".\n", timing_cfg.cs_sot);

        status = alt_qspi_timing_config_set(&timing_cfg);
    }

    /////

    // Configure the remap address register, no remap

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_ahb_remap_address_set(0);
    }

    // Configure the interrupt mask register, disabled all first

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_int_disable(ALT_QSPI_INT_STATUS_ALL);
    }

    // Configure the baud rate divisor
    // CASE 157095: Investigate using 108 MHz, and tweaking the rd_datacap param.

    if (status == ALT_E_SUCCESS)
    {
        uint32_t device_sclk_mhz = 54;
        uint32_t div_actual = (qspi_clk_mhz + (device_sclk_mhz - 1)) / device_sclk_mhz;
        dprintf("DEBUG[QSPI]: div_actual = %" PRIu32 ".\n", div_actual);

        ALT_QSPI_BAUD_DIV_t div_bits = (ALT_QSPI_BAUD_DIV_t)(((div_actual + 1) / 2) - 1);
        status = alt_qspi_baud_rate_div_set(div_bits);
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_uninit(void)
{
    // Put QSPI controller into reset.
    alt_setbits_word(ALT_RSTMGR_PERMODRST_ADDR, ALT_RSTMGR_PERMODRST_QSPI_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_disable(void)
{
    alt_clrbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_EN_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_enable(void)
{
    alt_setbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_EN_SET_MSK);

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    /////

    // Device specific configuration

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_N25Q_enable();
    }
#endif

    uint32_t rdid = 0;

    // Query device capabilities
    // This requires QSPI to be enabled.

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_rdid(&rdid);
    }

    if (status == ALT_E_SUCCESS)
    {
        // NOTE: The size code seems to be a form of BCD (binary coded decimal).
        //   The first nibble is the 10's digit and the second nibble is the 1's
        //   digit in the number of bytes.

        // Capacity ID samples:
        //  0x15 :   16 Mb =>   2 MiB => 1 << 21 ; BCD=15
        //  0x16 :   32 Mb =>   4 MiB => 1 << 22 ; BCD=16
        //  0x17 :   64 Mb =>   8 MiB => 1 << 23 ; BCD=17
        //  0x18 :  128 Mb =>  16 MiB => 1 << 24 ; BCD=18
        //  0x19 :  256 Mb =>  32 MiB => 1 << 25 ; BCD=19
        //  0x1a
        //  0x1b
        //  0x1c
        //  0x1d
        //  0x1e
        //  0x1f
        //  0x20 :  512 Mb =>  64 MiB => 1 << 26 ; BCD=20
        //  0x21 : 1024 Mb => 128 MiB => 1 << 27 ; BCD=21

        int cap_code = ALT_QSPI_STIG_RDID_CAPACITYID_GET(rdid);

        if ( ((cap_code >> 4) > 0x9) || ((cap_code & 0xf) > 0x9))
        {
            // If a non-valid BCD value is detected at the top or bottom nibble, chances
            // are that the chip has a problem.

            dprintf("DEBUG[QSPI]: Invalid CapacityID encountered: 0x%02x.\n", cap_code);
            status = ALT_E_ERROR;
        }
        else
        {
            int cap_decoded = ((cap_code >> 4) * 10) + (cap_code & 0xf);

            qspi_device_size = 1 << (cap_decoded + 6);

            dprintf("DEBUG[QSPI]: Device size = 0x%" PRIx32 ".\n", qspi_device_size);
        }
    }

    // Configure the device size and address bytes

    if (status == ALT_E_SUCCESS)
    {
        ALT_QSPI_DEV_SIZE_CONFIG_t size_cfg =
        {
            .block_size         = ALT_QSPI_DEVSZ_BYTESPERSUBSECTOR_RESET,  // 0x10  => 2^16 = 64 KiB
            .page_size          = ALT_QSPI_DEVSZ_BYTESPERDEVICEPAGE_RESET, // 0x100 => 256 B
            .addr_size          = ALT_QSPI_DEVSZ_NUMADDRBYTES_RESET,       // 0x2   => 3 bytes or 0x00ffffff mask.
            .lower_wrprot_block = 0,
            .upper_wrprot_block = (qspi_device_size - 1) >> 16,
            .wrprot_enable      = ALT_QSPI_WRPROT_EN_RESET
        };

        status = alt_qspi_device_size_config_set(&size_cfg);
    }

    /////

    // Configure the DMA parameters

    // This will allow DMA to work well without much intervention by users.

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_dma_config_set(4, 32);
    }

    /////

    return status;
}

/////

uint32_t alt_qspi_int_status_get(void)
{
    // Read and return the value of the QSPI controller Interrupt Status
    // Register (irqstat).
    return alt_read_word(ALT_QSPI_IRQSTAT_ADDR);
}

ALT_STATUS_CODE alt_qspi_int_clear(const uint32_t mask)
{
    // Check that the [mask] contains valid interrupt status conditions values.
    if ((ALT_QSPI_INT_STATUS_ALL & mask) == 0)
    {
        return ALT_E_BAD_ARG;
    }

    // Write 1's to clear the desired interrupt status condition(s).
    alt_write_word(ALT_QSPI_IRQSTAT_ADDR, mask);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_int_disable(const uint32_t mask)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Check that the [mask] contains valid interrupt status conditions values.
    if ((ALT_QSPI_INT_STATUS_ALL & mask) == 0)
    {
        return ALT_E_BAD_ARG;
    }

    // Write 0's to disable the desired interrupt status condition(s).
    alt_clrbits_word(ALT_QSPI_IRQMSK_ADDR, mask);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_int_enable(const uint32_t mask)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Check that the [mask] contains valid interrupt status conditions values.
    if ((ALT_QSPI_INT_STATUS_ALL & mask) == 0)
    {
        return ALT_E_BAD_ARG;
    }

    // Write 1's to enable the desired interrupt status condition(s).
    alt_setbits_word(ALT_QSPI_IRQMSK_ADDR, mask);

    return ALT_E_SUCCESS;
}

/////

bool alt_qspi_is_idle(void)
{
    // If the idle field of the QSPI configuration register is 1 then the serial
    // interface and QSPI pipeline is idle.
    return ALT_QSPI_CFG_IDLE_GET(alt_read_word(ALT_QSPI_CFG_ADDR)) == 1;
}

/////

static ALT_STATUS_CODE alt_qspi_indirect_write_start_bank(uint32_t dst, size_t length);

static ALT_STATUS_CODE alt_qspi_indirect_page_bound_write_helper(uint32_t dst, const char * src, size_t length)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_indirect_write_start_bank(dst, length);
    }

    if (status == ALT_E_SUCCESS)
    {
        uint32_t write_count = 0;
        uint32_t write_capacity = ALT_QSPI_SRAM_FIFO_ENTRY_COUNT - alt_qspi_sram_partition_get();

        while (write_count < length)
        {
            uint32_t space = write_capacity - alt_qspi_indirect_write_fill_level();
            space = MIN(space, (length - write_count)/ sizeof(uint32_t));

            const uint32_t * data = (const uint32_t *)(src + write_count);
            for (uint32_t i = 0; i < space; ++i)
            {
                alt_write_word(ALT_QSPIDATA_ADDR, *data++);
            }

            write_count += space * sizeof(uint32_t);
        }
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_indirect_write_finish();
    }

    return status;
}

static ALT_STATUS_CODE alt_qspi_indirect_subsector_aligned_write_helper(const char * data, uint32_t subsec_addr)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    for (int i = 0; i < ALT_QSPI_SUBSECTOR_SIZE / ALT_QSPI_PAGE_SIZE; i++)
    {
        int offset = i * ALT_QSPI_PAGE_SIZE;

        status = alt_qspi_indirect_page_bound_write_helper(subsec_addr + offset, data + offset, ALT_QSPI_PAGE_SIZE);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }
    }

    return status;
}

static ALT_STATUS_CODE alt_qspi_indirect_read_start_bank(uint32_t src, size_t size);

//
// This helper function reads a segment of data, which is limited to 1 bank
// (24 bits of addressing).
//
static ALT_STATUS_CODE alt_qspi_read_bank(char * dst, uint32_t src, size_t size)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_indirect_read_start_bank(src, size);
    }

    if (status == ALT_E_SUCCESS)
    {
        uint32_t read_count = 0;

        while (!alt_qspi_indirect_read_is_complete())
        {
            uint32_t level = alt_qspi_indirect_read_fill_level();
//            level = MIN(level, (size - read_count) / sizeof(uint32_t));

            uint32_t * data = (uint32_t *)(dst + read_count);
            for (uint32_t i = 0; i < level; ++i)
            {
                *data++ = alt_read_word(ALT_QSPIDATA_ADDR);
            }

            read_count += level * sizeof(uint32_t);
        }
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_indirect_read_finish();
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_read(void * dst, uint32_t src, size_t size)
{
    if (src >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (src + size - 1 >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (size == 0)
    {
        return ALT_E_SUCCESS;
    }

    if ((uintptr_t)dst & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (src & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (size & 0x3)
    {
        return ALT_E_ERROR;
    }

    /////

    // Verify that there is not already a read in progress.
    if (ALT_QSPI_INDRD_RD_STAT_GET(alt_read_word(ALT_QSPI_INDRD_ADDR)))
    {
        return ALT_E_ERROR;
    }

    /////

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    //
    // bank_count : The number of bank(s) affected, including partial banks.
    // bank_addr  : The aligned address of the first affected bank, including partial bank(s).
    // bank_ofst  : The offset of the bank to read. Only used when reading the first bank.
    //
    uint32_t bank_count = ((src + size - 1) >> 24) - (src >> 24) + 1;
    uint32_t bank_addr  = src & ALT_QSPI_BANK_ADDR_MSK;
    uint32_t bank_ofst  = src & (ALT_QSPI_BANK_SIZE - 1);

    char * data = (char *)dst;

    uint32_t copy_length = MIN(size, ALT_QSPI_BANK_SIZE - bank_ofst);

    dprintf("DEBUG[QSPI]: read(): bulk: mem_addr = %p; flash_addr = 0x%" PRIx32 ".\n", data, src);
    dprintf("DEBUG[QSPI]: read(): bulk: bank_count = 0x%" PRIx32 ", bank_ofst = 0x%" PRIx32 ".\n", bank_count, bank_ofst);

    for (uint32_t i = 0; i < bank_count; ++i)
    {
        dprintf("DEBUG[QSPI]: read(): bank 0x%" PRIx32 "; copy_length = 0x%" PRIx32 ".\n", bank_addr >> 24, copy_length);

        status = alt_qspi_device_bank_select(bank_addr >> 24);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        status = alt_qspi_read_bank(dst, bank_ofst, copy_length);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        bank_addr += ALT_QSPI_BANK_SIZE;
        data += copy_length;
        size -= copy_length;

        copy_length = MIN(size, ALT_QSPI_BANK_SIZE);
    }

    return status;
}

static ALT_STATUS_CODE alt_qspi_write_bank(uint32_t dst, const char * src, size_t size)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    /////

    uint32_t page_ofst  = dst & (ALT_QSPI_PAGE_SIZE - 1);
    uint32_t write_size = MIN(size, ALT_QSPI_PAGE_SIZE - page_ofst);

    while (size)
    {
        dprintf("DEBUG[QSPI]: write(): flash dst = 0x%" PRIx32 ", mem src = %p, write size = 0x%" PRIx32 ", size left = 0x%x.\n", dst, src, write_size, size);

        status = alt_qspi_indirect_page_bound_write_helper(dst, src, write_size);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        dst  += write_size;
        src  += write_size;
        size -= write_size;

        write_size = MIN(size, ALT_QSPI_PAGE_SIZE);
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_write(uint32_t dst, const void * src, size_t size)
{
    if (dst >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (dst + size - 1 >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (size == 0)
    {
        return ALT_E_SUCCESS;
    }

    if ((uintptr_t)src & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (dst & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (size & 0x3)
    {
        return ALT_E_ERROR;
    }

    /////

    // Verify that there is not already a write in progress.
    if (ALT_QSPI_INDWR_RDSTAT_GET(alt_read_word(ALT_QSPI_INDWR_ADDR)))
    {
        return ALT_E_ERROR;
    }

    /////

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    uint32_t bank_count = ((dst + size - 1) >> 24) - (dst >> 24) + 1;
    uint32_t bank_addr  = dst & ALT_QSPI_BANK_ADDR_MSK;
    uint32_t bank_ofst  = dst & (ALT_QSPI_BANK_SIZE - 1);

    const char * data  = src;

    uint32_t copy_length = MIN(size, ALT_QSPI_BANK_SIZE - bank_ofst);

    dprintf("DEBUG[QSPI]: write(): bulk: flash_addr = 0x%" PRIx32 "; mem_addr = %p.\n", dst, data);
    dprintf("DEBUG[QSPI]: write(): bulk: bank_count = 0x%" PRIx32 ", bank_ofst = 0x%" PRIx32 ".\n", bank_count, bank_ofst);

    for (uint32_t i = 0; i < bank_count; ++i)
    {
        dprintf("DEBUG[QSPI]: write(): bank 0x%" PRIx32 "; copy_length = 0x%" PRIx32 ".\n", bank_addr >> 24, copy_length);

        status = alt_qspi_device_bank_select(bank_addr >> 24);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        status = alt_qspi_write_bank(bank_ofst, data, copy_length);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        bank_addr += ALT_QSPI_BANK_SIZE;
        data += copy_length;
        size -= copy_length;

        copy_length = MIN(size, ALT_QSPI_BANK_SIZE);
    }

    return status;
}

static ALT_STATUS_CODE alt_qspi_erase_subsector_bank(uint32_t addr);

static ALT_STATUS_CODE alt_qspi_replace_bank(uint32_t dst, const char * src, size_t size)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    //
    // subsec_count        : The total number of affected subsector(s),
    //                       including partial subsector(s).
    // subsec_addr         : The aligned address of the next affected subsector,
    //                       including partial subsector(s).
    // subsec_partial_head : The number of subsector unaligned data to be
    //                       written out at the start of the flash write
    //                       request. This data ends at the end of the subsector
    //                       or earlier depending on the number of data to be
    //                       written.
    // subsec_partial_tail : The number of subsector unaligned data to be
    //                       written out at the end of the flash write request.
    //                       This data starts at the start of the subsector. If
    //                       only a single subsector is written (partial or
    //                       full), this value will be zero.
    //

    uint32_t subsec_count = ((dst + size - 1) >> 12) - (dst >> 12) + 1;
    uint32_t subsec_addr  = dst & ALT_QSPI_SUBSECTOR_ADDR_MSK;

    uint32_t subsec_partial_head = MIN(ALT_QSPI_SUBSECTOR_SIZE - (dst & (ALT_QSPI_SUBSECTOR_SIZE - 1)), size) & (ALT_QSPI_SUBSECTOR_SIZE - 1);
    uint32_t subsec_partial_tail = (size - subsec_partial_head) & (ALT_QSPI_SUBSECTOR_SIZE - 1);

    dprintf("DEBUG[QSPI]: replace(): report: dst = 0x%" PRIx32 "; size = 0x%x.\n",
            dst, size);
    dprintf("DEBUG[QSPI]: replace(): report: subsec_count = 0x%" PRIx32 "; subsec_addr = 0x%" PRIx32 ".\n",
            subsec_count, subsec_addr);
    dprintf("DEBUG[QSPI]: replace(): report: partial_head = 0x%" PRIx32 "; partial_tail = 0x%" PRIx32 ".\n",
            subsec_partial_head, subsec_partial_tail);

    // Write the first subsector, partial case.

    if (subsec_partial_head)
    {
        // The write request is not aligned to a subsector so we must do the
        // Read-Modify-Write cycle to preserve the existing data at the head of
        // the subsector not affected by the write.

        char subsec_buf[ALT_QSPI_SUBSECTOR_SIZE];

        uint32_t subsec_ofst = dst & ~ALT_QSPI_SUBSECTOR_ADDR_MSK;

        // - Read the subsector into buffer
        // - Erase that subsector
        // - Copy in the user data into buffer
        // - Write out buffer to subsector

        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_read_bank(subsec_buf, subsec_addr, subsec_ofst);
        }
        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_erase_subsector_bank(subsec_addr);
        }
        if (status == ALT_E_SUCCESS)
        {
            memcpy(subsec_buf + subsec_ofst, src, subsec_partial_head);
            status = alt_qspi_indirect_subsector_aligned_write_helper(subsec_buf, subsec_addr);
        }

        // Do some bookkeeping on the user buffer information
        src  += subsec_partial_head;
        size -= subsec_partial_head;

        // Do some bookkeeping on the subsector tracking
        subsec_count--;
        subsec_addr += ALT_QSPI_SUBSECTOR_SIZE;

        dprintf("DEBUG[QSPI]: replace(): partial head: subsec_ofst = 0x%" PRIx32 "; size left = 0x%x; status = %" PRIi32 ".\n",
                subsec_ofst, size, status);
    }

    // If there is a partial tail, then take 1 off the subsec_count. This way
    // the following loop will write out all the complete subsectors. The tail
    // will be written out afterwards.
    
    if (subsec_partial_tail)
    {
        subsec_count--;
    }

    // Write the aligned subsectors following any partial subsectors.

    for (uint32_t i = 0; i < subsec_count; ++i)
    {
        // - Erase subsector
        // - Write out buffer to subsector

        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_erase_subsector_bank(subsec_addr);
        }
        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_indirect_subsector_aligned_write_helper(src, subsec_addr);
        }

        src  += ALT_QSPI_SUBSECTOR_SIZE;
        size -= ALT_QSPI_SUBSECTOR_SIZE;

        // Don't modify subsec_count as it's being used by the loop.
        subsec_addr += ALT_QSPI_SUBSECTOR_SIZE;

        dprintf("DEBUG[QSPI]: replace(): subsec aligned: size left = 0x%x, status = %" PRIi32 ".\n",
                size, status);
    }

    // Write the last subsector, partial case.

    if (subsec_partial_tail)
    {
        // The write request is not aligned to a subsector so we must do the
        // Read-Modify-Write cycle to preserve the existing data at the end of
        // the subsector not affected by the write.

        char subsec_buf[ALT_QSPI_SUBSECTOR_SIZE];

        // - Read the subsector into buffer
        // - Erase that subsector
        // - Copy in the user data into buffer
        // - Write out buffer to subsector

        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_read_bank(subsec_buf  + subsec_partial_tail,
                                        subsec_addr + subsec_partial_tail,
                                        ALT_QSPI_SUBSECTOR_SIZE - subsec_partial_tail);
        }
        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_erase_subsector_bank(subsec_addr);
        }
        if (status == ALT_E_SUCCESS)
        {
            memcpy(subsec_buf, src, subsec_partial_tail);
            status = alt_qspi_indirect_subsector_aligned_write_helper(subsec_buf, subsec_addr);
        }

        src  += subsec_partial_tail;
        size -= subsec_partial_tail;

        dprintf("DEBUG[QSPI]: replace(): partial tail: size left = 0x%x, status = %" PRIi32 ".\n",
                size, status);
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_replace(uint32_t dst, const void * src, size_t size)
{
    if (dst >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (dst + size - 1 >= qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (size == 0)
    {
        return ALT_E_SUCCESS;
    }

    if ((uintptr_t)src & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (dst & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (size & 0x3)
    {
        return ALT_E_ERROR;
    }

    /////

    // Verify that there is not already a read in progress.
    if (ALT_QSPI_INDRD_RD_STAT_GET(alt_read_word(ALT_QSPI_INDRD_ADDR)))
    {
        return ALT_E_ERROR;
    }

    // Verify that there is not already a write in progress.
    if (ALT_QSPI_INDWR_RDSTAT_GET(alt_read_word(ALT_QSPI_INDWR_ADDR)))
    {
        return ALT_E_ERROR;
    }

    /////

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    uint32_t bank_count = ((dst + size - 1) >> 24) - (dst >> 24) + 1;
    uint32_t bank_addr  = dst & ALT_QSPI_BANK_ADDR_MSK;
    uint32_t bank_ofst  = dst & (ALT_QSPI_BANK_SIZE - 1);

    const char * data = (const char *)src;

    uint32_t copy_length = MIN(size, ALT_QSPI_BANK_SIZE - bank_ofst);

    dprintf("DEBUG[QSPI]: replace(): bulk: flash_addr = 0x%" PRIx32 "; mem_addr = %p.\n", dst, data);
    dprintf("DEBUG[QSPI]: replace(): bulk: bank_count = 0x%" PRIx32 ", bank_ofst = 0x%" PRIx32 ".\n", bank_count, bank_ofst);

    for (uint32_t i = 0; i < bank_count; ++i)
    {
        dprintf("DEBUG[QSPI]: replace(): bank 0x%" PRIx32 "; copy_length = 0x%" PRIx32 ".\n", bank_addr >> 24, copy_length);

        status = alt_qspi_device_bank_select(bank_addr >> 24);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        status = alt_qspi_replace_bank(bank_ofst, data, copy_length);
        if (status != ALT_E_SUCCESS)
        {
            break;
        }

        bank_addr += ALT_QSPI_BANK_SIZE;
        data += copy_length;
        size -= copy_length;

        copy_length = MIN(size, ALT_QSPI_BANK_SIZE);
    }

    return status;
}

/////

ALT_QSPI_BAUD_DIV_t alt_qspi_baud_rate_div_get(void)
{
    uint32_t baud_rate_div = ALT_QSPI_CFG_BAUDDIV_GET(alt_read_word(ALT_QSPI_CFG_ADDR));
    return (ALT_QSPI_BAUD_DIV_t) baud_rate_div;
}

ALT_STATUS_CODE alt_qspi_baud_rate_div_set(const ALT_QSPI_BAUD_DIV_t baud_rate_div)
{
    if (0xf < (uint32_t)baud_rate_div)
    {
        // Invalid baud rate divisor value.
        return ALT_E_BAD_ARG;
    }

    // Set the Master Mode Baud Rate Divisor Field of the QSPI Configuration Register.
    alt_replbits_word(ALT_QSPI_CFG_ADDR,
                      ALT_QSPI_CFG_BAUDDIV_SET_MSK,
                      ALT_QSPI_CFG_BAUDDIV_SET(baud_rate_div));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_chip_select_config_get(uint32_t* cs,
                                                ALT_QSPI_CS_MODE_t* cs_mode)
{
    uint32_t cfg = alt_read_word(ALT_QSPI_CFG_ADDR);

    *cs      = ALT_QSPI_CFG_PERCSLINES_GET(cfg);
    *cs_mode = (ALT_QSPI_CS_MODE_t) ALT_QSPI_CFG_PERSELDEC_GET(cfg);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_chip_select_config_set(const uint32_t cs,
                                                const ALT_QSPI_CS_MODE_t cs_mode)
{
    // chip select cs:
    // four bit value, bit 0 = cs0, bit 1 = cs1, bit 2 = cs2, bit 3 = cs3
    // since cs is low true, the value of each bit should be zero if enable the cs.
    // 
    // also allows multiple cs line enabled together.
 
    if (cs > ((1 << ALT_QSPI_CFG_PERCSLINES_WIDTH) - 1))
    {
        // [cs] not within possible 4 bit chip select line value range.
        return ALT_E_ARG_RANGE;
    }

    if ((cs_mode != ALT_QSPI_CS_MODE_SINGLE_SELECT) && (cs_mode != ALT_QSPI_CS_MODE_DECODE))
    {
        return ALT_E_INV_OPTION;
    }

    // Update the Peripheral Chip Select Lines and Peripheral Select Decode
    // Fields of the QSPI Configuration Register value with the chip select
    // options.
    uint32_t cfg = alt_read_word(ALT_QSPI_CFG_ADDR);
    cfg &= ALT_QSPI_CFG_PERCSLINES_CLR_MSK & ALT_QSPI_CFG_PERSELDEC_CLR_MSK;
    cfg |= ALT_QSPI_CFG_PERCSLINES_SET(cs) | ALT_QSPI_CFG_PERSELDEC_SET(cs_mode);
    alt_write_word(ALT_QSPI_CFG_ADDR, cfg);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_mode_bit_disable(void)
{
    // Clear the Mode Bit Enable Field of the Device Read Instruction Register
    // to disable mode bits from being sent after the address bytes.
    alt_clrbits_word(ALT_QSPI_DEVRD_ADDR, ALT_QSPI_DEVRD_ENMODBITS_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_mode_bit_enable(void)
{
    // Set the Mode Bit Enable Field of the Device Read Instruction Register
    // to enable mode bits to be sent after the address bytes.
    alt_setbits_word(ALT_QSPI_DEVRD_ADDR, ALT_QSPI_DEVRD_ENMODBITS_SET_MSK);

    return ALT_E_SUCCESS;
}

uint32_t alt_qspi_mode_bit_config_get(void)
{
    // Return the 8 bit value from the Mode Field of the Mode Bit Configuration
    // Register.
    return ALT_QSPI_MODBIT_MOD_GET(alt_read_word(ALT_QSPI_MODBIT_ADDR));
}

ALT_STATUS_CODE alt_qspi_mode_bit_config_set(const uint32_t mode_bits)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    if (mode_bits > ((1 << ALT_QSPI_MODBIT_MOD_WIDTH) - 1))
    {
        // 'mode_bits' not within possible 8 bit mode value range.
        return ALT_E_ARG_RANGE;
    }

    // Set the 8 bit value in the Mode Field of the Mode Bit Configuration
    // Register.
    alt_replbits_word(ALT_QSPI_MODBIT_ADDR,
                      ALT_QSPI_MODBIT_MOD_SET_MSK,
                      ALT_QSPI_MODBIT_MOD_SET(mode_bits));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_size_config_get(ALT_QSPI_DEV_SIZE_CONFIG_t * cfg)
{
    // Although not required, it is recommended that the write protect feature
    // be enabled prior to enabling the QSPI controller. This will block any AHB
    // writes from taking effect. This also means the write protection registers
    // (Lower Write Protection, Upper Write Protection, and Write Protection)
    // should be setup and the number of bytes per device block in the device
    // size configuration register should be setup prior to enabling the QSPI
    // controller.

    // Read Device Size Register and get the Number of Bytes per Block, Number
    // of Bytes per Device, and Number of Address Bytes Fields.

    uint32_t devsz = alt_read_word(ALT_QSPI_DEVSZ_ADDR);

    cfg->block_size = ALT_QSPI_DEVSZ_BYTESPERSUBSECTOR_GET(devsz);
    cfg->page_size  = ALT_QSPI_DEVSZ_BYTESPERDEVICEPAGE_GET(devsz);
    cfg->addr_size  = ALT_QSPI_DEVSZ_NUMADDRBYTES_GET(devsz);

    // Read Lower Write Protection, Upper Write Protection, and Write Protection
    // Registers.

    cfg->lower_wrprot_block = ALT_QSPI_LOWWRPROT_SUBSECTOR_GET(alt_read_word(ALT_QSPI_LOWWRPROT_ADDR));
    cfg->upper_wrprot_block = ALT_QSPI_UPPWRPROT_SUBSECTOR_GET(alt_read_word(ALT_QSPI_UPPWRPROT_ADDR));
    cfg->wrprot_enable      = ALT_QSPI_WRPROT_EN_GET(alt_read_word(ALT_QSPI_WRPROT_ADDR));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_size_config_set(const ALT_QSPI_DEV_SIZE_CONFIG_t * cfg)
{
    if (cfg->block_size > ((1 << ALT_QSPI_DEVSZ_BYTESPERSUBSECTOR_WIDTH) - 1))
    {
        return ALT_E_ARG_RANGE;
    }

    if (cfg->page_size > ((1 << ALT_QSPI_DEVSZ_BYTESPERDEVICEPAGE_WIDTH) - 1))
    {
        return ALT_E_ARG_RANGE;
    }

    if (cfg->addr_size > ((1 << ALT_QSPI_DEVSZ_NUMADDRBYTES_WIDTH) - 1))
    {
        return ALT_E_ARG_RANGE;
    }

    if (cfg->lower_wrprot_block > cfg->upper_wrprot_block)
    {
        // Null write protection regions are not allowed.
        return ALT_E_ARG_RANGE;
    }

    /////

    uint32_t value = ALT_QSPI_DEVSZ_BYTESPERSUBSECTOR_SET(cfg->block_size) |
                     ALT_QSPI_DEVSZ_BYTESPERDEVICEPAGE_SET(cfg->page_size) |
                     ALT_QSPI_DEVSZ_NUMADDRBYTES_SET(cfg->addr_size);

    alt_write_word(ALT_QSPI_DEVSZ_ADDR, value);

    if (cfg->wrprot_enable)
    {
        alt_write_word(ALT_QSPI_LOWWRPROT_ADDR, cfg->lower_wrprot_block);
        alt_write_word(ALT_QSPI_UPPWRPROT_ADDR, cfg->upper_wrprot_block);
    }

    // Read Upper Write Protection Register - uppwrprot.
    // Set the Write Protection Enable Bit Field of the Write Protection
    // Register accordingly.
    if (cfg->wrprot_enable)
    {
        alt_setbits_word(ALT_QSPI_WRPROT_ADDR, ALT_QSPI_WRPROT_EN_SET(1));
    }
    else
    {
        alt_clrbits_word(ALT_QSPI_WRPROT_ADDR, ALT_QSPI_WRPROT_EN_SET(1));
    }
    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_read_config_get(ALT_QSPI_DEV_INST_CONFIG_t * cfg)
{
    // Read the Device Read Instruction Register - devrd.
    uint32_t devrd = alt_read_word(ALT_QSPI_DEVRD_ADDR);

    cfg->op_code        = ALT_QSPI_DEVRD_RDOPCODE_GET(devrd);
    cfg->inst_type      = (ALT_QSPI_MODE_t) ALT_QSPI_DEVRD_INSTWIDTH_GET(devrd);
    cfg->addr_xfer_type = (ALT_QSPI_MODE_t) ALT_QSPI_DEVRD_ADDRWIDTH_GET(devrd);
    cfg->data_xfer_type = (ALT_QSPI_MODE_t) ALT_QSPI_DEVRD_DATAWIDTH_GET(devrd);
    cfg->dummy_cycles   = ALT_QSPI_DEVRD_DUMMYRDCLKS_GET(devrd);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_read_config_set(const ALT_QSPI_DEV_INST_CONFIG_t * cfg)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Validate input

    if (cfg->op_code > ((1 << ALT_QSPI_DEVRD_RDOPCODE_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    switch (cfg->inst_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    switch (cfg->addr_xfer_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    switch (cfg->data_xfer_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    if (cfg->dummy_cycles > ((1 << ALT_QSPI_DEVRD_DUMMYRDCLKS_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    /////

    // Read the Device Read Instruction Register - devrd.
    uint32_t devrd = alt_read_word(ALT_QSPI_DEVRD_ADDR);

    devrd &= ALT_QSPI_DEVRD_RDOPCODE_CLR_MSK &
             ALT_QSPI_DEVRD_INSTWIDTH_CLR_MSK &
             ALT_QSPI_DEVRD_ADDRWIDTH_CLR_MSK &
             ALT_QSPI_DEVRD_DATAWIDTH_CLR_MSK &
             ALT_QSPI_DEVRD_DUMMYRDCLKS_CLR_MSK;

    devrd |= ALT_QSPI_DEVRD_RDOPCODE_SET(cfg->op_code) |
             ALT_QSPI_DEVRD_INSTWIDTH_SET(cfg->inst_type) |
             ALT_QSPI_DEVRD_ADDRWIDTH_SET(cfg->addr_xfer_type) |
             ALT_QSPI_DEVRD_DATAWIDTH_SET(cfg->data_xfer_type) |
             ALT_QSPI_DEVRD_DUMMYRDCLKS_SET(cfg->dummy_cycles);

    alt_write_word(ALT_QSPI_DEVRD_ADDR, devrd);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_write_config_get(ALT_QSPI_DEV_INST_CONFIG_t * cfg)
{
    // Device Write Instruction Register - devwr.
    uint32_t devwr = alt_read_word(ALT_QSPI_DEVWR_ADDR);

    cfg->op_code        = ALT_QSPI_DEVWR_WROPCODE_GET(devwr);
    // The Instruction Type field in the Device READ Instruction Register only appears
    // once and applies to both READ and WRITE opertions. it is not included in the
    // Device WRITE Instruction Register.
    cfg->inst_type      = (ALT_QSPI_MODE_t) ALT_QSPI_DEVRD_INSTWIDTH_GET(alt_read_word(ALT_QSPI_DEVRD_ADDR));
    cfg->addr_xfer_type = (ALT_QSPI_MODE_t) ALT_QSPI_DEVWR_ADDRWIDTH_GET(devwr);
    cfg->data_xfer_type = (ALT_QSPI_MODE_t) ALT_QSPI_DEVWR_DATAWIDTH_GET(devwr);
    cfg->dummy_cycles   = ALT_QSPI_DEVWR_DUMMYWRCLKS_GET(devwr);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_device_write_config_set(const ALT_QSPI_DEV_INST_CONFIG_t * cfg)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Validate input

    if (cfg->op_code > ((1 << ALT_QSPI_DEVWR_WROPCODE_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    switch (cfg->inst_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    switch (cfg->addr_xfer_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    switch (cfg->data_xfer_type)
    {
    case ALT_QSPI_MODE_SINGLE:
    case ALT_QSPI_MODE_DUAL:
    case ALT_QSPI_MODE_QUAD:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    if (cfg->dummy_cycles > ((1 << ALT_QSPI_DEVWR_DUMMYWRCLKS_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    /////

    // Read the Device Write Instruction Register - devwr.
    uint32_t devwr = alt_read_word(ALT_QSPI_DEVWR_ADDR);

    devwr &= ALT_QSPI_DEVWR_WROPCODE_CLR_MSK &
             ALT_QSPI_DEVWR_ADDRWIDTH_CLR_MSK &
             ALT_QSPI_DEVWR_DATAWIDTH_CLR_MSK &
             ALT_QSPI_DEVWR_DUMMYWRCLKS_CLR_MSK;

    devwr |= ALT_QSPI_DEVWR_WROPCODE_SET(cfg->op_code) |
             ALT_QSPI_DEVWR_ADDRWIDTH_SET(cfg->addr_xfer_type) |
             ALT_QSPI_DEVWR_DATAWIDTH_SET(cfg->data_xfer_type) |
             ALT_QSPI_DEVWR_DUMMYWRCLKS_SET(cfg->dummy_cycles);

    alt_write_word(ALT_QSPI_DEVWR_ADDR, devwr);

    // The Instruction Type field in the Device READ Instruction Register only appears
    // once and applies to both READ and WRITE operations - it is not included in the
    // Device WRITE Instruction Register. Therefore, modify the Instruction Type
    // Field in the Device Read Register.
    alt_replbits_word(ALT_QSPI_DEVRD_ADDR,
                      ALT_QSPI_DEVRD_INSTWIDTH_SET_MSK,
                      ALT_QSPI_DEVRD_INSTWIDTH_SET((uint32_t) cfg->inst_type));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_timing_config_get(ALT_QSPI_TIMING_CONFIG_t * cfg)
{
    // QSPI Configuration Register - cfg
    uint32_t cfgreg = alt_read_word(ALT_QSPI_CFG_ADDR);
    cfg->clk_phase  = (ALT_QSPI_CLK_PHASE_t) ALT_QSPI_CFG_SELCLKPHASE_GET(cfgreg);
    cfg->clk_pol    = (ALT_QSPI_CLK_POLARITY_t) ALT_QSPI_CFG_SELCLKPOL_GET(cfgreg);

    // QSPI Device Delay Register
    uint32_t delayreg = alt_read_word(ALT_QSPI_DELAY_ADDR);
    cfg->cs_sot  = ALT_QSPI_DELAY_INIT_GET(delayreg);
    cfg->cs_eot  = ALT_QSPI_DELAY_AFTER_GET(delayreg);
    cfg->cs_dads = ALT_QSPI_DELAY_BTWN_GET(delayreg);
    cfg->cs_da   = ALT_QSPI_DELAY_NSS_GET(delayreg);

    // Read Data Capture Register
    cfg->rd_datacap = ALT_QSPI_RDDATACAP_DELAY_GET(alt_read_word(ALT_QSPI_RDDATACAP_ADDR));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_timing_config_set(const ALT_QSPI_TIMING_CONFIG_t * cfg)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Validate parameter(s)

    switch (cfg->clk_phase)
    {
    case ALT_QSPI_CLK_PHASE_ACTIVE:
    case ALT_QSPI_CLK_PHASE_INACTIVE:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    switch (cfg->clk_pol)
    {
    case ALT_QSPI_CLK_POLARITY_LOW:
    case ALT_QSPI_CLK_POLARITY_HIGH:
        break;
    default:
        return ALT_E_BAD_ARG;
    }

    if (cfg->cs_da > ((1 << ALT_QSPI_DELAY_NSS_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }
    if (cfg->cs_dads > ((1 << ALT_QSPI_DELAY_BTWN_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }
    if (cfg->cs_eot > ((1 << ALT_QSPI_DELAY_AFTER_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }
    if (cfg->cs_sot > ((1 << ALT_QSPI_DELAY_INIT_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    if (cfg->rd_datacap > ((1 << ALT_QSPI_RDDATACAP_DELAY_WIDTH) - 1))
    {
        return ALT_E_BAD_ARG;
    }

    /////

    // QSPI Configuration Register - cfg
    uint32_t cfgreg = alt_read_word(ALT_QSPI_CFG_ADDR);
    cfgreg &= ALT_QSPI_CFG_SELCLKPHASE_CLR_MSK &
              ALT_QSPI_CFG_SELCLKPOL_CLR_MSK;
    cfgreg |= ALT_QSPI_CFG_SELCLKPHASE_SET(cfg->clk_phase) |
              ALT_QSPI_CFG_SELCLKPOL_SET(cfg->clk_pol);
    alt_write_word(ALT_QSPI_CFG_ADDR, cfgreg);

    // QSPI Device Delay Register
    uint32_t delayreg = ALT_QSPI_DELAY_INIT_SET(cfg->cs_sot)  |
                        ALT_QSPI_DELAY_AFTER_SET(cfg->cs_eot) |
                        ALT_QSPI_DELAY_BTWN_SET(cfg->cs_dads) |
                        ALT_QSPI_DELAY_NSS_SET(cfg->cs_da);
    alt_write_word(ALT_QSPI_DELAY_ADDR, delayreg);

    // Read Data Capture Register

    alt_write_word(ALT_QSPI_RDDATACAP_ADDR,
                   ALT_QSPI_RDDATACAP_BYP_SET(1) | 
                   ALT_QSPI_RDDATACAP_DELAY_SET(cfg->rd_datacap));

    return ALT_E_SUCCESS;
}

/////

ALT_STATUS_CODE alt_qspi_direct_disable(void)
{
    // Clear (set to 0) the Enable Direct Access Controller Field of the QSPI
    // Configuration Register to disable the Direct Access Controller.
    alt_clrbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENDIRACC_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_direct_enable(void)
{
    // Set (set to 1) the Enable Direct Access Controller Field of the QSPI
    // Configuration Register to enable the Direct Access Controller.
    alt_setbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENDIRACC_SET_MSK);

    return ALT_E_SUCCESS;
}

uint32_t alt_qspi_ahb_remap_address_get(void)
{
    // Read and return the value of the Remap Address Register.
    return ALT_QSPI_REMAPADDR_VALUE_GET(alt_read_word(ALT_QSPI_REMAPADDR_ADDR));
}

ALT_STATUS_CODE alt_qspi_ahb_remap_address_set(const uint32_t ahb_remap_addr)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    // Read and return the value of the Remap Address Register.
    alt_setbits_word(ALT_QSPI_REMAPADDR_ADDR, ALT_QSPI_REMAPADDR_VALUE_SET(ahb_remap_addr));

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_ahb_address_remap_disable(void)
{
    // Clear (set to 0) the Enable AHB Address Remapping Field of the QSPI
    // Configuration Register to disable AHB address remapping.
    alt_clrbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENAHBREMAP_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_ahb_address_remap_enable(void)
{
    // Set (set to 1) the Enable AHB Address Remapping Field of the QSPI
    // Configuration Register to enable AHB address remapping.
    alt_setbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENAHBREMAP_SET_MSK);

    return ALT_E_SUCCESS;
}

/////

static ALT_STATUS_CODE alt_qspi_indirect_read_start_bank(uint32_t flash_addr,
                                                         size_t num_bytes)
{
    alt_write_word(ALT_QSPI_INDRDSTADDR_ADDR, flash_addr);
    alt_write_word(ALT_QSPI_INDRDCNT_ADDR, num_bytes);
    alt_write_word(ALT_QSPI_INDRD_ADDR, ALT_QSPI_INDRD_START_SET_MSK |
                                        ALT_QSPI_INDRD_IND_OPS_DONE_STAT_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_indirect_read_start(const uint32_t flash_addr,
                                             const size_t num_bytes)
{
    // flash_addr and num_bytes restriction is to prevent possible unaligned
    // exceptions.

    if (flash_addr & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (num_bytes & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (num_bytes == 0)
    {
        // Do not report this as a success. If a indirect read was not
        // previously completed, it may be cleared already, at which point
        // alt_qspi_indirect_read_is_complete() will never report true.
        return ALT_E_ERROR;
    }

    if (flash_addr > qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (flash_addr + num_bytes > qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    // Verify request does not cross bank boundary.
    // This limitation is due to the 3-byte addressing limitation.
    if ((flash_addr & ALT_QSPI_BANK_ADDR_MSK) != ((flash_addr + num_bytes - 1) & ALT_QSPI_BANK_ADDR_MSK))
    {
        return ALT_E_ERROR;
    }

    // Verify that there is not already a read in progress.
    if (ALT_QSPI_INDRD_RD_STAT_GET(alt_read_word(ALT_QSPI_INDRD_ADDR)))
    {
        return ALT_E_ERROR;
    }

    /////

    ALT_STATUS_CODE status;
    status = alt_qspi_device_bank_select(flash_addr >> 24);
    if (status != ALT_E_SUCCESS)
    {
        return status;
    }

    /////

    return alt_qspi_indirect_read_start_bank(flash_addr,
                                             num_bytes);

}

ALT_STATUS_CODE alt_qspi_indirect_read_finish(void)
{
    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_indirect_read_cancel(void)
{
    // An indirect operation may be cancelled at any time by setting Indirect
    // Transfer Control Register bit [1].
    alt_write_word(ALT_QSPI_INDRD_ADDR, ALT_QSPI_INDRD_CANCEL_SET_MSK);

    return ALT_E_SUCCESS;
}

uint32_t alt_qspi_indirect_read_fill_level(void)
{
    // Return the SRAM Fill Level (Indirect Read Partition) Field of the SRAM
    // Fill Register to get the SRAM Fill Level for the Indirect Read Partition
    // in units of SRAM Words (4 bytes).
    return ALT_QSPI_SRAMFILL_INDRDPART_GET(alt_read_word(ALT_QSPI_SRAMFILL_ADDR));
}

uint32_t alt_qspi_indirect_read_watermark_get(void)
{
    // Return the Watermark value in the Indirect Read Transfer Watermark Register.
    return alt_read_word(ALT_QSPI_INDRDWATER_ADDR);
}

ALT_STATUS_CODE alt_qspi_indirect_read_watermark_set(const uint32_t watermark)
{
    // Verify that there is not already a read in progress.
    if (ALT_QSPI_INDRD_RD_STAT_GET(alt_read_word(ALT_QSPI_INDRD_ADDR)))
    {
        return ALT_E_ERROR;
    }

    // Set the Watermark value in the Indirect Read Transfer Watermark Register.
    alt_write_word(ALT_QSPI_INDRDWATER_ADDR, watermark);

    return ALT_E_SUCCESS;
}

bool alt_qspi_indirect_read_is_complete(void)
{
    // The value of the Indirect Completion Status Field of the Indirect Read
    // Transfer Control Register is set by hardware when an indirect read
    // operation has completed.
    return (alt_read_word(ALT_QSPI_INDRD_ADDR) & ALT_QSPI_INDRD_IND_OPS_DONE_STAT_SET_MSK) != 0;
}

static ALT_STATUS_CODE alt_qspi_indirect_write_start_bank(uint32_t flash_addr,
                                                          size_t num_bytes)
{
    alt_write_word(ALT_QSPI_INDWRSTADDR_ADDR, flash_addr);
    alt_write_word(ALT_QSPI_INDWRCNT_ADDR, num_bytes);
    alt_write_word(ALT_QSPI_INDWR_ADDR, ALT_QSPI_INDWR_START_SET_MSK |
                                        ALT_QSPI_INDWR_INDDONE_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_indirect_write_start(const uint32_t flash_addr,
                                              const size_t num_bytes)
{
    // flash_addr and num_bytes restriction is to prevent possible unaligned
    // exceptions.

    if (flash_addr & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (num_bytes & 0x3)
    {
        return ALT_E_ERROR;
    }

    if (num_bytes == 0)
    {
        // Do not report this as a success. If a indirect write was not
        // previously completed, it may be cleared already, at which point
        // alt_qspi_indirect_write_is_complete() will never report true.
        return ALT_E_ERROR;
    }

    if (num_bytes > 256)
    {
        // The Micron part can only write up to 256 bytes at a time.
        return ALT_E_ERROR;
    }

    if (flash_addr > qspi_device_size)
    {
        return ALT_E_ERROR;
    }

    if (flash_addr + num_bytes > qspi_device_size)
    {
        return ALT_E_ERROR;
    }

/*
    // Verify request does not cross bank boundary.
    // This limitation is due to the 3-byte addressing limitation.
    if ((flash_addr & ALT_QSPI_BANK_ADDR_MSK) != ((flash_addr + num_bytes - 1) & ALT_QSPI_BANK_ADDR_MSK))
    {
        return ALT_E_ERROR;
    }
*/
    // Verify request does not cross page boundary.
    // This limitation is in place for the Micron part used.
    if ((flash_addr & ALT_QSPI_PAGE_ADDR_MSK) != ((flash_addr + num_bytes - 1) & ALT_QSPI_PAGE_ADDR_MSK))
    {
        return ALT_E_ERROR;
    }

    // Verify that there is not already a write in progress.
    if (ALT_QSPI_INDWR_RDSTAT_GET(alt_read_word(ALT_QSPI_INDWR_ADDR)))
    {
        return ALT_E_ERROR;
    }

    /////

    ALT_STATUS_CODE status = ALT_E_SUCCESS;
    status = alt_qspi_device_bank_select(flash_addr >> 24);
    if (status != ALT_E_SUCCESS)
    {
        return status;
    }

    /////

    return alt_qspi_indirect_write_start_bank(flash_addr,
                                              num_bytes);
}

ALT_STATUS_CODE alt_qspi_indirect_write_finish(void)
{
#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
    return alt_qspi_N25Q_flag_wait_for_program(ALT_QSPI_TIMEOUT_INFINITE);
#else
    return ALT_E_SUCCESS;
#endif
}

ALT_STATUS_CODE alt_qspi_indirect_write_cancel(void)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_N25Q_flag_wait_for_program(ALT_QSPI_TIMEOUT_INFINITE);
    }
#endif

    if (status == ALT_E_SUCCESS)
    {
        // An indirect operation may be cancelled at any time by setting Indirect
        // Transfer Control Register bit [1].
        alt_write_word(ALT_QSPI_INDWR_ADDR, ALT_QSPI_INDWR_CANCEL_SET_MSK);
    }

    return status;
}

uint32_t alt_qspi_indirect_write_fill_level(void)
{
    // Return the SRAM Fill Level (Indirect Write Partition) Field of the SRAM
    // Fill Register to get the SRAM Fill Level for the Indirect Write Partition
    // in units of SRAM Words (4 bytes).
    return ALT_QSPI_SRAMFILL_INDWRPART_GET(alt_read_word(ALT_QSPI_SRAMFILL_ADDR));
}

uint32_t alt_qspi_indirect_write_watermark_get(void)
{
    // Return the Watermark value in the Indirect Write Transfer Watermark Register.
    return alt_read_word(ALT_QSPI_INDWRWATER_ADDR);
}

ALT_STATUS_CODE alt_qspi_indirect_write_watermark_set(const uint32_t watermark)
{
    // Verify that there is not already a write in progress.
    if (ALT_QSPI_INDWR_RDSTAT_GET(alt_read_word(ALT_QSPI_INDWR_ADDR)))
    {
        return ALT_E_ERROR;
    }

    // Set the Watermark value in the Indirect Write Transfer Watermark Register.
    alt_write_word(ALT_QSPI_INDWRWATER_ADDR, watermark);

    return ALT_E_SUCCESS;
}

bool alt_qspi_indirect_write_is_complete(void)
{
    // The value of the Indirect Completion Status Field of the Indirect Write
    // Transfer Control Register is set by hardware when an indirect write
    // operation has completed.
    return (alt_read_word(ALT_QSPI_INDWR_ADDR) & ALT_QSPI_INDWR_INDDONE_SET_MSK) != 0;
}

/////

uint32_t alt_qspi_sram_partition_get(void)
{
    // The number of locations allocated to indirect read is equal to the value
    // of the SRAM partition register. See the documentation for this function
    // regarding the + 1 in the IP documentation. This way the get() and set()
    // will be symmetrical.

    return ALT_QSPI_SRAMPART_ADDR_GET(alt_read_word(ALT_QSPI_SRAMPART_ADDR));
}

ALT_STATUS_CODE alt_qspi_sram_partition_set(const uint32_t read_part_size)
{
    if (read_part_size > ((1 << ALT_QSPI_SRAMPART_ADDR_WIDTH) - 1))
    {
        return ALT_E_ARG_RANGE;
    }

    alt_replbits_word(ALT_QSPI_SRAMPART_ADDR,
                      ALT_QSPI_SRAMPART_ADDR_SET_MSK,
                      ALT_QSPI_SRAMPART_ADDR_SET(read_part_size));

    return ALT_E_SUCCESS;
}

/////


static ALT_STATUS_CODE alt_qspi_erase_subsector_bank(uint32_t addr)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_wren();
    }
    
    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_stig_addr_cmd(ALT_QSPI_STIG_OPCODE_SUBSEC_ERASE, 0, addr, 10000);
    }

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_N25Q_flag_wait_for_erase(ALT_QSPI_TIMEOUT_INFINITE);
    }
#endif

    return status;
}

ALT_STATUS_CODE alt_qspi_erase_subsector(const uint32_t addr)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_bank_select(addr >> 24);
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_erase_subsector_bank(addr);
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_erase_sector(const uint32_t addr)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_bank_select(addr >> 24);
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_wren();
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_stig_addr_cmd(ALT_QSPI_STIG_OPCODE_SEC_ERASE, 0, addr, ALT_QSPI_TIMEOUT_INFINITE);
    }

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_N25Q_flag_wait_for_erase(ALT_QSPI_TIMEOUT_INFINITE);
    }
#endif

    return status;
}

ALT_STATUS_CODE alt_qspi_erase_chip(void)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    if (qspi_device_size >= (2 * ALT_QSPI_N25Q_DIE_SIZE))
    {
        // NOTE: This path is specifically for 512 Mib and 1 Gib Micron N25Q
        //   chips only.

        dprintf("DEBUG[QSPI]: erase[chip]: FYI, wait time is ~800s for 128 MiB.\n");

        uint32_t die_count = qspi_device_size / ALT_QSPI_N25Q_DIE_SIZE;

        for (int i = 0; i < die_count; ++i)
        {
            if (status != ALT_E_SUCCESS)
            {
                break;
            }

            dprintf("DEBUG[QSPI]: Erase chip: die = %d, total = %" PRIu32 ".\n", i, die_count);

            if (status == ALT_E_SUCCESS)
            {
                status = alt_qspi_device_bank_select(i * (ALT_QSPI_N25Q_DIE_SIZE / ALT_QSPI_BANK_SIZE));
            }

            if (status == ALT_E_SUCCESS)
            {
                status = alt_qspi_device_wren(); 
            }

            if (status == ALT_E_SUCCESS)
            {
                status = alt_qspi_stig_addr_cmd(ALT_QSPI_STIG_OPCODE_DIE_ERASE, 0,
                                                i * ALT_QSPI_N25Q_DIE_SIZE,
                                                ALT_QSPI_TIMEOUT_INFINITE);
            }

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
            if (status == ALT_E_SUCCESS)
            {
                status = alt_qspi_N25Q_flag_wait_for_erase(ALT_QSPI_TIMEOUT_INFINITE);
            }
#endif
        }
    }
    else
    {
        // NOTE: Untested path.

        dprintf("DEBUG[QSPI]: Bulk erase.\n");

        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_device_bank_select(0);
        }

        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_device_wren();
        }

        if (status == ALT_E_SUCCESS)
        {
            // If BULK_ERASE is like other ERASE, it needs the address command.
            status = alt_qspi_stig_addr_cmd(ALT_QSPI_STIG_OPCODE_BULK_ERASE, 0,
                                            0,
                                            ALT_QSPI_TIMEOUT_INFINITE);
        }

#if ALT_QSPI_PROVISION_MICRON_N25Q_SUPPORT
        if (status == ALT_E_SUCCESS)
        {
            status = alt_qspi_N25Q_flag_wait_for_erase(ALT_QSPI_TIMEOUT_INFINITE);
        }
#endif
    }

    return status;
}

/////

ALT_STATUS_CODE alt_qspi_dma_disable(void)
{
    // Clear (set to 0) the Enable DMA Peripheral Interface Field of the QSPI
    // Configuration Register to disable the DMA peripheral interface.
    alt_clrbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENDMA_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_dma_enable(void)
{
    // Set (set to 1) the Enable DMA Peripheral Interface Field of the QSPI
    // Configuration Register to enable the DMA peripheral interface.
    alt_setbits_word(ALT_QSPI_CFG_ADDR, ALT_QSPI_CFG_ENDMA_SET_MSK);

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_dma_config_get(uint32_t * single_type_sz,
                                        uint32_t * burst_type_sz)
{
    // Get the current value of the DMA Peripheral Register - dmaper
    uint32_t dmaper = alt_read_word(ALT_QSPI_DMAPER_ADDR);

    // For both values, a programmed value of 0 represents a single byte. The
    // actual number of bytes used is 2 ** (value in this register field).
    *single_type_sz = 1 << ALT_QSPI_DMAPER_NUMSGLREQBYTES_GET(dmaper);
    *burst_type_sz  = 1 << ALT_QSPI_DMAPER_NUMBURSTREQBYTES_GET(dmaper);

    return ALT_E_SUCCESS;
}

//
// Returns true if [n] is a power of 2 value otherwise returns false.
//
static bool is_pow_2(uint32_t n)
{
    return ((n > 0) && ((n & (n - 1)) == 0));
}

//
// Return the log base 2 value of a number that is known to be a power of 2.
//
static uint32_t log2u(uint32_t value)
{
    uint32_t exp = 0;
    while ((exp < 32) && (value != (1 << exp)))
    {
        ++exp;
    }
    return exp;
}

ALT_STATUS_CODE alt_qspi_dma_config_set(const uint32_t single_type_sz,
                                        const uint32_t burst_type_sz)
{
    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    if (single_type_sz < 4)
    {
        return ALT_E_ERROR;
    }

    if (burst_type_sz < 4)
    {
        return ALT_E_ERROR;
    }

    if (burst_type_sz < single_type_sz)
    {
        return ALT_E_ERROR;
    }

    const uint32_t single_type_sz_max = 1 << ((1 << ALT_QSPI_DMAPER_NUMSGLREQBYTES_WIDTH) - 1);
    const uint32_t burst_type_sz_max  = 1 << ((1 << ALT_QSPI_DMAPER_NUMBURSTREQBYTES_WIDTH) - 1);

    // Both parameter values must be a power of 2 between 1 and 32728.
    if (  (single_type_sz > single_type_sz_max) || !is_pow_2(single_type_sz)
       || (burst_type_sz  > burst_type_sz_max)  || !is_pow_2(burst_type_sz)
       )
    {
        return ALT_E_ARG_RANGE;
    }

    // Get the current value of the DMA Peripheral Register - dmaper
    uint32_t dmaper = alt_read_word(ALT_QSPI_DMAPER_ADDR);
    dmaper &= ALT_QSPI_DMAPER_NUMBURSTREQBYTES_CLR_MSK &
              ALT_QSPI_DMAPER_NUMSGLREQBYTES_CLR_MSK;
    dmaper |= ALT_QSPI_DMAPER_NUMBURSTREQBYTES_SET(log2u(burst_type_sz)) |
              ALT_QSPI_DMAPER_NUMSGLREQBYTES_SET(log2u(single_type_sz));
    alt_write_word(ALT_QSPI_DMAPER_ADDR, dmaper);

    return ALT_E_SUCCESS;
}

/////

//
// Private STIG and device commands
//

static ALT_STATUS_CODE alt_qspi_stig_cmd_helper(uint32_t reg_value, uint32_t timeout)
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;
    bool infinite = (timeout == ALT_QSPI_TIMEOUT_INFINITE);

    alt_write_word(ALT_QSPI_FLSHCMD_ADDR, reg_value);
    alt_write_word(ALT_QSPI_FLSHCMD_ADDR, reg_value | ALT_QSPI_FLSHCMD_EXECCMD_E_EXECUTE);

    do
    {
        reg_value = alt_read_word(ALT_QSPI_FLSHCMD_ADDR);
        if (!(reg_value & ALT_QSPI_FLSHCMD_CMDEXECSTAT_SET_MSK))
        {
            break;
        }

    } while (timeout-- || infinite);

    if (timeout == (uint32_t)-1 && !infinite)
    {
        status = ALT_E_TMO;
    }

    return status;
}

ALT_STATUS_CODE alt_qspi_stig_cmd(uint32_t opcode, uint32_t dummy, uint32_t timeout)
{
    if (dummy > ((1 << ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_WIDTH) - 1))
    {
        return ALT_E_ERROR;
    }

    uint32_t reg = ALT_QSPI_FLSHCMD_CMDOPCODE_SET(opcode) |
                   ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_SET(dummy);

    return alt_qspi_stig_cmd_helper(reg, timeout);
}

ALT_STATUS_CODE alt_qspi_stig_rd_cmd(uint8_t opcode,
                                     uint32_t dummy,
                                     uint32_t num_bytes,
                                     uint32_t * output,
                                     uint32_t timeout)
{
    if (dummy > ((1 << ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_WIDTH) - 1))
    {
        return ALT_E_ERROR;
    }

    // STIG read can only return up to 8 bytes.
    if ((num_bytes > 8) || (num_bytes == 0))
    {
        return ALT_E_BAD_ARG;
    }

    uint32_t reg_value = 
        ALT_QSPI_FLSHCMD_CMDOPCODE_SET(opcode)                              |
        ALT_QSPI_FLSHCMD_ENRDDATA_SET(ALT_QSPI_FLSHCMD_ENRDDATA_E_EN)       |
        ALT_QSPI_FLSHCMD_NUMRDDATABYTES_SET(num_bytes - 1)                  |
        ALT_QSPI_FLSHCMD_ENCMDADDR_SET(ALT_QSPI_FLSHCMD_ENCMDADDR_E_DISD)   |
        ALT_QSPI_FLSHCMD_ENMODBIT_SET(ALT_QSPI_FLSHCMD_ENMODBIT_E_DISD)     |
        ALT_QSPI_FLSHCMD_NUMADDRBYTES_SET(0)                                |
        ALT_QSPI_FLSHCMD_ENWRDATA_SET(ALT_QSPI_FLSHCMD_ENWRDATA_E_NOACTION) |
        ALT_QSPI_FLSHCMD_NUMWRDATABYTES_SET(0)                              |
        ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_SET(dummy);

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    status = alt_qspi_stig_cmd_helper(reg_value, timeout);
    if (status != ALT_E_SUCCESS)
    {
        return status;
    }

    output[0] = alt_read_word(ALT_QSPI_FLSHCMDRDDATALO_ADDR);

    if (num_bytes > 4)
    {
        output[1] = alt_read_word(ALT_QSPI_FLSHCMDRDDATAUP_ADDR);
    }

    return ALT_E_SUCCESS;
}

ALT_STATUS_CODE alt_qspi_stig_wr_cmd(uint8_t opcode,
                                     uint32_t dummy,
                                     uint32_t num_bytes, 
                                     const uint32_t * input,
                                     uint32_t timeout)
{
    if (dummy > ((1 << ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_WIDTH) - 1))
    {
        return ALT_E_ERROR;
    }

    // STIG can only write up to 8 bytes.
    if ((num_bytes > 8) || (num_bytes == 0))
    {
        return ALT_E_BAD_ARG;
    }

    uint32_t reg_value =
        ALT_QSPI_FLSHCMD_CMDOPCODE_SET(opcode)                                 |
        ALT_QSPI_FLSHCMD_ENRDDATA_SET(ALT_QSPI_FLSHCMD_ENRDDATA_E_NOACTION)    |
        ALT_QSPI_FLSHCMD_NUMRDDATABYTES_SET(0)                                 |
        ALT_QSPI_FLSHCMD_ENCMDADDR_SET(ALT_QSPI_FLSHCMD_ENCMDADDR_E_DISD)      |
        ALT_QSPI_FLSHCMD_ENMODBIT_SET(ALT_QSPI_FLSHCMD_ENMODBIT_E_DISD)        |
        ALT_QSPI_FLSHCMD_NUMADDRBYTES_SET(0)                                   |
        ALT_QSPI_FLSHCMD_ENWRDATA_SET(ALT_QSPI_FLSHCMD_ENWRDATA_E_WRDATABYTES) |
        ALT_QSPI_FLSHCMD_NUMWRDATABYTES_SET(num_bytes - 1)                     |
        ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_SET(dummy);

    alt_write_word(ALT_QSPI_FLSHCMDWRDATALO_ADDR, input[0]);

    if (num_bytes > 4)
    {
        alt_write_word(ALT_QSPI_FLSHCMDWRDATAUP_ADDR, input[1]);
    }

    return alt_qspi_stig_cmd_helper(reg_value, timeout);
}

ALT_STATUS_CODE alt_qspi_stig_addr_cmd(uint8_t opcode,
                                       uint32_t dummy,
                                       uint32_t address,
                                       uint32_t timeout)
{
    if (dummy > ((1 << ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_WIDTH) - 1))
    {
        return ALT_E_ERROR;
    }

    uint32_t reg = ALT_QSPI_FLSHCMD_CMDOPCODE_SET(opcode) |
                   ALT_QSPI_FLSHCMD_NUMDUMMYBYTES_SET(dummy);

    reg |= ALT_QSPI_FLSHCMD_ENCMDADDR_SET(ALT_QSPI_FLSHCMD_ENCMDADDR_E_END);
    reg |= ALT_QSPI_FLSHCMD_NUMADDRBYTES_SET(ALT_QSPI_FLSHCMD_NUMADDRBYTES_E_ADDRBYTE3);

    alt_write_word(ALT_QSPI_FLSHCMDADDR_ADDR, address);

    return alt_qspi_stig_cmd_helper(reg, timeout);
}

/////

ALT_STATUS_CODE alt_qspi_device_wren(void) 
{
    // Write enable through STIG (not required, auto send by controller during write)
    return alt_qspi_stig_cmd(ALT_QSPI_STIG_OPCODE_WREN, 0, 10000);
}

ALT_STATUS_CODE alt_qspi_device_wrdis(void) 
{
    // Write disable through STIG (not required, auto send by controller during write)
    return alt_qspi_stig_cmd(ALT_QSPI_STIG_OPCODE_WRDIS, 0, 10000);
}

ALT_STATUS_CODE alt_qspi_device_rdid(uint32_t * rdid)
{
    // Read flash device ID through STIG
    return alt_qspi_stig_rd_cmd(ALT_QSPI_STIG_OPCODE_RDID, 0, 4, rdid, 10000);
}

ALT_STATUS_CODE alt_qspi_discovery_parameter(uint32_t * param)
{
    // Read flash discovery parameters through STIG

    return alt_qspi_stig_rd_cmd(ALT_QSPI_STIG_OPCODE_DISCVR_PARAM, 8, 8, param, 10000);
}

ALT_STATUS_CODE alt_qspi_device_bank_select(uint32_t bank) 
{
    ALT_STATUS_CODE status = ALT_E_SUCCESS;
    dprintf("DEBUG[QSPI]: bank_select(): switching to bank 0x%" PRIu32 ".\n", bank);

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_wren();
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_stig_wr_cmd(ALT_QSPI_STIG_OPCODE_WR_EXT_REG, 0, 1, &bank, 10000);
    }

    if (status == ALT_E_SUCCESS)
    {
        status = alt_qspi_device_wrdis();
    }

    return status;
}

/////

static bool alt_qspi_is_enabled(void)
{
    uint32_t cfg = alt_read_word(ALT_QSPI_CFG_ADDR);

    if (cfg & ALT_QSPI_CFG_EN_SET_MSK)
    {
        return true;
    }
    else
    {
        return false;
    }
}

ALT_STATUS_CODE alt_qspi_ecc_start(void * block, size_t size)
{
    if (size < (ALT_QSPI_PAGE_SIZE * 8))
    {
        return ALT_E_ERROR;
    }

    if (alt_qspi_is_enabled() == false)
    {
        return ALT_E_ERROR;
    }

    if (alt_qspi_is_idle() == false)
    {
        return ALT_E_ERROR;
    }

    ALT_STATUS_CODE status = ALT_E_SUCCESS;

    // 1. Configure SRAM Partition Register to 126 words for read, 2 words for write.
    // 2. Enable ECC on QSPI RAM
    // 3. Trigger an indirect read transfer that will fill up 126 words in FIFO by
    //    monitoring read FIFO fill level; Do not read out data through AHB.
    // 4. Start AHB read and start indirect write operation to write back to the same
    //    device location, this will fill up and initilaize the write partition RAM.
    // 5. To clear spurious interrupts, reset the QSPI controller.

    // Save the previous partition size

    uint32_t sram_orig = alt_qspi_sram_partition_get();
    dprintf("DEBUG[QSPI][ECC]: Save original SRAM as %" PRIu32 ".\n", sram_orig);

    // Step 1

    uint32_t sram_fill = (1 << ALT_QSPI_SRAMPART_ADDR_WIDTH) - 2;
    alt_qspi_sram_partition_set(sram_fill);
    dprintf("DEBUG[QSPI][ECC]: Set new SRAM as %" PRIu32 ".\n", sram_fill);

    // Step 2

    dprintf("DEBUG[QSPI][ECC]: Enable ECC in SysMgr.\n");
    alt_write_word(ALT_SYSMGR_ECC_QSPI_ADDR, ALT_SYSMGR_ECC_QSPI_EN_SET_MSK);

    // Step 3

    // Issue a read ~ 2x larger than the read partition. We will read out 1 page,
    // which will be used as the buffer to write back to QSPI. This way no data
    // actually changes thus no erase will be needed.

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Start indirect read PAGE * 8.\n");
        status = alt_qspi_indirect_read_start(0x0, ALT_QSPI_PAGE_SIZE * 8);
    }

    // Read out 1 page for the write data

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Reading out 1 page ...\n");

        uint32_t read_size = 0;
        char *   buffer    = block;
        while (read_size < ALT_QSPI_PAGE_SIZE)
        {
            uint32_t level = alt_qspi_indirect_read_fill_level();
            level = MIN(level, (ALT_QSPI_PAGE_SIZE - read_size) / sizeof(uint32_t));

            uint32_t * data = (uint32_t *)(&buffer[read_size]);
            for (uint32_t i = 0; i < level; ++i)
            {
                *data = alt_read_word(ALT_QSPIDATA_ADDR);
                ++data;
            }

            read_size += level * sizeof(uint32_t);
        }

        if (read_size != ALT_QSPI_PAGE_SIZE)
        {
            status = ALT_E_ERROR;
        }
    }

    // Wait for read FIFO to report it is up to the specified fill level.

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Waiting for read fill level ...\n");

        uint32_t timeout = 10000;

        while (alt_qspi_indirect_read_fill_level() < sram_fill)
        {
            if (--timeout == 0)
            {
                dprintf("DEBUG[QSPI][ECC]: Waiting for read fill timeout !!!\n");
                status = ALT_E_TMO;
                break;
            }
        }
    }

    // Step 4

    // Issue a write of 1 page of the same data from 0x0.

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Start indirect write PAGE.\n");
        status = alt_qspi_indirect_write_start(0x0, ALT_QSPI_PAGE_SIZE);
    }

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Writing in 1 page ...\n");

        uint32_t write_size = 0;
        char *   buffer     = block;

        while (write_size < ALT_QSPI_PAGE_SIZE)
        {
            uint32_t space = 2 - alt_qspi_indirect_write_fill_level();
            if (space == 0)
            {
                dprintf("DEBUG[QSPI][ECC]: Write FIFO filled at write_size = %" PRIu32 ".\n", write_size);
                // Space = 0; which means all 2 positions in the write FIFO is filled,
                // meaning it has been initialized with respect to ECC.
                break;
            }

            space = MIN(space, (ALT_QSPI_PAGE_SIZE - write_size) / sizeof(uint32_t));

            uint32_t * data = (uint32_t *)(&buffer[write_size]);
            for (uint32_t i = 0; i < space; ++i)
            {
                alt_write_word(ALT_QSPIDATA_ADDR, *data);
                ++data;
            }

            write_size += space * sizeof(uint32_t);
        }

        if (write_size != ALT_QSPI_PAGE_SIZE)
        {
            dprintf("DEBUG[QSPI][ECC]: Cancel indirect write.\n");
            status = alt_qspi_indirect_write_cancel();
        }
    }

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Finish indirect write.\n");
        status = alt_qspi_indirect_write_finish();
    }

    // Cancel the indirect read as it has initialized the read FIFO partition.

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Cancel indirect read.\n");
        status = alt_qspi_indirect_read_cancel();
    }

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Finish indirect read.\n");
        status = alt_qspi_indirect_read_finish();
    }

    // Step 5

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Clear any pending spurious QSPI ECC interrupts.\n");

        alt_write_word(ALT_SYSMGR_ECC_QSPI_ADDR,
                         ALT_SYSMGR_ECC_QSPI_EN_SET_MSK
                       | ALT_SYSMGR_ECC_QSPI_SERR_SET_MSK
                       | ALT_SYSMGR_ECC_QSPI_DERR_SET_MSK);
    }

    /////

    // Restore original partition

    if (status == ALT_E_SUCCESS)
    {
        dprintf("DEBUG[QSPI][ECC]: Restore original SRAM as %" PRIu32 ".\n", sram_orig);
        status = alt_qspi_sram_partition_set(sram_orig);
    }

    return status;
}
