/******************************************************************************

rp2040-psram

Copyright © 2023 Ian Scott

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

******************************************************************************/

/**
 * @file psram_spi.h
 *
 * \mainpage
 *
 * The interface to this file is defined in psram_spi.h. Please see the
 * documentation for this file.
 *
 * The following defines _MUST_ be defined:
 *
 * - @c PSRAM_PIN_CS - GPIO number of the chip select pin
 * - @c PSRAM_PIN_SCK - GPIO number of the clock pin
 * - @c PSRAM_PIN_MOSI - GPIO number of the MOSI pin
 * - @c PSRAM_PIN_MISO - GPIO number of the MISO pin
 *
 * Optional define:
 * - @c PSRAM_MUTEX - Define this to put PSRAM access behind a mutex. This must
 * be used if the PSRAM is to be used by multiple cores.
 *
 * Project homepage: https://github.com/polpo/rp2040-psram
 */

#pragma once
#include "iopins.h"
#include "hardware/pio.h"
#include "hardware/gpio.h"
#include "hardware/timer.h"
#include "hardware/dma.h"
#if defined(PSRAM_MUTEX)
#include "pico/mutex.h"
#elif defined(PSRAM_SPINLOCK)
#include "hardware/sync.h"
#endif
#include <string.h>
 
//#define PSRAM_ASYNC 1

#include "psram_spi.pio.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief A struct that holds the configuration for the PSRAM interface.
 *
 * This struct is generated by psram_spi_init() and must be passed to all calls to
 * the psram access functions.
 */
typedef struct psram_spi_inst {
    PIO pio;
    int sm;
    uint offset;
#if defined(PSRAM_MUTEX)
    mutex_t mtx;
#elif defined(PSRAM_SPINLOCK)
    spin_lock_t* spinlock;
    uint32_t spin_irq_state;
#endif
    int write_dma_chan;
    dma_channel_config write_dma_chan_config;
    int read_dma_chan;
    dma_channel_config read_dma_chan_config;
#if defined(PSRAM_ASYNC)
    int async_dma_chan;
    dma_channel_config async_dma_chan_config;
#endif
} psram_spi_inst_t;

#if defined(PSRAM_ASYNC)
extern psram_spi_inst_t* async_spi_inst;
#endif

/**
 * @brief Write and read raw data to the PSRAM SPI PIO, driven by the CPU
 * without DMA. This can be used if DMA has not yet been initialized.
 *
 * Used to send raw commands and receive data from the PSRAM. Usually the @c
 * psram_write*() and @c psram_read*() commands should be used instead.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param src Pointer to the source data to write.
 * @param src_len Length of the source data in bytes.
 * @param dst Pointer to the destination for read data, if any. Set to 0 or NULL
 * if no data is to be read.
 * @param dst_len Length of the destination data in bytes. Set to 0 if no data
 * is to be read.
 */
__force_inline static void __time_critical_func(pio_spi_write_read_blocking)(
        psram_spi_inst_t* spi,
        const uint8_t* src, const size_t src_len,
        uint8_t* dst, const size_t dst_len
) {
    size_t tx_remain = src_len, rx_remain = dst_len;

#if defined(PSRAM_MUTEX)
    mutex_enter_blocking(&spi->mtx); 
#elif defined(PSRAM_SPINLOCK)
    spi->spin_irq_state = spin_lock_blocking(spi->spinlock);
#endif
    io_rw_8 *txfifo = (io_rw_8 *) &spi->pio->txf[spi->sm];
    while (tx_remain) {
        if (!pio_sm_is_tx_fifo_full(spi->pio, spi->sm)) {
            *txfifo = *src++;
            --tx_remain;
        }
    }

    io_rw_8 *rxfifo = (io_rw_8 *) &spi->pio->rxf[spi->sm];
    while (rx_remain) {
        if (!pio_sm_is_rx_fifo_empty(spi->pio, spi->sm)) {
            *dst++ = *rxfifo;
            --rx_remain;
        }
    }

#if defined(PSRAM_MUTEX)
    mutex_exit(&spi->mtx);
#elif defined(PSRAM_SPINLOCK)
    spin_unlock(spi->spinlock, spi->spin_irq_state);
#endif
}

