/****************************************************************************
 * drivers/mtd/cfi.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 <sys/types.h>
#include <sys/param.h>
#include <stdint.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/fs/ioctl.h>
#include <nuttx/mtd/mtd.h>
#include <nuttx/fs/fs.h>
#include <nuttx/arch.h>
#include <nuttx/nuttx.h>

#include "cfi.h"

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

/* type of port */

#define CFI_PORT_8BIT                      0x01
#define CFI_PORT_16BIT                     0x02
#define CFI_PORT_32BIT                     0X04
#define CFI_PORT_64BIT                     0x08

/* Common CFI value */

#define CFI_CMD                            0x98
#define CFI_QRY_OFFSET                     0x10
#define CFI_ERASE_BLOCK_REGION_INFO_OFFSET 0x2D

/* CFI command set number */

#define CFI_CMDSET_INTEL_EXTENDED          0x0001
#define CFI_CMDSET_AMD_STANDARD            0x0002
#define CFI_CMDSET_INTEL_STANDARD          0x0003
#define CFI_CMDSET_AMD_EXTENDED            0x0004
#define CFI_CMDSET_MITS_STANDARD           0x0100
#define CFI_CMDSET_MITS_EXTENDED           0x0101

/* Intel command value */

#define CFI_INTEL_CMD_RESET                0xFF
#define CFI_INTEL_CMD_CLEAR_STATUS         0x50
#define CFI_INTEL_CMD_BLOCK_ERASE          0x20
#define CFI_INTEL_CMD_ERASE_CONFIRM        0xD0
#define CFI_INTEL_CMD_WRITE                0x40
#define CFI_INTEL_CMD_WRITE_BUFFER         0xE8
#define CFI_INTEL_CMD_WRITE_CONFIRM        0xD0

/* Intel status value */

#define CFI_INTEL_STATUS_DONE              0x80
#define CFI_INTEL_STATUS_ESS               0x40 /* erase suspended */
#define CFI_INTEL_STATUS_ECLBS             0x20 /* clear block lock bit
                                                 * or erase error */
#define CFI_INTEL_STATUS_PSLBS             0X10 /* write or block lock bit
                                                 * set error */
#define CFI_INTEL_STATUS_VPENS             0x08 /* voltage range error */
#define CFI_INTEL_STATUS_PSS               0x04 /* write is suspended */
#define CFI_INTEL_STATUS_DPS               0x02 /* device protected error */

/* Amd command value */

#define CFI_AMD_CMD_RESET                  0xF0
#define CFI_AMD_CMD_UNLOCK1                0xAA
#define CFI_AMD_CMD_UNLOCK2                0x55
#define CFI_AMD_CMD_ERASE_START            0x80
#define CFI_AMD_CMD_ERASE_SECTOR           0x30
#define CFI_AMD_CMD_WRITE                  0xA0
#define CFI_AMD_CMD_WRITE_BUFFER           0x25
#define CFI_AMD_CMD_WRITE_CONFIRM          0x29

/* Amd status value */

#define CFI_AMD_STATUS_TOGGLE              0x40 /* DQ6 will toggle after a
                                                 * reading cycle during
                                                 * erasing */

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* Supports up to 64bits bank width */

typedef uint64_t cfiword_t;

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* the offset of CFI query address is usually 0x55
 * Spansion S29WS-N CFI query fix is to try 0x555 if 0x55 fails
 */

static const uint32_t g_cfi_query_address[2] =
{
  0x55, 0x555
};

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

/****************************************************************************
 * Name: cfi_get_block_addr
 *
 * Description:
 *   Get the start address of the block.
 *
 ****************************************************************************/

static uintptr_t cfi_get_block_addr(FAR struct cfi_dev_s *cfi,
                                    blkcnt_t block)
{
  uint8_t i;
  uintptr_t base = cfi->base_addr;

  for (i = 0; i < cfi->info.erase_region_num; i++)
    {
      blkcnt_t block_num = cfi_get_blocknum(cfi, i);
      size_t block_size = cfi_get_blocksize(cfi, i);

      if (block >= block_num)
        {
          block -= block_num;
          base += block_num * block_size;
          continue;
        }

      base += block * block_size;
      break;
    }

  return base;
}

