| /* 3b2_cpu.h: AT&T 3B2 Model 400 Floppy (TMS2797NL) Implementation | |
| Copyright (c) 2017, Seth J. Morabito | |
| Permission is hereby granted, free of charge, to any person | |
| obtaining a copy of this software and associated documentation | |
| files (the "Software"), to deal in the Software without | |
| restriction, including without limitation the rights to use, copy, | |
| modify, merge, publish, distribute, sublicense, and/or sell copies | |
| of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be | |
| included in all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS | |
| BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | |
| ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. | |
| Except as contained in this notice, the name of the author shall | |
| not be used in advertising or otherwise to promote the sale, use or | |
| other dealings in this Software without prior written authorization | |
| from the author. | |
| */ | |
| #include "3b2_if.h" | |
| #include <assert.h> | |
| /* | |
| * TODO: Macros used for debugging timers. Remove when debugging is complete. | |
| */ | |
| double if_start_time; | |
| #define IF_START_TIME() { if_start_time = sim_gtime(); } | |
| #define IF_DIFF_MS() ((sim_gtime() - if_start_time) / INST_PER_MS) | |
| #ifndef max | |
| #define max(x,y) ((x) > (y) ? (x) : (y)) | |
| #endif | |
| #ifndef min | |
| #define min(x,y) ((x) < (y) ? (x) : (y)) | |
| #endif | |
| /* | |
| * Disk Format: | |
| * ------------ | |
| * | |
| * - 80 Tracks | |
| * - 9 Sectors per track | |
| * - 2 heads | |
| * - 512 bytes per sector | |
| * | |
| * 80 * 9 * 2 * 512 = 720KB | |
| * | |
| * The clock on pin 24 runs at 1.000 MHz, meaning that each | |
| * step is 6ms and head settling time is 30ms. | |
| */ | |
| #define IF_STEP_DELAY 6 /* ms */ | |
| #define IF_R_DELAY 85 /* ms */ | |
| #define IF_W_DELAY 90 /* ms */ | |
| #define IF_VERIFY_DELAY 30 /* ms */ | |
| #define IF_HLD_DELAY 80 /* ms */ | |
| #define IF_HSW_DELAY 60 /* ms */ | |
| UNIT if_unit = { | |
| UDATA (&if_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_BUFABLE+ | |
| UNIT_MUSTBUF+UNIT_BINK, IF_DSK_SIZE) | |
| }; | |
| REG if_reg[] = { | |
| { NULL } | |
| }; | |
| DEVICE if_dev = { | |
| "IF", &if_unit, if_reg, NULL, | |
| 1, 16, 8, 1, 16, 8, | |
| NULL, NULL, &if_reset, | |
| NULL, NULL, NULL, NULL, | |
| DEV_DEBUG, 0, sys_deb_tab | |
| }; | |
| IF_STATE if_state; | |
| uint32 if_sec_ptr = 0; | |
| t_bool if_irq = FALSE; | |
| /* Function implementation */ | |
| static SIM_INLINE void if_set_irq() | |
| { | |
| if_irq = TRUE; | |
| csr_data |= CSRDISK; | |
| } | |
| static SIM_INLINE void if_clear_irq() | |
| { | |
| if_irq = FALSE; | |
| csr_data &= ~CSRDISK; | |
| } | |
| static SIM_INLINE void if_activate(uint32 delay) | |
| { | |
| IF_START_TIME(); | |
| sim_activate_abs(&if_unit, (int32) DELAY_MS(delay)); | |
| } | |
| static SIM_INLINE void if_cancel_pending_irq() | |
| { | |
| sim_cancel(&if_unit); | |
| } | |
| t_stat if_svc(UNIT *uptr) | |
| { | |
| if_state.status &= ~(IF_BUSY); | |
| switch(if_state.cmd & 0xf0) { | |
| case IF_RESTORE: | |
| if_state.status = (IF_TK_0|IF_HEAD_LOADED); | |
| break; | |
| case IF_SEEK: | |
| if_state.status = IF_HEAD_LOADED; | |
| if (if_state.track == 0) { | |
| if_state.status |= IF_TK_0; | |
| } | |
| break; | |
| } | |
| if_state.cmd = 0; | |
| /* Request an interrupt */ | |
| sim_debug(IRQ_MSG, &if_dev, "\tINTR\t\tDELTA=%f ms\n", IF_DIFF_MS()); | |
| if_set_irq(); | |
| return SCPE_OK; | |
| } | |
| t_stat if_reset(DEVICE *dptr) | |
| { | |
| if_state.status = IF_TK_0; | |
| if_state.track = 0; | |
| if_state.sector = 1; | |
| if_sec_ptr = 0; | |
| return SCPE_OK; | |
| } | |
| uint32 if_read(uint32 pa, size_t size) { | |
| uint8 reg, data; | |
| uint32 pos, pc; | |
| UNIT *uptr; | |
| uint8 *fbuf; | |
| uptr = &(if_dev.units[0]); | |
| reg = (uint8)(pa - IFBASE); | |
| pc = R[NUM_PC]; | |
| fbuf = (uint8 *)uptr->filebuf; | |
| switch (reg) { | |
| case IF_STATUS_REG: | |
| data = if_state.status; | |
| /* If there's no image attached, we're not ready */ | |
| if ((uptr->flags & (UNIT_ATT|UNIT_BUF)) == 0) { | |
| data |= IF_NRDY; | |
| } | |
| /* Reading the status register always de-asserts the IRQ line */ | |
| if_clear_irq(); | |
| sim_debug(READ_MSG, &if_dev, "\tSTATUS\t%02x\n", data); | |
| break; | |
| case IF_TRACK_REG: | |
| data = if_state.track; | |
| sim_debug(READ_MSG, &if_dev, "\tTRACK\t%02x\n", data); | |
| break; | |
| case IF_SECTOR_REG: | |
| data = if_state.sector; | |
| sim_debug(READ_MSG, &if_dev, "\tSECTOR\t%02x\n", data); | |
| break; | |
| case IF_DATA_REG: | |
| if_state.status &= ~IF_DRQ; | |
| if (((uptr->flags & (UNIT_ATT|UNIT_BUF)) == 0) || | |
| ((if_state.cmd & 0xf0) != IF_READ_SEC && | |
| (if_state.cmd & 0xf0) != IF_READ_SEC_M)) { | |
| /* Not attached, or not a read command */ | |
| switch (if_state.cmd & 0xf0) { | |
| case IF_READ_ADDR: | |
| /* Special state machine. */ | |
| switch (if_state.read_addr_ptr++) { | |
| case 0: | |
| if_state.data = if_state.track; | |
| break; | |
| case 1: | |
| if_state.data = if_state.side; | |
| break; | |
| case 2: | |
| if_state.data = if_state.sector; | |
| break; | |
| case 3: | |
| if_state.data = 2; /* 512 byte */ | |
| break; | |
| case 4: | |
| /* TODO: Checksum */ | |
| if_state.data = 0; | |
| break; | |
| case 5: | |
| /* TODO: Checksum */ | |
| if_state.data = 0; | |
| if_state.read_addr_ptr = 0; | |
| break; | |
| } | |
| } | |
| sim_debug(READ_MSG, &if_dev, "\tDATA\t%02x\n", if_state.data); | |
| return if_state.data; | |
| } | |
| pos = if_buf_offset(); | |
| data = fbuf[pos + if_sec_ptr++]; | |
| sim_debug(READ_MSG, &if_dev, "\tDATA\t%02x\n", data); | |
| if (if_sec_ptr >= IF_SECTOR_SIZE) { | |
| if_sec_ptr = 0; | |
| } | |
| break; | |
| default: | |
| data = 0xffu; // Compiler warning | |
| break; | |
| } | |
| return data; | |
| } | |
| /* Handle the most recently received command */ | |
| void if_handle_command() | |
| { | |
| uint32 delay_ms = 0; | |
| uint32 head_switch_delay = 0; | |
| uint32 head_load_delay = 0; | |
| if_sec_ptr = 0; | |
| /* We're starting a new command. */ | |
| if_state.status = IF_BUSY; | |
| /* Clear read addr state */ | |
| if_state.read_addr_ptr = 0; | |
| switch(if_state.cmd & 0xf0) { | |
| case IF_RESTORE: | |
| case IF_SEEK: | |
| case IF_STEP: | |
| case IF_STEP_T: | |
| case IF_STEP_IN: | |
| case IF_STEP_IN_T: | |
| case IF_STEP_OUT: | |
| case IF_STEP_OUT_T: | |
| if_state.cmd_type = 1; | |
| if (if_state.cmd & IF_H_FLAG) { | |
| head_load_delay = IF_HLD_DELAY; | |
| } | |
| break; | |
| case IF_READ_SEC: | |
| case IF_READ_SEC_M: | |
| case IF_WRITE_SEC: | |
| case IF_WRITE_SEC_M: | |
| if_state.cmd_type = 2; | |
| if (((if_state.cmd & IF_U_FLAG) >> 1) != if_state.side) { | |
| head_switch_delay = IF_HSW_DELAY; | |
| if_state.side = (if_state.cmd & IF_U_FLAG) >> 1; | |
| } | |
| break; | |
| case IF_READ_ADDR: | |
| case IF_READ_TRACK: | |
| case IF_WRITE_TRACK: | |
| if_state.cmd_type = 3; | |
| if (((if_state.cmd & IF_U_FLAG) >> 1) != if_state.side) { | |
| head_switch_delay = IF_HSW_DELAY; | |
| if_state.side = (if_state.cmd & IF_U_FLAG) >> 1; | |
| } | |
| break; | |
| case IF_FORCE_INT: | |
| if_state.cmd_type = 4; | |
| break; | |
| } | |
| switch(if_state.cmd & 0xf0) { | |
| case IF_RESTORE: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tRestore\n", if_state.cmd); | |
| /* Reset HLT */ | |
| if_state.status &= ~IF_HEAD_LOADED; | |
| /* If head should be loaded immediately, do so now */ | |
| if (if_state.cmd & IF_H_FLAG) { | |
| if_state.status |= IF_HEAD_LOADED; | |
| } | |
| if (if_state.track == 0) { | |
| if_state.status |= IF_TK_0; | |
| if_state.track = 1; /* Kind of a gross hack */ | |
| } | |
| if (if_state.cmd & IF_V_FLAG) { | |
| delay_ms = (IF_STEP_DELAY * if_state.track) + IF_VERIFY_DELAY; | |
| } else { | |
| delay_ms = IF_STEP_DELAY * if_state.track; | |
| } | |
| if_activate(delay_ms); | |
| if_state.data = 0; | |
| if_state.track = 0; | |
| break; | |
| case IF_STEP: | |
| case IF_STEP_T: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tStep\n", if_state.cmd); | |
| if_activate(IF_STEP_DELAY); | |
| if_state.track = (uint8) min(max((int) if_state.track + if_state.step_dir, 0), 0x4f); | |
| break; | |
| case IF_STEP_IN: | |
| case IF_STEP_IN_T: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tStep In\n", if_state.cmd); | |
| if_state.step_dir = IF_STEP_IN_DIR; | |
| if_state.track = (uint8) max((int) if_state.track + if_state.step_dir, 0); | |
| if_activate(IF_STEP_DELAY); | |
| break; | |
| case IF_STEP_OUT: | |
| case IF_STEP_OUT_T: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tStep Out\n", if_state.cmd); | |
| if_state.step_dir = IF_STEP_OUT_DIR; | |
| if_state.track = (uint8) min((int) if_state.track + if_state.step_dir, 0x4f); | |
| if_activate(IF_STEP_DELAY); | |
| break; | |
| case IF_SEEK: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tSeek\n", if_state.cmd); | |
| /* Reset HLT */ | |
| if_state.status &= ~IF_HEAD_LOADED; | |
| /* If head should be loaded immediately, do so now */ | |
| if (if_state.cmd & IF_H_FLAG) { | |
| if_state.status |= IF_HEAD_LOADED; | |
| } | |
| /* Save the direction for stepping */ | |
| if (if_state.data > if_state.track) { | |
| if_state.step_dir = IF_STEP_IN_DIR; | |
| } else if (if_state.data < if_state.track) { | |
| if_state.step_dir = IF_STEP_OUT_DIR; | |
| } | |
| /* The new track is in the data register */ | |
| if (if_state.data > IF_TRACK_COUNT-1) { | |
| if_state.data = IF_TRACK_COUNT-1; | |
| } | |
| if (if_state.data == 0) { | |
| if_state.status |= IF_TK_0; | |
| } else { | |
| if_state.status &= ~(IF_TK_0); | |
| } | |
| delay_ms = (uint32) abs(if_state.data - if_state.track); | |
| if (delay_ms == 0) { | |
| delay_ms++; | |
| } | |
| if (if_state.cmd & IF_V_FLAG) { | |
| if_activate((IF_STEP_DELAY * delay_ms) + IF_VERIFY_DELAY + head_load_delay); | |
| } else { | |
| if_activate((IF_STEP_DELAY * delay_ms) + head_load_delay); | |
| } | |
| if_state.track = if_state.data; | |
| break; | |
| case IF_READ_SEC: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tRead Sector %d/%d/%d\n", | |
| if_state.cmd, if_state.track, if_state.side, if_state.sector); | |
| /* We set DRQ right away to request the transfer. */ | |
| if_state.drq = TRUE; | |
| if_state.status |= IF_DRQ; | |
| if (if_state.cmd & IF_E_FLAG) { | |
| if_activate(IF_R_DELAY + IF_VERIFY_DELAY + head_switch_delay); | |
| } else { | |
| if_activate(IF_R_DELAY + head_switch_delay); | |
| } | |
| break; | |
| case IF_READ_SEC_M: | |
| assert(0); | |
| case IF_WRITE_SEC: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tWrite Sector %d/%d/%d\n", | |
| if_state.cmd, if_state.track, if_state.side, if_state.sector); | |
| /* We set DRQ right away to request the transfer. */ | |
| if_state.drq = TRUE; | |
| if_state.status |= IF_DRQ; | |
| if (if_state.cmd & IF_E_FLAG) { | |
| if_activate(IF_W_DELAY + IF_VERIFY_DELAY + head_switch_delay); | |
| } else { | |
| if_activate(IF_W_DELAY + head_switch_delay); | |
| } | |
| break; | |
| case IF_WRITE_SEC_M: | |
| assert(0); | |
| break; | |
| case IF_READ_ADDR: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tRead Address\n", if_state.cmd); | |
| if_state.drq = TRUE; | |
| if_state.status |= IF_DRQ; | |
| if_activate(IF_R_DELAY); | |
| break; | |
| case IF_READ_TRACK: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tRead Track\n", if_state.cmd); | |
| assert(0); /* NOT YET IMPLEMENTED */ | |
| break; | |
| case IF_WRITE_TRACK: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tWrite Track\n", if_state.cmd); | |
| /* Set DRQ */ | |
| if_state.drq = TRUE; | |
| if_state.status |= IF_DRQ; | |
| if (if_state.cmd & IF_E_FLAG) { | |
| if_activate(IF_W_DELAY + IF_VERIFY_DELAY + head_switch_delay); | |
| } else { | |
| if_activate(IF_W_DELAY + head_switch_delay); | |
| } | |
| break; | |
| case IF_FORCE_INT: | |
| sim_debug(EXECUTE_MSG, &if_dev, "\tCOMMAND\t%02x\tForce Interrupt\n", if_state.cmd); | |
| if_state.status = 0; | |
| if (if_state.track == 0) { | |
| if_state.status |= (IF_TK_0|IF_HEAD_LOADED); | |
| } | |
| if ((if_state.cmd & 0xf) == 0) { | |
| if_cancel_pending_irq(); | |
| if_clear_irq(); /* TODO: Confirm this is right */ | |
| } else if ((if_state.cmd & 0x8) == 0x8) { | |
| if_state.status |= IF_DRQ; | |
| if_set_irq(); | |
| } | |
| break; | |
| } | |
| } | |
| void if_write(uint32 pa, uint32 val, size_t size) | |
| { | |
| UNIT *uptr; | |
| uint8 reg; | |
| uint32 pos; | |
| uint8 *fbuf; | |
| val = val & 0xff; | |
| uptr = &(if_dev.units[0]); | |
| reg = (uint8) (pa - IFBASE); | |
| fbuf = (uint8 *)uptr->filebuf; | |
| switch (reg) { | |
| case IF_CMD_REG: | |
| if_state.cmd = (uint8) val; | |
| /* Writing to the command register always de-asserts the IRQ line */ | |
| if_clear_irq(); | |
| if_handle_command(); | |
| break; | |
| case IF_TRACK_REG: | |
| if_state.track = (uint8) val; | |
| sim_debug(WRITE_MSG, &if_dev, "\tTRACK\t%02x\n", val); | |
| break; | |
| case IF_SECTOR_REG: | |
| if_state.sector = (uint8) val; | |
| sim_debug(WRITE_MSG, &if_dev, "\tSECTOR\t%02x\n", val); | |
| break; | |
| case IF_DATA_REG: | |
| if_state.data = (uint8) val; | |
| sim_debug(WRITE_MSG, &if_dev, "\tDATA\t%02x\n", val); | |
| if ((uptr->flags & UNIT_ATT) == 0) { | |
| /* Not attached */ | |
| break; | |
| } | |
| if ((if_state.cmd & 0xf0) == IF_WRITE_TRACK) { | |
| /* We intentionally ignore WRITE TRACK data, because | |
| * This is only used for low-level MFM formatting, | |
| * which we do not emulate. */ | |
| } else if ((if_state.cmd & 0xf0) == IF_WRITE_SEC || | |
| (if_state.cmd & 0xf0) == IF_WRITE_SEC_M) { | |
| /* Find the right offset, and update the value. */ | |
| pos = if_buf_offset(); | |
| fbuf[pos + if_sec_ptr++] = (uint8) val; | |
| if (if_sec_ptr >= IF_SECTOR_SIZE) { | |
| if_sec_ptr = 0; | |
| } | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| /* | |
| * Compute the offset of the currently selected C/H/S | |
| */ | |
| SIM_INLINE uint32 if_buf_offset() | |
| { | |
| uint32 pos; | |
| pos = IF_TRACK_SIZE * if_state.track * 2; | |
| if (if_state.side == 1) { | |
| pos += IF_TRACK_SIZE; | |
| } | |
| pos += IF_SECTOR_SIZE * (if_state.sector - 1); | |
| return pos; | |
| } | |
| void if_drq_handled() | |
| { | |
| if_state.status &= ~IF_DRQ; | |
| } |