/*
 * 6502ctl.ino
 * 6502 controller
 *
 * W65C02S
 * https://www.westerndesigncenter.com/wdc/documentation/w65c02s.pdf
 *
 * Copyright 2022 Bill Zissimopoulos
 */

#include "6502pins.h"

#define SERIAL_SPEED                    1000000

/* TMSR - time measurement */
/*
 * It is best to try this with the debugger initially disabled (debug_step = 0) and
 * with programs that do not use Serial I/O (which is slow). For example:
 *
 *     entry:
 *         LDA $100
 *         STA $100
 *         BRA entry
 *
 * Otherwise results will be unreliable, because we have interrupts disabled and our
 * timers are not getting updated properly.
 *
 * On the ATmega 2560 the avg clock cycle (see loop()) for the above program is 4.948 us,
 * which gives a max speed of 1000000 / 4.948 = 202KHz.
 */
#define TMSR                            0
#if TMSR
static unsigned long tmsr_tcnt, tmsr_lcnt;
#define TMSR_FACT                       (64000000L / F_CPU)
#define TMSR_INIT(t)                    uint8_t t = TCNT0
#define TMSR_LOOP(t)                    \
    ({                                  \
        uint8_t n = TCNT0;              \
        tmsr_tcnt += (uint8_t)(n - t);  \
        tmsr_lcnt++;                    \
        t = TCNT0;                      \
    })
#else
#define TMSR_INIT(t)                    ((void)0)
#define TMSR_LOOP(t)                    ((void)0)
#endif

/* 6502 address bus */
static inline void setup_abus()
{
    /* setup ATmega ports designated for 6502 address bus (ABLO, ABHI) for input */
    P6502_ABLO(DDR) = 0;
    P6502_ABLO(PORT) = 0;
    P6502_ABHI(DDR) = 0;
    P6502_ABHI(PORT) = 0;
}

static inline uint16_t read_abus()
{
    /* read ATmega ports designated for 6502 address bus (ABLO, ABHI) */
    return P6502_ABLO(PIN) | (P6502_ABHI(PIN) << 8);
}

/* 6502 data bus */
static inline void setup_dbus()
{
    /* setup ATmega port designated for 6502 data bus (DBUS) for input */
    P6502_DBUS(DDR) = 0;
    P6502_DBUS(PORT) = 0;
}

static inline uint8_t read_dbus()
{
    /* read ATmega port designated for 6502 data bus (DBUS) */
    P6502_DBUS(DDR) = 0;
    P6502_DBUS(PORT) = 0;
    return P6502_DBUS(PIN);
}

static inline void write_dbus(uint8_t v)
{
    /* write ATmega port designated for 6502 data bus (DBUS) */
    P6502_DBUS(DDR) = 0xff;
    P6502_DBUS(PORT) = v;
}

/* 6502 control */
static inline void setup_ictl()
{
    /* setup ATmega port designated for 6502 input control pins for output */
    P6502_ICTL(DDR) = 0xff;
}

static inline void setup_octl()
{
    /* setup ATmega port designated for 6502 output control pins for input */
    P6502_OCTL(DDR) = 0;
    P6502_OCTL(PORT) = 0;
}

static inline void write_ictl(uint8_t m, uint8_t v)
{
    /* write ATmega port designated for 6502 input control pins */
    P6502_ICTL(PORT) = (P6502_ICTL(PORT) & ~m) | (v & m);
}

static inline uint8_t read_octl()
{
    /* read ATmega port designated for 6502 output control pins */
    return P6502_OCTL(PIN);
}

/* clock */
#define CLK_DELAY()                     ((void)0)

static inline void clock_rise()
{
    write_ictl(P6502_ICTL_PIN(PHI2), 0xff);
    CLK_DELAY();
}

static inline void clock_fall()
{
    write_ictl(P6502_ICTL_PIN(PHI2), 0);
    CLK_DELAY();
}

static void clock_cycle(size_t n = 1)
{
    for (size_t i = 0; n > i; i++)
    {
        clock_rise();
        clock_fall();
    }
}

/* reset */
#define RESB_0_NCLK                     2
#define RESB_1_NCLK                     7

static void reset()
{
    write_ictl(P6502_ICTL_PIN(RESB), 0);
    clock_cycle(RESB_0_NCLK);
    write_ictl(P6502_ICTL_PIN(RESB), 0xff);
    clock_cycle(RESB_1_NCLK);
}

/* data read/write */
#define RAMSIZE                         (4 * 1024)
#define RAMMASK                         (RAMSIZE - 1)
#define ROMSIZE                         (16 * 1024)
#define ROMMASK                         (ROMSIZE - 1)
#define RAMADDR(a)                      ((a) < 0x8000)
#define MIOADDR(a)                      ((a) < 128)

static uint8_t ram[RAMSIZE];

static const uint8_t rom[ROMSIZE] PROGMEM =
{
    #include "6502rom.h"
};

static void write_mio(uint16_t addr, uint8_t data);

static inline uint8_t read_data(uint16_t addr)
{
    if (RAMADDR(addr))
        return ram[addr & RAMMASK];
    else
        return pgm_read_byte(&rom[addr & ROMMASK]);
}