/****************************************************************************
 * Name: cfi_addr_map
 *
 * Description:
 *   Get the address according to block number and offset, and needs to
 *   convert it according to bankwidth.
 *
 ****************************************************************************/

static uintptr_t cfi_addr_map(FAR struct cfi_dev_s *cfi, blkcnt_t block,
                              off_t offset)
{
  return cfi_get_block_addr(cfi, block) + cfi->bankwidth * offset;
}

/****************************************************************************
 * Name: cfi_make_cmd
 *
 * Description:
 *   Make command value according to bankwidth and device width. For example,
 *   a command 0x98 should be 0x00980098 for a flash with bankwidth = 4 and
 *   dev_width = 2.
 *
 ****************************************************************************/

static cfiword_t cfi_make_cmd(FAR struct cfi_dev_s *cfi, uint32_t cmd)
{
  cfiword_t result = 0;
  uint8_t i = cfi->bankwidth / cfi->dev_width;

  while (i--)
    {
      result = (result << (8 * cfi->dev_width)) | cmd;
    }

  return result;
}

/****************************************************************************
 * Name: cfi_add_byte
 *
 * Description:
 *   Add a byte to cfiword, which will be written into cfi-flash later.
 *
 ****************************************************************************/

static void cfi_add_byte(FAR struct cfi_dev_s *cfi, FAR cfiword_t *word,
                         uint8_t c)
{
  switch (cfi->bankwidth)
    {
      case CFI_PORT_8BIT:
        *word = c;
        break;
      case CFI_PORT_16BIT:
        *word = (*word >> 8) | (uint16_t)c << 8;
        break;
      case CFI_PORT_32BIT:
        *word = (*word >> 8) | (uint32_t)c << 24;
        break;
      default:
        *word = (*word >> 8) | (uint64_t)c << 56;
    }
}

/****************************************************************************
 * Name: cfi_write_data
 *
 * Description:
 *   Write a value to specified address.
 *
 ****************************************************************************/

static inline void cfi_write_data(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                                  FAR const void *buffer)
{
  switch (cfi->bankwidth)
    {
      case CFI_PORT_8BIT:
        *(FAR volatile uint8_t *)addr = *(FAR const uint8_t *)buffer;
        break;
      case CFI_PORT_16BIT:
        *(FAR volatile uint16_t *)addr = *(FAR const uint16_t *)buffer;
        break;
      case CFI_PORT_32BIT:
        *(FAR volatile uint32_t *)addr = *(FAR const uint32_t *)buffer;
        break;
      default:
        *(FAR volatile uint64_t *)addr = *(FAR const uint64_t *)buffer;
    }
}

/****************************************************************************
 * Name: cfi_write_block_cmd
 *
 * Description:
 *   Write a command about specified block.
 *
 ****************************************************************************/

static void cfi_write_block_cmd(FAR struct cfi_dev_s *cfi, blkcnt_t block,
                                uint32_t cmd)
{
  cfiword_t val = cfi_make_cmd(cfi, cmd);

  cfi_write_data(cfi, cfi_addr_map(cfi, block, 0), &val);
}

/****************************************************************************
 * Name: cfi_write_address_cmd
 *
 * Description:
 *   Write a command to specified address.
 *
 ****************************************************************************/

static void cfi_write_address_cmd(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                                  uint8_t cmd)
{
  cfiword_t val = cfi_make_cmd(cfi, cmd);

  cfi_write_data(cfi, cfi_addr_map(cfi, 0, addr), &val);
}

/****************************************************************************
 * Name: cfi_read_byte
 *
 * Description:
 *   Read an 8-bit value from addr.
 *
 ****************************************************************************/

static inline uint8_t cfi_read_byte(uintptr_t addr)
{
  return *(FAR volatile uint8_t *)addr;
}

/****************************************************************************
 * Name: cfi_read_data
 *
 * Description:
 *   Read a value to specified address.
 *
 ****************************************************************************/

