Эх сурвалжийг харах

Merge pull request #487 from ZuluSCSI/dev_msc_initiator

USB MSC initiator mode support
Alex Perez 10 сар өмнө
parent
commit
1141580b91

+ 4 - 0
README.md

@@ -160,6 +160,10 @@ LED indications in initiator mode:
 The firmware retries reads up to 5 times and attempts to skip any sectors that have problems.
 Any read errors are logged into `zululog.txt`.
 
+Alternatively, if SD card is not inserted, the SCSI drive will be available to PC through USB connection.
+The drive shows up as mass storage device (USB MSC).
+Currently this primarily supports SCSI harddrives, but basic read access is supported for CD-ROM and other SCSI devices.
+
 Depending on hardware setup, you may need to mount diode `D205` and jumper `JP201` to supply `TERMPWR` to the SCSI bus.
 This is necessary if the drives do not supply their own SCSI terminator power.
 

+ 35 - 2
lib/ZuluSCSI_platform_RP2MCU/ZuluSCSI_platform.cpp

@@ -51,6 +51,10 @@ extern "C" {
 }
 #endif // ZULUSCSI_NETWORK
 
+#ifdef PLATFORM_MASS_STORAGE
+#include "ZuluSCSI_platform_msc.h"
+#endif
+
 #ifdef ENABLE_AUDIO_OUTPUT
 #  include "audio.h"
 #endif // ENABLE_AUDIO_OUTPUT
@@ -283,9 +287,24 @@ void platform_init()
 
     }
 # else
-    g_scsi_initiator = SETUP_TRUE == read_setup_ack_pin();
+    pin_setup_state_t dip_state = read_setup_ack_pin();
+    if (dip_state == SETUP_UNDETERMINED)
+    {
+        // This path is used for the few early RP2040 boards assembled with
+        // Diodes Incorporated 74LVT245B, which has higher bus hold
+        // current.
+        working_dip = false;
+        g_scsi_initiator = !gpio_get(DIP_INITIATOR); // Read fallback value
+    }
+    else
+    {
+        g_scsi_initiator = (SETUP_TRUE == dip_state);
+        termination = !gpio_get(DIP_TERM);
+    }
+
+    // dbglog DIP switch works in any case, as it does not have bus hold.
     dbglog = !gpio_get(DIP_DBGLOG);
-    termination = !gpio_get(DIP_TERM);
+    g_log_debug = dbglog;
 # endif
 #else
     delay(10);
@@ -319,9 +338,13 @@ void platform_init()
     else
     {
         logmsg("SCSI termination is determined by the DIP switch labeled \"TERM\"");
+
+#if defined(ZULUSCSI_PICO) || defined(ZULUSCSI_PICO_2)
         logmsg("Debug logging can only be enabled via INI file \"DEBUG=1\" under [SCSI] in zuluscsi.ini");
         logmsg("-- DEBUG DIP switch setting is ignored on ZuluSCSI Pico FS Rev. 2023b and 2023c boards");
         g_log_debug = false;
+#endif
+
     }
 #else
     g_log_debug = false;
