diff --git a/src/mame/layout/paia_midi2cv8.lay b/src/mame/layout/paia_midi2cv8.lay new file mode 100644 index 0000000000000..aabae410d2c13 --- /dev/null +++ b/src/mame/layout/paia_midi2cv8.lay @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mame/mame.lst b/src/mame/mame.lst index 401883b6906f6..aeb4e3f875a00 100644 --- a/src/mame/mame.lst +++ b/src/mame/mame.lst @@ -35955,6 +35955,10 @@ penta // bootleg @source:pacman/schick.cpp schick // Microhard +@source:paia/midi2cv8.cpp +midi2cv8 // PAiA midi2cv8 (V/Oct) +midi2cv8_vhz // PAiA midi2cv8 with V/Hz daughterboard + @source:palm/palm.cpp palmiii // Palm III palmiiic // Palm IIIc diff --git a/src/mame/paia/midi2cv8.cpp b/src/mame/paia/midi2cv8.cpp new file mode 100644 index 0000000000000..056f9c016cd2b --- /dev/null +++ b/src/mame/paia/midi2cv8.cpp @@ -0,0 +1,396 @@ +// license:BSD-3-Clause +// copyright-holders:m1macrophage + +/* +The PAiA midi2cv8 is an 8-channel MIDI-to-CV converter. It is a module for +the PAiA 9700 Series modular system, but can be used as a standalone MIDI-to-CV +converter. + +Dipswitches control the MIDI channel to listen to, and the operating mode. +Depending on that mode, each of the 8 outputs produces control voltage (CV), +trigger, or gate signals in response to MIDI messages. + +The firmware is running on an 80C31. It listens for MIDI messages and sets the +voltage for the 8 outputs by time-multiplexing 8 sample & hold (S&H) circuits. +Specifically, the firmware will write a value to the 8-bit DAC, and after some +delay for the DAC to settle, it will enable the S&H for one output. After some +additional delay for the S&H capacitor to (dis)charge to the target value, the +firmware will disable that S&H and move on to the next one. + +The midi2cv8 comes in two configurations: Volt-per-octave (V/Oct) and +Volt-per-Hertz (V/Hz). + +In the V/Oct configuration, the output of the DAC (a current) is converted to a +voltage in the range 0-10V. + +The V/Hz configuration adds a daughterboard and changes some of the components. +R32 (2.7Kohm) is added, and R28 is changed from 5.6Kohm to 2.7Kohm. These change +the DAC output range to 5-10V. The daughterboard is then used to divide that by +1, 2, 4, 8 or 16 (controlled by the MCU). This is a neat trick to get +exponential voltage output for 5 octaves, while only using an 8-bit DAC. + +The firmware for the two configurations is the same. P1.7 of the MCU is read at +startup to detect whether the daughterboard is installed. If it is, the firmware +will switch to V/Hz mode. See the two variants of is_volts_per_hz_r(). + +This driver is based on the schematics, user manual and documentation provided +by the manufacturer on their website. Both the V/Oct and V/Hz configurations are +emulated. This driver cannot replace the real device. MAME does not output +physical voltages. This is just an educational tool. + +Usage: + +The provided layout will display the generated CVs/triggers/gates next to the +outputs. + +The default dipswitch setting for "Mode" ("Mode 8 - self tests") does not +require MIDI. Just run the driver and see voltages come to life on the screen. + +The other modes require supplying a MIDI input source to MAME. +Example: +./mame -listmidi # List MIDI devices, physical or virtual (e.g. DAWs). +./mame -window midi2cv8 -midiin "{midi device}" + +Keep in mind that dipswitch changes don't take effect until a restart or +reset (F3). +*/ + +#include "emu.h" + +#include "cpu/mcs51/mcs51.h" +#include "bus/midi/midiinport.h" +#include "bus/midi/midioutport.h" +#include "video/pwm.h" + +#include "paia_midi2cv8.lh" + +#define LOG_DAC (1U << 1) +#define LOG_CVS (1U << 2) + +#define VERBOSE (LOG_GENERAL) +//#define LOG_OUTPUT_FUNC osd_printf_info + +#include "logmacro.h" + +namespace { + +constexpr const char MAINCPU_TAG[] = "80c31"; + +class midi2cv8_state : public driver_device +{ +public: + midi2cv8_state(const machine_config &mconfig, device_type type, const char *tag) ATTR_COLD + : driver_device(mconfig, type, tag) + , m_maincpu(*this, MAINCPU_TAG) + , m_midi_pwm_led(*this, "midi_pwm_led") + , m_cv_display_integer(*this, "cv_%d_integer", 1U) + , m_cv_display_fractional(*this, "cv_%d_fractional", 1U) + , m_cv(8, -1) + { + } + + void midi2cv8(machine_config &config) ATTR_COLD; + +protected: + static constexpr const float DAC_V_MAX = 10; + + void machine_start() override ATTR_COLD; + + u8 get_dac_value() const; + void update_active_cv(); + virtual bool compute_cv(float *cv) const; + virtual int is_volts_per_hz_r() const; + + mcs51_cpu_device &get_maincpu() { return *m_maincpu; } + +private: + void midi_rxd_w(int state); + int midi_rxd_r() const; + + void dac_w(u8 data); + void output_mux_select_w(u8 data); + + void program_map(address_map &map) ATTR_COLD; + void external_memory_map(address_map &map) ATTR_COLD; + + required_device m_maincpu; + required_device m_midi_pwm_led; + output_finder<8> m_cv_display_integer; + output_finder<8> m_cv_display_fractional; + + bool m_inhibit_output_mux = false; + u8 m_selected_output_mux = 0; + u8 m_dac_value = 0; + u8 m_midi_rxd_bit = 1; // Initial value needs to be 1, for serial "idle". + std::vector m_cv; +}; + +// MIDI2CV8 with the V/Hz daughterboard. +class midi2cv8_vhz_state : public midi2cv8_state +{ +public: + midi2cv8_vhz_state(const machine_config &mconfig, device_type type, const char *tag) ATTR_COLD + : midi2cv8_state(mconfig, type, tag) + { + } + + void midi2cv8_vhz(machine_config &config) ATTR_COLD; + +protected: + void machine_start() override ATTR_COLD; + + bool compute_cv(float *cv) const override; + int is_volts_per_hz_r() const override; + +private: + void octave_mux_select_w(u8 data); + + u8 m_selected_octave_mux = 0; +}; + +// The implementations of midi2cv8_state and midi2cv8_vhz_state below are +// interleaved, to better demonstrate the difference in behavior between them. + +u8 midi2cv8_state::get_dac_value() const +{ + return m_dac_value; +} + +void midi2cv8_state::update_active_cv() +{ + // Mapping from CV MUX output to the output on the panel from top to bottom. + // 0 is at the top, 7 at the bottom. + static constexpr const int OUTPUT_MAPPING[8] = {0, 4, 1, 5, 2, 6, 3, 7}; + + if (m_inhibit_output_mux) + return; + + float cv = 0; + if (!compute_cv(&cv)) + return; + + const int physical_output = OUTPUT_MAPPING[m_selected_output_mux]; + if (cv == m_cv[physical_output]) + return; + + m_cv[physical_output] = cv; + const s32 cv_millis = s32(round(1000 * cv)); + m_cv_display_integer[physical_output] = cv_millis / 1000; + m_cv_display_fractional[physical_output] = cv_millis % 1000; + + LOGMASKED(LOG_CVS, "CV %d - %d: %f - %d @ %f\n", + physical_output + 1, m_selected_output_mux, cv, cv_millis, + machine().time().as_double()); +} + +bool midi2cv8_state::compute_cv(float *cv) const +{ + *cv = DAC_V_MAX * get_dac_value() / 255.0F; + return true; +} + +bool midi2cv8_vhz_state::compute_cv(float *cv) const +{ + // -1 means the MUX input is not connected. + static constexpr const float DIVIDE_BY[8] = {-1, 1, 2, 4, 8, 16, -1, -1}; + static constexpr const float V_HALF = DAC_V_MAX / 2; + + assert(m_selected_octave_mux >= 0 && m_selected_octave_mux < 8); + const float divisor = DIVIDE_BY[m_selected_octave_mux]; + if (divisor <= 0) + return false; + + *cv = (V_HALF + V_HALF * get_dac_value() / 255.0F) / divisor; + return true; +} + +int midi2cv8_state::is_volts_per_hz_r() const +{ + // P1.7 pulled up by R54, but connected to GND when the V/Hz option is not + // installed. This results in P1.7 reading as 0. + return 0; +} + +int midi2cv8_vhz_state::is_volts_per_hz_r() const +{ + // P1.7 pulled up by R54, and connected to R5 in the V/Hz board, when + // that board is installed. This results in P1.7 reading as 1. + return 1; +} + +void midi2cv8_state::midi_rxd_w(int state) +{ + m_midi_rxd_bit = state; + + // MIDI IN state is inverted twice (IC6:A and IC6:C) and connected to the + // cathode of LED D2. So the LED will be on when MIDI IN is low. + m_midi_pwm_led->write_element(0, 0, state ? 0 : 1); +} + +int midi2cv8_state::midi_rxd_r() const +{ + return m_midi_rxd_bit; +} + +void midi2cv8_state::dac_w(u8 data) +{ + if (m_dac_value == data) + return; + m_dac_value = data; + update_active_cv(); + LOGMASKED(LOG_DAC, "DAC value: %02x\n", m_dac_value); +} + +void midi2cv8_vhz_state::octave_mux_select_w(u8 data) +{ + // Octave MUX (IC4) is a 4051. X0, X6 and X7 are not connected. + // MUX INH is tied low, so it is always enabled. + // P1.5 -> OCT A. + // P1.6 -> OCT B. + // P1.7 -> OCT C. + // All signals above are inverted and level-shifted by Q1-Q3. + + const u8 selection = (~data & 0xe0) >> 5; // Bits 5-7. + if (m_selected_octave_mux == selection) + return; + m_selected_octave_mux = selection; + update_active_cv(); +} + +void midi2cv8_state::output_mux_select_w(u8 data) +{ + // MUX (IC13) is a 4051. + // P3.1 -> MUX INH + // P3.2 -> MUX B + // P3.3 -> MUX C + // P3.4 -> MUX A + // All signals above are inverted and level-shifted by Q4-Q7. + + data = ~data & 0x1e; + const bool inhibit = BIT(data, 1); + const u8 selection = bitswap<3>(data, 3, 2, 4); + if (inhibit == m_inhibit_output_mux && selection == m_selected_output_mux) + return; + m_inhibit_output_mux = inhibit; + m_selected_output_mux = selection; + update_active_cv(); +} + +void midi2cv8_state::program_map(address_map &map) +{ + // A13-A15 are not connected. + map(0x0000, 0x1fff).mirror(0xe000).rom(); +} + +void midi2cv8_state::external_memory_map(address_map &map) +{ + // Address lines ignored on external memory writes. + map(0x0000, 0x0000).mirror(0xffff).w(FUNC(midi2cv8_state::dac_w)); +} + +void midi2cv8_state::machine_start() +{ + m_cv_display_integer.resolve(); + m_cv_display_fractional.resolve(); + + save_item(NAME(m_inhibit_output_mux)); + save_item(NAME(m_selected_output_mux)); + save_item(NAME(m_dac_value)); + save_item(NAME(m_midi_rxd_bit)); + save_item(NAME(m_cv)); +} + +void midi2cv8_vhz_state::machine_start() +{ + midi2cv8_state::machine_start(); + save_item(NAME(m_selected_octave_mux)); +} + +void midi2cv8_state::midi2cv8(machine_config &config) +{ + I80C31(config, m_maincpu, 12_MHz_XTAL); + m_maincpu->set_addrmap(AS_PROGRAM, &midi2cv8_state::program_map); + m_maincpu->set_addrmap(AS_IO, &midi2cv8_state::external_memory_map); + + m_maincpu->port_in_cb<1>().set_ioport("dsw").mask(0x1f); // P1.0-P1.4 + m_maincpu->port_in_cb<1>().append(FUNC(midi2cv8_state::is_volts_per_hz_r)).lshift(7).mask(0x80); // P1.7 + + m_maincpu->port_in_cb<3>().set(FUNC(midi2cv8_state::midi_rxd_r)).mask(0x01); // P3.0 + m_maincpu->port_in_cb<3>().append_ioport("dsw").mask(0x20); // P3.5 <- DSW BIT 5 + m_maincpu->port_in_cb<3>().append_ioport("dsw").lshift(1).mask(0x80); // P3.7 <- DSW BIT 6 + m_maincpu->port_out_cb<3>().set(FUNC(midi2cv8_state::output_mux_select_w)); + + midi_port_device &midi_in(MIDI_PORT(config, "mdin", midiin_slot, "midiin")); + MIDI_PORT(config, "mdthru", midiout_slot, "midiout"); + midi_in.rxd_handler().set(FUNC(midi2cv8_state::midi_rxd_w)); + midi_in.rxd_handler().append("mdthru", FUNC(midi_port_device::write_txd)); + + PWM_DISPLAY(config, m_midi_pwm_led).set_size(1, 1); + m_midi_pwm_led->output_x().set_output("midi_led"); + // These values make the MIDI LED in the default layout functional, + // without being too annoying. + m_midi_pwm_led->set_interpolation(0.2); + m_midi_pwm_led->set_bri_levels(0.0001); + m_midi_pwm_led->set_refresh(attotime::from_hz(30)); + + config.set_default_layout(layout_paia_midi2cv8); +} + +void midi2cv8_vhz_state::midi2cv8_vhz(machine_config &config) +{ + midi2cv8(config); + get_maincpu().port_out_cb<1>().set(FUNC(midi2cv8_vhz_state::octave_mux_select_w)); +} + +INPUT_PORTS_START(midi2cv8) + PORT_START("dsw") + PORT_DIPNAME(0x0f, 0x00, "MIDI Channel") PORT_DIPLOCATION("SW1:1,2,3,4") + PORT_DIPSETTING( 0x00, "1") + PORT_DIPSETTING( 0x01, "2") + PORT_DIPSETTING( 0x02, "3") + PORT_DIPSETTING( 0x03, "4") + PORT_DIPSETTING( 0x04, "5") + PORT_DIPSETTING( 0x05, "6") + PORT_DIPSETTING( 0x06, "7") + PORT_DIPSETTING( 0x07, "8") + PORT_DIPSETTING( 0x08, "9") + PORT_DIPSETTING( 0x09, "10") + PORT_DIPSETTING( 0x0a, "11") + PORT_DIPSETTING( 0x0b, "12") + PORT_DIPSETTING( 0x0c, "13") + PORT_DIPSETTING( 0x0d, "14") + PORT_DIPSETTING( 0x0e, "15") + PORT_DIPSETTING( 0x0f, "16") + PORT_DIPNAME(0x70, 0x70, "Mode") PORT_DIPLOCATION("SW1:5,6,7") + PORT_DIPSETTING( 0x00, "Mode 1 - 1 voice") + PORT_DIPSETTING( 0x10, "Mode 2 - 2 voice") + PORT_DIPSETTING( 0x20, "Mode 3 - 4 voice") + PORT_DIPSETTING( 0x30, "Mode 4 - control change") + PORT_DIPSETTING( 0x40, "Mode 5 - analog drum") + PORT_DIPSETTING( 0x50, "Mode 6 - din sync") + PORT_DIPSETTING( 0x60, "Mode 7 - [unused]") + PORT_DIPSETTING( 0x70, "Mode 8 - self-tests") + PORT_DIPNAME(0x80, 0x00, "Not Connected") PORT_DIPLOCATION("SW1:8") + PORT_DIPSETTING( 0x00, DEF_STR(On)) + PORT_DIPSETTING( 0x80, DEF_STR(Off)) +INPUT_PORTS_END + +#define ROMS_MIDI2CV8 \ + ROM_REGION(0x2000, MAINCPU_TAG, 0) \ + ROM_DEFAULT_BIOS("v201") \ + ROM_SYSTEM_BIOS(0, "v201", "v2.01 - December 1997") \ + ROMX_LOAD("midi2cv_v2.01.ic2", 0x000000, 0x002000, CRC(bae8c045) SHA1(a5db57e53831b73903a0fb171e0444e6956febc3), ROM_BIOS(0)) + +ROM_START(midi2cv8) + ROMS_MIDI2CV8 +ROM_END + +ROM_START(midi2cv8_vhz) + ROMS_MIDI2CV8 +ROM_END + +} // anonymous namespace + +SYST(1997, midi2cv8, 0, 0, midi2cv8, midi2cv8, midi2cv8_state, empty_init, "PAiA Electronics", "midi2cv8", MACHINE_NO_SOUND_HW | MACHINE_SUPPORTS_SAVE) +SYST(1997, midi2cv8_vhz, 0, 0, midi2cv8_vhz, midi2cv8, midi2cv8_vhz_state, empty_init, "PAiA Electronics", "midi2cv8 V/Hz", MACHINE_NO_SOUND_HW | MACHINE_SUPPORTS_SAVE) +