Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 218 additions & 35 deletions src/mame/ensoniq/esq5505.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,12 @@
#include "sound/esqpump.h"
#include "emupal.h"
#include "speaker.h"
#include "vfxcart.h"

#include <cstdarg>
#include <cstdio>


// #define VERBOSE 1
#include "logmacro.h"

Expand Down Expand Up @@ -223,6 +225,7 @@ class esq5505_state : public driver_device
, m_pump(*this, "pump")
, m_fdc(*this, "wd1772")
, m_floppy_connector(*this, "wd1772:0")
, m_cart(*this, "cart")
, m_panel(*this, "panel")
, m_dmac(*this, "mc68450")
, m_mdout(*this, "mdout")
Expand Down Expand Up @@ -258,6 +261,7 @@ class esq5505_state : public driver_device
required_device<esq_5505_5510_pump_device> m_pump;
optional_device<wd1772_device> m_fdc;
optional_device<floppy_connector> m_floppy_connector;
optional_device<ensoniq_vfx_cartridge> m_cart;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought for future work: having more than one optional_device often means it would be cleaner to have a base class with nothing optional and then various inherited child classes that extend that in various ways. Back when I wrote esq5505 originally I wasn't in that mindset and it does show.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth looking at this for a refactor more generally. The EPS Classic, for example, doesn't really belong here at all: it has no es5510 ESP, and the sound generator is the es5504 "DOC II", not the es5505 "OTIS".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for sure.

required_device<esqpanel_device> m_panel;
optional_device<hd63450_device> m_dmac;
required_device<midi_port_device> m_mdout;
Expand All @@ -283,7 +287,18 @@ class esq5505_state : public driver_device

int m_system_type = 0;
uint8_t m_duart_io = 0;

bool m_otis_irq = false;
bool m_floppy_dskchg = false;
bool m_docirq = false;
bool m_floppy_is_loaded = false;
bool m_floppy_is_active = false;
emu_timer *m_motor_on_timer;
emu_timer *m_dskchg_reset_timer;

TIMER_CALLBACK_MEMBER(floppy_motor_on);
TIMER_CALLBACK_MEMBER(floppy_dskchg_reset);

static void floppy_drives(device_slot_interface &device);
static void floppy_formats(format_registration &fr);

void eps_map(address_map &map) ATTR_COLD;
Expand All @@ -294,15 +309,142 @@ class esq5505_state : public driver_device
void cpu_space_map(address_map &map) ATTR_COLD;
void eps_cpu_space_map(address_map &map) ATTR_COLD;

void update_floppy_inputs();
void floppy_loaded(bool loaded);
void floppy_load(floppy_image_device *floppy);
void floppy_unload(floppy_image_device *floppy);
void cartridge_loaded(bool loaded);
void cartridge_load(ensoniq_vfx_cartridge *cart);
void cartridge_unload(ensoniq_vfx_cartridge *cart);

void update_docirq_to_maincpu();
void otis_irq(int irq);

uint16_t m_analog_values[8];
};

void esq5505_state::cartridge_loaded(bool loaded)
{
LOG("Cartridge %s\n", loaded ? "Inserted" : "Ejected");
int state = loaded ? CLEAR_LINE : ASSERT_LINE;

// On VFX and later, DUART input bit 1 is 0 for cartridge present.
LOG("ip1 -> %d\n", state);
m_duart->ip1_w(state);
}

void esq5505_state::cartridge_load(ensoniq_vfx_cartridge *cart)
{
cartridge_loaded(true);
}

void esq5505_state::cartridge_unload(ensoniq_vfx_cartridge *cart)
{
cartridge_loaded(false);
}

void esq5505_state::floppy_drives(device_slot_interface &device)
{
device.option_add_internal("35dd", FLOPPY_35_DD);
}

