// license:BSD-3-Clause // copyright-holders:F. Ulivi /********************************************************************* hp_taco.cpp HP TApe COntroller (5006-3012) *********************************************************************/ // This device has been reverse engineered entirely through documents & study of HP software. // I had no access to the real device to experiment. // Available documentation on the internal working of TACO chip is close to nothing. The best // I could find is [1] (see below) where all that's described is a (too) brief summary of registers and little else. // In other words, no description of the commands that can be issued to TACO chips. // So, my main source of information was the careful study of HP software, especially the 9845 system test ROM (09845-66520). // The second half of this ROM holds a comprehensive set of tape drive tests. // Another source was the "SIF" utility tools which use a lot of peculiar command sequences. // The main shortcomings of my approach are: // * I could identify only those TACO commands that are actually used by the software. I managed // to identify 17 out of 32 possible commands. The purpose of the rest of commands is anyone's guess. // * I could only guess the behavior of TACO chips in corner cases (especially behavior in various error/abnormal // conditions) // // Documentation I used: // [1] HP, manual 64940-90905, may 80 rev. - Model 64940A tape control & drive service manual // [2] US patent 4,075,679 describing HP9825 system (this system had a discrete implementation of tape controller). The // firmware listing was quite useful in identifying sequences of commands (for example how to find a specific sector etc.). // [3] http://www.hp9845.net site // [4] April 1978 issue of HP Journal. There is a one-page summary of TACO chip on page 20. // [5] HP, manual 09845-10201, apr 81 rev. - General Utility Routines. This manual describes the SIF format and related // utility tools. // This is an overview of the TACO/CPU interface. // // Reg. | R/W | Content // ===================== // R4 | R/W | Data register: words read/written to/from tape pass through this register // R5 | R/W | Command and status register (see below) // R6 | R/W | Tachometer register. Writing it sets a pulse counter that counts up on either tachometer pulses or IRGs, depending // | | on command. When the counter rolls over from 0xffff to 0 it typically ends the command. // | | Current counter value is returned when reading from this register. // R7 | R | Checksum register. Reading it clears it next time the checksum is updated. // R7 | W | Timing register. It controls somehow the encoding and decoding of bits. For now I completely ignore it because its // | | content it's totally unknown to me. It seems safe to do so, anyway. I can see that it's always set to 0x661d before // | | writing to tape and to 0x0635 before reading (0x061d is set by SIF utilities). // // Format of TACO command/status register (R5) // Bit R/W Content // =============== // 15 RW Tape direction (1 = forward) // 14..10 RW Command (see the "enum" below) // 9 RW Automatic stopping of tape at command completion (when 1) // 8 RW Minimum size of gaps (1 = 1.5", 0 = 0.066") // 7 RW Speed of tape (1 = 90 ips, 0 = 22 ips) // 6 RW Option bit for various commands. Most of them use it to select read threshold (0 = low, 1 = high). // 5 R Current track (1 = B) // 4 R Gap detected (1) // 3 R Write protection (1) // 2 R Servo failure (1) // 1 R Cartridge out (1) // 0 R Hole detected (1) // Here's a summary of the on-tape format of HP9845 systems. // * A tape has two independent tracks (A & B). // * Each track holds 426 sectors. // * Each sector has an header and 256 bytes of payload (see below) // * Sectors are separated by gaps of uniform magnetization called IRGs (Inter-Record Gaps) // * The basic unit of data I/O are 16-bit words // * Bits are encoded by different distances between magnetic flux reversals // * The structure of tracks is: // - Begin of tape holes // - The deadzone: 350x 0xffff words // - 1" of IRG // - Sector #0 (track A) or #426 (track B) // - 1" of IRG (2.5" on track A) // - Sector #1 (track A) or #427 (track B) // - 1" of IRG // - Sector #2 (track A) or #428 (track B) // - ...and so on up to sector #425/#851 // - 6" of final gap // - Non-recorded tape // - End of tape holes // * Even though the tape format is not SIF (HP's own Standard Interchange Format), it is // clearly based on SIF itself. The whole tape content is stored according to SIF, // specifically it is "encapsulated" inside file #1. // * Sector #0 is not used. It serves as SIF "file identifier record" (i.e. it identifies // that the rest of tape is inside file #1 from SIF point of view). // * Sectors #1 and #2 hold the first copy of tape directory // * Sectors #3 and #4 hold the second/backup copy of tape directory // * User data are stored starting from sector #5 // * There is no "fragmentation" map (like file allocation table in FAT filesystem): a file // spanning more than 1 sector always occupy a single block of contiguous sectors. // // A sector is structured like this (see description of SIF in [5], pg 655 and following): // Word 0: Invisible preamble word (always 0). Preamble comes from 9825, don't know if it's // actually there in TACO encoding. I assumed it is. // Word 1: File word: file identifier bit, empty record indicator and file number // Word 2: Record word: sector number and "free field pattern" // Word 3: Length word: bytes available and used in sector // Word 4: Checksum (sum of words 1..3) // Words 5..132: Payload // Word 133: Checksum (sum of words 5..132) // // Physical encoding of words is borrowed from 9825 as I wasn't able // to gather any info on the actual encoding of TACO chips. // This is how 9825 encodes words on tape: // - the unit of encoding are 16-bit words // - each word is encoded from MSB to LSB // - each word has an extra invisible "1" encoded at the end (a kind of "stop" bit) // - tape is read/written at slow speed only (21.98 ips) // - a 0 is encoded with a distance between flux reversals of 1/35200 s // (giving a maximum density of about 1600 reversals per inch) // - a 1 is encoded with a distance that's 1.75 times that of a 0 // // This driver is based on the following model of the actual TACO/tape system: // * Tape immediately reaches working speed (no spin-up time) // * Change of speed is immediate as well // * Time & distance to stop and invert direction of tape are modeled, though. Firmware is upset by // a tape with null braking/inversion time/distance. // * Speed of tape is exceptionally accurate. Real tape was controlled by a closed loop // with something like 1% accuracy on speed. // * Storage is modeled by one "map" data structure per track. Each map maps the tape position // to the 16-bit word stored at that position. Gaps are modeled by lack of data in the map. // There is no model of the physical encoding of bits (except to compute how long each word // is on tape). // * Read threshold is ignored. Real tapes could be read with either a low or high threshold. // * "Flag" bit is used as a busy/ready signal in real TACO. Here I assumed the device is // always ready, so Flag is always active. // * I tried to fill the (many) gaps on chip behavior with "sensible" solutions. I could only // validate my solutions by running the original firmware in MAME, though (no real hw at hand). // // TODOs/issues: // * Commands 00 & 10 seem to do the same things. My bet is that they differ in some subtle way that // is not stimulated by all the tape software I used for R.E. Maybe it's just the length of the // no-data timeout they have. // * Find more info on TACO chips (does anyone with a working 9845 or access to internal HP docs want to // help me here, please?) // #include "emu.h" #include "hp_taco.h" #include "ui/uimain.h" // Debugging #define VERBOSE 1 #define LOG(x) do { if (VERBOSE) logerror x; } while (0) #define VERBOSE_0 0 #define LOG_0(x) do { if (VERBOSE_0) logerror x; } while (0) // Macros to clear/set single bits #define BIT_MASK(n) (1U << (n)) #define BIT_CLR(w , n) ((w) &= ~BIT_MASK(n)) #define BIT_SET(w , n) ((w) |= BIT_MASK(n)) // Timers enum { TAPE_TMR_ID, HOLE_TMR_ID, TIMEOUT_TMR_ID }; // Constants #define CMD_REG_MASK 0xffc0 // Command register mask #define STATUS_REG_MASK 0x003f // Status register mask #define TACH_TICKS_PER_INCH 968 // Tachometer pulses per inch of tape movement #define TAPE_POS_FRACT 1024 // 10 bits of fractional part in tape_pos_t #define TACH_FREQ_SLOW 21276 // Tachometer pulse frequency for slow speed (21.98 ips) #define TACH_FREQ_FAST 87196 // Tachometer pulse frequency for fast speed (90.08 ips) #define TACH_FREQ_BRAKE_SLOW 11606 // Tachometer pulse frequency when stopping from slow speed (11.99 ips) #define TACH_FREQ_BRAKE_FAST 44566 // Tachometer pulse frequency when stopping from fast speed (46.04 ips) #define TAPE_INIT_POS (80 * hti_format_t::ONE_INCH_POS) // Initial tape position: 80" from beginning (just past the punched part) #define QUICK_CMD_USEC 25 // usec for "quick" command execution (totally made up) #define FAST_BRAKE_DIST 3350450 // Braking distance at fast speed (~3.38 in) // There are 2 braking distances here: The first one (commented out) is the theoretical value, the second one // is the value that works. Ideally we would always be using the first value but there is a kind of race // condition when loading the memory test from the exerciser that freezes the system. So, the second (shorter) // value is used to avoid this condition. //#define SLOW_BRAKE_DIST 197883 // Braking distance at slow speed (~0.2 in) #define SLOW_BRAKE_DIST 71000 // Braking distance at slow speed (~0.07 in) #define PREAMBLE_WORD 0 // Value of preamble word #define END_GAP_LENGTH (6 * hti_format_t::ONE_INCH_POS) // Length of final gap: 6" // Minimum gap lengths are probably counted from tacho pulses in real TACO: short gaps could be equal to 64 pulses and long ones // to 1472 (23 * 64) #define SHORT_GAP_LENGTH ((hti_format_t::tape_pos_t)(0.066 * hti_format_t::ONE_INCH_POS)) // Minimum length of short gaps: 0.066" ([1], pg 8-10) #define LONG_GAP_LENGTH ((hti_format_t::tape_pos_t)(1.5 * hti_format_t::ONE_INCH_POS)) // Minimum length of long gaps: 1.5" ([1], pg 8-10) #define PREAMBLE_TIMEOUT ((hti_format_t::tape_pos_t)(2.6 * hti_format_t::ONE_INCH_POS)) // Min. length of gap making preamble search time out (totally made up) #define DATA_TIMEOUT ((hti_format_t::tape_pos_t)(0.066 * hti_format_t::ONE_INCH_POS)) // Min. length of gap that will cause data reading to time out (totally made up) // Parts of command register #define CMD_CODE(reg) \ (((reg) >> 10) & 0x1f) #define DIR_FWD(reg) \ (BIT(reg , 15)) #define AUTO_STOP(reg) \ (BIT(reg , 9)) #define LONG_GAP(reg) \ (BIT(reg , 8)) #define SPEED_FAST(reg) \ (BIT(reg , 7)) #define CMD_OPT(reg) \ (BIT(reg , 6)) #define DIR_FWD_MASK BIT_MASK(15) // Direction = forward #define SPEED_FAST_MASK BIT_MASK(7) // Speed = fast // Commands enum { CMD_INDTA_INGAP, // 00: scan for data first then for gap (see also cmd 10) CMD_UNK_01, // 01: unknown CMD_FINAL_GAP, // 02: write final gap CMD_INIT_WRITE, // 03: write words for tape formatting CMD_STOP, // 04: stop CMD_UNK_05, // 05: unknown CMD_SET_TRACK, // 06: set A/B track CMD_UNK_07, // 07: unknown CMD_UNK_08, // 08: unknown CMD_UNK_09, // 09: unknown CMD_MOVE, // 0a: move tape CMD_UNK_0b, // 0b: unknown CMD_INGAP_MOVE, // 0c: scan for gap then move a bit further (used to gain some margin when inverting tape movement) CMD_UNK_0d, // 0d: unknown CMD_CLEAR, // 0e: clear errors/unlatch status bits CMD_UNK_0f, // 0f: unknown CMD_NOT_INDTA, // 10: scan for end of data (at the moment it's the same as cmd 00) CMD_UNK_11, // 11: unknown CMD_UNK_12, // 12: unknown CMD_UNK_13, // 13: unknown CMD_UNK_14, // 14: unknown CMD_UNK_15, // 15: unknown CMD_WRITE_IRG, // 16: write inter-record gap CMD_UNK_17, // 17: unknown CMD_SCAN_RECORDS, // 18: scan records (count IRGs) CMD_RECORD_WRITE, // 19: write record words CMD_MOVE_INDTA, // 1a: move then scan for data CMD_UNK_1b, // 1b: unknown (for now it seems harmless to handle it as NOP) CMD_MOVE_INGAP, // 1c: move tape a given distance then scan for gap (as cmd 0c but in reverse order) CMD_START_READ, // 1d: start record reading CMD_DELTA_MOVE_IRG, // 1e: move tape a given distance, detect gaps in parallel CMD_END_READ // 1f: stop reading }; // Bits of status register #define STATUS_HOLE_BIT 0 // Hole detected #define STATUS_CART_OUT_BIT 1 // Cartridge out #define STATUS_SFAIL_BIT 2 // Servo failure #define STATUS_WPR_BIT 3 // Write protection #define STATUS_GAP_BIT 4 // Gap detected #define STATUS_TRACKB_BIT 5 // Track B selected #define STATUS_CART_OUT_MASK BIT_MASK(STATUS_CART_OUT_BIT) // Cartridge out #define STATUS_WPR_MASK BIT_MASK(STATUS_WPR_BIT) // Write protection #define STATUS_ERR_MASK (STATUS_CART_OUT_MASK) // Mask of errors in status reg. // Device type definition DEFINE_DEVICE_TYPE(HP_TACO, hp_taco_device, "hp_taco", "HP TACO") // Constructors hp_taco_device::hp_taco_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, type, tag, owner, clock), device_image_interface(mconfig , *this), m_irq_handler(*this), m_flg_handler(*this), m_sts_handler(*this), m_image(), m_image_dirty(false) { clear_state(); } hp_taco_device::hp_taco_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : hp_taco_device(mconfig, HP_TACO, tag, owner, clock) { } WRITE16_MEMBER(hp_taco_device::reg_w) { LOG_0(("wr R%u = %04x\n", 4 + offset , data)); // Any I/O activity clears IRQ irq_w(false); switch (offset) { case 0: // Data register m_data_reg = data; m_data_reg_full = true; break; case 1: // Command register start_cmd_exec(data & CMD_REG_MASK); break; case 2: // Tachometer register m_tach_reg = data; freeze_tach_reg(true); break; case 3: // Timing register m_timing_reg = data; break; } } READ16_MEMBER(hp_taco_device::reg_r) { uint16_t res = 0; // Any I/O activity clears IRQ irq_w(false); switch (offset) { case 0: // Data register res = m_data_reg; break; case 1: // Command & status register res = (m_cmd_reg & CMD_REG_MASK) | (m_status_reg & STATUS_REG_MASK); break; case 2: // Tachometer register update_tach_reg(); res = m_tach_reg; break; case 3: // Checksum register: it clears when read res = m_checksum_reg; m_clear_checksum_reg = true; break; } LOG_0(("rd R%u = %04x\n", 4 + offset , res)); return res; } READ_LINE_MEMBER(hp_taco_device::flg_r) { return m_flg; } READ_LINE_MEMBER(hp_taco_device::sts_r) { return m_sts; } // device_start void hp_taco_device::device_start() { LOG(("device_start")); m_irq_handler.resolve_safe(); m_flg_handler.resolve_safe(); m_sts_handler.resolve_safe(); save_item(NAME(m_data_reg)); save_item(NAME(m_data_reg_full)); save_item(NAME(m_cmd_reg)); //save_item(NAME(m_cmd_state)); save_item(NAME(m_status_reg)); save_item(NAME(m_tach_reg)); save_item(NAME(m_tach_reg_ref)); save_item(NAME(m_tach_reg_frozen)); save_item(NAME(m_checksum_reg)); save_item(NAME(m_clear_checksum_reg)); save_item(NAME(m_timing_reg)); save_item(NAME(m_irq)); save_item(NAME(m_flg)); save_item(NAME(m_sts)); save_item(NAME(m_tape_pos)); save_item(NAME(m_start_time)); save_item(NAME(m_tape_fwd)); save_item(NAME(m_tape_fast)); save_item(NAME(m_image_dirty)); save_item(NAME(m_tape_wr)); save_item(NAME(m_rw_pos)); save_item(NAME(m_next_word)); save_item(NAME(m_rd_it_valid)); save_item(NAME(m_gap_detect_start)); m_tape_timer = timer_alloc(TAPE_TMR_ID); m_hole_timer = timer_alloc(HOLE_TMR_ID); m_timeout_timer = timer_alloc(TIMEOUT_TMR_ID); } // device_stop void hp_taco_device::device_stop() { } // device_reset void hp_taco_device::device_reset() { LOG(("device_reset")); clear_state(); m_irq = false; m_flg = true; m_irq_handler(false); m_flg_handler(true); set_error(false); m_tape_timer->reset(); m_hole_timer->reset(); m_timeout_timer->reset(); } void hp_taco_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) { update_tape_pos(); switch (id) { case TAPE_TMR_ID: LOG_0(("Tape tmr @%g cmd %02x st %d\n" , machine().time().as_double() , CMD_CODE(m_cmd_reg) , m_cmd_state)); cmd_fsm(); break; case HOLE_TMR_ID: LOG_0(("Hole tmr @%g cmd %02x st %d\n" , machine().time().as_double() , CMD_CODE(m_cmd_reg) , m_cmd_state)); BIT_SET(m_status_reg , STATUS_HOLE_BIT); if (m_cmd_state >= CMD_PH0 && m_cmd_state <= CMD_END) { switch (CMD_CODE(m_cmd_reg)) { case CMD_FINAL_GAP: case CMD_WRITE_IRG: m_image.write_gap(current_track() , m_rw_pos , m_tape_pos); m_rw_pos = m_tape_pos; break; case CMD_INIT_WRITE: case CMD_RECORD_WRITE: case CMD_MOVE_INGAP: m_hole_timer->adjust(time_to_next_hole()); // No IRQ at holes return; case CMD_DELTA_MOVE_IRG: // Hit hole before end of programmed pulses terminate_cmd_now(); update_tach_reg(); freeze_tach_reg(true); return; case CMD_INDTA_INGAP: case CMD_INGAP_MOVE: case CMD_NOT_INDTA: case CMD_SCAN_RECORDS: // Commands are terminated at first hole (and failure is reported) terminate_cmd_now(); set_error(true); return; case CMD_START_READ: case CMD_END_READ: // Commands report failure at first hole set_error(true); break; default: // Other cmds: default processing (update tape pos, set IRQ, schedule timer for next hole) break; } irq_w(true); } m_hole_timer->adjust(time_to_next_hole()); break; case TIMEOUT_TMR_ID: LOG_0(("T/O tmr @%g cmd %02x st %d\n" , machine().time().as_double() , CMD_CODE(m_cmd_reg) , m_cmd_state)); switch (CMD_CODE(m_cmd_reg)) { case CMD_START_READ: if (m_cmd_state == CMD_PH1) { irq_w(true); } break; default: // Most commands are terminated with failure on data T/O terminate_cmd_now(); break; } set_error(true); break; default: break; } } void hp_taco_device::clear_state(void) { m_data_reg = 0; m_data_reg_full = false; m_cmd_reg = 0; m_status_reg = 0; m_tach_reg = 0; m_tach_reg_ref = m_tape_pos; m_tach_reg_frozen = true; m_checksum_reg = 0; m_clear_checksum_reg = false; m_timing_reg = 0; m_cmd_state = CMD_IDLE; m_tape_pos = TAPE_INIT_POS; m_start_time = attotime::never; m_tape_fwd = false; m_tape_fast = false; // m_image_dirty is not touched m_tape_wr = false; m_rw_pos = 0; m_next_word = 0; m_rd_it_valid = false; m_gap_detect_start = hti_format_t::NULL_TAPE_POS; set_tape_present(false); set_tape_present(is_loaded()); } void hp_taco_device::irq_w(bool state) { if (state != m_irq) { m_irq = state; m_irq_handler(state); LOG_0(("IRQ = %d\n" , state)); } } void hp_taco_device::set_error(bool state) { m_sts = !state; m_sts_handler(m_sts); LOG_0(("error = %d\n" , state)); } bool hp_taco_device::is_braking(void) const { return m_cmd_state == CMD_INVERTING || m_cmd_state == CMD_STOPPING; } unsigned hp_taco_device::speed_to_tick_freq(void) const { return is_braking() ? (m_tape_fast ? TACH_FREQ_BRAKE_FAST * TAPE_POS_FRACT : TACH_FREQ_BRAKE_SLOW * TAPE_POS_FRACT) : (m_tape_fast ? TACH_FREQ_FAST * TAPE_POS_FRACT : TACH_FREQ_SLOW * TAPE_POS_FRACT); } hti_format_t::tape_pos_t hp_taco_device::current_tape_pos(void) const { if (m_start_time.is_never()) { // Tape not moving return m_tape_pos; } attotime delta_time(machine().time() - m_start_time); LOG_0(("delta_time = %g\n" , delta_time.as_double())); // How many tachometer ticks has the tape moved? hti_format_t::tape_pos_t delta_tach = (hti_format_t::tape_pos_t)(delta_time.as_ticks(speed_to_tick_freq())); LOG_0(("delta_tach = %u\n" , delta_tach)); auto tape_pos = m_tape_pos; if (!hti_format_t::pos_offset(tape_pos , m_tape_fwd , delta_tach)) { LOG(("Tape unspooled!\n")); } return tape_pos; } void hp_taco_device::update_tape_pos(void) { if (m_start_time.is_never()) { // Tape not moving return; } m_tape_pos = current_tape_pos(); m_start_time = machine().time(); LOG_0(("Tape pos = %u\n" , m_tape_pos)); // Gap detection bool gap_detected = false; if (m_gap_detect_start != hti_format_t::NULL_TAPE_POS && abs(m_gap_detect_start - m_tape_pos) >= min_gap_size()) { auto tmp = m_tape_pos; hti_format_t::pos_offset(tmp , m_tape_fwd , -min_gap_size()); gap_detected = m_image.just_gap(current_track() , tmp , m_tape_pos); } if (gap_detected) { BIT_SET(m_status_reg, STATUS_GAP_BIT); } else { BIT_CLR(m_status_reg, STATUS_GAP_BIT); } // Tach register update update_tach_reg(); } void hp_taco_device::update_tach_reg(void) { if (m_tach_reg_frozen) { LOG_0(("Tach reg frozen\n")); return; } auto pos = current_tape_pos(); hti_format_t::tape_pos_t pos_int = pos / TAPE_POS_FRACT; hti_format_t::tape_pos_t ref_int = m_tach_reg_ref / TAPE_POS_FRACT; uint16_t reg_value = (uint16_t)(abs(pos_int - ref_int) + m_tach_reg); LOG_0(("Tach = %04x @ pos = %d, ref_pos = %d\n" , reg_value , pos , m_tach_reg_ref)); m_tach_reg = reg_value; m_tach_reg_ref = pos; } void hp_taco_device::freeze_tach_reg(bool freeze) { if (freeze) { m_tach_reg_frozen = true; } else { m_tach_reg_frozen = false; m_tach_reg_ref = current_tape_pos(); } } attotime hp_taco_device::time_to_distance(hti_format_t::tape_pos_t distance) const { // +1 for rounding return attotime::from_ticks(distance + 1 , speed_to_tick_freq()); } attotime hp_taco_device::time_to_target(hti_format_t::tape_pos_t target) const { return time_to_distance(abs(target - m_tape_pos)); } attotime hp_taco_device::time_to_stopping_pos(void) const { return time_to_distance(m_tape_fast ? FAST_BRAKE_DIST : SLOW_BRAKE_DIST); } bool hp_taco_device::start_tape_cmd(uint16_t cmd_reg , uint16_t must_be_1 , uint16_t must_be_0) { m_cmd_reg = cmd_reg; uint16_t to_be_tested = (m_cmd_reg & CMD_REG_MASK) | (m_status_reg & STATUS_REG_MASK); // Bits in STATUS_ERR_MASK must always be 0 must_be_0 |= STATUS_ERR_MASK; // It's not an error if the error state is already set (sts false) if (((to_be_tested & (must_be_1 | must_be_0)) ^ must_be_1) != 0) { return false; } else { bool prev_tape_wr = m_tape_wr; bool prev_tape_fwd = m_tape_fwd; bool prev_tape_fast = m_tape_fast; bool prev_tape_braking = is_braking(); bool not_moving = m_start_time.is_never(); m_start_time = machine().time(); m_tape_wr = (must_be_0 & STATUS_WPR_MASK) != 0; m_tape_fwd = DIR_FWD(m_cmd_reg); m_tape_fast = SPEED_FAST(m_cmd_reg); if (m_tape_wr) { // Write command: disable gap detector m_gap_detect_start = hti_format_t::NULL_TAPE_POS; BIT_CLR(m_status_reg, STATUS_GAP_BIT); m_image_dirty = true; } else if (not_moving || prev_tape_braking || prev_tape_wr != m_tape_wr || prev_tape_fwd != m_tape_fwd || prev_tape_fast != m_tape_fast) { // Tape started or re-started right now, switched from writing to reading, direction changed or speed changed: (re)start gap detector m_gap_detect_start = m_tape_pos; BIT_CLR(m_status_reg, STATUS_GAP_BIT); } if (!not_moving && prev_tape_fwd != m_tape_fwd) { // Tape direction inverted, stop tape before executing command m_tape_fwd = prev_tape_fwd; m_tape_fast = prev_tape_fast; m_cmd_state = CMD_INVERTING; LOG_0(("Direction reversed! fwd = %d fast = %d\n" , m_tape_fwd , m_tape_fast)); if (!prev_tape_braking) { m_tape_timer->adjust(time_to_stopping_pos()); } } else { // No change in direction, immediate execution m_cmd_state = CMD_PH0; m_tape_timer->adjust(attotime::zero); } m_hole_timer->reset(); m_timeout_timer->reset(); return true; } } void hp_taco_device::stop_tape(void) { m_start_time = attotime::never; m_gap_detect_start = hti_format_t::NULL_TAPE_POS; } unsigned hp_taco_device::current_track(void) { return BIT(m_status_reg , STATUS_TRACKB_BIT); } attotime hp_taco_device::fetch_next_wr_word(void) { if (m_data_reg_full) { m_next_word = m_data_reg; m_data_reg_full = false; LOG_0(("next %04x (DR)\n" , m_next_word)); } else { // When data register is empty, write checksum word m_next_word = m_checksum_reg; LOG_0(("next %04x (CS)\n" , m_next_word)); } if (m_clear_checksum_reg) { m_checksum_reg = 0; m_clear_checksum_reg = false; } // Update checksum with new word m_checksum_reg += m_next_word; return time_to_distance(hti_format_t::word_length(m_next_word)); } attotime hp_taco_device::time_to_rd_next_word(hti_format_t::tape_pos_t& word_rd_pos) { if (m_rd_it_valid) { word_rd_pos = hti_format_t::farthest_end(m_rd_it , m_tape_fwd); return time_to_target(word_rd_pos); } else { return attotime::never; } } hti_format_t::tape_pos_t hp_taco_device::min_gap_size(void) const { return LONG_GAP(m_cmd_reg) ? LONG_GAP_LENGTH : SHORT_GAP_LENGTH; } void hp_taco_device::set_tape_present(bool present) { if (present) { if (is_readonly()) { BIT_SET(m_status_reg, STATUS_WPR_BIT); } else { BIT_CLR(m_status_reg, STATUS_WPR_BIT); } // STATUS_CART_OUT_BIT is reset by CMD_CLEAR } else { BIT_SET(m_status_reg, STATUS_CART_OUT_BIT); BIT_SET(m_status_reg, STATUS_WPR_BIT); } } attotime hp_taco_device::time_to_next_hole(void) const { auto pos = hti_format_t::next_hole(m_tape_pos , m_tape_fwd); if (pos == hti_format_t::NULL_TAPE_POS) { return attotime::never; } else { return time_to_target(pos); } } attotime hp_taco_device::time_to_tach_pulses(void) const { return time_to_distance((hti_format_t::tape_pos_t)(0x10000U - m_tach_reg) * TAPE_POS_FRACT); } void hp_taco_device::terminate_cmd_now(void) { m_cmd_state = CMD_END; m_tape_timer->adjust(attotime::zero); m_hole_timer->reset(); m_timeout_timer->reset(); } void hp_taco_device::set_data_timeout(bool long_timeout) { attotime timeout = time_to_distance(long_timeout ? PREAMBLE_TIMEOUT : DATA_TIMEOUT); m_timeout_timer->adjust(timeout , 0 , timeout); } void hp_taco_device::cmd_fsm(void) { if (m_cmd_state == CMD_END) { // Command ended m_cmd_state = CMD_IDLE; m_hole_timer->reset(); m_timeout_timer->reset(); irq_w(true); if (AUTO_STOP(m_cmd_reg)) { // Automatic stop after command execution LOG_0(("Tape clamped\n")); m_cmd_state = CMD_STOPPING; m_tape_timer->adjust(time_to_stopping_pos()); } } else if (m_cmd_state == CMD_STOPPING) { // Braking phase ended m_cmd_state = CMD_IDLE; stop_tape(); if (CMD_CODE(m_cmd_reg) == CMD_STOP) { irq_w(true); } } else { attotime cmd_duration = attotime::never; if (m_cmd_state == CMD_INVERTING) { m_tape_fwd = DIR_FWD(m_cmd_reg); m_tape_fast = SPEED_FAST(m_cmd_reg); m_cmd_state = CMD_PH0; } if (m_cmd_state == CMD_PH0) { m_hole_timer->adjust(time_to_next_hole()); } switch (CMD_CODE(m_cmd_reg)) { case CMD_FINAL_GAP: if (m_cmd_state == CMD_PH0) { // PH0 m_rw_pos = m_tape_pos; cmd_duration = time_to_distance(END_GAP_LENGTH); m_cmd_state = CMD_PH1; } else { // PH1 m_image.write_gap(current_track() , m_rw_pos , m_tape_pos); cmd_duration = attotime::zero; m_cmd_state = CMD_END; } break; case CMD_RECORD_WRITE: if (m_cmd_state == CMD_PH0) { // PH0 // Search for preamble first m_rd_it_valid = m_image.next_data(current_track() , m_tape_pos , m_tape_fwd , false , m_rd_it); cmd_duration = time_to_rd_next_word(m_rw_pos); // Set T/O for preamble search set_data_timeout(true); m_cmd_state = CMD_PH1; break; } else if (m_cmd_state == CMD_PH1) { // PH1 if (m_rd_it->second == PREAMBLE_WORD) { LOG_0(("Got preamble\n")); m_cmd_state = CMD_PH2; // m_rw_pos already at correct position cmd_duration = fetch_next_wr_word(); m_timeout_timer->reset(); irq_w(true); } else { auto res = m_image.adv_it(current_track() , m_tape_fwd , m_rd_it); if (res != hti_format_t::ADV_NO_MORE_DATA) { cmd_duration = time_to_rd_next_word(m_rw_pos); } // Set T/O for arrival of data words set_data_timeout(false); } break; } // Intentional fall-through on PH2 case CMD_INIT_WRITE: if (m_cmd_state == CMD_PH0) { // PH0 m_next_word = PREAMBLE_WORD; m_rw_pos = m_tape_pos; cmd_duration = time_to_distance(hti_format_t::word_length(m_next_word)); m_cmd_state = CMD_PH1; } else { // PH1 & PH2 of CMD_RECORD_WRITE hti_format_t::tape_pos_t length; m_image.write_word(current_track() , m_rw_pos , m_next_word , length); hti_format_t::pos_offset(m_rw_pos , m_tape_fwd , length); // Just to be sure.. m_tape_pos = m_rw_pos; cmd_duration = fetch_next_wr_word(); irq_w(true); } break; case CMD_SET_TRACK: // PH0 // When b9 is 0, set track A/B // When b9 is 1, ignore command (in TACO chip it has an unknown purpose) if (!AUTO_STOP(m_cmd_reg)) { if (CMD_OPT(m_cmd_reg)) { BIT_SET(m_status_reg, STATUS_TRACKB_BIT); } else { BIT_CLR(m_status_reg, STATUS_TRACKB_BIT); } } cmd_duration = attotime::from_usec(QUICK_CMD_USEC); m_hole_timer->reset(); m_cmd_state = CMD_END; break; case CMD_MOVE: // PH0 // Endless movement: not setting cmd_duration m_cmd_state = CMD_END; break; case CMD_INGAP_MOVE: if (m_cmd_state == CMD_PH0) { // PH0 auto target = m_tape_pos; if (m_image.next_gap(current_track() , target, m_tape_fwd, min_gap_size())) { LOG_0(("IRG @%d\n" , target)); cmd_duration = time_to_target(target); } m_cmd_state = CMD_PH1; } else { // PH1 cmd_duration = time_to_tach_pulses(); freeze_tach_reg(false); m_cmd_state = CMD_END; } break; case CMD_INDTA_INGAP: case CMD_NOT_INDTA: if (m_cmd_state == CMD_PH0) { // PH0 if (m_image.next_data(current_track() , m_tape_pos , m_tape_fwd , true , m_rd_it)) { cmd_duration = time_to_target(hti_format_t::farthest_end(m_rd_it , m_tape_fwd)); } // Set T/O for data set_data_timeout(true); m_cmd_state = CMD_PH1; } else { // PH1 auto target = m_tape_pos; if (m_image.next_gap(current_track() , target, m_tape_fwd, min_gap_size())) { LOG_0(("End of data @%d\n" , target)); cmd_duration = time_to_target(target); } // Got data, stop T/O m_timeout_timer->reset(); m_cmd_state = CMD_END; } break; case CMD_WRITE_IRG: if (m_cmd_state == CMD_PH0) { // PH0 freeze_tach_reg(false); m_rw_pos = m_tape_pos; cmd_duration = time_to_tach_pulses(); m_cmd_state = CMD_PH1; } else { // PH1 m_image.write_gap(current_track() , m_rw_pos , m_tape_pos); cmd_duration = attotime::zero; m_cmd_state = CMD_END; } break; case CMD_SCAN_RECORDS: if (m_cmd_state == CMD_PH0 || m_cmd_state == CMD_PH2) { // PH0 and PH2 if (m_cmd_state == CMD_PH2) { m_tach_reg++; if (m_tach_reg == 0) { // All gaps found, bail out cmd_duration = attotime::zero; m_cmd_state = CMD_END; break; } } if (m_image.next_data(current_track() , m_tape_pos , m_tape_fwd , true , m_rd_it)) { cmd_duration = time_to_target(hti_format_t::farthest_end(m_rd_it , m_tape_fwd)); } // Set T/O for data set_data_timeout(true); m_cmd_state = CMD_PH1; } else if (m_cmd_state == CMD_PH1) { // PH1 auto target = m_tape_pos; if (m_image.next_gap(current_track() , target, m_tape_fwd, min_gap_size())) { LOG_0(("Gap @%d (%u to go)\n" , target , 0x10000U - m_tach_reg)); cmd_duration = time_to_target(target); } m_timeout_timer->reset(); m_cmd_state = CMD_PH2; } break; case CMD_MOVE_INDTA: if (m_cmd_state == CMD_PH0) { // PH0 freeze_tach_reg(false); cmd_duration = time_to_tach_pulses(); m_cmd_state = CMD_PH1; } else { // PH1 if (m_image.next_data(current_track() , m_tape_pos , m_tape_fwd , true , m_rd_it)) { cmd_duration = time_to_target(hti_format_t::farthest_end(m_rd_it , m_tape_fwd)); } // Apparently this cmd doesn't set no-data T/O m_cmd_state = CMD_END; } break; case CMD_UNK_1b: // PH0 // Unknown purpose, but make it a NOP (it's used in "T" test of test ROM) cmd_duration = attotime::from_usec(QUICK_CMD_USEC); m_cmd_state = CMD_END; break; case CMD_MOVE_INGAP: if (m_cmd_state == CMD_PH0) { // PH0 freeze_tach_reg(false); cmd_duration = time_to_tach_pulses(); m_cmd_state = CMD_PH1; } else { // PH1 auto target = m_tape_pos; if (m_image.next_gap(current_track() , target, m_tape_fwd, min_gap_size())) { LOG_0(("GAP @%d\n" , target)); cmd_duration = time_to_target(target); } m_cmd_state = CMD_END; } break; case CMD_START_READ: if (m_cmd_state == CMD_PH0) { // PH0 // Should also check if tape position has gone too far to read word @ m_rd_it if (!m_rd_it_valid) { // Search for preamble first m_rd_it_valid = m_image.next_data(current_track() , m_tape_pos , m_tape_fwd , false , m_rd_it); // Set T/O for preamble search set_data_timeout(true); m_cmd_state = CMD_PH1; } else { // Resume reading from last position, skip preamble search m_cmd_state = CMD_PH2; } } else { // Just to be sure.. m_tape_pos = m_rw_pos; if (m_cmd_state == CMD_PH1) { // PH1 // Any word with at least a 0 will do as preamble. // But anything that's not the correct preamble word (0) will cause a wrong alignment // with word boundaries. TACO will read garbage data in this case (this effect is not simulated here). if (m_rd_it->second != 0xffff) { m_cmd_state = CMD_PH2; LOG_0(("Got preamble %04x\n" , m_rd_it->second)); } } else { // PH2 if (m_irq) { LOG(("Data reg overflow!\n")); } irq_w(true); m_data_reg = m_rd_it->second; if (m_clear_checksum_reg) { m_checksum_reg = 0; m_clear_checksum_reg = false; } m_checksum_reg += m_data_reg; LOG_0(("RD %04x\n" , m_data_reg)); } // Set T/O for arrival of data words set_data_timeout(false); auto res = m_image.adv_it(current_track() , m_tape_fwd , m_rd_it); LOG_0(("adv_it %d\n" , res)); if (res == hti_format_t::ADV_NO_MORE_DATA) { m_rd_it_valid = false; } } cmd_duration = time_to_rd_next_word(m_rw_pos); break; case CMD_DELTA_MOVE_IRG: // PH0 freeze_tach_reg(false); cmd_duration = time_to_tach_pulses(); m_cmd_state = CMD_END; break; case CMD_END_READ: if (m_cmd_state == CMD_PH0) { // PH0 cmd_duration = time_to_rd_next_word(m_rw_pos); set_data_timeout(false); m_cmd_state = CMD_PH1; } else { // PH1 m_tape_pos = m_rw_pos; // Note: checksum is not updated m_data_reg = m_rd_it->second; LOG_0(("Final RD %04x\n" , m_data_reg)); auto res = m_image.adv_it(current_track() , m_tape_fwd , m_rd_it); if (res == hti_format_t::ADV_NO_MORE_DATA) { m_rd_it_valid = false; } cmd_duration = attotime::zero; m_cmd_state = CMD_END; } break; default: break; } m_tape_timer->adjust(cmd_duration); } } void hp_taco_device::start_cmd_exec(uint16_t new_cmd_reg) { LOG_0(("New cmd %02x @ %g cmd %02x st %d\n" , CMD_CODE(new_cmd_reg) , machine().time().as_double() , CMD_CODE(m_cmd_reg) , m_cmd_state)); update_tape_pos(); unsigned new_cmd_code = CMD_CODE(new_cmd_reg); if (new_cmd_code != CMD_START_READ && new_cmd_code != CMD_END_READ && new_cmd_code != CMD_CLEAR) { m_rd_it_valid = false; } bool started = false; switch (new_cmd_code) { case CMD_INDTA_INGAP: // Errors: CART OUT,FAST SPEED started = start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK); break; case CMD_FINAL_GAP: // Errors: WP,CART OUT started = start_tape_cmd(new_cmd_reg , 0 , STATUS_WPR_MASK); break; case CMD_INIT_WRITE: // Errors: WP,CART OUT,fast speed,reverse started = start_tape_cmd(new_cmd_reg , DIR_FWD_MASK , STATUS_WPR_MASK | SPEED_FAST_MASK); break; case CMD_STOP: m_cmd_reg = new_cmd_reg; freeze_tach_reg(false); m_hole_timer->reset(); m_timeout_timer->reset(); if (is_braking()) { // Already braking // m_tape_timer already set } else if (m_start_time.is_never()) { // Tape is already stopped LOG_0(("Already stopped\n")); m_tape_timer->adjust(attotime::from_usec(QUICK_CMD_USEC)); } else { // Start braking timer m_cmd_state = CMD_STOPPING; m_tape_timer->adjust(time_to_stopping_pos()); } m_cmd_state = CMD_STOPPING; return; case CMD_SET_TRACK: // Don't know if this command really starts the tape or not (probably it doesn't) started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_MOVE: started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_INGAP_MOVE: // Errors: CART OUT,FAST SPEED started = start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK); break; case CMD_CLEAR: set_error(false); BIT_CLR(m_status_reg, STATUS_HOLE_BIT); BIT_CLR(m_status_reg, STATUS_CART_OUT_BIT); BIT_CLR(m_status_reg, STATUS_WPR_BIT); set_tape_present(is_loaded()); // This is a special command: it doesn't raise IRQ at completion and it // doesn't replace current command return; case CMD_NOT_INDTA: // Errors: CART OUT started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_WRITE_IRG: // Errors: WP,CART OUT started = start_tape_cmd(new_cmd_reg , 0 , STATUS_WPR_MASK); break; case CMD_SCAN_RECORDS: // Errors: CART OUT started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_RECORD_WRITE: // Errors: WP,CART OUT,fast speed,reverse started = start_tape_cmd(new_cmd_reg , DIR_FWD_MASK , STATUS_WPR_MASK | SPEED_FAST_MASK); break; case CMD_MOVE_INDTA: // Errors: CART OUT,FAST SPEED started = start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK); break; case CMD_UNK_1b: started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_MOVE_INGAP: started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_START_READ: // Yes, you can read tape backwards: test "C" does that! // Because of this DIR_FWD_MASK is not in the "must be 1" mask. started = start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK); break; case CMD_DELTA_MOVE_IRG: started = start_tape_cmd(new_cmd_reg , 0 , 0); break; case CMD_END_READ: // This command only makes sense after CMD_START_READ if (CMD_CODE(m_cmd_reg) == CMD_START_READ) { started = start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK); LOG_0(("END_READ %d\n" , m_rd_it_valid)); } break; default: LOG(("Unrecognized command\n")); started = false; break; } if (!started) { set_error(true); m_cmd_state = CMD_IDLE; m_tape_timer->reset(); m_hole_timer->reset(); m_timeout_timer->reset(); } } image_init_result hp_taco_device::internal_load(bool is_create) { device_reset(); io_generic io; io.file = (device_image_interface *)this; io.procs = &image_ioprocs; io.filler = 0; if (is_create) { m_image.clear_tape(); m_image.save_tape(&io); } else if (!m_image.load_tape(&io)) { seterror(IMAGE_ERROR_INVALIDIMAGE , "Wrong format"); set_tape_present(false); return image_init_result::FAIL; } m_image_dirty = false; set_tape_present(true); return image_init_result::PASS; } image_init_result hp_taco_device::call_load() { LOG(("call_load\n")); return internal_load(false); } image_init_result hp_taco_device::call_create(int format_type, util::option_resolution *format_options) { LOG(("call_create\n")); return internal_load(true); } void hp_taco_device::call_unload() { LOG(("call_unload dirty=%d\n" , m_image_dirty)); device_reset(); if (m_image_dirty) { io_generic io; io.file = (device_image_interface *)this; io.procs = &image_ioprocs; io.filler = 0; m_image.save_tape(&io); m_image_dirty = false; } m_image.clear_tape(); set_tape_present(false); } std::string hp_taco_device::call_display() { std::string buffer; // Mostly lifted from cassette_image_device::call_display ;) // Do not show anything if image not loaded or tape not moving if (!exists() || m_start_time.is_never()) { return buffer; } char track = BIT(m_status_reg , STATUS_TRACKB_BIT) ? 'B' : 'A'; char r_w = m_tape_wr ? 'W' : 'R'; char m1; char m2; if (m_tape_fwd) { m1 = '>'; m2 = m_tape_fast ? '>' : ' '; } else { m1 = '<'; m2 = m_tape_fast ? '<' : ' '; } int pos_in = current_tape_pos() / hti_format_t::ONE_INCH_POS; buffer = string_format("%c %c %c%c [%04d/1824]" , track , r_w , m1 , m2 , pos_in); // Not correct when there are 2 or more instances of TACO return buffer; } const char *hp_taco_device::file_extensions() const { return "hti"; }