@@ -627,6 +650,11 @@ static void usb_log_poll()
 {
 #ifndef PIO_FRAMEWORK_ARDUINO_NO_USB
     static uint32_t logpos = 0;
+
+#ifdef PLATFORM_MASS_STORAGE
+    if (platform_msc_lock_get()) return; // Avoid re-entrant USB events
+#endif
+
     if (Serial.availableForWrite())
     {
         // Retrieve pointer to log start and determine number of bytes available.
@@ -651,6 +679,11 @@ static void usb_log_poll()
 static void usb_input_poll()
 {
     #ifndef PIO_FRAMEWORK_ARDUINO_NO_USB
+
+#ifdef PLATFORM_MASS_STORAGE
+    if (platform_msc_lock_get()) return; // Avoid re-entrant USB events
+#endif
+
     // Caputure reboot key sequence
     static bool mass_storage_reboot_keyed = false;
     static bool basic_reboot_keyed = false;

+ 84 - 14
lib/ZuluSCSI_platform_RP2MCU/ZuluSCSI_platform_msc.cpp

@@ -28,11 +28,14 @@
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_msc.h"
+#include "ZuluSCSI_msc_initiator.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_settings.h"
 #include <class/msc/msc.h>
 #include <class/msc/msc_device.h>
 
+#include <pico/mutex.h>
+extern mutex_t __usb_mutex;
 
 #if CFG_TUD_MSC_EP_BUFSIZE < SD_SECTOR_SIZE
   #error "CFG_TUD_MSC_EP_BUFSIZE is too small! It needs to be at least 512 (SD_SECTOR_SIZE)"
@@ -44,6 +47,51 @@
 extern SdFs SD;
 static bool unitReady = false;
 
+static bool g_msc_lock; // To block re-entrant calls
+static bool g_msc_usb_mutex_held;
+
+void platform_msc_lock_set(bool block)
+{
+  if (block)
+  {
+    if (g_msc_lock)
+    {
+      logmsg("Re-entrant MSC lock!");
+      assert(false);
+    }
+
+    g_msc_usb_mutex_held = mutex_try_enter(&__usb_mutex, NULL); // Blocks USB IRQ if not already blocked
+    g_msc_lock = true; // Blocks platform USB polling
+  }
+  else
+  {
+    if (!g_msc_lock)
+    {
+      logmsg("MSC lock released when not held!");
+      assert(false);
+    }
+
+    g_msc_lock = false;
+
+    if (g_msc_usb_mutex_held)
+    {
+      g_msc_usb_mutex_held = false;
+      mutex_exit(&__usb_mutex);
+    }
+  }
+}
+
+bool platform_msc_lock_get()
+{
+  return g_msc_lock;
+}
+
+struct MSCScopedLock {
+public:
+  MSCScopedLock() {  platform_msc_lock_set(true); }
+  ~MSCScopedLock() { platform_msc_lock_set(false); }
+};
+
 /* return true if USB presence detected / eligble to enter CR mode */
 bool platform_sense_msc() {
 
@@ -111,6 +159,9 @@ void __USBInstallMassStorage() { }
 extern "C" void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8],
                         uint8_t product_id[16], uint8_t product_rev[4]) {
 
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_inquiry_cb(lun, vendor_id, product_id, product_rev);
+
   const char vid[] = "ZuluSCSI";
   const char pid[] = PLATFORM_PID; 
   const char rev[] = "1.0";
@@ -122,7 +173,11 @@ extern "C" void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8],
 
 // max LUN supported
 // we only have the one SD card
-extern "C" uint8_t tud_msc_get_maxlun_cb(void) {
+extern "C" uint8_t tud_msc_get_maxlun_cb(void)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_get_maxlun_cb();
+
   return 1; // number of LUNs supported
 }
 
@@ -131,6 +186,9 @@ extern "C" uint8_t tud_msc_get_maxlun_cb(void) {
 // otherwise this is not actually needed
 extern "C" bool tud_msc_is_writable_cb (uint8_t lun)
 {
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_is_writable_cb(lun);
+
   (void) lun;
   return unitReady;
 }
@@ -138,8 +196,8 @@ extern "C" bool tud_msc_is_writable_cb (uint8_t lun)
 // see https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf pg 221
 extern "C" bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject)
 {
-  (void) lun;
-  (void) power_condition;
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_start_stop_cb(lun, power_condition, start, load_eject);
 
   if (load_eject)  {
     if (start) {
@@ -154,16 +212,20 @@ extern "C" bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool
 }
 
 // return true if we are ready to service reads/writes
-extern "C" bool tud_msc_test_unit_ready_cb(uint8_t lun) {
-  (void) lun;
+extern "C" bool tud_msc_test_unit_ready_cb(uint8_t lun)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_test_unit_ready_cb(lun);
 
   return unitReady;
 }
 
 // return size in blocks and block size
 extern "C" void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count,
-                         uint16_t *block_size) {
-  (void) lun;
+                         uint16_t *block_size)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_capacity_cb(lun, block_count, block_size);
 
   *block_count = unitReady ? (SD.card()->sectorCount()) : 0;
   *block_size = SD_SECTOR_SIZE;
@@ -172,7 +234,10 @@ extern "C" void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count,
 // Callback invoked when received an SCSI command not in built-in list (below) which have their own callbacks
 // - READ_CAPACITY10, READ_FORMAT_CAPACITY, INQUIRY, MODE_SENSE6, REQUEST_SENSE, READ10 and WRITE10
 extern "C" int32_t tud_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer,
-                        uint16_t bufsize) {
+                        uint16_t bufsize)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_scsi_cb(lun, scsi_cmd, buffer, bufsize);
 
   const void *response = NULL;
   uint16_t resplen = 0;
@@ -210,7 +275,8 @@ extern "C" int32_t tud_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void
 extern "C" int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, 
                             void* buffer, uint32_t bufsize)
 {
-  (void) lun;
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_read10_cb(lun, lba, offset, buffer, bufsize);
 
   bool rc = SD.card()->readSectors(lba, (uint8_t*) buffer, bufsize/SD_SECTOR_SIZE);
 
@@ -224,8 +290,10 @@ extern "C" int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset,
 // Callback invoked when receive WRITE10 command.
 // Process data in buffer to disk's storage and return number of written bytes (must be multiple of block size)
 extern "C" int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset,
-                           uint8_t *buffer, uint32_t bufsize) {
-  (void) lun;
+                           uint8_t *buffer, uint32_t bufsize)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_read10_cb(lun, lba, offset, buffer, bufsize);
 
   bool rc = SD.card()->writeSectors(lba, buffer, bufsize/SD_SECTOR_SIZE);
 
@@ -237,8 +305,10 @@ extern "C" int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset
 
 // Callback invoked when WRITE10 command is completed (status received and accepted by host).
 // used to flush any pending cache to storage
-extern "C" void tud_msc_write10_complete_cb(uint8_t lun) {
-  (void) lun;
+extern "C" void tud_msc_write10_complete_cb(uint8_t lun)
+{
+  MSCScopedLock lock;
+  if (g_msc_initiator) return init_msc_write10_complete_cb(lun);
 }
 
-#endif
+#endif

+ 7 - 1
lib/ZuluSCSI_platform_RP2MCU/ZuluSCSI_platform_msc.h

@@ -37,4 +37,10 @@ bool platform_run_msc();
 /* perform any cleanup tasks for the MSC-specific functionality */
 void platform_exit_msc();
 
-#endif
+/* Block re-entrant msc poll calls.
+   This avoids starting another command handler while first one is running.
+   */
+void platform_msc_lock_set(bool block);
+bool platform_msc_lock_get();
+
+#endif

+ 33 - 11
src/ZuluSCSI.cpp

@@ -58,6 +58,7 @@
 #include "ZuluSCSI_settings.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_initiator.h"
+#include "ZuluSCSI_msc_initiator.h"
 #include "ZuluSCSI_msc.h"
 #include "ROMDrive.h"
 
@@ -65,7 +66,7 @@ SdFs SD;
 FsFile g_logfile;
 bool g_rawdrive_active;
 static bool g_romdrive_active;
-static bool g_sdcard_present;
+bool g_sdcard_present;
 
 #ifndef SD_SPEED_CLASS_WARN_BELOW
 #define SD_SPEED_CLASS_WARN_BELOW 10
@@ -955,7 +956,7 @@ static void firmware_update()
 
 // Place all the setup code that requires the SD card to be initialized here
 // Which is pretty much everything after platform_init and and platform_late_init
-static void zuluscsi_setup_sd_card()
+static void zuluscsi_setup_sd_card(bool wait_for_card = true)
 {
 
   g_sdcard_present = mountSDCard();
@@ -980,11 +981,18 @@ static void zuluscsi_setup_sd_card()
       blinkStatus(BLINK_ERROR_NO_SD_CARD);
       platform_reset_watchdog();
       g_sdcard_present = mountSDCard();
-    } while (!g_sdcard_present);
+    } while (!g_sdcard_present && wait_for_card);
     blink_cancel();
     LED_OFF();
 
-    logmsg("SD card init succeeded after retry");
+    if (g_sdcard_present)
+    {
+      logmsg("SD card init succeeded after retry");
+    }
+    else
+    {
+      logmsg("Continuing without SD card");
+    }
   }
 
   firmware_update();
