/****************************************************************************
 * arch/arm/src/samd5e5/sam_progmem.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>

#include <string.h>
#include <semaphore.h>
#include <sys/param.h>
#include <assert.h>
#include <errno.h>

#include <nuttx/arch.h>
#include <nuttx/mutex.h>
#include <arch/samd5e5/chip.h>

#include "arm_internal.h"
#include "hardware/sam_memorymap.h"
#include "hardware/sam_nvmctrl.h"

#include "sam_progmem.h"

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

/* Configuration ************************************************************/

#ifndef CONFIG_SAMD5E5_PROGMEM_NSECTORS
#  error CONFIG_SAMD5E5_PROGMEM_NSECTORS is not defined
#endif

#define USE_WRITE_WQW
#define USE_UNLOCK
#define USE_LOCK

/* Chip dependencies */

#if defined(CONFIG_ARCH_CHIP_SAMD5X)
/* The page size is 512 bytes.  However, the smallest thing that can be
 * erased is 16 pages.  We will refer to this as a "cluster".
 */

#  define SAMD5E5_SECTOR_SHIFT      (15)   /* 2**15 = 32KB SAMD51J20 */
#  define SAMD5E5_PAGE_SHIFT        (9)    /* 2**9  = 512B  */
#  define SAMD5E5_CLUSTER_SHIFT     (13)   /* 2**13 = 8*KB = 16 pages */
#  define SAMD5E5_LOCK_REGION_SHIFT (15)   /* 2**15 = 32KB SAMD51J20 */
#else
#  error FLASH geometry for this SAMD5E5 chip not known
#endif

/* Sizes and masks */

#define SAMD5E5_SECTOR_SIZE        (1 << SAMD5E5_SECTOR_SHIFT)
#define SAMD5E5_SECTOR_MASK        (SAMD5E5_SECTOR_SIZE - 1)

#define SAMD5E5_PAGE_SIZE          (1 << SAMD5E5_PAGE_SHIFT)
#define SAMD5E5_PAGE_WORDS         (1 << (SAMD5E5_PAGE_SHIFT - 2))
#define SAMD5E5_PAGE_MASK          (SAMD5E5_PAGE_SIZE - 1)

#define SAMD5E5_CLUSTER_SIZE       (1 << SAMD5E5_CLUSTER_SHIFT)
#define SAMD5E5_CLUSTER_MASK       (SAMD5E5_CLUSTER_SHIFT - 1)

/* Relationships */

#define SAMD5E5_PAGE2SEC_SHIFT     (SAMD5E5_SECTOR_SHIFT - SAMD5E5_PAGE_SHIFT)
#define SAMD5E5_PAGE_PER_SEC       (1 << SAMD5E5_PAGE2SEC_SHIFT)

#define SAMD5E5_PAGE2CLUST_SHIFT   (SAMD5E5_CLUSTER_SHIFT - SAMD5E5_PAGE_SHIFT)
#define SAMD5E5_PAGE_PER_CLUSTER   (1 << SAMD5E5_PAGE2CLUST_SHIFT)

#define SAMD5E5_CLUST2SECT_SHIFT   (SAMD5E5_SECTOR_SHIFT - SAMD5E5_CLUSTER_SHIFT)
#define SAMD5E5_CLUSTER_PER_SEC    (1 << SAMD5E5_CLUST2SECT_SHIFT)

/* Conversions */

#define SAMD5E5_BYTE2PAGE(o)       ((o) >> SAMD5E5_PAGE_SHIFT)
#define SAMD5E5_BYTE2CLUST(o)      ((o) >> SAMD5E5_CLUSTER_SHIFT)
#define SAMD5E5_BYTE2SECT(o)       ((o) >> SAMD5E5_SECTOR_SHIFT)

#define SAMD5E5_PAGE2BYTE(p)       ((p) << SAMD5E5_PAGE_SHIFT)
#define SAMD5E5_PAGE2CLUST(p)      ((p) >> SAMD5E5_PAGE2CLUST_SHIFT)
#define SAMD5E5_PAGE2SEC(p)        ((p) >> SAMD5E5_PAGE2SEC_SHIFT)

