vt52: Draw characters on screen; add notes about processor architecture

vt50dasm: Slightly smarter jump decoding; note another (unemulated) VT52 difference (nw)
This commit is contained in:
AJR 2020-01-10 09:00:28 -05:00
parent a7e88ab526
commit 2b4c203d88
5 changed files with 203 additions and 21 deletions

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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<u8> 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)