void esq5505_state::floppy_formats(format_registration &fr)
{
fr.add_mfm_containers();
fr.add(FLOPPY_ESQIMG_FORMAT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not a bad idea to add the HFE format too - that's become kind of the standard way to distribute synth floppies with nonstandard physical formats, since a lot of people still using real hardware have Gotek or similar drive replacements that use that format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea, though the FlashFloppy firmware for Gotek drives explicitly supports the Ensoniq 800 kB format for Ensoniq hosts, so for these keyboards in particular, HFE may be less important. Either way I'd prefer to read up more on that before I make such a change, separately from this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reading Ensoniq floppies, the GreaseWeazle software also explicitly supports Ensoniq's floppy formats, so they can be directly read to .img files for use with a FlashFloppy Gotek drive, or here in MAME.

}

TIMER_CALLBACK_MEMBER(esq5505_state::floppy_motor_on)
{
bool motor_on = param;
if (m_floppy_connector)
{
floppy_image_device *floppy = m_floppy_connector->get_device();
if (floppy)
{
floppy->mon_w(!motor_on); // active low
m_panel->set_floppy_active(motor_on);
}
}
}

TIMER_CALLBACK_MEMBER(esq5505_state::floppy_dskchg_reset)
{
m_floppy_dskchg = !m_floppy_is_loaded;
LOG("Resetting floppy_dskchg -> %s\n", m_floppy_dskchg ? "true" : "false");
update_docirq_to_maincpu();
}

void esq5505_state::update_floppy_inputs()
{
// update the "Disk Ready" input
m_duart->ip0_w(m_floppy_is_active && m_floppy_is_loaded);

// Also update the DOC IRQ in case there's a pending disk change to handle.
update_docirq_to_maincpu();
}

void esq5505_state::floppy_loaded(bool loaded)
{
if (m_floppy_connector)
{
m_floppy_is_loaded = loaded;
if (!loaded)
{
// Only set m_floppy_dskchg; it will be reset a short time after
// the disk has been enabled while m_floppy_dskchg is true.
m_floppy_dskchg = true;
}

LOG("Floppy %s\n", loaded ? "Inserted" : "Ejected");
update_floppy_inputs();
}
else
{
LOG("<No Floppy connector for loaded=%d>\n", loaded);
}
}

void esq5505_state::floppy_load(floppy_image_device *floppy)
{
floppy_loaded(true);
}

void esq5505_state::floppy_unload(floppy_image_device *floppy)
{
floppy_loaded(false);
}

void esq5505_state::update_docirq_to_maincpu()
{
bool floppy_dskchg_irq = m_floppy_is_active && m_floppy_dskchg;
if (floppy_dskchg_irq)
LOG("docirq (m68k_irq1) due to disk change = %d\n", floppy_dskchg_irq);
if (floppy_dskchg_irq && m_floppy_is_loaded)
{
// The drives that Ensoniq use only _pulse_ DSKCHG for a brief time, when a disk is in the drive.
Copy link
Contributor

@rb6502 rb6502 Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this 100% verified? Most drives will raise DSKCHG on eject and then lower it the next time a seek happens with a disk in the drive, and MAME emulates that by default with the dskchg_r() method on the floppy image device.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so; please see this comment on the previous attempt at these changes:

Are you sure the dskchg is timer-based and not the usual "clear on first track change with a floppy in" that is implemented in floppy_image_device?

I an absolutely sure.

I first found it documented for the EPS in Keir Fraser's FlashFloppy firmware, when people tried to connect it to an Ensoniq EPS and it would not recognize disk changes.

But with this being so strange, I measured it with my cheap Saleae Logic clone logic analyzer on an SD-1/32. After a disk change - so, removed the previous disk, inserted a new disk, then went to try to read something from the disk: pressed the "Storage" button, then selected "Disk" by pressing the middle button below the screen, and then "Load" again with the middle button below the screen) - the keyboard signals (drives low) the DSEL and MO signals (enabling the drive and starting the motor), and the drive responds with a brief pulse: in my measurement with a cheap logic analyzer sampling at 4 MHz, all of two (2) samples were low, so a pulse of 500 nanoseconds and certainly no more than 1000 nanoseconds to 1 microsecond.

I'm including the sigrok trace recorded with PulseView:.

Load from floppy after new floppy inserted.zip

Look at the 335088 mycrosecond timestamp: the MO and DSEL signals both go low, and at the same time, DSKCHG goes low, but only for 2 samples, and with no other signal changing during that very short window.

The DSKCHG signal is actually routed, via a gate and a transistor, into the same IRQ line on the CPU itself that is also used by the ES5505 OTIS sound generator chip. It so happens that on the SD-1, if that interrupt is taken (and I presume after the keyboard has checked the ES5505 interrupt status), eventually the SD-1 will assert (drive low) the DSEL (and MO) lines to the floppy drive and issue a STEP command, which will inturn reset DSKCHG even on a standard drive; but that of course might take longer than it does when the drive itself only briefly pulses the signal.

And while this would technically work on the SD-1, it would not work on the EPS, as documented by FlashFloppy users - where they added a configuration setting precisely to reset DSKCHG after a short time. So including this code is useful, possibly even necessary, to get the EPS and EPS16+ to work in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. That's kind of wild.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the floppy signal routing in general and the DSKCHG pulsing behaviour in particular were interesting to trace and track down. Being able to measure on real hardware and seeing the behaviour really helped confirm that was what was actually happening, even after reading about it in the FlashFloppy firmware discussion and documentation.

// schedule a reset.
LOG("Scheduling DSKCHG reset\n");
m_dskchg_reset_timer->adjust(attotime::from_nsec(500));
}
bool updated_irq = m_otis_irq || floppy_dskchg_irq;
if (updated_irq != m_docirq)
{
LOG("docirq (m68k_irq1) -> %d\n", updated_irq);
m_maincpu->set_input_line(M68K_IRQ_1, updated_irq);
m_docirq = updated_irq;
}
}

