mirror of
https://github.com/akuker/RASCSI.git
synced 2026-04-19 20:16:52 +00:00
98f9292690
* NetBSD compatibility: Increase daynaport minimum packet size to 128 bytes
508 lines
18 KiB
C++
508 lines
18 KiB
C++
//---------------------------------------------------------------------------
|
|
//
|
|
// 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 <sstream>
|
|
#include <iomanip>
|
|
|
|
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;
|
|
}
|
|
|
|
void SCSIDaynaPort::CleanUp()
|
|
{
|
|
tap.CleanUp();
|
|
}
|
|
|
|
vector<uint8_t> SCSIDaynaPort::InquiryInternal() const
|
|
{
|
|
vector<uint8_t> 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<uint8_t>& 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<int>(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<const uint8_t> 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<int>(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<uint8_t>& buf) const
|
|
{
|
|
memcpy(buf.data(), &m_scsi_link_stats, sizeof(m_scsi_link_stats));
|
|
|
|
return static_cast<int>(min(sizeof(m_scsi_link_stats), static_cast<size_t>(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<PbStatistics> SCSIDaynaPort::GetStatistics() const
|
|
{
|
|
vector<PbStatistics> 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;
|
|
}
|