static inline void write_data(uint16_t addr, uint8_t data)
{
    if (MIOADDR(addr))
        write_mio(addr, data);
    else
        ram[addr & RAMMASK] = data;
}

/* memory mapped I/O */
#define MIO_IRQN                        (0)
#define MIO_OREG                        (64)
#define MIO_OREGSIZE                    (32)
#define MIO_OREGMASK                    (MIO_OREGSIZE - 1)
#define MIO_IREG                        (96)
#define MIO_IREGSIZE                    (32)
#define MIO_IREGMASK                    (MIO_IREGSIZE - 1)

static void write_mio(uint16_t addr, uint8_t data)
{
    switch (addr)
    {
    case MIO_IRQN:
        if (0 == data)
        {
            ram[addr] = data;
            write_ictl(P6502_ICTL_PIN(IRQB), 0xff);
        }
        break;
    case MIO_OREG:
        ram[addr] = data;
        if (0 != data)
        {
            sei();
            Serial.write(ram + MIO_OREG + 1, data & MIO_OREGMASK);
            cli();
            ram[MIO_OREG] = 0;
            ram[MIO_IRQN] = MIO_OREG;
            write_ictl(P6502_ICTL_PIN(IRQB), 0);
        }
        break;
    case MIO_IREG:
        ram[addr] = data;
        break;
    default:
        ram[addr] = data;
        break;
    }
}

/* debug */
static bool debug_step = 1;
size_t disasm(uint8_t (*read_data)(uint16_t), uint16_t addr, char buf[16]);

static void debug_header()
{
    Serial.begin(1000000);
    Serial.println();
    Serial.println("6502ctl:");
    Serial.println("    s to step");
    Serial.println("    c to continue");
    Serial.println("    b to break");
    Serial.println("    r to reset");
}

static void debug(uint16_t addr, uint8_t data, uint8_t octl)
{
    sei();

    if (debug_step)
    {
        char serbuf[64], disbuf[16];

        if (octl & P6502_OCTL_PIN(SYNC))
            disasm(read_data, addr, disbuf);

        snprintf(serbuf, sizeof serbuf, "%c%c%c%c %04x %02x%s%s",
            octl & P6502_OCTL_PIN(RWB) ? 'r' : 'W',
            octl & P6502_OCTL_PIN(SYNC) ? 'S' : '-',
            octl & P6502_OCTL_PIN(MLB) ? '-' : 'M',
            octl & P6502_OCTL_PIN(VPB) ? '-' : 'V',
            addr,
            data,
            octl & P6502_OCTL_PIN(SYNC) ? " " : "",
            octl & P6502_OCTL_PIN(SYNC) ? disbuf : "");
        Serial.println(serbuf);

#if TMSR
        float avg = TMSR_FACT * (float)tmsr_tcnt / (float)tmsr_lcnt;
        float iavg, favg;
        favg = modff(avg, &iavg);
        snprintf(serbuf, sizeof serbuf, "avg %d.%d us",
            (int)iavg, (int)(favg * 1000));
        Serial.println(serbuf);
#endif
    }

    while (debug_step || Serial.available())
    {
        int c;
        while (-1 == (c = Serial.read()))
            ;
        switch (c)
        {
        case 's': /* step */
            if (debug_step)
                goto exit;
            break;
        case 'c': /* continue */
            if (debug_step)
            {
                debug_step = false;
                goto exit;
            }
            break;
        case 'b': /* break */
            if (!debug_step)
            {
                debug_step = true;
                goto exit;
            }
            break;
        case 'r': /* reset */
            reset();
            goto exit;
        }
    }

exit:
    cli();
}

static inline bool debug_available()
{
    /*
     * ATmega 2560 datasheet 23.6.2
     *
     * UCSRnA – USART MSPIM Control and Status Register n A
     *
     * Bit 7 - RXCn: USART Receive Complete
     *
     * This flag bit is set when there are unread data in the receive buffer and cleared when the
     * receive buffer is empty (that is, does not contain any unread data).
     */
    return debug_step || (UCSR0A & (1 << RXC0));
}

void setup()
{
    setup_abus();
    setup_dbus();
    setup_ictl();
    setup_octl();

    write_ictl(0xff,
        P6502_ICTL_PIN(RDY) |
        P6502_ICTL_PIN(IRQB) |
        P6502_ICTL_PIN(NMIB) |
        P6502_ICTL_PIN(SOB) |
        P6502_ICTL_PIN(BE));

    reset();

    debug_header();
}

void loop()
{
    uint16_t addr;
    uint8_t data;
    uint8_t octl;

    cli();

    TMSR_INIT(time);

    for (;;)
    {
        clock_rise();

        octl = read_octl();
        addr = read_abus();

        if (octl & P6502_OCTL_PIN(RWB))
        {
            data = read_data(addr);
            write_dbus(data);
        }
        else
        {
            data = read_dbus();
            write_data(addr, data);
        }

        clock_fall();

        if (debug_available())
        {
            TMSR_LOOP(time);
            debug(addr, data, octl);
        }
        else
            TMSR_LOOP(time);
    }
}
$abcdeabcde151015202530354045505560fghijfghij
MOS 6502 CPUBreakout