#define SAMD5E5_CLUST2BYTE(c)      ((c) << SAMD5E5_CLUSTER_SHIFT)
#define SAMD5E5_CLUST2PAGE(c)      ((c) << SAMD5E5_PAGE2CLUST_SHIFT)
#define SAMD5E5_CLUST2SEC(c)       ((c) >> SAMD5E5_CLUST2SECT_SHIFT)

#define SAMD5E5_SEC2BYTE(s)        ((s) << SAMD5E5_SECTOR_SHIFT)
#define SAMD5E5_SEC2PAGE(s)        ((s) << SAMD5E5_PAGE2SEC_SHIFT)
#define SAMD5E5_SEC2CLUST(s)       ((s) << SAMD5E5_CLUST2SECT_SHIFT)

/* Lock region */

#define SAMD5E5_LOCK_REGION_SIZE   (1 << SAMD5E5_LOCK_REGION_SHIFT)
#define SAMD5E5_LOCK_REGION_MASK   (SAMD5E5_LOCK_REGION_SIZE - 1)

/* Total FLASH sizes */

#define SAMD5E5_TOTAL_NSECTORS     (SAMD5E5_FLASH_SIZE >> SAMD5E5_SECTOR_SHIFT)
#define SAMD5E5_TOTAL_NPAGES       SAMD5E5_SEC2PAGE(SAMD5E5_TOTAL_NSECTORS)
#define SAMD5E5_TOTAL_NCLUSTERS    SAMD5E5_SEC2CLUST(SAMD5E5_TOTAL_NSECTORS)

/* Start and size of the programmable region  */

#define SAMD5E5_PROGMEM_NBYTES     (CONFIG_SAMD5E5_PROGMEM_NSECTORS << SAMD5E5_SECTOR_SHIFT)
#define SAMD5E5_PROGMEM_END        (SAM_FLASH_BASE + SAMD5E5_FLASH_SIZE)
#define SAMD5E5_PROGMEM_START      (SAMD5E5_PROGMEM_END - SAMD5E5_PROGMEM_NBYTES)

#define SAMD5E5_PROGMEM_NPAGES     SAMD5E5_SEC2PAGE(CONFIG_SAMD5E5_PROGMEM_NSECTORS)
#define SAMD5E5_PROGMEM_ENDPAGE    (SAMD5E5_TOTAL_NPAGES)
#define SAMD5E5_PROGMEM_STARTPAGE  (SAMD5E5_PROGMEM_ENDPAGE - SAMD5E5_PROGMEM_NPAGES)

#define SAMD5E5_PROGMEM_NCLUSTERS  SAMD5E5_SEC2CLUST(CONFIG_SAMD5E5_PROGMEM_NSECTORS)
#define SAMD5E5_PROGMEM_ENDCLUST   (SAMD5E5_TOTAL_NCLUSTERS)
#define SAMD5E5_PROGMEM_STARTCLUST (SAMD5E5_PROGMEM_ENDCLUST - SAMD5E5_PROGMEM_NCLUSTERS)

#define SAMD5E5_PROGMEM_NSECTORS   (CONFIG_SAMD5E5_PROGMEM_NSECTORS)
#define SAMD5E5_PROGMEM_ENDSEC     (SAMD5E5_TOTAL_NSECTORS)
#define SAMD5E5_PROGMEM_STARTSEC   (SAMD5E5_PROGMEM_ENDSEC - CONFIG_SAMD5E5_PROGMEM_NSECTORS)

#define SAMD5E5_PROGMEM_ERASEDVAL  (0xffu)

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

static uint32_t g_page_buffer[SAMD5E5_PAGE_WORDS];
static mutex_t g_page_lock = NXMUTEX_INITIALIZER;

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

