mirror of
https://github.com/holub/mame
synced 2025-04-17 05:53:36 +03:00
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.
This commit is contained in:
parent
8d7d01caef
commit
9a0c63f673
@ -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
|
||||
|
@ -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",
|
||||
}
|
||||
|
321
src/lib/formats/p2000t_cas.cpp
Normal file
321
src/lib/formats/p2000t_cas.cpp
Normal file
@ -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
|
19
src/lib/formats/p2000t_cas.h
Normal file
19
src/lib/formats/p2000t_cas.h
Normal file
@ -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
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -46,15 +46,12 @@ 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());
|
||||
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)
|
||||
else if (offset < 10)
|
||||
{
|
||||
return m_keyboard[offset]->read();
|
||||
}
|
||||
@ -62,22 +59,29 @@ uint8_t p2000t_state::p2000t_port_000f_r(offs_t offset)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -96,6 +100,10 @@ uint8_t p2000t_state::p2000t_port_202f_r()
|
||||
void p2000t_state::p2000t_port_101f_w(uint8_t 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) {}
|
||||
|
321
src/mame/machine/p2000t_mdcr.cpp
Normal file
321
src/mame/machine/p2000t_mdcr.cpp
Normal file
@ -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));
|
||||
}
|
177
src/mame/machine/p2000t_mdcr.h
Normal file
177
src/mame/machine/p2000t_mdcr.h
Normal file
@ -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
|
@ -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"},
|
||||
|
Loading…
Reference in New Issue
Block a user