flycast/core/hw/modem/modem.cpp

760 lines
17 KiB
C++
Raw Normal View History

/*
Originally based on nullDC: nullExtDev/modem.cpp
Created on: Sep 9, 2018
Copyright 2018 skmp, flyinghead
This file is part of reicast.
reicast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
reicast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with reicast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "modem.h"
#include "modem_regs.h"
#include "hw/holly/holly_intc.h"
#include "hw/sh4/sh4_sched.h"
#include "oslib/oslib.h"
#include "picoppp.h"
#define start_pppd start_pico
#define stop_pppd stop_pico
#define write_pppd write_pico
#define read_pppd read_pico
#define MODEM_COUNTRY_RES 0
#define MODEM_COUNTRY_JAP 1
#define MODEM_COUNTRY_USA 2
#define MODEM_MAKER_SEGA 0
#define MODEM_MAKER_ROCKWELL 1
#define MODEM_DEVICE_TYPE_336K 0
#ifdef RELEASE
#define LOG(...)
#else
#define LOG(...) do { printf("[%d.%03d] MODEM ", (int)os_GetSeconds(), (int)(os_GetSeconds() * 1000) % 1000); printf(__VA_ARGS__); putchar('\n'); } while (false);
#endif
const static u32 MODEM_ID[2] =
{
MODEM_COUNTRY_RES,
(MODEM_MAKER_ROCKWELL<<4) | (MODEM_DEVICE_TYPE_336K),
};
static modemreg_t modem_regs;
static u8 dspram[0x1000];
int modem_sched;
enum ModemStates
{
MS_INVALID, //needs reset
MS_RESET, //reset is low
MS_RESETING, //reset is hi
MS_ST_CONTROLER, //Controller self test
MS_ST_DSP, //DSP self test
MS_END_DSP, //DSP self test end
MS_NORMAL, //Normal operation
};
static ModemStates state = MS_INVALID;
enum ConnectState
{
DISCONNECTED,
DIALING,
RINGING,
HANDSHAKING,
PRE_CONNECTED,
CONNECTED,
};
static ConnectState connect_state = DISCONNECTED;
static void schedule_callback(int ms);
static void update_interrupt()
{
modem_regs.reg1e.RDBIA = modem_regs.reg1e.RDBIE && modem_regs.reg1e.RDBF;
modem_regs.reg1e.TDBIA = modem_regs.reg1e.TDBIE && modem_regs.reg1e.TDBE;
if (modem_regs.reg1f.NCIA || modem_regs.reg1f.NSIA || modem_regs.reg1e.RDBIA || modem_regs.reg1e.TDBIA)
asic_RaiseInterrupt(holly_EXP_8BIT);
else
asic_CancelInterrupt(holly_EXP_8BIT);
}
static u32 get_masked_status(u32 reg)
{
u8 int_mask = dspram[regs_int_mask_addr[reg]];
return int_mask & modem_regs.ptr[reg];
}
#define SET_STATUS_BIT(reg, bit, value) do { \
if ((bit) != (value)) \
{ \
if (!modem_regs.reg1f.NSIE) \
{ \
bit = (value); \
} \
else \
{ \
u8 before = get_masked_status(reg); \
bit = (value); \
if (before != get_masked_status(reg)) \
modem_regs.reg1f.NSIA = 1; \
} \
} \
} while (false);
static void ControllerTestEnd();
static void DSPTestStart();
static void DSPTestEnd();
static double last_dial_time;
#ifndef RELEASE
static double last_comm_stats;
static int sent_bytes;
static int recvd_bytes;
static FILE *recv_fp;
static FILE *sent_fp;
#endif
static int modem_sched_func(int tag, int c, int j)
{
#ifndef RELEASE
if (os_GetSeconds() - last_comm_stats >= 2)
{
if (last_comm_stats != 0)
{
printf("Stats sent %d (%.2f kB/s) received %d (%.2f kB/s) TDBE %d RDBF %d\n", sent_bytes, sent_bytes / 2000.0,
recvd_bytes, recvd_bytes / 2000.0,
modem_regs.reg1e.TDBE, modem_regs.reg1e.RDBF);
sent_bytes = 0;
recvd_bytes = 0;
}
last_comm_stats = os_GetSeconds();
}
#endif
int callback_cycles = 0;
switch (state)
{
case MS_ST_CONTROLER:
ControllerTestEnd();
break;
case MS_ST_DSP:
DSPTestStart();
break;
case MS_END_DSP:
DSPTestEnd();
break;
case MS_NORMAL:
modem_regs.reg1f.NEWC = 0; // Not needed when state is CONNECTED but who cares
switch (connect_state)
{
case DIALING:
if (last_dial_time != 0 && os_GetSeconds() - last_dial_time > 0.99)
{
LOG("Switching to RINGING state");
connect_state = RINGING;
schedule_callback(100);
}
else
{
last_dial_time = os_GetSeconds();
modem_regs.reg1e.TDBE = 1;
schedule_callback(1000); // To switch to Ringing state
}
break;
case RINGING:
last_dial_time = 0;
LOG("\t\t *** RINGING STATE ***");
modem_regs.reg1f.NEWS = 1;
if (!modem_regs.reg09.DATA)
{
SET_STATUS_BIT(0x0f, modem_regs.reg0f.RI, 1);
SET_STATUS_BIT(0x0b, modem_regs.reg0b.ATV25, 1);
}
break;
case HANDSHAKING:
LOG("\t\t *** HANDSHAKING STATE ***");
if (modem_regs.reg12 == 0xAA)
{
// V8 AUTO mode
dspram[0x302] |= 1 << 3; // ANSam detected
}
modem_regs.reg1f.NEWS = 1;
SET_STATUS_BIT(0x0f, modem_regs.reg0f.RI, 0);
SET_STATUS_BIT(0x0b, modem_regs.reg0b.ATV25, 0);
callback_cycles = SH4_MAIN_CLOCK / 1000000 * 500; // 500 ms
connect_state = PRE_CONNECTED;
break;
case PRE_CONNECTED:
printf("MODEM Connected\n");
if (modem_regs.reg03.RLSDE)
SET_STATUS_BIT(0x0f, modem_regs.reg0f.RLSD, 1);
if (modem_regs.reg12 == 0xAA)
{
// V8 AUTO mode
dspram[0x302] |= 1 << 4; // protocol octet received
dspram[0x301] |= 1 << 4; // JM detected
dspram[0x303] |= 0xE0; // Received protocol bits (?)
dspram[0x2e3] |= 5; // Symbol rate 3429
dspram[0x239] |= 12; // RTD 0 @ 3429 sym rate
if (modem_regs.reg08.ASYN)
{
modem_regs.reg12 = 0xce; // CONF V34 - K56flex
modem_regs.reg0e.SPEED = 0x10; // 33.6k
}
else
{
// Force the driver to ASYN=1 so it sends raw data
modem_regs.reg12 = 0xa1; // CONF V23 75 TX/1200 RX
modem_regs.reg0e.SPEED = 0x02; // 1.2k
}
if (modem_regs.reg1f.NSIE)
{
// CONF
if (dspram[regs_int_mask_addr[0x12]] & (1 << 7))
modem_regs.reg1f.NSIA = 1;
// SPEED
if (dspram[regs_int_mask_addr[0x0e]] & 0x1f)
modem_regs.reg1f.NSIA = 1;
}
modem_regs.reg09.DATA = 1;
modem_regs.reg15.AUTO = 0;
}
modem_regs.reg14 = 0x00; // ABCODE: no error
if (modem_regs.reg1f.NSIE)
{
// ABCODE
if (dspram[regs_int_mask_addr[0x014]])
modem_regs.reg1f.NSIA = 1;
}
modem_regs.reg1f.NEWS = 1;
SET_STATUS_BIT(0x0f, modem_regs.reg0f.DSR, 1);
if (modem_regs.reg02.v0.RTSDE)
SET_STATUS_BIT(0x0f, modem_regs.reg0f.RTSDT, 1);
// What is this? This is required for games to detect the connection
SET_STATUS_BIT(0x0f, modem_regs.reg0f.FED, 1);
// V.34 Remote Modem Data Rate Capability
dspram[0x208] = 0xff; // 2.4 - 19.2 kpbs supported
dspram[0x209] = 0xbf; // 21.6 - 33.6 kpbs supported, asymmetric supported
start_pppd();
connect_state = CONNECTED;
callback_cycles = SH4_MAIN_CLOCK / 1000000 * 238; // 238 us
break;
case CONNECTED:
#ifndef RELEASE
static bool mem_dumped;
if (!mem_dumped)
{
mem_dumped = true;
for (int i = 0 ; i < sizeof(modem_regs); i++)
LOG("modem_regs %02x == %02x", i, modem_regs.ptr[i]);
}
#endif
if (!modem_regs.reg1e.RDBF)
{
int c = read_pppd();
if (c >= 0)
{
//LOG("pppd received %02x", c);
#ifndef RELEASE
recvd_bytes++;
#endif
modem_regs.reg00 = c & 0xFF;
modem_regs.reg1e.RDBF = 1;
if (modem_regs.reg04.FIFOEN)
SET_STATUS_BIT(0x0c, modem_regs.reg0c.RXFNE, 1);
SET_STATUS_BIT(0x01, modem_regs.reg01.RXHF, 1);
}
}
modem_regs.reg1e.TDBE = 1;
callback_cycles = SH4_MAIN_CLOCK / 1000000 * 238; // 238 us
break;
default:
break;
}
break;
default:
break;
}
update_interrupt();
return callback_cycles;
}
void ModemInit()
{
modem_sched = sh4_sched_register(0, &modem_sched_func);
}
static void schedule_callback(int ms)
{
sh4_sched_request(modem_sched, SH4_MAIN_CLOCK / 1000 * ms);
}
#define SetReg16(rh,rl,v) {modem_regs.ptr[rh]=(v)>>8;modem_regs.ptr[rl]=(v)&0xFF; }
static void NormalDefaultRegs()
{
verify(state == MS_NORMAL);
verify(sizeof(regs_write_mask) == sizeof(modem_regs));
verify(sizeof(por_dspram) == sizeof(dspram));
// Default values for normal state
memset(&modem_regs, 0, sizeof(modem_regs));
memcpy(dspram, por_dspram, sizeof(dspram));
modem_regs.reg05.CEQ = 1;
modem_regs.reg12 = 0x76; // CONF: V.32 bis TCM 14400
modem_regs.reg09.DATA = 1;
modem_regs.reg09.DTMF = 1;
modem_regs.reg15.HWRWK = 1;
modem_regs.reg07.RDLE = 1;
modem_regs.reg15.RDWK = 1;
modem_regs.reg03.RLSDE = 1;
modem_regs.reg02.TDE = 1;
modem_regs.reg13.TLVL = 0x9;
modem_regs.reg1e.TDBE = 1;
connect_state = DISCONNECTED;
last_dial_time = 0;
}
static void DSPTestEnd()
{
verify(state==MS_END_DSP);
state=MS_NORMAL;
LOG("DSPTestEnd");
NormalDefaultRegs();
}
static void DSPTestStart()
{
verify(state==MS_ST_DSP);
state=MS_END_DSP;
LOG("DSPTestStart");
modem_regs.reg1e.TDBE = 1;
SetReg16(0x1B,0x1A,0xF083); // EC checksum
SetReg16(0x19,0x18,0x46EE); // Multiplier checksum
SetReg16(0x17,0x16,0x00FA); // RAM checksum
SetReg16(0x15,0x14,0x0A09); // ROM checksum
SetReg16(0x13,0x12,0x3730); // Part number
SetReg16(0x11,0x00,0x2041); // Revision level
schedule_callback(50);
}
static void ControllerTestEnd()
{
verify(state==MS_ST_CONTROLER);
state=MS_ST_DSP;
schedule_callback(50);
}
//End the reset and start internal tests
static void ControllerTestStart()
{
verify(state==MS_RESETING);
//Set Self test values :)
state=MS_ST_CONTROLER;
//k, lets set values
//1E:3 -> set
modem_regs.reg1e.TDBE=1;
/*
RAM1 Checksum = 0xEA3C or 0x451
RAM2 Checksum = 0x5536 or 0x49A5
ROM1 Checksum = 0x5F4C
ROM2 Checksum = 0x3835 or 0x3836
Timer/ROM/RAM = 0x801 or 0xDE00
Part Number = 0x3730 or 0x3731
Revision Level = 0x4241
*/
SetReg16(0x1D,0x1C,0xEA3C);
SetReg16(0x1B,0x1A,0x5536);
SetReg16(0x19,0x18,0x5F4C);
SetReg16(0x17,0x16,0x3835);
SetReg16(0x15,0x14,0x801);
SetReg16(0x13,0x12,0x3730);
SetReg16(0x11,0x0,0x4241);
ControllerTestEnd();
}
static void modem_reset(u32 v)
{
if (v==0)
{
memset(&modem_regs,0,sizeof(modem_regs));
state=MS_RESET;
LOG("Modem reset start ...");
}
else
{
stop_pppd();
memset(&modem_regs,0,sizeof(modem_regs));
state=MS_RESETING;
modem_regs.ptr[0x20]=1;
ControllerTestStart();
printf("MODEM Reset\n");
}
}
static void check_start_handshake()
{
if (modem_regs.reg09.DTR
&& (modem_regs.reg09.DATA || modem_regs.reg15.AUTO)
&& connect_state == RINGING)
{
LOG("DTR asserted. starting handshaking");
connect_state = HANDSHAKING;
schedule_callback(1);
}
}
static bool word_dspram_write;
static bool module_download;
static u32 reg1b_save;
static u8 download_crc;
static void ModemNormalWrite(u32 reg, u32 data)
{
#ifndef RELEASE
if (recv_fp == NULL)
{
recv_fp = fopen("ppp_recv.dump", "w");
sent_fp = fopen("ppp_sent.dump", "w");
}
#endif
//if (!module_download && reg != 0x10)
// LOG("ModemNormalWrite : %03X=%X", reg,data);
u32 old = modem_regs.ptr[reg];
modem_regs.ptr[reg] = (old & ~regs_write_mask[reg]) | (data & regs_write_mask[reg]);
switch(reg)
{
case 0x02:
modem_regs.reg0f.RTSDT = modem_regs.reg02.v0.RTSDE && connect_state == CONNECTED;
break;
case 0x06:
LOG("PEN = %d", modem_regs.reg06.PEN);
if (modem_regs.reg06.PEN)
die("PEN = 1");
if (modem_regs.reg06.HDLC)
die("HDLC = 1");
break;
case 0x08:
LOG("TPDM = %d ASYN = %d", modem_regs.reg08.TPDM, modem_regs.reg08.ASYN);
break;
case 0x09:
check_start_handshake(); // DTR and DATA
break;
case 0x10: // TBUFFER
if (module_download)
{
download_crc = (download_crc << 1) + ((download_crc & 0x80) >> 7) + (data & 0xFF);
}
else if (connect_state == DISCONNECTED || connect_state == DIALING)
{
//LOG("ModemNormalWrite : TBUFFER = %X", data);
if (connect_state == DISCONNECTED)
{
printf("MODEM Dialing\n");
connect_state = DIALING;
}
schedule_callback(100);
}
else if (connect_state == CONNECTED && modem_regs.reg08.RTS)
{
//LOG("ModemNormalWrite : TBUFFER = %X", data);
#ifndef RELEASE
sent_bytes++;
if (sent_fp)
fputc(data, sent_fp);
#endif
write_pppd(data);
modem_regs.reg1e.TDBE = 0;
}
break;
case 0x11:
LOG("PARSL = %d", modem_regs.reg11.PARSL);
die("PARSL");
break;
case 0x14: // ABCODE
if (data == 0x4f || data == 0x5f)
{
reg1b_save = modem_regs.ptr[0x1b];
modem_regs.ptr[0x1b] = data;
//LOG("SPX/CTL download initiated");
module_download = true;
download_crc = 0;
}
else if (data == 0 && module_download)
{
// If module download is in progress, this signals the end of it
modem_regs.reg17 = 0xFF;
modem_regs.reg16 = download_crc;
// Restore reg 1b
modem_regs.ptr[0x1b] = reg1b_save;
module_download = false;
//LOG("SPX/CTL download finished CRC %02x", download_crc);
}
break;
case 0x15:
check_start_handshake(); // AUTO
break;
//Data Write Regs, for transfers to DSP
case 0x18: // MEDAL
break;
case 0x19: // MEDAM
word_dspram_write = true;
break;
case 0x1a:
verify(connect_state != CONNECTED || !modem_regs.reg1a.SCIBE);
break;
//Address low
case 0x1C: // MEADDL
break;
case 0x1D: // MEADDH
if (modem_regs.reg1c_1d.MEMW && !(old & (1 << 5)))
{
word_dspram_write = false;
}
if (modem_regs.reg1c_1d.MEACC)
{
modem_regs.reg1c_1d.MEACC = 0;
modem_regs.reg1f.NEWS = 1;
u32 dspram_addr = modem_regs.reg1c_1d.MEMADD_l | (modem_regs.reg1c_1d.MEMADD_h << 8);
if (modem_regs.reg1c_1d.MEMW)
{
//LOG("DSP mem Write%s address %08x = %x",
// word_dspram_write ? " (w)" : "",
// dspram_addr,
// modem_regs.reg18_19 );
if (word_dspram_write)
{
if (dspram_addr & 1)
{
dspram[dspram_addr] = modem_regs.reg18_19 & 0xFF;
dspram[dspram_addr + 1] = (modem_regs.reg18_19 >> 8) & 0xFF;
}
else
*(u16*)&dspram[dspram_addr] = modem_regs.reg18_19;
}
else
dspram[dspram_addr] = modem_regs.reg18_19 & 0xFF;
}
else
{
if (dspram_addr & 1)
modem_regs.reg18_19 = dspram[dspram_addr] | (dspram[dspram_addr + 1] << 8);
else
modem_regs.reg18_19 = *(u16*)&dspram[dspram_addr];
//LOG("DSP mem Read address %08x == %x",
// dspram_addr,
// modem_regs.reg18_19 );
}
}
break;
case 0x1F:
if (!modem_regs.reg1f.NCIE)
modem_regs.reg1f.NCIA = 0;
if (modem_regs.reg1f.NEWC)
{
if(modem_regs.reg1a.SFRES)
{
modem_regs.reg1a.SFRES = 0;
LOG("Soft Reset SET && NEWC, executing reset and init");
modem_reset(1);
}
else
{
modem_regs.reg1f.NEWC = 0; // accept the settings
if (modem_regs.reg1f.NCIE)
modem_regs.reg1f.NCIA = 1;
LOG("NEWC CONF=%x", modem_regs.reg12);
}
}
// Don't allow NEWS to be set if 0
if ((old & (1 << 3)) == 0)
modem_regs.reg1f.NEWS = 0;
if (!modem_regs.reg1f.NEWS)
{
modem_regs.reg1f.NSIA = 0;
}
break;
default:
//printf("ModemNormalWrite : undef %03X=%X\n",reg,data);
break;
}
update_interrupt();
}
u32 ModemReadMem_A0_006(u32 addr, u32 size)
{
u32 reg=addr&0x7FF;
verify((reg&3)==0);
reg>>=2;
if (reg<0x100)
{
verify(reg<=1);
return MODEM_ID[reg];
}
else
{
reg-=0x100;
if (reg<0x21)
{
if (state==MS_NORMAL)
{
// Dial tone is detected if TONEA, TONEB and TONEC are set
//if (reg==0xF)
{
SET_STATUS_BIT(0x0f, modem_regs.reg0f.CTS, modem_regs.reg08.RTS && connect_state == CONNECTED);
SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEA, connect_state == DISCONNECTED);
SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEB, connect_state == DISCONNECTED);
SET_STATUS_BIT(0x0b, modem_regs.reg0b.TONEC, connect_state == DISCONNECTED);
// FIXME This should be reset if transmit buffer is full
if (modem_regs.reg04.FIFOEN || module_download)
SET_STATUS_BIT(0x0d, modem_regs.reg0d.TXFNF, 1);
}
u8 data = modem_regs.ptr[reg];
if (reg == 0x00) // RBUFFER
{
//LOG("Read RBUFFER = %X", data);
modem_regs.reg1e.RDBF = 0;
SET_STATUS_BIT(0x0c, modem_regs.reg0c.RXFNE, 0);
SET_STATUS_BIT(0x01, modem_regs.reg01.RXHF, 0);
#ifndef RELEASE
if (connect_state == CONNECTED && recv_fp)
fputc(data, recv_fp);
#endif
update_interrupt();
}
else if (reg == 0x16 || reg == 0x17)
{
//LOG("SECTXB / SECTXB being read %02x", reg)
}
//else
// LOG("Read Reg %03x = %x", reg, data);
return data;
}
else if (state==MS_ST_CONTROLER || state==MS_ST_DSP)
{
if (reg==0x10)
{
modem_regs.reg1e.TDBE=0;
return 0;
}
else
{
return modem_regs.ptr[reg];
}
}
else if (state==MS_RESETING)
{
return 0; //still reset
}
else
{
//LOG("Read (reset) reg %03x == %x", reg, modem_regs.ptr[reg]);
return 0;
}
}
else
{
LOG("modem reg %03X read -- wtf is it ?",reg);
return 0;
}
}
}
void ModemWriteMem_A0_006(u32 addr, u32 data, u32 size)
{
u32 reg=addr&0x7FF;
verify((reg&3)==0);
reg>>=2;
if (reg<0x100)
{
verify(reg<=1);
LOG("modem reg %03X write -- MODEM ID?!",reg);
}
else
{
reg-=0x100;
if (reg<0x20)
{
if (state==MS_NORMAL)
{
ModemNormalWrite(reg,data);
}
else
{
LOG("modem reg %03X write %X -- undef state?",reg,data);
}
return;
}
else if (reg==0x20)
{
//Hard reset
modem_reset(data);
}
else
{
LOG("modem reg %03X write %X -- wtf is it?",reg,data);
return;
}
}
}