/**
 * @brief Write raw data to the PSRAM SPI PIO, driven by DMA without CPU
 * involvement. 
 *
 * It's recommended to use DMA when possible as it's higher speed. Used to send
 * raw commands to the PSRAM. This function is faster than
 * pio_spi_write_read_dma_blocking() if no data is to be read.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param src Pointer to the source data to write.
 * @param src_len Length of the source data in bytes.
 */
__force_inline static void __time_critical_func(pio_spi_write_dma_blocking)(
        psram_spi_inst_t* spi,
        const uint8_t* src, const size_t src_len
) {
#ifdef PSRAM_MUTEX
    mutex_enter_blocking(&spi->mtx); 
#elif defined(PSRAM_SPINLOCK)
    spi->spin_irq_state = spin_lock_blocking(spi->spinlock);
#endif // PSRAM_SPINLOCK
#if defined(PSRAM_WAITDMA)
#if defined(PSRAM_ASYNC)
    dma_channel_wait_for_finish_blocking(spi->async_dma_chan);
#endif // PSRAM_ASYNC
    dma_channel_wait_for_finish_blocking(spi->write_dma_chan);
    dma_channel_wait_for_finish_blocking(spi->read_dma_chan);
#endif // PSRAM_WAITDMA
    dma_channel_transfer_from_buffer_now(spi->write_dma_chan, src, src_len);
    dma_channel_wait_for_finish_blocking(spi->write_dma_chan);
#ifdef PSRAM_MUTEX
    mutex_exit(&spi->mtx);
#elif defined(PSRAM_SPINLOCK)
    spin_unlock(spi->spinlock, spi->spin_irq_state);
#endif // PSRAM_SPINLOCK
}

/**
 * @brief Write and read raw data to the PSRAM SPI PIO, driven by DMA without CPU
 * involvement. 
 *
 * It's recommended to use DMA when possible as it's higher speed. Used to send
 * raw commands and receive data from the PSRAM. Usually the @c psram_write* and
 * @c psram_read* commands should be used instead.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param src Pointer to the source data to write.
 * @param src_len Length of the source data in bytes.
 * @param dst Pointer to the destination for read data, if any. Set to 0 or NULL
 * if no data is to be read.
 * @param dst_len Length of the destination data in bytes. Set to 0 if no data
 * is to be read.
 */
__force_inline static void __time_critical_func(pio_spi_write_read_dma_blocking)(
        psram_spi_inst_t* spi,
        const uint8_t* src, const size_t src_len,
        uint8_t* dst, const size_t dst_len
) {
#ifdef PSRAM_MUTEX
    mutex_enter_blocking(&spi->mtx); 
#elif defined(PSRAM_SPINLOCK)
    spi->spin_irq_state = spin_lock_blocking(spi->spinlock);
#endif // PSRAM_SPINLOCK
#if defined(PSRAM_WAITDMA)
#if defined(PSRAM_ASYNC)
    dma_channel_wait_for_finish_blocking(spi->async_dma_chan);
#endif // PSRAM_ASYNC
    dma_channel_wait_for_finish_blocking(spi->write_dma_chan);
    dma_channel_wait_for_finish_blocking(spi->read_dma_chan);
#endif // PSRAM_WAITDMA
    dma_channel_transfer_from_buffer_now(spi->write_dma_chan, src, src_len);
    dma_channel_transfer_to_buffer_now(spi->read_dma_chan, dst, dst_len);
    dma_channel_wait_for_finish_blocking(spi->write_dma_chan);
    dma_channel_wait_for_finish_blocking(spi->read_dma_chan);
#ifdef PSRAM_MUTEX
    mutex_exit(&spi->mtx);
#elif defined(PSRAM_SPINLOCK)
    spin_unlock(spi->spinlock, spi->spin_irq_state);
#endif // PSRAM_SPINLOCK
}

