Files
vt1200/harness/mame/vt1200.cpp
T
SDD c19c603d77 harness: VT1200_DUART_IP3 -- drive DUART1 IP3 high to take the OS cold-start gate
Add an opt-in knob (VT1200_DUART_IP3=1) that asserts DUART1 input-port bit 3.
The POST gate at bit 0x3F2042D0 tests IP3 (@0x64000D0 bit 3) and, when it is
high and the 0x6200000 status handshake did not return 0x55, branches to the
OS cold-start 0x3F5788C0 (jauc), skipping the whole level-1 batch: the sole
caller of the level-1 runner 0x3FDF72D0 is 0x3F204400, inside the skipped
region.  Verified from the raw opcodes against MAME's tms34010 opcode table
(BTST_K sets Z=NOT bit; jane=JNE absolute-long) and empirically.

Default behaviour is unchanged (no env -> the gate is not taken).  The real
terminal reads IP3 low, so this is a modelled board input, not a faithful POST
path; results from a level-1-skipped harness are not evidence about the real
hardware's POST, but are evidence about the OS/graphics route.

osprobe.lua: gate/OS/Park probe (breakpoints on 0x3F2043B0 / 0x3F204400 /
0x3F5788C0, per-frame PC histogram, optional instruction trace and RAM-global
dump).
2026-10-16 12:55:36 +03:00