static inline cfiword_t cfi_read_data(FAR struct cfi_dev_s *cfi,
                                      uintptr_t addr)
{
  switch (cfi->bankwidth)
    {
      case CFI_PORT_8BIT:
        return cfi_read_byte(addr);
      case CFI_PORT_16BIT:
        return *(FAR const volatile uint16_t *)addr;
      case CFI_PORT_32BIT:
        return *(FAR const volatile uint32_t *)addr;
      default:
        return *(FAR const volatile uint64_t *)addr;
    }
}

/****************************************************************************
 * Name: cfi_is_equal
 *
 * Description:
 *   Check whether the value corresponding to addr is equal to cmd.
 *
 ****************************************************************************/

static bool cfi_is_equal(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                         uint8_t cmd)
{
  cfiword_t val = cfi_make_cmd(cfi, cmd);

  addr = cfi_addr_map(cfi, 0, addr);
  return cfi_read_data(cfi, addr) == val;
}

/****************************************************************************
 * Name: cfi_is_set
 *
 * Description:
 *   Check the status bit, which is set by cmd.
 *
 ****************************************************************************/

static bool cfi_is_set(FAR struct cfi_dev_s *cfi, uint8_t cmd)
{
  cfiword_t val = cfi_make_cmd(cfi, cmd);
  uintptr_t addr = cfi->base_addr;

  return (cfi_read_data(cfi, addr) & val) == val;
}

/****************************************************************************
 * Name: cfi_get_time_us
 *
 * Description:
 *   Check whether the value corresponding to offset is equal to cmd.
 *
 ****************************************************************************/

static inline uint32_t cfi_get_time_us(void)
{
  struct timespec ts;

  clock_systime_timespec(&ts);
  return 1000000 * ts.tv_sec + ts.tv_nsec / 1000;
}

/****************************************************************************
 * Name: cfi_is_timeout
 *
 * Description:
 *   Check whether the operation has timed out.
 *
 ****************************************************************************/

static bool cfi_is_timeout(uint32_t start_us, uint32_t tout_us)
{
  return (int32_t)(cfi_get_time_us() - start_us) >= tout_us;
}

/****************************************************************************
 * Name: cfi_intel_is_busy
 *
 * Description:
 *   Function of intel cfi command set.
 *   Check whether the cfi-flash is busy.
 *
 ****************************************************************************/

static bool cfi_intel_is_busy(FAR struct cfi_dev_s *cfi, blkcnt_t block)
{
  return !cfi_is_set(cfi, CFI_INTEL_STATUS_DONE);
}

/****************************************************************************
 * Name: cfi_intel_status_check
 *
 * Description:
 *   Function of intel cfi command set.
 *   Check the status register of the cfi-flash.
 *
 ****************************************************************************/

static int cfi_intel_status_check(FAR struct cfi_dev_s *cfi,
                                  blkcnt_t block, uint32_t tout)
{
  uint32_t start;

  /* wait for command completion */

  start = cfi_get_time_us();
  while (cfi_intel_is_busy(cfi, block))
    {
      if (!cfi_is_timeout(start, tout))
        {
          up_udelay(1);
          continue;
        }

      ferr("CFI flash operation has timed out.\n");

      /* Check each status bit */

      if (!cfi_is_equal(cfi, 0, CFI_INTEL_STATUS_DONE))
        {
          ferr("CFI flash operation failed.\n");
          if (cfi_is_set(cfi, CFI_INTEL_STATUS_ECLBS |
                         CFI_INTEL_STATUS_PSLBS))
            {
             ferr("CFI: command sequence error\n");
            }
          else if (cfi_is_set(cfi, CFI_INTEL_STATUS_ECLBS))
            {
             ferr("CFI: block erase error!\n");
            }
          else if (cfi_is_set(cfi, CFI_INTEL_STATUS_PSLBS))
            {
             ferr("CFI: locking error!\n");
            }
          else if (cfi_is_set(cfi, CFI_INTEL_STATUS_DPS))
            {
             ferr("CFI: block locked!\n");
            }
          else if (cfi_is_set(cfi, CFI_INTEL_STATUS_VPENS))
            {
             ferr("CFI: Vpp Low error!\n");
            }
        }

      cfi_reset(cfi);
      return -EAGAIN;
    }

  return 0;
}