/**
 * @brief Write raw data asynchronously to the PSRAM SPI PIO, driven by DMA without CPU
 * involvement. 
 *
 * Used to send raw commands to the PSRAM. Usually the @c psram_write*_async()
 * command should be used instead.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param src Pointer to the source data to write.
 * @param src_len Length of the source data in bytes.
 */
#if defined(PSRAM_ASYNC)
__force_inline static void __time_critical_func(pio_spi_write_async)(
        psram_spi_inst_t* spi,
        const uint8_t* src, const size_t src_len
) {
#if defined(PSRAM_ASYNC_SYNCHRONIZE)
#ifdef PSRAM_MUTEX
    mutex_enter_blocking(&spi->mtx); 
#elif defined(PSRAM_SPINLOCK)
    spi->spin_irq_state = spin_lock_blocking(spi->spinlock);
#endif // PSRAM_SPINLOCK
#endif // defined(PSRAM_ASYNC_SYNCHRONIZE)
    // Wait for all DMA to PSRAM to complete
    dma_channel_wait_for_finish_blocking(spi->write_dma_chan);
    dma_channel_wait_for_finish_blocking(spi->read_dma_chan);
    dma_channel_wait_for_finish_blocking(spi->async_dma_chan);
    async_spi_inst = spi;

    dma_channel_transfer_from_buffer_now(spi->async_dma_chan, src, src_len);
}
#endif


/**
 * @brief Initialize the PSRAM over SPI. This function must be called before
 * accessing PSRAM.
 *
 * @param pio The PIO instance to use (PIO0 or PIO1).
 * @param sm The state machine number in the PIO module to use. If -1 is given,
 * will use the first available state machine.
 * @param clkdiv Clock divisor for the state machine. At RP2040 speeds greater
 * than 280MHz, a clkdiv >1.0 is needed. For example, at 400MHz, a clkdiv of
 * 1.6 is recommended.
 * @param fudge Whether to insert an extra "fudge factor" of one clock cycle
 * before reading from the PSRAM. Depending on your PCB layout or PSRAM type,
 * you may need to do this.
 *
 * @return The PSRAM configuration instance. This instance should be passed to
 * all PSRAM access functions.
 */
psram_spi_inst_t psram_spi_init_clkdiv(PIO pio, int sm, float clkdiv, bool fudge);

/**
 * @brief Initialize the PSRAM over SPI. This function must be called before
 * accessing PSRAM.
 *
 * Defaults to a clkdiv of 1.0. This function is provided for backwards
 * compatibility. Use psram_spi_init_clkdiv instead if you want a clkdiv other
 * than 1.0.
 *
 * @param pio The PIO instance to use (PIO0 or PIO1).
 * @param sm The state machine number in the PIO module to use. If -1 is given,
 * will use the first available state machine.
 *
 * @return The PSRAM configuration instance. This instance should be passed to
 * all PSRAM access functions.
 */
psram_spi_inst_t psram_spi_init(PIO pio, int sm);
int test_psram(psram_spi_inst_t* psram_spi, int increment);

void psram_spi_uninit(psram_spi_inst_t spi, bool fudge);

static uint8_t write8_command[] = {
    40,         // 40 bits write
    0,          // 0 bits read
    0x02u,      // Write command
    0, 0, 0,    // Address
    0           // 8 bits data
};
/**
 * @brief Write 8 bits of data to a given address asynchronously to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement.
 *
 * This function is optimized to write 8 bits as quickly as possible to the
 * PSRAM as opposed to the more general-purpose psram_write() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param val Value to write.
 */
#if defined(PSRAM_ASYNC)
__force_inline static void psram_write8_async(psram_spi_inst_t* spi, uint32_t addr, uint8_t val) {
    write8_command[3] = addr >> 16;
    write8_command[4] = addr >> 8;
    write8_command[5] = addr;
    write8_command[6] = val;

    pio_spi_write_async(spi, write8_command, sizeof(write8_command));
};
#endif


/**
 * @brief Write 8 bits of data to a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the write is
 * complete.
 *
 * This function is optimized to write 8 bits as quickly as possible to the
 * PSRAM as opposed to the more general-purpose psram_write() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param val Value to write.
 */
