/* rk11_cpp: RK11 UNIBUS controller Copyright Vulcan Inc. 2019 via Living Computers: Museum + Labs, Seattle, WA. Contributed under the BSD 2-clause license. */ #include #include #include #include "unibus.h" #include "unibusadapter.hpp" #include "unibusdevice.hpp" #include "storagecontroller.hpp" #include "rk11.hpp" #include "rk05.hpp" rk11_c::rk11_c() : storagecontroller_c(), _new_command_ready(false) { // static config name.value = "rk"; type_name.value = "RK11"; log_label = "rk"; // base addr, intr-vector, intr level set_default_bus_params(0777400, 10, 0220, 5) ; // The RK11 controller has seven registers, // We allocate 8 because one address in the address space is unused. register_count = 8; // Drive Status register (read-only) RKDS_reg = &(this->registers[0]); // @ base addr strcpy(RKDS_reg->name, "RKDS"); RKDS_reg->active_on_dati = false; // no controller state change RKDS_reg->active_on_dato = false; RKDS_reg->reset_value = 0; RKDS_reg->writable_bits = 0x0000; // read only // Error Register (read-only) RKER_reg = &(this->registers[1]); // @ base addr + 2 strcpy(RKER_reg->name, "RKER"); RKER_reg->active_on_dati = false; // no controller state change RKER_reg->active_on_dato = false; RKER_reg->reset_value = 0; RKER_reg->writable_bits = 0x0000; // read only // Control Status Register (read/write) RKCS_reg = &(this->registers[2]); // @ base addr + 4 strcpy(RKCS_reg->name, "RKCS"); RKCS_reg->active_on_dati = false; RKCS_reg->active_on_dato = true; RKCS_reg->reset_value = 0x0080; // RDY high after INIT RKCS_reg->writable_bits = 0x0f7f; // Word Count Register (read/write) RKWC_reg = &(this->registers[3]); // @ base addr + 6 strcpy(RKWC_reg->name, "RKWC"); RKWC_reg->active_on_dati = false; RKWC_reg->active_on_dato = false; RKWC_reg->reset_value = 0; RKWC_reg->writable_bits = 0xffff; // Current Bus Address Register (read/write) RKBA_reg = &(this->registers[4]); // @ base addr + 10 strcpy(RKBA_reg->name, "RKBA"); RKBA_reg->active_on_dati = false; RKBA_reg->active_on_dato = false; RKBA_reg->reset_value = 0; RKBA_reg->writable_bits = 0xffff; // Disk Address Register (write only?) RKDA_reg = &(this->registers[5]); // @ base addr + 12 strcpy(RKDA_reg->name, "RKDA"); RKDA_reg->active_on_dati = false; RKDA_reg->active_on_dato = true; // To allow writes only when controller is in READY state. RKDA_reg->reset_value = 0; RKDA_reg->writable_bits = 0xffff; // Unused entry (@ Base addr + 14) // Data Buffer Register (read only) RKDB_reg = &(this->registers[7]); // @ base addr + 16 strcpy(RKDB_reg->name, "RKDB"); RKDB_reg->active_on_dati = false; RKDB_reg->active_on_dato = false; RKDB_reg->reset_value = 0; RKDB_reg->writable_bits = 0x0000; // read only _rkda_drive = 0; // // Drive configuration: up to eight drives. // drivecount = 8; for (uint32_t i=0; iunitno.value = i; drive->name.value = name.value + std::to_string(i); drive->log_label = drive->name.value; drive->parent = this; storagedrives.push_back(drive); } } rk11_c::~rk11_c() { for (uint32_t i=0; iDMA(dma_request, true, UNIBUS_CONTROL_DATO, request.address, request.buffer + request.count - 1, 1); request.timeout = !dma_request.success ; } else { // Read FROM unibus memory TO buffer, IBA on: // We read a single word from the unibus and fill the // entire buffer with this value. unibusadapter->DMA(dma_request, true, UNIBUS_CONTROL_DATI, request.address, request.buffer, 1); request.timeout = !dma_request.success ; } } else { // Just a normal DMA transfer. if (request.write) { // Write FROM buffer TO unibus memory unibusadapter->DMA(dma_request, true, UNIBUS_CONTROL_DATO, request.address, request.buffer, request.count); request.timeout = !dma_request.success ; } else { // Read FROM unibus memory TO buffer unibusadapter->DMA(dma_request, true, UNIBUS_CONTROL_DATI, request.address, request.buffer, request.count); request.timeout = !dma_request.success ; } } // If an IBA DMA read from memory, we need to fill the request buffer // with the single word returned from memory by the DMA operation. if (request.iba && !request.write) { for(uint32_t i = 1; i < request.count; i++) { request.buffer[i] = request.buffer[0]; } } } // Background worker. // Handle device operations. void rk11_c::worker(unsigned instance) { UNUSED(instance) ; // only one worker_init_realtime_priority(rt_device); _worker_state = Worker_Idle; WorkerCommand command = { 0 }; bool do_interrupt = true; timeout_c timeout; while (!workers_terminate) { switch (_worker_state) { case Worker_Idle: { pthread_mutex_lock(&on_after_register_access_mutex); while (!_new_command_ready) { pthread_cond_wait( &on_after_register_access_cond, &on_after_register_access_mutex); } // // Make a local copy of the new command to execute. // command = _new_command; _new_command_ready = false; pthread_mutex_unlock(&on_after_register_access_mutex); // // Clear GO now that we've accepted the command. // _go = false; update_RKCS(); // // We will interrupt after completion of a command except // in certain error cases. // do_interrupt = true; // // Move to the execution state to start executing the command. // _worker_state = Worker_Execute; } break; case Worker_Execute: switch(command.function) { case Write: case Read: case Write_Check: { bool write = command.function == Write; bool read_format = command.function == Read && command.format; bool read = command.function == Read && !command.format; bool write_check = command.function == Write_Check; // We loop over the requested address range // and submit DMA requests one at a time, waiting for // their completion. uint16_t sectorBuffer[256]; uint16_t checkBuffer[256]; uint32_t current_address = command.address; int16_t current_count = -(int16_t)(get_register_dato_value(RKWC_reg)); bool abort = false; while(current_count > 0 && !abort) { // If a new command has been written in the CS register, abandon // this one ASAP. if (_new_command_ready) { DEBUG("Command canceled."); abort = true; continue; } // Validate that the current disk address is valid. if (!validate_seek()) { // The above check set error bits accordingly. // Just abort the transfer now. // Set OVR if we've gone off the end of the disk. if (_rkda_cyl > 202) { _ovr = true; // update_RKER(); } abort = true; continue; } // // Clear the buffer. This is only necessary because short writes // and reads expect the rest of the sector to be filled with zeroes. // memset(sectorBuffer, 0, sizeof(sectorBuffer)); if (read) { // Doing a normal read from disk: Grab the sector data and then // DMA it into memory. selected_drive()->read_sector( _rkda_cyl, _rkda_surface, _rkda_sector, sectorBuffer); } else if (read_format) { // Doing a Read Format: Read only the header word from the disk // and DMA that single word into memory. // We don't actually read the header word from disk since we don't // store header data in the disk image; we just fake it up -- // since we always seek correctly this is all that is required. // // The header is just the cylinder address, as in RKDA 05-12 (p. 3-9) sectorBuffer[0] = (_rkda_cyl << 5); } else if (write_check) { // Doing a Write Check: Grab the sector data from the disk into // the check buffer. selected_drive()->read_sector( _rkda_cyl, _rkda_surface, _rkda_sector, checkBuffer); } // // Normal Read, or Write/Write Format: DMA the data to/from memory, // etc. // // build the DMA transfer request: DMARequest request = { 0 }; request.address = current_address; request.count = (!read_format) ? min(static_cast(256) , current_count) : 1; request.write = !(write || write_check); // Inverted sense from disk action request.timeout = false; request.buffer = sectorBuffer; request.iba = command.iba; // And actually do the transfer. dma_transfer(request); // Check completion status -- if there was an error, // we'll abort and set the appropriate flags. if (request.timeout) { _nxm = true; _he = true; _err = true; // update_RKCS(); // update_RKER(); abort = true; } else { if (write) { // Doing a write to disk: Write the buffer DMA'd from // memory to the disk. selected_drive()->write_sector( _rkda_cyl, _rkda_surface, _rkda_sector, sectorBuffer); } else if (write_check) { // Compare check buffer with sector buffer, if there are // any discrepancies, set WCE and interrupt as necessary. for (int i = 0; i < request.count; i++) { if (sectorBuffer[i] != checkBuffer[i]) { _wce = true; _err = true; // update_RKER(); // update_RKCS(); if (_sse) { // Finish this transfer and abort. abort = true; break; } } } if (_wce && _sse) { // Control action stops on error after this transfer // if SSE (Stop on Soft Error) is set. abort = true; } } else // Read { // After read complete, set RKDB to the last word // read (this satisfies ZRKK): set_register_dati_value( RKDB_reg, sectorBuffer[request.count - 1], "RK11 READ"); } } // Transfer completed. Move to next and update registers. current_count -= request.count; set_register_dati_value( RKWC_reg, (uint16_t)(-current_count), __func__); if (!command.iba) { current_address += (request.count * 2); // Byte address set_register_dati_value( RKBA_reg, (uint16_t)current_address, __func__); _mex = ((current_address) >> 16) & 0x3; // update_RKCS(); } // Move to next disk address increment_RKDA(); // And go around, do it again. } // timeout.wait_us(100); DEBUG("R/W: Complete."); _worker_state = Worker_Finish; } break; case Read_Check: // // "...is identical to a normal Read function, except that no // NPRs occur. Only the checksum is calculated and compared // with the checksum read from the disk drive... it must be // performed on a whole-sector basis only." // Since all data is error free in our emulation, the only // thing we do here is increment RKDA the requisite number // of times and clear RKWC. // { uint32_t current_address = command.address; int16_t current_count = -(int16_t)(get_register_dato_value(RKWC_reg)); // TODO: could do all this without going through the motions. while (current_count > 0) { if (!validate_seek()) { // TODO: set RKER, etc. break; } current_count -= 256; // TODO: remove hardcoding. if (!command.iba) { current_address += (256 * 2); // Byte address set_register_dati_value( RKWC_reg, (uint16_t)(-current_count), __func__); _mex = ((current_address) >> 16) & 0x3; // update_RKCS(); } // Move to next disk address. increment_RKDA(); // And go around, do it again. } _worker_state = Worker_Finish; } break; case Seek: // // Per ZRKK, if IDE is set, an interrupt is raised at the beginning // of the seek, and another is raised when the seek is complete. // if (validate_seek()) { // Per ZRKK, ID bits in RKDS get cleared at the beginning of a seek. _id = 0; update_RKDS(); // Start the seek; this will complete asynchronously. selected_drive()->seek( _rkda_cyl); } else { do_interrupt = false; } _worker_state = Worker_Finish; break; case Drive_Reset: if (check_drive_present()) { selected_drive()->drive_reset(); } else { do_interrupt = false; } _worker_state = Worker_Finish; break; default: INFO("Unhandled function %d.", command.function); // For now we just move to the Finish state so that future // commands can execute. _worker_state = Worker_Finish; break; } break; case Worker_Finish: // Set RDY, interrupt (if enabled) and go back to the Idle state. // We do this so that the update of ER, CS regs and the interrupt // are atomic w.r.t. RKCS access (diagnostic code polls CS and will // start a new operation immediately, lowering RDY before we invoke // the interrupt, causing behavior diagnostics do not expect.) pthread_mutex_lock(&on_after_register_access_mutex); _rdy = true; update_RKER(); update_RKCS(); // Interrupt unless specified not to. if (do_interrupt) { assert(_rdy); invoke_interrupt(); } pthread_mutex_unlock(&on_after_register_access_mutex); _worker_state = Worker_Idle; break; } } } bool rk11_c::validate_seek(void) { bool error = false; if (_rkda_sector > 11) { _nxs = true; error = true; } if (_rkda_cyl > 202) { _nxc = true; error = true; } if (!check_drive_present()) { _nxd = true; error = true; } if (error) { // Update the RKER register // update_RKER(); // Set the Control/Status register HE and ERR registers. _he = true; _err = true; // update_RKCS(); // Do not interrupt here, caller will do so based on // our return value. // invoke_interrupt(); } return !error; } void rk11_c::increment_RKDA() { _rkda_sector++; if (_rkda_sector > 11) { _rkda_sector = 0; _rkda_surface++; if (_rkda_surface > 1) { _rkda_surface = 0; _rkda_cyl++; } } update_RKDA(); } // // process DATI/DATO access to the RK11's "active" registers. // !! called asynchronuously by PRU, with SSYN asserted and blocking UNIBUS. // The time between PRU event and program flow into this callback // is determined by ARM Linux context switch // // UNIBUS DATO cycles let dati_flipflops "flicker" outside of this proc: // do not read back dati_flipflops. void rk11_c::on_after_register_access( unibusdevice_register_t *device_reg, uint8_t unibus_control) { UNUSED(unibus_control); // The RK11 has only one "active" register, RKCS. // When "GO" bit is set, kick off an operation and clear the // RDY bit. switch(device_reg->index) { case 2: // RKCS _go = !!(RKCS_reg->active_dato_flipflops & 0x1); _function = (RKCS_reg->active_dato_flipflops & 0xe) >> 1; _mex = (RKCS_reg->active_dato_flipflops & 0x30) >> 4; _ide = !!(RKCS_reg->active_dato_flipflops & 0x40); _sse = !!(RKCS_reg->active_dato_flipflops & 0x100); _exb = !!(RKCS_reg->active_dato_flipflops & 0x200); _fmt = !!(RKCS_reg->active_dato_flipflops & 0x400); _iba = !!(RKCS_reg->active_dato_flipflops & 0x800); // // GO bit is set: // "Causes the control to carry out the function contained in bits 01 through 03 of the // "RKCS (Function). Remains set until the control actually begins to respond to GO, which // may take from 1us to 3.3ms, depending on the current operation of the selected disk drive..." // TODO: what happens if RDY is low and GO is high? if (_go) { bool error = false; // // Check for PGE (Programming Error): use of FMT with a function other than Read or Write. // if (_fmt && (_function != Read && _function != Write && _function != Control_Reset)) { _pge = true; _he = true; _err = true; // GO gets cleared _go = false; // update_RKER(); // update_RKCS(); // error = true; } else { switch(_function) { case Control_Reset: // // "The Control Reset function initializes all internal registers and flip-flops // and clears all the bits of the seven programmable registers except RKCS 07 (RDY) // which it sets, and RKDS 01 through 11, which are not affected. // reset_controller(); break; case Write_Lock: // // "Write-protects a selected disk drive until the condition is overridden by the // operation of the corresponding WT PROT switch on the disk drive..." // selected_drive()->set_write_protect(true); _scp = false; // update_RKCS(); break; default: // All other commands take significant time and happen on the worker thread. // First check: // If this is not a Drive Reset command and the drive is not ready or has taken // a fault, we will abort, set the appropriate error bits and interrupt. // if (_function != Drive_Reset && !check_drive_ready()) { _dre = true; // update_RKER(); _he = true; _err = true; _scp = false; _go = false; // update_RKCS(); // INFO("setting DRE, fn %o dr rdy %d rws rdy %d", _function, // selected_drive()->get_drive_ready(), // selected_drive()->get_rws_ready()); // invoke_interrupt(); error = true; break; } // If this is an attempt to address a nonexistent (not present or not // loaded) drive, this triggers an NXD error. if (!check_drive_present()) { _nxd = true; _he = true; _err = true; _scp = false; _go = false; // update_RKCS(); // update_RKER(); // invoke_interrupt(); error = true; break; } // Clear RDY, SCP bits: pthread_mutex_lock(&on_after_register_access_mutex); _rdy = false; _scp = false; // update_RKCS(); // // Stow command data for this operation so that if RKCS gets changed // mid-op weird things won't happen. // _new_command.address = get_register_dato_value(RKBA_reg) | (_mex << 16); _new_command.drive = selected_drive(); _new_command.function = _function; _new_command.interrupt = _ide; _new_command.stop_on_soft_error = _sse; _new_command.format = _fmt; _new_command.iba = _iba; _new_command_ready = true; // // Wake the worker. // pthread_cond_signal(&on_after_register_access_cond); pthread_mutex_unlock(&on_after_register_access_mutex); break; } } update_RKER(); update_RKCS(); if (error) { assert(_rdy); invoke_interrupt(); } } else { // Stow the value in the register. update_RKCS(); // If IDE is set (interrupt on done) and RDY is set, we will interrupt. if (_rdy) { invoke_interrupt(); } } break; case 5: // RKDA // // Writes are only accepted when RDY is high. // if (_rdy) { uint32_t oldDrive = _rkda_drive; _rkda_sector = (RKDA_reg->active_dato_flipflops & 0xf); _rkda_surface = (RKDA_reg->active_dato_flipflops & 0x10) >> 4; _rkda_cyl = (RKDA_reg->active_dato_flipflops & 0x1fe0) >> 5; _rkda_drive = (RKDA_reg->active_dato_flipflops & 0xe000) >> 13; update_RKDA(); // Pick up new drive's status if drive ID changed. if (_rkda_drive != oldDrive) { update_RKDS(); } } break; default: // This should never happen. assert(false); break; } } bool rk11_c::check_drive_ready(void) { if (!selected_drive()->get_drive_ready() || selected_drive()->get_seek_incomplete() || selected_drive()->get_drive_unsafe() || selected_drive()->get_drive_power_low()) { return false; } else { return true; } } bool rk11_c::check_drive_present(void) { return (selected_drive()->get_drive_ready()); } void rk11_c::on_drive_status_changed(storagedrive_c *drive) { // only update if drive reporting a change // is the same as currently selected drive in RKDA. if (drive->unitno.value == selected_drive()->unitno.value) { update_RKDS(); } // If interrupts are enabled and the drive has completed a seek, // interrupt now. Note that the call to get_search_complete() has // the side effect (eww) of resetting the drive's SCP bit, so we do it // first (so it always gets cleared even if we're not interrupting.) if (dynamic_cast(drive)->get_search_complete() && _ide) { // Set SCP to indicate that this interrupt was due to a previous // Seek or Drive Reset function. _scp = true; _id = drive->unitno.value; update_RKDS(); update_RKCS(); invoke_interrupt(); } } void rk11_c::update_RKER(void) { unsigned short new_RKER = (_wce ? 0x0001 : 0x0000) | (_cse ? 0x0002 : 0x0000) | (_nxs ? 0x0020 : 0x0000) | (_nxc ? 0x0040 : 0x0000) | (_nxd ? 0x0080 : 0x0000) | (_te ? 0x0100 : 0x0000) | (_dlt ? 0x0200 : 0x0000) | (_nxm ? 0x0400 : 0x0000) | (_pge ? 0x0800 : 0x0000) | (_ske ? 0x1000 : 0x0000) | (_wlo ? 0x2000 : 0x0000) | (_ovr ? 0x4000 : 0x0000) | (_dre ? 0x8000 : 0x0000); set_register_dati_value( RKER_reg, new_RKER, "update_RKER"); } void rk11_c::update_RKCS(void) { unsigned short new_RKCS = (_go ? 0x0001 : 0x0000) | (_function << 1) | (_mex << 4) | (_ide ? 0x0040 : 0x0000) | (_rdy ? 0x0080 : 0x0000) | (_sse ? 0x0100 : 0x0000) | (_exb ? 0x0200 : 0x0000) | (_fmt ? 0x0400 : 0x0000) | (_iba ? 0x0800 : 0x0000) | (_scp ? 0x2000 : 0x0000) | (_he ? 0x4000 : 0x0000) | (_err ? 0x8000 : 0x0000); set_register_dati_value( RKCS_reg, new_RKCS, "update_RKCS"); } void rk11_c::update_RKDS(void) { rk05_c* drive = selected_drive(); uint32_t sectorCounter = drive->get_sector_counter(); bool sceqsa = sectorCounter == _rkda_sector; bool wps = drive->get_write_protect(); bool rwsrdy = drive->get_rws_ready(); bool dry = drive->get_drive_ready(); bool sok = drive->get_sector_counter_ok(); bool sin = drive->get_seek_incomplete(); bool dru = drive->get_drive_unsafe(); bool rk05 = drive->get_rk05_disk_online(); bool dpl = drive->get_drive_power_low(); unsigned short new_RKDS = sectorCounter | (sceqsa ? 0x0010 : 0x0000) | (wps ? 0x0020 : 0x0000) | (rwsrdy ? 0x0040 : 0x0000) | (dry ? 0x0080 : 0x0000) | (sok ? 0x0100 : 0x0000) | (sin ? 0x0200 : 0x0000) | (dru ? 0x0400 : 0x0000) | (rk05 ? 0x0800 : 0x0000) | (dpl ? 0x1000 : 0x0000) | (_id << 13); set_register_dati_value( RKDS_reg, new_RKDS, "update_RKDS"); } void rk11_c::update_RKDA(void) { unsigned short new_RKDA = _rkda_sector | (_rkda_surface << 4) | (_rkda_cyl << 5) | (_rkda_drive << 13); set_register_dati_value( RKDA_reg, new_RKDA, "update_RKDA"); } void rk11_c::invoke_interrupt(void) { // // Invoke an interrupt if the IDE bit of RKCS is set. // if (_ide) { unibusadapter->INTR(intr_request, NULL, 0); // todo: link to interupt register } } void rk11_c::reset_controller(void) { // This will reset the DATI values to their defaults. // We then need to reset our copy of the values to correspond. reset_unibus_registers(); // // Clear all RKER bits. // _wce = false; _cse = false; _nxs = false; _nxc = false; _nxd = false; _te = false; _dlt = false; _nxm = false; _pge = false; _ske = false; _wlo = false; _ovr = false; _dre = false; update_RKER(); // // Clear all RKCS bits except RDY. // _go = false; _function = 0; _mex = 0; _ide = false; _rdy = true; _sse = false; _exb = false; _fmt = false; _iba = false; _scp = false; _he = false; _err = false; update_RKCS(); // // Clear all RKDA bits. // _rkda_sector = 0; _rkda_surface = 0; _rkda_cyl = 0; _rkda_drive = 0; update_RKDA(); // // Update RKDS bits to match the newly selected drive (drive 0) // _id = 0; update_RKDS(); // // Clear RKWC // set_register_dati_value( RKWC_reg, 0, "reset_controller"); // // Clear RKBA // set_register_dati_value( RKBA_reg, 0, "reset_controller"); } void rk11_c::on_power_changed(void) { storagecontroller_c::on_power_changed(); if (power_down) { // power-on defaults reset_controller(); } } // UNIBUS INIT: clear all registers void rk11_c::on_init_changed(void) { // write all registers to "reset-values" if (init_asserted) { reset_controller(); } storagecontroller_c::on_init_changed(); } rk05_c* rk11_c::selected_drive(void) { return dynamic_cast(storagedrives[_rkda_drive]); }