/*--------------------------------------------------------------------
  Sega CD Transfer Compatible Dumper / Backup RAM Cartridge Writer 
  for Arduino
  Arduino用 Sega CD Transfer対応 ダンプ処理+バックアップRAMカートリッジ
  書き込み処理 (XMODEM対応)
 About this:
  You can dump via USB serial communication using Sega CD Transfer.
  Use the terminal software to receive the dump file via XMODEM communication.
  (COM Setting: 115200BPS,8bit,none,0,1)
  You can also use RAM_DUMP.BIN and RAM_WRTE.BIN to read and write to the 
  backup RAM cartridge.
  Sega CD Transferを使ってメガCDのダンプデータをUSB経由で読み出します。
  Teraterm等のターミナルソフトを使い、XMODEM通信でダンプファイルを受信します。
  (COM通信設定: 115200BPS,8bit,none,0,1)
  また、RAM_DUMP.BINとRAM_WRTE.BINを利用してバックアップRAMカートリッジの
  読書きが可能です。
 NOTE:
   This sketch file is for use with Arduino Leonardo/Arduino Pro Micro only.
   Please refer to the official page of sparkfun for how to use Pro Micro.
    https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide/
 注意:
  このスケッチは、Arduino Leonardo/Pro Micro(ATmega 32U4搭載)専用です。
  メガドライブ側の読み出しを最速で行うためポート(PinD)アクセスを利用しており、
  他マイコンと互換性がありません。
[Cable Connection]
 +---------+------------+
 |Genesis  |Pro Micro   |
 |MD(DSUB9)|(Atmega32U4)|
 +---------+------------+
 |  1(D0)  |Pin3 (PD0)  |
 +---------+------------+
 |  2(D1)  |Pin2 (PD1)  |
 +---------+------------+
 |  3(D2)  |Pin1 (PD2)  |
 +---------+------------+
 |  4(D3)  |Pin0 (PD3)  |
 +---------+------------+
 |  5(+5v) | NC         |
 +---------+------------+
 |  6(ERR) |Pin4 (PD4)  |
 +---------+------------+
 |  7(ALF) |Pin5 (PC6)  |
 +---------+------------+
 |  8(GND) | GND        |
 +---------+------------+
 |  9(SEL) |Pin6 (PD7)  |
 +---------+------------+
[Rsest button (Pro Micro)]
  RST -- GND
[Dumper/Writer Select S/W (Pro Micro)]
  Pin10 -- GND
   OFF: Dumper
   ON : Writer
 note:
  After selecting Dumper or Writer with this switch,
   press the reset button to enable the setting.
  このスイッチでDumperかWriterを選択後、リセットボタンを押す事で設定が
  有効になります。
[Reference]
 Sega CD Transfer Suite
   https://www.retrodev.com/transfer.html
 Extract the Mega CD BIOS using a backup RAM cartridge
   http://blog.livedoor.jp/scrap_a/archives/24228164.html
 UCON64
   https://ucon64.sourceforge.io/
 I/O ポートの制御
   http://rio.la.coocan.jp/lab/driver24/004hardware.html
 XMODEM通信
   https://s-suzuki.hatenadiary.org/entry/20150104/1420390724
 
 2020.11.14 BLUE( qze12045@nifty.com )
--------------------------------------------------------------------*/
//For Debug
#define __XmodemTest    0       // 1:XMODEM test(send dummy data)
 
// Sega CD Transfer Control
#define     ERRWAIT     9000    // ERR WAIT
#define     ERR         0x10    // Error bit mask
 
#define WAIT_ERRLo(x) for(int i=0 ; i<x ; i++ ){if(!(PIND & ERR)) break;}
#define WAIT_ERRHi(x) for(int i=0 ; i<x ; i++ ){if(PIND & ERR) break;}
 
// Dump/Write select Port
#define     MD_SW       10      // DUMP/WRITE select swicth
 
