///////////////////////////////////////////////////////////////////////////////
// BOSSA
//
// Copyright (c) 2011-2018, ShumaTech
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * 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.
//     * Neither the name of the <organization> 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 <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
///////////////////////////////////////////////////////////////////////////////
#include "EefcFlash.h"

#include <assert.h>
#include <unistd.h>
#include <stdio.h>

#define EEFC_KEY        0x5a

#define EEFC0_FMR       (_regs + 0x00)
#define EEFC0_FCR       (_regs + 0x04)
#define EEFC0_FSR       (_regs + 0x08)
#define EEFC0_FRR       (_regs + 0x0C)

#define EEFC1_FMR       (_regs + 0x200)
#define EEFC1_FCR       (_regs + 0x204)
#define EEFC1_FSR       (_regs + 0x208)
#define EEFC1_FRR       (_regs + 0x20C)

#define EEFC_FCMD_GETD  0x0
#define EEFC_FCMD_WP    0x1
#define EEFC_FCMD_WPL   0x2
#define EEFC_FCMD_EWP   0x3
#define EEFC_FCMD_EWPL  0x4
#define EEFC_FCMD_EA    0x5
#define EEFC_FCMD_EPA   0x7
#define EEFC_FCMD_SLB   0x8
#define EEFC_FCMD_CLB   0x9
#define EEFC_FCMD_GLB   0xa
#define EEFC_FCMD_SGPB  0xb
#define EEFC_FCMD_CGPB  0xc
#define EEFC_FCMD_GGPB  0xd
#define EEFC_FCMD_STUI  0xe
#define EEFC_FCMD_SPUI  0xf
#define EEFC_FCMD_GCALB 0x10
#define EEFC_FCMD_ES    0x11
#define EEFC_FCMD_WUS   0x12
#define EEFC_FCMD_EUS   0x13
#define EEFC_FCMD_STUS  0x14
#define EEFC_FCMD_SPUS  0x15

const uint32_t
EefcFlash::PagesPerErase = 8;

EefcFlash::EefcFlash(Samba& samba,
                     const std::string& name,
                     uint32_t addr,
                     uint32_t pages,
                     uint32_t size,
                     uint32_t planes,
                     uint32_t lockRegions,
                     uint32_t uniqueIdWords,
                     uint32_t user,
                     uint32_t stack,
                     uint32_t regs,
                     bool canBrownout)
    : Flash(samba, name, addr, pages, size, planes, lockRegions, user, stack),
      _regs(regs), _uniqueIdWords(uniqueIdWords), _canBrownout(canBrownout), _eraseAuto(true)
{
    assert(planes == 1 || planes == 2);
    assert(pages <= 4096);
    assert(lockRegions <= 256);

    // SAM3 Errata (FWS must be 6)
    _samba.writeWord(EEFC0_FMR, 0x6 << 8);
    if (planes == 2)
        _samba.writeWord(EEFC1_FMR, 0x6 << 8);
}

EefcFlash::~EefcFlash()
{
}

void
EefcFlash::eraseAll(uint32_t offset)
{
    // Do a full chip erase if the offset is 0
    if (offset == 0)
    {
        waitFSR();
        writeFCR0(EEFC_FCMD_EA, 0);
        if (_planes == 2)
        {
            waitFSR();
            writeFCR1(EEFC_FCMD_EA, 0);
        }

        // Erase all can take an exceptionally long time on some devices
        // so wait on FSR for up to 30 seconds
        waitFSR(30);
    }
    // Else we must do it by pages
    else
    {
        // Offset must be on an erase page boundary
        if (offset % (_size * PagesPerErase))
            throw FlashEraseError();

        // Erase each PagesPerErase set of pages
        for (uint32_t pageNum = offset / _size; pageNum < _pages; pageNum += PagesPerErase)
        {
            if (_planes == 1 || pageNum < _pages / 2)
            {
                waitFSR();
                writeFCR0(EEFC_FCMD_EPA, pageNum | 0x1);
            }
            else
            {
                waitFSR();
                writeFCR1(EEFC_FCMD_EPA, (pageNum % (_pages / 2)) | 0x1);
            }
        }
    }
}