/****************************************************************************
 * Name: cfi_intel_write_word
 *
 * Description:
 *   Function of intel cfi command set.
 *   Write data with the width of cfiword_t into cfi-flash.
 *
 ****************************************************************************/

static int cfi_intel_write_word(FAR struct cfi_dev_s *cfi,
                                uintptr_t addr, cfiword_t word)
{
  off_t offset = addr - cfi->base_addr;

  cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_CLEAR_STATUS);
  cfi_write_address_cmd(cfi, offset / cfi->bankwidth, CFI_INTEL_CMD_WRITE);
  cfi_write_data(cfi, addr, &word);

  return cfi_intel_status_check(cfi, cfi_find_block(cfi, offset),
                                (1 << cfi->info.single_write_timeout_typ) *
                                (1 << cfi->info.single_write_timeout_max));
}

/****************************************************************************
 * Name: cfi_intel_write_buffer
 *
 * Description:
 *   Function of intel cfi command set.
 *   Write data from buffer into cfi-flash.
 *
 ****************************************************************************/

static int cfi_intel_write_buffer(FAR struct cfi_dev_s *cfi,
                                  uintptr_t addr, size_t nbytes,
                                  FAR const uint8_t *buffer)
{
  int ret;
  blkcnt_t block;
  off_t offset = addr - cfi->base_addr;

  block = cfi_find_block(cfi, offset);
  cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_CLEAR_STATUS);
  cfi_write_block_cmd(cfi, block, CFI_INTEL_CMD_WRITE_BUFFER);
  ret = cfi_intel_status_check(cfi, block,
                               (1 << cfi->info.buffer_write_timeout_typ) *
                               (1 << cfi->info.buffer_write_timeout_max));
  if (ret < 0)
    {
      return ret;
    }

  addr = cfi->base_addr + offset;
  nbytes = nbytes / cfi->bankwidth;

  cfi_write_block_cmd(cfi, block, nbytes - 1);
  while (nbytes-- > 0)
    {
      cfi_write_data(cfi, addr, buffer);
      addr += cfi->bankwidth;
      buffer += cfi->bankwidth;
    }

  cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_WRITE_CONFIRM);

  return cfi_intel_status_check(cfi, block,
                               (1 << cfi->info.buffer_write_timeout_typ) *
                               (1 << cfi->info.buffer_write_timeout_max));
}

/****************************************************************************
 * Name: cfi_intel_erase
 *
 * Description:
 *   Function of intel cfi command set.
 *   Erase one block.
 *
 ****************************************************************************/

static int cfi_intel_erase(FAR struct cfi_dev_s *cfi, blkcnt_t block)
{
  cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_CLEAR_STATUS);
  cfi_write_block_cmd(cfi, block, CFI_INTEL_CMD_BLOCK_ERASE);
  cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_ERASE_CONFIRM);
  return cfi_intel_status_check(cfi, block, 1000 *
                                (1 << cfi->info.block_erase_timeout_typ) *
                                (1 << cfi->info.block_erase_timeout_max));
}

/****************************************************************************
 * Name: cfi_amd_fix
 *
 * Description:
 *   Set the variables specific to AMD.
 *
 ****************************************************************************/

static void cfi_amd_fix(FAR struct cfi_dev_s *cfi)
{
  /* x8/x16 in x8 mode or x16/x32 in x16 mode */

  if ((cfi->dev_width == 1 && cfi->info.interface_desc == 0x02) ||
      (cfi->dev_width == 2 && cfi->info.interface_desc == 0x04))
    {
      cfi->unlock_addr1 = 0xaaa;
      cfi->unlock_addr2 = 0x555;
    }
  else
    {
      cfi->unlock_addr1 = 0x555;
      cfi->unlock_addr2 = 0x2aa;
    }
}

/****************************************************************************
 * Name: cfi_amd_unlock
 *
 * Description:
 *   Flash unlock command sequence specific to AMD.
 *
 ****************************************************************************/

