Переглянути джерело

Merge pull request #75 from ZuluSCSI/rp2040_initiator

RP2040 initiator mode support
Alex Perez 3 роки тому
батько
коміт
4e09cda87f

+ 22 - 0
README.md

@@ -76,6 +76,28 @@ For ZuluSCSI V1.1, the DIP switch settings are as follows:
 
 ZuluSCSI Mini has no DIP switches, so all optional configuration parameters must be defined in zuluscsi.ini
 
+ZuluSCSI RP2040 DIP switch settings are:
+- INITIATOR: Enable SCSI initiator mode for imaging SCSI drives
+- DEBUG LOG: Enable verbose debug log (saved to `Zululog.txt`)
+- TERMINATION: Enable SCSI termination
+- BOOTLOADER: Enable built-in USB bootloader, this DIP switch MUST remain off during normal operation.
+
+SCSI initiator mode
+-------------------
+The RP2040 model supports SCSI initiator mode for reading SCSI drives.
+When enabled by the DIP switch, ZuluSCSI RP2040 will scan for SCSI drives on the bus and copy the data as `HDxx_imaged.hda` to the SD card.
+
+LED indications in initiator mode:
+
+- Short blink once a second: idle, searching for SCSI drives
+- Slow blink every 5 seconds: copying data. The blink acts as a progress bar: first it is short and becomes longer when data copying progresses.
+
+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`.
+
+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.
+
 Project structure
 -----------------
 - **src/ZuluSCSI.cpp**: Main portable SCSI implementation.

+ 7 - 6
lib/ZuluSCSI_platform_GD32F205/sd_card_sdio.cpp

@@ -222,7 +222,7 @@ static void sdio_callback(uint32_t complete)
     }
 }
 
-static sdio_callback_t get_stream_callback(const uint8_t *buf, uint32_t count)
+static sdio_callback_t get_stream_callback(const uint8_t *buf, uint32_t count, const char *accesstype, uint32_t sector)
 {
     m_stream_count_start = m_stream_count;
 
@@ -235,7 +235,8 @@ static sdio_callback_t get_stream_callback(const uint8_t *buf, uint32_t count)
         }
         else
         {
-            azdbg("Stream buffer mismatch: ", (uint32_t)buf, " vs. ", (uint32_t)(m_stream_buffer + m_stream_count));
+            azdbg("SD card ", accesstype, "(", (int)sector,
+                  ") slow transfer, buffer", (uint32_t)buf, " vs. ", (uint32_t)(m_stream_buffer + m_stream_count));
             return NULL;
         }
     }
@@ -247,19 +248,19 @@ static sdio_callback_t get_stream_callback(const uint8_t *buf, uint32_t count)
 bool SdioCard::writeSector(uint32_t sector, const uint8_t* src)
 {
     return checkReturnOk(sd_block_write((uint32_t*)src, (uint64_t)sector * 512, 512,
-        get_stream_callback(src, 512)));
+        get_stream_callback(src, 512, "writeSector", sector)));
 }
 
 bool SdioCard::writeSectors(uint32_t sector, const uint8_t* src, size_t n)
 {
     return checkReturnOk(sd_multiblocks_write((uint32_t*)src, (uint64_t)sector * 512, 512, n,
-        get_stream_callback(src, n * 512)));
+        get_stream_callback(src, n * 512, "writeSectors", sector)));
 }
 
 bool SdioCard::readSector(uint32_t sector, uint8_t* dst)
 {
     return checkReturnOk(sd_block_read((uint32_t*)dst, (uint64_t)sector * 512, 512,
-        get_stream_callback(dst, 512)));
+        get_stream_callback(dst, 512, "readSector", sector)));
 }
 
 bool SdioCard::readSectors(uint32_t sector, uint8_t* dst, size_t n)
@@ -279,7 +280,7 @@ bool SdioCard::readSectors(uint32_t sector, uint8_t* dst, size_t n)
     }
 
     return checkReturnOk(sd_multiblocks_read((uint32_t*)dst, (uint64_t)sector * 512, 512, n,
-        get_stream_callback(dst, n * 512)));
+        get_stream_callback(dst, n * 512, "readSectors", sector)));
 }
 
 // Check if a DMA request for SD card read has completed.

+ 146 - 52
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.cpp

@@ -17,6 +17,7 @@ extern "C" {
 #include <hardware/flash.h>
 
 const char *g_azplatform_name = PLATFORM_NAME;
+static bool g_scsi_initiator = false;
 
 void mbed_error_hook(const mbed_error_ctx * error_context);
 
@@ -56,7 +57,6 @@ void azplatform_init()
 
     delay(10); // 10 ms delay to let pull-ups do their work
 
-    bool initiator = !gpio_get(DIP_INITIATOR);
     bool dbglog = !gpio_get(DIP_DBGLOG);
     bool termination = !gpio_get(DIP_TERM);
 
@@ -65,12 +65,7 @@ void azplatform_init()
     uart_init(uart0, 1000000);
     mbed_set_error_hook(mbed_error_hook);
 
-    azlog("DIP switch settings: initiator ", (int)initiator, ", debug log ", (int)dbglog, ", termination ", (int)termination);
-
-    if (initiator)
-    {
-        azlog("ERROR: SCSI initiator mode is not implemented yet, turn DIP switch off for proper operation!");
-    }
+    azlog("DIP switch settings: debug log ", (int)dbglog, ", termination ", (int)termination);
 
     g_azlog_debug = dbglog;
     
@@ -83,7 +78,76 @@ void azplatform_init()
         azlog("NOTE: SCSI termination is disabled");
     }
 
-    /* Initialize SCSI and SD card pins to required modes.
+    // SD card pins
+    // Card is used in SDIO mode for main program, and in SPI mode for crash handler & bootloader.
+    //        pin             function       pup   pdown  out    state fast
+    gpio_conf(SD_SPI_SCK,     GPIO_FUNC_SPI, true, false, true,  true, true);
+    gpio_conf(SD_SPI_MOSI,    GPIO_FUNC_SPI, true, false, true,  true, true);
+    gpio_conf(SD_SPI_MISO,    GPIO_FUNC_SPI, true, false, false, true, true);
+    gpio_conf(SD_SPI_CS,      GPIO_FUNC_SIO, true, false, true,  true, true);
+    gpio_conf(SDIO_D1,        GPIO_FUNC_SIO, true, false, false, true, true);
+    gpio_conf(SDIO_D2,        GPIO_FUNC_SIO, true, false, false, true, true);
+
+    // LED pin
+    gpio_conf(LED_PIN,        GPIO_FUNC_SIO, false,false, true,  false, false);
+
+    // I2C pins
+    //        pin             function       pup   pdown  out    state fast
+    gpio_conf(GPIO_I2C_SCL,   GPIO_FUNC_I2C, true,false, false,  true, true);
+    gpio_conf(GPIO_I2C_SDA,   GPIO_FUNC_I2C, true,false, false,  true, true);
+}
+
+static bool read_initiator_dip_switch()
+{
+    /* Revision 2022d hardware has problems reading initiator DIP switch setting.
+     * The 74LVT245 hold current is keeping the GPIO_ACK state too strongly.
+     * Detect this condition by toggling the pin up and down and seeing if it sticks.
+     */
+
+    // Strong output high, then pulldown
+    //        pin             function       pup   pdown   out    state  fast
+    gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, false, false, true,  true,  false);
+    gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, false, true,  false, true,  false);
+    delay(1);
+    bool initiator_state1 = gpio_get(DIP_INITIATOR);
+    
+    // Strong output low, then pullup
+    //        pin             function       pup   pdown   out    state  fast
+    gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, false, false, true,  false, false);
+    gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, true,  false, false, false, false);
+    delay(1);
+    bool initiator_state2 = gpio_get(DIP_INITIATOR);
+
+    if (initiator_state1 == initiator_state2)
+    {
+        // Ok, was able to read the state directly
+        return !initiator_state1;
+    }
+
+    // Enable OUT_BSY for a short time.
+    // If in target mode, this will force GPIO_ACK high.
+    gpio_put(SCSI_OUT_BSY, 0);
+    delay_100ns();
+    gpio_put(SCSI_OUT_BSY, 1);
+
+    return !gpio_get(DIP_INITIATOR);
+}
+
+// late_init() only runs in main application, SCSI not needed in bootloader
+void azplatform_late_init()
+{
+    if (read_initiator_dip_switch())
+    {
+        g_scsi_initiator = true;
+        azlog("SCSI initiator mode selected by DIP switch, expecting SCSI disks on the bus");
+    }
+    else
+    {
+        g_scsi_initiator = false;
+        azlog("SCSI target mode selected by DIP switch, acting as an SCSI disk");
+    }
+
+    /* Initialize SCSI pins to required modes.
      * SCSI pins should be inactive / input at this point.
      */
 
@@ -100,57 +164,57 @@ void azplatform_init()
     gpio_conf(SCSI_IO_DB7,    GPIO_FUNC_SIO, true, false, false, true, true);
     gpio_conf(SCSI_IO_DBP,    GPIO_FUNC_SIO, true, false, false, true, true);
 
-    // SCSI control outputs
-    //        pin             function       pup   pdown  out    state fast
-    gpio_conf(SCSI_OUT_IO,    GPIO_FUNC_SIO, false,false, true,  true, true);
-    gpio_conf(SCSI_OUT_MSG,   GPIO_FUNC_SIO, false,false, true,  true, true);
-
-    // REQ pin is switched between PIO and SIO, pull-up makes sure no glitches
-    gpio_conf(SCSI_OUT_REQ,   GPIO_FUNC_SIO, true ,false, true,  true, true);
-
-    // Shared pins are changed to input / output depending on communication phase
-    gpio_conf(SCSI_IN_SEL,    GPIO_FUNC_SIO, true, false, false, true, true);
-    if (SCSI_OUT_CD != SCSI_IN_SEL)
+    if (!g_scsi_initiator)
     {
-        gpio_conf(SCSI_OUT_CD,    GPIO_FUNC_SIO, false,false, true,  true, true);
-    }
+        // Act as SCSI device / target
 
-    gpio_conf(SCSI_IN_BSY,    GPIO_FUNC_SIO, true, false, false, true, true);
-    if (SCSI_OUT_MSG != SCSI_IN_BSY)
-    {
-        gpio_conf(SCSI_OUT_MSG,    GPIO_FUNC_SIO, false,false, true,  true, true);
-    }
+        // SCSI control outputs
+        //        pin             function       pup   pdown  out    state fast
+        gpio_conf(SCSI_OUT_IO,    GPIO_FUNC_SIO, false,false, true,  true, true);
+        gpio_conf(SCSI_OUT_MSG,   GPIO_FUNC_SIO, false,false, true,  true, true);
 
-    // SCSI control inputs
-    //        pin             function       pup   pdown  out    state fast
-    gpio_conf(SCSI_IN_ACK,    GPIO_FUNC_SIO, true, false, false, true, false);
-    gpio_conf(SCSI_IN_ATN,    GPIO_FUNC_SIO, true, false, false, true, false);
-    gpio_conf(SCSI_IN_RST,    GPIO_FUNC_SIO, true, false, false, true, false);
+        // REQ pin is switched between PIO and SIO, pull-up makes sure no glitches
+        gpio_conf(SCSI_OUT_REQ,   GPIO_FUNC_SIO, true ,false, true,  true, true);
 
-    // SD card pins
-    // Card is used in SDIO mode for main program, and in SPI mode for crash handler & bootloader.
-    //        pin             function       pup   pdown  out    state fast
-    gpio_conf(SD_SPI_SCK,     GPIO_FUNC_SPI, true, false, true,  true, true);
-    gpio_conf(SD_SPI_MOSI,    GPIO_FUNC_SPI, true, false, true,  true, true);
-    gpio_conf(SD_SPI_MISO,    GPIO_FUNC_SPI, true, false, false, true, true);
-    gpio_conf(SD_SPI_CS,      GPIO_FUNC_SIO, true, false, true,  true, true);
-    gpio_conf(SDIO_D1,        GPIO_FUNC_SIO, true, false, false, true, true);
-    gpio_conf(SDIO_D2,        GPIO_FUNC_SIO, true, false, false, true, true);
+        // Shared pins are changed to input / output depending on communication phase
+        gpio_conf(SCSI_IN_SEL,    GPIO_FUNC_SIO, true, false, false, true, true);
+        if (SCSI_OUT_CD != SCSI_IN_SEL)
+        {
+            gpio_conf(SCSI_OUT_CD,    GPIO_FUNC_SIO, false,false, true,  true, true);
+        }
 
-    // LED pin
-    gpio_conf(LED_PIN,        GPIO_FUNC_SIO, false,false, true,  false, false);
+        gpio_conf(SCSI_IN_BSY,    GPIO_FUNC_SIO, true, false, false, true, true);
+        if (SCSI_OUT_MSG != SCSI_IN_BSY)
+        {
+            gpio_conf(SCSI_OUT_MSG,    GPIO_FUNC_SIO, false,false, true,  true, true);
+        }
 
-    // I2C pins
-    //        pin             function       pup   pdown  out    state fast
-    gpio_conf(GPIO_I2C_SCL,   GPIO_FUNC_I2C, true,false, false,  true, true);
-    gpio_conf(GPIO_I2C_SDA,   GPIO_FUNC_I2C, true,false, false,  true, true);
+        // SCSI control inputs
+        //        pin             function       pup   pdown  out    state fast
+        gpio_conf(SCSI_IN_ACK,    GPIO_FUNC_SIO, true, false, false, true, false);
+        gpio_conf(SCSI_IN_ATN,    GPIO_FUNC_SIO, true, false, false, true, false);
+        gpio_conf(SCSI_IN_RST,    GPIO_FUNC_SIO, true, false, false, true, false);
+    }
+    else
+    {
+        // Act as SCSI initiator
+
+        //        pin             function       pup   pdown  out    state fast
+        gpio_conf(SCSI_IN_IO,     GPIO_FUNC_SIO, true ,false, false, true, false);
+        gpio_conf(SCSI_IN_MSG,    GPIO_FUNC_SIO, true ,false, false, true, false);
+        gpio_conf(SCSI_IN_CD,     GPIO_FUNC_SIO, true ,false, false, true, false);
+        gpio_conf(SCSI_IN_REQ,    GPIO_FUNC_SIO, true ,false, false, true, false);
+        gpio_conf(SCSI_IN_BSY,    GPIO_FUNC_SIO, true, false, false, true, false);
+        gpio_conf(SCSI_IN_RST,    GPIO_FUNC_SIO, true, false, false, true, false);
+        gpio_conf(SCSI_OUT_SEL,   GPIO_FUNC_SIO, false,false, true,  true, true);
+        gpio_conf(SCSI_OUT_ACK,   GPIO_FUNC_SIO, false,false, true,  true, true);
+        gpio_conf(SCSI_OUT_ATN,   GPIO_FUNC_SIO, false,false, true,  true, true);
+    }
 }
 
-void azplatform_late_init()
+bool azplatform_is_initiator_mode_enabled()
 {
-    /* This function can usually be left empty.
-     * It can be used for initialization code that should not run in bootloader.
-     */
+    return g_scsi_initiator;
 }
 
 /*****************************************/
@@ -243,15 +307,45 @@ static void watchdog_callback(unsigned alarm_num)
 
     if (g_watchdog_timeout <= WATCHDOG_CRASH_TIMEOUT - WATCHDOG_BUS_RESET_TIMEOUT)
     {
-        if (!scsiDev.resetFlag)
+        if (!scsiDev.resetFlag || !g_scsiHostPhyReset)
         {
+            azlog("--------------");
             azlog("WATCHDOG TIMEOUT, attempting bus reset");
+            azlog("GPIO states: out ", sio_hw->gpio_out, " oe ", sio_hw->gpio_oe, " in ", sio_hw->gpio_in);
+
+            uint32_t *p = (uint32_t*)__get_PSP();
+            for (int i = 0; i < 8; i++)
+            {
+                if (p == &__StackTop) break; // End of stack
+
+                azlog("STACK ", (uint32_t)p, ":    ", p[0], " ", p[1], " ", p[2], " ", p[3]);
+                p += 4;
+            }
+
             scsiDev.resetFlag = 1;
+            g_scsiHostPhyReset = true;
         }
 
         if (g_watchdog_timeout <= 0)
         {
-            assert(false);
+            azlog("--------------");
+            azlog("WATCHDOG TIMEOUT!");
+            azlog("Platform: ", g_azplatform_name);
+            azlog("FW Version: ", g_azlog_firmwareversion);
+            azlog("GPIO states: out ", sio_hw->gpio_out, " oe ", sio_hw->gpio_oe, " in ", sio_hw->gpio_in);
+
+            uint32_t *p = (uint32_t*)__get_PSP();
+            for (int i = 0; i < 8; i++)
+            {
+                if (p == &__StackTop) break; // End of stack
+
+                azlog("STACK ", (uint32_t)p, ":    ", p[0], " ", p[1], " ", p[2], " ", p[3]);
+                p += 4;
+            }
+
+            azplatform_emergency_log_save();
+
+            azplatform_boot_to_main_firmware();
         }
     }
 

+ 17 - 3
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.h

@@ -5,6 +5,7 @@
 #include <stdint.h>
 #include <Arduino.h>
 #include "ZuluSCSI_platform_gpio.h"
+#include "scsiHostPhy.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -16,9 +17,10 @@ extern const char *g_azplatform_name;
 #define PLATFORM_REVISION "2.0"
 #define PLATFORM_MAX_SCSI_SPEED S2S_CFG_SPEED_SYNC_10
 #define PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE 4096
-#define PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE 65536
+#define PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE 32768
 #define PLATFORM_OPTIMAL_LAST_SD_WRITE_SIZE 8192
 #define SD_USE_SDIO 1
+#define PLATFORM_HAS_INITIATOR_MODE 1
 
 // NOTE: The driver supports synchronous speeds higher than 10MB/s, but this
 // has not been tested due to lack of fast enough SCSI adapter.
@@ -43,7 +45,7 @@ static inline void delay_ns(unsigned long ns)
 // Approximate fast delay
 static inline void delay_100ns()
 {
-    asm volatile ("nop \n nop \n nop \n nop \n nop");
+    asm volatile ("nop \n nop \n nop \n nop \n nop \n nop \n nop \n nop \n nop \n nop \n nop");
 }
 
 // Initialize SD card and GPIO configuration
@@ -52,6 +54,9 @@ void azplatform_init();
 // Initialization for main application, not used for bootloader
 void azplatform_late_init();
 
+// Query whether initiator mode is enabled on targets with PLATFORM_HAS_INITIATOR_MODE
+bool azplatform_is_initiator_mode_enabled();
+
 // Setup soft watchdog if supported
 void azplatform_reset_watchdog();
 
@@ -81,6 +86,15 @@ void azplatform_boot_to_main_firmware();
 #define SCSI_IN(pin) \
     ((sio_hw->gpio_in & (1 << (SCSI_IN_ ## pin))) ? 0 : 1)
 
+// Set pin directions for initiator vs. target mode
+#define SCSI_ENABLE_INITIATOR() \
+    (sio_hw->gpio_oe_set = (1 << SCSI_OUT_ACK) | \
+                           (1 << SCSI_OUT_ATN)), \
+    (sio_hw->gpio_oe_clr = (1 << SCSI_IN_IO) | \
+                           (1 << SCSI_IN_CD) | \
+                           (1 << SCSI_IN_MSG) | \
+                           (1 << SCSI_IN_REQ))
+
 // Enable driving of shared control pins
 #define SCSI_ENABLE_CONTROL_OUT() \
     (sio_hw->gpio_oe_set = (1 << SCSI_OUT_CD) | \
@@ -117,7 +131,7 @@ extern const uint32_t g_scsi_parity_lookup[256];
                        (1 << SCSI_OUT_SEL)
 
 // Read SCSI data bus
-#define SCSI_IN_DATA(data) \
+#define SCSI_IN_DATA() \
     (~sio_hw->gpio_in & SCSI_IO_DATA_MASK) >> SCSI_IO_SHIFT
 
 #ifdef __cplusplus

+ 10 - 0
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio.h

@@ -38,6 +38,16 @@
 #define SCSI_IN_BSY  13
 #define SCSI_IN_RST  27
 
+// Status line outputs for initiator mode
+#define SCSI_OUT_ACK  10
+#define SCSI_OUT_ATN  29
+
+// Status line inputs for initiator mode
+#define SCSI_IN_IO    12
+#define SCSI_IN_CD    11
+#define SCSI_IN_MSG   13
+#define SCSI_IN_REQ   9
+
 // Status LED pins
 #define LED_PIN      25
 #define LED_ON()     sio_hw->gpio_set = 1 << LED_PIN

+ 6 - 0
lib/ZuluSCSI_platform_RP2040/rp2040.ld

@@ -65,6 +65,12 @@ SECTIONS
         *libc*:*printf*(.text .text*)
         *libc*:*toa*(.text .text*)
         *libminIni.a:(.text .text*)
+
+        /* RP2040 breakpoints in RAM code don't always work very well
+         * because the boot routine tends to overwrite them.
+         * Uncommenting this line puts all code in flash.
+         */
+        /* *(.text .text*) */
     } > FLASH
     .rodata : {
         . = ALIGN(4);

+ 238 - 0
lib/ZuluSCSI_platform_RP2040/scsiHostPhy.cpp

@@ -0,0 +1,238 @@
+#include "scsiHostPhy.h"
+#include "ZuluSCSI_platform.h"
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_log_trace.h"
+#include "scsi_accel_host.h"
+#include <assert.h>
+
+#include <scsi2sd.h>
+extern "C" {
+#include <scsi.h>
+}
+
+volatile int g_scsiHostPhyReset;
+
+// Release bus and pulse RST signal, initialize PHY to host mode.
+void scsiHostPhyReset(void)
+{
+    SCSI_RELEASE_OUTPUTS();
+    SCSI_ENABLE_INITIATOR();
+
+    scsi_accel_host_init();
+
+    SCSI_OUT(RST, 1);
+    delay(2);
+    SCSI_OUT(RST, 0);
+    delay(250);
+    g_scsiHostPhyReset = false;
+}
+
+// Select a device, id 0-7.
+// Returns true if the target answers to selection request.
+bool scsiHostPhySelect(int target_id)
+{
+    SCSI_RELEASE_OUTPUTS();
+
+    // We can't write individual data bus bits, so use a bit modified
+    // arbitration scheme. We always yield to any other initiator on
+    // the bus.
+    scsiLogInitiatorPhaseChange(BUS_BUSY);
+    SCSI_OUT(BSY, 1);
+    for (int wait = 0; wait < 10; wait++)
+    {
+        delayMicroseconds(1);
+
+        if (SCSI_IN_DATA() != 0)
+        {
+            azdbg("scsiHostPhySelect: bus is busy");
+            scsiLogInitiatorPhaseChange(BUS_FREE);
+            SCSI_RELEASE_OUTPUTS();
+            return false;
+        }
+    }
+
+    // Selection phase
+    scsiLogInitiatorPhaseChange(SELECTION);
+    azdbg("------ SELECTING ", target_id);
+    SCSI_OUT(SEL, 1);
+    delayMicroseconds(5);
+    SCSI_OUT_DATA(1 << target_id);
+    delayMicroseconds(5);
+    SCSI_OUT(BSY, 0);
+
+    // Wait for target to respond
+    for (int wait = 0; wait < 2500; wait++)
+    {
+        delayMicroseconds(100);
+        if (SCSI_IN(BSY))
+        {
+            break;
+        }
+    }
+
+    if (!SCSI_IN(BSY))
+    {
+        // No response
+        SCSI_RELEASE_OUTPUTS();
+        return false;
+    }
+
+    // We need to assert OUT_BSY to enable IO buffer U105 to read status signals.
+    SCSI_RELEASE_DATA_REQ();
+    SCSI_OUT(BSY, 1);
+    SCSI_OUT(SEL, 0);
+    return true;
+}
+
+// Read the current communication phase as signaled by the target
+int scsiHostPhyGetPhase()
+{
+    static absolute_time_t last_online_time;
+
+    if (g_scsiHostPhyReset)
+    {
+        // Reset request from watchdog timer
+        scsiHostPhyRelease();
+        return BUS_FREE;
+    }
+
+    int phase = 0;
+    bool req_in = SCSI_IN(REQ);
+    if (SCSI_IN(CD)) phase |= __scsiphase_cd;
+    if (SCSI_IN(IO)) phase |= __scsiphase_io;
+    if (SCSI_IN(MSG)) phase |= __scsiphase_msg;
+
+    if (phase == 0 && absolute_time_diff_us(last_online_time, get_absolute_time()) > 100)
+    {
+        // Disable OUT_BSY for a short time to see if the target is still on line
+        SCSI_OUT(BSY, 0);
+        delayMicroseconds(1);
+
+        if (!SCSI_IN(BSY))
+        {
+            scsiLogInitiatorPhaseChange(BUS_FREE);
+            return BUS_FREE;
+        }
+
+        // Still online, re-enable OUT_BSY to enable IO buffers
+        SCSI_OUT(BSY, 1);
+        last_online_time = get_absolute_time();
+    }
+    else if (phase != 0)
+    {
+        last_online_time = get_absolute_time();
+    }
+
+    if (!req_in)
+    {
+        // Don't act on phase changes until target asserts request signal.
+        // This filters out any spurious changes on control signals.
+        return BUS_BUSY;
+    }
+    else
+    {
+        scsiLogInitiatorPhaseChange(phase);
+        return phase;
+    }
+}
+
+bool scsiHostRequestWaiting()
+{
+    return SCSI_IN(REQ);
+}
+
+// Blocking data transfer
+#define SCSIHOST_WAIT_ACTIVE(pin) \
+  if (!SCSI_IN(pin)) { \
+    if (!SCSI_IN(pin)) { \
+      while(!SCSI_IN(pin) && !g_scsiHostPhyReset); \
+    } \
+  }
+
+#define SCSIHOST_WAIT_INACTIVE(pin) \
+  if (SCSI_IN(pin)) { \
+    if (SCSI_IN(pin)) { \
+      while(SCSI_IN(pin) && !g_scsiHostPhyReset); \
+    } \
+  }
+
+// Write one byte to SCSI target using the handshake mechanism
+static inline void scsiHostWriteOneByte(uint8_t value)
+{
+    SCSIHOST_WAIT_ACTIVE(REQ);
+    SCSI_OUT_DATA(value);
+    delay_100ns(); // DB setup time before ACK
+    SCSI_OUT(ACK, 1);
+    SCSIHOST_WAIT_INACTIVE(REQ);
+    SCSI_RELEASE_DATA_REQ();
+    SCSI_OUT(ACK, 0);
+}
+
+// Read one byte from SCSI target using the handshake mechanism.
+static inline uint8_t scsiHostReadOneByte(int* parityError)
+{
+    SCSIHOST_WAIT_ACTIVE(REQ);
+    uint16_t r = SCSI_IN_DATA();
+    SCSI_OUT(ACK, 1);
+    SCSIHOST_WAIT_INACTIVE(REQ);
+    SCSI_OUT(ACK, 0);
+
+    if (parityError && r != (g_scsi_parity_lookup[r & 0xFF] ^ SCSI_IO_DATA_MASK))
+    {
+        azlog("Parity error in scsiReadOneByte(): ", (uint32_t)r);
+        *parityError = 1;
+    }
+
+    return (uint8_t)r;
+}
+
+bool scsiHostWrite(const uint8_t *data, uint32_t count)
+{
+    scsiLogDataOut(data, count);
+
+    for (uint32_t i = 0; i < count; i++)
+    {
+        if (g_scsiHostPhyReset) return false;
+
+        scsiHostWriteOneByte(data[i]);
+    }
+
+    return true;
+}
+
+bool scsiHostRead(uint8_t *data, uint32_t count)
+{
+    int parityError = 0;
+
+    if ((count & 1) == 0)
+    {
+        // Even number of bytes, use accelerated routine
+        scsi_accel_host_read(data, count, &parityError, &g_scsiHostPhyReset);
+    }
+    else
+    {
+        for (uint32_t i = 0; i < count; i++)
+        {
+            if (g_scsiHostPhyReset) return false;
+
+            data[i] = scsiHostReadOneByte(&parityError);
+        }
+    }
+
+    if (parityError || g_scsiHostPhyReset)
+    {
+        return false;
+    }
+    else
+    {
+        scsiLogDataIn(data, count);
+        return true;
+    }
+}
+
+// Release all bus signals
+void scsiHostPhyRelease()
+{
+    scsiLogInitiatorPhaseChange(BUS_FREE);
+    SCSI_RELEASE_OUTPUTS();
+}

+ 31 - 0
lib/ZuluSCSI_platform_RP2040/scsiHostPhy.h

@@ -0,0 +1,31 @@
+// Host side SCSI physical interface.
+// Used in initiator to interface to an SCSI drive.
+
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+// Request to stop activity and reset the bus
+extern volatile int g_scsiHostPhyReset;
+
+// Release bus and pulse RST signal, initialize PHY to host mode.
+void scsiHostPhyReset(void);
+
+// Select a device, id 0-7.
+// Returns true if the target answers to selection request.
+bool scsiHostPhySelect(int target_id);
+
+// Read the current communication phase as signaled by the target
+// Matches SCSI_PHASE enumeration from scsi.h.
+int scsiHostPhyGetPhase();
+
+// Returns true if the device has asserted REQ signal, i.e. data waiting
+bool scsiHostRequestWaiting();
+
+// Blocking data transfer
+bool scsiHostWrite(const uint8_t *data, uint32_t count);
+bool scsiHostRead(uint8_t *data, uint32_t count);
+
+// Release all bus signals
+void scsiHostPhyRelease();

+ 131 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.cpp

@@ -0,0 +1,131 @@
+// Accelerated SCSI subroutines for SCSI initiator/host side communication
+
+#include "scsi_accel_host.h"
+#include "ZuluSCSI_platform.h"
+#include "ZuluSCSI_log.h"
+#include "scsi_accel_host.pio.h"
+#include <hardware/pio.h>
+#include <hardware/dma.h>
+#include <hardware/irq.h>
+#include <hardware/structs/iobank0.h>
+#include <hardware/sync.h>
+
+#define SCSI_PIO pio0
+#define SCSI_SM 0
+
+static struct {
+    // PIO configurations
+    uint32_t pio_offset_async_read;
+    pio_sm_config pio_cfg_async_read;
+} g_scsi_host;
+
+enum scsidma_state_t { SCSIHOST_IDLE = 0,
+                       SCSIHOST_READ };
+static volatile scsidma_state_t g_scsi_host_state;
+
+static void scsi_accel_host_config_gpio()
+{
+    if (g_scsi_host_state == SCSIHOST_IDLE)
+    {
+        iobank0_hw->io[SCSI_IO_DB0].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB1].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB2].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB3].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB4].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB5].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB6].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB7].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DBP].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_OUT_ACK].ctrl = GPIO_FUNC_SIO;
+    }
+    else if (g_scsi_host_state == SCSIHOST_READ)
+    {
+        // Data bus and REQ as input, ACK pin as output
+        pio_sm_set_pins(SCSI_PIO, SCSI_SM, 0x7FF);
+        pio_sm_set_consecutive_pindirs(SCSI_PIO, SCSI_SM, 0, 10, false);
+        pio_sm_set_consecutive_pindirs(SCSI_PIO, SCSI_SM, 10, 1, true);
+
+        iobank0_hw->io[SCSI_IO_DB0].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB1].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB2].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB3].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB4].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB5].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB6].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DB7].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_IO_DBP].ctrl  = GPIO_FUNC_SIO;
+        iobank0_hw->io[SCSI_OUT_ACK].ctrl = GPIO_FUNC_PIO0;
+    }
+}
+
+void scsi_accel_host_read(uint8_t *buf, uint32_t count, int *parityError, volatile int *resetFlag)
+{
+    // Currently this method just reads from the PIO RX fifo directly in software loop.
+    // The SD card access is parallelized using DMA, so there is limited benefit from using DMA here.
+    g_scsi_host_state = SCSIHOST_READ;
+
+    pio_sm_init(SCSI_PIO, SCSI_SM, g_scsi_host.pio_offset_async_read, &g_scsi_host.pio_cfg_async_read);
+    scsi_accel_host_config_gpio();
+    pio_sm_set_enabled(SCSI_PIO, SCSI_SM, true);
+
+    // Set the number of bytes to read, must be divisible by 2.
+    assert((count & 1) == 0);
+    pio_sm_put(SCSI_PIO, SCSI_SM, count - 1);
+
+    // Read results from PIO RX FIFO
+    uint8_t *dst = buf;
+    uint8_t *end = buf + count;
+    uint32_t paritycheck = 0;
+    while (dst < end)
+    {
+        if (*resetFlag)
+        {
+            break;
+        }
+
+        uint32_t available = pio_sm_get_rx_fifo_level(SCSI_PIO, SCSI_SM);
+
+        while (available > 0)
+        {
+            available--;
+            uint32_t word = pio_sm_get(SCSI_PIO, SCSI_SM);
+            paritycheck ^= word;
+            word = ~word;
+            *dst++ = word & 0xFF;
+            *dst++ = word >> 16;
+        }
+    }
+
+    // Check parity errors in whole block
+    // This doesn't detect if there is even number of parity errors in block.
+    uint8_t byte0 = ~(paritycheck & 0xFF);
+    uint8_t byte1 = ~(paritycheck >> 16);
+    if (paritycheck != ((g_scsi_parity_lookup[byte1] << 16) | g_scsi_parity_lookup[byte0]))
+    {
+        azlog("Parity error in scsi_accel_host_read(): ", paritycheck);
+        *parityError = 1;
+    }
+
+    g_scsi_host_state = SCSIHOST_IDLE;
+    SCSI_RELEASE_DATA_REQ();
+    scsi_accel_host_config_gpio();
+    pio_sm_set_enabled(SCSI_PIO, SCSI_SM, false);
+}
+
+
+void scsi_accel_host_init()
+{
+    g_scsi_host_state = SCSIHOST_IDLE;
+    scsi_accel_host_config_gpio();
+
+    // Load PIO programs
+    pio_clear_instruction_memory(SCSI_PIO);
+
+    // Asynchronous / synchronous SCSI read
+    g_scsi_host.pio_offset_async_read = pio_add_program(SCSI_PIO, &scsi_host_async_read_program);
+    g_scsi_host.pio_cfg_async_read = scsi_host_async_read_program_get_default_config(g_scsi_host.pio_offset_async_read);
+    sm_config_set_in_pins(&g_scsi_host.pio_cfg_async_read, SCSI_IO_DB0);
+    sm_config_set_sideset_pins(&g_scsi_host.pio_cfg_async_read, SCSI_OUT_ACK);
+    sm_config_set_out_shift(&g_scsi_host.pio_cfg_async_read, true, false, 32);
+    sm_config_set_in_shift(&g_scsi_host.pio_cfg_async_read, true, true, 32);
+}

+ 11 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.h

@@ -0,0 +1,11 @@
+// Accelerated SCSI subroutines for SCSI initiator/host side communication
+
+#pragma once
+
+#include <stdint.h>
+
+void scsi_accel_host_init();
+
+// Read data from SCSI bus.
+// Number of bytes to read must be divisible by two.
+void scsi_accel_host_read(uint8_t *buf, uint32_t count, int *parityError, volatile int *resetFlag);

+ 26 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.pio

@@ -0,0 +1,26 @@
+; RP2040 PIO program for accelerating SCSI initiator / host function
+; Run "pioasm scsi_accel_host.pio scsi_accel_host.pio.h" to regenerate the C header from this.
+; GPIO mapping:
+; - 0-7: DB0-DB7
+; -   8: DBP
+; Side set is ACK pin
+
+.define REQ 9
+.define ACK 10
+
+; Read from SCSI bus using asynchronous handshake.
+; Data is returned as 16-bit words that contain the 8 data bits + 1 parity bit.
+; Number of bytes to receive minus 1 should be written to TX fifo.
+; Number of bytes to receive must be divisible by 2.
+.program scsi_host_async_read
+    .side_set 1
+
+    pull block                  side 1  ; Get number of bytes to receive
+    mov x, osr                  side 1  ; Store to counter X
+
+start:
+    wait 0 gpio REQ             side 1  ; Wait for REQ low
+    in pins, 9                  side 0  ; Assert ACK, read GPIO
+    in null, 7                  side 0  ; Padding bits
+    wait 1 gpio REQ             side 0  ; Wait for REQ high
+    jmp x-- start               side 1  ; Deassert ACK, decrement byte count and jump to start

+ 44 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.pio.h

@@ -0,0 +1,44 @@
+// -------------------------------------------------- //
+// This file is autogenerated by pioasm; do not edit! //
+// -------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// -------------------- //
+// scsi_host_async_read //
+// -------------------- //
+
+#define scsi_host_async_read_wrap_target 0
+#define scsi_host_async_read_wrap 6
+
+static const uint16_t scsi_host_async_read_program_instructions[] = {
+            //     .wrap_target
+    0x90a0, //  0: pull   block           side 1     
+    0xb027, //  1: mov    x, osr          side 1     
+    0x3009, //  2: wait   0 gpio, 9       side 1     
+    0x4009, //  3: in     pins, 9         side 0     
+    0x4067, //  4: in     null, 7         side 0     
+    0x2089, //  5: wait   1 gpio, 9       side 0     
+    0x1042, //  6: jmp    x--, 2          side 1     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_host_async_read_program = {
+    .instructions = scsi_host_async_read_program_instructions,
+    .length = 7,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_host_async_read_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_host_async_read_wrap_target, offset + scsi_host_async_read_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+

+ 7 - 6
lib/ZuluSCSI_platform_RP2040/sd_card_sdio.cpp

@@ -40,7 +40,7 @@ void azplatform_set_sd_callback(sd_callback_t func, const uint8_t *buffer)
     m_stream_count_start = 0;
 }
 
-static sd_callback_t get_stream_callback(const uint8_t *buf, uint32_t count)
+static sd_callback_t get_stream_callback(const uint8_t *buf, uint32_t count, const char *accesstype, uint32_t sector)
 {
     m_stream_count_start = m_stream_count;
 
@@ -53,7 +53,8 @@ static sd_callback_t get_stream_callback(const uint8_t *buf, uint32_t count)
         }
         else
         {
-            azdbg("Stream buffer mismatch: ", (uint32_t)buf, " vs. ", (uint32_t)(m_stream_buffer + m_stream_count));
+            azdbg("SD card ", accesstype, "(", (int)sector,
+                  ") slow transfer, buffer", (uint32_t)buf, " vs. ", (uint32_t)(m_stream_buffer + m_stream_count));
             return NULL;
         }
     }
@@ -307,7 +308,7 @@ bool SdioCard::writeSector(uint32_t sector, const uint8_t* src)
     }
 
     // If possible, report transfer status to application through callback.
-    sd_callback_t callback = get_stream_callback(src, 512);
+    sd_callback_t callback = get_stream_callback(src, 512, "writeSector", sector);
 
     uint32_t reply;
     if (!checkReturnOk(rp2040_sdio_command_R1(16, 512, &reply)) || // SET_BLOCKLEN
@@ -350,7 +351,7 @@ bool SdioCard::writeSectors(uint32_t sector, const uint8_t* src, size_t n)
         return true;
     }
 
-    sd_callback_t callback = get_stream_callback(src, n * 512);
+    sd_callback_t callback = get_stream_callback(src, n * 512, "writeSectors", sector);
 
     uint32_t reply;
     if (!checkReturnOk(rp2040_sdio_command_R1(16, 512, &reply)) || // SET_BLOCKLEN
@@ -393,7 +394,7 @@ bool SdioCard::readSector(uint32_t sector, uint8_t* dst)
         dst = (uint8_t*)g_sdio_dma_buf;
     }
 
-    sd_callback_t callback = get_stream_callback(dst, 512);
+    sd_callback_t callback = get_stream_callback(dst, 512, "readSector", sector);
 
     uint32_t reply;
     if (!checkReturnOk(rp2040_sdio_command_R1(16, 512, &reply)) || // SET_BLOCKLEN
@@ -441,7 +442,7 @@ bool SdioCard::readSectors(uint32_t sector, uint8_t* dst, size_t n)
         return true;
     }
 
-    sd_callback_t callback = get_stream_callback(dst, n * 512);
+    sd_callback_t callback = get_stream_callback(dst, n * 512, "readSectors", sector);
 
     uint32_t reply;
     if (!checkReturnOk(rp2040_sdio_command_R1(16, 512, &reply)) || // SET_BLOCKLEN

+ 38 - 11
src/ZuluSCSI.cpp

@@ -50,6 +50,7 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_disk.h"
+#include "ZuluSCSI_initiator.h"
 
 SdFs SD;
 FsFile g_logfile;
@@ -373,11 +374,6 @@ void readSCSIDeviceConfig()
   {
     scsiDiskLoadConfig(i);
   }
-  
-  if (ini_getbool("SCSI", "Debug", 0, CONFIGFILE))
-  {
-    g_azlog_debug = true;
-  }
 }
 
 /*********************************/
@@ -386,6 +382,26 @@ void readSCSIDeviceConfig()
 
 static void reinitSCSI()
 {
+  if (ini_getbool("SCSI", "Debug", 0, CONFIGFILE))
+  {
+    g_azlog_debug = true;
+  }
+
+#ifdef PLATFORM_HAS_INITIATOR_MODE
+  if (azplatform_is_initiator_mode_enabled())
+  {
+    // Initialize scsiDev to zero values even though it is not used
+    scsiInit();
+
+    // Initializer initiator mode state machine
+    scsiInitiatorInit();
+
+    blinkStatus(BLINK_STATUS_OK);
+
+    return;
+  }
+#endif
+
   scsiDiskResetImages();
   readSCSIDeviceConfig();
   findHDDImages();
@@ -454,15 +470,26 @@ extern "C" void zuluscsi_main_loop(void)
   static uint32_t sd_card_check_time = 0;
 
   azplatform_reset_watchdog();
-  scsiPoll();
-  scsiDiskPoll();
-  scsiLogPhaseChange(scsiDev.phase);
-
-  // Save log periodically during status phase if there are new messages.
-  if (scsiDev.phase == STATUS)
+  
+#ifdef PLATFORM_HAS_INITIATOR_MODE
+  if (azplatform_is_initiator_mode_enabled())
   {
+    scsiInitiatorMainLoop();
     save_logfile();
   }
+  else
+#endif
+  {
+    scsiPoll();
+    scsiDiskPoll();
+    scsiLogPhaseChange(scsiDev.phase);
+
+    // Save log periodically during status phase if there are new messages.
+    if (scsiDev.phase == STATUS)
+    {
+      save_logfile();
+    }
+  }
 
   // Check SD card status for hotplug
   if (scsiDev.phase == BUS_FREE &&

+ 2 - 0
src/ZuluSCSI_config.h

@@ -3,6 +3,8 @@
 
 #pragma once
 
+#include <ZuluSCSI_platform.h>
+
 // Use variables for version number
 #define FW_VER_NUM      "1.0.9"
 #define FW_VER_SUFFIX   "devel"

+ 505 - 0
src/ZuluSCSI_initiator.cpp

@@ -0,0 +1,505 @@
+/*
+ *  ZuluSCSI
+ *  Copyright (c) 2022 Rabbit Hole Computing
+ * 
+ * Main program for initiator mode.
+ */
+
+#include "ZuluSCSI_config.h"
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_log_trace.h"
+#include "ZuluSCSI_initiator.h"
+#include <ZuluSCSI_platform.h>
+#include "SdFat.h"
+
+#include <scsi2sd.h>
+extern "C" {
+#include <scsi.h>
+}
+
+#ifndef PLATFORM_HAS_INITIATOR_MODE
+
+void scsiInitiatorInit()
+{
+}
+
+void scsiInitiatorMainLoop()
+{
+}
+
+int scsiInitiatorRunCommand(const uint8_t *command, size_t cmdlen,
+                            uint8_t *bufIn, size_t bufInLen,
+                            const uint8_t *bufOut, size_t bufOutLen)
+{
+    return -1;
+}
+
+bool scsiInitiatorReadCapacity(int target_id, uint32_t *sectorcount, uint32_t *sectorsize)
+{
+    return false;
+}
+
+#else
+
+/*************************************
+ * High level initiator mode logic   *
+ *************************************/
+
+static struct {
+    // Bitmap of all drives that have been imaged
+    uint32_t drives_imaged;
+
+    // Is imaging a drive in progress, or are we scanning?
+    bool imaging;
+
+    // Information about currently selected drive
+    int target_id;
+    uint32_t sectorsize;
+    uint32_t sectorcount;
+    uint32_t sectors_done;
+    int retrycount;
+
+    FsFile target_file;
+} g_initiator_state;
+
+extern SdFs SD;
+
+// Initialization of initiator mode
+void scsiInitiatorInit()
+{
+    scsiHostPhyReset();
+
+    g_initiator_state.drives_imaged = 0;
+    g_initiator_state.imaging = false;
+    g_initiator_state.target_id = -1;
+    g_initiator_state.sectorsize = 0;
+    g_initiator_state.sectorcount = 0;
+    g_initiator_state.sectors_done = 0;
+    g_initiator_state.retrycount = 0;
+}
+
+// High level logic of the initiator mode
+void scsiInitiatorMainLoop()
+{
+    if (!g_initiator_state.imaging)
+    {
+        // Scan for SCSI drives one at a time
+        g_initiator_state.target_id = (g_initiator_state.target_id + 1) % 8;
+        g_initiator_state.sectors_done = 0;
+        g_initiator_state.retrycount = 0;
+
+        if (!(g_initiator_state.drives_imaged & (1 << g_initiator_state.target_id)))
+        {
+            delay(1000);
+            LED_ON();
+            bool readcapok = scsiInitiatorReadCapacity(g_initiator_state.target_id, &g_initiator_state.sectorcount, &g_initiator_state.sectorsize);
+            LED_OFF();
+
+            if (readcapok)
+            {
+                azlog("SCSI id ", g_initiator_state.target_id,
+                    " capacity ", (int)g_initiator_state.sectorcount,
+                    " sectors x ", (int)g_initiator_state.sectorsize, " bytes");
+
+                char filename[] = "HD00_imaged.hda";
+                filename[2] += g_initiator_state.target_id;
+
+                SD.remove(filename);
+                g_initiator_state.target_file = SD.open(filename, O_RDWR | O_CREAT | O_TRUNC);
+                if (!g_initiator_state.target_file.isOpen())
+                {
+                    azlog("Failed to open file for writing: ", filename);
+                    return;
+                }
+
+                azlog("Starting to copy drive data to ", filename);
+                g_initiator_state.target_file.preAllocate((uint64_t)g_initiator_state.sectorcount * g_initiator_state.sectorsize);
+                g_initiator_state.imaging = true;
+            }
+        }
+    }
+    else
+    {
+        // Copy sectors from SCSI drive to file
+        if (g_initiator_state.sectors_done >= g_initiator_state.sectorcount)
+        {
+            azlog("Finished imaging drive with id ", g_initiator_state.target_id);
+            LED_OFF();
+            g_initiator_state.drives_imaged |= (1 << g_initiator_state.target_id);
+            g_initiator_state.imaging = false;
+            g_initiator_state.target_file.close();
+            return;
+        }
+
+        // Update status indicator, the led blinks every 5 seconds and is on the longer the more data has been transferred
+        uint32_t time_start = millis();
+        int phase = (time_start % 5000);
+        int duty = g_initiator_state.sectors_done * 5000 / g_initiator_state.sectorcount;
+        if (duty < 100) duty = 100;
+        if (phase <= duty)
+        {
+            LED_ON();
+        }
+        else
+        {
+            LED_OFF();
+        }
+
+        // How many sectors to read in one batch?
+        int numtoread = g_initiator_state.sectorcount - g_initiator_state.sectors_done;
+        if (numtoread > 512) numtoread = 512;
+
+        // Retry sector-by-sector
+        if (g_initiator_state.retrycount > 1)
+            numtoread = 1;
+
+        bool status = scsiInitiatorReadDataToFile(g_initiator_state.target_id,
+            g_initiator_state.sectors_done, numtoread, g_initiator_state.sectorsize,
+            g_initiator_state.target_file);
+
+        if (!status)
+        {
+            azlog("Failed to transfer ", numtoread, " sectors starting at ", (int)g_initiator_state.sectors_done);
+
+            if (g_initiator_state.retrycount < 5)
+            {
+                azlog("Retrying.. ", g_initiator_state.retrycount, "/5");
+                delay(200);
+                scsiHostPhyReset();
+                delay(200);
+
+                g_initiator_state.retrycount++;
+                g_initiator_state.target_file.seek((uint64_t)g_initiator_state.sectors_done * g_initiator_state.sectorsize);
+            }
+            else
+            {
+                azlog("Retry limit exceeded, skipping one sector");
+                g_initiator_state.retrycount = 0;
+                g_initiator_state.sectors_done++;
+                g_initiator_state.target_file.seek((uint64_t)g_initiator_state.sectors_done * g_initiator_state.sectorsize);
+            }
+        }
+        else
+        {
+            g_initiator_state.retrycount = 0;
+            g_initiator_state.sectors_done += numtoread;
+            g_initiator_state.target_file.flush();
+
+            int speed_kbps = numtoread * g_initiator_state.sectorsize / (millis() - time_start);
+            azlog("SCSI read succeeded, sectors done: ",
+                  (int)g_initiator_state.sectors_done, " / ", (int)g_initiator_state.sectorcount,
+                  " speed ", speed_kbps, " kB/s");
+        }
+    }
+}
+
+/*************************************
+ * Low level command implementations *
+ *************************************/
+
+int scsiInitiatorRunCommand(int target_id,
+                            const uint8_t *command, size_t cmdLen,
+                            uint8_t *bufIn, size_t bufInLen,
+                            const uint8_t *bufOut, size_t bufOutLen,
+                            bool returnDataPhase)
+{
+    if (!scsiHostPhySelect(target_id))
+    {
+        azdbg("------ Target ", target_id, " did not respond");
+        scsiHostPhyRelease();
+        return -1;
+    }
+
+    SCSI_PHASE phase;
+    int status = -1;
+    while ((phase = (SCSI_PHASE)scsiHostPhyGetPhase()) != BUS_FREE)
+    {
+        if (phase == MESSAGE_IN)
+        {
+            uint8_t dummy = 0;
+            scsiHostRead(&dummy, 1);
+        }
+        else if (phase == MESSAGE_OUT)
+        {
+            uint8_t identify_msg = 0x80;
+            scsiHostWrite(&identify_msg, 1);
+        }
+        else if (phase == COMMAND)
+        {
+            scsiHostWrite(command, cmdLen);
+        }
+        else if (phase == DATA_IN)
+        {
+            if (returnDataPhase) return 0;
+            if (bufInLen == 0)
+            {
+                azlog("DATA_IN phase but no data to receive!");
+                status = -3;
+                break;
+            }
+
+            if (!scsiHostRead(bufIn, bufInLen))
+            {
+                azlog("scsiHostRead failed, was writing ", bytearray(bufOut, bufOutLen));
+                status = -2;
+                break;
+            }
+        }
+        else if (phase == DATA_OUT)
+        {
+            if (returnDataPhase) return 0;
+            if (bufOutLen == 0)
+            {
+                azlog("DATA_OUT phase but no data to send!");
+                status = -3;
+                break;
+            }
+
+            if (!scsiHostWrite(bufOut, bufOutLen))
+            {
+                azlog("scsiHostWrite failed, was writing ", bytearray(bufOut, bufOutLen));
+                status = -2;
+                break;
+            }
+        }
+        else if (phase == STATUS)
+        {
+            uint8_t tmp = 0;
+            scsiHostRead(&tmp, 1);
+            status = tmp;
+            azdbg("------ STATUS: ", tmp);
+        }
+    }
+
+    scsiHostPhyRelease();
+
+    return status;
+}
+
+bool scsiInitiatorReadCapacity(int target_id, uint32_t *sectorcount, uint32_t *sectorsize)
+{
+    uint8_t command[10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t response[8] = {0};
+    int status = scsiInitiatorRunCommand(target_id,
+                                         command, sizeof(command),
+                                         response, sizeof(response),
+                                         NULL, 0);
+    
+    if (status == 0)
+    {
+        *sectorcount = ((uint32_t)response[0] << 24)
+                    | ((uint32_t)response[1] << 16)
+                    | ((uint32_t)response[2] <<  8)
+                    | ((uint32_t)response[3] <<  0);
+        
+        *sectorcount += 1; // SCSI reports last sector address
+
+        *sectorsize = ((uint32_t)response[4] << 24)
+                    | ((uint32_t)response[5] << 16)
+                    | ((uint32_t)response[6] <<  8)
+                    | ((uint32_t)response[7] <<  0);
+
+        return true;
+    }
+    else
+    {
+        *sectorcount = *sectorsize = 0;
+        return false;
+    }
+} 
+
+// This uses callbacks to run SD and SCSI transfers in parallel
+static struct {
+    uint32_t bytes_sd; // Number of bytes that have been transferred on SD card side
+    uint32_t bytes_sd_scheduled; // Number of bytes scheduled for transfer on SD card side
+    uint32_t bytes_scsi; // Number of bytes that have been scheduled for transfer on SCSI side
+    uint32_t bytes_scsi_done; // Number of bytes that have been transferred on SCSI side
+    
+    uint32_t bytes_per_sector;
+    bool all_ok;
+} g_initiator_transfer;
+
+static void initiatorReadSDCallback(uint32_t bytes_complete)
+{
+    if (g_initiator_transfer.bytes_scsi_done < g_initiator_transfer.bytes_scsi)
+    {
+        // How many bytes remaining in the transfer?
+        uint32_t remain = g_initiator_transfer.bytes_scsi - g_initiator_transfer.bytes_scsi_done;
+        uint32_t len = remain;
+
+        // Limit maximum amount of data transferred at one go, to give enough callbacks to SD driver.
+        // Select the limit based on total bytes in the transfer.
+        // Transfer size is reduced towards the end of transfer to reduce the dead time between
+        // end of SCSI transfer and the SD write completing.
+        uint32_t limit = g_initiator_transfer.bytes_scsi / 8;
+        uint32_t bytesPerSector = g_initiator_transfer.bytes_per_sector;
+        if (limit < PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE) limit = PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE;
+        if (limit > PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE) limit = PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE;
+        if (limit > len) limit = PLATFORM_OPTIMAL_LAST_SD_WRITE_SIZE;
+        if (limit < bytesPerSector) limit = bytesPerSector;
+
+        if (len > limit)
+        {
+            len = limit;
+        }
+
+        // Split read so that it doesn't wrap around buffer edge
+        uint32_t bufsize = sizeof(scsiDev.data);
+        uint32_t start = (g_initiator_transfer.bytes_scsi_done % bufsize);
+        if (start + len > bufsize)
+            len = bufsize - start;
+
+        // Don't overwrite data that has not yet been written to SD card
+        uint32_t sd_ready_cnt = g_initiator_transfer.bytes_sd + bytes_complete;
+        if (g_initiator_transfer.bytes_scsi_done + len > sd_ready_cnt + bufsize)
+            len = sd_ready_cnt + bufsize - g_initiator_transfer.bytes_scsi_done;
+        
+        if (sd_ready_cnt == g_initiator_transfer.bytes_sd_scheduled &&
+            g_initiator_transfer.bytes_sd_scheduled + bytesPerSector <= g_initiator_transfer.bytes_scsi_done)
+        {
+            // Current SD transfer is complete, it is better we return now and offer a chance for the next
+            // transfer to begin.
+            return;
+        }
+
+        // Keep transfers a multiple of sector size.
+        if (remain >= bytesPerSector && len % bytesPerSector != 0)
+        {
+            len -= len % bytesPerSector;
+        }
+
+        if (len == 0)
+            return;
+
+        // azdbg("SCSI read ", (int)start, " + ", (int)len, ", sd ready cnt ", (int)sd_ready_cnt, " ", (int)bytes_complete, ", scsi done ", (int)g_initiator_transfer.bytes_scsi_done);
+        if (!scsiHostRead(&scsiDev.data[start], len))
+        {
+            azlog("Read failed at byte ", (int)g_initiator_transfer.bytes_scsi_done);
+            g_initiator_transfer.all_ok = false;
+        }
+        g_initiator_transfer.bytes_scsi_done += len;
+    }
+}
+
+static void scsiInitiatorWriteDataToSd(FsFile &file, bool use_callback)
+{
+    // Figure out longest continuous block in buffer
+    uint32_t bufsize = sizeof(scsiDev.data);
+    uint32_t start = g_initiator_transfer.bytes_sd % bufsize;
+    uint32_t len = g_initiator_transfer.bytes_scsi_done - g_initiator_transfer.bytes_sd;
+    if (start + len > bufsize) len = bufsize - start;
+
+    // Try to do writes in multiple of 512 bytes
+    // This allows better performance for SD card access.
+    if (len >= 512) len &= ~511;
+
+    // Start writing to SD card and simultaneously reading more from SCSI bus
+    uint8_t *buf = &scsiDev.data[start];
+    // azdbg("SD write ", (int)start, " + ", (int)len);
+
+    if (use_callback)
+    {
+        azplatform_set_sd_callback(&initiatorReadSDCallback, buf);
+    }
+
+    g_initiator_transfer.bytes_sd_scheduled = g_initiator_transfer.bytes_sd + len;
+    if (file.write(buf, len) != len)
+    {
+        azlog("scsiInitiatorReadDataToFile: SD card write failed");
+        g_initiator_transfer.all_ok = false;
+    }
+    azplatform_set_sd_callback(NULL, NULL);
+    g_initiator_transfer.bytes_sd += len;
+}
+
+bool scsiInitiatorReadDataToFile(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize,
+                                 FsFile &file)
+{
+    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
+    };
+
+    // Start executing command, return in data phase
+    int status = scsiInitiatorRunCommand(target_id, command, sizeof(command), NULL, 0, NULL, 0, true);
+
+    if (status != 0)
+    {
+        azlog("scsiInitiatorReadDataToFile: Issuing command failed: ", status);
+        scsiHostPhyRelease();
+        return false;
+    }
+
+    SCSI_PHASE phase;
+
+    g_initiator_transfer.bytes_scsi = sectorcount * sectorsize;
+    g_initiator_transfer.bytes_per_sector = sectorsize;
+    g_initiator_transfer.bytes_sd = 0;
+    g_initiator_transfer.bytes_sd_scheduled = 0;
+    g_initiator_transfer.bytes_scsi_done = 0;
+    g_initiator_transfer.all_ok = true;
+
+    while (true)
+    {
+        phase = (SCSI_PHASE)scsiHostPhyGetPhase();
+        if (phase != DATA_IN && phase != BUS_BUSY)
+        {
+            break;
+        }
+
+        // Read next block from SCSI bus if buffer empty
+        if (g_initiator_transfer.bytes_sd == g_initiator_transfer.bytes_scsi_done)
+        {
+            initiatorReadSDCallback(0);
+        }
+        else
+        {
+            // Write data to SD card and simultaneously read more from SCSI
+            scsiInitiatorWriteDataToSd(file, true);
+        }
+    }
+
+    // Write any remaining buffered data
+    while (g_initiator_transfer.bytes_sd < g_initiator_transfer.bytes_scsi_done)
+    {
+        scsiInitiatorWriteDataToSd(file, false);
+    }
+
+    if (g_initiator_transfer.bytes_sd != g_initiator_transfer.bytes_scsi)
+    {
+        azlog("SCSI read from sector ", (int)start_sector, " was incomplete: expected ",
+             (int)g_initiator_transfer.bytes_scsi, " got ", (int)g_initiator_transfer.bytes_sd, " bytes");
+        g_initiator_transfer.all_ok = false;
+    }
+
+    while ((phase = (SCSI_PHASE)scsiHostPhyGetPhase()) != BUS_FREE)
+    {
+        if (phase == MESSAGE_IN)
+        {
+            uint8_t dummy = 0;
+            scsiHostRead(&dummy, 1);
+        }
+        else if (phase == MESSAGE_OUT)
+        {
+            uint8_t identify_msg = 0x80;
+            scsiHostWrite(&identify_msg, 1);
+        }
+        else if (phase == STATUS)
+        {
+            uint8_t tmp = 0;
+            scsiHostRead(&tmp, 1);
+            status = tmp;
+            azdbg("------ STATUS: ", tmp);
+        }
+    }
+
+    scsiHostPhyRelease();
+
+    return status == 0 && g_initiator_transfer.all_ok;
+}
+
+
+#endif

+ 25 - 0
src/ZuluSCSI_initiator.h

@@ -0,0 +1,25 @@
+// Main state machine for SCSI initiator mode
+
+#pragma once
+
+#include <stdint.h>
+#include <stdlib.h>
+
+void scsiInitiatorInit();
+
+void scsiInitiatorMainLoop();
+
+// Select target and execute SCSI command
+int scsiInitiatorRunCommand(int target_id,
+                            const uint8_t *command, size_t cmdLen,
+                            uint8_t *bufIn, size_t bufInLen,
+                            const uint8_t *bufOut, size_t bufOutLen,
+                            bool returnDataPhase = false);
+
+// Execute READ CAPACITY command
+bool scsiInitiatorReadCapacity(int target_id, uint32_t *sectorcount, uint32_t *sectorsize);
+
+// Read a block of data from SCSI device and write to file on SD card
+class FsFile;
+bool scsiInitiatorReadDataToFile(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize,
+                                 FsFile &file);

+ 35 - 6
src/ZuluSCSI_log_trace.cpp

@@ -10,6 +10,7 @@ extern "C" {
 }
 
 static bool g_LogData = false;
+static bool g_LogInitiatorCommand = false;
 static int g_InByteCount = 0;
 static int g_OutByteCount = 0;
 static uint16_t g_DataChecksum = 0;
@@ -60,9 +61,10 @@ static const char *getCommandName(uint8_t cmd)
     }
 }
 
-static void printNewPhase(int phase)
+static void printNewPhase(int phase, bool initiator = false)
 {
     g_LogData = false;
+    g_LogInitiatorCommand = false;
     if (!g_azlog_debug)
     {
         return;
@@ -83,7 +85,10 @@ static void printNewPhase(int phase)
             break;
         
         case SELECTION:
-            azdbg("---- SELECTION: ", (int)(*SCSI_STS_SELECTED & 7));
+            if (initiator)
+                azdbg("---- SELECTION");
+            else
+                azdbg("---- SELECTION: ", (int)(*SCSI_STS_SELECTED & 7));
             break;
         
         case RESELECTION:
@@ -91,7 +96,12 @@ static void printNewPhase(int phase)
             break;
         
         case STATUS:
-            if (scsiDev.status == GOOD)
+            if (initiator)
+            {
+                azdbg("---- STATUS");
+                g_LogData = true;
+            }
+            else if (scsiDev.status == GOOD)
             {
                 azdbg("---- STATUS: 0 GOOD");
             }
@@ -106,11 +116,12 @@ static void printNewPhase(int phase)
             break;
         
         case COMMAND:
+            g_LogInitiatorCommand = initiator;
             g_LogData = true;
             break;
         
         case DATA_IN:
-            if (scsiDev.target->syncOffset > 0)
+            if (!initiator && scsiDev.target->syncOffset > 0)
                 azdbg("---- DATA_IN, syncOffset ", (int)scsiDev.target->syncOffset,
                                    " syncPeriod ", (int)scsiDev.target->syncPeriod);
             else
@@ -118,7 +129,7 @@ static void printNewPhase(int phase)
             break;
         
         case DATA_OUT:
-            if (scsiDev.target->syncOffset > 0)
+            if (!initiator && scsiDev.target->syncOffset > 0)
                 azdbg("---- DATA_OUT, syncOffset ", (int)scsiDev.target->syncOffset,
                                     " syncPeriod ", (int)scsiDev.target->syncPeriod);
             else
@@ -177,6 +188,24 @@ void scsiLogPhaseChange(int new_phase)
     }
 }
 
+void scsiLogInitiatorPhaseChange(int new_phase)
+{
+    static int old_phase = BUS_FREE;
+
+    if (new_phase != old_phase)
+    {
+        if (old_phase == DATA_IN || old_phase == DATA_OUT)
+        {
+            azdbg("---- Total IN: ", g_InByteCount, " OUT: ", g_OutByteCount, " CHECKSUM: ", (int)g_DataChecksum);
+        }
+        g_InByteCount = g_OutByteCount = 0;
+        g_DataChecksum = 0;
+
+        printNewPhase(new_phase, true);
+        old_phase = new_phase;
+    }
+}
+
 void scsiLogDataIn(const uint8_t *buf, uint32_t length)
 {
     if (g_LogData)
@@ -199,7 +228,7 @@ void scsiLogDataIn(const uint8_t *buf, uint32_t length)
 
 void scsiLogDataOut(const uint8_t *buf, uint32_t length)
 {
-    if (buf == scsiDev.cdb)
+    if (buf == scsiDev.cdb || g_LogInitiatorCommand)
     {
         azdbg("---- COMMAND: ", getCommandName(buf[0]));
     }

+ 1 - 0
src/ZuluSCSI_log_trace.h

@@ -6,5 +6,6 @@
 
 // Called from scsiPhy.cpp
 void scsiLogPhaseChange(int new_phase);
+void scsiLogInitiatorPhaseChange(int new_phase);
 void scsiLogDataIn(const uint8_t *buf, uint32_t length);
 void scsiLogDataOut(const uint8_t *buf, uint32_t length);