@@ -1062,12 +1070,18 @@ extern "C" void zuluscsi_setup(void)
 {
   platform_init();
   platform_late_init();
-  zuluscsi_setup_sd_card();
+
+  bool is_initiator = false;
+#ifdef PLATFORM_HAS_INITIATOR_MODE
+  is_initiator = platform_is_initiator_mode_enabled();
+#endif
+
+  zuluscsi_setup_sd_card(!is_initiator);
 
 #ifdef PLATFORM_MASS_STORAGE
   static bool check_mass_storage = true;
-  if ((check_mass_storage && g_scsi_settings.getSystem()->enableUSBMassStorage)
-      || platform_rebooted_into_mass_storage())
+  if (((check_mass_storage && g_scsi_settings.getSystem()->enableUSBMassStorage)
+      || platform_rebooted_into_mass_storage()) && !is_initiator)
   {
     check_mass_storage = false;
 
@@ -1089,6 +1103,11 @@ extern "C" void zuluscsi_main_loop(void)
   static uint32_t sd_card_check_time = 0;
   static uint32_t last_request_time = 0;
 
+  bool is_initiator = false;
+#ifdef PLATFORM_HAS_INITIATOR_MODE
+  is_initiator = platform_is_initiator_mode_enabled();
+#endif
+
   platform_reset_watchdog();
   platform_poll();
   diskEjectButtonUpdate(true);
@@ -1099,7 +1118,7 @@ extern "C" void zuluscsi_main_loop(void)
 #endif // ZULUSCSI_NETWORK
 
 #ifdef PLATFORM_HAS_INITIATOR_MODE
-  if (platform_is_initiator_mode_enabled())
+  if (is_initiator)
   {
     scsiInitiatorMainLoop();
     save_logfile();
@@ -1128,7 +1147,7 @@ extern "C" void zuluscsi_main_loop(void)
   {
     // Check SD card status for hotplug
     if (scsiDev.phase == BUS_FREE &&
-        (uint32_t)(millis() - sd_card_check_time) > 5000)
+        (uint32_t)(millis() - sd_card_check_time) > SDCARD_POLL_INTERVAL)
     {
       sd_card_check_time = millis();
       uint32_t ocr;
@@ -1143,8 +1162,11 @@ extern "C" void zuluscsi_main_loop(void)
     }
   }
 
-  if (!g_sdcard_present)
+  if (!g_sdcard_present && (uint32_t)(millis() - sd_card_check_time) > SDCARD_POLL_INTERVAL
+      && !g_msc_initiator)
   {
+    sd_card_check_time = millis();
+
     // Try to remount SD card
     do
     {
@@ -1166,6 +1188,6 @@ extern "C" void zuluscsi_main_loop(void)
         platform_reset_watchdog();
         platform_poll();
       }
-    } while (!g_sdcard_present && !g_romdrive_active);
+    } while (!g_sdcard_present && !g_romdrive_active && !is_initiator);
   }
 }