/****************************************************************************
 * Name: nvm_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 nvm_command(uint16_t cmd, uint32_t arg)
{
  uint16_t regval;

  while (!(getreg16(SAM_NVMCTRL_STATUS) & NVMCTRL_STATUS_READY))
    {
      /* Wait until this module isn't busy */
    }

  /* Check for errors */

  regval = getreg16(SAM_NVMCTRL_INTFLAG);
  if ((regval & (NVMCTRL_INT_LOCKE |
                 NVMCTRL_INT_PROGE |
                 NVMCTRL_INT_ADDRE)) != 0)
    {
      ferr("ERROR: cmd=0x%x regval=0x%x\n", cmd, regval);
      return -EIO;
    }

  putreg16(NVMCTRL_INT_DONE, SAM_NVMCTRL_INTFLAG);

  /* Set address */

  if (arg)
    putreg32(arg, SAM_NVMCTRL_ADDR);

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

  putreg16(cmd | NVMCTRL_CTRLB_CMDEX_KEY, SAM_NVMCTRL_CTRLB);

  while (!(getreg16(SAM_NVMCTRL_STATUS) & NVMCTRL_STATUS_READY))
    {
      /* Wait until this module isn't busy */
    }

  return OK;
}

#ifdef USE_UNLOCK
/****************************************************************************
 * Name: nvm_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 nvm_unlock(size_t page, size_t npages)
{
  size_t start_region;
  size_t end_region;
  size_t unlockregion;
  int ret;

  /* Align the page to the unlock region */

  end_region   = SAMD5E5_PAGE2BYTE(page + npages) &
                ~SAMD5E5_LOCK_REGION_MASK;
  start_region = SAMD5E5_PAGE2BYTE(page) &
                ~SAMD5E5_LOCK_REGION_MASK;
  unlockregion = start_region;

  do
    {
      finfo("INFO: unlock region=%d address=0x%x\n",
        unlockregion >> SAMD5E5_LOCK_REGION_SHIFT, unlockregion);
      ret = nvm_command(NVMCTRL_CTRLB_CMD_UR, unlockregion);
      if (ret < 0)
        {
          return ret;
        }

      unlockregion += SAMD5E5_LOCK_REGION_SIZE;
    }
  while (unlockregion < end_region);

  return OK;
}
#endif

#ifdef USE_LOCK

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

static int nvm_lock(size_t page, size_t npages)
{
  size_t start_region;
  size_t end_region;
  size_t lockregion;
  int ret;

  /* Align the page to the unlock region */

  end_region   = SAMD5E5_PAGE2BYTE(page + npages) &
                ~SAMD5E5_LOCK_REGION_MASK;
  start_region = SAMD5E5_PAGE2BYTE(page) &
                ~SAMD5E5_LOCK_REGION_MASK;
  lockregion = start_region;

  do
    {
      finfo("INFO: lock region=%d address=0x%x\n",
        lockregion >> SAMD5E5_LOCK_REGION_SHIFT, lockregion);
      ret = nvm_command(NVMCTRL_CTRLB_CMD_LR, lockregion);
      if (ret < 0)
        {
          return ret;
        }

      lockregion += SAMD5E5_LOCK_REGION_SIZE;
    }
  while (lockregion < end_region);

  return OK;
}
#endif

/****************************************************************************
 * 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)
{
  uint16_t ctrla;

  ctrla = getreg16(SAM_NVMCTRL_CTRLA);
  ctrla &= ~(NVMCTRL_CTRLA_AHBNS1    |
            NVMCTRL_CTRLA_AHBNS0     |
            NVMCTRL_CTRLA_PRM_MASK   |
            NVMCTRL_CTRLA_WMODE_MASK |
            NVMCTRL_CTRLA_SUSPEN);

  ctrla |= NVMCTRL_CTRLA_CACHEDIS1  |
           NVMCTRL_CTRLA_CACHEDIS0  |
           NVMCTRL_CTRLA_PRM_MANUAL |
           NVMCTRL_CTRLA_WMODE_MAN  |
           NVMCTRL_CTRLA_AUTOWS;
  putreg16(ctrla, SAM_NVMCTRL_CTRLA);
}

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

size_t up_progmem_neraseblocks(void)
{
  return SAMD5E5_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 page size
 *
 ****************************************************************************/

size_t up_progmem_pagesize(size_t cluster)
{
  return SAMD5E5_PAGE_SIZE;
}

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

size_t up_progmem_erasesize(size_t cluster)
{
  return SAMD5E5_CLUSTER_SIZE;
}