__force_inline static void psram_write8(psram_spi_inst_t* spi, uint32_t addr, uint8_t val) {
    write8_command[3] = addr >> 16;
    write8_command[4] = addr >> 8;
    write8_command[5] = addr;
    write8_command[6] = val;

    pio_spi_write_dma_blocking(spi, write8_command, sizeof(write8_command));
};


static uint8_t read8_command[] = {
    40,         // 40 bits write
    8,          // 8 bits read
    0x0bu,      // Fast read command
    0, 0, 0,    // Address
    0           // 8 delay cycles
};
/**
 * @brief Read 8 bits of data from a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the read is
 * complete.
 *
 * This function is optimized to read 8 bits as quickly as possible from the
 * PSRAM as opposed to the more general-purpose psram_read() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to read from.
 * @return The data at the specified address.
 */
__force_inline static uint8_t psram_read8(psram_spi_inst_t* spi, uint32_t addr) {
    read8_command[3] = addr >> 16;
    read8_command[4] = addr >> 8;
    read8_command[5] = addr;

    uint8_t val; 
    pio_spi_write_read_dma_blocking(spi, read8_command, sizeof(read8_command), &val, 1);
    return val;
};


static uint8_t write16_command[] = {
    48,         // 48 bits write
    0,          // 0 bits read
    0x02u,      // Write command
    0, 0, 0,    // Address
    0, 0        // 16 bits data
};
/**
 * @brief Write 16 bits of data to a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the write is
 * complete.
 *
 * This function is optimized to write 16 bits as quickly as possible to the
 * PSRAM as opposed to the more general-purpose psram_write() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param val Value to write.
 */

__force_inline static void psram_write16(psram_spi_inst_t* spi, uint32_t addr, uint16_t val) {
    write16_command[3] = addr >> 16;
    write16_command[4] = addr >> 8;
    write16_command[5] = addr;
    write16_command[6] = val;
    write16_command[7] = val >> 8;

    pio_spi_write_dma_blocking(spi, write16_command, sizeof(write16_command));
};


static uint8_t read16_command[] = {
    40,         // 40 bits write
    16,         // 16 bits read
    0x0bu,      // Fast read command
    0, 0, 0,    // Address
    0           // 8 delay cycles
};
/**
 * @brief Read 16 bits of data from a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the read is
 * complete.
 *
 * This function is optimized to read 16 bits as quickly as possible from the
 * PSRAM as opposed to the more general-purpose psram_read() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to read from.
 * @return The data at the specified address.
 */
__force_inline static uint16_t psram_read16(psram_spi_inst_t* spi, uint32_t addr) {
    read16_command[3] = addr >> 16;
    read16_command[4] = addr >> 8;
    read16_command[5] = addr;

    uint16_t val; 
    pio_spi_write_read_dma_blocking(spi, read16_command, sizeof(read16_command), (unsigned char*)&val, 2);
    return val;
};


static uint8_t write32_command[] = {
    64,         // 64 bits write
    0,          // 0 bits read
    0x02u,      // Write command
    0, 0, 0,    // Address
    0, 0, 0, 0  // 32 bits data
};
/**
 * @brief Write 32 bits of data to a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the write is
 * complete.
 *
 * This function is optimized to write 32 bits as quickly as possible to the
 * PSRAM as opposed to the more general-purpose psram_write() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param val Value to write.
 */
__force_inline static void psram_write32(psram_spi_inst_t* spi, uint32_t addr, uint32_t val) {
    // Break the address into three bytes and send read command
    write32_command[3] = addr >> 16;
    write32_command[4] = addr >> 8;
    write32_command[5] = addr;
    write32_command[6] = val;
    write32_command[7] = val >> 8;
    write32_command[8] = val >> 16;
    write32_command[9] = val >> 24;

    pio_spi_write_dma_blocking(spi, write32_command, sizeof(write32_command));
};


/**
 * @brief Write 32 bits of data to a given address asynchronously to the PSRAM
 * SPI PIO, driven by DMA without CPU involvement.
 *
 * This function is optimized to write 32 bits as quickly as possible to the
 * PSRAM as opposed to the more general-purpose psram_write() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param val Value to write.
 */
