RASCSI/cpp/devices/scsi_daynaport.cpp
Uwe Seimet d1936e9f53 NetBSD compatibility: Increase daynaport minimum packet size to 128 bytes (#1334) (#1335)
* NetBSD compatibility: Increase daynaport minimum packet size to 128 bytes
2024-05-01 16:15:42 +09:00

508 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2020 akuker
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) 2001-2006 (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;
}