//  Sega CD Transfer Ports
#define     MD_D0       3       // PD0/Pin3
#define     MD_D1       2       // PD1/Pin2
#define     MD_D2       1       // PD2/Pin1(RX1)
#define     MD_D3       0       // PD3/Pin0(TX1)
#define     MD_ERR      4       // PD4/Pin4 Error信号
#define     MD_SEL      6       // PD7/Pin6 Select信号
#define     MD_ALF      5       // PC6/Pin5 Auto Line Feed信号ポート番号
 
// XMODEM Parameter
#define     _BAUDRATE   115200  // 通信速度 115200Bps
#define     _TIMEOUT    1000    // 通信タイムアウト(2秒)
#define     _RETRY      60      // ACK/NAK受信リトライ回数
#define     _BLKSIZE    128     // 通信パケットサイズ(128Byte)
#define     _BLKRETRY   200     // ブロック送信リトライ回数
 
// XMODEM code
#define     SOH         0x01    // ブロック先頭
#define     EOT         0x04    // 通信終了
#define     ACK         0x06    // 通信正常
#define     NAK         0x15    // 通信開始/パケット再送付要求
#define     CAN         0x18    // 通信中断
 
// XMODEM packet
typedef struct xmodem {
    char    soh;                // SOH
    char    blk;                // Block Number
    char    _blk;               // ~Block Number(BLock番号補数)
    char    data[_BLKSIZE];     // data / 通信データ
    char    checksum;           // Checksum
} XMODEM;
 
XMODEM  buf;                    // send pachet / 送信バッファ
int     xmodem_phese = 0;       // 通信フェーズ(0:初回NAK待ち、1:データ送信中、2:EOT送信前、3:完了)
char    block;                  // ブロック番号
unsigned short  check_sum;      // チェックサム
 
// dump data buffer
char    data_buf[_BLKSIZE];     // ダンプデータ用バッファ
 
bool dump_mode = true;          // Dump/Write status
 
//////////////////////////////////////
// serial read (1byte)
// Received code (Timeout:-1)
//////////////////////////////////////
char recv_c(void)
{
    char code;
    for (int i=0 ; i<_RETRY ;i++)
    {
        Serial.readBytes(&code, 1);
        if ( (code != ACK) && (code!=NAK) )
        {
            // retry
            continue;
        }
        else
        {
            if((!xmodem_phese)&&(code!=NAK))
            {
                // retry
                continue;
            }
            return code;
        }
    }
    return -1;
}
 
//////////////////////////////////////
// initialize XMODEM
//////////////////////////////////////
void xmodem_init(void)
{
    // initial phase
    xmodem_phese = 0;
    // set first
    block = 1;
    // set timeout
    Serial.setTimeout(_TIMEOUT);
}
 
//////////////////////////////////////
// send XMODEM
//////////////////////////////////////
int xmodem_send(char *data)
{
char status;
    switch(xmodem_phese)
    {
    // first NAK
    case 0:
        if (recv_c()==NAK)
        {
            xmodem_phese = 1;
        }
        else
        {
            xmodem_phese = 2;
            return 1;
        }
    // XMODEM:Send Packet
    case 1:
        buf.soh = SOH;
        buf.blk = block;
        buf._blk = ~block;
        check_sum = 0;
        for( int i=0 ; i < _BLKSIZE ; i++ )
        {
            buf.data[i] = data[i];
            check_sum += data[i];
        }
        buf.checksum = (char)(check_sum&0xff);
        
        for( int i=0 ; i < _BLKRETRY ; i++ )
        {
            Serial.write((const char*)&buf, sizeof(buf));
            status = recv_c();
            // ACK(normal?)
            if (status == ACK)
            {
                block++;
                return 0;
            }
            // NAK(retry?)
            if (status == NAK)
            {
                continue;
            }
            // other(CAN?)
            else
                break;
        }
        // retry over(XMODEM:error)
        xmodem_phese = 2;
        return 1;
    default:
        break;
    }
    return 0;
}
 
