diff --git a/scripts/src/formats.lua b/scripts/src/formats.lua index f88266bcd7c..de31770aaf1 100644 --- a/scripts/src/formats.lua +++ b/scripts/src/formats.lua @@ -1409,6 +1409,19 @@ if (FORMATS["IBMXDF_DSK"]~=null or _OPTIONS["with-tools"]) then } end +-------------------------------------------------- +-- +--@src/lib/formats/adam_cas.h,FORMATS["ADAM_CAS"] = true +-------------------------------------------------- + +if (FORMATS["P2000T_CAS"]~=null or _OPTIONS["with-tools"]) then + files { + MAME_DIR.. "src/lib/formats/p2000t_cas.cpp", + MAME_DIR.. "src/lib/formats/p2000t_cas.h", + } +end + + -------------------------------------------------- -- --@src/lib/formats/p6001_cas.h,FORMATS["P6001_CAS"] = true diff --git a/scripts/target/mame/mess.lua b/scripts/target/mame/mess.lua index e01532fb463..1e016b307a3 100644 --- a/scripts/target/mame/mess.lua +++ b/scripts/target/mame/mess.lua @@ -883,6 +883,7 @@ BUSES["NSCSI"] = true BUSES["NUBUS"] = true BUSES["O2"] = true BUSES["ORICEXT"] = true +BUSES["P2000"] = true BUSES["PASOPIA"] = true BUSES["PC1512"] = true BUSES["PCE"] = true @@ -3225,6 +3226,7 @@ files { MAME_DIR .. "src/mame/drivers/p2000t.cpp", MAME_DIR .. "src/mame/includes/p2000t.h", MAME_DIR .. "src/mame/machine/p2000t.cpp", + MAME_DIR .. "src/mame/machine/p2000t_mdcr.cpp", MAME_DIR .. "src/mame/video/p2000t.cpp", MAME_DIR .. "src/mame/drivers/vg5k.cpp", } diff --git a/src/lib/formats/p2000t_cas.cpp b/src/lib/formats/p2000t_cas.cpp new file mode 100644 index 00000000000..e3351dc99dc --- /dev/null +++ b/src/lib/formats/p2000t_cas.cpp @@ -0,0 +1,321 @@ +// license:BSD-3-Clause +// copyright-holders:Curt Coder + +#include + +#include "cassimg.h" +#include "p2000t_cas.h" +#include + +// This code will reproduce the timing of a P2000 mini cassette tape. +constexpr double P2000_CLOCK_PERIOD = 0.000084; +constexpr double P2000_BOT_GAP = 1; +constexpr double P2000_BOB_GAP = 0.515; +constexpr double P2000_MARK_GAP = 0.085; +constexpr double P2000_END_GAP = 0.155; +constexpr double P2000_EOT_GAP = 1.8; +constexpr int P2000_HIGH = 0x7FFFFFFF; +constexpr int P2000_LOW = -1 * 0x7FFFFFFF; + + +#define CHR(x) \ + err = (x); \ + if (err != cassette_image::error::SUCCESS) \ + return err; + +/* +Here's a description on how the P2000t stores data on tape. + +## Tape Format + +Each track (or side) of a cassette is divided into 40 block of information. A +file may comprise between 1 and 40 blocks, depending on its length. + +## Blocks, Gaps, Marks and Records + +At the start of the tape is an area of clear (erased) tape, the BOT (beginning +of tape) gap; to read pas this gap takes approximately 1 second. After this, the +first block starts, followed directly by the second, third and so on. After the +last block on the track comes the EOT (end of tape) gap; if all 40 blocks on the +tracks are used, this area of erased tape has a length equivalent to 1.8 seconds +of reading time. + + | BOT | BLOCK 1 | BLOCK 2 | BLOCK ... | EOT | + +### BLOCK + +Each tape block is made up of five sections of tape: + +| START GAP | MARK | MARK GAP | DATA RECORD | END GAP | + +- Start gap: A section of erased tape separating the start of one block from the + end of the previous block. This takes approx. 515ms to read over. +- Mark: Four bytes of recorded information (described below) +- Mark Gap: A section of erased tape separating the mark from the data record; + length about 85ms. +- Data Record: 1056 bytes of recorded data (described below) +- End Gap: A section of erased tape of around 155 ms. + +### MARK + +The mark is made up of four bytes with the following bit patterns: + + - preamble syncronization pattern (0xAA) + - 0000 0000 (0x00) + - 0000 0000 (0x00) + - postamble syncronization pattern (0xAA) + +The function of the synchronisation byte is to make sure the RDC clock is set +properly. + +### DATA RECORD + +The data record contains the information which has been written onto the tape. +It compromises five sections: + +| sync byte | header | data | check sum | post sync | + +- sync byte: Preamble synchronisation pattern 0xAA +- header: 32 bytes which specify the contents and type of the data section. (See below) +- data section: 1024 bytes of information. +- checksum: 2 bytes that contain checksum +- post sync: 0xAA + +### HEADER + +See the struct declaration below for details on what is contained. + +### DATA + +The data section consists of 1024 bytes written in serial form, least +significant byte first. + +### Checksum + +The checksum is only calculated for the header and data section. The algorithm +is not documented, an implementation can be found below. +*/ + +// Specifies the type of data stored in the tape block +enum P2000_File_Type : uint8_t +{ + Basic = 'B', + Program = 'P', + Viewdata = 'V', + WordProcessing = 'W', + Other = 'O', +}; + +// Represents the internal code for data, differrent codes are used in different countries +// only relevant when P2000_File_Type is Program. +enum P2000_Data_Type : uint8_t +{ + German = 'D', + Swedish = 'S', + Dutch_English = 'U', +}; + + +// This is the 32 byte header definition used by the P2000, it is mainly +// here for documentation. +struct P2000T_Header +{ + // Starting address in ram where data should go. This address is supplied by + // the application program when calling the monitor cassette routine to write + // the data on cassette in the first place. + uint16_t data_transfer_address; + // Total # of bytes which make up the file (can be spread over many blocks). + // The monitor uses this to determine how many blocks to read. + uint16_t file_length; + // # bytes in this record that are actually used. For example if this is 256 + // only 256 bytes will be loaded in ram + uint16_t data_section_length; + // The eight character file name identifies the file to which the record + // belongs; it will be the same in all records making up the file. Each record + // except the first is considered an extension. + char file_name[8]; + // Addition file extension. + char ext[3]; + // This file type specifies the type of data stored. + P2000_File_Type file_type; + // Code and region information. + P2000_Data_Type data_code; + // Start address where program should start. (if type = Program) + uint16_t start_addr; + // Address in ram where the program should load (if type = Program) + uint16_t load_addr; + // Unused. + char reserved[8]; + // Record number (i.e. which block) + uint8_t rec_nr; +}; + +std::ostream &operator<<(std::ostream &os, P2000T_Header const &hdr) +{ + return os << "File: " << std::string(hdr.file_name, 8) << '.' + << std::string(hdr.ext, 3) << " " << hdr.file_length; +} + +static cassette_image::error p2000t_cas_identify(cassette_image *cass, struct CassetteOptions *opts) +{ + opts->bits_per_sample = 32; + opts->channels = 1; + opts->sample_frequency = 44100; + return cassette_image::error::SUCCESS; +} + +uint16_t rotr16a(uint16_t x, uint16_t n) +{ + return (x >> n) | (x << (16 - n)); +} + +void update_chksum(uint16_t *de, bool bit) +{ + // Reverse engineered from monitor.rom + // code is at: [0x07ac, 0x07c5] + uint8_t e = *de & 0xff; + uint8_t d = (*de >> 8) & 0xff; + e = e ^ (bit ? 1 : 0); + if (e & 0x01) + { + e = e ^ 2; + d = d ^ 0x40; + } + else + { + d = d ^ 0x00; + } + *de = rotr16a((d << 8) | e, 1); +} + +/* + A transition on a clock boundary from low to high is a 1. + A transition on a clock boundary from high to low is a 0 + An intermediate transition halfway between the clock boundary + can occur when there are consecutive 0s or 1s. See the example + below where the clock is marked by a | + + + 1 0 1 1 0 0 + RDA: _|----|____|--__|----|__--|__-- + RDC: _|-___|-___|-___|-___|-___|-___ + ^ ^ + |-- clock signal |-- intermediate transition. + + This signal can be written by a simple algorithm where the first bit + is always false (transition to low, half clock). Now only one bit is needed + to determine what the next partial clock should look like. + + This works because we are always guaranteed that a block starts with 0xAA, + and hence will ALWAYS find a signal like this on tape: _-- (low, high, high) + after a gap. This is guaranteed when the tape is moving forward as well as + backwards. +*/ +cassette_image::error p2000t_put_bit(cassette_image *cass, double *time_index, bool bit) +{ + const int channel = 0; + cassette_image::error err = cassette_image::error::SUCCESS; + CHR(cassette_put_sample(cass, channel, *time_index, P2000_CLOCK_PERIOD, bit ? P2000_HIGH : P2000_LOW)); + *time_index += P2000_CLOCK_PERIOD; + + CHR(cassette_put_sample(cass, channel, *time_index, P2000_CLOCK_PERIOD, bit ? P2000_LOW : P2000_HIGH)); + *time_index += P2000_CLOCK_PERIOD; + return err; +} + +// Store byte of data, updating the checksum +cassette_image::error p2000t_put_byte(cassette_image *cass, double *time_index, uint16_t *chksum, uint8_t byte) +{ + cassette_image::error err = cassette_image::error::SUCCESS; + for (int i = 0; i < 8 && err == cassette_image::error::SUCCESS; i++) + { + update_chksum(chksum, util::BIT(byte, i)); + CHR(p2000t_put_bit(cass, time_index, util::BIT(byte, i))); + } + return err; +} + +// Store a sequence of bytes, updating the checksum +cassette_image::error p2000t_put_bytes(cassette_image *cass, double *time_index, uint16_t *chksum, const uint8_t *bytes, const uint16_t cByte) +{ + cassette_image::error err = cassette_image::error::SUCCESS; + for (int i = 0; i < cByte && err == cassette_image::error::SUCCESS; i++) + { + CHR(p2000t_put_byte(cass, time_index, chksum, bytes[i])); + } + return err; +} + +// Insert time seconds of silence. +cassette_image::error p2000t_silence(cassette_image *cassette, +double *time_index, +double time) +{ + auto err = cassette_put_sample(cassette, 0, *time_index, time, 0); + *time_index += time; + return err; +} + +static cassette_image::error p2000t_cas_load(cassette_image *cassette) +{ + cassette_image::error err = cassette_image::error::SUCCESS; + uint64_t image_size = cassette_image_size(cassette); + constexpr int CAS_BLOCK = 1280; + + /* + The cas format is pretty simple. it consists of a sequence of blocks, + where a block consists of the following: + + [0-256] P2000 memory address 0x6000 - 0x6100 + .... Nonsense (keyboard status etc.) + 0x30 P200T_Header + 0x50 + ... Nonsense.. + [0-1024] Data block + + This means that one block gets stored in 1280 bytes. + */ + if (image_size % CAS_BLOCK != 0) + { + return cassette_image::error::INVALID_IMAGE; + } + + uint8_t block[CAS_BLOCK]; + constexpr uint8_t BLOCK_MARK[4] = { 0xAA, 0x00, 0x00, 0xAA }; + auto blocks = image_size / CAS_BLOCK; + double time_idx = 0; + + // Beginning of tape marker + CHR(p2000t_silence(cassette, &time_idx, P2000_BOT_GAP)); + for (int i = 0; i < blocks; i++) + { + uint16_t crc = 0, unused = 0; + cassette_image_read(cassette, &block, CAS_BLOCK * i, CAS_BLOCK); + + // Insert sync header.. 0xAA, 0x00, 0x00, 0xAA + CHR(p2000t_silence(cassette, &time_idx, P2000_BOB_GAP)); + CHR(p2000t_put_bytes(cassette, &time_idx, &unused, BLOCK_MARK, ARRAY_LENGTH(BLOCK_MARK))); + CHR(p2000t_silence(cassette, &time_idx, P2000_MARK_GAP)); + + // Insert data block + CHR(p2000t_put_byte(cassette, &time_idx, &unused, 0xAA)); + CHR(p2000t_put_bytes(cassette, &time_idx, &crc, block + 0x30, 32)); + CHR(p2000t_put_bytes(cassette, &time_idx, &crc, block + 256, 1024)); + CHR(p2000t_put_bytes(cassette, &time_idx, &unused, ( uint8_t * )&crc, 2)); + CHR(p2000t_put_byte(cassette, &time_idx, &unused, 0xAA)); + + // Block finished. + CHR(p2000t_silence(cassette, &time_idx, P2000_END_GAP)); + } + + // End of tape marker + return p2000t_silence(cassette, &time_idx, P2000_EOT_GAP); +} + +static const struct CassetteFormat p2000t_cas = { + "cas", p2000t_cas_identify, p2000t_cas_load, nullptr /* no save */ +}; + +CASSETTE_FORMATLIST_START(p2000t_cassette_formats) +CASSETTE_FORMAT(p2000t_cas) +CASSETTE_FORMATLIST_END diff --git a/src/lib/formats/p2000t_cas.h b/src/lib/formats/p2000t_cas.h new file mode 100644 index 00000000000..6d63e314a33 --- /dev/null +++ b/src/lib/formats/p2000t_cas.h @@ -0,0 +1,19 @@ +// license:BSD-3-Clause +// copyright-holders:Erwin Jansen +/********************************************************************* + + p2000t_cas.h + + Format code for P2000t .cas cassette files. + +*********************************************************************/ +#ifndef MAME_FORMATS_P2000T_CAS_H +#define MAME_FORMATS_P2000T_CAS_H + +#pragma once + +#include "cassimg.h" + +CASSETTE_FORMATLIST_EXTERN(p2000t_cassette_formats); + +#endif // MAME_FORMATS_P2000T_CAS_H diff --git a/src/mame/drivers/p2000t.cpp b/src/mame/drivers/p2000t.cpp index 6eebef3acd0..a473f49faba 100644 --- a/src/mame/drivers/p2000t.cpp +++ b/src/mame/drivers/p2000t.cpp @@ -139,7 +139,7 @@ static INPUT_PORTS_START (p2000t) PORT_BIT (0x80, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_RIGHT) PORT_CHAR(UCHAR_MAMEKEY(RIGHT)) PORT_START("KEY.3") - PORT_BIT (0x01, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("Shift Lock") PORT_CODE(KEYCODE_CAPSLOCK) PORT_CHAR(UCHAR_MAMEKEY(CAPSLOCK)) + PORT_BIT (0x01, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("Shift Lock") PORT_CODE(KEYCODE_CAPSLOCK) PORT_CHAR(UCHAR_MAMEKEY(CAPSLOCK)) PORT_BIT (0x02, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_N) PORT_CHAR('n') PORT_CHAR('N') PORT_BIT (0x04, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_TILDE) PORT_CHAR('<') PORT_CHAR('>') PORT_BIT (0x08, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_X) PORT_CHAR('x') PORT_CHAR('X') @@ -159,7 +159,7 @@ static INPUT_PORTS_START (p2000t) PORT_BIT (0x80, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_R) PORT_CHAR('r') PORT_CHAR('R') PORT_START("KEY.5") - PORT_BIT (0x01, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("Clrln") PORT_CODE(KEYCODE_END) PORT_CHAR(UCHAR_MAMEKEY(F2)) + PORT_BIT (0x01, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("Clrln") PORT_CODE(KEYCODE_END) PORT_CHAR(UCHAR_MAMEKEY(F2)) PORT_BIT (0x02, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_9) PORT_CHAR('9') PORT_CHAR(')') PORT_BIT (0x04, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_ASTERISK) PORT_CHAR(UCHAR_MAMEKEY(PLUS_PAD)) PORT_CHAR(UCHAR_MAMEKEY(ASTERISK)) PORT_BIT (0x08, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_SLASH_PAD) PORT_CHAR(UCHAR_MAMEKEY(MINUS_PAD)) PORT_CHAR(UCHAR_MAMEKEY(SLASH_PAD)) @@ -176,7 +176,7 @@ static INPUT_PORTS_START (p2000t) PORT_BIT (0x10, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_ENTER) PORT_CHAR(13) PORT_BIT (0x20, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_P) PORT_CHAR('p') PORT_CHAR('P') PORT_BIT (0x40, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_8) PORT_CHAR('8') PORT_CHAR('(') - PORT_BIT (0x80, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("@ \xE2\x86\x91") PORT_CODE(KEYCODE_OPENBRACE) PORT_CHAR('@') PORT_CHAR('^') + PORT_BIT (0x80, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_NAME("@ \xE2\x86\x91") PORT_CODE(KEYCODE_OPENBRACE) PORT_CHAR('@') PORT_CHAR('^') PORT_START("KEY.7") PORT_BIT (0x01, IP_ACTIVE_LOW, IPT_KEYBOARD) PORT_CODE(KEYCODE_3_PAD) PORT_CHAR(UCHAR_MAMEKEY(3_PAD)) @@ -245,6 +245,9 @@ void p2000t_state::p2000t(machine_config &config) /* sound hardware */ SPEAKER(config, "mono").front_center(); SPEAKER_SOUND(config, m_speaker).add_route(ALL_OUTPUTS, "mono", 0.25); + + /* the mini cassette driver */ + MDCR(config, m_mdcr, 0); } diff --git a/src/mame/includes/p2000t.h b/src/mame/includes/p2000t.h index 3999d02c345..d0f2654f6f2 100644 --- a/src/mame/includes/p2000t.h +++ b/src/mame/includes/p2000t.h @@ -14,6 +14,7 @@ #include "cpu/z80/z80.h" #include "sound/spkrdev.h" #include "video/saa5050.h" +#include "machine/p2000t_mdcr.h" #include "emupal.h" @@ -25,6 +26,7 @@ public: , m_videoram(*this, "videoram") , m_maincpu(*this, "maincpu") , m_speaker(*this, "speaker") + , m_mdcr(*this, "mdcr") , m_keyboard(*this, "KEY.%u", 0) { } @@ -52,6 +54,8 @@ protected: required_device m_maincpu; required_device m_speaker; + // Only the P2000t has this device. + optional_device m_mdcr; private: required_ioport_array<10> m_keyboard; diff --git a/src/mame/machine/p2000t.cpp b/src/mame/machine/p2000t.cpp index 459c6a97d34..06adeb063f4 100644 --- a/src/mame/machine/p2000t.cpp +++ b/src/mame/machine/p2000t.cpp @@ -12,25 +12,25 @@ #include "emu.h" #include "includes/p2000t.h" -#define P2000M_101F_CASDAT 0x01 -#define P2000M_101F_CASCMD 0x02 -#define P2000M_101F_CASREW 0x04 -#define P2000M_101F_CASFOR 0x08 -#define P2000M_101F_KEYINT 0x40 -#define P2000M_101F_PRNOUT 0x80 +#define P2000M_101F_CASDAT 0x01 +#define P2000M_101F_CASCMD 0x02 +#define P2000M_101F_CASREW 0x04 +#define P2000M_101F_CASFOR 0x08 +#define P2000M_101F_KEYINT 0x40 +#define P2000M_101F_PRNOUT 0x80 -#define P2000M_202F_PINPUT 0x01 -#define P2000M_202F_PREADY 0x02 -#define P2000M_202F_STRAPN 0x04 -#define P2000M_202F_CASENB 0x08 -#define P2000M_202F_CASPOS 0x10 -#define P2000M_202F_CASEND 0x20 -#define P2000M_202F_CASCLK 0x40 -#define P2000M_202F_CASDAT 0x80 +#define P2000M_202F_PINPUT 0x01 +#define P2000M_202F_PREADY 0x02 +#define P2000M_202F_STRAPN 0x04 +#define P2000M_202F_CASENB 0x08 +#define P2000M_202F_CASPOS 0x10 +#define P2000M_202F_CASEND 0x20 +#define P2000M_202F_CASCLK 0x40 +#define P2000M_202F_CASDAT 0x80 -#define P2000M_303F_VIDEO 0x01 +#define P2000M_303F_VIDEO 0x01 -#define P2000M_707F_DISA 0x01 +#define P2000M_707F_DISA 0x01 /* Keyboard port 0x0x @@ -44,40 +44,44 @@ */ uint8_t p2000t_state::p2000t_port_000f_r(offs_t offset) { - if (m_port_101f & P2000M_101F_KEYINT) - { - return ( - m_keyboard[0]->read() & m_keyboard[1]->read() & - m_keyboard[2]->read() & m_keyboard[3]->read() & - m_keyboard[4]->read() & m_keyboard[5]->read() & - m_keyboard[6]->read() & m_keyboard[7]->read() & - m_keyboard[8]->read() & m_keyboard[9]->read()); - } - else - if (offset < 10) - { - return m_keyboard[offset]->read(); - } - else - return 0xff; + if (m_port_101f & P2000M_101F_KEYINT) + { + return (m_keyboard[0]->read() & m_keyboard[1]->read() & m_keyboard[2]->read() + & m_keyboard[3]->read() & m_keyboard[4]->read() & m_keyboard[5]->read() + & m_keyboard[6]->read() & m_keyboard[7]->read() & m_keyboard[8]->read() + & m_keyboard[9]->read()); + } + else if (offset < 10) + { + return m_keyboard[offset]->read(); + } + else + return 0xff; } - /* Input port 0x2x bit 0 - Printer input bit 1 - Printer ready bit 2 - Strap N (daisy/matrix) - bit 3 - Cassette write enabled - bit 4 - Cassette in position - bit 5 - Begin/end of tape - bit 6 - Cassette read clock + bit 3 - Cassette write enabled, 0 = Write enabled + bit 4 - Cassette in position, 0 = Cassette in position + bit 5 - Begin/end of tape 0 = Beginning or End of tap + bit 6 - Cassette read clock Flips when a bit is available. bit 7 - Cassette read data + + Note: bit 6 & 7 are swapped when the cassette is moving in reverse. */ uint8_t p2000t_state::p2000t_port_202f_r() { - return (0xff); + uint8_t data = 0x00; + data |= !m_mdcr->wen() << 3; + data |= !m_mdcr->cip() << 4; + data |= !m_mdcr->bet() << 5; + data |= m_mdcr->rdc() << 6; + data |= !m_mdcr->rda() << 7; + return data; } @@ -95,7 +99,11 @@ uint8_t p2000t_state::p2000t_port_202f_r() */ void p2000t_state::p2000t_port_101f_w(uint8_t data) { - m_port_101f = data; + m_port_101f = data; + m_mdcr->wda(BIT(data, 0)); + m_mdcr->wdc(BIT(data, 1)); + m_mdcr->rev(BIT(data, 2)); + m_mdcr->fwd(BIT(data, 3)); } /* @@ -110,10 +118,7 @@ void p2000t_state::p2000t_port_101f_w(uint8_t data) bit 6 - \ bit 7 - Video disable (0 = enabled) */ -void p2000t_state::p2000t_port_303f_w(uint8_t data) -{ - m_port_303f = data; -} +void p2000t_state::p2000t_port_303f_w(uint8_t data) { m_port_303f = data; } /* Beeper 0x5x @@ -127,10 +132,7 @@ void p2000t_state::p2000t_port_303f_w(uint8_t data) bit 6 - Unused bit 7 - Unused */ -void p2000t_state::p2000t_port_505f_w(uint8_t data) -{ - m_speaker->level_w(BIT(data, 0)); -} +void p2000t_state::p2000t_port_505f_w(uint8_t data) { m_speaker->level_w(BIT(data, 0)); } /* DISAS 0x7x (P2000M only) @@ -148,10 +150,7 @@ void p2000t_state::p2000t_port_505f_w(uint8_t data) video refresh is disabled when the CPU accesses video memory */ -void p2000t_state::p2000t_port_707f_w(uint8_t data) -{ - m_port_707f = data; -} +void p2000t_state::p2000t_port_707f_w(uint8_t data) { m_port_707f = data; } void p2000t_state::p2000t_port_888b_w(uint8_t data) {} void p2000t_state::p2000t_port_8c90_w(uint8_t data) {} diff --git a/src/mame/machine/p2000t_mdcr.cpp b/src/mame/machine/p2000t_mdcr.cpp new file mode 100644 index 00000000000..8eb69177866 --- /dev/null +++ b/src/mame/machine/p2000t_mdcr.cpp @@ -0,0 +1,321 @@ +// license:BSD-3-Clause +// copyright-holders:Erwin Jansen +/********************************************************************** + + Philips P2000T Mini Digital Cassette Recorder emulation + +**********************************************************************/ + +#include "emu.h" +#include "p2000t_mdcr.h" +#include "formats/p2000t_cas.h" + +DEFINE_DEVICE_TYPE(MDCR, mdcr_device, "mdcr", "Philips Mini DCR") + +READ_LINE_MEMBER(mdcr_device::rdc) +{ + // According to mdcr spec there is cross talk on the wires when writing, + // hence the clock signal is always false when writing. + if (m_recording) + return false; + + return m_fwd ? m_rdc : m_rda; +} + +READ_LINE_MEMBER(mdcr_device::rda) +{ + return m_fwd ? m_rda : m_rdc; +} + +READ_LINE_MEMBER(mdcr_device::bet) +{ + return tape_start_or_end(); +} + +READ_LINE_MEMBER(mdcr_device::cip) +{ + return true; +} + +READ_LINE_MEMBER(mdcr_device::wen) +{ + return true; +} + +WRITE_LINE_MEMBER(mdcr_device::rev) +{ + m_rev = state; + if (m_rev) + { + rewind(); + } + + if (!m_rev && !m_fwd) + { + stop(); + } +} + +WRITE_LINE_MEMBER(mdcr_device::fwd) +{ + m_fwd = state; + if (m_fwd) + { + forward(); + } + + if (!m_rev && !m_fwd) + { + stop(); + } +} + +WRITE_LINE_MEMBER(mdcr_device::wda) +{ + m_wda = state; +} + +WRITE_LINE_MEMBER(mdcr_device::wdc) +{ + if (state) + { + write_bit(m_wda); + }; +} + +void mdcr_device::device_add_mconfig(machine_config &config) +{ + CASSETTE(config, m_cassette); + m_cassette->set_default_state(CASSETTE_STOPPED | CASSETTE_MOTOR_DISABLED | + CASSETTE_SPEAKER_MUTED); + m_cassette->set_interface("p2000_cass"); + m_cassette->set_formats(p2000t_cassette_formats); +} + +mdcr_device::mdcr_device(machine_config const &mconfig, char const *tag, device_t *owner, uint32_t clock) +: device_t(mconfig, MDCR, tag, owner, clock) +, m_cassette(*this, "cassette") +, m_read_timer(nullptr) +{ +} + +void mdcr_device::device_start() +{ + m_read_timer = timer_alloc(); + m_read_timer->adjust(attotime::from_hz(44100), 0, attotime::from_hz(44100)); + + save_item(NAME(m_fwd)); + save_item(NAME(m_rev)); + save_item(NAME(m_rdc)); + save_item(NAME(m_rda)); + save_item(NAME(m_wda)); + save_item(NAME(m_recording)); + save_item(NAME(m_fwd_pulse_time)); + save_item(NAME(m_last_tape_time)); + save_item(NAME(m_save_tape_time)); + // Phase decoder + save_item(STRUCT_MEMBER(m_phase_decoder, m_last_signal)); + save_item(STRUCT_MEMBER(m_phase_decoder, m_needs_sync)); + save_item(STRUCT_MEMBER(m_phase_decoder, m_bit_queue)); + save_item(STRUCT_MEMBER(m_phase_decoder, m_bit_place)); + save_item(STRUCT_MEMBER(m_phase_decoder, m_current_clock)); + save_item(STRUCT_MEMBER(m_phase_decoder, m_clock_period)); +} + +void mdcr_device::device_pre_save() +{ + m_save_tape_time = m_cassette->get_position(); +} + +void mdcr_device::device_post_load() +{ + m_cassette->seek(m_save_tape_time, SEEK_SET); +} + +void mdcr_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) +{ + if (!m_recording && m_cassette->motor_on()) + { + // Account for moving backwards. + auto delay = abs(m_cassette->get_position() - m_last_tape_time); + + // Decode the signal using the fake phase decode circuit + bool newBit = m_phase_decoder.signal((m_cassette->input() > +0.04), delay); + if (newBit) + { + // Flip rdc + m_rdc = !m_rdc; + m_rda = m_phase_decoder.pull_bit(); + } + } + m_last_tape_time = m_cassette->get_position(); +} + +void mdcr_device::write_bit(bool bit) +{ + m_recording = true; + m_cassette->change_state(CASSETTE_RECORD, CASSETTE_MASK_UISTATE); + m_cassette->output(bit ? +1.0 : -1.0); + m_phase_decoder.reset(); +} + +void mdcr_device::rewind() +{ + m_fwd = false; + m_recording = false; + m_cassette->set_motor(true); + m_cassette->change_state(CASSETTE_PLAY, CASSETTE_MASK_UISTATE); + m_cassette->go_reverse(); +} + +void mdcr_device::forward() +{ + // A pulse of 1us < T < 20 usec should reset the phase decoder. + // See mdcr spec for details. + constexpr double RESET_PULSE_TIMING = 2.00e-05; + auto now = machine().time().as_double(); + auto pulse_delay = now - m_fwd_pulse_time; + m_fwd_pulse_time = now; + + if (pulse_delay < RESET_PULSE_TIMING) + { + m_phase_decoder.reset(); + } + + m_fwd = true; + m_cassette->set_motor(true); + m_cassette->change_state(m_recording ? CASSETTE_RECORD : CASSETTE_PLAY, + CASSETTE_MASK_UISTATE); + m_cassette->go_forward(); +} + +void mdcr_device::stop() +{ + m_cassette->change_state(CASSETTE_PLAY, CASSETTE_MASK_UISTATE); + m_cassette->set_motor(false); +} + +bool mdcr_device::tape_start_or_end() +{ + auto pos = m_cassette->get_position(); + return m_cassette->motor_on() && + (pos <= 0 || pos >= m_cassette->get_length()); +} + +void p2000_mdcr_devices(device_slot_interface &device) +{ + device.option_add("mdcr", MDCR); +} + +// +// phase_decoder +// + +mdcr_device::phase_decoder::phase_decoder(double tolerance) +: m_tolerance(tolerance) +{ + reset(); +} + +bool mdcr_device::phase_decoder::pull_bit() +{ + if (m_bit_place == 0) + return false; + auto res = BIT(m_bit_queue, 0); + m_bit_place--; + m_bit_queue >>= 1; + return res; +} + +bool mdcr_device::phase_decoder::signal(bool state, double delay) +{ + m_current_clock += delay; + if (state == m_last_signal) + { + if (m_needs_sync == 0 && m_current_clock > m_clock_period && + !within_tolerance(m_current_clock, m_clock_period)) + { + // We might be at the last bit in a sequence, meaning we + // are only getting the reference signal for a while. + // so we produce one last clock signal. + reset(); + return true; + } + return false; + } + + // A transition happened! + m_last_signal = state; + if (m_needs_sync > 0) + { + // We have not yet determined our clock period. + return sync_signal(state); + } + + // We are within bounds of the current clock + if (within_tolerance(m_current_clock, m_clock_period)) + { + add_bit(state); + return true; + }; + + // We went out of sync, our clock is wayyy out of bounds. + if (m_current_clock > m_clock_period) + reset(); + + // We are likely halfway in our clock signal.. + return false; +}; + +void mdcr_device::phase_decoder::reset() +{ + m_last_signal = false; + m_current_clock = {}; + m_clock_period = {}; + m_needs_sync = SYNCBITS; +} + +void mdcr_device::phase_decoder::add_bit(bool bit) +{ + if (bit) + m_bit_queue |= bit << m_bit_place; + else + m_bit_queue &= ~(bit << m_bit_place); + + if (m_bit_place <= QUEUE_DELAY) + m_bit_place++; + + m_current_clock = {}; +} + +bool mdcr_device::phase_decoder::sync_signal(bool state) +{ + m_needs_sync--; + if (m_needs_sync == SYNCBITS - 1) + { + // We can only synchronize when we go up + // on the first bit. + if (state) + add_bit(true); + return false; + } + if (m_clock_period != 0 && !within_tolerance(m_current_clock, m_clock_period)) + { + // Clock is way off! + reset(); + return false; + } + + // We've derived a clock period, we will use the average. + auto div = SYNCBITS - m_needs_sync - 1; + m_clock_period = ((div - 1) * m_clock_period + m_current_clock) / div; + add_bit(state); + return true; +} + +// y * (1 - tolerance) < x < y * (1 + tolerance) +bool mdcr_device::phase_decoder::within_tolerance(double x, double y) +{ + assert(m_tolerance > 0 && m_tolerance < 1); + return (y * (1 - m_tolerance)) < x && x < (y * (1 + m_tolerance)); +} diff --git a/src/mame/machine/p2000t_mdcr.h b/src/mame/machine/p2000t_mdcr.h new file mode 100644 index 00000000000..9b312ed42d7 --- /dev/null +++ b/src/mame/machine/p2000t_mdcr.h @@ -0,0 +1,177 @@ +// license:BSD-3-Clause +// copyright-holders:Erwin Jansen +/********************************************************************** + + Philips P2000t Mini Digital Cassette Recorder Emulation + +********************************************************************** + + +12V 1 8 !WCD + OV (signal) 2 9 !REV + OV (power) 3 A !FWD + GND 4 B RDC + !WDA 6 C !RDA + !BET 7 D !CIP + E !WEN + +**********************************************************************/ + +#ifndef MAME_MACHINE_P2000T_MDCR_H +#define MAME_MACHINE_P2000T_MDCR_H + +#pragma once + +#include "imagedev/cassette.h" + +/// \brief Models a MCR220 Micro Cassette Recorder +/// +/// Detailed documentation on the device can be found in this repository: +/// https://github.com/p2000t/documentation/tree/master/hardware +/// +/// This device was built in the P2000t and was used as tape storage. +/// It used mini-cassettes that were also used in dictation devices. +/// The P2000t completely controls this device without user intervention. +class mdcr_device : public device_t +{ +public: + mdcr_device(machine_config const &mconfig, char const *tag, device_t *owner, uint32_t clock); + + /// \brief The read clock, switches state when a bit is available. + /// + /// This is the read clock. The read clock is a flip flop that switches + /// whenever a new bit is available on rda. The original flips every 167us + /// This system flips at 154 / 176 usec, which is within the tolerance for + /// the rom and system diagnostics. + /// + /// Note that rdc & rda are flipped when the tape is moving in reverse. + DECLARE_READ_LINE_MEMBER(rdc); + + /// The current active data bit. + DECLARE_READ_LINE_MEMBER(rda); + + /// False indicates we have reached end/beginning of tape + DECLARE_READ_LINE_MEMBER(bet); + + /// False if a cassette is in place. + DECLARE_READ_LINE_MEMBER(cip); + + /// False when the cassette is write enabled. + DECLARE_READ_LINE_MEMBER(wen); + + /// True if we should activate the reverse motor. + DECLARE_WRITE_LINE_MEMBER(rev); + + /// True if we should activate the forward motor. + /// Note: A quick pulse (<20usec) will reset the phase decoder. + DECLARE_WRITE_LINE_MEMBER(fwd); + + /// The bit to write to tape. Make sure to set wda after wdc. + DECLARE_WRITE_LINE_MEMBER(wda); + + /// True if the current wda should be written to tape. + DECLARE_WRITE_LINE_MEMBER(wdc); + +protected: + virtual void device_start() override; + virtual void device_pre_save() override; + virtual void device_post_load() override; + virtual void device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) override; + + virtual void device_add_mconfig(machine_config &config) override; + +private: + /// \brief A Phase Decoder used in a Philips MDCR220 Mini Cassette Recorder + /// + /// A phase decoder is capable of converting a signal stream into a + /// a series of bits that go together with a clock signal. This phase + /// decoder is conform to what you would find in an Philips MDCR220 + /// + /// Signals are converted into bits whenever the line signal + /// changes from low to high and vice versa on a clock signal. + /// + /// A transition on a clock boundary from low to high is a 1. + /// A transition on a clock boundary from high to low is a 0 + /// An intermediate transition halfway between the clock boundary + /// can occur when there are consecutive 0s or 1s. See the example + /// below where the clock is marked by a | + /// + /// 1 0 1 1 0 0 + /// RDA: _|----|____|--__|----|__--|__-- + /// RDC: _|-___|-___|-___|-___|-___|-___ + /// ^ ^ + /// |-- clock signal |-- intermediate transition. + /// + /// This particular phase decoder expects a signal of + /// 1010 1010 which is used to derive the clock T. + /// after a reset. + class phase_decoder + { + using time_in_seconds = double; + + public: + /// Creates a phase decoder with the given tolerance. + phase_decoder(double tolerance = 0.15); + + /// Pulls the bit out of the queue. + bool pull_bit(); + + /// Returns true if a new bit can be read, you can now pull the bit. + bool signal(bool state, double delay); + + /// Reset the clock state, the system will now need to resynchronize on 0xAA. + void reset(); + + private: + // add a bit and reset the current clock. + void add_bit(bool bit); + + // tries to sync up the signal and calculate the clockperiod. + bool sync_signal(bool state); + + // y * (1 - tolerance) < x < y * (1 + tolerance) + bool within_tolerance(double x, double y); + + double m_tolerance; + + static constexpr int SYNCBITS = 7; + static constexpr int QUEUE_DELAY = 2; + + public: + // Needed for save state. + bool m_last_signal{ false }; + int m_needs_sync{ SYNCBITS }; + uint8_t m_bit_queue{ 0 }; + uint8_t m_bit_place{ 0 }; + time_in_seconds m_current_clock{ 0 }; + time_in_seconds m_clock_period{ 0 }; + }; + + + void write_bit(bool bit); + void rewind(); + void forward(); + void stop(); + bool tape_start_or_end(); + + bool m_fwd{ false }; + bool m_rev{ false }; + bool m_rdc{ false }; + bool m_rda{ false }; + bool m_wda{ false }; + + bool m_recording{ false }; + double m_fwd_pulse_time{ 0 }; + double m_last_tape_time{ 0 }; + double m_save_tape_time{ 0 }; + + required_device m_cassette; + phase_decoder m_phase_decoder; + + // timers + emu_timer *m_read_timer; +}; + + +DECLARE_DEVICE_TYPE(MDCR, mdcr_device) + +#endif // MAME_MACHINE_P2000T_MDCR_H diff --git a/src/tools/castool.cpp b/src/tools/castool.cpp index eb9973ad48d..0b2b4230f8b 100644 --- a/src/tools/castool.cpp +++ b/src/tools/castool.cpp @@ -38,6 +38,7 @@ #include "formats/mz_cas.h" #include "formats/orao_cas.h" #include "formats/oric_tap.h" +#include "formats/p2000t_cas.h" #include "formats/p6001_cas.h" #include "formats/phc25_cas.h" #include "formats/pmd_cas.h" @@ -90,6 +91,7 @@ const struct SupportedCassetteFormats formats[] = { {"mz", mz700_cassette_formats ,"Sharp MZ-700"}, {"orao", orao_cassette_formats ,"PEL Varazdin Orao"}, {"oric", oric_cassette_formats ,"Tangerine Oric"}, + {"p2000t", p2000t_cassette_formats ,"Philips P2000T"}, {"pc6001", pc6001_cassette_formats ,"NEC PC-6001"}, {"phc25", phc25_cassette_formats ,"Sanyo PHC-25"}, {"pmd85", pmd85_cassette_formats ,"Tesla PMD-85"},