summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
author Erwin Jansen <pokowaka@users.noreply.github.com>2020-09-17 01:39:48 -0700
committer GitHub <noreply@github.com>2020-09-17 18:39:48 +1000
commit9a0c63f673d1e6b91e8abbf6e54729aad7e439e9 (patch)
tree6b547d13a36c6517412b2e8402f276f2d9283ce7
parent8d7d01caef380b119fbd8653fa5ad23e0d592564 (diff)
Add MDCR support for P2000t (#7215)
This adds support for the mini digital cassette recorder that can be found inside a P2000t. This implementation is based on documentation that can be found in https://github.com/p2000t/documentation. In memory of NPM Jansen, who taught me all the magic of bits and bytes.
-rw-r--r--scripts/src/formats.lua13
-rw-r--r--scripts/target/mame/mess.lua2
-rw-r--r--src/lib/formats/p2000t_cas.cpp321
-rw-r--r--src/lib/formats/p2000t_cas.h19
-rw-r--r--src/mame/drivers/p2000t.cpp9
-rw-r--r--src/mame/includes/p2000t.h4
-rw-r--r--src/mame/machine/p2000t.cpp107
-rw-r--r--src/mame/machine/p2000t_mdcr.cpp321
-rw-r--r--src/mame/machine/p2000t_mdcr.h177
-rw-r--r--src/tools/castool.cpp2
10 files changed, 918 insertions, 57 deletions
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
@@ -1411,6 +1411,19 @@ 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 <cassert>
+
+#include "cassimg.h"
+#include "p2000t_cas.h"
+#include <ostream>
+
+// 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<cpu_device> m_maincpu;
required_device<speaker_sound_device> m_speaker;
+ // Only the P2000t has this device.
+ optional_device<mdcr_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_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_707F_DISA 0x01
+#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_303F_VIDEO 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<cassette_image_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"},