static void cfi_amd_unlock(FAR struct cfi_dev_s *cfi)
{
  cfi_write_address_cmd(cfi, cfi->unlock_addr1, CFI_AMD_CMD_UNLOCK1);
  cfi_write_address_cmd(cfi, cfi->unlock_addr2, CFI_AMD_CMD_UNLOCK2);
}

/****************************************************************************
 * Name: cfi_amd_is_busy
 *
 * Description:
 *   Function of amd cfi command set.
 *   Check whether the cfi-flash is busy.
 *
 ****************************************************************************/

static bool cfi_amd_is_busy(FAR struct cfi_dev_s *cfi, blkcnt_t block)
{
  uintptr_t addr;

  addr = cfi_addr_map(cfi, block, 0);

  /* status bit DQ6 will toggle after a reading cycle during erasing */

  return ((cfi_read_byte(addr) ^ cfi_read_byte(addr)) &
          CFI_AMD_STATUS_TOGGLE) != 0;
}

/****************************************************************************
 * Name: cfi_amd_status_check
 *
 * Description:
 *   Function of amd cfi command set.
 *   Check the status register of the cfi-flash.
 *
 ****************************************************************************/

static int cfi_amd_status_check(FAR struct cfi_dev_s *cfi,
                                blkcnt_t block, uint32_t tout)
{
  uint32_t start;

  /* wait for command completion */

  start = cfi_get_time_us();
  while (cfi_amd_is_busy(cfi, block))
    {
      if (cfi_is_timeout(start, tout))
        {
          ferr("CFI flash operation has timed out.\n");
          cfi_reset(cfi);
          return -EAGAIN;
        }

      up_udelay(1);
    }

  return 0;
}

/****************************************************************************
 * Name: cfi_amd_write_word
 *
 * Description:
 *   Function of amd cfi command set.
 *   Write data with the width of cfiword_t into cfi-flash.
 *
 ****************************************************************************/

static int cfi_amd_write_word(FAR struct cfi_dev_s *cfi,
                              uintptr_t addr, cfiword_t word)
{
  off_t offset = addr - cfi->base_addr;

  cfi_amd_unlock(cfi);
  cfi_write_address_cmd(cfi, cfi->unlock_addr1, CFI_AMD_CMD_WRITE);
  cfi_write_data(cfi, addr, &word);

  return cfi_amd_status_check(cfi, cfi_find_block(cfi, offset),
                              (1 << cfi->info.single_write_timeout_typ) *
                              (1 << cfi->info.single_write_timeout_max));
}

/****************************************************************************
 * Name: cfi_amd_write_buffer
 *
 * Description:
 *   Function of amd cfi command set.
 *   Write data from buffer into cfi-flash.
 *
 ****************************************************************************/

static int cfi_amd_write_buffer(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                                size_t nbytes, FAR const uint8_t *buffer)
{
  blkcnt_t block;
  off_t offset = addr - cfi->base_addr;

  block = cfi_find_block(cfi, offset);
  cfi_amd_unlock(cfi);
  cfi_write_block_cmd(cfi, block, CFI_AMD_CMD_WRITE_BUFFER);

  nbytes = nbytes / cfi->bankwidth;
  cfi_write_block_cmd(cfi, block, nbytes - 1);

  while (nbytes-- > 0)
    {
      cfi_write_data(cfi, addr, buffer);
      addr += cfi->bankwidth;
      buffer += cfi->bankwidth;
    }

  cfi_write_address_cmd(cfi, 0, CFI_AMD_CMD_WRITE_CONFIRM);
  return cfi_amd_status_check(cfi, block,
                              (1 << cfi->info.buffer_write_timeout_max) *
                              (1 << cfi->info.buffer_write_timeout_max));
}

/****************************************************************************
 * Name: cfi_amd_erase
 *
 * Description:
 *   Function of amd cfi command set.
 *   Erase one block.
 *
 ****************************************************************************/

