Browse Source

Merge pull request #487 from ZuluSCSI/dev_msc_initiator

USB MSC initiator mode support
Alex Perez 10 months ago
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.
 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`.
 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.
 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.
 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
 #endif // ZULUSCSI_NETWORK
 
 
+#ifdef PLATFORM_MASS_STORAGE
+#include "ZuluSCSI_platform_msc.h"
+#endif
+
 #ifdef ENABLE_AUDIO_OUTPUT
 #ifdef ENABLE_AUDIO_OUTPUT
 #  include "audio.h"
 #  include "audio.h"
 #endif // ENABLE_AUDIO_OUTPUT
 #endif // ENABLE_AUDIO_OUTPUT
@@ -283,9 +287,24 @@ void platform_init()
 
 
     }
     }
 # else
 # 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);
     dbglog = !gpio_get(DIP_DBGLOG);
-    termination = !gpio_get(DIP_TERM);
+    g_log_debug = dbglog;
 # endif
 # endif
 #else
 #else
     delay(10);
     delay(10);
@@ -319,9 +338,13 @@ void platform_init()
     else
     else
     {
     {
         logmsg("SCSI termination is determined by the DIP switch labeled \"TERM\"");
         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 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");
         logmsg("-- DEBUG DIP switch setting is ignored on ZuluSCSI Pico FS Rev. 2023b and 2023c boards");
         g_log_debug = false;
         g_log_debug = false;
+#endif
+
     }
     }
 #else
 #else
     g_log_debug = false;
     g_log_debug = false;
@@ -627,6 +650,11 @@ static void usb_log_poll()
 {
 {
 #ifndef PIO_FRAMEWORK_ARDUINO_NO_USB
 #ifndef PIO_FRAMEWORK_ARDUINO_NO_USB
     static uint32_t logpos = 0;
     static uint32_t logpos = 0;
+
+#ifdef PLATFORM_MASS_STORAGE
+    if (platform_msc_lock_get()) return; // Avoid re-entrant USB events
+#endif
+
     if (Serial.availableForWrite())
     if (Serial.availableForWrite())
     {
     {
         // Retrieve pointer to log start and determine number of bytes available.
         // 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()
 static void usb_input_poll()
 {
 {
     #ifndef PIO_FRAMEWORK_ARDUINO_NO_USB
     #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
     // Caputure reboot key sequence
     static bool mass_storage_reboot_keyed = false;
     static bool mass_storage_reboot_keyed = false;
     static bool basic_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_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_msc.h"
 #include "ZuluSCSI_msc.h"
+#include "ZuluSCSI_msc_initiator.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_settings.h"
 #include "ZuluSCSI_settings.h"
 #include <class/msc/msc.h>
 #include <class/msc/msc.h>
 #include <class/msc/msc_device.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
 #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)"
   #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;
 extern SdFs SD;
 static bool unitReady = false;
 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 */
 /* return true if USB presence detected / eligble to enter CR mode */
 bool platform_sense_msc() {
 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],
 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]) {
                         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 vid[] = "ZuluSCSI";
   const char pid[] = PLATFORM_PID; 
   const char pid[] = PLATFORM_PID; 
   const char rev[] = "1.0";
   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
 // max LUN supported
 // we only have the one SD card
 // 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
   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
 // otherwise this is not actually needed
 extern "C" bool tud_msc_is_writable_cb (uint8_t lun)
 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;
   (void) lun;
   return unitReady;
   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
 // 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)
 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 (load_eject)  {
     if (start) {
     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
 // 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 unitReady;
 }
 }
 
 
 // return size in blocks and block size
 // return size in blocks and block size
 extern "C" void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count,
 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_count = unitReady ? (SD.card()->sectorCount()) : 0;
   *block_size = SD_SECTOR_SIZE;
   *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
 // 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
 // - 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,
 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;
   const void *response = NULL;
   uint16_t resplen = 0;
   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, 
 extern "C" int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, 
                             void* buffer, uint32_t bufsize)
                             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);
   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.
 // 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)
 // 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,
 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);
   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).
 // Callback invoked when WRITE10 command is completed (status received and accepted by host).
 // used to flush any pending cache to storage
 // 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 */
 /* perform any cleanup tasks for the MSC-specific functionality */
 void platform_exit_msc();
 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_settings.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_initiator.h"
 #include "ZuluSCSI_initiator.h"
+#include "ZuluSCSI_msc_initiator.h"
 #include "ZuluSCSI_msc.h"
 #include "ZuluSCSI_msc.h"
 #include "ROMDrive.h"
 #include "ROMDrive.h"
 
 
@@ -65,7 +66,7 @@ SdFs SD;
 FsFile g_logfile;
 FsFile g_logfile;
 bool g_rawdrive_active;
 bool g_rawdrive_active;
 static bool g_romdrive_active;
 static bool g_romdrive_active;
-static bool g_sdcard_present;
+bool g_sdcard_present;
 
 
 #ifndef SD_SPEED_CLASS_WARN_BELOW
 #ifndef SD_SPEED_CLASS_WARN_BELOW
 #define SD_SPEED_CLASS_WARN_BELOW 10
 #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
 // 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
 // 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();
   g_sdcard_present = mountSDCard();
@@ -980,11 +981,18 @@ static void zuluscsi_setup_sd_card()
       blinkStatus(BLINK_ERROR_NO_SD_CARD);
       blinkStatus(BLINK_ERROR_NO_SD_CARD);
       platform_reset_watchdog();
       platform_reset_watchdog();
       g_sdcard_present = mountSDCard();
       g_sdcard_present = mountSDCard();
-    } while (!g_sdcard_present);
+    } while (!g_sdcard_present && wait_for_card);
     blink_cancel();
     blink_cancel();
     LED_OFF();
     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();
   firmware_update();
@@ -1062,12 +1070,18 @@ extern "C" void zuluscsi_setup(void)
 {
 {
   platform_init();
   platform_init();
   platform_late_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
 #ifdef PLATFORM_MASS_STORAGE
   static bool check_mass_storage = true;
   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;
     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 sd_card_check_time = 0;
   static uint32_t last_request_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_reset_watchdog();
   platform_poll();
   platform_poll();
   diskEjectButtonUpdate(true);
   diskEjectButtonUpdate(true);
@@ -1099,7 +1118,7 @@ extern "C" void zuluscsi_main_loop(void)
 #endif // ZULUSCSI_NETWORK
 #endif // ZULUSCSI_NETWORK
 
 
 #ifdef PLATFORM_HAS_INITIATOR_MODE
 #ifdef PLATFORM_HAS_INITIATOR_MODE
-  if (platform_is_initiator_mode_enabled())
+  if (is_initiator)
   {
   {
     scsiInitiatorMainLoop();
     scsiInitiatorMainLoop();
     save_logfile();
     save_logfile();
@@ -1128,7 +1147,7 @@ extern "C" void zuluscsi_main_loop(void)
   {
   {
     // Check SD card status for hotplug
     // Check SD card status for hotplug
     if (scsiDev.phase == BUS_FREE &&
     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();
       sd_card_check_time = millis();
       uint32_t ocr;
       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
     // Try to remount SD card
     do
     do
     {
     {
@@ -1166,6 +1188,6 @@ extern "C" void zuluscsi_main_loop(void)
         platform_reset_watchdog();
         platform_reset_watchdog();
         platform_poll();
         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
 #endif
 #define LOG_SAVE_INTERVAL_MS 1000
 #define LOG_SAVE_INTERVAL_MS 1000
 
 
+// How often to check for SD card presence
+#define SDCARD_POLL_INTERVAL 5000
+
 // Watchdog timeout
 // Watchdog timeout
 // Watchdog will first issue a bus reset and if that does not help, crashdump.
 // Watchdog will first issue a bus reset and if that does not help, crashdump.
 #define WATCHDOG_BUS_RESET_TIMEOUT 15000
 #define WATCHDOG_BUS_RESET_TIMEOUT 15000

+ 35 - 0
src/ZuluSCSI_initiator.cpp

@@ -28,6 +28,8 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_initiator.h"
 #include "ZuluSCSI_initiator.h"
+#include "ZuluSCSI_msc_initiator.h"
+#include "ZuluSCSI_msc.h"
 #include <ZuluSCSI_platform.h>
 #include <ZuluSCSI_platform.h>
 #include <minIni.h>
 #include <minIni.h>
 #include "SdFat.h"
 #include "SdFat.h"
@@ -61,6 +63,9 @@ bool scsiInitiatorReadCapacity(int target_id, uint32_t *sectorcount, uint32_t *s
 
 
 #else
 #else
 
 
+// From ZuluSCSI.cpp
+extern bool g_sdcard_present;
+
 /*************************************
 /*************************************
  * High level initiator mode logic   *
  * 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
 // Update progress bar LED during transfers
 static void scsiInitiatorUpdateLed()
 static void scsiInitiatorUpdateLed()
 {
 {
@@ -202,6 +212,31 @@ void scsiInitiatorMainLoop()
         scsiHostPhyReset();
         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)
     if (!g_initiator_state.imaging)
     {
     {
         // Scan for SCSI drives one at a time
         // Scan for SCSI drives one at a time

+ 3 - 0
src/ZuluSCSI_initiator.h

@@ -34,6 +34,9 @@ void scsiInitiatorInit();
 
 
 void scsiInitiatorMainLoop();
 void scsiInitiatorMainLoop();
 
 
+// Get the SCSI ID used by the initiator itself
+int scsiInitiatorGetOwnID();
+
 // Select target and execute SCSI command
 // Select target and execute SCSI command
 int scsiInitiatorRunCommand(int target_id,
 int scsiInitiatorRunCommand(int target_id,
                             const uint8_t *command, size_t cmdLen,
                             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
 #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
 #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
 #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.
 # Settings that can be specified either per-device or for all devices.