void
EefcFlash::eraseAuto(bool enable)
{
    _eraseAuto = enable;
}

std::vector<bool>
EefcFlash::getLockRegions()
{
    std::vector<bool> regions(_lockRegions);
    uint32_t frr;
    uint32_t bit;

    waitFSR();
    for (uint32_t region = 0; region < _lockRegions; region++)
    {
        if (_planes == 2 && region >= _lockRegions / 2)
        {
            bit = region - _lockRegions / 2;
            writeFCR1(EEFC_FCMD_GLB, 0);
            waitFSR();
            frr = readFRR1();
            while (bit >= 32)
            {
                frr = readFRR1();
                bit -= 32;
            }
            regions[region] = (frr & (1 << bit)) != 0;
        }
        else
        {
            bit = region;
            writeFCR0(EEFC_FCMD_GLB, 0);
            waitFSR();
            frr = readFRR0();
            while (bit >= 32)
            {
                frr = readFRR0();
                bit -= 32;
            }
            regions[region] = (frr & (1 << bit)) != 0;
        }
    }

    return regions;
}

bool
EefcFlash::getSecurity()
{
    waitFSR();
    writeFCR0(EEFC_FCMD_GGPB, 0);
    waitFSR();
    return (readFRR0() & (1 << 0));
}

bool
EefcFlash::getBod()
{
    if (!_canBrownout)
        return false;

    waitFSR();
    writeFCR0(EEFC_FCMD_GGPB, 0);
    waitFSR();
    return (readFRR0() & (1 << 1));
}

bool
EefcFlash::getBor()
{
    if (!_canBrownout)
        return false;

    waitFSR();
    writeFCR0(EEFC_FCMD_GGPB, 0);
    waitFSR();
    return (readFRR0() & (1 << 2));
}

bool
EefcFlash::getBootFlash()
{
    waitFSR();
    writeFCR0(EEFC_FCMD_GGPB, 0);
    waitFSR();
    return (readFRR0() & (1 << (_canBrownout ? 3 : 1)));
}

std::vector<uint32_t>
EefcFlash::getUniqueId()
{
    std::vector<uint32_t> value;

    // No unique id for this chip
    if (_uniqueIdWords == 0)
        return value;

    waitFSR();

    // Start read
    writeFCR0(EEFC_FCMD_STUI, 0);

    // Unique id (so far, only 128-bit, or 0 if feature is not available)
    uint32_t addr = address();
    for (uint8_t word = 0; word < _uniqueIdWords; word++)
    {
        // Read from the beginning of the flash region
        // Other flash reads are not allowed while reading the unique id
        value.push_back(_samba.readWord(addr + word * 4));
    }

    // End read
    writeFCR0(EEFC_FCMD_SPUI, 0);

    waitFSR();
    return value;
}

void
EefcFlash::writeOptions()
{
    if (canBootFlash() && _bootFlash.isDirty() && _bootFlash.get() != getBootFlash())
    {
        waitFSR();
        writeFCR0(_bootFlash.get() ? EEFC_FCMD_SGPB : EEFC_FCMD_CGPB, (canBod() ? 3 : 1));
    }
    if (canBor() && _bor.isDirty() && _bor.get() != getBor())
    {
        waitFSR();
        writeFCR0(_bor.get() ? EEFC_FCMD_SGPB : EEFC_FCMD_CGPB, 2);
    }
    if (canBod() && _bod.isDirty() && _bod.get() != getBod())
    {
        waitFSR();
        writeFCR0(_bod.get() ? EEFC_FCMD_SGPB : EEFC_FCMD_CGPB, 1);
    }
    if (_regions.isDirty())
    {
        uint32_t page;
        std::vector<bool> current;

        if (_regions.get().size() > _lockRegions)
            throw FlashRegionError();

        current = getLockRegions();

        for (uint32_t region = 0; region < _lockRegions; region++)
        {
            if (_regions.get()[region] != current[region])
            {
                if (_planes == 2 && region >= _lockRegions / 2)
                {
                    page = (region - _lockRegions / 2) * _pages / _lockRegions;
                    waitFSR();
                    writeFCR1(_regions.get()[region] ? EEFC_FCMD_SLB : EEFC_FCMD_CLB, page);
                }
                else
                {
                    page = region * _pages / _lockRegions;
                    waitFSR();
                    writeFCR0(_regions.get()[region] ? EEFC_FCMD_SLB : EEFC_FCMD_CLB, page);
                }
            }
        }
    }
    if (_security.isDirty() && _security.get() == true && _security.get() != getSecurity())
    {
        waitFSR();
        writeFCR0(EEFC_FCMD_SGPB, 0);
    }
}

