mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
The skip gate reaches the root weave and stops: video RAM and the rendered
frame are byte-identical at frames 3000/10000/30000/45000/60000 (42 s -> 14
min), and all 4 MiB of main RAM is identical at every sample except one byte
that oscillates. The PC histogram is tight: entered at frame ~611, dominated
by 0x3FB30390/0x3FB30110.
Wall: DUART2 (0x0620000, keyboard/mouse) is unmodelled, so the read-only ISR
at 0x06200005 is served by the board write-back latch and returns the value
the OS wrote to the write-only IMR (0x0033). The OS's DUART dispatcher
(0x3FB43800) masks 0x66 -> 0x22, so it services a phantom interrupt that can
never clear, spinning in the channel handler 0x3FB42410 and queue routine
0x3FB300F0. Forcing only 0x06200005 to 0 reproduces the full DUART2-mapped
run byte-for-byte; with VT1200_DUART2=1 the Terminal Manager dialog box from
the reference video appears at frame 3000 and the machine runs on.
Defaults unchanged: no-env still reaches HOST PORT - 15, skip still reaches
the weave. New opt-in knobs VT1200_DUMP_{FRAME,RAM_FRAMES,VRAM_FRAMES} and
resolve_console.py; progress.lua untouched.
959 lines
39 KiB
C++
959 lines
39 KiB
C++
// license:BSD-3-Clause
|
|
// copyright-holders: VT1200 reverse-engineering project
|
|
/***************************************************************************
|
|
|
|
DEC VT1200 X terminal -- MAME execution harness
|
|
|
|
This is NOT an accurate hardware emulation. It exists to give us a way
|
|
to *execute* the VT1200 firmware under the TMS34010 interpreter with a
|
|
debugger (breakpoints, single step, memory watch, trace).
|
|
|
|
What is modelled BY DEFAULT. A bare `vt1200 vt1200` boots the way the
|
|
old six-variable incantation
|
|
VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2
|
|
VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_DUART=1 VT1200_DUART_SEL=1
|
|
did. Five of those knobs are now the default and no longer exist; the
|
|
sixth, VT1200_BOARD_SWITCH, was superseded once DUART1's input port
|
|
(IP4-IP6) became the monitor-switch source.
|
|
|
|
* TMS34010 CPU (bit-addressed: MAME's tms34010 memory space uses a
|
|
right shift of 3, so address-map units are BITS).
|
|
* RAM at logical bit address 0, 4 MiB (byte 0 .. 0x3fffff).
|
|
* Video RAM at bit 0x04000000 (required for the level-1 VIDEO MEMORY
|
|
test to pass).
|
|
* The four undecoded board windows, driving the handshake register model
|
|
with a two-tick busy bit (required to leave early POST).
|
|
* MAME's SCN2681 at DUART1 (0x0640000 = host/printer), with its input
|
|
port IP4-IP6 held high -- which is how the driver reads the VR150
|
|
monitor switch (value 0x70). The level-1 host-port transmit is
|
|
ISR-driven, so a real DUART is required.
|
|
* The 1.75 MiB firmware ROM (physical byte base 0x07e40000 = bit
|
|
0x3f200000), mirrored into the three upper 1 GiB quadrants so that
|
|
the reset-vector page at logical bit 0xffffffe0 resolves to the last
|
|
four bytes of the image (file offset 0x1bfffc).
|
|
|
|
What is NOT modelled: LANCE Ethernet, DUART2 (0x0620000 = keyboard/mouse;
|
|
opt in with VT1200_DUART2=1), and the board system logic beyond the
|
|
register model. The firmware therefore still stops at the level-1 HOST
|
|
PORT test and wedges in the system-board LED loop; that is expected.
|
|
|
|
---------------------------------------------------------------------
|
|
Instrumentation knobs (all opt-in; none changes execution):
|
|
|
|
VT1200_BOARD_LOG=<path> append a line for every *distinct*
|
|
(address, PC) board access, flushed
|
|
immediately. Shows every register the
|
|
firmware polls and the PC polling it.
|
|
VT1200_BOARD_MAP=<list> per-address overrides, e.g.
|
|
"0x06200010=1;0x0640000d=0"
|
|
VT1200_DUARTLOG=<path> every DUART access (not de-duplicated),
|
|
so a write that happens once per iteration
|
|
cannot hide.
|
|
VT1200_IRQLOG=<path> DUART1 IRQ assert: ISR/SRA at the moment of
|
|
the interrupt.
|
|
VT1200_LOG_UNMAPPED=<path> catch-all over the whole address space;
|
|
anything the driver does not model is
|
|
logged once per distinct (address, PC,
|
|
direction).
|
|
VT1200_DPYLOG=<path> dump the TMS34010 display params, one line
|
|
per scanline, for the first
|
|
VT1200_DPYLOG_FRAMES frames (default 1).
|
|
VT1200_DUMP_FRAME=<n> write frame <n> as vt1200-frame-<n>.ppm
|
|
VT1200_DUMP_RAM=<n> write main RAM at frame <n> as
|
|
vt1200-ram-<n>.bin
|
|
VT1200_DUMP_VRAM=<n> write video memory at frame <n> as
|
|
vram-<n>.bin
|
|
VT1200_DUMP_FRAMES=<list> comma-separated frames, all in ONE run, so the
|
|
same machine can be sampled at several
|
|
increasing times and the samples compared.
|
|
Same for VT1200_DUMP_RAM_FRAMES and
|
|
VT1200_DUMP_VRAM_FRAMES.
|
|
VT1200_DUMP_DIR=<dir> directory for the dumps (default ".")
|
|
VT1200_NO_RENDER=1 blank the screen instead of painting it
|
|
|
|
Behaviour switch (deliberately diverges from the real terminal):
|
|
|
|
VT1200_SKIP_LEVEL1=1 drive DUART1's input-port bit 3 high. The
|
|
real terminal reads IP3 low, so with this
|
|
UNSET the boot runs the whole level-1
|
|
self-test batch (the faithful default).
|
|
With it set, the boot gate at bit
|
|
0x3F2042D0 (`@0x64000D0` bit 3 set and the
|
|
0x6200000 status handshake not 0x55)
|
|
branches to the OS cold-start 0x3F5788C0,
|
|
skipping ALL of the level-1 batch. Because
|
|
the OS phase is then in charge, the switch
|
|
ALSO routes DUART1's IRQ to INT2 (line 1):
|
|
the OS's DUART1 driver attaches there,
|
|
while POST's DUART dispatcher sits on INT1
|
|
(line 0), the default. Both lines are
|
|
enabled (INTENB = 0x0606) and the RAM stub
|
|
at 0x0F60 is rewritten between the two
|
|
phases, so the two wirings correspond to
|
|
the two phases' own routings. See
|
|
reference/level1-bypass-analysis.md.
|
|
|
|
Video: the driver paints the active scanlines itself, reading the
|
|
framebuffer out of video RAM (see render_active_line).
|
|
|
|
VT1200_DUART2=1 additionally maps MAME's SCN2681 at 0x0620000
|
|
(keyboard/mouse). It is off by default because mapping both DUARTs
|
|
diverts the level-0 keyboard path.
|
|
|
|
See FULL_BOOT_ESTIMATE.md section 4b for measured results.
|
|
|
|
***************************************************************************/
|
|
|
|
#include "emu.h"
|
|
#include "cpu/tms34010/tms34010.h"
|
|
#include "emupal.h"
|
|
#include "machine/mc68681.h"
|
|
#include "screen.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "vt1200_board.h"
|
|
|
|
|
|
namespace {
|
|
|
|
// The undecoded windows, in bit-address units. 0x5900000 is paired with
|
|
// 0x5800000 as a command/data register interface (write a command to
|
|
// 0x5900000, then data to 0x5800000; the firmware converts a bit address to a
|
|
// byte address with >>3 before feeding it, which implies a DMA-style device
|
|
// that uses byte addressing).
|
|
static const u32 BOARD_BASE[4] = { VT1200_WIN0, VT1200_WIN1, VT1200_WIN2, VT1200_WIN3 };
|
|
|
|
// --- Framebuffer geometry (measured, not derived from MAME) -----------------
|
|
// Video RAM is mapped at bit address 0x04000000 (the base that unblocks the
|
|
// level-1 VIDEO MEMORY test). The firmware's own blitter writes it as a
|
|
// linear 1-bit-per-pixel buffer with a 0x40-word (128-byte) row pitch; the
|
|
// TMS34010 paints the visible pixels LSB-first within each 16-bit word.
|
|
// Verified by rendering the dumped VRAM: at this geometry the level-1 error
|
|
// screen is legible ("LEVEL1/2 ERROR -> HOST PORT - 15"); at the derived
|
|
// 160-byte / 80-word pitch MAME computed from HEBLNK/HSBLNK/HTOTAL the text is
|
|
// sheared and unreadable. See RENDER_REPORT.md.
|
|
static const u32 VRAM_BASE_BITS = 0x04000000; // where the framebuffer lives
|
|
static const int VMEM_PITCH_BITS = 1024; // 0x40 words = 128 bytes
|
|
static const int VMEM_WIDTH = 1024; // visible pixels per row
|
|
static const int VMEM_HEIGHT = 1024; // rows in the framebuffer
|
|
|
|
class vt1200_state : public driver_device
|
|
{
|
|
public:
|
|
vt1200_state(const machine_config &mconfig, device_type type, const char *tag)
|
|
: driver_device(mconfig, type, tag)
|
|
, m_maincpu(*this, "maincpu")
|
|
, m_screen(*this, "screen")
|
|
, m_palette(*this, "palette")
|
|
, m_mainram(*this, "mainram")
|
|
, m_duart1(*this, "duart1")
|
|
, m_duart2(*this, "duart2")
|
|
, m_vram(*this, "vram")
|
|
{
|
|
}
|
|
|
|
void vt1200(machine_config &config);
|
|
|
|
virtual void machine_start() override ATTR_COLD;
|
|
virtual void machine_reset() override ATTR_COLD;
|
|
|
|
// scanline callback: renders (or blanks) the line, and can dump a frame
|
|
void scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline,
|
|
const tms340x0_device::display_params *params);
|
|
void render_active_line(bitmap_ind16 &bitmap, int scanline,
|
|
const tms340x0_device::display_params *params);
|
|
void dump_frame(screen_device &screen, bitmap_ind16 &bitmap, long frame);
|
|
void dump_ram(long frame);
|
|
void dump_vram(long frame);
|
|
|
|
// We drive the TMS34010 renderer ourselves so the frame dump does not
|
|
// depend on the device's scanline-callback plumbing.
|
|
uint32_t screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect);
|
|
|
|
// Board window access, one pair per window so the absolute address is
|
|
// unambiguous no matter whether MAME hands us an entry-relative or an
|
|
// absolute offset.
|
|
u16 board0_r(offs_t off); void board0_w(offs_t off, u16 data);
|
|
u16 board1_r(offs_t off); void board1_w(offs_t off, u16 data);
|
|
u16 board2_r(offs_t off); void board2_w(offs_t off, u16 data);
|
|
u16 board3_r(offs_t off); void board3_w(offs_t off, u16 data);
|
|
|
|
// The two DUARTs. DUART1 = host (ch A) + printer (ch B) at window
|
|
// 0x0640000 and is always mapped; DUART2 = keyboard (ch A) + mouse (ch B)
|
|
// at window 0x0620000 is opt-in via VT1200_DUART2.
|
|
u16 duart1_r(offs_t off); void duart1_w(offs_t off, u16 data);
|
|
u16 duart2_r(offs_t off); void duart2_w(offs_t off, u16 data);
|
|
|
|
// DUART interrupt outputs -> the TMS34010's two external interrupt lines.
|
|
// The firmware installs RAM trampolines in the CPU's fixed vector table
|
|
// (INT1 at vector 0xffffffc0 -> stub at bit 0x0f60 = JA 0x3FA92F40, the
|
|
// DUART1 ISR; INT2 at 0xffffffa0 -> stub at bit 0x0f30 = JA 0x3FA93070,
|
|
// also a DUART1 entry). Without this wiring the level-1 host-port test's
|
|
// interrupt-driven transmit never runs (see DUART_REPORT.md).
|
|
void duart1_irq_w(int state);
|
|
void duart2_irq_w(int state);
|
|
|
|
private:
|
|
void program_map(address_map &map) ATTR_COLD;
|
|
|
|
u16 board_read(int win, offs_t off);
|
|
void board_write(int win, offs_t off, u16 data);
|
|
void log_pc_access(char kind, u32 addr, u16 value);
|
|
|
|
required_device<tms34010_device> m_maincpu;
|
|
required_device<screen_device> m_screen;
|
|
required_device<palette_device> m_palette;
|
|
required_shared_ptr<u16> m_mainram;
|
|
|
|
// The two DUARTs (Signetics SCN2681 / Motorola MC68681 family). The
|
|
// firmware's level-1 I/O tests drive them byte-per-register (register N at
|
|
// byte 2N, i.e. harness offset N), which MAME's 68681 model expects.
|
|
required_device<scn2681_device> m_duart1; // 0x0640000: host(A)/printer(B)
|
|
required_device<scn2681_device> m_duart2; // 0x0620000: keyboard(A)/mouse(B)
|
|
bool m_duart1_int2 = false; // set by VT1200_SKIP_LEVEL1
|
|
|
|
// The four undecoded board windows, owned by the standalone model in
|
|
// vt1200_board.h so it can be unit-tested without MAME. The driver only
|
|
// forwards its window callbacks and configures the model from the env knobs.
|
|
vt1200_board m_board;
|
|
|
|
std::map<std::pair<u32, u32>, u32> m_seen; // (addr, pc) -> count
|
|
FILE *m_log = nullptr;
|
|
|
|
// Video. Rendering is on by default now that the firmware reaches the
|
|
// display code; VT1200_NO_RENDER=1 restores the old blank-everything
|
|
// behaviour. VT1200_DUMP_FRAME=<n> writes that frame as a PPM.
|
|
//
|
|
// The *_FRAMES variants take a comma-separated list and dump at EXACTLY
|
|
// each frame, so one run can sample the same machine at several increasing
|
|
// times -- the instrument for telling "progressing" from "stuck": if two
|
|
// samples minutes apart are byte-identical, nothing is being drawn.
|
|
bool m_render = true;
|
|
long m_dump_frame = -1;
|
|
std::vector<long> m_dump_frames; // VT1200_DUMP_FRAMES=3000,10000,...
|
|
std::string m_dump_dir = ".";
|
|
|
|
// VT1200_DPYLOG=<path>: dump the TMS34010 display params for the first
|
|
// VT1200_DPYLOG_FRAMES frames (default 1), one line per visible scanline.
|
|
// Used to pin the VRAM fetch address/geometry against the real machine's
|
|
// 1280x1024 (160 bytes/line) numbers.
|
|
FILE *m_dpylog = nullptr;
|
|
long m_dpylog_frames = 1;
|
|
|
|
// VT1200_DUMP_RAM=<frame> writes main RAM at that frame to
|
|
// vt1200-ram-<frame>.bin so the framebuffer can be located offline.
|
|
long m_dump_ram = -1;
|
|
bool m_dumped_ram = false;
|
|
std::vector<long> m_dump_ram_frames; // VT1200_DUMP_RAM_FRAMES=...
|
|
|
|
// VT1200_DUMP_VRAM=<frame> writes the video memory to vram-<frame>.bin so the
|
|
// framebuffer layout can be determined offline.
|
|
long m_dump_vram = -1;
|
|
bool m_dumped_vram = false;
|
|
std::vector<long> m_dump_vram_frames; // VT1200_DUMP_VRAM_FRAMES=...
|
|
optional_shared_ptr<u16> m_vram;
|
|
|
|
// VT1200_LOG_UNMAPPED=<path>
|
|
// A catch-all mapping over the whole address space, registered FIRST so every
|
|
// real region added afterwards takes precedence. Anything the firmware
|
|
// touches that we do not model lands here, and is logged once per distinct
|
|
// (address, PC, direction). This is how we find regions we have never
|
|
// mapped -- the video memory in particular.
|
|
FILE *m_unmapped_log = nullptr;
|
|
std::map<std::pair<u32, u32>, u32> m_unmapped_seen; // (addr, pc|dir) -> count
|
|
u16 unmapped_r(offs_t off);
|
|
void unmapped_w(offs_t off, u16 data);
|
|
void log_unmapped(const char *kind, u32 addr, const char *pc);
|
|
|
|
// VT1200_IRQLOG=<path>: log DUART1's IRQ assertions (ISR/SRA at the
|
|
// moment of the interrupt).
|
|
FILE *m_irqlog = nullptr;
|
|
|
|
// VT1200_DUARTLOG=<path>
|
|
// EVERY DUART access (not de-duplicated), so a write that happens once per
|
|
// iteration cannot hide. Used to prove whether the transmit register is
|
|
// ever written on the host-port test path.
|
|
FILE *m_duartlog = nullptr;
|
|
void log_duart(int win, char kind, u32 reg, u16 value);
|
|
};
|
|
|
|
// Parse a comma-separated frame list ("3000,10000,30000,60000") into a vector,
|
|
// ignoring empty items. Used by the multi-timestamp dump knobs.
|
|
static std::vector<long> parse_frame_list(const char *s)
|
|
{
|
|
std::vector<long> out;
|
|
if (!s)
|
|
return out;
|
|
std::string str(s);
|
|
size_t pos = 0;
|
|
while (pos <= str.size())
|
|
{
|
|
const size_t comma = str.find(',', pos);
|
|
std::string item = str.substr(pos, (comma == std::string::npos)
|
|
? std::string::npos : comma - pos);
|
|
// trim surrounding whitespace
|
|
const size_t b = item.find_first_not_of(" \t");
|
|
const size_t e = item.find_last_not_of(" \t");
|
|
if (b != std::string::npos)
|
|
out.push_back(strtol(item.substr(b, e - b + 1).c_str(), nullptr, 0));
|
|
pos = (comma == std::string::npos) ? str.size() + 1 : comma + 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void vt1200_state::machine_start()
|
|
{
|
|
if (const char *list = getenv("VT1200_BOARD_MAP"))
|
|
{
|
|
std::string s(list), item;
|
|
size_t pos = 0;
|
|
while (pos <= s.size())
|
|
{
|
|
size_t semi = s.find(';', pos);
|
|
item = s.substr(pos, (semi == std::string::npos) ? std::string::npos : semi - pos);
|
|
pos = (semi == std::string::npos) ? s.size() + 1 : semi + 1;
|
|
if (item.empty())
|
|
continue;
|
|
size_t eq = item.find('=');
|
|
if (eq == std::string::npos)
|
|
continue;
|
|
u32 a = u32(strtoul(item.substr(0, eq).c_str(), nullptr, 0));
|
|
u16 v = u16(strtoul(item.substr(eq + 1).c_str(), nullptr, 0));
|
|
m_board.set_override(a, v);
|
|
}
|
|
}
|
|
|
|
if (const char *path = getenv("VT1200_BOARD_LOG"))
|
|
{
|
|
m_log = fopen(path, "w");
|
|
if (m_log)
|
|
fprintf(m_log, "# kind addr pc value (bit addresses; distinct (addr,pc) only)\n");
|
|
}
|
|
|
|
// Handshake model: write-back registers + a self-clearing busy bit. The
|
|
// board model owns the state machine; these are the settled defaults (a
|
|
// two-tick busy bit is what leaves early POST).
|
|
m_board.set_model_handshake(true);
|
|
m_board.set_busy_ticks(2);
|
|
|
|
// Video: render by default; optionally dump a frame as PPM.
|
|
if (getenv("VT1200_NO_RENDER"))
|
|
m_render = false;
|
|
if (const char *f = getenv("VT1200_DUMP_FRAME"))
|
|
m_dump_frame = strtol(f, nullptr, 0);
|
|
if (const char *f = getenv("VT1200_DUMP_RAM"))
|
|
m_dump_ram = strtol(f, nullptr, 0);
|
|
if (const char *f = getenv("VT1200_DUMP_VRAM"))
|
|
m_dump_vram = strtol(f, nullptr, 0);
|
|
if (const char *f = getenv("VT1200_DUMP_FRAMES"))
|
|
m_dump_frames = parse_frame_list(f);
|
|
if (const char *f = getenv("VT1200_DUMP_RAM_FRAMES"))
|
|
m_dump_ram_frames = parse_frame_list(f);
|
|
if (const char *f = getenv("VT1200_DUMP_VRAM_FRAMES"))
|
|
m_dump_vram_frames = parse_frame_list(f);
|
|
if (const char *d = getenv("VT1200_DUMP_DIR"))
|
|
m_dump_dir = d;
|
|
if (const char *f = getenv("VT1200_DPYLOG"))
|
|
{
|
|
m_dpylog = fopen(f, "w");
|
|
if (m_dpylog)
|
|
fprintf(m_dpylog, "# line rowaddr coladdr yoff heblnk hsblnk veblnk vsblnk vcount\n");
|
|
}
|
|
if (const char *f = getenv("VT1200_DPYLOG_FRAMES"))
|
|
m_dpylog_frames = strtol(f, nullptr, 0);
|
|
|
|
if (const char *p = getenv("VT1200_LOG_UNMAPPED"))
|
|
{
|
|
m_unmapped_log = fopen(p, "w");
|
|
if (m_unmapped_log)
|
|
fprintf(m_unmapped_log,
|
|
"# kind addr pc -- accesses to memory the driver does NOT model\n"
|
|
"# address units are the address-map's; firmware operand = addr << 4\n");
|
|
}
|
|
|
|
if (const char *p = getenv("VT1200_IRQLOG"))
|
|
{
|
|
m_irqlog = fopen(p, "w");
|
|
if (m_irqlog)
|
|
fprintf(m_irqlog, "# DUART1 IRQ assert: ISR/SRA at the moment of the interrupt\n");
|
|
}
|
|
|
|
if (const char *p = getenv("VT1200_DUARTLOG"))
|
|
{
|
|
m_duartlog = fopen(p, "w");
|
|
if (m_duartlog)
|
|
fprintf(m_duartlog, "# win kind reg val pc -- every DUART access (not de-duplicated)\n");
|
|
}
|
|
|
|
// With the real DUART in place, board-level signals that used to be faked
|
|
// by the window model come from the DUART's input port (read register 0x0D:
|
|
// IP0-IP6, bit 7 always set). The monitor-selection switch is IP bits 4-6;
|
|
// driving them high selects the VR150 CRT (0x70), matching the value the
|
|
// firmware looks for. IP2 stays low (the value for the blit-status test at
|
|
// bit 0x3F204AA0).
|
|
m_duart1->ip4_w(1);
|
|
m_duart1->ip5_w(1);
|
|
m_duart1->ip6_w(1);
|
|
|
|
// VT1200_SKIP_LEVEL1=1 drives DUART1 IP3 high, which makes the boot gate at
|
|
// bit 0x3F2042D0 take the OS cold-start branch (0x3F5788C0) and skip the
|
|
// whole level-1 self-test batch. Since the OS phase is then in charge, the
|
|
// same switch routes DUART1's IRQ to INT2 (line 1): the OS's DUART1 driver
|
|
// attaches there, while POST's dispatcher sits on INT1 (line 0). Both
|
|
// lines are enabled (INTENB = 0x0606), so this selects which phase's
|
|
// routing applies.
|
|
if (getenv("VT1200_SKIP_LEVEL1"))
|
|
{
|
|
m_duart1->ip3_w(1);
|
|
m_duart1_int2 = true;
|
|
}
|
|
|
|
logerror("vt1200: board model=handshake busy_ticks=2 switch=duart-ip4/5/6 log=%s render=%d dump=%ld\n",
|
|
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
|
|
logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=%s skip_level1=%s\n",
|
|
getenv("VT1200_DUART2") ? "on (kbd/mouse)" : "off",
|
|
getenv("VT1200_SKIP_LEVEL1") ? "on (IP3 high, IRQ->INT2)" : "off (IP3 low, IRQ->INT1)");
|
|
}
|
|
|
|
void vt1200_state::machine_reset()
|
|
{
|
|
if (m_log)
|
|
fflush(m_log);
|
|
if (m_unmapped_log)
|
|
fflush(m_unmapped_log);
|
|
}
|
|
|
|
void vt1200_state::log_unmapped(const char *kind, u32 addr, const char *pc)
|
|
{
|
|
if (!m_unmapped_log)
|
|
return;
|
|
const u32 p = (u32)strtoul(pc, nullptr, 16);
|
|
const char d = (kind[0] == 'W') ? 1u : 0u;
|
|
auto key = std::make_pair(addr, (p << 1) | d);
|
|
auto it = m_unmapped_seen.find(key);
|
|
if (it == m_unmapped_seen.end())
|
|
{
|
|
m_unmapped_seen.emplace(key, 1);
|
|
fprintf(m_unmapped_log, "%s %08X pc=%08X\n", kind, addr, p);
|
|
fflush(m_unmapped_log);
|
|
}
|
|
}
|
|
|
|
u16 vt1200_state::unmapped_r(offs_t off)
|
|
{
|
|
char pc[16];
|
|
snprintf(pc, sizeof(pc), "%08X", (unsigned)m_maincpu->state_int(TMS34010_PC));
|
|
log_unmapped("R", (u32)off, pc);
|
|
return 0x0000;
|
|
}
|
|
|
|
void vt1200_state::unmapped_w(offs_t off, u16 data)
|
|
{
|
|
char pc[16];
|
|
snprintf(pc, sizeof(pc), "%08X", (unsigned)m_maincpu->state_int(TMS34010_PC));
|
|
log_unmapped("W", (u32)off, pc);
|
|
}
|
|
|
|
void vt1200_state::program_map(address_map &map)
|
|
{
|
|
// Catch-all for anything we do not model -- registered FIRST so every region
|
|
// below takes precedence. Anything still landing here is memory the
|
|
// firmware touches that the driver has never mapped; it is logged.
|
|
// NOTE: gated on the environment variable, not on m_unmapped_log, because
|
|
// program_map() runs at machine-configuration time and machine_start()
|
|
// (which opens the file) has not run yet.
|
|
if (getenv("VT1200_LOG_UNMAPPED"))
|
|
map(0x00000000, 0xffffffff).rw(FUNC(vt1200_state::unmapped_r), FUNC(vt1200_state::unmapped_w));
|
|
|
|
// Video memory, at bit 0x04000000. Located empirically and then corrected
|
|
// by a closed cross-check of two instruments: the catch-all's handler
|
|
// receives (bitaddr - base) >> 4, so its logged 0x400000..0x40D7FF means BIT
|
|
// 0x04000000..0x040D7FF0 == bytes 0x800000..0x81AFFE (~110 KB, about one
|
|
// 1280x1024 1bpp framebuffer). Mapping it is what lets the level-1 VIDEO
|
|
// MEMORY test's walking-bit read-back pass, so it is unconditional now.
|
|
map(VRAM_BASE_BITS, 0x1fffffff).ram().share("vram");
|
|
|
|
// Main RAM: 4 MiB (1 MiB onboard + 3 x 1 MiB SIMMs on the real machine).
|
|
// The TMS34010 is bit-addressed, so 4 MiB of bytes == 0x2000000 address
|
|
// units. (The earlier 0x200000 range modelled only 256 KiB, which is
|
|
// almost certainly why POST mis-detected memory.)
|
|
map(0x00000000, 0x01ffffff).ram().share("mainram");
|
|
|
|
// ROM: physical byte base 0x07e40000 -> bit base 0x3f200000, 0x1c0000 bytes
|
|
// (0xe00000 bit addresses), plus the three upper logical aliases that the
|
|
// TMS34010 generates because its top two address bits are not emitted.
|
|
map(0x3f200000, 0x3fffffff).rom().region("maincpu", 0);
|
|
map(0x7f200000, 0x7fffffff).rom().region("maincpu", 0);
|
|
map(0xbf200000, 0xbfffffff).rom().region("maincpu", 0);
|
|
map(0xff200000, 0xffffffff).rom().region("maincpu", 0);
|
|
|
|
// Undecoded board windows, driving the handshake register model. Always
|
|
// mapped: the busy handshake is what leaves early POST.
|
|
map(BOARD_BASE[0], BOARD_BASE[0] + 0xffff).rw(FUNC(vt1200_state::board0_r), FUNC(vt1200_state::board0_w));
|
|
map(BOARD_BASE[1], BOARD_BASE[1] + 0xffff).rw(FUNC(vt1200_state::board1_r), FUNC(vt1200_state::board1_w));
|
|
map(BOARD_BASE[2], BOARD_BASE[2] + 0xffff).rw(FUNC(vt1200_state::board2_r), FUNC(vt1200_state::board2_w));
|
|
map(BOARD_BASE[3], BOARD_BASE[3] + 0xffff).rw(FUNC(vt1200_state::board3_r), FUNC(vt1200_state::board3_w));
|
|
|
|
// MAME's SCN2681 at DUART1 (0x0640000 = host/printer), registered AFTER the
|
|
// board-window model so it wins the register slice (map precedence is
|
|
// last-wins); the model still serves any other register in the same window.
|
|
// Base in map units is the bit address; 16 registers at 2 bytes each = 32
|
|
// bytes = 0x100 bit units. DUART2 (0x0620000 = keyboard/mouse) is opt-in
|
|
// via VT1200_DUART2: mapping both diverts the level-0 keyboard path.
|
|
map(VT1200_WIN3, VT1200_WIN3 + 0xff).rw(FUNC(vt1200_state::duart1_r), FUNC(vt1200_state::duart1_w));
|
|
if (getenv("VT1200_DUART2"))
|
|
map(VT1200_WIN2, VT1200_WIN2 + 0xff).rw(FUNC(vt1200_state::duart2_r), FUNC(vt1200_state::duart2_w));
|
|
}
|
|
// MAME may hand us either an entry-relative offset (0..0xffff) or the absolute
|
|
// address; BOARD_BASE is 0x10000-aligned, so ORing the low 16 bits is correct
|
|
// in both cases.
|
|
u16 vt1200_state::board_read(int win, offs_t off)
|
|
{
|
|
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
|
|
const u16 value = m_board.read(addr);
|
|
log_pc_access('R', addr, value);
|
|
return value;
|
|
}
|
|
|
|
void vt1200_state::board_write(int win, offs_t off, u16 data)
|
|
{
|
|
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
|
|
m_board.write(addr, data);
|
|
log_pc_access('W', addr, data);
|
|
}
|
|
|
|
void vt1200_state::log_pc_access(char kind, u32 addr, u16 value)
|
|
{
|
|
if (!m_log)
|
|
return;
|
|
const u32 pc = u32(m_maincpu->state_int(TMS34010_PC));
|
|
auto key = std::make_pair(addr, pc);
|
|
auto it = m_seen.find(key);
|
|
if (it == m_seen.end())
|
|
{
|
|
m_seen.emplace(key, 1);
|
|
fprintf(m_log, "%c %08X pc=%08X val=%04X\n", kind, addr, pc, value);
|
|
fflush(m_log);
|
|
}
|
|
else if (kind == 'R')
|
|
{
|
|
it->second++;
|
|
}
|
|
}
|
|
|
|
u16 vt1200_state::board0_r(offs_t off) { return board_read(0, off); }
|
|
u16 vt1200_state::board1_r(offs_t off) { return board_read(1, off); }
|
|
u16 vt1200_state::board2_r(offs_t off) { return board_read(2, off); }
|
|
u16 vt1200_state::board3_r(offs_t off) { return board_read(3, off); }
|
|
void vt1200_state::board0_w(offs_t off, u16 data) { board_write(0, off, data); }
|
|
void vt1200_state::board1_w(offs_t off, u16 data) { board_write(1, off, data); }
|
|
void vt1200_state::board2_w(offs_t off, u16 data) { board_write(2, off, data); }
|
|
void vt1200_state::board3_w(offs_t off, u16 data) { board_write(3, off, data); }
|
|
|
|
// --- DUARTs ---------------------------------------------------------------
|
|
//
|
|
// Remap. The map entry base is a BIT address and the handler receives
|
|
// (bitaddr - base) >> 4, so byte offset within the window = off * 2. The
|
|
// observed access pattern is a canonical 68681 setup (CRA writes of
|
|
// 0x3A/0x2A/0x4A/0x1A, CSRA=0x88/0x99, MR1A/MR2A at off 0) and those offsets
|
|
// line up with the datasheet register map only when register index = off:
|
|
// byte 2N (harness off N) with the register in the low byte of the 16-bit
|
|
// access. So:
|
|
//
|
|
// firmware operand 0x064000D0 -> bit addr -> harness off 0x0D
|
|
// -> byte offset 0x1A = register 0x0D (IP)
|
|
//
|
|
// i.e. duart_byte = handler_off * 2 and duart_register = handler_off.
|
|
//
|
|
// Each window is 16 registers (32 bytes = 0x100 map units); the entry only
|
|
// covers that, so anything else in the window still falls through to the
|
|
// board model below it (a window may mix a DUART with other board registers).
|
|
// The firmware presents the register value in the LOW byte of its 16-bit
|
|
// move (the board log shows value 0x0088 for CSRA), hence `data & 0xff`.
|
|
|
|
u16 vt1200_state::duart1_r(offs_t off)
|
|
{
|
|
const u32 a = VT1200_WIN3 | (u32(off) & 0xffff);
|
|
if ((off & 0xffff) < 0x10)
|
|
{
|
|
const u16 v = m_duart1->read(u32(off) & 0xf);
|
|
log_pc_access('R', a, v);
|
|
log_duart(1, 'R', u32(off) & 0xf, v);
|
|
return v;
|
|
}
|
|
return board_read(3, off);
|
|
}
|
|
|
|
void vt1200_state::duart1_w(offs_t off, u16 data)
|
|
{
|
|
const u32 a = VT1200_WIN3 | (u32(off) & 0xffff);
|
|
if ((off & 0xffff) < 0x10)
|
|
{
|
|
m_duart1->write(u32(off) & 0xf, u8(data & 0xff));
|
|
log_pc_access('W', a, data);
|
|
log_duart(1, 'W', u32(off) & 0xf, data);
|
|
return;
|
|
}
|
|
board_write(3, off, data);
|
|
}
|
|
|
|
u16 vt1200_state::duart2_r(offs_t off)
|
|
{
|
|
const u32 a = VT1200_WIN2 | (u32(off) & 0xffff);
|
|
if ((off & 0xffff) < 0x10)
|
|
{
|
|
const u16 v = m_duart2->read(u32(off) & 0xf);
|
|
log_pc_access('R', a, v);
|
|
log_duart(2, 'R', u32(off) & 0xf, v);
|
|
return v;
|
|
}
|
|
return board_read(2, off);
|
|
}
|
|
|
|
void vt1200_state::duart2_w(offs_t off, u16 data)
|
|
{
|
|
const u32 a = VT1200_WIN2 | (u32(off) & 0xffff);
|
|
if ((off & 0xffff) < 0x10)
|
|
{
|
|
m_duart2->write(u32(off) & 0xf, u8(data & 0xff));
|
|
log_pc_access('W', a, data);
|
|
log_duart(2, 'W', u32(off) & 0xf, data);
|
|
return;
|
|
}
|
|
board_write(2, off, data);
|
|
}
|
|
|
|
// The DUART's IRQ output is a single open-drain line per chip. Wire it to the
|
|
// CPU's external interrupt input the firmware's vector trampoline expects:
|
|
// line 0 -> TMS34010_INT1 (INTPEND 0x0002), line 1 -> TMS34010_INT2 (0x0004).
|
|
// MAME's tms34010 latches the pending bit from the line state, so deasserting
|
|
// the line (which the DUART does once ISR & IMR == 0) clears it again.
|
|
void vt1200_state::duart1_irq_w(int state)
|
|
{
|
|
if (m_irqlog && state)
|
|
{
|
|
// ISR (reg 5) and SRA (reg 1) are side-effect-free reads.
|
|
const u32 isr = m_duart1->read(5) & 0xff;
|
|
const u32 sra = m_duart1->read(1) & 0xff;
|
|
fprintf(m_irqlog, "irq=1 isr=%02X sra=%02X cyc=%llu\n", isr, sra,
|
|
(unsigned long long)m_maincpu->total_cycles());
|
|
fflush(m_irqlog);
|
|
}
|
|
m_maincpu->set_input_line(m_duart1_int2 ? 1 : 0, state ? ASSERT_LINE : CLEAR_LINE);
|
|
}
|
|
|
|
void vt1200_state::duart2_irq_w(int state)
|
|
{
|
|
m_maincpu->set_input_line(1, state ? ASSERT_LINE : CLEAR_LINE);
|
|
}
|
|
|
|
void vt1200_state::log_duart(int win, char kind, u32 reg, u16 value)
|
|
{
|
|
if (!m_duartlog)
|
|
return;
|
|
fprintf(m_duartlog, "%d %c reg=%X val=%04X pc=%08X cyc=%llu\n",
|
|
win, kind, reg, value, (unsigned)m_maincpu->state_int(TMS34010_PC),
|
|
(unsigned long long)m_maincpu->total_cycles());
|
|
fflush(m_duartlog);
|
|
}
|
|
|
|
// Copy one active scanline out of the video RAM framebuffer.
|
|
//
|
|
// The TMS34010 screen-refresh address is reconstructed by the device into
|
|
// `params`, but the firmware's timing registers (HEBLNK/HSBLNK/VTOTAL) are not
|
|
// initialised to the real machine's values, so the geometry MAME derives from
|
|
// them (a 1280x901 screen, active x 192..1215) does not describe the memory
|
|
// layout. The measured memory layout does:
|
|
//
|
|
// * base bit 0x04000000 (the mapped video RAM)
|
|
// * pitch 1024 bits (0x40 words / 128 bytes) per row — taken from the
|
|
// firmware's own blitter, which advances its destination by
|
|
// 0x40 words per glyph row (pc=3FDF9A00 in VRAM write logs)
|
|
// * bit order LSB-first within each 16-bit word — the TMS34010 pixel order
|
|
// * active x the device's heblnk..hsblnk span is 1024 pixels, exactly the
|
|
// framebuffer width, so screen x-heblnk maps to framebuffer x
|
|
//
|
|
// The screen line maps 1:1 to a framebuffer row, offset by the vertical
|
|
// blanking start (params->veblnk).
|
|
void vt1200_state::render_active_line(bitmap_ind16 &bitmap, int scanline,
|
|
const tms340x0_device::display_params *params)
|
|
{
|
|
// Blank first: the device only repaints the horizontal blanking regions,
|
|
// so anything we leave here would be stale bitmap contents.
|
|
std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0));
|
|
|
|
if (!m_vram) // no video RAM mapped -> nothing to draw
|
|
return;
|
|
|
|
const int fby = scanline - (int)params->veblnk;
|
|
if (fby < 0 || fby >= VMEM_HEIGHT)
|
|
return;
|
|
|
|
const u16 *const vram = m_vram.target();
|
|
const int x0 = (int)params->heblnk;
|
|
const int x1 = (int)params->hsblnk;
|
|
const u32 row_bit = (u32)fby * VMEM_PITCH_BITS;
|
|
|
|
for (int fx = 0; fx < VMEM_WIDTH; fx++)
|
|
{
|
|
const int x = x0 + fx;
|
|
if (x >= x1 || x >= bitmap.width())
|
|
break;
|
|
const u32 bit = row_bit + (u32)fx;
|
|
// Address-map units are bits, so a 16-bit word is 16 units.
|
|
bitmap.pix(scanline, x) = (vram[bit >> 4] >> (bit & 15)) & 1;
|
|
}
|
|
}
|
|
|
|
void vt1200_state::scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline,
|
|
const tms340x0_device::display_params *params)
|
|
{
|
|
{
|
|
static int calls = 0;
|
|
if (calls++ < 4)
|
|
osd_printf_info("vt1200: scanline#%d line=%d enabled=%d\n",
|
|
calls, scanline, params ? (params->enabled ? 1 : 0) : -1);
|
|
}
|
|
if (m_dpylog && params && (long)screen.frame_number() < m_dpylog_frames)
|
|
{
|
|
fprintf(m_dpylog, "%d %04X %04X %d %d %d %d %d %d\n",
|
|
scanline, params->rowaddr, params->coladdr, params->yoffset,
|
|
params->heblnk, params->hsblnk, params->veblnk, params->vsblnk, params->vcount);
|
|
fflush(m_dpylog);
|
|
}
|
|
|
|
// Paint the active pixels ourselves. MAME's TMS34010 renderer does NOT
|
|
// read VRAM; tms340x0_ind16 calls this callback for the active region and
|
|
// only paints the blanking regions itself. If we leave the active pixels
|
|
// untouched the frame stays whatever the bitmap was (all black on the
|
|
// first frame).
|
|
if (m_render && params && params->enabled)
|
|
render_active_line(bitmap, scanline, params);
|
|
else
|
|
std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0));
|
|
|
|
// Dump on the last visible line of the target frame: by then the whole
|
|
// frame is in the bitmap, and unlike screen_update the scanline callback
|
|
// is reliably invoked once per line.
|
|
if (scanline == screen.visible_area().max_y)
|
|
{
|
|
// Once per frame, advance the board model's state machines (e.g. the
|
|
// self-clearing busy bit). Not screen_update: that is called with
|
|
// small clip regions, and its "reaches the last line" guard only fires
|
|
// sporadically on this driver.
|
|
m_board.tick();
|
|
|
|
const long f = (long)screen.frame_number();
|
|
// PPM frame dumps. The single-frame knob fires at GE n (its historical
|
|
// semantics); the list knob fires at EXACTLY each listed frame.
|
|
if (m_dump_frame >= 0 && f == m_dump_frame)
|
|
dump_frame(screen, bitmap, f);
|
|
for (long fr : m_dump_frames)
|
|
if (f == fr)
|
|
dump_frame(screen, bitmap, fr);
|
|
|
|
if (m_dump_ram >= 0 && !m_dumped_ram && f >= m_dump_ram)
|
|
{
|
|
m_dumped_ram = true;
|
|
dump_ram(f);
|
|
}
|
|
for (long fr : m_dump_ram_frames)
|
|
if (f == fr)
|
|
dump_ram(fr);
|
|
|
|
if (m_dump_vram >= 0 && !m_dumped_vram && f >= m_dump_vram)
|
|
{
|
|
m_dumped_vram = true;
|
|
dump_vram(f);
|
|
}
|
|
for (long fr : m_dump_vram_frames)
|
|
if (f == fr)
|
|
dump_vram(fr);
|
|
}
|
|
}
|
|
|
|
void vt1200_state::dump_frame(screen_device &screen, bitmap_ind16 &bitmap, long frame)
|
|
{
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/vt1200-frame-%ld.ppm", m_dump_dir.c_str(), frame);
|
|
FILE *fp = fopen(path, "wb");
|
|
if (!fp)
|
|
{
|
|
osd_printf_info("vt1200: cannot write %s\n", path);
|
|
return;
|
|
}
|
|
const int w = bitmap.width(), h = bitmap.height();
|
|
fprintf(fp, "P6\n%d %d\n255\n", w, h);
|
|
unsigned long lit = 0;
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
const u16 v = bitmap.pix(y, x);
|
|
if (v)
|
|
lit++;
|
|
const unsigned char c = v ? 255 : 0;
|
|
fputc(c, fp); fputc(c, fp); fputc(c, fp);
|
|
}
|
|
}
|
|
fclose(fp);
|
|
osd_printf_info("vt1200: dumped frame %ld -> %s (%dx%d, %lu lit = %.4f%%)\n",
|
|
frame, path, w, h, lit, 100.0 * lit / ((double)w * h));
|
|
}
|
|
|
|
uint32_t vt1200_state::screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect)
|
|
{
|
|
{
|
|
static int calls = 0;
|
|
if (calls++ < 4)
|
|
osd_printf_info("vt1200: screen_update#%d frame=%u clip_y=%d..%d screen=%dx%d\n",
|
|
calls, (unsigned)screen.frame_number(), cliprect.min_y, cliprect.max_y,
|
|
screen.width(), screen.height());
|
|
}
|
|
|
|
// The TMS34010 display controller does not read VRAM: it calls our
|
|
// scanline callback for the active region (which paints it from the
|
|
// framebuffer) and blanks the blanking region itself.
|
|
const uint32_t changed = m_maincpu->tms340x0_ind16(screen, bitmap, cliprect);
|
|
|
|
// Dump once the whole frame has been rendered (the video manager calls us
|
|
// per region, so wait for the region that reaches the bottom line).
|
|
if (cliprect.max_y >= screen.height() - 1)
|
|
{
|
|
static int guards = 0;
|
|
if (guards++ < 5)
|
|
osd_printf_info("vt1200: guard#%d frame=%u dump_frame=%ld dump_ram=%ld\n",
|
|
guards, (unsigned)screen.frame_number(), m_dump_frame, m_dump_ram);
|
|
const long f = (long)screen.frame_number();
|
|
if (m_dump_frame >= 0 && f == m_dump_frame)
|
|
dump_frame(screen, bitmap, f);
|
|
if (m_dump_ram >= 0 && !m_dumped_ram && f >= m_dump_ram)
|
|
{
|
|
m_dumped_ram = true;
|
|
dump_ram(f);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void vt1200_state::dump_ram(long frame)
|
|
{
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/vt1200-ram-%ld.bin", m_dump_dir.c_str(), frame);
|
|
FILE *fp = fopen(path, "wb");
|
|
if (!fp)
|
|
{
|
|
osd_printf_info("vt1200: cannot write %s\n", path);
|
|
return;
|
|
}
|
|
address_space &sp = m_maincpu->space(AS_PROGRAM);
|
|
(void)sp;
|
|
const u8 *base = reinterpret_cast<const u8 *>(m_mainram.target());
|
|
const size_t n = m_mainram.bytes();
|
|
fwrite(base, 1, n, fp);
|
|
fclose(fp);
|
|
osd_printf_info("vt1200: dumped %zu bytes of main RAM (frame %ld) -> %s\n",
|
|
n, frame, path);
|
|
}
|
|
|
|
void vt1200_state::dump_vram(long frame)
|
|
{
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/vram-%ld.bin", m_dump_dir.c_str(), frame);
|
|
FILE *fp = fopen(path, "wb");
|
|
if (!fp)
|
|
{
|
|
osd_printf_info("vt1200: cannot write %s\n", path);
|
|
return;
|
|
}
|
|
if (!m_vram)
|
|
{
|
|
osd_printf_info("vt1200: video RAM not mapped; nothing to dump\n");
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
const u8 *base = reinterpret_cast<const u8 *>(m_vram.target());
|
|
const size_t n = m_vram.bytes();
|
|
fwrite(base, 1, n, fp);
|
|
fclose(fp);
|
|
osd_printf_info("vt1200: dumped %zu bytes of video RAM (frame %ld) -> %s\n",
|
|
n, frame, path);
|
|
}
|
|
|
|
void vt1200_state::vt1200(machine_config &config)
|
|
{
|
|
// Clocks, derived from the real machine's 1280x1024 @ 72 Hz mono display and
|
|
// the firmware's own raster registers (HTOTAL=79, VTOTAL=900 observed):
|
|
//
|
|
// HTOTAL = 79 -> 80 pixel-clocks per line, and 80 * 16 = 1280 pixels,
|
|
// so the TMS34010 fetches SIXTEEN pixels per clock.
|
|
// refresh = pixel_clock / ((HTOTAL+1) * (VTOTAL+1))
|
|
// = pixel_clock / 72000 -> 72 Hz => pixel_clock = 5.184 MHz
|
|
//
|
|
// The CPU is a TMS34010FNL-50, i.e. 50 MHz (per the TMS34010 users guide and
|
|
// the wiki's part number).
|
|
TMS34010(config, m_maincpu, XTAL(50'000'000));
|
|
m_maincpu->set_addrmap(AS_PROGRAM, &vt1200_state::program_map);
|
|
m_maincpu->set_pixel_clock(5'184'000);
|
|
m_maincpu->set_pixels_per_clock(16);
|
|
m_maincpu->set_scanline_ind16_callback(FUNC(vt1200_state::scanline));
|
|
m_maincpu->set_screen("screen");
|
|
|
|
SCREEN(config, m_screen, SCREEN_TYPE_RASTER);
|
|
// Must be consistent with pixels_per_clock=16, or the renderer (which
|
|
// computes heblnk = HEBLNK * 16) overruns the bitmap and corrupts the heap.
|
|
// The device reconfigures this from the firmware's registers anyway.
|
|
m_screen->set_raw(5'184'000, 1280, 0, 1280, 901, 0, 901);
|
|
m_screen->set_screen_update(FUNC(vt1200_state::screen_update));
|
|
m_screen->set_palette(m_palette);
|
|
|
|
// 256 entries so the TMS34010 renderer can encode multi-bit pixels; the
|
|
// terminal's real output stage is a later question.
|
|
// Explicit monochrome ramp: pen 0 = black, pen 1 = white. render_active_line()
|
|
// writes only pens 0/1, so with no init delegate the on-screen colour was
|
|
// whatever MAME's default palette held (reported: red). Two entries suffice.
|
|
PALETTE(config, m_palette, palette_device::MONOCHROME).set_entries(2);
|
|
|
|
// The two Signetics SCN2681 DUARTs (same family as the Motorola MC68681:
|
|
// 2681 = RD/WR strobes, 68681 = R/W + DTACK + interrupt vector). The
|
|
// VT1200 firmware drives them with the generic 68681 register map, and the
|
|
// level-1 host-port test uses the chip's own local loopback (MR2 bits 7:6 =
|
|
// 10), so the SCN2681 fits and needs no vector-register behaviour.
|
|
SCN2681(config, m_duart1, XTAL(3'686'400));
|
|
SCN2681(config, m_duart2, XTAL(3'686'400));
|
|
|
|
// Connect the DUART interrupt outputs to the CPU. The level-1 host-port
|
|
// test enables the DUART's TxRDYA/RxRDYA interrupts (IMR = 7) and the
|
|
// firmware's ISR is what feeds THRA -- but only if the IRQ reaches the CPU.
|
|
// No external TX->RX loopback is wired: the firmware sets MR2's local
|
|
// loopback itself and the datasheet holds TxD high in local loopback, so
|
|
// the external wire is unnecessary. duart1_irq_w picks INT1 (POST) or INT2
|
|
// (OS) from VT1200_SKIP_LEVEL1; DUART2's trampoline is on INT2.
|
|
m_duart1->irq_cb().set(FUNC(vt1200_state::duart1_irq_w));
|
|
m_duart2->irq_cb().set(FUNC(vt1200_state::duart2_irq_w));
|
|
}
|
|
|
|
INPUT_PORTS_START(vt1200)
|
|
INPUT_PORTS_END
|
|
|
|
ROM_START(vt1200)
|
|
ROM_REGION(0x1c0000, "maincpu", 0)
|
|
ROM_LOAD("vt1200_v22.bin", 0x000000, 0x1c0000,
|
|
CRC(29448f23) SHA1(c2d65bce2c34701b53006f16ccba33e0adbbfd67))
|
|
ROM_END
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
// year, name, parent, compat, machine, input, class, init, company, fullname
|
|
CONS(1990, vt1200, 0, 0, vt1200, vt1200, vt1200_state, empty_init, "DEC",
|
|
"VT1200 X terminal (v2.2 firmware)", MACHINE_NOT_WORKING | MACHINE_NO_SOUND)
|