/****************************************************************************
 * arch/arm/src/samv7/sam_progmem.c
 *
 *   Copyright (C) 2015 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * 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. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <string.h>
#include <semaphore.h>
#include <errno.h>

#include <nuttx/arch.h>
#include <arch/samv7/chip.h>

#include "up_arch.h"
#include "cache.h"
#include "chip/sam_memorymap.h"

#include "sam_progmem.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/
/* Configuration ************************************************************/

#ifndef CONFIG_SAMV7_PROGMEM_NSECTORS
#  error CONFIG_SAMV7_PROGMEM_NSECTORS is not defined
#endif

/* Chip dependencies */

#if defined(CONFIG_ARCH_CHIP_SAMV71) || defined(CONFIG_ARCH_CHIP_SAME70)
/* All sectors are 128KB and are uniform in size.
 * The only execption is sector 0 which is subdivided into two small sectors
 * of 8KB and one larger sector of 112KB.
 * The page size is 515 bytes.  However, the smallest thing that can be
 * erased is four pages.  We will refer to this as a "cluster".
 */

#  define SAMV7_SECTOR_SHIFT      (17)   /* 2**17 = 128KB */
#  define SAMV7_PAGE_SHIFT        (9)    /* 2**9  = 512B  */
#  define SAMV7_CLUSTER_SHIFT     (13)   /* 2**13 = 8*KB = 16 pages */
#  define SAMV7_LOCK_REGION_SHIFT (13)   /* 2**13 = 8*KB = 16 pages */
#else
#  error FLASH geometry for this SAMV7 chip not known
#endif

/* Sizes and masks */

#define SAMV7_SECTOR_SIZE        (1 << SAMV7_SECTOR_SHIFT)
#define SAMV7_SECTOR_MASK        (SAMV7_SECTOR_SIZE - 1)

#define SAMV7_PAGE_SIZE          (1 << SAMV7_PAGE_SHIFT)
#define SAMV7_PAGE_WORDS         (1 << (SAMV7_PAGE_SHIFT - 2))
#define SAMV7_PAGE_MASK          (SAMV7_PAGE_SIZE - 1)

#define SAMV7_CLUSTER_SIZE       (1 << SAMV7_CLUSTER_SHIFT)
#define SAMV7_CLUSTER_MASK       (SAMV7_CLUSTER_SHIFT - 1)

/* Relationships */

#define SAMV7_PAGE2SEC_SHIFT     (SAMV7_SECTOR_SHIFT - SAMV7_PAGE_SHIFT)
#define SAMV7_PAGE_PER_SEC       (1 << SAMV7_PAGE2SEC_SHIFT)

#define SAMV7_PAGE2CLUST_SHIFT   (SAMV7_CLUSTER_SHIFT - SAMV7_PAGE_SHIFT)
#define SAMV7_PAGE_PER_CLUSTER   (1 << SAMV7_PAGE2CLUST_SHIFT)

#define SAMV7_CLUST2SECT_SHIFT   (SAMV7_SECTOR_SHIFT - SAMV7_CLUSTER_SHIFT)
#define SAMV7_CLUSTER_PER_SEC    (1 << SAMV7_CLUST2SECT_SHIFT)

/* Conversions */

#define SAMV7_BYTE2PAGE(o)       ((o) >> SAMV7_PAGE_SHIFT)
#define SAMV7_BYTE2CLUST(o)      ((o) >> SAMV7_CLUSTER_SHIFT)
#define SAMV7_BYTE2SECT(o)       ((o) >> SAMV7_SECTOR_SHIFT)

#define SAMV7_PAGE2BYTE(p)       ((p) << SAMV7_PAGE_SHIFT)
#define SAMV7_PAGE2CLUST(p)      ((p) >> SAMV7_PAGE2CLUST_SHIFT)
#define SAMV7_PAGE2SEC(p)        ((p) >> SAMV7_PAGE2SEC_SHIFT)

#define SAMV7_CLUST2BYTE(c)      ((c) << SAMV7_CLUSTER_SHIFT)
#define SAMV7_CLUST2PAGE(c)      ((c) << SAMV7_PAGE2CLUST_SHIFT)
#define SAMV7_CLUST2SEC(c)       ((c) >> SAMV7_CLUST2SECT_SHIFT)

