forked from arti/vt1200
MAME's TMS34010 renderer does not read VRAM: tms340x0_ind16 calls the driver's scanline callback for the active region and paints only the blanking regions. Our callback drew nothing, so the active area was never painted and every frame dump was all-black. Add render_active_line(), called from scanline(): it reads the framebuffer out of the video-RAM share at bit 0x04000000 with a measured 1024-bit (0x40-word, 128-byte) row pitch and LSB-first bit order within each 16-bit word. The pitch comes from the firmware's own character blitter (PIXBLT at pc=3FDF9A00 advances its destination by 0x40 words per glyph row); at the 160-byte pitch MAME derived from HEBLNK/HSBLNK/HTOTAL the text is sheared and unreadable. Validated: frame 160 shows the level-1 CP437 character table legibly, frame 600 the level-1 error screen. VT1200_DPYLOG added as an opt-in per-scanline display-params dump. Regression (no VT1200_* env): PC=3FDCE400 samples=199 -- holds. See harness/mame/RENDER_REPORT.md.
1219 lines
49 KiB
C++
1219 lines
49 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).
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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)
|