// Flash programmer
// Tool to flash the SPI flash chip

#define word char

#include "data/ASCII_BW.c"
#include "lib/math.c"
#include "lib/stdlib.c"
#include "lib/gfx.c"


#define COMMAND_IDLE                    0
#define COMMAND_FLASH_READ              114 // 'r'
#define COMMAND_FLASH_ERASE_BLOCK       101 // 'e'
#define COMMAND_FLASH_WRITE             119 // 'w'
#define COMMAND_FILL_BUFFER             98  // 'b'
#define COMMAND_DONE                    100 // 'd'

#define BLOCK_SIZE 32768
#define SECTOR_SIZE 4096
#define PAGE_SIZE 256
#define COMMAND_SIZE 8


// the current command the program is busy with
// if idle, should listen to UART for new command
word currentCommand;

// UART buffer that stores a single command
word UARTbufferIndex = 0;
word UARTbuffer[COMMAND_SIZE];

// Buffer of a page to write
word pageBufferIndex = 0;
word pageBuffer[PAGE_SIZE];

/* Description of commands:

READ x(24bit) Bytes from address y(24bit):
    b0: 'r'
    b1: x[23:16]
    b2: x[15:8]
    b3: x[7:0]
    b4: y[23:16]
    b5: y[15:8]
    b6: y[7:0]
    b7: '\n' (indicate end of command)
*/

void initVram()
{
    GFX_initVram(); // clear all VRAM
    GFX_copyPaletteTable((word)DATA_PALETTE_DEFAULT);
    GFX_copyPatternTable((word)DATA_ASCII_DEFAULT);

    GFX_cursor = 0;
}

// Workaround for defines in ASM
void SpiFlash_asmDefines()
{
    asm(
        "define W5500_SPI0_CS_ADDR = 0xC02729 ; address of SPI0_CS\n"
        "define W5500_SPI0_ADDR = 0xC02728    ; address of SPI0\n"
        );
}

// Sets SPI0_CS low
void SpiBeginTransfer()
{
    asm(
        "; backup regs\n"
        "push r1\n"
        "push r2\n"

        "load32 W5500_SPI0_CS_ADDR r2       ; r2 = W5500_SPI0_CS_ADDR\n"

        "load 0 r1                          ; r1 = 0 (enable)\n"
        "write 0 r2 r1                      ; write to SPI0_CS\n"

        "; restore regs\n"
        "pop r2\n"
        "pop r1\n"
        );
}

// Sets SPI0_CS high
void SpiEndTransfer()
{
    asm(
        "; backup regs\n"
        "push r1\n"
        "push r2\n"

        "load32 W5500_SPI0_CS_ADDR r2       ; r2 = W5500_SPI0_CS_ADDR\n"

        "load 1 r1                          ; r1 = 1 (disable)\n"
        "write 0 r2 r1                      ; write to SPI0_CS\n"

        "; restore regs\n"
        "pop r2\n"
        "pop r1\n"
        );
}

// write dataByte and return read value
// write 0x00 for a read
// Writes byte over SPI0
word SpiTransfer(word dataByte)
{
    word retval = 0;
    asm(
        "ccache\n"
        "load32 W5500_SPI0_ADDR r2          ; r2 = W5500_SPI0_ADDR\n"
        "write 0 r2 r4                      ; write r4 over SPI0\n"
        "read 0 r2 r2                       ; read return value\n"
        "write -4 r14 r2                    ; write to stack to return\n"
        );

    return retval;
}



// inits SPI by enabling SPI0 and resetting the chip
void initSPI()
{
    // already set CS high before enabling SPI0
    SpiEndTransfer();

    // enable SPI0
    word *p = (word *) 0xC0272A; // set address (SPI0 enable)
    *p = 1; // write value
    delay(10);

    // reset to get out of continuous read mode
    SpiBeginTransfer();
    SpiTransfer(0x66);
    SpiEndTransfer();
    delay(1);
    SpiBeginTransfer();
    SpiTransfer(0x99);
    SpiEndTransfer();
    delay(1);
    SpiBeginTransfer();
    SpiTransfer(0x66);
    SpiEndTransfer();
    delay(1);
    SpiBeginTransfer();
    SpiTransfer(0x99);
    SpiEndTransfer();
    delay(1);
}



// Should print 0xEF as Winbond device ID, and 0x17 for W25Q128
void readDeviceID()
{
    SpiBeginTransfer();
    SpiTransfer(0x90);
    SpiTransfer(0);
    SpiTransfer(0);
    SpiTransfer(0);
    word manufacturer = SpiTransfer(0);
    word deviceID = SpiTransfer(0);
    SpiEndTransfer();

    char b[10];
    itoah(manufacturer, b);
    GFX_PrintConsole("Chip manufacturer: ");
    GFX_PrintConsole(b);
    GFX_PrintConsole("\n");

    GFX_PrintConsole("Chip ID: ");
    itoah(deviceID, b);
    GFX_PrintConsole(b);
    GFX_PrintConsole("\n");
}