+ 3 - 0
src/ZuluSCSI_config.h

@@ -54,6 +54,9 @@
 #endif
 #define LOG_SAVE_INTERVAL_MS 1000
 
+// How often to check for SD card presence
+#define SDCARD_POLL_INTERVAL 5000
+
 // Watchdog timeout
 // Watchdog will first issue a bus reset and if that does not help, crashdump.
 #define WATCHDOG_BUS_RESET_TIMEOUT 15000

+ 35 - 0
src/ZuluSCSI_initiator.cpp

@@ -28,6 +28,8 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_initiator.h"
+#include "ZuluSCSI_msc_initiator.h"
+#include "ZuluSCSI_msc.h"
 #include <ZuluSCSI_platform.h>
 #include <minIni.h>
 #include "SdFat.h"
@@ -61,6 +63,9 @@ bool scsiInitiatorReadCapacity(int target_id, uint32_t *sectorcount, uint32_t *s
 
 #else
 
+// From ZuluSCSI.cpp
+extern bool g_sdcard_present;
+
 /*************************************
  * High level initiator mode logic   *
  *************************************/
@@ -137,6 +142,11 @@ void scsiInitiatorInit()
 
 }
 
+int scsiInitiatorGetOwnID()
+{
+    return g_initiator_state.initiator_id;
+}
+
 // Update progress bar LED during transfers
 static void scsiInitiatorUpdateLed()
 {
@@ -202,6 +212,31 @@ void scsiInitiatorMainLoop()
         scsiHostPhyReset();
     }
 