#define SAMV7_SEC2BYTE(s)        ((s) << SAMV7_SECTOR_SHIFT)
#define SAMV7_SEC2PAGE(s)        ((s) << SAMV7_PAGE2SEC_SHIFT)
#define SAMV7_SEC2CLUST(s)       ((s) << SAMV7_CLUST2SECT_SHIFT)

/* Lock region */

#define SAMV7_LOCK_REGION_SIZE   (1 << SAMV7_LOCK_REGION_SHIFT)
#define SAMV7_LOCK_REGION_MASK   (SAMV7_LOCK_REGION_SIZE - 1)

/* Total FLASH sizes */

#define SAMV7_TOTAL_NSECTORS     (SAMV7_FLASH_SIZE >> SAMV7_SECTOR_SHIFT)
#define SAMV7_TOTAL_NPAGES       SAMV7_SEC2PAGE(SAMV7_TOTAL_NSECTORS)
#define SAMV7_TOTAL_NCLUSTERS    SAMV7_SEC2CLUST(SAMV7_TOTAL_NSECTORS)

/* Start and size of the programmable region  */

#define SAMV7_PROGMEM_NBYTES     (CONFIG_SAMV7_PROGMEM_NSECTORS << SAMV7_SECTOR_SHIFT)
#define SAMV7_PROGMEM_END        (SAM_INTFLASH_BASE + SAMV7_FLASH_SIZE)
#define SAMV7_PROGMEM_START      (SAMV7_PROGMEM_END - SAMV7_PROGMEM_NBYTES)

#define SAMV7_PROGMEM_NPAGES     SAMV7_SEC2PAGE(CONFIG_SAMV7_PROGMEM_NSECTORS)
#define SAMV7_PROGMEM_ENDPAGE    (SAMV7_TOTAL_NPAGES)
#define SAMV7_PROGMEM_STARTPAGE  (SAMV7_PROGMEM_ENDPAGE - SAMV7_PROGMEM_NPAGES)

#define SAMV7_PROGMEM_NCLUSTERS  SAMV7_SEC2CLUST(CONFIG_SAMV7_PROGMEM_NSECTORS)
#define SAMV7_PROGMEM_ENDCLUST   (SAMV7_TOTAL_NCLUSTERS)
#define SAMV7_PROGMEM_STARTCLUST (SAMV7_PROGMEM_ENDCLUST - SAMV7_PROGMEM_NCLUSTERS)

#define SAMV7_PROGMEM_NSECTORS   (CONFIG_SAMV7_PROGMEM_NSECTORS)
#define SAMV7_PROGMEM_ENDSEC     (SAMV7_TOTAL_NSECTORS)
#define SAMV7_PROGMEM_STARTSEC   (SAMV7_PROGMEM_ENDSEC - CONFIG_SAMV7_PROGMEM_NSECTORS)

/* Misc stuff */

#ifndef MIN
#  define MIN(a, b)              ((a) < (b) ? (a) : (b))
#endif

#ifndef MAX
#  define MAX(a, b)              ((a) > (b) ? (a) : (b))
#endif

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static uint32_t g_page_buffer[SAMV7_PAGE_WORDS];
static sem_t g_page_sem;

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: page_buffer_lock
 *
 * Description:
 *   Get exclusive access to the global page buffer
 *
 * Input Parameters:
 *   None
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void page_buffer_lock(void)
{
  while (sem_wait(&g_page_sem) < 0)
    {
      int errcode = errno;

      /* sem_wait should only fail if it was awakened by a signal */

      DEBUGASSERT(errcode == EINTR);
      if (errcode != EINTR)
        {
          break;
        }
    }
}

#define page_buffer_unlock() sem_post(&g_page_sem)

/****************************************************************************
 * Name: efc_command
 *
 * Description:
 *   Send a FLASH command
 *
 * Input Parameters:
 *   cmd - The FLASH command to be sent
 *   arg - The argument to accompany the command
 *
 * Returned Value:
 *   Zero is returned on success; a negated errno value is returned on any
 *   failure.
 *
 ****************************************************************************/