void
EefcFlash::ready()
{
    // wait for Flash operations to be done
    waitFSR();
}

void
EefcFlash::writePage(uint32_t page)
{
    if (page >= _pages)
        throw FlashPageError();

    _wordCopy.setDstAddr(_addr + page * _size);
    _wordCopy.setSrcAddr(_onBufferA ? _pageBufferA : _pageBufferB);
    _onBufferA = !_onBufferA;
    // Some chip families have page restrictions on calling EEFC_FCMD_EWP on all pages
    // e.g. 16K boundary on SAM4S
    // Print a warning indicating that the flash must be erased first
    try
    {
        waitFSR();
    }
    catch (FlashCmdError& exc)
    {
        if (page > 0)
        {
            printf("\nNOTE: Some chip families may not support auto-erase on all flash regions.\n");
            printf("      Try erasing the flash first (bossash), or erasing at the same time (bossac).");
            fflush(stdout);
        }
        throw;
    }
    _wordCopy.runv();
    if (_planes == 2 && page >= _pages / 2)
        writeFCR1(_eraseAuto ? EEFC_FCMD_EWP : EEFC_FCMD_WP, page - _pages / 2);
    else
        writeFCR0(_eraseAuto ? EEFC_FCMD_EWP : EEFC_FCMD_WP, page);
}

void
EefcFlash::readPage(uint32_t page, uint8_t* data)
{
    if (page >= _pages)
        throw FlashPageError();

    // The SAM3 firmware has a bug where it returns all zeros for reads
    // directly from the flash so instead, we copy the flash page to
    // SRAM and read it from there.
    _wordCopy.setDstAddr(_onBufferA ? _pageBufferA : _pageBufferB);
    _wordCopy.setSrcAddr(_addr + page * _size);
    waitFSR();
    _wordCopy.runv();
    _samba.read(_onBufferA ? _pageBufferA : _pageBufferB, data, _size);
}

void
EefcFlash::waitFSR(int seconds)
{
    int tries = seconds * 1000;
    uint32_t fsr0;
    uint32_t fsr1 = 0x1;

    while (tries-- > 0)
    {
        fsr0 = _samba.readWord(EEFC0_FSR);
        if (fsr0 & 0x2)
            throw FlashCmdError();
        if (fsr0 & 0x4)
            throw FlashLockError();

        if (_planes == 2)
        {
            fsr1 = _samba.readWord(EEFC1_FSR);
            if (fsr1 & 0x2)
                throw FlashCmdError();
            if (fsr1 & 0x4)
                throw FlashLockError();
        }
        if (fsr0 & fsr1 & 0x1)
            break;
        usleep(1000);
    }
    if (tries == 0)
        throw FlashTimeoutError();
}

void
EefcFlash::writeFCR0(uint8_t cmd, uint32_t arg)
{
    _samba.writeWord(EEFC0_FCR, (EEFC_KEY << 24) | (arg << 8) | cmd);
}

void
EefcFlash::writeFCR1(uint8_t cmd, uint32_t arg)
{
    _samba.writeWord(EEFC1_FCR, (EEFC_KEY << 24) | (arg << 8) | cmd);
}

uint32_t
EefcFlash::readFRR0()
{
    return _samba.readWord(EEFC0_FRR);
}

uint32_t
EefcFlash::readFRR1()
{
    return _samba.readWord(EEFC1_FRR);
}
