diff --git a/Components/5380/DirectAccessDevice.cpp b/Components/5380/DirectAccessDevice.cpp index 52260bda7..3402dbe44 100644 --- a/Components/5380/DirectAccessDevice.cpp +++ b/Components/5380/DirectAccessDevice.cpp @@ -8,71 +8,34 @@ #include "DirectAccessDevice.hpp" -using namespace SCSI; +using namespace SCSI::Target; -DirectAccessDevice::DirectAccessDevice(Bus &bus, int scsi_id) : - bus_(bus), - scsi_id_mask_(BusState(1 << scsi_id)), - scsi_bus_device_id_(bus.add_device()) { - bus.add_observer(this); -} +CommandArguments::CommandArguments(const std::vector &data) : data_(data) {} -void DirectAccessDevice::scsi_bus_did_change(Bus *, BusState new_state) { - /* - "The target determines that it is selected when the SEL# signal - and its SCSI ID bit are active and the BSY# and I#/O signals - are false. It then asserts the signal within a selection abort - time." - */ - - // A reset always takes precedence over anything else ongoing. - if(new_state & Line::Reset) { - phase_ = Phase::AwaitingSelection; - bus_state_ = DefaultBusState; - bus_.set_device_output(scsi_bus_device_id_, bus_state_); - return; - } - - switch(phase_) { - case Phase::AwaitingSelection: - if( - (new_state & scsi_id_mask_) && - ((new_state & (Line::SelectTarget | Line::Busy | Line::Input)) == Line::SelectTarget) - ) { - printf("Selected\n"); - phase_ = Phase::Command; - bus_state_ |= Line::Busy; // Initiate the command phase: request a command byte. - bus_.set_device_output(scsi_bus_device_id_, bus_state_); - } else { - if(!(new_state & scsi_id_mask_)) printf("No ID mask\n"); - else printf("Not SEL|~BSY|~IO"); - } - break; - - case Phase::Command: - // Wait for select to be disabled before beginning the control phase proper. - if((new_state & Line::SelectTarget)) return; - - bus_state_ |= Line::Control; - - switch(new_state & (Line::Request | Line::Acknowledge)) { - // If request and acknowledge are both enabled, grab a byte and cancel the request. - case Line::Request | Line::Acknowledge: - bus_state_ &= ~Line::Request; - printf("Got %02x maybe?\n", new_state & 0xff); - - // TODO: is the command phase over? - break; - - // The reset of request has caused the initiator to reset acknowledge, so it is now - // safe to request the next byte. - case 0: - bus_state_ |= Line::Request; - break; - - default: break; - } - bus_.set_device_output(scsi_bus_device_id_, bus_state_); - break; +uint32_t CommandArguments::address() { + switch(data_.size()) { + default: return 0; + case 6: + return + (uint32_t(data_[1]) << 16) | + (uint32_t(data_[2]) << 8) | + uint32_t(data_[3]); + case 10: + case 12: + return + (uint32_t(data_[1]) << 24) | + (uint32_t(data_[2]) << 16) | + (uint32_t(data_[3]) << 8) | + uint32_t(data_[4]); + } +} + +uint16_t CommandArguments::number_of_blocks() { + switch(data_.size()) { + default: return 0; + case 6: + return uint16_t(data_[4]); + case 10: + return uint16_t((data_[7] << 8) | data_[8]); } } diff --git a/Components/5380/DirectAccessDevice.hpp b/Components/5380/DirectAccessDevice.hpp index 9ff616f56..cb4807021 100644 --- a/Components/5380/DirectAccessDevice.hpp +++ b/Components/5380/DirectAccessDevice.hpp @@ -12,18 +12,79 @@ #include "SCSI.hpp" namespace SCSI { +namespace Target { /*! - Models a SCSI direct access device, ordinarily some sort of - hard drive. + Encapsulates the arguments supplied for a target SCSI command during + the command phase. An instance of TargetCommandArguments will be + supplied to the target whenever a function is called. */ -class DirectAccessDevice: public Bus::Observer { +class CommandArguments { + public: + CommandArguments(const std::vector &data); + + uint32_t address(); + uint16_t number_of_blocks(); + + private: + const std::vector &data_; +}; + +/*! + Executors contain device-specific logic; when the target has completed + the command phase it will call the appropriate method on its executor, + supplying it with the command's arguments. + + If you implement a method, you should push a result and return @c true. + Return @c false if you do not implement a method (or, just inherit from + the basic executor below, and don't implement anything you don't support). +*/ +struct Executor { + /* Group 0 commands. */ + bool test_unit_ready(const CommandArguments &) { return false; } + bool rezero_unit(const CommandArguments &) { return false; } + bool request_sense(const CommandArguments &) { return false; } + bool format_unit(const CommandArguments &) { return false; } + bool seek(const CommandArguments &) { return false; } + bool reserve_unit(const CommandArguments &) { return false; } + bool release_unit(const CommandArguments &) { return false; } + bool read_diagnostic(const CommandArguments &) { return false; } + bool write_diagnostic(const CommandArguments &) { return false; } + bool inquiry(const CommandArguments &) { return false; } + + /* Group 0/1 commands. */ + bool read(const CommandArguments &) { return false; } + bool write(const CommandArguments &) { return false; } + + /* Group 1 commands. */ + bool read_capacity(const CommandArguments &) { return false; } + bool write_and_verify(const CommandArguments &) { return false; } + bool verify(const CommandArguments &) { return false; } + bool search_data_equal(const CommandArguments &) { return false; } + bool search_data_high(const CommandArguments &) { return false; } + bool search_data_low(const CommandArguments &) { return false; } + + /* Group 5 commands. */ + bool set_block_limits(const CommandArguments &) { return false; } + void reset_block_limits(const CommandArguments &) {} +}; + +/*! + A template for any SCSI target; provides the necessary bus glue to + receive and respond to commands. Specific targets should be implemented + as Executors. +*/ +template class Target: public Bus::Observer { public: /*! - Instantiates a direct access device attached to @c bus, + Instantiates a target attached to @c bus, with SCSI ID @c scsi_id — a number in the range 0 to 7. + + Received commands will be handed to the Executor to perform. */ - DirectAccessDevice(Bus &bus, int scsi_id); + Target(Bus &bus, int scsi_id); + + Executor executor; private: void scsi_bus_did_change(Bus *, BusState new_state) final; @@ -37,8 +98,16 @@ class DirectAccessDevice: public Bus::Observer { Command } phase_ = Phase::AwaitingSelection; BusState bus_state_ = DefaultBusState; + + void begin_command(uint8_t first_byte); + std::vector command_; + size_t command_pointer_ = 0; + bool dispatch_command(); }; +#import "TargetImplementation.hpp" + +} } #endif /* DirectAccessDevice_hpp */ diff --git a/Components/5380/TargetImplementation.hpp b/Components/5380/TargetImplementation.hpp new file mode 100644 index 000000000..85ba81762 --- /dev/null +++ b/Components/5380/TargetImplementation.hpp @@ -0,0 +1,144 @@ +// +// TargetImplementation.hpp +// Clock Signal +// +// Created by Thomas Harte on 19/08/2019. +// Copyright © 2019 Thomas Harte. All rights reserved. +// + +template Target::Target(Bus &bus, int scsi_id) : + bus_(bus), + scsi_id_mask_(BusState(1 << scsi_id)), + scsi_bus_device_id_(bus.add_device()) { + bus.add_observer(this); +} + +template void Target::scsi_bus_did_change(Bus *, BusState new_state) { + /* + "The target determines that it is selected when the SEL# signal + and its SCSI ID bit are active and the BSY# and I#/O signals + are false. It then asserts the signal within a selection abort + time." + */ + + // A reset always takes precedence over anything else ongoing. + if(new_state & Line::Reset) { + phase_ = Phase::AwaitingSelection; + bus_state_ = DefaultBusState; + bus_.set_device_output(scsi_bus_device_id_, bus_state_); + return; + } + + switch(phase_) { + case Phase::AwaitingSelection: + if( + (new_state & scsi_id_mask_) && + ((new_state & (Line::SelectTarget | Line::Busy | Line::Input)) == Line::SelectTarget) + ) { + printf("Selected\n"); + phase_ = Phase::Command; + bus_state_ |= Line::Busy; // Initiate the command phase: request a command byte. + bus_.set_device_output(scsi_bus_device_id_, bus_state_); + } else { + if(!(new_state & scsi_id_mask_)) printf("No ID mask\n"); + else printf("Not SEL|~BSY|~IO"); + } + break; + + case Phase::Command: + // Wait for select to be disabled before beginning the control phase proper. + if((new_state & Line::SelectTarget)) return; + + bus_state_ |= Line::Control; + + switch(new_state & (Line::Request | Line::Acknowledge)) { + // If request and acknowledge are both enabled, grab a byte and cancel the request. + case Line::Request | Line::Acknowledge: + bus_state_ &= ~Line::Request; + + if(command_.empty()) { + begin_command(uint8_t(new_state)); + + // TODO: if(command_.empty()) signal_error_somehow(); + } else { + command_[command_pointer_] = uint8_t(new_state); + ++command_pointer_; + if(command_pointer_ == command_.size()) { + dispatch_command(); + + // TODO: if(!dispatch_command()) signal_error_somehow(); + } + } + break; + + // The reset of request has caused the initiator to reset acknowledge, so it is now + // safe to request the next byte. + case 0: + bus_state_ |= Line::Request; + break; + + default: break; + } + bus_.set_device_output(scsi_bus_device_id_, bus_state_); + break; + } +} + +template void Target::begin_command(uint8_t first_byte) { + // The logic below is valid for SCSI-1. TODO: other SCSIs. + switch(first_byte >> 5) { + default: break; + case 0: command_.resize(6); break; // Group 0 commands: 6 bytes long. + case 1: command_.resize(10); break; // Group 1 commands: 10 bytes long. + case 5: command_.resize(12); break; // Group 5 commands: 12 bytes long. + } + + // Store the first byte if it was recognised. + if(!command_.empty()) { + command_[0] = first_byte; + command_pointer_ = 1; + } +} + +template bool Target::dispatch_command() { + + CommandArguments arguments(command_); + +#define G0(x) x +#define G1(x) (0x20|x) +#define G5(x) (0xa0|x) + + switch(command_[0]) { + default: return false; + + case G0(0x00): return executor.test_unit_ready(arguments); + case G0(0x01): return executor.rezero_unit(arguments); + case G0(0x03): return executor.request_sense(arguments); + case G0(0x04): return executor.format_unit(arguments); + case G0(0x08): return executor.read(arguments); + case G0(0x0a): return executor.write(arguments); + case G0(0x0b): return executor.seek(arguments); + case G0(0x16): return executor.reserve_unit(arguments); + case G0(0x17): return executor.release_unit(arguments); + case G0(0x1c): return executor.read_diagnostic(arguments); + case G0(0x1d): return executor.write_diagnostic(arguments); + case G0(0x12): return executor.inquiry(arguments); + + case G1(0x05): return executor.read_capacity(arguments); + case G1(0x08): return executor.read(arguments); + case G1(0x0a): return executor.write(arguments); + case G1(0x0e): return executor.write_and_verify(arguments); + case G1(0x0f): return executor.verify(arguments); + case G1(0x11): return executor.search_data_equal(arguments); + case G1(0x10): return executor.search_data_high(arguments); + case G1(0x12): return executor.search_data_low(arguments); + + case G5(0x09): return executor.set_block_limits(arguments); + } + +#undef G0 +#undef G1 +#undef G5 + + return false; +} diff --git a/Components/5380/ncr5380.hpp b/Components/5380/ncr5380.hpp index b9b551a7b..fbdd7054c 100644 --- a/Components/5380/ncr5380.hpp +++ b/Components/5380/ncr5380.hpp @@ -54,9 +54,9 @@ class NCR5380 final: public ClockingHint::Source { private: // TEMPORARY. For development expediency, the 5380 owns its own - // SCSI bus and direct access device. These will be moved out. + // SCSI bus and target. These will be moved out. SCSI::Bus bus_; - SCSI::DirectAccessDevice device_; + SCSI::Target::Target device_; const int clock_rate_; size_t device_id_; diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 58c2dc5f7..ca0b1184a 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1501,6 +1501,7 @@ 4BE3231520532AA7006EF799 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; 4BE3231620532BED006EF799 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; 4BE3231720532CC0006EF799 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; + 4BE64626230B939B00C36C50 /* TargetImplementation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TargetImplementation.hpp; sourceTree = ""; }; 4BE76CF822641ED300ACD6FA /* QLTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = QLTests.mm; sourceTree = ""; }; 4BE7C9161E3D397100A5496D /* TIA.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TIA.cpp; sourceTree = ""; }; 4BE7C9171E3D397100A5496D /* TIA.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TIA.hpp; sourceTree = ""; }; @@ -3338,6 +3339,7 @@ 4B89BCFB23024BB500EA0782 /* SCSI.hpp */, 4BB8F5252308E5A50015C2A6 /* DirectAccessDevice.cpp */, 4BB8F5262308E5A50015C2A6 /* DirectAccessDevice.hpp */, + 4BE64626230B939B00C36C50 /* TargetImplementation.hpp */, ); path = 5380; sourceTree = "";