static int efc_command(uint32_t cmd, uint32_t arg)
{
  uint32_t regval;

  /* Write the command to the flash command register */

  regval = EEFC_FCR_FCMD(cmd) |  EEFC_FCR_FARG(arg) | EEFC_FCR_FKEY_PASSWD;
  putreg32(regval, SAM_EEFC_FCR);

  /* Wait for the FLASH to become ready again */

  do
    {
      regval = getreg32(SAM_EEFC_FSR);
    }
  while ((regval & EEFC_FSR_FRDY) == 0);

  /* Check for errors */

  if ((regval & (EEFC_FSR_FLOCKE | EEFC_FSR_FCMDE | EEFC_FSR_FLERR)) != 0)
    {
      return -EIO;
    }
  else
    {
      return OK;
    }
}

/****************************************************************************
 * Name: efc_lock
 *
 * Description:
 *   Lock a region of FLASH
 *
 * Input Parameters:
 *   page  - The first page to unlock
 *   npages - The number of consecutive pages to unlock
 *
 * Returned Value:
 *   Zero on success; a negated errno value on failure.
 *
 ****************************************************************************/

#if 0 /* Not used */
static int efc_lock(size_t page, size_t npages)
{
  size_t start_page;
  size_t end_page;
  size_t lockpage;
  int ret;

  /* Align the page to the lock region */

  end_page   = page + npages;
  start_page = page & SAMV7_LOCK_REGION_MASK;

  for (lockpage = start_page;
       lockpage < end_page;
       lockpage += SAMV7_LOCK_REGION_SIZE)
    {
      ret = efc_command(FCMD_SLB, lockpage);
      if (ret < 0)
        {
          return ret;
        }
    }

  return OK;
}
#endif

/****************************************************************************
 * Name: efc_unlock
 *
 * Description:
 *   Make sure that the FLASH is unlocked
 *
 * Input Parameters:
 *   page  - The first page to unlock
 *   npages - The number of consecutive pages to unlock
 *
 * Returned Value:
 *   Zero on success; a negated errno value on failure.
 *
 ****************************************************************************/