1236 lines
50 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:
* 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).
* 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).
* OPTIONALLY (see below) the four undecoded board windows.
What is NOT modelled beyond the opt-in register model documented below:
LANCE Ethernet, the DUARTs, and the board system logic at bit 0x5800000 /
0x5900000 / 0x6200000 / 0x6400000. Because of that the terminal does not
boot to the X server; the firmware parks in early POST waiting on a
hardware-ready poll.
---------------------------------------------------------------------
Board-window experiments (all opt-in; with no env vars set the windows
stay unmapped and behave exactly as before):
VT1200_BOARD_LOG=<path> append a line for every *distinct*
(address, PC) board access, flushed
immediately. This is the evidence
gatherer: it shows every register the
firmware polls and the PC doing the
polling.
VT1200_BOARD_DEFAULT=<value> value returned for addresses without an
override (default 0).
VT1200_BOARD_MAP=<list> per-address overrides, e.g.
"0x06200010=1;0x0640000d0=0"
VT1200_BOARD_RAM=1 map the windows as plain read/write RAM
instead (writes persist and read back).
VT1200_DUART=1 put MAME's SCN2681 on the two DUART windows
(0x0640000 = host/printer, 0x0620000 =
keyboard/mouse), with TX wired back to RX for
the self-test's loopback connector. Gated so
the no-env baseline is unchanged.
VT1200_DUART_SEL=<s> which DUART(s): "1" = 0x0640000, "2" =
0x0620000; default "12".
VT1200_DUART_LB1/LB2=1 external TX->RX loopback on DUART1 / DUART2.
VT1200_DUARTLOG=<path> every DUART access (not de-duplicated).
VT1200_DUART_IP3=1 drive DUART1 IP3 (input-port bit 3) high.
The boot gate at bit 0x3F2042D0 tests IP3
(firmware @0x64000D0 bit 3) and, when set
and the 0x6200000 status handshake did not
return 0x55, branches to the OS cold-start
0x3F5788C0 -- skipping ALL of the level-1
batch (the sole caller of the level-1
runner 0x3FDF72D0 is 0x3F204400, inside the
skipped region). This is a modelled board
input, not a ROM change: the real terminal
reads IP3 low. 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_NO_RENDER=1
blanks instead. Dumps:
VT1200_DUMP_FRAME=<n> write frame <n> as vt1200-frame-<n>.ppm
VT1200_DUMP_DIR=<dir> directory for the dumps (default ".")
VT1200_DPYLOG=<path> dump the TMS34010 display params, one line
per scanline, for the first
VT1200_DPYLOG_FRAMES frames (default 1)
VT1200_DUART also wires each DUART's IRQ output to the TMS34010's external
interrupt inputs (DUART1 -> INT1, DUART2 -> INT2). This is required: the
level-1 host-port test's transmit is entirely ISR-driven (the ISR writes
THRA), and without the wiring THRA is never written at all. See
DUART_REPORT.md section FINAL FINDINGS.
The remaining board knobs refine VT1200_BOARD_MODEL=handshake. They are
modifiers, not on-switches: one of the knobs above must also be set, or
the windows stay unmapped and they do nothing.
VT1200_BOARD_BUSY_TICKS=<n> busy ticks for every window (default 0).
VT1200_BOARD_BUSY_W2=<n> busy ticks for 0x06200000 only; applied
after BUSY_TICKS, so it overrides it.
VT1200_BOARD_BUSY_W3=<n> busy ticks for 0x06400000 only; likewise.
VT1200_BOARD_DMA_TICKS=<n> completion time of a command written to
the 0x05900000 DMA command port; 0 (the
default) leaves the pair as a plain latch.
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 <tuple>
#include <utility>
#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);
void dump_ram();
void dump_vram();
// 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, gated on VT1200_DUART. DUART1 = host (ch A) +
// printer (ch B) at window 0x0640000; DUART2 = keyboard (ch A) + mouse
// (ch B) at window 0x0620000.
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)
// 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.
bool m_render = true;
long m_dump_frame = -1;
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;
// 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;
optional_shared_ptr<u16> m_vram;
// VT1200_CAL=<path>
// Address-unit and map-precedence cross-check. Each probe is registered at
// the END of program_map (so, if precedence is last-wins, it wins its
// slice) and logs the delegate offset `off` it is handed. It also prints
// the space address reconstructed under the two candidate unit rules:
// space4 = base + (off << 4) (delegate off = space-bit-address >> 4)
// space0 = base + off (delegate off = space-bit-address)
// Whichever rule yields a coherent picture for the positive control is the
// real one. See XCROSS_REPORT.md.
FILE *m_cal_log = nullptr;
std::map<std::tuple<u32, u32, u32>, u32> m_cal_seen; // (base, off, pc) -> count
std::vector<u16> m_cal_ram;
void log_probe(const char *tag, char kind, u32 off, u32 base);
// One read/write pair per probe so each knows its own base address. These
// bases are SPACE (bit) addresses, matching the driver's map entries.
u16 calctrl_r(offs_t off); void calctrl_w(offs_t off, u16 data); // 0x00000000 control
u16 calvida_r(offs_t off); void calvida_w(offs_t off, u16 data); // 0x00400000 (old VID-A)
u16 calvid4m_r(offs_t off); void calvid4m_w(offs_t off, u16 data); // 0x04000000 (0x400000<<4)
u16 calram80_r(offs_t off); void calram80_w(offs_t off, u16 data); // 0x00800000
u16 calram100_r(offs_t off); void calram100_w(offs_t off, u16 data); // 0x01000000
u16 calram180_r(offs_t off); void calram180_w(offs_t off, u16 data); // 0x01800000
u16 calram200_r(offs_t off); void calram200_w(offs_t off, u16 data); // 0x02000000 (outside RAM)
// Mismatched-width overlap check (gated on VT1200_CAL_W8, needs VT1200_CAL
// too): an 8-bit handler over the same bottom slice as CTRL. It is
// registered even later, so if the mismatched populate path also follows
// last-wins it fires and CTRL does not.
u8 calw8_r(offs_t off); void calw8_w(offs_t off, u8 data);
// VT1200_VRAMLOG=<path>
// Transparent logging proxy over the start of video memory, to compare what
// the level-1 video RAM test writes against what it reads back.
FILE *m_vramlog = nullptr;
std::vector<u16> m_vramlog_ram;
u16 vramlog_r(offs_t off);
void vramlog_w(offs_t off, u16 data);
// 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_LOWLOG=<path>
// A *transparent* logging proxy over the bottom of main RAM (bit
// 0x00000000-0x00003fff). It reads/writes the real mainram share, so it
// does not perturb the machine; it just records every distinct
// (addr, pc, value, kind) seen. Used to catch the write that loads the
// level-1 error code into the frame slot the dispatcher reads.
FILE *m_lowlog = nullptr;
std::map<std::tuple<u32, u32, u32, u32>, u32> m_lowlog_seen; // (addr,pc,val,kind) -> count
u16 lowlog_r(offs_t off);
void lowlog_w(offs_t off, u16 data);
void log_low(char kind, u32 addr, u16 value);
// VT1200_STRUCTLOG=<path>
// Transparent logging proxy over the DUART self-test descriptor area
// (bit 0x270000-0x270fff), to find who writes the status/count fields the
// level-1 port tests check. Forwards to the real mainram share.
FILE *m_irqlog = nullptr;
FILE *m_structlog = nullptr;
std::map<std::tuple<u32, u32, u32, u32>, u32> m_structlog_seen; // (addr,pc,val,kind) -> count
u16 structlog_r(offs_t off);
void structlog_w(offs_t off, u16 data);
void log_struct(char kind, u32 addr, u16 value);
// 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);
};
void vt1200_state::machine_start()
{
if (const char *p = getenv("VT1200_BOARD_DEFAULT"))
m_board.set_default(u16(strtoul(p, nullptr, 0)));
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; the env knobs configure it.
if (const char *model = getenv("VT1200_BOARD_MODEL"))
m_board.set_model_handshake(!strcmp(model, "handshake"));
if (const char *t = getenv("VT1200_BOARD_BUSY_TICKS"))
m_board.set_busy_ticks((int)strtol(t, nullptr, 0));
// Per-window refinements of the busy count: W2/W3 target the two big
// windows individually. Applied after BUSY_TICKS so they win for their
// window, and the DMA pair's completion time is its own knob.
if (const char *t = getenv("VT1200_BOARD_BUSY_W2"))
m_board.set_window_busy_ticks(VT1200_WIN2, (int)strtol(t, nullptr, 0));
if (const char *t = getenv("VT1200_BOARD_BUSY_W3"))
m_board.set_window_busy_ticks(VT1200_WIN3, (int)strtol(t, nullptr, 0));
if (const char *t = getenv("VT1200_BOARD_DMA_TICKS"))
m_board.set_dma_ticks((int)strtol(t, nullptr, 0));
// The monitor-selection switch lives in bits 4-6 of 0x64000D0. The model
// mixes those input bits with the port's write-back output latches; the
// value may be the raw register bits (e.g. 70) or a position 0-7. Any
// other port or mask is not modelled, so say so rather than ignoring it.
if (const char *sw = getenv("VT1200_BOARD_SWITCH"))
{
unsigned a = 0, m = 0, v = 0;
if (sscanf(sw, "%x:%x:%x", &a, &m, &v) == 3)
{
if (a != VT1200_WIN3 + VT1200_OFF_SWITCH)
osd_printf_info("vt1200: VT1200_BOARD_SWITCH addr %07X is not the monitor switch port %07X; addr ignored\n",
a, VT1200_WIN3 + VT1200_OFF_SWITCH);
if (m != 0x70)
osd_printf_info("vt1200: VT1200_BOARD_SWITCH mask %02X is not the monitor switch mask 70; mask ignored\n", m);
m_board.set_monitor_switch(u8(v));
}
}
// 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 *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_CAL"))
{
m_cal_log = fopen(p, "w");
if (m_cal_log)
fprintf(m_cal_log, "# tag kind addr pc -- address-unit / precedence calibration\n");
}
if (const char *p = getenv("VT1200_VRAMLOG"))
{
m_vramlog = fopen(p, "w");
m_vramlog_ram.assign(0x10000, 0);
if (m_vramlog)
fprintf(m_vramlog, "# kind off value pc -- video-memory accesses\n");
}
if (const char *p = getenv("VT1200_LOWLOG"))
{
m_lowlog = fopen(p, "w");
if (m_lowlog)
fprintf(m_lowlog, "# kind bitaddr pc value -- transparent low-RAM access log\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_STRUCTLOG"))
{
m_structlog = fopen(p, "w");
if (m_structlog)
fprintf(m_structlog, "# kind bitaddr pc value -- DUART descriptor area (bit 0x270000-0x270fff)\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 DUARTs 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 and the value the board model was configured with.
// IP2 stays low (the value the board model produced for the blit-status
// test at bit 0x3F204AA0).
if (getenv("VT1200_DUART"))
{
const char *sel = getenv("VT1200_DUART_SEL");
if (!sel || strchr(sel, '1'))
{
m_duart1->ip4_w(1);
m_duart1->ip5_w(1);
m_duart1->ip6_w(1);
// VT1200_DUART_IP3=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.
if (getenv("VT1200_DUART_IP3"))
m_duart1->ip3_w(1);
}
}
logerror("vt1200: board model=%s busy_ticks=%s busy_w2=%s busy_w3=%s dma_ticks=%s switch=%s log=%s render=%d dump=%ld\n",
getenv("VT1200_BOARD_MODEL") ? getenv("VT1200_BOARD_MODEL") : "off",
getenv("VT1200_BOARD_BUSY_TICKS") ? getenv("VT1200_BOARD_BUSY_TICKS") : "0",
getenv("VT1200_BOARD_BUSY_W2") ? getenv("VT1200_BOARD_BUSY_W2") : "-",
getenv("VT1200_BOARD_BUSY_W3") ? getenv("VT1200_BOARD_BUSY_W3") : "-",
getenv("VT1200_BOARD_DMA_TICKS") ? getenv("VT1200_BOARD_DMA_TICKS") : "0",
getenv("VT1200_BOARD_SWITCH") ? getenv("VT1200_BOARD_SWITCH") : "off",
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
logerror("vt1200: duart=%s\n", getenv("VT1200_DUART") ? "on (SCN2681 @0640000 host/printer, @0620000 kbd/mouse, tx->rx loopback)" : "off");
}
void vt1200_state::log_probe(const char *tag, char kind, u32 off, u32 base)
{
if (!m_cal_log)
return;
const u32 p = (u32)m_maincpu->state_int(TMS34010_PC);
auto key = std::make_tuple(base, off, (p << 8) | (u8)kind);
if (m_cal_seen.find(key) == m_cal_seen.end())
{
m_cal_seen.emplace(key, 1);
// space4: the rule "delegate off = space address >> 4". space0: off is
// already the space address. Only one of them can be right for a run.
fprintf(m_cal_log, "%s %c off=%08X space4=%08X space0=%08X pc=%08X\n",
tag, kind, off, base + (off << 4), base + off, p);
fflush(m_cal_log);
}
}
// The control probe shadows the very bottom of RAM (bit 0..0xffff = byte
// 0..0x1fff). It behaves like RAM via a small backing array so it perturbs as
// little as possible, and it MUST fire if last-wins precedence holds.
u16 vt1200_state::calctrl_r(offs_t off) { log_probe("CTRL", 'R', (u32)off, 0x00000000); return m_cal_ram[(u32)off % m_cal_ram.size()]; }
void vt1200_state::calctrl_w(offs_t off, u16 data) { log_probe("CTRL", 'W', (u32)off, 0x00000000); m_cal_ram[(u32)off % m_cal_ram.size()] = data; }
u16 vt1200_state::calvida_r(offs_t off) { log_probe("VID-A-0x00400000", 'R', (u32)off, 0x00400000); return 0; }
void vt1200_state::calvida_w(offs_t off, u16 data) { log_probe("VID-A-0x00400000", 'W', (u32)off, 0x00400000); }
u16 vt1200_state::calvid4m_r(offs_t off) { log_probe("VID-4M-0x04000000", 'R', (u32)off, 0x04000000); return 0; }
void vt1200_state::calvid4m_w(offs_t off, u16 data) { log_probe("VID-4M-0x04000000", 'W', (u32)off, 0x04000000); }
u16 vt1200_state::calram80_r(offs_t off) { log_probe("RAM-0x00800000", 'R', (u32)off, 0x00800000); return 0; }
void vt1200_state::calram80_w(offs_t off, u16 data) { log_probe("RAM-0x00800000", 'W', (u32)off, 0x00800000); }
u16 vt1200_state::calram100_r(offs_t off) { log_probe("RAM-0x01000000", 'R', (u32)off, 0x01000000); return 0; }
void vt1200_state::calram100_w(offs_t off, u16 data) { log_probe("RAM-0x01000000", 'W', (u32)off, 0x01000000); }
u16 vt1200_state::calram180_r(offs_t off) { log_probe("RAM-0x01800000", 'R', (u32)off, 0x01800000); return 0; }
void vt1200_state::calram180_w(offs_t off, u16 data) { log_probe("RAM-0x01800000", 'W', (u32)off, 0x01800000); }
u16 vt1200_state::calram200_r(offs_t off) { log_probe("RAM-0x02000000", 'R', (u32)off, 0x02000000); return 0; }
void vt1200_state::calram200_w(offs_t off, u16 data) { log_probe("RAM-0x02000000", 'W', (u32)off, 0x02000000); }
u8 vt1200_state::calw8_r(offs_t off) { log_probe("W8-MISMATCH", 'R', (u32)off, 0x00000000); return 0; }
void vt1200_state::calw8_w(offs_t off, u8 data) { log_probe("W8-MISMATCH", 'W', (u32)off, 0x00000000); }
u16 vt1200_state::vramlog_r(offs_t off)
{
const u32 o = (u32)off & 0xffff;
const u16 v = (o < m_vramlog_ram.size()) ? m_vramlog_ram[o] : 0;
if (m_vramlog)
{
fprintf(m_vramlog, "R %04X %04X pc=%08X\n", o, v, (unsigned)m_maincpu->state_int(TMS34010_PC));
fflush(m_vramlog);
}
return v;
}
void vt1200_state::vramlog_w(offs_t off, u16 data)
{
const u32 o = (u32)off & 0xffff;
if (o < m_vramlog_ram.size())
m_vramlog_ram[o] = data;
if (m_vramlog)
{
fprintf(m_vramlog, "W %04X %04X pc=%08X\n", o, data, (unsigned)m_maincpu->state_int(TMS34010_PC));
fflush(m_vramlog);
}
}
void vt1200_state::machine_reset()
{
if (m_log)
fflush(m_log);
if (m_unmapped_log)
fflush(m_unmapped_log);
if (m_lowlog)
fflush(m_lowlog);
if (m_structlog)
fflush(m_structlog);
}
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);
}
// Transparent low-RAM logging proxy. It sits over bit 0x0000-0x3fff (the
// frame/argument block the self-test uses) and forwards reads/writes to the
// real mainram share, so it changes nothing about execution. Base is 0, so
// the delegate offset is (bitaddr >> 4) == the u16 index into mainram.
void vt1200_state::log_low(char kind, u32 addr, u16 value)
{
if (!m_lowlog)
return;
const u32 pc = (u32)m_maincpu->state_int(TMS34010_PC);
auto key = std::make_tuple(addr, pc, (u32)value, (u32)(u8)kind);
if (m_lowlog_seen.find(key) == m_lowlog_seen.end())
{
m_lowlog_seen.emplace(key, 1);
fprintf(m_lowlog, "%c %06X pc=%08X val=%04X\n", kind, addr, pc, value);
fflush(m_lowlog);
}
}
void vt1200_state::log_struct(char kind, u32 addrof, u16 value)
{
if (!m_structlog)
return;
const u32 pc = (u32)m_maincpu->state_int(TMS34010_PC);
auto key = std::make_tuple(addrof, pc, (u32)value, (u32)(u8)kind);
if (m_structlog_seen.find(key) == m_structlog_seen.end())
{
m_structlog_seen.emplace(key, 1);
fprintf(m_structlog, "%c %06X pc=%08X val=%04X\n", kind, addrof, pc, value);
fflush(m_structlog);
}
}
// The entry base is bit 0x270000, so the delegate offset is
// (bitaddr - 0x270000) >> 4 and the mainram word index is bitaddr >> 4.
u16 vt1200_state::structlog_r(offs_t off)
{
const u32 bitaddr = 0x270000 + ((u32)off << 4);
const u16 v = m_mainram[bitaddr >> 4];
log_struct('R', bitaddr, v);
return v;
}
void vt1200_state::structlog_w(offs_t off, u16 data)
{
const u32 bitaddr = 0x270000 + ((u32)off << 4);
m_mainram[bitaddr >> 4] = data;
log_struct('W', bitaddr, data);
}
u16 vt1200_state::lowlog_r(offs_t off)
{
const u16 v = m_mainram[(u32)off & 0x3ff];
log_low('R', (u32)off << 4, v);
return v;
}
void vt1200_state::lowlog_w(offs_t off, u16 data)
{
m_mainram[(u32)off & 0x3ff] = data;
log_low('W', (u32)off << 4, data);
}
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. Discovered empirically: the firmware's blit paths
// (PC 3F204A20 right after FILL XY, and 3FDF97A0 / 3FDF9A00 in the display
// code) read and write 0x00400000..0x0040D7FF -- memory the driver never
// mapped, which is exactly why the screen renders black: the blits went
// nowhere and reads returned 0. Gated on an env var while we characterise
// its size and layout, so the established no-env baseline is preserved.
// Video memory. 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). The earlier 0x00400000 was 16x too low.
//
// This is the memory the level-1 VIDEO RAM test walks (it reports failing at
// 0x40000C in the same >>4 units, i.e. bit 0x040000C0), which is why the
// firmware reports error 22 and restarts before it ever draws anything.
if (getenv("VT1200_VRAM"))
map(VRAM_BASE_BITS, 0x1fffffff).ram().share("vram");
// Transparent logging proxy over the first 64 KiB of video memory, registered
// last so it wins over the .ram() above. off = (bitaddr - base) >> 4, so this
// covers bit 0x04000000..0x040FFFFF, which includes the address the level-1
// test reports failing at (bit 0x040000C0).
if (getenv("VT1200_VRAMLOG"))
map(0x04000000, 0x0400ffff).rw(FUNC(vt1200_state::vramlog_r), FUNC(vt1200_state::vramlog_w));
// 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);
// Calibration cross-check. Every probe is registered LAST, so under
// last-wins it wins its own slice. Bases are SPACE (bit) addresses.
if (getenv("VT1200_CAL"))
{
m_cal_ram.assign(0x10000, 0);
// Positive control: proven to fire. Last-wins -> this shadows the
// bottom 64K bits of RAM.
map(0x00000000, 0x0000ffff).rw(FUNC(vt1200_state::calctrl_r), FUNC(vt1200_state::calctrl_w));
// The original disputed "video" range (a space address inside RAM).
map(0x00400000, 0x004fffff).rw(FUNC(vt1200_state::calvida_r), FUNC(vt1200_state::calvida_w));
// The catch-all's logged 0x400000 read as a space address, times 16:
// 0x400000 << 4 = 0x04000000. Gated separately so a plain VT1200_CAL run
// leaves the region to the catch-all (the strict A-vs-B cross-check).
if (getenv("VT1200_CAL_VID4M"))
map(0x04000000, 0x040fffff).rw(FUNC(vt1200_state::calvid4m_r), FUNC(vt1200_state::calvid4m_w));
// Where does RAM actually end? Probes at three MiB marks of the
// declared 4 MiB range, plus one OUTSIDE it (0x02000000).
map(0x00800000, 0x00800fff).rw(FUNC(vt1200_state::calram80_r), FUNC(vt1200_state::calram80_w));
map(0x01000000, 0x01000fff).rw(FUNC(vt1200_state::calram100_r), FUNC(vt1200_state::calram100_w));
map(0x01800000, 0x01800fff).rw(FUNC(vt1200_state::calram180_r), FUNC(vt1200_state::calram180_w));
map(0x02000000, 0x02000fff).rw(FUNC(vt1200_state::calram200_r), FUNC(vt1200_state::calram200_w));
// Mismatched-width check: same slice as CTRL, but an 8-bit handler.
if (getenv("VT1200_CAL_W8"))
map(0x00000000, 0x0000ffff).rw(FUNC(vt1200_state::calw8_r), FUNC(vt1200_state::calw8_w));
}
// Undecoded board windows. Left unmapped unless an experiment asks for
// them, so the default behaviour is unchanged.
if (getenv("VT1200_BOARD_RAM"))
{
for (int i = 0; i < 4; i++)
map(BOARD_BASE[i], BOARD_BASE[i] + 0xffff).ram();
}
else if (getenv("VT1200_BOARD_LOG") || getenv("VT1200_BOARD_MAP")
|| getenv("VT1200_BOARD_DEFAULT") || getenv("VT1200_BOARD_MODEL"))
{
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));
}
// The two SCN2681 DUARTs, gated on VT1200_DUART so the no-env baseline is
// untouched. Registered AFTER the board-window model so they win 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.
// VT1200_DUART_SEL picks which ("1" = 0x0640000 host/printer, "2" =
// 0x0620000 keyboard/mouse; default both).
if (getenv("VT1200_DUART"))
{
const char *sel = getenv("VT1200_DUART_SEL");
std::string s(sel ? sel : "12");
if (s.find('1') != std::string::npos)
map(VT1200_WIN3, VT1200_WIN3 + 0xff).rw(FUNC(vt1200_state::duart1_r), FUNC(vt1200_state::duart1_w));
if (s.find('2') != std::string::npos)
map(VT1200_WIN2, VT1200_WIN2 + 0xff).rw(FUNC(vt1200_state::duart2_r), FUNC(vt1200_state::duart2_w));
}
// Transparent low-RAM logging proxy, registered LAST so it wins its slice
// of RAM. It forwards to the real mainram share and only records.
if (getenv("VT1200_LOWLOG"))
map(0x00000000, 0x00003fff).rw(FUNC(vt1200_state::lowlog_r), FUNC(vt1200_state::lowlog_w));
// Transparent logging proxy over the DUART self-test descriptor area.
if (getenv("VT1200_STRUCTLOG"))
map(0x270000, 0x270fff).rw(FUNC(vt1200_state::structlog_r), FUNC(vt1200_state::structlog_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(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();
if (m_dump_frame >= 0 && (long)screen.frame_number() == m_dump_frame)
dump_frame(screen, bitmap);
if (m_dump_ram >= 0 && !m_dumped_ram && (long)screen.frame_number() >= m_dump_ram)
{
m_dumped_ram = true;
dump_ram();
}
if (m_dump_vram >= 0 && !m_dumped_vram && (long)screen.frame_number() >= m_dump_vram)
{
m_dumped_vram = true;
dump_vram();
}
}
}
void vt1200_state::dump_frame(screen_device &screen, bitmap_ind16 &bitmap)
{
char path[1024];
snprintf(path, sizeof(path), "%s/vt1200-frame-%ld.ppm", m_dump_dir.c_str(), m_dump_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",
m_dump_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);
if (m_dump_frame >= 0 && (long)screen.frame_number() == m_dump_frame)
dump_frame(screen, bitmap);
if (m_dump_ram >= 0 && !m_dumped_ram && (long)screen.frame_number() >= m_dump_ram)
{
m_dumped_ram = true;
dump_ram();
}
}
return changed;
}
void vt1200_state::dump_ram()
{
char path[1024];
snprintf(path, sizeof(path), "%s/vt1200-ram-%ld.bin", m_dump_dir.c_str(), m_dump_ram);
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, m_dump_ram, path);
}
void vt1200_state::dump_vram()
{
char path[1024];
snprintf(path, sizeof(path), "%s/vram-%ld.bin", m_dump_dir.c_str(), m_dump_vram);
FILE *fp = fopen(path, "wb");
if (!fp)
{
osd_printf_info("vt1200: cannot write %s\n", path);
return;
}
if (!m_vram)
{
osd_printf_info("vt1200: VT1200_VRAM 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, m_dump_vram, 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.
PALETTE(config, m_palette).set_entries(256);
// 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));
// External loopback (TX wired back to RX) models the loopback connectors
// the service guide's port tests use. Selectable per DUART so the
// host/printer connector can be fitted without feeding the keyboard/mouse
// DUART looped-back bytes that the firmware would read as key presses.
if (getenv("VT1200_DUART_LB1"))
{
m_duart1->a_tx_cb().set(m_duart1, FUNC(scn2681_device::rx_a_w));
m_duart1->b_tx_cb().set(m_duart1, FUNC(scn2681_device::rx_b_w));
}
if (getenv("VT1200_DUART_LB2"))
{
m_duart2->a_tx_cb().set(m_duart2, FUNC(scn2681_device::rx_a_w));
m_duart2->b_tx_cb().set(m_duart2, FUNC(scn2681_device::rx_b_w));
}
// 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.
// Gated on VT1200_DUART so the no-env baseline is untouched. DUART1's
// firmware trampoline sits on INT1; DUART2's on INT2.
if (getenv("VT1200_DUART"))
{
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)