//--------------------------------------------------------------------------- // // SCSI Target Emulator PiSCSI // for Raspberry Pi // // Copyright (C) 2020 akuker // Copyright (C) 2014-2020 GIMONS // Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) // Copyright (C) 2023 Uwe Seimet // // Licensed under the BSD 3-Clause License. // See LICENSE file in the project root folder. // // [ Emulation of the DaynaPort SCSI Link Ethernet interface ] // // This design is derived from the SLINKCMD.TXT file, as well as David Kuder's // Tiny SCSI Emulator // - SLINKCMD: http://www.bitsavers.org/pdf/apple/scsi/dayna/daynaPORT/SLINKCMD.TXT // - Tiny SCSI : https://hackaday.io/project/18974-tiny-scsi-emulator // // Additional documentation and clarification is available at the // following link: // - https://github.com/PiSCSI/piscsi/wiki/Dayna-Port-SCSI-Link // // Note: This requires a DaynaPort SCSI Link driver. //--------------------------------------------------------------------------- #include "shared/piscsi_exceptions.h" #include "scsi_command_util.h" #include "scsi_daynaport.h" #include #include using namespace scsi_defs; using namespace scsi_command_util; SCSIDaynaPort::SCSIDaynaPort(int lun) : PrimaryDevice(SCDP, lun) { SupportsParams(true); } bool SCSIDaynaPort::Init(const param_map& params) { PrimaryDevice::Init(params); AddCommand(scsi_command::eCmdTestUnitReady, [this] { TestUnitReady(); }); AddCommand(scsi_command::eCmdRead6, [this] { Read6(); }); AddCommand(scsi_command::eCmdWrite6, [this] { Write6(); }); AddCommand(scsi_command::eCmdRetrieveStats, [this] { RetrieveStatistics(); }); AddCommand(scsi_command::eCmdSetIfaceMode, [this] { SetInterfaceMode(); }); AddCommand(scsi_command::eCmdSetMcastAddr, [this] { SetMcastAddr(); }); AddCommand(scsi_command::eCmdEnableInterface, [this] { EnableInterface(); }); // The Daynaport needs to have a delay after the size/flags field of the read response. // In the MacOS driver, it looks like the driver is doing two "READ" system calls. SetSendDelay(DAYNAPORT_READ_HEADER_SZ); tap_enabled = tap.Init(GetParams()); if (!tap_enabled) { // Not terminating on regular Linux PCs is helpful for testing #if !defined(__x86_64__) && !defined(__X86__) return false; #endif } else { LogTrace("Tap interface created"); } Reset(); SetReady(true); SetReset(false); return true; } #if 0 void SCSIDaynaPort::CleanUp() { tap.CleanUp(); } #endif vector SCSIDaynaPort::InquiryInternal() const { vector buf = HandleInquiry(device_type::processor, scsi_level::scsi_2, false); // The Daynaport driver for the Mac expects 37 bytes: Increase additional length and // add a vendor-specific byte in order to satisfy this driver. buf[4]++; buf.push_back(0); return buf; } //--------------------------------------------------------------------------- // // READ // // Command: 08 00 00 LL LL XX (LLLL is data length, XX = c0 or 80) // Function: Read a packet at a time from the device (standard SCSI Read) // Type: Input; the following data is returned: // LL LL NN NN NN NN XX XX XX ... CC CC CC CC // where: // LLLL is normally the length of the packet (a 2-byte // big-endian hex value), including 4 trailing bytes // of CRC, but excluding itself and the flag field. // See below for special values // NNNNNNNN is a 4-byte flag field with the following meanings: // FFFFFFFF a packet has been dropped (?); in this case // the length field appears to be always 4000 // 00000010 there are more packets currently available // in SCSI/Link memory // 00000000 this is the last packet // XX XX ... is the actual packet // CCCCCCCC is the CRC // // Notes: // - When all packets have been retrieved successfully, a length field // of 0000 is returned; however, if a packet has been dropped, the // SCSI/Link will instead return a non-zero length field with a flag // of FFFFFFFF when there are no more packets available. This behaviour // seems to continue until a disable/enable sequence has been issued. // - The SCSI/Link apparently has about 6KB buffer space for packets. // //--------------------------------------------------------------------------- int SCSIDaynaPort::Read(cdb_t cdb, vector& buf, uint64_t) { int rx_packet_size = 0; const auto response = (scsi_resp_read_t*)buf.data(); const int requested_length = cdb[4]; LogTrace("Read maximum length: " + to_string(requested_length)); // At startup the host may send a READ(6) command with a sector count of 1 to read the root sector. // We should respond by going into the status mode with a code of 0x02. if (requested_length == 1) { return 0; } // Some of the packets we receive will not be for us. So, we'll keep pulling messages // until the buffer is empty, or we've read X times. (X is just a made up number) // TODO send_message_to_host is effctively always true bool send_message_to_host; int read_count = 0; while (read_count < MAX_READ_RETRIES) { read_count++; // The first 2 bytes are reserved for the length of the packet // The next 4 bytes are reserved for a flag field //rx_packet_size = m_tap.Rx(response->data); rx_packet_size = tap.Receive(&buf[DAYNAPORT_READ_HEADER_SZ]); // If we didn't receive anything, return size of 0 if (rx_packet_size <= 0) { LogTrace("No packet received"); response->length = 0; response->flags = read_data_flags_t::e_no_more_data; return DAYNAPORT_READ_HEADER_SZ; } byte_read_count += rx_packet_size; LogTrace("Packet Size " + to_string(rx_packet_size) + ", read count: " + to_string(read_count)); // This is a very basic filter to prevent unnecessary packets from // being sent to the SCSI initiator. send_message_to_host = false; // The following doesn't seem to work with unicast messages. Temporarily removing the filtering // functionality. /////// // Check if received packet destination MAC address matches the /////// // DaynaPort MAC. For IP packets, the mac_address will be the first 6 bytes /////// // of the data. /////// if (memcmp(response->data, m_mac_addr, 6) == 0) { /////// send_message_to_host = true; /////// } /////// // Check to see if this is a broadcast message /////// if (memcmp(response->data, m_bcast_addr, 6) == 0) { /////// send_message_to_host = true; /////// } /////// // Check to see if this is an AppleTalk Message /////// if (memcmp(response->data, m_apple_talk_addr, 6) == 0) { /////// send_message_to_host = true; /////// } send_message_to_host = true; // TODO: We should check to see if this message is in the multicast // configuration from SCSI command 0x0D if (!send_message_to_host) { stringstream s; s << "Received a packet that's not for me:" << setfill('0') << setw(2) << hex; for (int i = 0 ; i < 6; i++) { s << " $" << static_cast(response->data[i]); } LogDebug(s.str()); // If there are pending packets to be processed, we'll tell the host that the read // length was 0. if (!tap.HasPendingPackets()) { response->length = 0; response->flags = read_data_flags_t::e_no_more_data; return DAYNAPORT_READ_HEADER_SZ; } } else { // TODO: Need to do some sort of size checking. The buffer can easily overflow, probably. // response->length = rx_packet_size; // if(m_tap.PendingPackets()){ // response->flags = e_more_data_available; // } else { // response->flags = e_no_more_data; // } int size = rx_packet_size; if (size < 128) { // A frame must have at least 64 bytes for the Atari driver, see https://github.com/PiSCSI/piscsi/issues/619, // but also works with 128 bytes. // The NetBSD driver requires at least 128 bytes, see https://github.com/PiSCSI/piscsi/issues/1098. // The Mac driver is also fine with 128 bytes. // Note that this work-around breaks the checksum. As currently there are no known drivers // that care for the checksum it was decided to accept the broken checksum. // If a driver should pop up that breaks because of this, the work-around has to be re-evaluated. size = 128; } SetInt16(buf, 0, size); SetInt32(buf, 2, tap.HasPendingPackets() ? 0x10 : 0x00); // Return the packet size + 2 for the length + 4 for the flag field // The CRC was already appended by the ctapdriver return size + DAYNAPORT_READ_HEADER_SZ; } // If we got to this point, there are still messages in the queue, so // we should loop back and get the next one. } // end while response->length = 0; response->flags = read_data_flags_t::e_no_more_data; return DAYNAPORT_READ_HEADER_SZ; } //--------------------------------------------------------------------------- // // Write // // Command: 0a 00 00 LL LL XX (LLLL is data length, XX = 80 or 00) // Function: Write a packet at a time to the device (standard SCSI Write) // Type: Output; the format of the data to be sent depends on the value // of XX, as follows: // - if XX = 00, LLLL is the packet length, and the data to be sent // must be an image of the data packet // - if XX = 80, LLLL is the packet length + 8, and the data to be // sent is: // PP PP 00 00 XX XX XX ... 00 00 00 00 // where: // PPPP is the actual (2-byte big-endian) packet length // XX XX ... is the actual packet // //--------------------------------------------------------------------------- bool SCSIDaynaPort::Write(cdb_t cdb, span buf) { if (const int data_format = cdb[5]; data_format == 0x00) { const int data_length = GetInt16(cdb, 3); tap.Send(buf.data(), data_length); byte_write_count += data_length; LogTrace("Transmitted " + to_string(data_length) + " byte(s) (00 format)"); } else if (data_format == 0x80) { // The data length is specified in the first 2 bytes of the payload const int data_length = buf[1] + ((static_cast(buf[0]) & 0xff) << 8); tap.Send(&(buf.data()[4]), data_length); byte_write_count += data_length; LogTrace("Transmitted " + to_string(data_length) + "byte(s) (80 format)"); } else { stringstream s; s << "Unknown data format: " << setfill('0') << setw(2) << hex << data_format; LogWarn(s.str()); } GetController()->SetBlocks(0); return true; } //--------------------------------------------------------------------------- // // RetrieveStats // // Command: 09 00 00 00 12 00 // Function: Retrieve MAC address and device statistics // Type: Input; returns 18 (decimal) bytes of data as follows: // - bytes 0-5: the current hardware ethernet (MAC) address // - bytes 6-17: three long word (4-byte) counters (little-endian). // Notes: The contents of the three longs are typically zero, and their // usage is unclear; they are suspected to be: // - long #1: frame alignment errors // - long #2: CRC errors // - long #3: frames lost // //--------------------------------------------------------------------------- int SCSIDaynaPort::RetrieveStats(cdb_t cdb, vector& buf) const { memcpy(buf.data(), &m_scsi_link_stats, sizeof(m_scsi_link_stats)); return static_cast(min(sizeof(m_scsi_link_stats), static_cast(GetInt16(cdb, 3)))); } void SCSIDaynaPort::TestUnitReady() { // Always successful EnterStatusPhase(); } void SCSIDaynaPort::Read6() { // Get record number and block number const uint32_t record = GetInt24(GetController()->GetCmd(), 1) & 0x1fffff; GetController()->SetBlocks(1); // If any commands have a bogus control value, they were probably not // generated by the DaynaPort driver so ignore them if (GetController()->GetCmdByte(5) != 0xc0 && GetController()->GetCmdByte(5) != 0x80) { LogTrace("Control value: " + to_string(GetController()->GetCmdByte(5))); throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); } stringstream s; s << "READ(6) command, record: $" << setfill('0') << setw(8) << hex << record; LogTrace(s.str()); GetController()->SetLength(Read(GetController()->GetCmd(), GetController()->GetBuffer(), record)); LogTrace("Length is " + to_string(GetController()->GetLength())); // Set next block GetController()->SetNext(record + 1); EnterDataInPhase(); } void SCSIDaynaPort::Write6() const { // Ensure a sufficient buffer size (because it is not transfer for each block) GetController()->AllocateBuffer(DAYNAPORT_BUFFER_SIZE); const int data_format = GetController()->GetCmdByte(5); if (data_format == 0x00) { GetController()->SetLength(GetInt16(GetController()->GetCmd(), 3)); } else if (data_format == 0x80) { GetController()->SetLength(GetInt16(GetController()->GetCmd(), 3) + 8); } else { stringstream s; s << "Unknown data format: " << setfill('0') << setw(2) << hex << data_format; LogWarn(s.str()); } stringstream s; s << "Length: " << GetController()->GetLength() << ", format: $" << setfill('0') << setw(2) << hex << data_format; LogTrace(s.str()); if (GetController()->GetLength() <= 0) { throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); } // Set next block GetController()->SetBlocks(1); GetController()->SetNext(1); EnterDataOutPhase(); } void SCSIDaynaPort::RetrieveStatistics() const { GetController()->SetLength(RetrieveStats(GetController()->GetCmd(), GetController()->GetBuffer())); // Set next block GetController()->SetBlocks(1); GetController()->SetNext(1); EnterDataInPhase(); } //--------------------------------------------------------------------------- // // Set interface mode/Set MAC address // // Set Interface Mode (0c) // ----------------------- // Command: 0c 00 00 00 FF 80 (FF = 08 or 04) // Function: Allow interface to receive broadcast messages (FF = 04); the // function of (FF = 08) is currently unknown. // Type: No data transferred // Notes: This command is accepted by firmware 1.4a & 2.0f, but has no // effect on 2.0f, which is always capable of receiving broadcast // messages. In 1.4a, once broadcast mode is set, it remains set // until the interface is disabled. // // Set MAC Address (0c) // -------------------- // Command: 0c 00 00 00 FF 40 (FF = 08 or 04) // Function: Set MAC address // Type: Output; overrides built-in MAC address with user-specified // 6-byte value // Notes: This command is intended primarily for debugging/test purposes. // Disabling the interface resets the MAC address to the built-in // value. // //--------------------------------------------------------------------------- void SCSIDaynaPort::SetInterfaceMode() const { // Check whether this command is telling us to "Set Interface Mode" or "Set MAC Address" GetController()->SetLength(RetrieveStats(GetController()->GetCmd(), GetController()->GetBuffer())); switch(GetController()->GetCmdByte(5)){ case CMD_SCSILINK_SETMODE: // Not implemented, do nothing EnterStatusPhase(); break; case CMD_SCSILINK_SETMAC: GetController()->SetLength(6); EnterDataOutPhase(); break; default: stringstream s; s << "Unsupported SetInterface command: " << setfill('0') << setw(2) << hex << GetController()->GetCmdByte(5); LogWarn(s.str()); throw scsi_exception(sense_key::illegal_request, asc::invalid_command_operation_code); break; } } void SCSIDaynaPort::SetMcastAddr() const { GetController()->SetLength(GetController()->GetCmdByte(4)); if (GetController()->GetLength() == 0) { stringstream s; s << "Unsupported SetMcastAddr command: " << setfill('0') << setw(2) << hex << GetController()->GetCmdByte(2); LogWarn(s.str()); throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); } EnterDataOutPhase(); } //--------------------------------------------------------------------------- // // Enable or Disable the interface // // Command: 0e 00 00 00 00 XX (XX = 80 or 00) // Function: Enable (80) / disable (00) Ethernet interface // Type: No data transferred // Notes: After issuing an Enable, the initiator should avoid sending // any subsequent commands to the device for approximately 0.5 // seconds // //--------------------------------------------------------------------------- void SCSIDaynaPort::EnableInterface() const { if (GetController()->GetCmdByte(5) & 0x80) { if (const string error = tap.IpLink(true); !error.empty()) { LogWarn("Unable to enable the DaynaPort Interface: " + error); throw scsi_exception(sense_key::aborted_command); } tap.Flush(); LogInfo("The DaynaPort interface has been ENABLED"); } else { if (const string error = tap.IpLink(false); !error.empty()) { LogWarn("Unable to disable the DaynaPort Interface: " + error); throw scsi_exception(sense_key::aborted_command); } LogInfo("The DaynaPort interface has been DISABLED"); } EnterStatusPhase(); } vector SCSIDaynaPort::GetStatistics() const { vector statistics = PrimaryDevice::GetStatistics(); PbStatistics s; s.set_id(GetId()); s.set_unit(GetLun()); s.set_category(PbStatisticsCategory::CATEGORY_INFO); s.set_key(BYTE_READ_COUNT); s.set_value(byte_read_count); statistics.push_back(s); s.set_key(BYTE_WRITE_COUNT); s.set_value(byte_write_count); statistics.push_back(s); return statistics; }