//////////////////////////////////////
// Recive XMODEM packet
//////////////////////////////////////
int xmodem_recv(char *data)
{
    if ( xmodem_phese >= 2 )
    {
        // finish
        return EOT;
    }
    char code = ACK;
    // first send NAK?
    if ( !xmodem_phese )
    {
        code= NAK;
        xmodem_phese = 1;
    }
    for ( int i = 0 ; i < _RETRY ; i++ )
    {
        // send packet request
        Serial.write(code);
        
        // wait SOH
        Serial.readBytes(&code, 1);
        if ( code == EOT )
        {
            // xmodem finish
            xmodem_phese = 2;
            // send ACK
            Serial.write(ACK);
            // finish
            return EOT;
        }
        if ( code == CAN )
        {
            // send ACK
            Serial.write(ACK);
            // finish
            return EOT;
        }
        if ( code == SOH )
        {
            code = 0;
            // recive block number
            Serial.readBytes(&buf.blk, 1);
            Serial.readBytes(&buf._blk, 1);
            // recive packet data
            for ( int j = 0 ; j < _BLKSIZE ; j++ )
            {
                Serial.readBytes(&buf.data[j], 1);
                // calc checksum
                code += buf.data[j];
            }
            // recive checksum
            Serial.readBytes(&buf.checksum, 1);
            
            // block number OK?
            if (( buf.blk == block )&&( buf._blk == ~block ))
            {
                // cheksum OK?
                if (buf.checksum == code)
                {
                    memcpy( data ,&buf.data[0] ,_BLKSIZE);
                    // renewal block number
                    block++;
                    return 0;
                }
            }
        }
        // send NAK
        code= NAK;
    }
    // retry error
    return EOT;
}
 
//////////////////////////////////////
// XMODEM:finish
//////////////////////////////////////
void xmodem_end(void)
{
    if ( xmodem_phese == 3 )
    {
        return;
    }
    Serial.write(EOT);  // Send EOT --bug fix--
    for( int i ; i < 5 ; i++ )
    {
        // send EOT
        Serial.write(EOT);
        char status = recv_c();
        // ACK(normal?)
        if( status == ACK )
        {
            break;
        }
        // NAK(retry?)
        if( status == NAK )
        {
            continue;
        }
    }
    xmodem_phese = 3;
    return;
}
 
//////////////////////////////////////
// Sega CD transfer 1バイト書込み
//////////////////////////////////////
void byte_write(unsigned char data )
{
    noInterrupts(); //Disable Interrupts
 
    //  Wait ERR=Lo
    WAIT_ERRLo(ERRWAIT);
 
    // write data(high)
    PORTD = (data>>4);
 
    // Auto Line Feed = Hi
    digitalWrite(MD_ALF, HIGH );
 
    // Wait ERR=Hi
    WAIT_ERRHi(ERRWAIT);
 
    // Auto Line Feed = Lo
    digitalWrite(MD_ALF, LOW );
 
    //  Wait ERR=Lo
    WAIT_ERRLo(ERRWAIT);
 
    // write data(low)
    PORTD = data & 0xf;
 
    // Auto Line Feed = Hi
    digitalWrite(MD_ALF, HIGH );
    // Wait ERR=Hi
    WAIT_ERRHi(ERRWAIT);
 
    // Auto Line Feed = Lo
    digitalWrite(MD_ALF, LOW );
 
    interrupts(); //Enable Interrupts
 
}
 
//////////////////////////////////////
// Sega CD transfer:dump 1byte
//////////////////////////////////////
unsigned char byte_dump(void)
{
#if __XmodemTest
static unsigned char d = 0;
    return d++;
#endif //__XmodemTest
 
unsigned char   data=0;
 
    noInterrupts(); //Disable Interrupts
 
    // Auto Line Feed = Hi
    digitalWrite(MD_ALF, HIGH );
    // Wait ERR=Hi
    WAIT_ERRHi(ERRWAIT);
 
    data = PIND & 0xf;
 
    // Auto Line Feed = Lo
    digitalWrite(MD_ALF, LOW );
    // Wait  ERR=Lo
    WAIT_ERRLo(ERRWAIT);
 
    // Auto Line Feed = Hi
    digitalWrite(MD_ALF, HIGH );
    // Wait ERR=Hi
    WAIT_ERRHi(ERRWAIT);
 
    data |= (PIND & 0xf)<<4;
 
    // Auto Line Feed = Lo
    digitalWrite(MD_ALF, LOW );
    //  Wait ERR=Lo
    WAIT_ERRLo(ERRWAIT);
 
    interrupts(); //Enable Interrupts
 
    return data;
}
 