/****************************************************************************
 * Name: up_progmem_getpage
 *
 * Description:
 *   Address to page 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 >= SAMD5E5_PROGMEM_START)
    {
      address -= SAMD5E5_PROGMEM_START;
    }

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

  return address >> SAMD5E5_PAGE_SHIFT;
}

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

size_t up_progmem_getaddress(size_t page)
{
  if (page >= SAMD5E5_PROGMEM_NPAGES)
    {
      return SAMD5E5_PROGMEM_NBYTES;
    }

  return (page << SAMD5E5_PAGE_SHIFT) + SAMD5E5_PROGMEM_START;
}

/****************************************************************************
 * Name: up_progmem_eraseblock
 *
 * 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_eraseblock(size_t cluster)
{
  uint32_t page;
  int ret;

  finfo("INFO: cluster=%d\n", cluster);
  if (cluster >= SAMD5E5_PROGMEM_NCLUSTERS)
    {
      return -EFAULT;
    }

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

  page = SAMD5E5_CLUST2PAGE((uint32_t)cluster) + SAMD5E5_PROGMEM_STARTPAGE;

  /* Erase all pages in the cluster */

#ifdef USE_UNLOCK
  nvm_unlock(page, SAMD5E5_PAGE_PER_CLUSTER);
#endif

  finfo("INFO: erase block=%d address=0x%x\n",
    page, SAMD5E5_PAGE2BYTE(page));
  ret = nvm_command(NVMCTRL_CTRLB_CMD_EB, SAMD5E5_PAGE2BYTE(page));

#ifdef USE_LOCK
  nvm_lock(page, SAMD5E5_PAGE_PER_CLUSTER);
#endif

  if (ret < 0)
    {
      return ret;
    }

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

  if (up_progmem_ispageerased(cluster) == 0)
    {
      return SAMD5E5_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;

  finfo("INFO: cluster=%d\n", cluster);
  if (cluster >= SAMD5E5_PROGMEM_NCLUSTERS)
    {
      return -EFAULT;
    }

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

  address = (cluster << SAMD5E5_CLUSTER_SHIFT) + SAMD5E5_PROGMEM_START;
  up_flush_dcache(address, address + SAMD5E5_CLUSTER_SIZE);

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

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

  if (nwritten)
    fwarn("WARN: non written=%d\n", 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)
{
  irqstate_t flags;
  uint32_t *dest;
  const uint32_t *src;
  size_t written;
  size_t xfrsize;
  size_t offset;
  size_t page;
  size_t i;
  int ret;
#ifdef USE_UNLOCK
  size_t lock;
  size_t locksize;
#endif

  finfo("INFO: address=0x%x buflen=%d\n", address, buflen);

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

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

      offset -= SAMD5E5_PROGMEM_START;
    }

  /* Check for valid address range */

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

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

  nxmutex_lock(&g_page_lock);

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

  page = SAMD5E5_BYTE2PAGE((uint32_t)offset) + SAMD5E5_PROGMEM_STARTPAGE;
  offset &= SAMD5E5_PAGE_MASK;

#ifdef USE_UNLOCK /* Make sure that the FLASH is unlocked */
  lock = page;
  locksize = SAMD5E5_BYTE2PAGE(buflen);
  nvm_unlock(lock, locksize);
#endif

  flags = enter_critical_section();

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

  dest = (uint32_t *)(address & ~SAMD5E5_PAGE_MASK);
  written = 0;
  while (buflen > 0)
    {
      /* How much can we write into this page? */

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

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

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

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

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

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

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

          src = g_page_buffer;
        }

#ifdef USE_WRITE_WQW
      if (xfrsize <= (0x10 - (offset & 0xf)))
        {
          /* Write the page buffer */

          dest += (offset & ~0xf) >> 2;
          src += (offset & ~0xf) >> 2;

          /* Dump flash data */

          for (i = 0; i < 4; i++)
            {
              finfo("INFO: dest=%p write 0x%x over 0x%x\n",
                dest + i, *(src + i), *(dest + i));
            }

          nvm_command(NVMCTRL_CTRLB_CMD_PBC, 0);

          /* Write the page buffer */

          for (i = 0; i < 4; i++)
            {
              *dest++ = *src++;
            }

          /* Flush the data cache to memory */

          up_clean_dcache(address & ~SAMD5E5_PAGE_MASK,
            (address & ~SAMD5E5_PAGE_MASK) + SAMD5E5_PAGE_SIZE);

          /* Send the 4 words write command */

          ret = nvm_command(NVMCTRL_CTRLB_CMD_WQW, 0);
          if (ret >= 0)
            {
              written += xfrsize;
            }

          dest -= i;
          src -= i;

          /* Compare page data */

          for (i = 0; i < 4; i++)
            {
              if (*dest != *src)
                {
                  fwarn("WQW dest=%p (dest 0x%x != src 0x%x) address=0x%x",
                    dest, *dest, *src, address);
                  fwarn("offset=0x%x xfrsize=%d buflen=%d ECCERR=0x%x\n",
                    offset, xfrsize, buflen, getreg32(SAM_NVMCTRL_ECCERR));
                }

              dest++;
              src++;
            }
        }
      else
        {
#endif
          nvm_command(NVMCTRL_CTRLB_CMD_PBC, 0);

          /* Write the page buffer */

          for (i = 0; i < SAMD5E5_PAGE_WORDS; i++)
            {
              *dest++ = *src++;
            }

          /* Flush the data cache to memory */

          up_clean_dcache(address & ~SAMD5E5_PAGE_MASK,
            (address & ~SAMD5E5_PAGE_MASK) + SAMD5E5_PAGE_SIZE);

          /* Send the write command */

          finfo("INFO: WP address=0x%x\n", address & ~SAMD5E5_PAGE_MASK);
          ret = nvm_command(NVMCTRL_CTRLB_CMD_WP, 0);
          if (ret >= 0)
            {
              written += xfrsize;
            }

          dest -= i;
          src -= i;

          /* Compare page data */

          for (i = 0; i < SAMD5E5_PAGE_WORDS; i++)
            {
              if (*dest != *src)
                {
                  fwarn("WQW dest=%p (dest 0x%x != src 0x%x) address=0x%x",
                      dest, *dest, *src, address);
                  fwarn("offset=0x%x xfrsize=%d buflen=%d ECCERR=0x%x\n",
                      offset, xfrsize, buflen, getreg32(SAM_NVMCTRL_ECCERR));
                }

              dest++;
              src++;
            }

#ifdef USE_WRITE_WQW
        }
#endif

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

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

#ifdef USE_LOCK
  nvm_lock(lock, locksize);
#endif

  leave_critical_section(flags);
  nxmutex_unlock(&g_page_lock);
  return written;
}

