// 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. // The main shortcomings of my approach are: // * I could indentify 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. // 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. It's not clear to me // | | what value could be read from this register, if it's just the same value that was written last time or the internal // | | counter or something else entirely. // R7 | R | Checksum register. Reading it clears it. // 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. // // 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 ? Drive ON according to [1], the actual use seems to be selection of gap length // 8 RW ? Size of gaps according to [1], N/U in my opinion // 7 RW Speed of tape (1 = 90 ips, 0 = 22 ips) // 6 RW Option bit for various commands // 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 // * Sector #0 is not used // * 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: // 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: Format/sector in use and other unidentified bits. // Word 2: Sector number // Word 3: Sector length and other bits // 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 // - 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) // * Inversion of tape direction and change of speed are immediate as well // * Time & distance to stop the tape are modeled, though. Firmware is upset by // a tape with null braking 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: // * Some code cleanup // * Handling of tape holes seems to be wrong: test "C" of test ROM only works partially // * Find out what is read from register R6 // * Handle device_image_interface::call_display to show state of tape // * 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" // 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 }; // 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 ONE_INCH_POS (TACH_TICKS_PER_INCH * TAPE_POS_FRACT) // Value in tape_pos_t representing 1 inch of tape #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 TAPE_LENGTH ((140 * 12 + 72 * 2) * ONE_INCH_POS) // Tape length: 140 ft of usable tape + 72" of punched tape at either end #define TAPE_INIT_POS (80 * ONE_INCH_POS) // Initial tape position: 80" from beginning (just past the punched part) #define ZERO_BIT_LEN 619 // Length of 0 bits at slow tape speed: 1/(35200 Hz) #define ONE_BIT_LEN 1083 // Length of 1 bits at slow tape speed: 1.75 times ZERO_BIT_LEN #define QUICK_CMD_USEC 25 // usec for "quick" command execution #define FAST_BRAKE_MSEC 73 // Braking time from fast speed to stop (2 ips) in msec (deceleration is 1200 in/s^2) #define SLOW_BRAKE_MSEC 17 // Braking time from slow speed to stop in msec #define FAST_BRAKE_DIST 3350450 // Braking distance at fast speed (~3.38 in) #define SLOW_BRAKE_DIST 197883 // Braking distance at slow speed (~0.2 in) #define PREAMBLE_WORD 0 // Value of preamble word #define END_GAP_LENGTH (6 * ONE_INCH_POS) // Length of final gap: 6" #define MIN_IRG_LENGTH ((tape_pos_t)(0.2 * ONE_INCH_POS)) // Minimum length of IRGs: 0.2" (from 9825, not sure about value in TACO) #define NULL_TAPE_POS ((tape_pos_t)-1) // Special value for invalid/unknown tape position #define NO_DATA_GAP (17 * ONE_BIT_LEN) // Minimum gap size to detect end of data: length of longest word (0xffff) #define FILE_MAGIC 0x4f434154 // Magic value at start of image file: "TACO" // Parts of command register #define CMD_CODE(reg) \ (((reg) >> 10) & 0x1f) #define DIR_FWD(reg) \ (BIT(reg , 15)) #define SPEED_FAST(reg) \ (BIT(reg , 7)) #define CMD_OPT(reg) \ (BIT(reg , 6)) #define UNKNOWN_B9(reg) \ (BIT(reg , 9)) #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 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 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_DELTA_MOVE_HOLE, // 1c: move tape a given distance, intr at end or first hole found (whichever comes first) 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. // *** Position of tape holes *** // At beginning of tape: // *START* // |<-----24"----->|<---12"--->|<---12"--->|<-----24"----->| // O O O O O O O // |<->| |<->| |<->| // 0.218" 0.218" 0.218" // At end of tape: // *END* // |<-----24"----->|<---12"--->|<---12"--->|<-----24"----->| // O O O O // static const hp_taco_device::tape_pos_t tape_holes[] = { (hp_taco_device::tape_pos_t)(23.891 * ONE_INCH_POS), // 24 - 0.218 / 2 (hp_taco_device::tape_pos_t)(24.109 * ONE_INCH_POS), // 24 + 0.218 / 2 (hp_taco_device::tape_pos_t)(35.891 * ONE_INCH_POS), // 36 - 0.218 / 2 (hp_taco_device::tape_pos_t)(36.109 * ONE_INCH_POS), // 36 + 0.218 / 2 (hp_taco_device::tape_pos_t)(47.891 * ONE_INCH_POS), // 48 - 0.218 / 2 (hp_taco_device::tape_pos_t)(48.109 * ONE_INCH_POS), // 48 + 0.218 / 2 72 * ONE_INCH_POS, // 72 1752 * ONE_INCH_POS, // 1752 1776 * ONE_INCH_POS, // 1776 1788 * ONE_INCH_POS, // 1788 1800 * ONE_INCH_POS // 1800 }; // Device type definition const device_type HP_TACO = &device_creator; // Constructors hp_taco_device::hp_taco_device(const machine_config &mconfig, device_type type, const char *name, const char *tag, device_t *owner, UINT32 clock, const char *shortname) : device_t(mconfig, type, name, tag, owner, clock, shortname, __FILE__), device_image_interface(mconfig , *this), m_irq_handler(*this), m_flg_handler(*this), m_sts_handler(*this), m_tape_pos(TAPE_INIT_POS), m_image_dirty(false) { clear_state(); } hp_taco_device::hp_taco_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock) : device_t(mconfig, HP_TACO, "HP TACO", tag, owner, clock, "TACO", __FILE__), device_image_interface(mconfig , *this), m_irq_handler(*this), m_flg_handler(*this), m_sts_handler(*this), m_tape_pos(TAPE_INIT_POS), m_image_dirty(false) { clear_state(); } 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; break; case 3: // Timing register m_timing_reg = data; break; } } READ16_MEMBER(hp_taco_device::reg_r) { UINT16 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 res = m_tach_reg; break; case 3: // Checksum register: it clears when read res = m_checksum_reg; m_checksum_reg = 0; 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_config_complete void hp_taco_device::device_config_complete() { LOG(("device_config_complete")); update_names(); } // 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_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); } // 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); } void hp_taco_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) { if (CMD_CODE(m_cmd_reg) != CMD_STOP) { update_tape_pos(); } switch (id) { case TAPE_TMR_ID: LOG_0(("Tape tmr @%g\n" , machine().time().as_double())); tape_pos_t length; switch (CMD_CODE(m_cmd_reg)) { case CMD_INDTA_INGAP: if (m_cmd_state == 0) { m_cmd_state = 1; tape_pos_t target = m_tape_pos; if (next_n_gap(target, 1, MIN_IRG_LENGTH)) { m_tape_timer->adjust(time_to_target(target)); } return; } break; case CMD_RECORD_WRITE: if (m_cmd_state == 0) { if (m_rd_it->second == PREAMBLE_WORD) { LOG_0(("Got preamble\n")); m_cmd_state = 1; // m_rw_pos already at correct position m_tape_timer->adjust(fetch_next_wr_word()); break; } else { adv_res_t res = adv_it(m_rd_it); if (res != ADV_NO_MORE_DATA) { m_tape_timer->adjust(time_to_rd_next_word(m_rw_pos)); } // No IRQ return; } } // Intentional fall-through case CMD_INIT_WRITE: write_word(m_rw_pos , m_next_word , length); pos_offset(m_rw_pos , length); // Just to be sure.. m_tape_pos = m_rw_pos; m_tape_timer->adjust(fetch_next_wr_word()); break; case CMD_STOP: move_tape_pos(m_tape_fast ? FAST_BRAKE_DIST : SLOW_BRAKE_DIST); stop_tape(); break; case CMD_INGAP_MOVE: if (m_cmd_state == 0) { m_cmd_state = 1; m_tape_timer->adjust(time_to_tach_pulses()); return; } break; case CMD_FINAL_GAP: case CMD_WRITE_IRG: write_gap(m_rw_pos , m_tape_pos); m_hole_timer->reset(); break; case CMD_SCAN_RECORDS: if (m_cmd_state == 0) { m_cmd_state = 1; tape_pos_t target = m_tape_pos; if (next_n_gap(target, 0x10000U - m_tach_reg, MIN_IRG_LENGTH)) { LOG_0(("%u gaps @%d\n" , 0x10000U - m_tach_reg, target)); m_tape_timer->adjust(time_to_target(target)); } return; } else { m_hole_timer->reset(); } break; case CMD_MOVE_INDTA: if (m_cmd_state == 0) { if (next_data(m_rd_it , m_tape_pos , true)) { m_cmd_state = 1; m_tape_timer->adjust(time_to_target(farthest_end(m_rd_it))); } // No IRQ return; } // m_cmd_state == 1 -> IRQ & cmd end break; case CMD_DELTA_MOVE_HOLE: case CMD_DELTA_MOVE_IRG: // Interrupt at end of movement m_hole_timer->reset(); break; case CMD_START_READ: { bool set_intr = true; // Just to be sure.. m_tape_pos = m_rw_pos; if (m_cmd_state == 0) { set_intr = false; if (m_rd_it->second == PREAMBLE_WORD) { m_cmd_state = 1; } LOG_0(("Got preamble\n")); } else { m_data_reg = m_rd_it->second; m_checksum_reg += m_data_reg; LOG_0(("RD %04x\n" , m_data_reg)); } adv_res_t res = adv_it(m_rd_it); LOG_0(("adv_it %d\n" , res)); if (res == ADV_NO_MORE_DATA) { m_rd_it_valid = false; } else { if (res == ADV_DISCONT_DATA) { // Hit a gap, restart preamble search m_cmd_state = 0; } m_tape_timer->adjust(time_to_rd_next_word(m_rw_pos)); } if (!set_intr) { return; } } break; case CMD_END_READ: { 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)); adv_res_t res = adv_it(m_rd_it); if (res == ADV_NO_MORE_DATA) { m_rd_it_valid = false; } m_hole_timer->reset(); } break; default: // Other commands: just raise irq break; } irq_w(true); break; case HOLE_TMR_ID: LOG_0(("Hole tmr @%g\n" , machine().time().as_double())); BIT_SET(m_status_reg , STATUS_HOLE_BIT); switch (CMD_CODE(m_cmd_reg)) { case CMD_FINAL_GAP: case CMD_WRITE_IRG: write_gap(m_rw_pos , m_tape_pos); m_rw_pos = m_tape_pos; break; case CMD_SCAN_RECORDS: case CMD_DELTA_MOVE_HOLE: // Cmds 18 & 1c are terminated at first hole m_tape_timer->reset(); irq_w(true); // No reloading of hole timer return; case CMD_DELTA_MOVE_IRG: // TODO: update r6 m_hole_timer->adjust(time_to_next_hole()); // No IRQ at holes return; case CMD_START_READ: case CMD_END_READ: 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; 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_checksum_reg = 0; m_timing_reg = 0; m_cmd_state = 0; // m_tape_pos is not reset, tape stays where it is 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 = 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)); } unsigned hp_taco_device::speed_to_tick_freq(void) const { return m_tape_fast ? TACH_FREQ_FAST * TAPE_POS_FRACT : TACH_FREQ_SLOW * TAPE_POS_FRACT; } bool hp_taco_device::pos_offset(tape_pos_t& pos , tape_pos_t offset) const { if (offset == 0) { return true; } if (!m_tape_fwd) { offset = -offset; } pos += offset; // In real life tape would unspool.. if (pos > TAPE_LENGTH) { pos = TAPE_LENGTH; return false; } else if (pos < 0) { pos = 0; return false; } else { return true; } } void hp_taco_device::move_tape_pos(tape_pos_t delta_pos) { tape_pos_t tape_start_pos = m_tape_pos; if (!pos_offset(m_tape_pos , delta_pos)) { LOG(("Tape unspooled!\n")); } m_start_time = machine().time(); LOG_0(("Tape pos = %u\n" , m_tape_pos)); if (any_hole(tape_start_pos , m_tape_pos)) { // Crossed one or more holes BIT_SET(m_status_reg , STATUS_HOLE_BIT); } } void hp_taco_device::update_tape_pos(void) { if (m_start_time.is_never()) { // Tape not moving return; } 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? tape_pos_t delta_tach = (tape_pos_t)(delta_time.as_ticks(speed_to_tick_freq())); LOG_0(("delta_tach = %u\n" , delta_tach)); move_tape_pos(delta_tach); // Gap detection bool gap_detected = false; if (m_gap_detect_start != NULL_TAPE_POS && abs(m_gap_detect_start - m_tape_pos) >= MIN_IRG_LENGTH) { tape_pos_t tmp = m_tape_pos; pos_offset(tmp , -MIN_IRG_LENGTH); gap_detected = just_gap(tmp , m_tape_pos); } if (gap_detected) { BIT_SET(m_status_reg, STATUS_GAP_BIT); } else { BIT_CLR(m_status_reg, STATUS_GAP_BIT); } } void hp_taco_device::ensure_a_lt_b(tape_pos_t& a , tape_pos_t& b) { if (a > b) { // Ensure A always comes before B tape_pos_t tmp; tmp = a; a = b; b = tmp; } } // Is there any hole in a given section of tape? bool hp_taco_device::any_hole(tape_pos_t tape_pos_a , tape_pos_t tape_pos_b) { ensure_a_lt_b(tape_pos_a , tape_pos_b); for (tape_pos_t hole : tape_holes) { if (tape_pos_a < hole && tape_pos_b >= hole) { return true; } } return false; } // Position of next hole tape will reach in a given direction hp_taco_device::tape_pos_t hp_taco_device::next_hole(void) const { if (m_tape_fwd) { for (tape_pos_t hole : tape_holes) { if (hole > m_tape_pos) { LOG_0(("next hole fwd @%u = %u\n" , m_tape_pos , hole)); return hole; } } // No more holes: will hit end of tape return TAPE_LENGTH; } else { for (int i = (sizeof(tape_holes) / sizeof(tape_holes[ 0 ])) - 1; i >= 0; i--) { if (tape_holes[ i ] < m_tape_pos) { LOG_0(("next hole rev @%u = %u\n" , m_tape_pos , tape_holes[ i ])); return tape_holes[ i ]; } } // No more holes: will hit start of tape return 0; } } attotime hp_taco_device::time_to_distance(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(tape_pos_t target) const { return time_to_distance(abs(target - m_tape_pos)); } bool hp_taco_device::start_tape_cmd(UINT16 cmd_reg , UINT16 must_be_1 , UINT16 must_be_0) { m_cmd_reg = cmd_reg; UINT16 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) { set_error(true); 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 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); // TODO: remove? BIT_CLR(m_status_reg, STATUS_HOLE_BIT); if (m_tape_wr) { // Write command: disable gap detector m_gap_detect_start = NULL_TAPE_POS; BIT_CLR(m_status_reg, STATUS_GAP_BIT); m_image_dirty = true; } else if (not_moving || prev_tape_wr != m_tape_wr || prev_tape_fwd != m_tape_fwd || prev_tape_fast != m_tape_fast) { // Tape 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); } return true; } } void hp_taco_device::stop_tape(void) { m_start_time = attotime::never; m_gap_detect_start = NULL_TAPE_POS; } hp_taco_device::tape_track_t& hp_taco_device::current_track(void) { return m_tracks[ BIT(m_status_reg , STATUS_TRACKB_BIT) ]; } // Return physical length of a 16-bit word on tape hp_taco_device::tape_pos_t hp_taco_device::word_length(tape_word_t w) { unsigned zeros , ones; // pop count of w ones = (w & 0x5555) + ((w >> 1) & 0x5555); ones = (ones & 0x3333) + ((ones >> 2) & 0x3333); ones = (ones & 0x0f0f) + ((ones >> 4) & 0x0f0f); ones = (ones & 0x00ff) + ((ones >> 8) & 0x00ff); zeros = 16 - ones; return zeros * ZERO_BIT_LEN + (ones + 1) * ONE_BIT_LEN; } hp_taco_device::tape_pos_t hp_taco_device::word_end_pos(const tape_track_t::iterator& it) { return it->first + word_length(it->second); } void hp_taco_device::adjust_it(tape_track_t& track , tape_track_t::iterator& it , tape_pos_t pos) { if (it != track.begin()) { it--; if (word_end_pos(it) <= pos) { it++; } } } // Write a word on current tape track void hp_taco_device::write_word(tape_pos_t start , tape_word_t word , tape_pos_t& length) { tape_track_t& track = current_track(); tape_track_t::iterator it_low = track.lower_bound(start); adjust_it(track , it_low , start); length = word_length(word); tape_pos_t end_pos = start + length; tape_track_t::iterator it_high = track.lower_bound(end_pos); track.erase(it_low , it_high); track.insert(it_high , std::make_pair(start, word)); LOG_0(("WR %04x @ T%u:%u\n" , word , BIT(m_status_reg , STATUS_TRACKB_BIT) , start)); } // Write a gap on current track void hp_taco_device::write_gap(tape_pos_t a , tape_pos_t b) { ensure_a_lt_b(a , b); tape_track_t& track = current_track(); tape_track_t::iterator it_low = track.lower_bound(a); adjust_it(track , it_low , a); tape_track_t::iterator it_high = track.lower_bound(b); track.erase(it_low, it_high); LOG_0(("GAP on T%u:[%u,%u)\n" , BIT(m_status_reg , STATUS_TRACKB_BIT) , a , b)); } bool hp_taco_device::just_gap(tape_pos_t a , tape_pos_t b) { ensure_a_lt_b(a , b); tape_track_t& track = current_track(); tape_track_t::iterator it_low = track.lower_bound(a); tape_track_t::iterator it_high = track.lower_bound(b); adjust_it(track, it_low, a); return it_low == it_high; } hp_taco_device::tape_pos_t hp_taco_device::farthest_end(const tape_track_t::iterator& it) const { if (m_tape_fwd) { return word_end_pos(it); } else { return it->first; } } bool hp_taco_device::next_data(tape_track_t::iterator& it , tape_pos_t pos , bool inclusive) { tape_track_t& track = current_track(); it = track.lower_bound(pos); if (m_tape_fwd) { if (inclusive) { adjust_it(track, it, pos); } return it != track.end(); } else { // Never more than 2 iterations do { if (it == track.begin()) { it = track.end(); return false; } it--; } while (!inclusive && word_end_pos(it) > pos); return true; } } hp_taco_device::adv_res_t hp_taco_device::adv_it(tape_track_t::iterator& it) { tape_track_t& track = current_track(); if (m_tape_fwd) { tape_pos_t prev_pos = word_end_pos(it); it++; if (it == track.end()) { return ADV_NO_MORE_DATA; } else { adv_res_t res = prev_pos == it->first ? ADV_CONT_DATA : ADV_DISCONT_DATA; return res; } } else { if (it == track.begin()) { it = track.end(); return ADV_NO_MORE_DATA; } else { tape_pos_t prev_pos = it->first; it--; return prev_pos == word_end_pos(it) ? ADV_CONT_DATA : ADV_DISCONT_DATA; } } } 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)); } // Update checksum with new word m_checksum_reg += m_next_word; return time_to_distance(word_length(m_next_word)); } attotime hp_taco_device::time_to_rd_next_word(tape_pos_t& word_rd_pos) { if (m_rd_it_valid) { word_rd_pos = farthest_end(m_rd_it); return time_to_target(word_rd_pos); } else { return attotime::never; } } /** * Scan for next "n_gaps" gaps * * @param[in,out] pos Start position on input, start of gap on output * @param it Pointer to data word where scan is to start * @param n_gaps Number of gaps to scan * @param min_gap Minimum gap size * * @return true if n_gaps gaps are found */ bool hp_taco_device::next_n_gap(tape_pos_t& pos , tape_track_t::iterator it , unsigned n_gaps , tape_pos_t min_gap) { tape_track_t& track = current_track(); bool done = false; tape_track_t::iterator prev_it; if (m_tape_fwd) { tape_pos_t next_pos; while (1) { if (it == track.end()) { next_pos = TAPE_LENGTH; done = true; } else { next_pos = it->first; } if (((next_pos - pos) >= min_gap && --n_gaps == 0) || done) { break; } adv_res_t adv_res; do { prev_it = it; adv_res = adv_it(it); } while (adv_res == ADV_CONT_DATA); pos = word_end_pos(prev_it); } } else { tape_pos_t next_pos; while (1) { if (it == track.end()) { next_pos = 0; done = true; } else { next_pos = word_end_pos(it); } if (((pos - next_pos) >= min_gap && --n_gaps == 0) || done) { break; } adv_res_t adv_res; do { prev_it = it; adv_res = adv_it(it); } while (adv_res == ADV_CONT_DATA); pos = prev_it->first; } } // Set "pos" where minimum gap size is met pos_offset(pos , min_gap); return n_gaps == 0; } bool hp_taco_device::next_n_gap(tape_pos_t& pos , unsigned n_gaps , tape_pos_t min_gap) { tape_track_t::iterator it; // First align with next data next_data(it, pos, true); // Then scan for n_gaps return next_n_gap(pos, it, n_gaps, min_gap); } void hp_taco_device::clear_tape(void) { for (unsigned track_n = 0; track_n < 2; track_n++) { m_tracks[ track_n ].clear(); } } void hp_taco_device::dump_sequence(tape_track_t::const_iterator it_start , unsigned n_words) { if (n_words) { UINT32 tmp32; UINT16 tmp16; tmp32 = n_words; fwrite(&tmp32 , sizeof(tmp32)); tmp32 = it_start->first; fwrite(&tmp32 , sizeof(tmp32)); for (unsigned i = 0; i < n_words; i++) { tmp16 = it_start->second; fwrite(&tmp16 , sizeof(tmp16)); it_start++; } } } void hp_taco_device::save_tape(void) { UINT32 tmp32; fseek(0, SEEK_SET); tmp32 = FILE_MAGIC; fwrite(&tmp32 , sizeof(tmp32)); for (unsigned track_n = 0; track_n < 2; track_n++) { const tape_track_t& track = m_tracks[ track_n ]; tape_pos_t next_pos = (tape_pos_t)-1; unsigned n_words = 0; tape_track_t::const_iterator it_start; for (tape_track_t::const_iterator it = track.cbegin(); it != track.cend(); it++) { if (it->first != next_pos) { dump_sequence(it_start , n_words); it_start = it; n_words = 0; } next_pos = it->first + word_length(it->second); n_words++; } dump_sequence(it_start , n_words); // End of track tmp32 = (UINT32)-1; fwrite(&tmp32 , sizeof(tmp32)); } } bool hp_taco_device::load_track(tape_track_t& track) { UINT32 tmp32; track.clear(); while (1) { if (fread(&tmp32 , sizeof(tmp32)) != sizeof(tmp32)) { return false; } if (tmp32 == (UINT32)-1) { return true; } unsigned n_words = tmp32; if (fread(&tmp32 , sizeof(tmp32)) != sizeof(tmp32)) { return false; } tape_pos_t pos = (tape_pos_t)tmp32; for (unsigned i = 0; i < n_words; i++) { UINT16 tmp16; if (fread(&tmp16 , sizeof(tmp16)) != sizeof(tmp16)) { return false; } track.insert(std::make_pair(pos , tmp16)); pos += word_length(tmp16); } } } bool hp_taco_device::load_tape(void) { UINT32 magic; if (fread(&magic , sizeof(magic)) != sizeof(magic) || magic != FILE_MAGIC) { return false; } for (unsigned track_n = 0; track_n < 2; track_n++) { if (!load_track(m_tracks[ track_n ])) { LOG(("load_tape failed")); clear_tape(); return false; } } LOG(("load_tape done\n")); return true; } 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 { return time_to_target(next_hole()); } attotime hp_taco_device::time_to_tach_pulses(void) const { return time_to_distance((tape_pos_t)(0x10000U - m_tach_reg) * TAPE_POS_FRACT); } void hp_taco_device::start_cmd_exec(UINT16 new_cmd_reg) { LOG(("Cmd = %02x\n" , CMD_CODE(new_cmd_reg))); update_tape_pos(); attotime cmd_duration = attotime::never; attotime time_to_hole = attotime::never; 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; } switch (new_cmd_code) { case CMD_INDTA_INGAP: // Errors: CART OUT,FAST SPEED if (start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { m_cmd_state = 0; if (next_data(m_rd_it , m_tape_pos , true)) { cmd_duration = time_to_target(farthest_end(m_rd_it)); } } break; case CMD_FINAL_GAP: // Errors: WP,CART OUT if (start_tape_cmd(new_cmd_reg , 0 , STATUS_WPR_MASK)) { m_rw_pos = m_tape_pos; cmd_duration = time_to_distance(END_GAP_LENGTH); time_to_hole = time_to_next_hole(); } 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,FAST SPEED if (start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { tape_pos_t target = m_tape_pos; if (next_n_gap(target, 1, NO_DATA_GAP)) { LOG_0(("End of data @%d\n" , target)); cmd_duration = time_to_target(target); } // Holes detected? } break; case CMD_INIT_WRITE: // Errors: WP,CART OUT,fast speed,reverse if (start_tape_cmd(new_cmd_reg , DIR_FWD_MASK , STATUS_WPR_MASK | SPEED_FAST_MASK)) { m_next_word = PREAMBLE_WORD; m_rw_pos = m_tape_pos; cmd_duration = time_to_distance(word_length(m_next_word)); } break; case CMD_STOP: if (CMD_CODE(m_cmd_reg) != CMD_STOP) { if (m_start_time.is_never()) { // Tape is already stopped cmd_duration = attotime::from_usec(QUICK_CMD_USEC); } else { // Start braking timer cmd_duration = attotime::from_msec(m_tape_fast ? FAST_BRAKE_MSEC : SLOW_BRAKE_MSEC); } m_cmd_reg = new_cmd_reg; } else { // TODO: check if ok return; } break; case CMD_SET_TRACK: // Don't know if this command really starts the tape or not (probably it doesn't) if (start_tape_cmd(new_cmd_reg , 0 , 0)) { // When b9 is 0, set track A/B // When b9 is 1, ignore command (in TACO chip it has an unknown purpose) if (!UNKNOWN_B9(new_cmd_reg)) { if (CMD_OPT(new_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); } break; case CMD_MOVE: if (start_tape_cmd(new_cmd_reg , 0 , 0)) { time_to_hole = time_to_next_hole(); } break; case CMD_INGAP_MOVE: // Errors: CART OUT,FAST SPEED if (start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { m_cmd_state = 0; tape_pos_t target = m_tape_pos; if (next_n_gap(target, 1, MIN_IRG_LENGTH)) { LOG_0(("IRG @%d\n" , target)); cmd_duration = time_to_target(target); } // Holes detected? } break; case CMD_WRITE_IRG: // Errors: WP,CART OUT if (start_tape_cmd(new_cmd_reg , 0 , STATUS_WPR_MASK)) { m_rw_pos = m_tape_pos; cmd_duration = time_to_tach_pulses(); time_to_hole = time_to_next_hole(); } break; case CMD_SCAN_RECORDS: // Errors: CART OUT if (start_tape_cmd(new_cmd_reg , 0 , 0)) { m_cmd_state = 0; if (next_data(m_rd_it , m_tape_pos , true)) { cmd_duration = time_to_target(farthest_end(m_rd_it)); } time_to_hole = time_to_next_hole(); } break; case CMD_RECORD_WRITE: // Errors: WP,CART OUT,fast speed,reverse if (start_tape_cmd(new_cmd_reg , DIR_FWD_MASK , STATUS_WPR_MASK | SPEED_FAST_MASK)) { // Search for preamble first m_cmd_state = 0; m_rd_it_valid = next_data(m_rd_it , m_tape_pos , false); cmd_duration = time_to_rd_next_word(m_rw_pos); // Holes detected? } break; case CMD_MOVE_INDTA: // Errors: CART OUT,FAST SPEED if (start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { m_cmd_state = 0; cmd_duration = time_to_tach_pulses(); // Holes detected? } break; case CMD_UNK_1b: if (start_tape_cmd(new_cmd_reg , 0 , 0)) { // Unknown purpose, but make it a NOP (it's used in "T" test of test ROM) cmd_duration = attotime::from_usec(QUICK_CMD_USEC); } break; case CMD_DELTA_MOVE_HOLE: case CMD_DELTA_MOVE_IRG: if (start_tape_cmd(new_cmd_reg , 0 , 0)) { cmd_duration = time_to_tach_pulses(); time_to_hole = time_to_next_hole(); } 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. if (start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { // TODO: check anche m_rw_pos sforato if (!m_rd_it_valid) { // Search for preamble first m_cmd_state = 0; m_rd_it_valid = next_data(m_rd_it , m_tape_pos , false); } cmd_duration = time_to_rd_next_word(m_rw_pos); time_to_hole = time_to_next_hole(); } break; case CMD_END_READ: // This command only makes sense after CMD_START_READ if (CMD_CODE(m_cmd_reg) == CMD_START_READ && m_cmd_state == 1 && start_tape_cmd(new_cmd_reg , 0 , SPEED_FAST_MASK)) { LOG_0(("END_READ %d\n" , m_rd_it_valid)); cmd_duration = time_to_rd_next_word(m_rw_pos); time_to_hole = time_to_next_hole(); } break; default: LOG(("Unrecognized command\n")); break; } m_tape_timer->adjust(cmd_duration); m_hole_timer->adjust(time_to_hole); } bool hp_taco_device::call_load() { LOG(("call_load %d\n" , has_been_created())); if (has_been_created()) { clear_tape(); save_tape(); } else if (!load_tape()) { seterror(IMAGE_ERROR_INVALIDIMAGE , "Wrong format"); set_tape_present(false); return IMAGE_INIT_FAIL; } m_image_dirty = false; set_tape_present(true); return IMAGE_INIT_PASS; } bool hp_taco_device::call_create(int format_type, option_resolution *format_options) { LOG(("call_create %d\n" , has_been_created())); return call_load(); } void hp_taco_device::call_unload() { LOG(("call_unload dirty=%d\n" , m_image_dirty)); if (m_image_dirty) { save_tape(); m_image_dirty = false; } clear_tape(); set_tape_present(false); } const char *hp_taco_device::file_extensions() const { return "hti"; }