static int cfi_amd_erase(FAR struct cfi_dev_s *cfi, blkcnt_t block)
{
  cfi_amd_unlock(cfi);
  cfi_write_address_cmd(cfi, cfi->unlock_addr1, CFI_AMD_CMD_ERASE_START);
  cfi_amd_unlock(cfi);
  cfi_write_block_cmd(cfi, block, CFI_AMD_CMD_ERASE_SECTOR);
  return cfi_amd_status_check(cfi, block, 1000 *
                              (1 << cfi->info.block_erase_timeout_typ) *
                              (1 << cfi->info.block_erase_timeout_max));
}

/****************************************************************************
 * Name: cfi_write_word
 *
 * Description:
 *   Write data with the width of cfiword_t into cfi-flash.
 *
 ****************************************************************************/

static int cfi_write_word(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                          cfiword_t word)
{
  switch (cfi->info.p_id)
    {
      case CFI_CMDSET_INTEL_EXTENDED:
      case CFI_CMDSET_INTEL_STANDARD:
        return cfi_intel_write_word(cfi, addr, word);
      case CFI_CMDSET_AMD_STANDARD:
      case CFI_CMDSET_AMD_EXTENDED:
        return cfi_amd_write_word(cfi, addr, word);
      case CFI_CMDSET_MITS_STANDARD:
      case CFI_CMDSET_MITS_EXTENDED:
      default:
        return -EINVAL;
    }
}

/****************************************************************************
 * Name: cfi_write_buffer
 *
 * Description:
 *   Write data from buffer into cfi-flash.
 *
 ****************************************************************************/

static int cfi_write_buffer(FAR struct cfi_dev_s *cfi, uintptr_t addr,
                            size_t nbytes, FAR const uint8_t *buffer)
{
  switch (cfi->info.p_id)
    {
      case CFI_CMDSET_INTEL_EXTENDED:
      case CFI_CMDSET_INTEL_STANDARD:
        return cfi_intel_write_buffer(cfi, addr, nbytes, buffer);
      case CFI_CMDSET_AMD_STANDARD:
      case CFI_CMDSET_AMD_EXTENDED:
        return cfi_amd_write_buffer(cfi, addr, nbytes, buffer);
      case CFI_CMDSET_MITS_STANDARD:
      case CFI_CMDSET_MITS_EXTENDED:
      default:
        return -EINVAL;
    }
}

/****************************************************************************
 * Name: cfi_erase_one
 *
 * Description:
 *   Erase one block.
 *
 ****************************************************************************/

static int cfi_erase_one(FAR struct cfi_dev_s *cfi, blkcnt_t block)
{
  switch (cfi->info.p_id)
    {
      case CFI_CMDSET_INTEL_EXTENDED:
      case CFI_CMDSET_INTEL_STANDARD:
        return cfi_intel_erase(cfi, block);
      case CFI_CMDSET_AMD_STANDARD:
      case CFI_CMDSET_AMD_EXTENDED:
        return cfi_amd_erase(cfi, block);
      case CFI_CMDSET_MITS_STANDARD:
      case CFI_CMDSET_MITS_EXTENDED:
      default:
        return -EINVAL;
    }
}

static ssize_t cfi_write_unalign(FAR struct cfi_dev_s *cfi, off_t offset,
                                 size_t nbytes, FAR const uint8_t *buffer)
{
  uint8_t i;
  int ret;
  size_t delta;
  uintptr_t wp;
  cfiword_t word = 0;

  /* get lower aligned address */

  wp = offset & ~(cfi->bankwidth - 1);

  /* handle unaligned start */

  if ((delta = offset - wp) == 0 && nbytes >= cfi->bankwidth)
    {
      return 0;
    }

  for (i = 0; i < delta; i++)
    {
      cfi_add_byte(cfi, &word, cfi_read_byte(wp + i));
    }

  delta = 0;
  for (; i < cfi->bankwidth && nbytes > 0; i++)
    {
      cfi_add_byte(cfi, &word, *buffer++);
      nbytes--;
      delta++;
    }

  for (; i < cfi->bankwidth; i++)
    {
      cfi_add_byte(cfi, &word, cfi_read_byte(wp + i));
    }

  ret = cfi_write_word(cfi, wp, word);
  return ret < 0 ? ret : delta;
}

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