+#ifdef PLATFORM_MASS_STORAGE
+    if (g_msc_initiator)
+    {
+        poll_msc_initiator();
+        platform_run_msc();
+        return;
+    }
+    else
+    {
+        if (!g_sdcard_present || ini_getbool("SCSI", "InitiatorMSC", false, CONFIGFILE))
+        {
+            logmsg("Entering USB MSC initiator mode");
+            platform_enter_msc();
+            setup_msc_initiator();
+            return;
+        }
+    }
+#endif
+
+    if (!g_sdcard_present)
+    {
+        // Wait for SD card
+        return;
+    }
+
     if (!g_initiator_state.imaging)
     {
         // Scan for SCSI drives one at a time

+ 3 - 0
src/ZuluSCSI_initiator.h

@@ -34,6 +34,9 @@ void scsiInitiatorInit();
 
 void scsiInitiatorMainLoop();
 
+// Get the SCSI ID used by the initiator itself
+int scsiInitiatorGetOwnID();
+
 // Select target and execute SCSI command
 int scsiInitiatorRunCommand(int target_id,
                             const uint8_t *command, size_t cmdLen,

+ 514 - 0
src/ZuluSCSI_msc_initiator.cpp

@@ -0,0 +1,514 @@
+/* Initiator mode USB Mass Storage Class connection.
+ * This file binds platform-specific MSC routines to the initiator mode
+ * SCSI bus interface. The call structure is modeled after TinyUSB, but
+ * should be usable with other USB libraries.
+ *
+ * ZuluSCSI™ - Copyright (c) 2023 Rabbit Hole Computing™
+ *
+ * This file is licensed under the GPL version 3 or any later version. 
+ * It is derived from cdrom.c in SCSI2SD V6
+ *
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include "ZuluSCSI_config.h"
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_log_trace.h"
+#include "ZuluSCSI_initiator.h"
+#include "ZuluSCSI_platform_msc.h"
+#include <scsi.h>
+#include <ZuluSCSI_platform.h>
+#include <minIni.h>
+#include "SdFat.h"
+
+bool g_msc_initiator;
+
+#ifndef PLATFORM_HAS_INITIATOR_MODE
+
+bool setup_msc_initiator() { return false; }
+void poll_msc_initiator() {}
+
+void init_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) {}
+uint8_t init_msc_get_maxlun_cb(void) { return 0; }
+bool init_msc_is_writable_cb (uint8_t lun) { return false; }
+bool init_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) { return false; }
+bool init_msc_test_unit_ready_cb(uint8_t lun) { return false; }
+void init_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) {}
+int32_t init_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer, uint16_t bufsize) {return -1;}
+int32_t init_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) {return -1;}
+int32_t init_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { return -1;}
+void init_msc_write10_complete_cb(uint8_t lun) {}
+
+#else
+
+// If there are multiple SCSI devices connected, they are mapped into LUNs for host.
+static struct {
+    int target_id;
+    uint32_t sectorsize;
+    uint32_t sectorcount;
+} g_msc_initiator_targets[NUM_SCSIID];
+static int g_msc_initiator_target_count;
+
+// Prefetch next sector in main loop while USB is transferring previous one.
+static struct {
+    uint8_t *prefetch_buffer; // Buffer to use for storing the data
+    uint32_t prefetch_bufsize;
+    uint32_t prefetch_lba; // First sector to fetch
+    int prefetch_target_id; // Target to read from
+    size_t prefetch_sectorcount; // Number of sectors to fetch
+    size_t prefetch_sectorsize;
+    bool prefetch_done; // True after prefetch is complete
+
+    // Periodic status reporting to log output
+    uint32_t status_prev_time;
+    uint32_t status_interval;
+    uint32_t status_reqcount;
+    uint32_t status_bytecount;
+} g_msc_initiator_state;
+
+static int do_read6_or_10(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize, void *buffer);
+
+static void scan_targets()
+{
+    int initiator_id = scsiInitiatorGetOwnID();
+    uint8_t inquiry_data[36] = {0};
+    g_msc_initiator_target_count = 0;
+    for (int target_id = 0; target_id < NUM_SCSIID; target_id++)
+    {
+        if (target_id == initiator_id) continue;
+
+        if (scsiTestUnitReady(target_id))
+        {
+            uint32_t sectorcount, sectorsize;
+
+            bool inquiryok =
+                scsiStartStopUnit(target_id, true) &&
+                scsiInquiry(target_id, inquiry_data) &&
+                scsiInitiatorReadCapacity(target_id, &sectorcount, &sectorsize);
+
+            char vendor_id[9] = {0};
+            char product_id[17] = {0};
+            memcpy(vendor_id, &inquiry_data[8], 8);
+            memcpy(product_id, &inquiry_data[16], 16);
+
+            if (inquiryok)
+            {
+                logmsg("Found SCSI drive with ID ", target_id, ": ", vendor_id, " ", product_id);
+                g_msc_initiator_targets[g_msc_initiator_target_count].target_id = target_id;
+                g_msc_initiator_targets[g_msc_initiator_target_count].sectorcount = sectorcount;
+                g_msc_initiator_targets[g_msc_initiator_target_count].sectorsize = sectorsize;
+                g_msc_initiator_target_count++;
+            }
+            else
+            {
+                logmsg("Detected SCSI device with ID ", target_id, ", but failed to get inquiry response, skipping");
+            }
+        }
+    }
+}
+
+bool setup_msc_initiator()
+{
+    logmsg("SCSI Initiator: activating USB MSC mode");
+    g_msc_initiator = true;
+
+    if (!ini_getbool("SCSI", "InitiatorMSCDisablePrefetch", false, CONFIGFILE))
+    {
+        // We can use the device mode buffer for prefetching data in initiator mode
+        g_msc_initiator_state.prefetch_buffer = scsiDev.data;
+        g_msc_initiator_state.prefetch_bufsize = sizeof(scsiDev.data);
+    }
+
+    g_msc_initiator_state.status_interval = ini_getl("SCSI", "InitiatorMSCStatusInterval", 5000, CONFIGFILE);
+
+    scsiInitiatorInit();
+
+    // Scan for targets
+    scan_targets();
+
+    logmsg("SCSI Initiator: found " , g_msc_initiator_target_count, " SCSI drives");
+    return g_msc_initiator_target_count > 0;
+}
+
+void poll_msc_initiator()
+{
+    if (g_msc_initiator_target_count == 0)
+    {
+        // Scan for targets until we find one
+        scan_targets();
+    }
+
+    uint32_t time_now = millis();
+    uint32_t delta = time_now - g_msc_initiator_state.status_prev_time;
+    if (g_msc_initiator_state.status_interval > 0 &&
+        delta > g_msc_initiator_state.status_interval)
+    {
+        if (g_msc_initiator_state.status_reqcount > 0)
+        {
+            logmsg("USB MSC: ", (int)g_msc_initiator_state.status_reqcount, " commands, ",
+                   (int)(g_msc_initiator_state.status_bytecount / delta), " kB/s");
+        }
+
+        g_msc_initiator_state.status_reqcount = 0;
+        g_msc_initiator_state.status_bytecount = 0;
+        g_msc_initiator_state.status_prev_time = time_now;
+    }
+
+
+    platform_poll();
+    platform_msc_lock_set(true); // Cannot handle new MSC commands while running prefetch
+    if (g_msc_initiator_state.prefetch_sectorcount > 0
+        && !g_msc_initiator_state.prefetch_done)
+    {
+        LED_ON();
+
+        dbgmsg("Prefetch ", (int)g_msc_initiator_state.prefetch_lba, " + ",
+                (int)g_msc_initiator_state.prefetch_sectorcount, "x",
+                (int)g_msc_initiator_state.prefetch_sectorsize);
+        // Read next block while USB is transferring
+        int status = do_read6_or_10(g_msc_initiator_state.prefetch_target_id,
+                                    g_msc_initiator_state.prefetch_lba,
+                                    g_msc_initiator_state.prefetch_sectorcount,
+                                    g_msc_initiator_state.prefetch_sectorsize,
+                                    g_msc_initiator_state.prefetch_buffer);
+        if (status == 0)
+        {
+            g_msc_initiator_state.prefetch_done = true;
+        }
+        else
+        {
+            logmsg("Prefetch of sector ", g_msc_initiator_state.prefetch_lba, " failed: status ", status);
+            g_msc_initiator_state.prefetch_sectorcount = 0;
+        }
+
+        LED_OFF();
+    }
+    platform_msc_lock_set(false);
+}
+
+static int get_target(uint8_t lun)
+{
+    if (lun >= g_msc_initiator_target_count)
+    {
+        logmsg("Host requested access to non-existing lun ", (int)lun);
+        return 0;
+    }
+    else
+    {
+        return g_msc_initiator_targets[lun].target_id;
+    }
+}
+
+void init_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4])
+{
+    LED_ON();
+    g_msc_initiator_state.status_reqcount++;
+
+    int target = get_target(lun);
+    uint8_t response[36] = {0};
+    bool status = scsiInquiry(target, response);
+    if (!status)
+    {
+        logmsg("SCSI Inquiry to target ", target, " failed");
+    }
+
+    memcpy(vendor_id, &response[8], 8);
+    memcpy(product_id, &response[16], 16);
+    memcpy(product_rev, &response[32], 4);
+
+    LED_OFF();
+}
+
+uint8_t init_msc_get_maxlun_cb(void)
+{
+    return g_msc_initiator_target_count;
+}
+
+bool init_msc_is_writable_cb (uint8_t lun)
+{
+    LED_ON();
+    g_msc_initiator_state.status_reqcount++;
+
+    int target = get_target(lun);
+    uint8_t command[6] = {0x1A, 0x08, 0, 0, 4, 0}; // MODE SENSE(6)
+    uint8_t response[4] = {0};
+    scsiInitiatorRunCommand(target, command, 6, response, 4, NULL, 0);
+
+    LED_OFF();
+    return (response[2] & 0x80) == 0; // Check write protected bit
+}
+
+bool init_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject)
+{
+    LED_ON();
+    g_msc_initiator_state.status_reqcount++;
+
+    int target = get_target(lun);
+    uint8_t command[6] = {0x1B, 0x1, 0, 0, 0, 0};
+    uint8_t response[4] = {0};
+    
+    if (start)
+    {
+        command[4] |= 1; // Start
+        command[1] = 0;  // Immediate
+    }
+
+    if (load_eject)
+    {
+        command[4] |= 2;
+    }
+
+    command[4] |= power_condition << 4;
+
+    int status = scsiInitiatorRunCommand(target,
+                                         command, sizeof(command),
+                                         response, sizeof(response),
+                                         NULL, 0);
+
+    if (status == 2)
+    {
+        uint8_t sense_key;
+        scsiRequestSense(target, &sense_key);
+        logmsg("START STOP UNIT on target ", target, " failed, sense key ", sense_key);
+    }
+
+    LED_OFF();
+
+    return status == 0;
+}
+
+bool init_msc_test_unit_ready_cb(uint8_t lun)
+{
+    g_msc_initiator_state.status_reqcount++;
+    return scsiTestUnitReady(get_target(lun));
+}
+
+void init_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size)
+{
+    g_msc_initiator_state.status_reqcount++;
+
+    uint32_t sectorcount = 0;
+    uint32_t sectorsize = 0;
+    scsiInitiatorReadCapacity(get_target(lun), &sectorcount, &sectorsize);
+    *block_count = sectorcount;
+    *block_size = sectorsize;
+}
+
+int32_t init_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer, uint16_t bufsize)
+{
+    LED_ON();
+    g_msc_initiator_state.status_reqcount++;
+
+    // NOTE: the TinyUSB API around free-form commands is not very good,
+    // this function could need improvement.
+    
+    // Figure out command length
+    static const uint8_t CmdGroupBytes[8] = {6, 10, 10, 6, 16, 12, 6, 6}; // From SCSI2SD
+    int cmdlen = CmdGroupBytes[scsi_cmd[0] >> 5];
+
+    int target = get_target(lun);
+    int status = scsiInitiatorRunCommand(target,
+                                         scsi_cmd, cmdlen,
+                                         NULL, 0,
+                                         (const uint8_t*)buffer, bufsize);
+
+    LED_OFF();
+
+    return status;
+}
+
+static int do_read6_or_10(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize, void *buffer)
+{
+    int status;
+
+    // Read6 command supports 21 bit LBA - max of 0x1FFFFF
+    // ref: https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf pg 134
+    if (start_sector < 0x1FFFFF && sectorcount <= 256)
+    {
+        // Use READ6 command for compatibility with old SCSI1 drives
+        uint8_t command[6] = {0x08,
+            (uint8_t)(start_sector >> 16),
+            (uint8_t)(start_sector >> 8),
+            (uint8_t)start_sector,
+            (uint8_t)sectorcount,
+            0x00
+        };
+
+        // Note: we must not call platform poll in the commands,
+        status = scsiInitiatorRunCommand(target_id, command, sizeof(command), (uint8_t*)buffer, sectorcount * sectorsize, NULL, 0);
+    }
+    else
+    {
+        // Use READ10 command for larger number of blocks
+        uint8_t command[10] = {0x28, 0x00,
+            (uint8_t)(start_sector >> 24), (uint8_t)(start_sector >> 16),
+            (uint8_t)(start_sector >> 8), (uint8_t)start_sector,
+            0x00,
+            (uint8_t)(sectorcount >> 8), (uint8_t)(sectorcount),
+            0x00
+        };
+
+        status = scsiInitiatorRunCommand(target_id, command, sizeof(command), (uint8_t*)buffer, sectorcount * sectorsize, NULL, 0);
+    }
+
+    return status;
+}
+
+int32_t init_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize)
+{
+    LED_ON();
+    int status = 0;
+
+    int target_id = get_target(lun);
+    int sectorsize = g_msc_initiator_targets[lun].sectorsize;
+    uint32_t sectorcount = bufsize / sectorsize;
+    uint32_t total_sectorcount = sectorcount;
+    uint32_t orig_lba = lba;
+
+    if (sectorcount == 0)
+    {
+        // Not enough buffer left for a full sector
+        return 0;
+    }
+
+    if (g_msc_initiator_state.prefetch_done)
+    {
+        int32_t offset = (int32_t)lba - (int32_t)g_msc_initiator_state.prefetch_lba;
+        uint8_t *dest = (uint8_t*)buffer;
+        while (offset >= 0 && offset < g_msc_initiator_state.prefetch_sectorcount && sectorcount > 0)
+        {
+            // Copy sectors from prefetch
+            memcpy(dest, g_msc_initiator_state.prefetch_buffer + sectorsize * offset, sectorsize);
+            dest += sectorsize;
+            offset += 1;
+            lba += 1;
+            sectorcount -= 1;
+        }
+    }
+
+    if (sectorcount > 0)
+    {
+        dbgmsg("USB Read command ", (int)orig_lba, " + ", (int)total_sectorcount, "x", (int)sectorsize,
+               " got ", (int)(total_sectorcount - sectorcount), " sectors from prefetch");
+        status = do_read6_or_10(target_id, lba, sectorcount, sectorsize, buffer);
+        lba += sectorcount;
+    }
+    else
+    {
+        dbgmsg("USB Read command ", (int)orig_lba, " + ", (int)total_sectorcount, "x", (int)sectorsize, " fully satisfied from prefetch");
+    }
+
+    g_msc_initiator_state.status_reqcount++;
+    g_msc_initiator_state.status_bytecount += total_sectorcount * sectorsize;
+    LED_OFF();
+
+    if (status != 0)
+    {
+        uint8_t sense_key;
+        scsiRequestSense(target_id, &sense_key);
+
+        logmsg("SCSI Initiator read failed: ", status, " sense key ", sense_key);
+        return -1;
+    }
+
+    if (lba + total_sectorcount <= g_msc_initiator_targets[lun].sectorcount)
+    {
+        int prefetch_sectorcount = total_sectorcount;
+        if (prefetch_sectorcount * sectorsize > g_msc_initiator_state.prefetch_bufsize)
+        {
+            prefetch_sectorcount = g_msc_initiator_state.prefetch_bufsize / sectorsize;
+        }
+
+        // Request prefetch of the next block while USB transfers the previous one
+        g_msc_initiator_state.prefetch_lba = lba;
+        g_msc_initiator_state.prefetch_target_id = target_id;
+        g_msc_initiator_state.prefetch_sectorcount = total_sectorcount;
+        g_msc_initiator_state.prefetch_sectorsize = sectorsize;
+        g_msc_initiator_state.prefetch_done = false;
+    }
+
+    return total_sectorcount * sectorsize;
+}
+
+int32_t init_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize)
+{
+    int status = -1;
+
+    int target_id = get_target(lun);
+    int sectorsize = g_msc_initiator_targets[lun].sectorsize;
+    uint32_t start_sector = lba;
+    uint32_t sectorcount = bufsize / sectorsize;
+
+    if (sectorcount == 0)
+    {
+        // Not a complete sector
+        return 0;
+    }
+
+    LED_ON();
+
+    // Write6 command supports 21 bit LBA - max of 0x1FFFFF
+    if (start_sector < 0x1FFFFF && sectorcount <= 256)
+    {
+        // Use WRITE6 command for compatibility with old SCSI1 drives
+        uint8_t command[6] = {0x0A,
+            (uint8_t)(start_sector >> 16),
+            (uint8_t)(start_sector >> 8),
+            (uint8_t)start_sector,
+            (uint8_t)sectorcount,
+            0x00
+        };
+
+        status = scsiInitiatorRunCommand(target_id, command, sizeof(command), NULL, 0, buffer, bufsize);
+    }
+    else
+    {
+        // Use WRITE10 command for larger number of blocks
+        uint8_t command[10] = {0x2A, 0x00,
+            (uint8_t)(start_sector >> 24), (uint8_t)(start_sector >> 16),
+            (uint8_t)(start_sector >> 8), (uint8_t)start_sector,
+            0x00,
+            (uint8_t)(sectorcount >> 8), (uint8_t)(sectorcount),
+            0x00
+        };
+
+        status = scsiInitiatorRunCommand(target_id, command, sizeof(command), NULL, 0, buffer, bufsize);
+    }
+
+    g_msc_initiator_state.status_reqcount++;
+    g_msc_initiator_state.status_bytecount += sectorcount * sectorsize;
+    LED_OFF();
+
+    if (status != 0)
+    {
+        uint8_t sense_key;
+        scsiRequestSense(target_id, &sense_key);
+
+        logmsg("SCSI Initiator write failed: ", status, " sense key ", sense_key);
+        return -1;
+    }
+
+    return sectorcount * sectorsize;
+}
+
+void init_msc_write10_complete_cb(uint8_t lun)
+{
+    (void)lun;
+}
+
+
+#endif

+ 46 - 0
src/ZuluSCSI_msc_initiator.h

@@ -0,0 +1,46 @@
+/* Initiator mode USB Mass Storage Class connection.
+ * This file binds platform-specific MSC routines to the initiator mode
+ * SCSI bus interface. The call structure is modeled after TinyUSB, but
+ * should be usable with other USB libraries.
+ *
+ * ZuluSCSI™ - Copyright (c) 2023 Rabbit Hole Computing™
+ *
+ * This file is licensed under the GPL version 3 or any later version. 
+ * It is derived from cdrom.c in SCSI2SD V6
+ *
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <stdint.h>
+
+// When true, initiator MSC mode is enabled.
+extern bool g_msc_initiator;
+bool setup_msc_initiator();
+void poll_msc_initiator();
+
+void init_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]);
+uint8_t init_msc_get_maxlun_cb(void);
+bool init_msc_is_writable_cb (uint8_t lun);
+bool init_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject);
+bool init_msc_test_unit_ready_cb(uint8_t lun);
+void init_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size);
+int32_t init_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer, uint16_t bufsize);
+int32_t init_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize);
+int32_t init_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize);
+void init_msc_write10_complete_cb(uint8_t lun);
+
+

+ 4 - 0
zuluscsi.ini

@@ -45,6 +45,10 @@
 #InitiatorMaxRetry = 5 #  number of retries on failed reads 0-255, default is 5
 #InitiatorImageHandling = 0 # 0: skip existing images, 1: create new image with incrementing suffix, 2: overwrite existing image
 
+#InitiatorMSC = 0 # Force USB MSC mode for initiator. By default enabled only if SD card is not inserted.
+#InitiatorMSCDisablePrefetch = 0 # Disable read prefetching in USB MSC mode
+#InitiatorMSCStatusInterval = 5000 # Periodically report access status to log
+
 #EnableCDAudio = 0 # 1: Enable CD audio - an external I2S DAC on the v1.2 is required
 
 # Settings that can be specified either per-device or for all devices.