void readAndPrint(word len, word addr24, word addr16, word addr8)
{
    word l = len;
    word a24 = addr24;
    word a16 = addr16;
    word a8 = addr8;

    SpiBeginTransfer();
    SpiTransfer(0x03);
    SpiTransfer(a24);
    SpiTransfer(a16);
    SpiTransfer(a8);


    word i;
    for (i = 0; i < l; i++)
    {
        word x = SpiTransfer(0x00);
        // delay a bit here
        word n = 3;
        word m = 4;
        word f = n*m;
        uprintc(x);
    }

    SpiEndTransfer();
}


/* Returns status register 1 2 or 3, depending on the value of reg (1, 2 or 3)

Status register description:

Register 1: (default: 0x0)
    b7: Status Register Protect 0
    b6: Sector Protect Bit
    b5: Top/Bottom Protect Bit
    b4: Block Protect Bit 2
    b3: Block Protect Bit 1
    b2: Block Protect Bit 0
    b1: Write Enable Latch
    b0: BUSY: Erase/Write in Progress

Register 2: (default: 0x2)
    b7: Suspend Status
    b6: Complement Protect
    b5: Security Register Lock bit 2
    b4: Security Register Lock bit 1
    b3: Security Register Lock bit 0
    b2: Reserved
    b1: Quad Enable
    b0: Status Register Protect 1

Register 3: (default: 0x60)
    b7: Reserved
    b6: Output Driver Strength bit 1
    b5: Output Driver Strength bit 0
    b4: Reserved
    b3: Reserved
    b2: Write Protect Selection
    b1: Reserved
    b0: Reserved


*/

word readStatusRegister(word reg)
{
    SpiBeginTransfer();

    if (reg == 2)
        SpiTransfer(0x35);
    else if (reg == 3)
        SpiTransfer(0x15);
    else
        SpiTransfer(0x05);

    word status = SpiTransfer(0);
    SpiEndTransfer();

    return status;
}



void enableWrite()
{
    SpiBeginTransfer();
    SpiTransfer(0x06);
    SpiEndTransfer();
}


// Executes chip erase operation
// TAKES AT LEAST 20 SECONDS!
// Returns 1 on success
word chipErase()
{
    enableWrite();

    word status = readStatusRegister(1);

    // Check if write is enabled
    if ((status & 0x2) == 0)
        return 0;

    // Send command
    SpiBeginTransfer();
    SpiTransfer(0xC7);
    SpiEndTransfer();


    // Wait for busy bit to be 0
    status = 1;

    while((status & 0x1) == 1) 
    {
        status = readStatusRegister(1);
    }

    return 1;
}



// Executes 32KiB block erase operation
// Erases the 32KiB block that contains the given byte address
// Returns 1 on success
word blockErase(word addr24, word addr16, word addr8)
{

    word a24 = addr24;
    word a16 = addr16;
    word a8 = addr8;

    enableWrite();

    word status = readStatusRegister(1);

    // Check if write is enabled
    if ((status & 0x2) == 0)
        return 0;

    // Send command
    SpiBeginTransfer();
    SpiTransfer(0x52);
    SpiTransfer(a24);
    SpiTransfer(a16);
    SpiTransfer(a8);
    SpiEndTransfer();


    // Wait for busy bit to be 0
    status = 1;

    while((status & 0x1) == 1) 
    {
        status = readStatusRegister(1);
    }

    return 1;
}


// Executes 256 byte page program operation
// The address is the byte address of the first byte to write
// Data wraps around at the end of a page,
//  so last address byte will usually be 0
// Returns 1 on success
word pageProgram(word addr24, word addr16, word addr8)
{

    word a24 = addr24;
    word a16 = addr16;
    word a8 = addr8;

    enableWrite();

    word status = readStatusRegister(1);

    // Check if write is enabled
    if ((status & 0x2) == 0)
        return 0;

    // Send command
    SpiBeginTransfer();
    SpiTransfer(0x2);
    SpiTransfer(a24);
    SpiTransfer(a16);
    SpiTransfer(a8);

    // Send page
    word *p_pageBuffer = pageBuffer;
    word i;
    for (i = 0; i < 256; i++)
    {
        SpiTransfer(*(p_pageBuffer + i));
    }

    SpiEndTransfer();


    // Wait for busy bit to be 0
    status = 1;

    while((status & 0x1) == 1) 
    {
        status = readStatusRegister(1);
    }

    return 1;
}