/****************************************************************************
 * Name: up_progmem_erasestate
 *
 * Description:
 *   Return value of erase state.
 *
 ****************************************************************************/

uint8_t up_progmem_erasestate(void)
{
  return SAMD5E5_PROGMEM_ERASEDVAL;
}

/****************************************************************************
 *  The NVM User Row contains calibration data that are
 *  automatically read at device power on.
 *  The NVM User Row can be read at address 0x804000.
 ****************************************************************************/

#ifndef _NVM_USER_ROW_BASE
#define _NVM_USER_ROW_BASE 0x804000
#endif
#define _NVM_USER_ROW_N_BITS 96
#define _NVM_USER_ROW_N_BYTES (_NVM_USER_ROW_N_BITS / 8)
#define _NVM_USER_ROW_END (((uint8_t *)_NVM_USER_ROW_BASE) + _NVM_USER_ROW_N_BYTES - 1)
#define _IS_NVM_USER_ROW(b)                                                                                            \
  (((uint8_t *)(b) >= (uint8_t *)(_NVM_USER_ROW_BASE)) && ((uint8_t *)(b) <= (uint8_t *)(_NVM_USER_ROW_END)))
#define _IN_NVM_USER_ROW(b, o) (((uint8_t *)(b) + (o)) <= (uint8_t *)(_NVM_USER_ROW_END))

#define _NVM_USER_PAGE_SIZE 512
#define _NVM_USER_PAGE_OFFSET 32
#define _IS_NVM_USER_PAGE(b)                                                                                            \
  (((uint8_t *)(b) >= (uint8_t *)(_NVM_USER_ROW_BASE)) && ((uint8_t *)(b) <= (((uint8_t *)_NVM_USER_ROW_BASE) + _NVM_USER_PAGE_SIZE - 1)))