void esq5505_state::otis_irq(int irq)
{
if (irq != m_otis_irq)
{
m_otis_irq = irq;
update_docirq_to_maincpu();
}
}

void esq5505_state::cpu_space_map(address_map &map)
{
map(0xfffff0, 0xffffff).m(m_maincpu, FUNC(m68000_base_device::autovectors_map));
Expand All @@ -317,11 +459,59 @@ void esq5505_state::eps_cpu_space_map(address_map &map)

void esq5505_state::machine_start()
{
LOG("machine_start()\n");
if (m_floppy_connector) {
floppy_image_device *floppy = m_floppy_connector->get_device();
if (floppy) {
floppy->setup_load_cb(floppy_image_device::load_cb(&esq5505_state::floppy_load, this));
floppy->setup_unload_cb(floppy_image_device::unload_cb(&esq5505_state::floppy_unload, this));

m_motor_on_timer = timer_alloc(FUNC(esq5505_state::floppy_motor_on), this);
m_dskchg_reset_timer = timer_alloc(FUNC(esq5505_state::floppy_dskchg_reset), this);

// Set DSKCHG according to whether there is a floppy in the drive.
if (floppy->exists()) {
LOG("\nFloppy Drive has Floppy '%s'\n", floppy->filename());
m_floppy_dskchg = false;
} else {
LOG("\nFloppy Drive has No Floppy\n");
m_floppy_dskchg = true;
}
} else {
LOG("\nFloppy Drive has No Image Device!\n");
}
} else {
LOG("\nNo Floppy Drive\n");
}
if (m_cart) {
m_cart->setup_load_cb(ensoniq_vfx_cartridge::load_cb(&esq5505_state::cartridge_load, this));
m_cart->setup_unload_cb(ensoniq_vfx_cartridge::unload_cb(&esq5505_state::cartridge_unload, this));

if (m_cart->exists()) {
LOG("\nCartridge Slot has Cartridge '%s'\n", m_cart->filename());
} else {
LOG("\nCartridge Slot has No Cartridge\n");
}
}
}

void esq5505_state::machine_reset()
{
floppy_image_device *floppy = m_floppy_connector ? m_floppy_connector->get_device() : nullptr;
// Check our image devices for load status.
if (m_floppy_connector) {
floppy_image_device *floppy = m_floppy_connector->get_device();
if (floppy && floppy->exists()) {
floppy_load(floppy);
} else {
floppy_unload(floppy);
}
}

if (m_cart && m_cart->exists()) {
cartridge_load(m_cart);
} else {
cartridge_unload(m_cart);
}

// Default analog values: all values are 10 bits, left-justified within 16 bits.
m_analog_values[0] = 0x7fc0; // pitch mod: start in the center
Expand All @@ -332,27 +522,6 @@ void esq5505_state::machine_reset()
m_analog_values[5] = 0xffc0; // Volume control: full on.
m_analog_values[6] = 0x7fc0; // Battery voltage: something reasonable.
m_analog_values[7] = 0x5540; // vRef to check battery.

// on VFX, bit 0 is 1 for 'cartridge present'.
// on VFX-SD and later, bit 0 is2 1 for floppy present, bit 1 is 1 for cartridge present
if (strcmp(machine().system().name, "vfx") == 0)
{
// todo: handle VFX cart-in when we support cartridges
m_duart->ip0_w(ASSERT_LINE);
}
else
{
m_duart->ip1_w(CLEAR_LINE);

if (floppy)
{
m_duart->ip0_w(CLEAR_LINE);
}
else
{
m_duart->ip0_w(ASSERT_LINE);
}
}
}

uint16_t esq5505_state::lower_r(offs_t offset)
Expand Down Expand Up @@ -396,6 +565,7 @@ void esq5505_state::vfx_map(address_map &map)
map(0x200000, 0x20001f).rw("otis", FUNC(es5505_device::read), FUNC(es5505_device::write));
map(0x280000, 0x28001f).rw(m_duart, FUNC(mc68681_device::read), FUNC(mc68681_device::write)).umask16(0x00ff);
map(0x260000, 0x2601ff).rw(m_esp, FUNC(es5510_device::host_r), FUNC(es5510_device::host_w)).umask16(0x00ff);
map(0x2e0000, 0x2fffff).rw(m_cart, FUNC(ensoniq_vfx_cartridge::read), FUNC(ensoniq_vfx_cartridge::write)).umask16(0x00ff);
map(0xc00000, 0xc1ffff).rom().region("osrom", 0);
map(0xff0000, 0xffffff).ram().share("osram");
}
Expand All @@ -407,6 +577,7 @@ void esq5505_state::vfxsd_map(address_map &map)
map(0x280000, 0x28001f).rw(m_duart, FUNC(mc68681_device::read), FUNC(mc68681_device::write)).umask16(0x00ff);
map(0x260000, 0x2601ff).rw(m_esp, FUNC(es5510_device::host_r), FUNC(es5510_device::host_w)).umask16(0x00ff);
map(0x2c0000, 0x2c0007).rw(m_fdc, FUNC(wd1772_device::read), FUNC(wd1772_device::write)).umask16(0x00ff);
map(0x2e0000, 0x2fffff).rw(m_cart, FUNC(ensoniq_vfx_cartridge::read), FUNC(ensoniq_vfx_cartridge::write)).umask16(0x00ff);
map(0x330000, 0x37ffff).ram().share("seqram");
map(0xc00000, 0xc3ffff).rom().region("osrom", 0);
map(0xff0000, 0xffffff).ram().share("osram");
Expand Down Expand Up @@ -469,7 +640,7 @@ void esq5505_state::duart_output(uint8_t data)
VFX-SD & SD-1 (32):
bits 0/1/2 = analog sel
bit 3 = SSEL (disk side)
bit 4 = DSEL (drive select?)
bit 4 = DSEL (Drive Select and floppy Motor On)
bit 6 = ESPHALT
bit 7 = SACK (?)
*/
Expand Down Expand Up @@ -499,7 +670,23 @@ void esq5505_state::duart_output(uint8_t data)
}
else
{
floppy->ss_w(((data & 8)>>3)^1);
floppy->ss_w(!BIT(data, 3)); // bit 3, inverted -> floppy
m_floppy_is_active = BIT(data, 4); // bit 4 is used to activate the floppy:
if (m_floppy_is_active)
{
// immediately assert DISK SELECT (active low)
floppy->ds_w(CLEAR_LINE);
// but schedule a delayed MOTOR ON, since the keyboard constantly pulses this after a file has been read.
m_motor_on_timer->adjust(attotime::from_usec(100), 1);
}
else
{
// immediately deassert DISK SELECT (active low)
floppy->ds_w(ASSERT_LINE);
// but schedule a slightly delayed MOTOR OFF, since the keyboard sometimes seems to pulse this.
m_motor_on_timer->adjust(attotime::from_usec(50), 0);
}
update_floppy_inputs();
}
}

