// 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 three undecoded board windows. What is NOT modelled: LANCE Ethernet, the DUARTs, and the board system logic at bit 0x5800000 / 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= 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 returned for addresses without an override (default 0). VT1200_BOARD_MAP= 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). 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= busy ticks for every window (default 0). VT1200_BOARD_BUSY_W2= busy ticks for 0x06200000 only; applied after BUSY_TICKS, so it overrides it. VT1200_BOARD_BUSY_W3= busy ticks for 0x06400000 only; likewise. VT1200_BOARD_DMA_TICKS= 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 "screen.h" #include #include #include #include #include #include #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 }; 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") { } 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 dump_frame(screen_device &screen, bitmap_ind16 &bitmap); void dump_ram(); // 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); 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 m_maincpu; required_device m_screen; required_device m_palette; required_shared_ptr m_mainram; // 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, 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= writes that frame as a PPM. bool m_render = true; long m_dump_frame = -1; std::string m_dump_dir = "."; // VT1200_DUMP_RAM= writes main RAM at that frame to // vt1200-ram-.bin so the framebuffer can be located offline. long m_dump_ram = -1; bool m_dumped_ram = false; }; 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 *d = getenv("VT1200_DUMP_DIR")) m_dump_dir = d; 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); } void vt1200_state::machine_reset() { if (m_log) fflush(m_log); } void vt1200_state::program_map(address_map &map) { // Main RAM: 4 MiB (1 MiB onboard + 3 x 1 MiB SIMMs on the real machine). // The TMS34010 is bit-addressed, so 4 MiB of bytes == 0x2000000 address // units. (The earlier 0x200000 range modelled only 256 KiB, which is // almost certainly why POST mis-detected memory.) map(0x00000000, 0x01ffffff).ram().share("mainram"); // ROM: physical byte base 0x07e40000 -> bit base 0x3f200000, 0x1c0000 bytes // (0xe00000 bit addresses), plus the three upper logical aliases that the // TMS34010 generates because its top two address bits are not emitted. map(0x3f200000, 0x3fffffff).rom().region("maincpu", 0); map(0x7f200000, 0x7fffffff).rom().region("maincpu", 0); map(0xbf200000, 0xbfffffff).rom().region("maincpu", 0); map(0xff200000, 0xffffffff).rom().region("maincpu", 0); // Undecoded board windows. 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)); } } // 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); } 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_render) 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(); } } } 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()); } // Let the TMS34010 display controller do the rendering. 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(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::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); } 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)