void processCommand()
{
    if (currentCommand == COMMAND_IDLE)
    {
        // do nothing
    }
    else if (currentCommand == COMMAND_FLASH_READ)
    {

        word len = UARTbuffer[1] << 16;
        len += UARTbuffer[2] << 8;
        len += UARTbuffer[3];

        word addr24 = UARTbuffer[4];
        word addr16 = UARTbuffer[5];
        word addr8 = UARTbuffer[6];

        GFX_printWindowColored("                                        ", 40, GFX_WindowPosFromXY(0, 8), 0);
        GFX_printWindowColored("Reading ", 8, GFX_WindowPosFromXY(0, 8), 0);
        char b[10];
        itoa(len, b);
        GFX_printWindowColored(b, strlen(b), GFX_WindowPosFromXY(8, 8), 0);
        GFX_printWindowColored(" bytes", 6, GFX_WindowPosFromXY(8 + strlen(b), 8), 0);

        readAndPrint(len, addr24, addr16, addr8);

        GFX_printWindowColored(" done!", 5, GFX_WindowPosFromXY(8 + strlen(b) + 6, 8), 0);

        currentCommand = COMMAND_IDLE;
        uprintc('r');
    }
    else if (currentCommand == COMMAND_FLASH_ERASE_BLOCK)
    {
        word addr24 = UARTbuffer[4];
        word addr16 = UARTbuffer[5];
        word addr8 = UARTbuffer[6];

        blockErase(addr24, addr16, addr8);
        word addrCombined = (unsigned) ((addr24 << 16) + (addr16 << 8) + addr8) >> 15;

        GFX_printWindowColored("                                        ", 40, GFX_WindowPosFromXY(0, 8), 0);
        GFX_printWindowColored("Block ", 6, GFX_WindowPosFromXY(0, 8), 0);
        char b[10];
        itoa(addrCombined, b);
        GFX_printWindowColored(b, strlen(b), GFX_WindowPosFromXY(6, 8), 0);
        GFX_printWindowColored(" erased", 7, GFX_WindowPosFromXY(6 + strlen(b), 8), 0);

        currentCommand = COMMAND_IDLE;
        uprintc('r');
    }
    else if (currentCommand == COMMAND_FLASH_WRITE)
    {
        word addr24 = UARTbuffer[4];
        word addr16 = UARTbuffer[5];
        word addr8 = UARTbuffer[6];

        pageProgram(addr24, addr16, addr8);
        word addrCombined = (addr24 << 8) + addr16;

        GFX_printWindowColored("                                        ", 40, GFX_WindowPosFromXY(0, 8), 0);
        GFX_printWindowColored("Page ", 5, GFX_WindowPosFromXY(0, 8), 0);
        char b[10];
        itoa(addrCombined, b);
        GFX_printWindowColored(b, strlen(b), GFX_WindowPosFromXY(5, 8), 0);
        GFX_printWindowColored(" written", 8, GFX_WindowPosFromXY(5 + strlen(b), 8), 0);

        currentCommand = COMMAND_IDLE;
        uprintc('r');
    }
    else if (currentCommand == COMMAND_FILL_BUFFER)
    {
        // done when full page is received
        if (pageBufferIndex == PAGE_SIZE)
        {
            pageBufferIndex = 0;
            currentCommand = COMMAND_IDLE;
            uprintc('r');
        }
    }
    else if (currentCommand == COMMAND_DONE)
    {
        // notify done
        GFX_printWindowColored("                                        ", 40, GFX_WindowPosFromXY(0, 6), 0);
        GFX_printWindowColored("                                        ", 40, GFX_WindowPosFromXY(0, 8), 0);
        GFX_printWindowColored("Done!", 5, GFX_WindowPosFromXY(0, 6), 0);

        // allow new commands, just in case
        currentCommand = COMMAND_IDLE;
    }
}


int main() 
{
    initVram();
    GFX_PrintConsole("Flash Programmer\n\n");

    currentCommand = COMMAND_IDLE;

    initSPI();
    GFX_PrintConsole("SPI0 mode set\n");

    readDeviceID();


    GFX_PrintConsole("\nReady to receive commands\n\n");
    // Notify that we are ready to recieve commands
    uprintc('r');

    while (1)
    {
        processCommand();
    }

    return 48;
}

void int1()
{
    /*
    word *p = (word *) 0x4C0000; // set address (timer1 state)
    *p = 1; // write value
    */
    timer1Value = 1; // notify ending of timer1
}

void int2()
{
   
}


void int3()
{
    // UART RX interrupt

    // Fill buffer
    if (currentCommand == COMMAND_FILL_BUFFER)
    {
        // read byte
        word *p = (word *)0xC02722;   // address of UART RX
        word b = *p;                 // read byte from UART

        // write byte to buffer, increase index
        word *p_pageBuffer = pageBuffer;
        *(p_pageBuffer + pageBufferIndex) = b;
        pageBufferIndex++;
        //uprintc('a'); // notify write to buffer is done
    }
    // Get command
    else if (currentCommand == COMMAND_IDLE)
    {
        // read byte
        word *p = (word *)0xC02722;   // address of UART RX
        word b = *p;                 // read byte from UART

        // write byte to buffer, increase index
        word *p_UARTbuffer = UARTbuffer;
        *(p_UARTbuffer + UARTbufferIndex) = b;
        UARTbufferIndex++;

        // execute command when 8 bytes received
        if (UARTbufferIndex == COMMAND_SIZE)
        {
            currentCommand = *(p_UARTbuffer);
            UARTbufferIndex = 0;

            // notify ready to receive when filling buffer
            if (currentCommand == COMMAND_FILL_BUFFER)
            {
                uprintc('b');
            }
        }

    }
   
}

void int4()
{
}

void interrupt()
{
    word i = getIntID();

    if (i == 1)
    {
        int1();
    }
    else if (i == 3)
    {
        int3();
    }
}