Expand Down Expand Up @@ -594,7 +781,7 @@ void esq5505_state::common(machine_config &config)
m_otis->set_region0("waverom"); /* Bank 0 */
m_otis->set_region1("waverom2"); /* Bank 1 */
m_otis->set_channels(4); /* channels */
m_otis->irq_cb().set_inputline(m_maincpu, M68K_IRQ_1);
m_otis->irq_cb().set(FUNC(esq5505_state::otis_irq));
m_otis->read_port_cb().set(FUNC(esq5505_state::analog_r)); /* ADC */
m_otis->add_route(0, "pump", 1.0, 0);
m_otis->add_route(1, "pump", 1.0, 1);
Expand All @@ -610,6 +797,8 @@ void esq5505_state::vfx(machine_config &config, int panel_type)
{
common(config);

ENSONIQ_VFX_CARTRIDGE(config, m_cart);

ESQPANEL2X40_VFX(config, m_panel, panel_type);
m_panel->write_tx().set(m_duart, FUNC(mc68681_device::rx_b_w));
m_panel->write_analog().set(FUNC(esq5505_state::analog_w));
Expand All @@ -630,10 +819,7 @@ void esq5505_state::eps(machine_config &config)
m_panel->write_analog().set(FUNC(esq5505_state::analog_w));

WD1772(config, m_fdc, 8_MHz_XTAL);
FLOPPY_CONNECTOR(config, m_floppy_connector);
m_floppy_connector->option_add("35dd", FLOPPY_35_DD);
m_floppy_connector->set_default_option("35dd");
m_floppy_connector->set_formats(esq5505_state::floppy_formats);
FLOPPY_CONNECTOR(config, m_floppy_connector, esq5505_state::floppy_drives, "35dd", esq5505_state::floppy_formats, true);//.enable_sound(true);

HD63450(config, m_dmac, 10_MHz_XTAL); // MC68450 compatible
m_dmac->set_cpu_tag(m_maincpu);
Expand All @@ -656,10 +842,7 @@ void esq5505_state::vfxsd(machine_config &config, int panel_type)
m_pump->add_route(3, "aux", 1.0, 1);

WD1772(config, m_fdc, 8000000);
FLOPPY_CONNECTOR(config, m_floppy_connector);
m_floppy_connector->option_add("35dd", FLOPPY_35_DD);
m_floppy_connector->set_default_option("35dd");
m_floppy_connector->set_formats(esq5505_state::floppy_formats);
FLOPPY_CONNECTOR(config, m_floppy_connector, esq5505_state::floppy_drives, "35dd", esq5505_state::floppy_formats, true).enable_sound(true);
}

void esq5505_state::sd1(machine_config &config, int panel_type)
Expand Down Expand Up @@ -724,7 +907,7 @@ void esq5505_state::common32(machine_config &config)
m_otis->set_region0("waverom"); /* Bank 0 */
m_otis->set_region1("waverom2"); /* Bank 1 */
m_otis->set_channels(4); /* channels */
m_otis->irq_cb().set_inputline(m_maincpu, M68K_IRQ_1);
m_otis->irq_cb().set(FUNC(esq5505_state::otis_irq));
m_otis->read_port_cb().set(FUNC(esq5505_state::analog_r)); /* ADC */
m_otis->add_route(0, "pump", 1.0, 0);
m_otis->add_route(1, "pump", 1.0, 1);
Expand Down
Loading
Loading