/****************************************************************************
 * Name: cfi_get_blocksize
 *
 * Description:
 *   Get the size of block in the specified region.
 *
 ****************************************************************************/

size_t cfi_get_blocksize(FAR struct cfi_dev_s *cfi, uint8_t region)
{
  return cfi->dev_num * (cfi->info.erase_region_info[region] >> 16) * 256;
}

/****************************************************************************
 * Name: cfi_get_blocknum
 *
 * Description:
 *   Get the number of blocks in the specified region.
 *
 ****************************************************************************/

blkcnt_t cfi_get_blocknum(FAR struct cfi_dev_s *cfi, uint8_t region)
{
  return ((uint16_t)(cfi->info.erase_region_info[region])) + 1;
}

/****************************************************************************
 * Name: cfi_find_block
 *
 * Description:
 *   Get the block number corresponding to offset.
 *
 ****************************************************************************/

blkcnt_t cfi_find_block(FAR struct cfi_dev_s *cfi, off_t offset)
{
  uint8_t i;
  blkcnt_t ret = 0;

  for (i = 0; i < cfi->info.erase_region_num; i++)
    {
      blkcnt_t block_num = cfi_get_blocknum(cfi, i);
      size_t block_size = cfi_get_blocksize(cfi, i);

      if (block_num * block_size <= offset)
        {
          offset -= block_num * block_size;
          ret += block_num;
        }
      else
        {
          ret += offset / block_size;
          break;
        }
    }

  return ret;
}

/****************************************************************************
 * Name: cfi_get_total_blocknum
 *
 * Description:
 *   Get total number of erase blocks, assuming that all erase blocks are the
 *   same size.
 *
 ****************************************************************************/

blkcnt_t cfi_get_total_blocknum(FAR struct cfi_dev_s *cfi)
{
  uint8_t i;
  blkcnt_t blocknum = 0;

  for (i = 0; i < cfi->info.erase_region_num; i++)
    {
      blocknum += cfi_get_blocknum(cfi, i) * cfi_get_blocksize(cfi, i) /
                  cfi_get_blocksize(cfi, 0);
    }

  return blocknum;
}

/****************************************************************************
 * Name: cfi_check
 *
 * Description:
 *   Check whether the device is a cfi device, if so, then get the cfi data.
 *
 ****************************************************************************/

int cfi_check(FAR struct cfi_dev_s *cfi)
{
  uint8_t i;
  uint8_t j;

  /* try different single device width which we don't know now */

  for (cfi->dev_width = 1; cfi->dev_width <= cfi->bankwidth;
       cfi->dev_width <<= 1)
    {
      cfi_reset(cfi);
      for (i = 0; i < nitems(g_cfi_query_address); i++)
        {
          cfi_write_address_cmd(cfi, g_cfi_query_address[i], CFI_CMD);

          /* try to get into cfi mode and read cfi data */

          if (cfi_is_equal(cfi, CFI_QRY_OFFSET, 'Q') &&
              cfi_is_equal(cfi, CFI_QRY_OFFSET + 1, 'R') &&
              cfi_is_equal(cfi, CFI_QRY_OFFSET + 2, 'Y'))
            {
              FAR struct cfi_info_s *info = &(cfi->info);
              FAR uint8_t *ptr = (FAR uint8_t *)info;

              for (j = CFI_QRY_OFFSET;
                   j < CFI_ERASE_BLOCK_REGION_INFO_OFFSET; j++)
                {
                  *ptr++ = cfi_read_byte(cfi_addr_map(cfi, 0, j));
                }

              while (j < CFI_ERASE_BLOCK_REGION_INFO_OFFSET + 4 *
                     info->erase_region_num)
                {
                  *ptr++ = cfi_read_byte(cfi_addr_map(cfi, 0, j++));
                }

              cfi->cfi_offset = g_cfi_query_address[i];
              cfi->dev_num = cfi->bankwidth / cfi->dev_width;
              cfi->page_size = MIN(CONFIG_MTD_CFI_PAGE_SIZE,
                                   (1 << info->max_write_bytes_num) *
                                   cfi->dev_num);

              /* fix amd feature */

              if (info->p_id == CFI_CMDSET_AMD_STANDARD ||
                  info->p_id == CFI_CMDSET_AMD_EXTENDED)
                {
                  cfi_amd_fix(cfi);
                }

              /* exit cfi mode, enter into read array mode */

              cfi_reset(cfi);
              return 0;
            }
        }
    }

  /* cfi flash not found */

  return -EINVAL;
}