//////////////////////////////////////
// read dump size
//////////////////////////////////////
unsigned char read_dump_size(void)
{
#if __XmodemTest
static unsigned char d = 0;
    return d++;
#endif //__XmodemTest
    // Sega CD Transfer connect check
    while (PIND & ERR){}
    return byte_dump();
}
 
//////////////////////////////////////
// initialize
//////////////////////////////////////
void setup() {
// put your setup code here, to run once:
 
    // initialize Dump/Write selector port(INPUT_PULLUP)
    pinMode(MD_SW, INPUT_PULLUP);
    delayMicroseconds(10);
    
    if (digitalRead(MD_SW) == HIGH)
    {
        // initialize port for DUMP(INPUT_PULLUP)
        pinMode(MD_D0, INPUT_PULLUP);
        pinMode(MD_D1, INPUT_PULLUP);
        pinMode(MD_D2, INPUT_PULLUP);
        pinMode(MD_D3, INPUT_PULLUP);
        
        dump_mode = true;
    }
    else
    {
        // initialize port for WRITE(OUTPUT)
        pinMode(MD_D0, OUTPUT);
        pinMode(MD_D1, OUTPUT);
        pinMode(MD_D2, OUTPUT);
        pinMode(MD_D3, OUTPUT);
        
        dump_mode = false;
    }
    pinMode(MD_ERR, INPUT_PULLUP);
    pinMode(MD_SEL, INPUT_PULLUP);
 
    // Auto Line Feed(OUTPUT)
    pinMode(MD_ALF, OUTPUT);
    // Auto Line Feed = Lo
    digitalWrite(MD_ALF, LOW );
 
    Serial.begin(_BAUDRATE);
    // Arduino Leonardo/Arduino Pro Micro only.
    while (!Serial){}
}
 
//////////////////////////////////////
// main loop
//////////////////////////////////////
void loop() {
// put your main code here, to run repeatedly:
 
    if( dump_mode == true )
    {
    long dump = 0;  // count of dump data
    long size = 0;  // size of dump data
    int index = 0;  // index of data_buf[]
 
        Serial.print("Sega CD Transfer Compatible Dumper for Arduino.\r\n");
        Serial.print("Waiting for Respons from Genesis/MegaDrive...\r\n");
 
        size = (long)read_dump_size();
 
        Serial.print("Link Archved.  Dumping ");
        Serial.print((size+1)/2,DEC);
        Serial.print(" Megabits...\r\n");
 
        size = (size+1)<<16;
 
        Serial.print("\r\nPlease start receiving XMODEM protocol.\r\n");
        xmodem_init();
 
        while (dump < size)
        {
            data_buf[index++] = byte_dump();
            if( index >= _BLKSIZE )
            {
                if( xmodem_send(data_buf)!=0 )
                {
                    xmodem_end();
                }
                index = 0;
            }
            dump++;
        }
        xmodem_end();
 
        Serial.print("Dump data send finished.\r\n\r\n");
    }
    if( dump_mode == false )
    {
        Serial.print("Sega CD Transfer Backup RAM Writer for Arduino.\r\n");
        Serial.print("\r\nPlease start to send file with XMODEM protocol.\r\n");
 
        // Sega CD Transfer connect check
        while (PIND & ERR){}
 
        xmodem_init();
 
        while(1)
        {
            if ( xmodem_recv(data_buf) != 0 )
            {
                break;
            }
            for (int i = 0 ; i < _BLKSIZE ; i++ )
            {
                byte_write(data_buf[i]);
            }
        }
        Serial.print("Recive data write finished.\r\n\r\n");
    }
}