/****************************************************************************
 *  The NVM Software Calibration Area can be read at address 0x00800080.
 *  The NVM Software Calibration Area can not be written.
 ****************************************************************************/

#ifndef _NVM_SW_CALIB_AREA_BASE
#define _NVM_SW_CALIB_AREA_BASE 0x00800080
#endif
#define _NVM_SW_CALIB_AREA_N_BITS 45
#define _NVM_SW_CALIB_AREA_N_BYTES (_NVM_SW_CALIB_AREA_N_BITS / 8)
#define _NVM_SW_CALIB_AREA_END (((uint8_t *)_NVM_SW_CALIB_AREA_BASE) + _NVM_SW_CALIB_AREA_N_BYTES - 1)
#define _IS_NVM_SW_CALIB_AREA(b)                                                                                       \
  (((uint8_t *)(b) >= (uint8_t *)_NVM_SW_CALIB_AREA_BASE) && ((uint8_t *)(b) <= (uint8_t *)_NVM_SW_CALIB_AREA_END))
#define _IN_NVM_SW_CALIB_AREA(b, o) (((uint8_t *)(b) + (o)) <= (uint8_t *)(_NVM_SW_CALIB_AREA_END))

ssize_t up_progmem_writeuserpage(const uint32_t offset,
                                 const uint8_t *buffer,
                                 uint16_t count)
{
  size_t i;
  size_t written;
  uint32_t *dest;
  const uint32_t *src;
  uint32_t userpage[128]; /* Copy of user page */

  ASSERT(buffer);

  /* Parameter check. */

  if (!_IS_NVM_USER_PAGE(_NVM_USER_ROW_BASE + offset))
    {
      return -EFAULT;
    }

  /* Cut off if request too many bytes */

  if (!_IS_NVM_USER_PAGE(_NVM_USER_ROW_BASE + offset + count - 1))
    {
      return -EFAULT;
    }

  /* Store previous data. */

  memcpy((uint8_t *)userpage,
        ((uint8_t *)_NVM_USER_ROW_BASE),
        _NVM_USER_PAGE_SIZE);

  /* Modify with buffer data. */

  memcpy((uint8_t *)userpage + offset, buffer, count);

  /* Erase AUX page. */

  nvm_command(NVMCTRL_CTRLB_CMD_EP, _NVM_USER_ROW_BASE);

  dest = (uint32_t *)(_NVM_USER_ROW_BASE);
  src = (const uint32_t *)userpage;
  for (written = 0; written <
    _NVM_USER_PAGE_SIZE; written += 4*sizeof(uint32_t))
    {
      /* Page buffer clear & write. */

      nvm_command(NVMCTRL_CTRLB_CMD_PBC, 0);

      /* Write the page buffer */

      for (i = 0; i < 4; i++)
        {
          *dest++ = *src++;
        }

      /* Send the 4 words write command */

      nvm_command(NVMCTRL_CTRLB_CMD_WQW, 0);

      dest -= i;
      src -= i;

      /* Compare page data */

      for (i = 0; i < 4; i++)
        {
          if (*dest != *src)
            {
              fwarn("WQW dest=%p (dest 0x%x != src 0x%x) ECCERR=0x%x\n",
                dest, *dest, *src, getreg32(SAM_NVMCTRL_ECCERR));
            }

          dest++;
          src++;
        }
    }

  return OK;
}

ssize_t up_progmem_readuserpage(const uint32_t offset,
                                uint8_t *buffer,
                                uint16_t count)
{
  ASSERT(buffer);

  /* Parameter check. */

  if (!_IS_NVM_USER_PAGE(_NVM_USER_ROW_BASE + offset))
    {
      return -EFAULT;
    }

  /* Cut off if request too many bytes */

  if (!_IS_NVM_USER_PAGE(_NVM_USER_ROW_BASE + offset + count - 1))
    {
      return -EFAULT;
    }

  /* Copy data */

  memcpy(buffer, ((uint8_t *)_NVM_USER_ROW_BASE) + offset, count);

  return OK;
}