/****************************************************************************
 * Name: cfi_reset
 *
 * Description:
 *   Get into read array mode.
 *
 ****************************************************************************/

void cfi_reset(FAR struct cfi_dev_s *cfi)
{
  switch (cfi->info.p_id)
    {
      case CFI_CMDSET_INTEL_EXTENDED:
      case CFI_CMDSET_INTEL_STANDARD:
        cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_CLEAR_STATUS);
        cfi_write_address_cmd(cfi, 0, CFI_INTEL_CMD_RESET);
        break;
      case CFI_CMDSET_AMD_STANDARD:
      case CFI_CMDSET_AMD_EXTENDED:
        cfi_write_address_cmd(cfi, 0, CFI_AMD_CMD_RESET);
    }
}

/****************************************************************************
 * Name: cfi_erase
 *
 * Description:
 *   Erase multi blocks.
 *
 ****************************************************************************/

int cfi_erase(FAR struct cfi_dev_s *cfi, blkcnt_t startblock,
              blkcnt_t blockcnt)
{
  blkcnt_t i;
  int ret = 0;

  for (i = startblock; i < startblock + blockcnt; i++)
    {
      ret = cfi_erase_one(cfi, i);
      if (ret < 0)
        {
          break;
        }
    }

  cfi_reset(cfi);
  return ret;
}

/****************************************************************************
 * Name: cfi_read
 *
 * Description:
 *   Read data from cfi-flash to buffer.
 *
 ****************************************************************************/

int cfi_read(FAR struct cfi_dev_s *cfi, off_t offset, size_t nbytes,
             FAR uint8_t *buffer)
{
  uintptr_t base = cfi->base_addr;

  memcpy(buffer, (FAR const uint8_t *)base + offset, nbytes);
  return 0;
}

/****************************************************************************
 * Name: cfi_write
 *
 * Description:
 *   Write data from buffer to cfi-flash.
 *
 ****************************************************************************/

int cfi_write(FAR struct cfi_dev_s *cfi, off_t offset, size_t nbytes,
              FAR const uint8_t *buffer)
{
  ssize_t ret;

  offset = cfi->base_addr + offset;
  ret = cfi_write_unalign(cfi, offset, nbytes, buffer);
  if (ret < 0)
    {
      goto out;
    }

  offset = ALIGN_UP(offset, cfi->bankwidth);
  nbytes -= ret;
  buffer += ret;

  /* handle the aligned part */

  /* if the device support buffer write */

  if (cfi->info.max_write_bytes_num != 0)
    {
      while (nbytes >= cfi->bankwidth)
        {
          size_t size = cfi->page_size - offset % cfi->page_size;

          if (size > nbytes)
            {
              size = ALIGN_DOWN(nbytes, cfi->bankwidth);
            }

          ret = cfi_write_buffer(cfi, offset, size, buffer);
          if (ret < 0)
            {
              goto out;
            }

          offset += size;
          buffer += size;
          nbytes -= size;
        }
    }
  else
    {
      while (nbytes >= cfi->bankwidth)
        {
          cfiword_t word = 0;
          uint8_t i;

          for (i = 0; i < cfi->bankwidth; i++)
            {
              cfi_add_byte(cfi, &word, *buffer++);
            }

          ret = cfi_write_word(cfi, offset, word);
          if (ret < 0)
            {
              goto out;
            }

          offset += cfi->bankwidth;
          nbytes -= cfi->bankwidth;
        }
    }

  if (nbytes == 0)
    {
      goto out;
    }

  /* handle unaligned tail bytes */

  ret = cfi_write_unalign(cfi, offset, nbytes, buffer);

out:
  cfi_reset(cfi);
  return ret < 0 ? ret : 0;
}