static int efc_unlock(size_t page, size_t npages)
{
  size_t start_page;
  size_t end_page;
  size_t lockpage;
  int ret;

  /* Align the page to the lock region */

  end_page   = page + npages;
  start_page = page & SAMV7_LOCK_REGION_MASK;

  for (lockpage = start_page;
       lockpage < end_page;
       lockpage += SAMV7_LOCK_REGION_SIZE)
    {
      ret = efc_command(FCMD_CLB, lockpage);
      if (ret < 0)
        {
          return ret;
        }
    }

  return OK;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: sam_progmem_initialize
 *
 * Description:
 *   Call to initialize FLASH programming memory access
 *
 * Input Parameters:
 *   None
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void sam_progmem_initialize(void)
{
  uint32_t regval;

  /* Make sure that the read interrupt is disabled */

  regval  = getreg32(SAM_EEFC_FMR);
  regval &= ~EEFC_FMR_FRDY;
  putreg32(regval, SAM_EEFC_FMR);

  /* Initialize the semaphore that manages exclusive access to the global
   * page buffer.
   */

  sem_init(&g_page_sem, 0, 1);
}

/****************************************************************************
 * Name: up_progmem_npages
 *
 * Description:
 *   Return number of clusters in the available FLASH memory.
 *
 ****************************************************************************/

size_t up_progmem_npages(void)
{
  return SAMV7_PROGMEM_NCLUSTERS;
}

/****************************************************************************
 * Name: up_progmem_isuniform
 *
 * Description:
 *   Cluster size is uniform?  Say 'yes' even though that is not strictly
 *   true do to the odd organization of sector 0.
 *
 ****************************************************************************/

bool up_progmem_isuniform(void)
{
  return true;
}

/****************************************************************************
 * Name: up_progmem_pagesize
 *
 * Description:
 *   Return cluster size
 *
 ****************************************************************************/

size_t up_progmem_pagesize(size_t cluster)
{
  return SAMV7_CLUSTER_SIZE;
}

/****************************************************************************
 * Name: up_progmem_getpage
 *
 * Description:
 *   Address to cluster conversion
 *
 * Input Parameters:
 *   address - Address with or without flash offset
 *
 * Returned Value:
 *   Page or negative value on error.  The following errors are reported
 *   (errno is not set!):
 *
 *     -EFAULT: On invalid address
 *
 ****************************************************************************/

ssize_t up_progmem_getpage(size_t address)
{
  if (address >= SAMV7_PROGMEM_START)
    {
      address -= SAMV7_PROGMEM_START;
    }

  if (address >= SAMV7_PROGMEM_NBYTES)
    {
      return -EFAULT;
    }

  return address >> SAMV7_CLUSTER_SHIFT;
}

/****************************************************************************
 * Name: up_progmem_getaddress
 *
 * Description:
 *   Cluster to address conversion
 *
 * Input Parameters:
 *   cluster - cluster index
 *
 * Returned Value:
 *   Base address of given cluster, maximum size if cluster index is not valid.
 *
 ****************************************************************************/

size_t up_progmem_getaddress(size_t cluster)
{
  if (cluster >= SAMV7_PROGMEM_NCLUSTERS)
    {
      return SAMV7_PROGMEM_NBYTES;
    }

  return (cluster << SAMV7_CLUSTER_SHIFT) + SAMV7_PROGMEM_START;
}

/****************************************************************************
 * Name: up_progmem_erasepage
 *
 * Description:
 *   Erase selected cluster.
 *
 * Input Parameters:
 *   cluster - cluster index to be erased
 *
 * Returned Value:
 *   Page size or negative value on error.  The following errors are reported
 *   (errno is not set!):
 *
 *     -EFAULT: On invalid cluster
 *     -EIO:    On unsuccessful erase
 *     -EROFS:  On access to write protected area
 *     -EACCES: Insufficient permissions (read/write protected)
 *     -EPERM:  If operation is not permitted due to some other constraints
 *              (i.e. some internal block is not running etc.)
 *
 ****************************************************************************/

ssize_t up_progmem_erasepage(size_t cluster)
{
  uint32_t page;
  uint32_t arg;
  int ret;

  if (cluster >= SAMV7_PROGMEM_NCLUSTERS)
    {
      return -EFAULT;
    }

  /* Get the page number of the start of the cluster */

  page = SAMV7_CLUST2PAGE((uint32_t)cluster) + SAMV7_PROGMEM_STARTPAGE;

  /* Erase all pages in the cluster */

  (void)efc_unlock(page, SAMV7_PAGE_PER_CLUSTER);

  /* Get FARG field for EPA command:
   *
   * The first page to be erased is specified in the FARG[15:2] field of
   * the EEFC_FCR register. The first page number must be modulo 4, 8,16 or
   * 32 according to the number of pages to erase at the same time.
   *
   * The 2 lowest bits of the FARG field define the number of pages to
   * be erased (FARG[1:0]).
   */

#if SAMV7_PAGE_PER_CLUSTER == 32
    arg   = page | 3;   /* Not valid for small 8 KB sectors */
#elif SAMV7_PAGE_PER_CLUSTER == 16
    arg   = page | 2;
#elif SAMV7_PAGE_PER_CLUSTER == 8
#  error Cluster size of 8 not suportted
    arg   = page | 1;   /* 0nly valid for small 8 KB sectors */
#elif SAMV7_PAGE_PER_CLUSTER == 4
#  error Cluster size of 4 not suportted
    arg   = page | 0;   /* 0nly valid for small 8 KB sectors */
#else
#  error Unsupported/undefined pages-per-cluster size
#endif

  ret = efc_command(FCMD_EPA, arg);
  if (ret < 0)
    {
      return ret;
    }

  /* Verify that the cluster of pages is really erased */

  if (up_progmem_ispageerased(cluster) == 0)
    {
      return SAMV7_CLUSTER_SIZE; /* Success */
    }
  else
    {
      return -EIO; /* Failure */
    }
}

/****************************************************************************
 * Name: up_progmem_ispageerased
 *
 * Description:
 *   Checks whether cluster is erased
 *
 * Input Parameters:
 *    cluster - cluster to be checked
 *
 * Returned Value:
 *   Returns number of bytes erased or negative value on error. If it
 *   returns zero then complete cluster is empty (erased).
 *
 *   The following errors are reported (errno is not set!)
 *     -EFAULT: On invalid cluster
 *
 ****************************************************************************/

ssize_t up_progmem_ispageerased(size_t cluster)
{
  size_t address;
  size_t nwritten;
  int nleft;

  if (cluster >= SAMV7_PROGMEM_NCLUSTERS)
    {
      return -EFAULT;
    }

  /* Flush and invalidate nvalidate D-Cache for this address range */

  address = (cluster << SAMV7_CLUSTER_SHIFT) + SAMV7_PROGMEM_START;
  arch_flush_dcache(address, address + SAMV7_CLUSTER_SIZE);

  /* Verify that the cluster is erased (i.e., all 0xff) */

  for (nleft = SAMV7_CLUSTER_SIZE, nwritten = 0;
       nleft > 0;
       nleft--, address++)
    {
      if (getreg8(address) != 0xff)
        {
          nwritten++;
        }
    }

  return nwritten;
}

/****************************************************************************
 * Name: up_progmem_write
 *
 * Description:
 *   Program data at given address
 *
 * Input Parameters:
 *   address - Address with or without flash offset
 *   buffer  - Pointer to buffer
 *   buflen   - Number of bytes to write
 *
 * Returned Value:
 *   Bytes written or negative value on error.  The following errors are
 *   reported (errno is not set!)
 *
 *     EINVAL: If buflen is not aligned with the flash boundaries (i.e.
 *             some MCU's require per half-word or even word access)
 *     EFAULT: On invalid address
 *     EIO:    On unsuccessful write
 *     EROFS:  On access to write protected area
 *     EACCES: Insufficient permissions (read/write protected)
 *     EPERM:  If operation is not permitted due to some other constraints
 *             (i.e. some internal block is not running etc.)
 *
 ****************************************************************************/

ssize_t up_progmem_write(size_t address, const void *buffer, size_t buflen)
{
  FAR uint32_t *dest;
  FAR const uint32_t *src;
  size_t written;
  size_t xfrsize;
  size_t offset;
  size_t page;
  size_t i;
  int ret;

  /* Convert the address into a FLASH byte offset, if necessary */

  offset = address;
  if (address >= SAMV7_PROGMEM_START)
    {
      /* Convert address to an offset relative to be beginning of the
       * writable FLASH region.
       */

      offset -= SAMV7_PROGMEM_START;
    }

  /* Check for valid address range */

  if ((offset + buflen) > SAMV7_PROGMEM_NBYTES)
    {
      return -EFAULT;
    }

  /* Get the page number corresponding to the flash offset and the byte
   * offset into the page.
   */

  page = SAMV7_BYTE2PAGE((uint32_t)offset) + SAMV7_PROGMEM_STARTPAGE;
  offset &= SAMV7_PAGE_MASK;

  /* Get exclusive access to the global page buffer */

  page_buffer_lock();

  /* Make sure that the FLASH is unlocked */

  (void)efc_unlock(page, SAMV7_BYTE2PAGE(buflen + SAMV7_PAGE_MASK));

  /* Loop until all of the data has been written */

  dest    = (FAR uint32_t *)address;
  written = 0;

  while (buflen > 0)
    {
      /* How much can we write into this page? */

      xfrsize = MIN((size_t)SAMV7_PAGE_SIZE - offset, buflen) ;

      /* Do we need to use the intermediate buffer? */

      if (offset == 0 && xfrsize == SAMV7_PAGE_SIZE)
        {
          /* No, we can take the data directly from the user buffer */

          src = (FAR const uint32_t *)buffer;
        }
      else
        {
          /* Yes, copy data into global page buffer */

          if (offset > 0)
            {
              memcpy(g_page_buffer, dest, offset);
            }

          memcpy((uint8_t *)g_page_buffer + offset, buffer, xfrsize);

          if (offset + xfrsize < SAMV7_PAGE_SIZE)
            {
              memcpy((uint8_t *)g_page_buffer + offset + xfrsize,
                     (const uint8_t *)dest + offset + xfrsize,
                     SAMV7_PAGE_SIZE - offset - xfrsize);
            }

          src = g_page_buffer;
        }

      /* Write the page */

      for (i = 0; i < (SAMV7_PAGE_SIZE / sizeof(uint32_t)); i++)
        {
          *dest++ = *src++; 
           ARM_DMB();
        }

      /* Flush the data cache to memory */

      arch_clean_dcache(address, address + SAMV7_PAGE_SIZE);

      /* Send the write command */

      ret = efc_command(FCMD_WP, page);
      if (ret >= 0)
        {
          written += xfrsize;
        }

      /* Adjust pointers and counts for the next time through the loop */

      address += xfrsize;
      dest     = (FAR uint32_t *)address;
      buffer   = (FAR void *)((uintptr_t)buffer + xfrsize);
      buflen  -= xfrsize;
      page++;
    }

  page_buffer_unlock();
  return written;
}
