From 2b4c203d88e672e7c117d6266c5bb6ed7ef5bd00 Mon Sep 17 00:00:00 2001 From: AJR Date: Fri, 10 Jan 2020 09:00:28 -0500 Subject: vt52: Draw characters on screen; add notes about processor architecture vt50dasm: Slightly smarter jump decoding; note another (unemulated) VT52 difference (nw) --- src/devices/cpu/vt50/vt50.cpp | 155 ++++++++++++++++++++++++++++++++++++-- src/devices/cpu/vt50/vt50.h | 16 +++- src/devices/cpu/vt50/vt50dasm.cpp | 26 +++++-- src/devices/cpu/vt50/vt50dasm.h | 3 +- src/mame/drivers/vt52.cpp | 24 ++++-- 5 files changed, 203 insertions(+), 21 deletions(-) diff --git a/src/devices/cpu/vt50/vt50.cpp b/src/devices/cpu/vt50/vt50.cpp index d2ed58dde79..b12b75e79da 100644 --- a/src/devices/cpu/vt50/vt50.cpp +++ b/src/devices/cpu/vt50/vt50.cpp @@ -2,13 +2,69 @@ // copyright-holders:AJR /*************************************************************************** - DEC VT50/VT52 CPU skeleton + DEC VT50/VT52 microprocessor emulation + + The principal components of this custom TTL-based processor are + inelegantly divided between two PCBs: ROM, UART & Timing (RUT) and + Data Paths, Memory & Decoders. The UART present on the former board + is not included in this CPU emulation, which uses callbacks instead + (as for the keyboard, which is a separate component in the same case). + Opcodes may contain up to four instructions each, which are executed + sequentially during defined phases of the instruction cycle. + + The machine cycle time (each instruction takes two cycles) is also the + time it takes to display one character. RAM addresses are determined + by the contents of the X and Y registers (plus one XOR gate applied to + bit 3 of the X output) for both displayed characters and programmed + data transfers. During non-blanked portions of of horizontal lines, X + (but not Y) is automatically incremented as each character is latched, + with the lowest 3 bits of the accumulator defining the character scan + line. The firmware uses the tail end of RAM as its scratchpad area. + + The accumulator, X and Y registers are mostly implemented as 74193 + up/down counters. There is no proper ALU, only a 7485 magnitude + comparator and an 8242 equality checker whose output is also used to + establish the position of the underline cursor. + + RAM is 7 bits wide, even though the VT50's character generator can + only accept 6 bits. Most of the registers are also effectively 7 bits + wide, though unused eighth bits are physically present. PC is also + physically 12 bits wide, though only up to 10 bits are usable. Y is + only 4 bits wide on the VT50, which has a 12-line display; in order + to double the quantity of addressable RAM to allow for 24 lines, the + VT52 adds an extra flip-flop stage to Y and rejumpers the address + encoding circuit. + + The mode flip-flop changes the meanings of the jump conditions and the + function of the constant load instruction, whose execution in mode 0 + is conditioned on equality with the preincremented accumulator. Jumps, + if taken, load the highest two bits of the destination (which define + the ROM page) from a ripple counter whose value may be incremented by + the IROM instruction. + + The done flip-flop is set any time data is committed to memory. Its + purpose is to ensure that only one in a sequence of consecutive + load instructions in the firmware's keyboard lookup routine is + actually executed. + + While horizontal blanking is defined in hardware as 20 characters out + of every 100, vertical blanking periods are arbitrarily determined by + when the firmware decides to deactivate the video flip-flop, which + necessitates an awkward workaround since MAME's screen emulation + expects a definite value. The vertical and horizontal synchronization + pulses are also generated without regard to each other, which causes + the screen refresh period to be 256 lines in 60 Hz mode and 307.2 + lines in 50 Hz mode. The unorthodox split structure of the timing + chain permits it to double as a baud rate generator. ***************************************************************************/ #include "emu.h" #include "vt50.h" #include "vt50dasm.h" +#include "screen.h" + +#define FIND_FIRST_LINE 0 // device type definitions DEFINE_DEVICE_TYPE(VT50_CPU, vt50_cpu_device, "vt50_cpu", "DEC VT50 CPU") @@ -16,6 +72,7 @@ DEFINE_DEVICE_TYPE(VT52_CPU, vt52_cpu_device, "vt52_cpu", "DEC VT52 CPU") vt5x_cpu_device::vt5x_cpu_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, u32 clock, int bbits, int ybits) : cpu_device(mconfig, type, tag, owner, clock) + , device_video_interface(mconfig, *this) , m_rom_config("program", ENDIANNESS_LITTLE, 8, 10, 0) , m_ram_config("data", ENDIANNESS_LITTLE, 8, 6 + ybits, 0) // actually 7 bits wide , m_rom_cache(nullptr) @@ -34,6 +91,7 @@ vt5x_cpu_device::vt5x_cpu_device(const machine_config &mconfig, device_type type , m_cen_callback(*this) , m_csf_callback(*this) , m_ccf_callback(*this) + , m_char_data_callback(*this) , m_bbits(bbits) , m_ybits(ybits) , m_pc(0) @@ -59,6 +117,8 @@ vt5x_cpu_device::vt5x_cpu_device(const machine_config &mconfig, device_type type , m_horiz_count(0) , m_vert_count(0) , m_top_of_screen(false) + , m_current_line(0) + , m_first_line(~0) { m_rom_config.m_is_octal = true; m_ram_config.m_is_octal = true; @@ -93,6 +153,18 @@ device_memory_interface::space_config_vector vt5x_cpu_device::memory_space_confi }; } +void vt5x_cpu_device::device_config_complete() +{ + if (!has_screen()) + return; + + if (!screen().has_screen_update()) + screen().set_screen_update(*this, FUNC(vt5x_cpu_device::screen_update)); + + if (!screen().refresh_attoseconds()) + screen().set_raw(clock(), 900, 128, 848, 256, 4, 244); // 60 Hz default parameters +} + void vt5x_cpu_device::device_resolve_objects() { // resolve callbacks @@ -110,6 +182,7 @@ void vt5x_cpu_device::device_resolve_objects() m_cen_callback.resolve_safe(); m_csf_callback.resolve_safe(1); m_ccf_callback.resolve_safe(1); + m_char_data_callback.resolve_safe(0177); } void vt52_cpu_device::device_resolve_objects() @@ -125,6 +198,7 @@ void vt5x_cpu_device::device_start() m_rom_cache = space(AS_PROGRAM).cache<0, 0, ENDIANNESS_LITTLE>(); m_ram_cache = space(AS_DATA).cache<0, 0, ENDIANNESS_LITTLE>(); + screen().register_screen_bitmap(m_bitmap); set_icountptr(m_icount); state_add(VT5X_PC, "PC", m_pc).formatstr("%04O").mask(01777); @@ -168,6 +242,12 @@ void vt5x_cpu_device::device_start() save_item(NAME(m_horiz_count)); save_item(NAME(m_vert_count)); save_item(NAME(m_top_of_screen)); + save_item(NAME(m_current_line)); +#if FIND_FIRST_LINE + save_item(NAME(m_first_line)); +#else + (void)m_first_line; +#endif } void vt5x_cpu_device::device_reset() @@ -184,11 +264,45 @@ void vt5x_cpu_device::device_reset() m_horiz_count = 0; m_vert_count = 0; m_top_of_screen = true; + m_current_line = 0; m_baud_9600_callback(0); m_vert_count_callback(0); } +u32 vt5x_cpu_device::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect) +{ + copybitmap(bitmap, m_bitmap, 0, 0, 0, 0, cliprect); + return 0; +} + +void vt5x_cpu_device::draw_char_line() +{ + if (m_current_line < screen().visible_area().top() || m_current_line > screen().visible_area().bottom()) + return; + + u8 hc = (u8(m_horiz_count) >> 4) * 10 + (m_horiz_count & 15); + unsigned xc = ((hc >= 22 ? hc : hc + 100) - 22) * 9 + screen().visible_area().left(); + if (xc > screen().visible_area().right() - 8) + return; + + u32 *pix = &m_bitmap.pix32(m_current_line, xc); + if (m_video_process && m_cursor_ff && m_cursor_active) + std::fill_n(pix, 9, rgb_t::white()); + else if (!m_video_process || m_cursor_ff) + std::fill_n(pix, 9, rgb_t::black()); + else + { + // CD6 is first shifted out; CD0 is last out + u8 vsr = m_char_data_callback(u16(m_ram_do) << 3 | (m_ac & 7)) | 0200; + for (int i = 0; i < 9; i++) + { + *pix++ = BIT(vsr, 7) ? rgb_t::black() : rgb_t::white(); + vsr = (vsr << 1) | 1; + } + } +} + offs_t vt5x_cpu_device::translate_xy() const { // A9 A8 A7 A6 A5 A4 A3 A2 A1 A0 @@ -525,7 +639,8 @@ void vt5x_cpu_device::execute_th(u8 inst) break; case 0120: - // TRUJ + // M0: COPJ (TODO?) + // M1: TRUJ m_load_pc = true; break; @@ -599,7 +714,27 @@ void vt5x_cpu_device::clock_video_counters() m_horiz_count++; if (m_horiz_count == 8) { - m_top_of_screen = false; + if (m_top_of_screen) + { + m_top_of_screen = false; + + // This calculates the number of visible lines, which is actually firmware-defined. + bool is_60hz = BIT(m_vert_count, 9); + unsigned first_line = is_60hz ? 4 : 32; + screen().configure( + 900, + (010000 - m_vert_count) / 10, + rectangle(128, 847, first_line, 24 * (is_60hz ? 10 : 11) + first_line - 1), + clocks_to_attotime((010000 - m_vert_count) * 90).as_attoseconds() + ); + screen().reset_origin(); + m_current_line = 0; +#if FIND_FIRST_LINE + m_first_line = ~0; +#endif + } + else + m_current_line++; m_baud_9600_callback(0); } else if (m_horiz_count == 4) @@ -632,8 +767,18 @@ void vt5x_cpu_device::execute_run() if (!m_write_ff) m_ram_do = m_ram_cache->read_byte(translate_xy()) & 0177; m_cursor_active = m_ac == (m_x ^ (m_x8 ? 8 : 0)); - if (m_video_process && u8(m_horiz_count - 2) >= 2 * 16) - m_x = (m_x + 1) & 0177; + if (u8(m_horiz_count - 2) >= 2 * 16) + { + if (m_video_process) + { +#if FIND_FIRST_LINE + if (m_first_line > m_current_line) + m_first_line = m_current_line; +#endif + m_x = (m_x + 1) & 0177; + } + draw_char_line(); + } m_t = 3; break; diff --git a/src/devices/cpu/vt50/vt50.h b/src/devices/cpu/vt50/vt50.h index 206dfe86488..fd73d1fd83e 100644 --- a/src/devices/cpu/vt50/vt50.h +++ b/src/devices/cpu/vt50/vt50.h @@ -6,7 +6,7 @@ #pragma once -class vt5x_cpu_device : public cpu_device +class vt5x_cpu_device : public cpu_device, public device_video_interface { public: enum { @@ -31,12 +31,17 @@ public: auto cen_callback() { return m_cen_callback.bind(); } auto ccf_callback() { return m_ccf_callback.bind(); } auto csf_callback() { return m_csf_callback.bind(); } + auto char_data_callback() { return m_char_data_callback.bind(); } + + // screen update method + u32 screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect); protected: // construction/destruction vt5x_cpu_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, u32 clock, int bbits, int ybits); // device-level overrides + virtual void device_config_complete() override; virtual void device_resolve_objects() override; virtual void device_start() override; virtual void device_reset() override; @@ -50,6 +55,9 @@ protected: // device_state_interface overrides virtual void state_string_export(const device_state_entry &entry, std::string &str) const override; + // video helpers + void draw_char_line(); + // address translation offs_t translate_xy() const; @@ -83,6 +91,7 @@ protected: devcb_write_line m_cen_callback; devcb_read_line m_csf_callback; devcb_read_line m_ccf_callback; + devcb_read8 m_char_data_callback; // register dimensions const u8 m_bbits; @@ -116,6 +125,11 @@ protected: u8 m_horiz_count; u16 m_vert_count; bool m_top_of_screen; + u16 m_current_line; + u16 m_first_line; + + // display bitmap + bitmap_rgb32 m_bitmap; }; class vt50_cpu_device : public vt5x_cpu_device diff --git a/src/devices/cpu/vt50/vt50dasm.cpp b/src/devices/cpu/vt50/vt50dasm.cpp index 84e9f231fb3..5f97d1eb1a9 100644 --- a/src/devices/cpu/vt50/vt50dasm.cpp +++ b/src/devices/cpu/vt50/vt50dasm.cpp @@ -34,7 +34,7 @@ const char *const vt5x_disassembler::s_opcodes_e[8] = { }; const char *const vt5x_disassembler::s_opcodes_f[8] = { - "DXDY", "IA", "IA1", "IY", "DY", "IROM", "DX", "DA" + "DXDY", "IA", "IA1", "IY", "DY", "IROM", "DX", "DA" // IA is functionally duplicated }; const char *const vt50_disassembler::s_opcodes_g[8] = { @@ -45,11 +45,16 @@ const char *const vt52_disassembler::s_opcodes_g[8] = { "M2A", "A2M", "M2U", "B2M", "M2X", "U2M", "M2B", "GRPH" }; -const char *const vt5x_disassembler::s_jumps_h[2][8] = { +const char *const vt50_disassembler::s_jumps_h[2][8] = { { "PSC", "TAB", "KCL", "FRQ", "PRQ", "TRU", "UT", "TOS" }, // mode 0 { "UR", "AEM", "ALM", "ADX", "AEM2", nullptr, "VSC", "KEY" } // mode 1 }; +const char *const vt52_disassembler::s_jumps_h[2][8] = { + { "PSC", "TAB", "KCL", "FRQ", "PRQ", "COP", "UT", "TOS" }, // mode 0 + { "UR", "AEM", "ALM", "ADX", "AEM2", "TRU", "VSC", "KEY" } // mode 1 +}; + const char *const vt5x_disassembler::s_opcodes_w[8] = { "SCFF", "SVID", "B2Y", "CBFF", "ZCAV", "LPB", "EPR", "HPR!ZY" }; @@ -99,12 +104,21 @@ offs_t vt5x_disassembler::disassemble(std::ostream &stream, offs_t pc, const vt5 { if (!first) stream << "!"; - util::stream_format(stream, "%sJ", m_jumps_h[0][(opcode & 0160) >> 4]); - if (m_jumps_h[1][(opcode & 0160) >> 4] != nullptr) - util::stream_format(stream, "/%sJ", m_jumps_h[1][(opcode & 0160) >> 4]); + bool m0 = (opcode & 0170) != 0130; + bool m1 = (opcode & 0170) != 0170; + if (m_jumps_h[1][(opcode & 0160) >> 4] == nullptr) + m0 = std::exchange(m1, false); + if (m0) + { + util::stream_format(stream, "%sJ", m_jumps_h[0][(opcode & 0160) >> 4]); + if (m1) + stream << "/"; + } + if (m1) + util::stream_format(stream, "%sJ", m_jumps_h[1][(opcode & 0160) >> 4]); u16 nextpc = pc + 1; - if ((opcode & 0164) == 0124) // IROM!TRUJ adjustment + if ((opcode & 0164) == 0124) // IROM adjustment nextpc += 0400; util::stream_format(stream, " %04o", (nextpc & 01400) | opcodes.r8(pc + 1)); return 2; diff --git a/src/devices/cpu/vt50/vt50dasm.h b/src/devices/cpu/vt50/vt50dasm.h index ce9b16a3d06..163598c133c 100644 --- a/src/devices/cpu/vt50/vt50dasm.h +++ b/src/devices/cpu/vt50/vt50dasm.h @@ -18,7 +18,6 @@ protected: // tables static const char *const s_opcodes_e[8]; static const char *const s_opcodes_f[8]; - static const char *const s_jumps_h[2][8]; static const char *const s_opcodes_w[8]; private: @@ -38,6 +37,7 @@ public: private: // tables static const char *const s_opcodes_g[8]; + static const char *const s_jumps_h[2][8]; }; class vt52_disassembler : public vt5x_disassembler @@ -49,6 +49,7 @@ public: private: // tables static const char *const s_opcodes_g[8]; + static const char *const s_jumps_h[2][8]; }; #endif // MAME_CPU_VT50_VT50DASM_H diff --git a/src/mame/drivers/vt52.cpp b/src/mame/drivers/vt52.cpp index 110c7ebb03e..254fb36c932 100644 --- a/src/mame/drivers/vt52.cpp +++ b/src/mame/drivers/vt52.cpp @@ -14,8 +14,8 @@ character generator (on a board of its own). VT50 and VT52 each had minor variants differing in keyboard function and printer availability. - The VT55 was a graphical terminal ostensibly in the same family as the - VT50 and VT52. Its hardware commonalities and differences are unknown. + The VT55 DECgraphic Scope was a graphical terminal based on the same + main boards as the VT50 and VT52. ****************************************************************************/ @@ -37,6 +37,7 @@ public: , m_keys(*this, "KEY%d", 0U) , m_baud_sw(*this, "BAUD") , m_data_sw(*this, "DATABITS") + , m_chargen(*this, "chargen") { } @@ -56,6 +57,7 @@ private: void uart_xd_w(u8 data); DECLARE_WRITE_LINE_MEMBER(serial_out_w); DECLARE_READ_LINE_MEMBER(xrdy_eoc_r); + u8 chargen_r(offs_t offset); void rom_1k(address_map &map); void ram_2k(address_map &map); @@ -65,6 +67,7 @@ private: required_ioport_array<8> m_keys; required_ioport m_baud_sw; required_ioport m_data_sw; + required_region_ptr m_chargen; }; void vt52_state::machine_reset() @@ -150,6 +153,12 @@ READ_LINE_MEMBER(vt52_state::xrdy_eoc_r) return m_uart->tbmt_r() || m_uart->eoc_r(); } +u8 vt52_state::chargen_r(offs_t offset) +{ + // ROM is on its own board, shared only with 7404 inverters + return ~m_chargen[offset]; +} + void vt52_state::rom_1k(address_map &map) { map(00000, 01777).rom().region("program", 0); @@ -306,6 +315,7 @@ void vt52_state::vt52(machine_config &mconfig) VT52_CPU(mconfig, m_maincpu, 13.824_MHz_XTAL); m_maincpu->set_addrmap(AS_PROGRAM, &vt52_state::rom_1k); m_maincpu->set_addrmap(AS_DATA, &vt52_state::ram_2k); + m_maincpu->set_screen("screen"); m_maincpu->baud_9600_callback().set(FUNC(vt52_state::baud_9600_w)); m_maincpu->vert_count_callback().set(FUNC(vt52_state::vert_count_w)); m_maincpu->uart_rd_callback().set(m_uart, FUNC(ay51013_device::receive)); @@ -317,14 +327,12 @@ void vt52_state::vt52(machine_config &mconfig) m_maincpu->kclk_callback().set_ioport("KEYCLICK"); m_maincpu->frq_callback().set_ioport("60HJ"); m_maincpu->bell_callback().set("bell", FUNC(speaker_sound_device::level_w)); + m_maincpu->char_data_callback().set(FUNC(vt52_state::chargen_r)); AY51013(mconfig, m_uart); // TR1402 or equivalent m_uart->write_so_callback().set(FUNC(vt52_state::serial_out_w)); - screen_device &screen(SCREEN(mconfig, "screen", SCREEN_TYPE_RASTER)); - screen.set_raw(13.824_MHz_XTAL, 900, 0, 720, 256, 0, 240); - //screen.set_raw(13.824_MHz_XTAL, 900, 0, 720, 307.2, 0, 264); // not a whole number of scans - screen.set_screen_update(FUNC(vt52_state::screen_update)); + SCREEN(mconfig, "screen", SCREEN_TYPE_RASTER); SPEAKER(mconfig, "mono").front_center(); SPEAKER_SOUND(mconfig, "bell").add_route(ALL_OUTPUTS, "mono", 1.0); // FIXME: uses a flyback diode circuit @@ -337,8 +345,8 @@ ROM_START(vt52) ROM_LOAD_NIB_LOW( "23-126a9.e37", 0x200, 0x200, CRC(4883a600) SHA1(c5d9b0c21493065c75b4a7d52d5bd47f9851dfe7)) ROM_LOAD_NIB_HIGH("23-127a9.e21", 0x200, 0x200, CRC(56c1c0d6) SHA1(ab0eb6e7bbafcc3d28481b62de3d3490f01c0174)) - ROM_REGION(0x400, "chargen", 0) // 2608 character generator + ROM_REGION(0x400, "chargen", 0) // 2608 (non-JEDEC) character generator ROM_LOAD("23-002b4.e1", 0x000, 0x400, CRC(b486500c) SHA1(029f07424d6c23ee083db42d9f9c252ac728ccd0)) ROM_END -COMP(1975, vt52, 0, 0, vt52, vt52, vt52_state, empty_init, "Digital Equipment Corporation", "VT52 Video Display Terminal", MACHINE_NOT_WORKING | MACHINE_IMPERFECT_SOUND) +COMP(1975, vt52, 0, 0, vt52, vt52, vt52_state, empty_init, "Digital Equipment Corporation", "VT52 Video Display Terminal", MACHINE_NOT_WORKING | MACHINE_IMPERFECT_SOUND | MACHINE_NODEVICE_PRINTER) -- cgit v1.2.3