__force_inline static void psram_write32_async(psram_spi_inst_t* spi, uint32_t addr, uint32_t val) {
    // Break the address into three bytes and send read command
    write32_command[3] = addr >> 16;
    write32_command[4] = addr >> 8;
    write32_command[5] = addr;
    write32_command[6] = val;
    write32_command[7] = val >> 8;
    write32_command[8] = val >> 16;
    write32_command[9] = val >> 24;

    pio_spi_write_async(spi, write32_command, sizeof(write32_command));
};


static uint8_t read32_command[] = {
    40,         // 40 bits write
    32,         // 32 bits read
    0x0bu,      // Fast read command
    0, 0, 0,    // Address
    0           // 8 delay cycles
};
/**
 * @brief Read 32 bits of data from a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the read is
 * complete.
 *
 * This function is optimized to read 32 bits as quickly as possible from the
 * PSRAM as opposed to the more general-purpose psram_read() function.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to read from.
 * @return The data at the specified address.
 */
__force_inline static uint32_t psram_read32(psram_spi_inst_t* spi, uint32_t addr) {
    read32_command[3] = addr >> 16;
    read32_command[4] = addr >> 8;
    read32_command[5] = addr;

    uint32_t val;
    pio_spi_write_read_dma_blocking(spi, read32_command, sizeof(read32_command), (unsigned char*)&val, 4);
    return val;
};


static uint8_t write_command[] = {
    0,          // n bits write
    0,          // 0 bits read
    0x02u,      // Fast write command
    0, 0, 0     // Address
};
/**
 * @brief Write @c count bytes of data to a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the write is
 * complete.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param src Pointer to the source data to write.
 * @param count Number of bytes to write.
 */
__force_inline static void psram_writen(psram_spi_inst_t* spi, const uint32_t addr, const uint8_t* src, const size_t count) {
    // Break the address into three bytes and send read command
    write_command[0] = (4 + count) * 8;
    write_command[3] = addr >> 16;
    write_command[4] = addr >> 8;
    write_command[5] = addr;

    pio_spi_write_dma_blocking(spi, write_command, sizeof(write_command));
    pio_spi_write_dma_blocking(spi, src, count);
};


static uint8_t read_command[] = {
    40,         // 40 bits write
    0,          // n bits read
    0x0bu,      // Fast read command
    0, 0, 0,    // Address
    0           // 8 delay cycles
};
/**
 * @brief Read @c count bits of data from a given address to the PSRAM SPI PIO,
 * driven by DMA without CPU involvement, blocking until the read is
 * complete.
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to read from.
 * @param dst Pointer to the destination for the read data.
 * @param count Number of bytes to read.
 */
__force_inline static void psram_readn(psram_spi_inst_t* spi, const uint32_t addr, uint8_t* dst, const size_t count) {
    read_command[1] = count * 8;
    read_command[3] = addr >> 16;
    read_command[4] = addr >> 8;
    read_command[5] = addr;

    pio_spi_write_read_dma_blocking(spi, read_command, sizeof(read_command), dst, count);
};


static uint8_t write_async_fast_command[134] = {
    0,          // n bits write
    0,          // 0 bits read
    0x02u      // Fast write command
};
/**
 * @brief Write @c count bytes of data to a given address asynchronously to the
 * PSRAM SPI PIO, driven by DMA without CPU involvement. 
 *
 * @param spi The PSRAM configuration instance returned from psram_spi_init().
 * @param addr Address to write to.
 * @param src Pointer to the source data to write.
 * @param count Number of bytes to write.
 */
__force_inline static void psram_write_async_fast(psram_spi_inst_t* spi, uint32_t addr, uint8_t* val, const size_t count) {
    write_async_fast_command[0] = (4 + count) * 8;
    write_async_fast_command[3] = addr >> 16;
    write_async_fast_command[4] = addr >> 8;
    write_async_fast_command[5] = addr;

    memcpy(write_async_fast_command + 6, val, count);

    pio_spi_write_async(spi, write_async_fast_command, 6 + count);
};


#ifdef __cplusplus
}
#endif
