// license:GPL-2.0+
// copyright-holders:Juergen Buchmueller, David Haywood
/***********************************************************************
DECO Cassette System machine
***********************************************************************/
#include "emu.h"
#include "machine/decocass_tape.h"
#include "cpu/m6502/m6502.h"
#include "cpu/mcs48/mcs48.h"
#define LOG_CASSETTE_STATE 0
/***************************************************************************
CASSETTE DEVICE INTERFACE
***************************************************************************/
/* number of tape clock pulses per second */
#define TAPE_CLOCKRATE 4800
#define TAPE_CLOCKS_PER_BIT 2
#define TAPE_CLOCKS_PER_BYTE (8 * TAPE_CLOCKS_PER_BIT)
#define TAPE_MSEC_TO_CLOCKS(x) ((x) * TAPE_CLOCKRATE / 1000)
/* Note on a tapes leader-BOT-data-EOT-trailer format:
* A cassette has a transparent piece of tape on both ends,
* leader and trailer. And data tapes also have BOT and EOT
* holes, shortly before the leader and trailer.
* The holes and clear tape are detected using a photo-resitor.
* When rewinding, the BOT/EOT signal will show a short
* pulse and if rewind continues a constant high signal later.
* The specs say the holes are "> 2ms" in length.
*/
/* duration of the clear LEADER (and trailer) of the tape */
#define REGION_LEADER_START_CLOCK 0
#define REGION_LEADER_LEN_CLOCKS TAPE_MSEC_TO_CLOCKS(1000) /* 1s */
#define REGION_LEADER_END_CLOCK (REGION_LEADER_START_CLOCK+REGION_LEADER_LEN_CLOCKS)
/* duration of the GAP between leader and BOT/EOT */
#define REGION_LEADER_GAP_START_CLOCK REGION_LEADER_END_CLOCK
#define REGION_LEADER_GAP_LEN_CLOCKS TAPE_MSEC_TO_CLOCKS(1500) /* 1.5s */
#define REGION_LEADER_GAP_END_CLOCK (REGION_LEADER_GAP_START_CLOCK+REGION_LEADER_GAP_LEN_CLOCKS)
/* duration of BOT/EOT holes */
#define REGION_BOT_START_CLOCK REGION_LEADER_GAP_END_CLOCK
#define REGION_BOT_LEN_CLOCKS TAPE_MSEC_TO_CLOCKS(2.5) /* 0.0025s */
#define REGION_BOT_END_CLOCK (REGION_BOT_START_CLOCK+REGION_BOT_LEN_CLOCKS)
/* gap between BOT/EOT and first/last data block */
#define REGION_BOT_GAP_START_CLOCK REGION_BOT_END_CLOCK
#define REGION_BOT_GAP_LEN_CLOCKS TAPE_MSEC_TO_CLOCKS(300) /* 300ms */
#define REGION_BOT_GAP_END_CLOCK (REGION_BOT_GAP_START_CLOCK+REGION_BOT_GAP_LEN_CLOCKS)
static uint16_t tape_crc16_byte(uint16_t crc, uint8_t data);
DEFINE_DEVICE_TYPE(DECOCASS_TAPE, decocass_tape_device, "decocass_tape", "DECO Cassette Tape")
decocass_tape_device::decocass_tape_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, DECOCASS_TAPE, tag, owner, clock),
m_tape_timer(nullptr),
m_speed(0),
m_region(REGION_LEADER),
m_bitnum(0),
m_clockpos(0),
m_numclocks(0),
m_tape_data(*this, DEVICE_SELF)
{
for (auto & elem : m_crc16)
elem = 0;
}
//-------------------------------------------------
// device_start - device-specific startup
//-------------------------------------------------
void decocass_tape_device::device_start()
{
int curblock, offs, numblocks;
/* fetch the data pointer */
m_tape_timer = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(decocass_tape_device::tape_clock_callback), this));
if (!m_tape_data.found())
return;
/* scan for the first non-empty block in the image */
for (offs = m_tape_data.bytes() - 1; offs >= 0; offs--)
if (m_tape_data[offs] != 0)
break;
numblocks = ((offs | 0xff) + 1) / 256;
assert(numblocks < ARRAY_LENGTH(m_crc16));
/* compute the total length */
m_numclocks = REGION_BOT_GAP_END_CLOCK + numblocks * BYTE_BLOCK_TOTAL * 16 + REGION_BOT_GAP_END_CLOCK;
/* compute CRCs for each block */
for (curblock = 0; curblock < numblocks; curblock++)
{
uint16_t crc = 0;
int testval;
/* first CRC the 256 bytes of data */
for (offs = 256 * curblock; offs < 256 * curblock + 256; offs++)
crc = tape_crc16_byte(crc, m_tape_data[offs]);
/* then find a pair of bytes that will bring the CRC to 0 (any better way than brute force?) */
for (testval = 0; testval < 0x10000; testval++)
if (tape_crc16_byte(tape_crc16_byte(crc, testval >> 8), testval) == 0)
break;
m_crc16[curblock] = testval;
}
/* register states */
save_item(NAME(m_speed));
save_item(NAME(m_bitnum));
save_item(NAME(m_clockpos));
}
//-------------------------------------------------
// device_reset - device-specific reset
//-------------------------------------------------
void decocass_tape_device::device_reset()
{
/* turn the tape off */
change_speed(0);
}
/*-------------------------------------------------
tape_crc16_byte - accumulate 8 bits worth of
CRC data
-------------------------------------------------*/
static uint16_t tape_crc16_byte(uint16_t crc, uint8_t data)
{
int bit;
for (bit = 0; bit < 8; bit++)
{
crc = (crc >> 1) | (crc << 15);
crc ^= (data << 7) & 0x80;
if (crc & 0x80)
crc ^= 0x0120;
data >>= 1;
}
return crc;
}
/*-------------------------------------------------
tape_describe_state - create a string that
describes the state of the tape
-------------------------------------------------*/
const char *decocass_tape_device::describe_state()
{
static char buffer[40];
char temprname[40];
const char *rname = temprname;
if (m_region == REGION_LEADER)
rname = "LEAD";
else if (m_region == REGION_LEADER_GAP)
rname = "LGAP";
else if (m_region == REGION_BOT)
rname = "BOT ";
else if (m_region == REGION_BOT_GAP)
rname = "BGAP";
else if (m_region == REGION_TRAILER)
rname = "TRLR";
else if (m_region == REGION_TRAILER_GAP)
rname = "TGAP";
else if (m_region == REGION_EOT)
rname = "EOT ";
else if (m_region == REGION_EOT_GAP)
rname = "EGAP";
else
{
char tempbname[40];
const char *bname = tempbname;
int clk;
if (m_bytenum <= BYTE_PRE_GAP_33)
sprintf(tempbname, "PR%02d", m_bytenum - BYTE_PRE_GAP_0);
else if (m_bytenum == BYTE_LEADIN)
bname = "LDIN";
else if (m_bytenum == BYTE_HEADER)
bname = "HEAD";
else if (m_bytenum <= BYTE_DATA_255)
sprintf(tempbname, "BY%02X", m_bytenum - BYTE_DATA_0);
else if (m_bytenum == BYTE_CRC16_MSB)
bname = "CRCM";
else if (m_bytenum == BYTE_CRC16_LSB)
bname = "CRCL";
else if (m_bytenum == BYTE_TRAILER)
bname = "TRLR";
else if (m_bytenum == BYTE_LEADOUT)
bname = "LOUT";
else if (m_bytenum == BYTE_LONGCLOCK)
bname = "LONG";
else
sprintf(tempbname, "PO%02d", m_bytenum - BYTE_POSTGAP_0);
/* in the main data area, the clock alternates at the clock rate */
if (m_bytenum >= BYTE_LEADIN && m_bytenum <= BYTE_LEADOUT)
clk = ((uint32_t)(m_clockpos - REGION_BOT_GAP_END_CLOCK) & 1) ? 0 : 1;
else if (m_bytenum == BYTE_LONGCLOCK)
clk = 1;
else
clk = 0;
sprintf(temprname, "BL%02X.%4s.%d.%d", m_region - REGION_DATA_BLOCK_0, bname, m_bitnum, clk);
}
sprintf(buffer, "{%9d=%s}", m_clockpos, rname);
return buffer;
}
/*-------------------------------------------------
tape_clock_callback - called once per clock
to increment/decrement the tape location
-------------------------------------------------*/
TIMER_CALLBACK_MEMBER( decocass_tape_device::tape_clock_callback )
{
/* advance by one clock in the desired direction */
if (m_speed < 0 && m_clockpos > 0)
m_clockpos--;
else if (m_speed > 0 && m_clockpos < m_numclocks)
m_clockpos++;
/* look for states before the start of data */
if (m_clockpos < REGION_LEADER_END_CLOCK)
m_region = REGION_LEADER;
else if (m_clockpos < REGION_LEADER_GAP_END_CLOCK)
m_region = REGION_LEADER_GAP;
else if (m_clockpos < REGION_BOT_END_CLOCK)
m_region = REGION_BOT;
else if (m_clockpos < REGION_BOT_GAP_END_CLOCK)
m_region = REGION_BOT_GAP;
/* look for states after the end of data */
else if (m_clockpos >= m_numclocks - REGION_LEADER_END_CLOCK)
m_region = REGION_TRAILER;
else if (m_clockpos >= m_numclocks - REGION_LEADER_GAP_END_CLOCK)
m_region = REGION_TRAILER_GAP;
else if (m_clockpos >= m_numclocks - REGION_BOT_END_CLOCK)
m_region = REGION_EOT;
else if (m_clockpos >= m_numclocks - REGION_BOT_GAP_END_CLOCK)
m_region = REGION_EOT_GAP;
/* everything else is data */
else
{
uint32_t dataclock = m_clockpos - REGION_BOT_GAP_END_CLOCK;
/* compute the block number */
m_region = (tape_region)(REGION_DATA_BLOCK_0 + dataclock / (TAPE_CLOCKS_PER_BYTE * BYTE_BLOCK_TOTAL));
dataclock -= (m_region - REGION_DATA_BLOCK_0) * TAPE_CLOCKS_PER_BYTE * BYTE_BLOCK_TOTAL;
/* compute the byte within the block */
m_bytenum = (tape_byte)(dataclock / TAPE_CLOCKS_PER_BYTE);
dataclock -= m_bytenum * TAPE_CLOCKS_PER_BYTE;
/* compute the bit within the byte */
m_bitnum = dataclock / TAPE_CLOCKS_PER_BIT;
}
/* log */
if (LOG_CASSETTE_STATE)
describe_state();
}
/*-------------------------------------------------
tape_get_status_bits - return the 3 status
bits from the tape
-------------------------------------------------*/
uint8_t decocass_tape_device::get_status_bits()
{
uint8_t tape_bits = 0;
/* bit 0x20 is the BOT/EOT signal, which is also set in the leader/trailer area */
if (m_region == REGION_LEADER || m_region == REGION_BOT || m_region == REGION_EOT || m_region == REGION_TRAILER)
tape_bits |= 0x20;
/* bit 0x40 is the clock, which is only valid in some areas of the data block */
/* bit 0x80 is the data, which is only valid in some areas of the data block */
if (m_region >= REGION_DATA_BLOCK_0 && m_region <= REGION_DATA_BLOCK_255)
{
int blocknum = m_region - REGION_DATA_BLOCK_0;
uint8_t byteval = 0x00;
/* in the main data area, the clock alternates at the clock rate */
if (m_bytenum >= BYTE_LEADIN && m_bytenum <= BYTE_LEADOUT)
tape_bits |= ((uint32_t)(m_clockpos - REGION_BOT_GAP_END_CLOCK) & 1) ? 0x00 : 0x40;
/* in the longclock area, the clock holds high */
else if (m_bytenum == BYTE_LONGCLOCK)
tape_bits |= 0x40;
/* everywhere else, the clock holds to 0 */
else
{
/* nothing */
}
/* lead-in and lead-out bytes are 0xAA */
if (m_bytenum == BYTE_HEADER || m_bytenum == BYTE_TRAILER)
byteval = 0xaa;
/* data block bytes are data */
else if (m_bytenum >= BYTE_DATA_0 && m_bytenum <= BYTE_DATA_255)
byteval = m_tape_data[blocknum * 256 + (m_bytenum - BYTE_DATA_0)];
/* CRC MSB */
else if (m_bytenum == BYTE_CRC16_MSB)
byteval = m_crc16[blocknum] >> 8;
/* CRC LSB */
else if (m_bytenum == BYTE_CRC16_LSB)
byteval = m_crc16[blocknum];
/* select the appropriate bit from the byte and move to the upper bit */
if ((byteval >> m_bitnum) & 1)
tape_bits |= 0x80;
}
return tape_bits;
}
/*-------------------------------------------------
tape_is_present - return true if the tape is
present
-------------------------------------------------*/
bool decocass_tape_device::is_present()
{
return m_tape_data.found();
}
/*-------------------------------------------------
tape_change_speed - alter the speed of tape
playback
-------------------------------------------------*/
void decocass_tape_device::change_speed(int8_t newspeed)
{
attotime newperiod;
int8_t absnewspeed;
/* do nothing if speed has not changed */
if (m_speed == newspeed)
return;
/* compute how fast to run the tape timer */
absnewspeed = (newspeed < 0) ? -newspeed : newspeed;
if (newspeed == 0)
newperiod = attotime::never;
else
newperiod = attotime::from_hz(TAPE_CLOCKRATE * absnewspeed);
/* set the new speed */
m_tape_timer->adjust(newperiod, 0, newperiod);
m_speed = newspeed;
}