Przeglądaj źródła

Merge branch 'main' into f4-usb

Morio 2 lat temu
rodzic
commit
6d68e4f7f0
58 zmienionych plików z 2008 dodań i 189 usunięć
  1. 1 0
      .github/workflows/firmware_build.yml
  2. 1 6
      .gitignore
  3. 7 3
      README.md
  4. 2 1
      lib/SCSI2SD/include/scsi2sd.h
  5. 1 11
      lib/SCSI2SD/src/firmware/diagnostic.c
  6. 1 1
      lib/SCSI2SD/src/firmware/diagnostic.h
  7. 6 6
      lib/SCSI2SD/src/firmware/mode.c
  8. 51 13
      lib/SCSI2SD/src/firmware/scsi.c
  9. 34 2
      lib/SCSI2SD/src/firmware/vendor.c
  10. 1 0
      lib/ZuluSCSI_platform_GD32F205/ZuluSCSI_platform.h
  11. 10 1
      lib/ZuluSCSI_platform_GD32F205/scsiPhy.cpp
  12. 27 0
      lib/ZuluSCSI_platform_GD32F205/scsi_accel_dma.cpp
  13. 2 0
      lib/ZuluSCSI_platform_GD32F205/sd_card_sdio.cpp
  14. 17 4
      lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.cpp
  15. 10 3
      lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.h
  16. 171 0
      lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio_Pico.h
  17. 0 0
      lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio_RP2040.h
  18. 28 5
      lib/ZuluSCSI_platform_RP2040/audio.cpp
  19. 9 4
      lib/ZuluSCSI_platform_RP2040/run_pioasm.sh
  20. 6 3
      lib/ZuluSCSI_platform_RP2040/scsiHostPhy.cpp
  21. 1 1
      lib/ZuluSCSI_platform_RP2040/scsiPhy.cpp
  22. 12 4
      lib/ZuluSCSI_platform_RP2040/scsi_accel_host.cpp
  23. 46 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_host_Pico.pio
  24. 43 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_host_Pico.pio.h
  25. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_host_RP2040.pio
  26. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_host_RP2040.pio.h
  27. 6 4
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target.cpp
  28. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target.h
  29. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_BS2.pio
  30. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_BS2.pio.h
  31. 124 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_Pico.pio
  32. 225 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_Pico.pio.h
  33. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_RP2040.pio
  34. 0 0
      lib/ZuluSCSI_platform_RP2040/scsi_accel_target_RP2040.pio.h
  35. 4 2
      lib/ZuluSCSI_platform_RP2040/sd_card_sdio.cpp
  36. 6 4
      lib/ZuluSCSI_platform_RP2040/sdio.cpp
  37. 0 0
      lib/ZuluSCSI_platform_RP2040/sdio.h
  38. 0 0
      lib/ZuluSCSI_platform_RP2040/sdio_BS2.pio
  39. 0 0
      lib/ZuluSCSI_platform_RP2040/sdio_BS2.pio.h
  40. 164 0
      lib/ZuluSCSI_platform_RP2040/sdio_Pico.pio
  41. 121 0
      lib/ZuluSCSI_platform_RP2040/sdio_Pico.pio.h
  42. 0 0
      lib/ZuluSCSI_platform_RP2040/sdio_RP2040.pio
  43. 0 0
      lib/ZuluSCSI_platform_RP2040/sdio_RP2040.pio.h
  44. 23 6
      platformio.ini
  45. 24 40
      src/ZuluSCSI.cpp
  46. 30 1
      src/ZuluSCSI_audio.h
  47. 4 3
      src/ZuluSCSI_bootloader.cpp
  48. 396 30
      src/ZuluSCSI_cdrom.cpp
  49. 5 1
      src/ZuluSCSI_cdrom.h
  50. 4 1
      src/ZuluSCSI_config.h
  51. 268 20
      src/ZuluSCSI_disk.cpp
  52. 28 6
      src/ZuluSCSI_disk.h
  53. 16 1
      src/ZuluSCSI_log_trace.cpp
  54. 63 2
      src/ZuluSCSI_mode.cpp
  55. 1 0
      src/ZuluSCSI_mode.h
  56. 5 0
      src/ZuluSCSI_presets.cpp
  57. 1 0
      src/ZuluSCSI_presets.h
  58. 3 0
      zuluscsi.ini

+ 1 - 0
.github/workflows/firmware_build.yml

@@ -3,6 +3,7 @@ name: Build ZuluSCSI firmware
 on: 
   push:
   workflow_dispatch:
+  pull_request:
 
 jobs:
   build_firmware:

+ 1 - 6
.gitignore

@@ -1,7 +1,2 @@
 .pio
-.vscode/.browse.c_cpp.db*
-.vscode/c_cpp_properties.json
-.vscode/launch.json
-.vscode/ipch
-.vscode/extensions.json
-.vscode/
+.vscode

+ 7 - 3
README.md

@@ -7,7 +7,7 @@ ZuluSCSI uses raw hard drive image files, which are stored on a FAT32 or exFAT-f
 
 Examples of valid filenames:
 * `HD5.hda` or `HD5.img`: hard drive with SCSI ID 5
-* `HD20_512.hda`: hard drive with SCSI ID 2, LUN 0, block size 512
+* `HD20_512.hda`: hard drive with SCSI ID 2, LUN 0, block size 512. Currently, ZuluSCSI does not support multiple LUNs, only LUN 0.
 * `CD3.iso`: CD drive with SCSI ID 3
 
 In addition to the simplified filenames style above, the ZuluSCSI firmware also looks for images using the BlueSCSI-style "HDxy_512.hda" filename formatting.
@@ -31,9 +31,11 @@ Creating new image files
 Empty image files can be created using operating system tools:
 
 * Windows: `fsutil file createnew HD1.img 1073741824` (1 GB)
-* Linux: `fallocate -l 1G HD1.img`
+* Linux: `truncate -s 1G HD1.img`
 * Mac OS X: `mkfile -n 1g HD1.img`
 
+If you need to use image files larger than 4GB, you _must_ use an exFAT-formatted SD card, as the FAT32 filesystem does not support files larger than 4,294,967,295 bytes (4GB-1 byte).
+
 ZuluSCSI firmware can also create image files itself.
 To do this, create a text file with filename such as `Create 1024M HD40.txt`.
 The special filename must start with "Create" and be followed by file size and the name of resulting image file.
@@ -101,11 +103,13 @@ 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:
+ZuluSCSI RP2040 Full Size 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.
+Later (Rev2023a) ZuluSCSI RP2040 Full Size boards have a bootloader button instead of a DIP switch. 
+
 
 Physical eject button for CDROM
 -------------------------------

+ 2 - 1
lib/SCSI2SD/include/scsi2sd.h

@@ -83,7 +83,8 @@ typedef enum
 	S2S_CFG_QUIRKS_APPLE = 1,
 	S2S_CFG_QUIRKS_OMTI = 2,
 	S2S_CFG_QUIRKS_XEBEC = 4,
-	S2S_CFG_QUIRKS_VMS = 8
+	S2S_CFG_QUIRKS_VMS = 8,
+	S2S_CFG_QUIRKS_EMU = 9
 } S2S_CFG_QUIRKS;
 
 typedef enum

+ 1 - 11
lib/SCSI2SD/src/firmware/diagnostic.c

@@ -196,7 +196,7 @@ void scsiReadBuffer()
 }
 
 // Callback after the DATA OUT phase is complete.
-static void doWriteBuffer(void)
+void doWriteBuffer(void)
 {
 	if (scsiDev.status == GOOD) // skip if we've already encountered an error
 	{
@@ -234,14 +234,4 @@ void scsiWriteBuffer()
 	}
 }
 
-// XEBEC specific command. See
-// http://www.bitsavers.org/pdf/westernDigital/WD100x/79-000004_WD1002-SHD_OEM_Manual_Aug1984.pdf
-// Section 4.3.14
-void scsiWriteSectorBuffer()
-{
-	scsiDev.dataLen = scsiDev.target->liveCfg.bytesPerSector;
-	scsiDev.phase = DATA_OUT;
-	scsiDev.postDataOutHook = doWriteBuffer;
-}
-
 

+ 1 - 1
lib/SCSI2SD/src/firmware/diagnostic.h

@@ -19,8 +19,8 @@
 
 void scsiSendDiagnostic(void);
 void scsiReceiveDiagnostic(void);
+void doWriteBuffer(void);
 void scsiWriteBuffer(void);
-void scsiWriteSectorBuffer(void);
 void scsiReadBuffer(void);
 
 #endif

+ 6 - 6
lib/SCSI2SD/src/firmware/mode.c

@@ -242,8 +242,8 @@ static const uint8_t SequentialDeviceConfigPage[] =
 static const uint8_t AppleVendorPage[] =
 {
 0x30, // Page code
-23, // Page length
-'A','P','P','L','E',' ','C','O','M','P','U','T','E','R',',',' ','I','N','C',' ',' ',' ',0x00
+0x16, // Page length
+'A','P','P','L','E',' ','C','O','M','P','U','T','E','R',',',' ','I','N','C',' ',' ',' '
 };
 
 static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
@@ -514,10 +514,10 @@ static void doModeSense(
 		idx += sizeof(SequentialDeviceConfigPage);
 	}
 
-	if ((
-			(scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_APPLE) ||
-			(idx + sizeof(AppleVendorPage) <= allocLength)
-		) &&
+	idx += modeSenseCDCapabilitiesPage(pc, idx, pageCode, &pageFound);
+
+	if ((scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_APPLE) &&
+	    (idx + sizeof(AppleVendorPage) <= allocLength) &&
 		(pageCode == 0x30 || pageCode == 0x3F))
 	{
 		pageFound = 1;

+ 51 - 13
lib/SCSI2SD/src/firmware/scsi.c

@@ -382,14 +382,57 @@ static void process_Command()
 		{
 			// Completely non-standard
 			allocLength = 4;
-			if (scsiDev.target->sense.code == NO_SENSE)
-				scsiDev.data[0] = 0;
-			else if (scsiDev.target->sense.code == ILLEGAL_REQUEST)
-				scsiDev.data[0] = 0x20; // Illegal command
-			else if (scsiDev.target->sense.code == NOT_READY)
-				scsiDev.data[0] = 0x04; // Drive not ready
-			else
-				scsiDev.data[0] = 0x11;  // Uncorrectable data error
+
+			switch (scsiDev.target->sense.code)
+			{
+				case NO_SENSE:
+					scsiDev.data[0] = 0;
+					break;
+				case MEDIUM_ERROR:
+					switch (scsiDev.target->sense.asc)
+					{
+						case NO_SEEK_COMPLETE:
+							scsiDev.data[0] = 0x15; // Seek Error
+							break;
+						case WRITE_ERROR_AUTO_REALLOCATION_FAILED:
+							scsiDev.data[0] = 0x03; // Write fault
+							break;
+						default:
+						case UNRECOVERED_READ_ERROR:
+							scsiDev.data[0] = 0x11; // Uncorrectable read error
+							break;
+					}
+					break;
+				case ILLEGAL_REQUEST:
+					switch (scsiDev.target->sense.asc)
+					{
+						case LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE:
+							scsiDev.data[0] = 0x14; // Target sector not found
+							break;
+						case WRITE_PROTECTED:
+							scsiDev.data[0] = 0x03; // Write fault
+							break;
+						default:
+							scsiDev.data[0] = 0x20; // Invalid command
+							break;
+					}
+					break;
+				case NOT_READY:
+					switch (scsiDev.target->sense.asc)
+					{
+						default:
+						case MEDIUM_NOT_PRESENT:
+							scsiDev.data[0] = 0x04; // Drive not ready
+							break;
+						case LOGICAL_UNIT_NOT_READY_INITIALIZING_COMMAND_REQUIRED:
+							scsiDev.data[0] = 0x1A; // Format Error
+							break;
+					}
+					break;
+				default:
+					scsiDev.data[0] = 0x11;  // Uncorrectable data error
+					break;
+			}
 
 			scsiDev.data[1] = (scsiDev.cdb[1] & 0x20) | ((transfer.lba >> 16) & 0x1F);
 			scsiDev.data[2] = transfer.lba >> 8;
@@ -540,11 +583,6 @@ static void process_Command()
 	{
 		scsiWriteBuffer();
 	}
-	else if (command == 0x0f &&
-		scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_XEBEC)
-	{
-		scsiWriteSectorBuffer();
-	}
 	else if (command == 0x3C)
 	{
 		scsiReadBuffer();

+ 34 - 2
lib/SCSI2SD/src/firmware/vendor.c

@@ -17,12 +17,27 @@
 
 #include "scsi.h"
 #include "vendor.h"
-
+#include "diagnostic.h"
 
 // Callback after the DATA OUT phase is complete.
 static void doAssignDiskParameters(void)
 {
-	scsiDev.phase = STATUS;
+	if (scsiDev.status == GOOD)
+	{
+		scsiDev.phase = STATUS;
+	}
+}
+
+// XEBEC specific commands
+// http://www.bitsavers.org/pdf/xebec/104524C_S1410Man_Aug83.pdf
+// WD100x seems to be identical to the Xebec but calls this command "Set Parameters"
+// http://www.bitsavers.org/pdf/westernDigital/WD100x/79-000004_WD1002-SHD_OEM_Manual_Aug1984.pdf
+static void doXebecInitializeDriveCharacteristics()
+{
+	if (scsiDev.status == GOOD)
+	{
+		scsiDev.phase = STATUS;
+	}
 }
 
 int scsiVendorCommand()
@@ -48,6 +63,23 @@ int scsiVendorCommand()
 		scsiDev.phase = DATA_OUT;
 		scsiDev.postDataOutHook = doAssignDiskParameters;
 	}
+	else if (command == 0x0C &&
+		scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_XEBEC)
+	{
+		// XEBEC S1410: "Initialize Drive Characteristics"
+		// WD100x: "Set Parameters"
+		scsiDev.dataLen = 8;
+		scsiDev.phase = DATA_OUT;
+		scsiDev.postDataOutHook = doXebecInitializeDriveCharacteristics;
+	}
+	else if (command == 0x0F &&
+		scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_XEBEC)
+	{
+		// XEBEC S1410, WD100x: "Write Sector Buffer"
+		scsiDev.dataLen = scsiDev.target->liveCfg.bytesPerSector;
+		scsiDev.phase = DATA_OUT;
+		scsiDev.postDataOutHook = doWriteBuffer;
+	}
 	else
 	{
 		commandHandled = 0;

+ 1 - 0
lib/ZuluSCSI_platform_GD32F205/ZuluSCSI_platform.h

@@ -190,6 +190,7 @@ extern SdioConfig g_sd_sdio_config_crash;
 
 // Check if a DMA request for SD card read has completed.
 // This is used to optimize the timing of data transfers on SCSI bus.
+// When called outside of SD callback processing, always returns false.
 bool check_sd_read_done();
 
 #endif

+ 10 - 1
lib/ZuluSCSI_platform_GD32F205/scsiPhy.cpp

@@ -447,8 +447,17 @@ static bool isPollingWriteFinished(const uint8_t *data)
 extern "C" bool scsiIsWriteFinished(const uint8_t *data)
 {
     // Check if there is still a polling transfer in progress
-    if (!isPollingWriteFinished(data) && !check_sd_read_done())
+    if (!isPollingWriteFinished(data))
     {
+        if (check_sd_read_done())
+        {
+            // Current SD card transfer is finished so return early
+            // to start a new transfer before doing SCSI data transfer.
+            // This is faster because the SD transfer can run on background,
+            // but PIO mode SCSI transfer cannot.
+            return false;
+        }
+
         // Process the transfer piece-by-piece while waiting
         // for SD card to react.
         int max_count = g_scsi_writereq.count / 8;

+ 27 - 0
lib/ZuluSCSI_platform_GD32F205/scsi_accel_dma.cpp

@@ -21,6 +21,8 @@
 
 #include "scsi_accel_dma.h"
 #include <ZuluSCSI_log.h>
+#include <ZuluSCSI_disk.h>
+#include <scsi2sd.h>
 #include <gd32f20x_timer.h>
 #include <gd32f20x_rcu.h>
 #include <assert.h>
@@ -67,6 +69,20 @@ static bool g_scsi_dma_use_greenpak;
 
 void scsi_accel_timer_dma_init()
 {
+    // TODO: find root cause of of DMA timeout
+    // Temporary fix, setting prefetch prefetch buffer to 4k seems to fix a timing issue
+    // in Timer DMA SCSI phy mode only
+    
+    image_config_t* img;
+    for (uint8_t i = 0; i < S2S_MAX_TARGETS; i++)
+    {
+        img = &scsiDiskGetImageConfig(i);
+        if (img->prefetchbytes > 4096)
+        {
+            img->prefetchbytes = 4096;
+        }
+    }
+
     g_scsi_dma_state = SCSIDMA_IDLE;
     g_scsi_dma_use_greenpak = false;
     rcu_periph_clock_enable(SCSI_TIMER_RCU);
@@ -369,6 +385,17 @@ extern "C" void SCSI_TIMER_DMACHB_IRQ()
         }
         else
         {
+            // Wait for final ACK to go low, shouldn't take long.
+            int maxwait = 10000;
+            while (TIMER_CNT(SCSI_TIMER) < 1)
+            {
+                if (maxwait-- < 0)
+                {
+                    logmsg("SCSI_TIMER_DMACHB_IRQ: timeout waiting for final ACK");
+                    break;
+                }
+            }
+
             // No more data available
             stop_dma();
         }

+ 2 - 0
lib/ZuluSCSI_platform_GD32F205/sd_card_sdio.cpp

@@ -318,6 +318,8 @@ bool SdioCard::readSectors(uint32_t sector, uint8_t* dst, size_t n)
 // This is used to optimize the timing of data transfers on SCSI bus.
 bool check_sd_read_done()
 {
+    if (!m_stream_callback) return false;
+
     return (DMA_CHCTL(DMA1, DMA_CH3) & DMA_CHXCTL_CHEN)
         && (DMA_INTF(DMA1) & DMA_FLAG_ADD(DMA_FLAG_FTF, DMA_CH3));
 }

+ 17 - 4
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.cpp

@@ -38,7 +38,7 @@
 #include <multicore.h>
 #include <USB/PluggableUSBSerial.h>
 #include "audio.h"
-#include "scsi_accel_rp2040.h"
+#include "scsi_accel_target.h"
 
 extern "C" {
 
@@ -121,27 +121,38 @@ void platform_init()
     /* Check dip switch settings */
 #ifdef HAS_DIP_SWITCHES
     gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, false, false, false, false, false);
+# ifndef ZULUSCSI_PICO
     gpio_conf(DIP_DBGLOG,     GPIO_FUNC_SIO, false, false, false, false, false);
     gpio_conf(DIP_TERM,       GPIO_FUNC_SIO, false, false, false, false, false);
-
+# endif    
     delay(10); // 10 ms delay to let pull-ups do their work
-
+# ifndef ZULUSCSI_PICO
     bool dbglog = !gpio_get(DIP_DBGLOG);
     bool termination = !gpio_get(DIP_TERM);
+# endif
 #else
     delay(10);
 #endif
 
+#ifndef DISABLE_SWO
     /* Initialize logging to SWO pin (UART0) */
     gpio_conf(SWO_PIN,        GPIO_FUNC_UART,false,false, true,  false, true);
     uart_init(uart0, 1000000);
     g_uart_initialized = true;
+#endif
+
     mbed_set_error_hook(mbed_error_hook);
 
     logmsg("Platform: ", g_platform_name);
     logmsg("FW Version: ", g_log_firmwareversion);
 
-#ifdef HAS_DIP_SWITCHES
+#ifdef ZULUSCSI_PICO
+    logmsg("SCSI termination is determined by the DIP switch labeled \"TERM\"");
+    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 boards");
+    g_log_debug = false;
+
+#elif defined(HAS_DIP_SWITCHES)
     logmsg("DIP switch settings: debug log ", (int)dbglog, ", termination ", (int)termination);
     g_log_debug = dbglog;
 
@@ -326,6 +337,8 @@ void platform_late_init()
         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);
+        // Reinitialize OUT_RST to output mode. On RP Pico variant the pin is shared with IN_RST.
+        gpio_conf(SCSI_OUT_RST,   GPIO_FUNC_SIO, false, false, true,  true, true);
         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);

+ 10 - 3
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.h

@@ -26,12 +26,15 @@
 #include <stdint.h>
 #include <Arduino.h>
 
-#ifdef ZULUSCSI_BS2
+#ifdef ZULUSCSI_PICO
+// ZuluSCSI Pico carrier board variant
+#include "ZuluSCSI_platform_gpio_Pico.h"
+#elif defined(ZULUSCSI_BS2)
 // BS2 hardware variant, using Raspberry Pico board on a carrier PCB
 #include "ZuluSCSI_platform_gpio_BS2.h"
 #else
 // Normal RP2040 variant, using RP2040 chip directly
-#include "ZuluSCSI_platform_gpio.h"
+#include "ZuluSCSI_platform_gpio_RP2040.h"
 #endif
 
 #include "scsiHostPhy.h"
@@ -44,7 +47,11 @@ extern "C" {
 /* These are used in debug output and default SCSI strings */
 extern const char *g_platform_name;
 
-#ifdef ZULUSCSI_BS2
+#ifdef ZULUSCSI_PICO
+# define PLATFORM_NAME "ZuluSCSI Pico"
+# define PLATFORM_REVISION "2.0"
+# define PLATFORM_HAS_INITIATOR_MODE 1
+#elif defined(ZULUSCSI_BS2)
 # define PLATFORM_NAME "ZuluSCSI BS2"
 # define PLATFORM_REVISION "1.0"
 #else

+ 171 - 0
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio_Pico.h

@@ -0,0 +1,171 @@
+/** 
+ * ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+ * 
+ * ZuluSCSI™ firmware is licensed under the GPL version 3 or any later version. 
+ * 
+ * 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/>.
+**/
+
+// GPIO definitions for ZuluSCSI RP2040-based hardware
+
+#pragma once
+
+#include <hardware/gpio.h>
+
+// SCSI data input/output port.
+// The data bus uses external bidirectional buffer, with
+// direction controlled by DATA_DIR pin.
+#define SCSI_IO_DB0  0
+#define SCSI_IO_DB1  1
+#define SCSI_IO_DB2  2
+#define SCSI_IO_DB3  3
+#define SCSI_IO_DB4  4
+#define SCSI_IO_DB5  5
+#define SCSI_IO_DB6  6
+#define SCSI_IO_DB7  7
+#define SCSI_IO_DBP  8
+#define SCSI_IO_DATA_MASK 0x1FF
+#define SCSI_IO_SHIFT 0
+
+// Data direction control
+#define SCSI_DATA_DIR 9
+
+// SCSI output status lines
+#define SCSI_OUT_IO   22
+#define SCSI_OUT_CD   18
+#define SCSI_OUT_MSG  20
+#define SCSI_OUT_RST  21
+#define SCSI_OUT_BSY  27
+#define SCSI_OUT_REQ  17
+#define SCSI_OUT_SEL  19
+
+// SCSI input status signals
+#define SCSI_IN_SEL  18
+#define SCSI_IN_ACK  26
+#define SCSI_IN_ATN  28
+#define SCSI_IN_BSY  20
+#define SCSI_IN_RST  21
+
+// Status line outputs for initiator mode
+#define SCSI_OUT_ACK  26
+#define SCSI_OUT_ATN  28
+
+// Status line inputs for initiator mode
+#define SCSI_IN_IO    22
+#define SCSI_IN_CD    18
+#define SCSI_IN_MSG   20
+#define SCSI_IN_REQ   17
+
+// Status LED pins
+#define LED_PIN      16
+#define LED_ON()     sio_hw->gpio_set = 1 << LED_PIN
+#define LED_OFF()    sio_hw->gpio_clr = 1 << LED_PIN
+
+// SD card pins in SDIO mode
+#define SDIO_CLK 10
+#define SDIO_CMD 11
+#define SDIO_D0  12
+#define SDIO_D1  13
+#define SDIO_D2  14
+#define SDIO_D3  15
+
+// SD card pins in SPI mode
+#define SD_SPI       spi0
+#define SD_SPI_SCK   10
+#define SD_SPI_MOSI  11
+#define SD_SPI_MISO  12
+#define SD_SPI_CS    15
+
+#ifndef ENABLE_AUDIO_OUTPUT
+    // No spare pins for I2C
+    // IO expander I2C
+    // #define GPIO_I2C_SDA 14
+    // #define GPIO_I2C_SCL 15
+#else
+    // IO expander I2C pins being used as SPI for audio
+    #define AUDIO_SPI      spi1
+    #define GPIO_EXP_SPARE 14
+    #define GPIO_EXP_AUDIO 15
+#endif
+
+// DIP switch pins
+#define HAS_DIP_SWITCHES
+#define DIP_INITIATOR 28
+#define DIP_DBGLOG 17
+#define DIP_TERM 22
+
+// Other pins
+#define SWO_PIN 16
+
+// Below are GPIO access definitions that are used from scsiPhy.cpp.
+
+// Write a single SCSI pin.
+// Example use: SCSI_OUT(ATN, 1) sets SCSI_ATN to low (active) state.
+#define SCSI_OUT(pin, state) \
+    *(state ? &sio_hw->gpio_clr : &sio_hw->gpio_set) = 1 << (SCSI_OUT_ ## pin)
+
+// Read a single SCSI pin.
+// Example use: SCSI_IN(ATN), returns 1 for active low state.
+#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) | \
+                           (1 << SCSI_OUT_MSG))
+
+// Set SCSI data bus to output
+#define SCSI_ENABLE_DATA_OUT() \
+    (sio_hw->gpio_clr = (1 << SCSI_DATA_DIR), \
+     sio_hw->gpio_oe_set = SCSI_IO_DATA_MASK)
+
+// Write SCSI data bus, also sets REQ to inactive.
+#define SCSI_OUT_DATA(data) \
+    gpio_put_masked(SCSI_IO_DATA_MASK | (1 << SCSI_OUT_REQ), \
+                    g_scsi_parity_lookup[(uint8_t)(data)] | (1 << SCSI_OUT_REQ)), \
+    SCSI_ENABLE_DATA_OUT()
+
+// Release SCSI data bus and REQ signal
+#define SCSI_RELEASE_DATA_REQ() \
+    (sio_hw->gpio_oe_clr = SCSI_IO_DATA_MASK, \
+     sio_hw->gpio_set = (1 << SCSI_DATA_DIR) | (1 << SCSI_OUT_REQ))
+
+// Release all SCSI outputs
+#define SCSI_RELEASE_OUTPUTS() \
+    SCSI_RELEASE_DATA_REQ(), \
+    sio_hw->gpio_oe_clr = (1 << SCSI_OUT_CD) | \
+                          (1 << SCSI_OUT_MSG), \
+    sio_hw->gpio_set = (1 << SCSI_OUT_IO) | \
+                       (1 << SCSI_OUT_CD) | \
+                       (1 << SCSI_OUT_MSG) | \
+                       (1 << SCSI_OUT_RST) | \
+                       (1 << SCSI_OUT_BSY) | \
+                       (1 << SCSI_OUT_REQ) | \
+                       (1 << SCSI_OUT_SEL)
+
+// Read SCSI data bus
+#define SCSI_IN_DATA() \
+    (~sio_hw->gpio_in & SCSI_IO_DATA_MASK) >> SCSI_IO_SHIFT
+

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio.h → lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform_gpio_RP2040.h


+ 28 - 5
lib/ZuluSCSI_platform_RP2040/audio.cpp

@@ -142,8 +142,12 @@ static audio_status_code audio_last_status[8] = {ASC_NO_STATUS};
 
 // volume information for targets
 static volatile uint16_t volumes[8] = {
-    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL,
-    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL
+    DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH,
+    DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH, DEFAULT_VOLUME_LEVEL_2CH
+};
+static volatile uint16_t channels[8] = {
+    AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK,
+    AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK
 };
 
 // mechanism for cleanly stopping DMA units
@@ -165,10 +169,17 @@ static uint8_t invert = 0; // biphase encode help: set if last wire bit was '1'
  */
 static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len, uint8_t swap) {
     uint16_t wvol = volumes[audio_owner & 7];
-    uint8_t vol = ((wvol >> 8) + (wvol & 0xFF)) >> 1; // average of both values
+    uint8_t lvol = ((wvol >> 8) + (wvol & 0xFF)) >> 1; // average of both values
     // limit maximum volume; with my DACs I've had persistent issues
     // with signal clipping when sending data in the highest bit position
-    vol = vol >> 2;
+    lvol = lvol >> 2;
+    uint8_t rvol = lvol;
+    // enable or disable based on the channel information for both output
+    // ports, where the high byte and mask control the right channel, and
+    // the low control the left channel
+    uint16_t chn = channels[audio_owner & 7] & AUDIO_CHANNEL_ENABLE_MASK;
+    if (!(chn >> 8)) rvol = 0;
+    if (!(chn & 0xFF)) lvol = 0;
 
     uint16_t widx = 0;
     for (uint16_t i = 0; i < len; i += 2) {
@@ -182,7 +193,11 @@ static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len,
                 rsamp = (int16_t)(samples[i] + (samples[i + 1] << 8));
             }
             // linear scale to requested audio value
-            rsamp *= vol;
+            if (i & 2) {
+                rsamp *= rvol;
+            } else {
+                rsamp *= lvol;
+            }
             // use 20 bits of value only, which allows ignoring the lowest 8
             // bits during biphase conversion (after including sample shift)
             sample = ((uint32_t)rsamp) & 0xFFFFF0;
@@ -562,4 +577,12 @@ void audio_set_volume(uint8_t id, uint16_t vol) {
     volumes[id & 7] = vol;
 }
 
+uint16_t audio_get_channel(uint8_t id) {
+    return channels[id & 7];
+}
+
+void audio_set_channel(uint8_t id, uint16_t chn) {
+    channels[id & 7] = chn;
+}
+
 #endif // ENABLE_AUDIO_OUTPUT

+ 9 - 4
lib/ZuluSCSI_platform_RP2040/run_pioasm.sh

@@ -2,8 +2,13 @@
 
 # This script regenerates the .pio.h files from .pio
 
-pioasm rp2040_sdio.pio rp2040_sdio.pio.h
-pioasm rp2040_sdio_BS2.pio rp2040_sdio_BS2.pio.h
+pioasm sdio_RP2040.pio sdio_RP2040.pio.h
+pioasm sdio_Pico.pio sdio_Pico.pio.h
+pioasm sdio_BS2.pio sdio_BS2.pio.h
 
-pioasm scsi_accel.pio scsi_accel.pio.h
-pioasm scsi_accel_BS2.pio scsi_accel_BS2.pio.h
+pioasm scsi_accel_target_RP2040.pio scsi_accel_target_RPP2040.pio.h
+pioasm scsi_accel_target_Pico.pio scsi_accel_target_Pico.pio.h
+pioasm scsi_accel_target_BS2.pio scsi_accel_target_BS2.pio.h
+
+pioasm scsi_accel_host_RP2040.pio scsi_accel_host_RP2040.pio.h
+pioasm scsi_accel_host_Pico.pio scsi_accel_host_Pico.pio.h

+ 6 - 3
lib/ZuluSCSI_platform_RP2040/scsiHostPhy.cpp

@@ -86,12 +86,15 @@ bool scsiHostPhySelect(int target_id)
         }
     }
 
+    // Choose initiator ID different than target ID
+    uint8_t initiator_id = (target_id == 7) ? 0 : 7;
+
     // Selection phase
     scsiLogInitiatorPhaseChange(SELECTION);
-    dbgmsg("------ SELECTING ", target_id);
+    dbgmsg("------ SELECTING ", target_id, " with initiator ID ", (int)initiator_id);
     SCSI_OUT(SEL, 1);
     delayMicroseconds(5);
-    SCSI_OUT_DATA(1 << target_id);
+    SCSI_OUT_DATA((1 << target_id) | (1 << initiator_id));
     delayMicroseconds(5);
     SCSI_OUT(BSY, 0);
 
@@ -300,4 +303,4 @@ void scsiHostPhyRelease()
     SCSI_RELEASE_OUTPUTS();
 }
 
-#endif
+#endif

+ 1 - 1
lib/ZuluSCSI_platform_RP2040/scsiPhy.cpp

@@ -29,7 +29,7 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_config.h"
-#include "scsi_accel_rp2040.h"
+#include "scsi_accel_target.h"
 #include "hardware/structs/iobank0.h"
 
 #include <scsi2sd.h>

+ 12 - 4
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.cpp

@@ -24,7 +24,6 @@
 #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>
@@ -32,6 +31,12 @@
 #include <hardware/sync.h>
 
 #ifdef PLATFORM_HAS_INITIATOR_MODE
+#ifdef ZULUSCSI_PICO
+#include "scsi_accel_host_Pico.pio.h"
+#else
+#include "scsi_accel_host_RP2040.pio.h"
+#endif
+
 
 #define SCSI_PIO pio0
 #define SCSI_SM 0
@@ -59,14 +64,16 @@ static void scsi_accel_host_config_gpio()
         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_IN_REQ].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);
+        pio_sm_set_pins(SCSI_PIO, SCSI_SM, SCSI_IO_DATA_MASK | 1 << SCSI_IN_REQ | 1 << SCSI_OUT_ACK);
+        pio_sm_set_consecutive_pindirs(SCSI_PIO, SCSI_SM, SCSI_IO_DB0, 9, false);
+        pio_sm_set_consecutive_pindirs(SCSI_PIO, SCSI_SM, SCSI_IN_REQ, 1, false);
+        pio_sm_set_consecutive_pindirs(SCSI_PIO, SCSI_SM, SCSI_OUT_ACK, 1, true);
 
         iobank0_hw->io[SCSI_IO_DB0].ctrl  = GPIO_FUNC_SIO;
         iobank0_hw->io[SCSI_IO_DB1].ctrl  = GPIO_FUNC_SIO;
@@ -77,6 +84,7 @@ static void scsi_accel_host_config_gpio()
         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_IN_REQ].ctrl  = GPIO_FUNC_SIO;
         iobank0_hw->io[SCSI_OUT_ACK].ctrl = GPIO_FUNC_PIO0;
     }
 }

+ 46 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host_Pico.pio

@@ -0,0 +1,46 @@
+; ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+; 
+; ZuluSCSI™ firmware is licensed under the GPL version 3 or any later version. 
+; 
+; 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/>.
+
+
+; 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 17
+.define ACK 26
+
+; 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

+ 43 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host_Pico.pio.h

@@ -0,0 +1,43 @@
+// -------------------------------------------------- //
+// 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     
+    0x3011, //  2: wait   0 gpio, 17      side 1     
+    0x4009, //  3: in     pins, 9         side 0     
+    0x4067, //  4: in     null, 7         side 0     
+    0x2091, //  5: wait   1 gpio, 17      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

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.pio → lib/ZuluSCSI_platform_RP2040/scsi_accel_host_RP2040.pio


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.pio.h → lib/ZuluSCSI_platform_RP2040/scsi_accel_host_RP2040.pio.h


+ 6 - 4
lib/ZuluSCSI_platform_RP2040/scsi_accel_rp2040.cpp → lib/ZuluSCSI_platform_RP2040/scsi_accel_target.cpp

@@ -29,7 +29,7 @@
 
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_log.h"
-#include "scsi_accel_rp2040.h"
+#include "scsi_accel_target.h"
 #include <hardware/pio.h>
 #include <hardware/dma.h>
 #include <hardware/irq.h>
@@ -38,10 +38,12 @@
 #include <audio.h>
 #include <multicore.h>
 
-#ifdef ZULUSCSI_BS2
-#include "scsi_accel_BS2.pio.h"
+#ifdef ZULUSCSI_PICO
+#include "scsi_accel_target_Pico.pio.h"
+#elif defined(ZULUSCSI_BS2)
+#include "scsi_accel_target_BS2.pio.h"
 #else
-#include "scsi_accel.pio.h"
+#include "scsi_accel_target_RP2040.pio.h"
 #endif
 
 // SCSI bus write acceleration uses up to 3 PIO state machines:

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_rp2040.h → lib/ZuluSCSI_platform_RP2040/scsi_accel_target.h


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_BS2.pio → lib/ZuluSCSI_platform_RP2040/scsi_accel_target_BS2.pio


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_BS2.pio.h → lib/ZuluSCSI_platform_RP2040/scsi_accel_target_BS2.pio.h


+ 124 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_target_Pico.pio

@@ -0,0 +1,124 @@
+; ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+; 
+; ZuluSCSI™ firmware is licensed under the GPL version 3 or any later version. 
+; 
+; 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/>.
+
+
+; RP2040 PIO program for accelerating SCSI communication
+; Run "pioasm scsi_accel.pio scsi_accel.pio.h" to regenerate the C header from this.
+; GPIO mapping:
+; - 0-7: DB0-DB7
+; -   8: DBP
+; Side set is REQ pin
+
+.define REQ 17
+.define ACK 26
+
+; Delay from data setup to REQ assertion.
+; deskew delay + cable skew delay = 55 ns minimum
+; One clock cycle is 8 ns => delay 7 clocks
+.define REQ_DLY 7
+
+; Adds parity to data that is to be written to SCSI
+; This works by generating addresses for DMA to fetch data from.
+; Register X should be initialized to the base address of the lookup table.
+.program scsi_parity
+    pull block
+    in NULL, 1
+    in OSR, 8
+    in X, 23
+
+; Write to SCSI bus using asynchronous handshake.
+; Data is written as 32-bit words that contain the 8 data bits + 1 parity bit.
+; 23 bits in each word are discarded.
+; Number of bytes to send must be multiple of 2.
+.program scsi_accel_async_write
+    .side_set 1
+
+    pull ifempty block          side 1  ; Get data from TX FIFO
+    out pins, 9                 side 1  ; Write data and parity bit
+    out null, 23 [REQ_DLY-2]    side 1  ; Discard unused bits, wait for data preset time
+    wait 1 gpio ACK             side 1  ; Wait for ACK to be inactive
+    wait 0 gpio ACK             side 0  ; Assert REQ, wait for ACK low
+
+; Read from SCSI bus using sync or async handshake.
+; Data is returned as 32-bit words:
+; - bit  0: always zero
+; - bits 1-8: data byte
+; - bit  9: parity bit
+; - bits 10-31: lookup table address
+; Lookup table address should be loaded into register Y.
+; One dummy word should be written to TX fifo for every byte to receive.
+.program scsi_accel_read
+    .side_set 1
+
+    pull block                  side 1  ; Pull from TX fifo for counting bytes and pacing sync mode
+    wait 1 gpio ACK             side 1  ; Wait for ACK high
+    in null, 1                  side 0  ; Zero bit because lookup table entries are 16-bit
+    wait 0 gpio ACK             side 0  ; Assert REQ, wait for ACK low
+    in pins, 9                  side 1  ; Deassert REQ, read GPIO
+    in y, 22                    side 1  ; Copy parity lookup table address
+
+; Data state machine for synchronous writes.
+; Takes the lowest 9 bits of each 32 bit word and writes them to bus with REQ pulse.
+; The delay times will be rewritten by C code to match the negotiated SCSI sync speed.
+;
+; Shifts one bit to ISR per every byte transmitted. This is used to control the transfer
+; pace, the RX fifo acts as a counter to keep track of unacknowledged bytes. The C code
+; can set the syncOffset by changing autopush threshold, e.g. threshold 3 = 12 bytes offset.
+.program scsi_sync_write
+    .side_set 1
+
+    out pins, 9      [0]        side 1  ; Write data and parity bit, wait for deskew delay
+    out null, 23     [0]        side 0  ; Assert REQ, wait for assert time
+    in null, 1       [0]        side 1  ; Deassert REQ, wait for transfer period, wait for space in ACK buffer
+
+; Data pacing state machine for synchronous writes.
+; Takes one bit from ISR on every falling edge of ACK.
+; The C code should set autopull threshold to match scsi_sync_write autopush threshold.
+; System DMA will then move words from scsi_sync_write RX fifo to scsi_sync_write_pacer TX fifo.
+.program scsi_sync_write_pacer
+    wait 1 gpio ACK
+    wait 0 gpio ACK   ; Wait for falling edge on ACK
+    out null, 1       ; Let scsi_sync_write send one more byte
+
+; Data pacing state machine for synchronous reads.
+; The delay times will be rewritten by C code to match the negotiated SCSI sync speed.
+; Number of bytes to receive minus one should be loaded into register X.
+; In synchronous mode this generates the REQ pulses and dummy words.
+; In asynchronous mode it just generates dummy words to feed to scsi_accel_read.
+.program scsi_sync_read_pacer
+    .side_set 1
+
+start:
+    push block      [0]      side 1  ; Send dummy word to scsi_accel_read, wait for transfer period
+    jmp x-- start   [0]      side 0  ; Assert REQ, wait for assert time
+
+finish:
+    jmp finish      [0]      side 1
+
+; Parity checker for reads from SCSI bus.
+; Receives 16-bit words from g_scsi_parity_check_lookup
+; Bottom 8 bits are the data byte, which is passed to output FIFO
+; The 9th bit is parity valid bit, which is 1 for valid and 0 for parity error.
+.program scsi_read_parity
+parity_valid:
+    out isr, 8                ; Take the 8 data bits for passing to RX fifo
+    push block                ; Push the data to RX fifo
+    out x, 24                 ; Take the parity valid bit, and the rest of 32-bit word
+    jmp x-- parity_valid      ; If parity valid bit is 1, repeat from start
+    irq set 0                 ; Parity error, set interrupt flag

+ 225 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel_target_Pico.pio.h

@@ -0,0 +1,225 @@
+// -------------------------------------------------- //
+// This file is autogenerated by pioasm; do not edit! //
+// -------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// ----------- //
+// scsi_parity //
+// ----------- //
+
+#define scsi_parity_wrap_target 0
+#define scsi_parity_wrap 3
+
+static const uint16_t scsi_parity_program_instructions[] = {
+            //     .wrap_target
+    0x80a0, //  0: pull   block                      
+    0x4061, //  1: in     null, 1                    
+    0x40e8, //  2: in     osr, 8                     
+    0x4037, //  3: in     x, 23                      
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_parity_program = {
+    .instructions = scsi_parity_program_instructions,
+    .length = 4,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_parity_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_parity_wrap_target, offset + scsi_parity_wrap);
+    return c;
+}
+#endif
+
+// ---------------------- //
+// scsi_accel_async_write //
+// ---------------------- //
+
+#define scsi_accel_async_write_wrap_target 0
+#define scsi_accel_async_write_wrap 4
+
+static const uint16_t scsi_accel_async_write_program_instructions[] = {
+            //     .wrap_target
+    0x90e0, //  0: pull   ifempty block   side 1     
+    0x7009, //  1: out    pins, 9         side 1     
+    0x7577, //  2: out    null, 23        side 1 [5] 
+    0x309a, //  3: wait   1 gpio, 26      side 1     
+    0x201a, //  4: wait   0 gpio, 26      side 0     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_accel_async_write_program = {
+    .instructions = scsi_accel_async_write_program_instructions,
+    .length = 5,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_accel_async_write_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_accel_async_write_wrap_target, offset + scsi_accel_async_write_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+
+// --------------- //
+// scsi_accel_read //
+// --------------- //
+
+#define scsi_accel_read_wrap_target 0
+#define scsi_accel_read_wrap 5
+
+static const uint16_t scsi_accel_read_program_instructions[] = {
+            //     .wrap_target
+    0x90a0, //  0: pull   block           side 1     
+    0x309a, //  1: wait   1 gpio, 26      side 1     
+    0x4061, //  2: in     null, 1         side 0     
+    0x201a, //  3: wait   0 gpio, 26      side 0     
+    0x5009, //  4: in     pins, 9         side 1     
+    0x5056, //  5: in     y, 22           side 1     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_accel_read_program = {
+    .instructions = scsi_accel_read_program_instructions,
+    .length = 6,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_accel_read_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_accel_read_wrap_target, offset + scsi_accel_read_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+
+// --------------- //
+// scsi_sync_write //
+// --------------- //
+
+#define scsi_sync_write_wrap_target 0
+#define scsi_sync_write_wrap 2
+
+static const uint16_t scsi_sync_write_program_instructions[] = {
+            //     .wrap_target
+    0x7009, //  0: out    pins, 9         side 1     
+    0x6077, //  1: out    null, 23        side 0     
+    0x5061, //  2: in     null, 1         side 1     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_sync_write_program = {
+    .instructions = scsi_sync_write_program_instructions,
+    .length = 3,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_sync_write_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_sync_write_wrap_target, offset + scsi_sync_write_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+
+// --------------------- //
+// scsi_sync_write_pacer //
+// --------------------- //
+
+#define scsi_sync_write_pacer_wrap_target 0
+#define scsi_sync_write_pacer_wrap 2
+
+static const uint16_t scsi_sync_write_pacer_program_instructions[] = {
+            //     .wrap_target
+    0x209a, //  0: wait   1 gpio, 26                 
+    0x201a, //  1: wait   0 gpio, 26                 
+    0x6061, //  2: out    null, 1                    
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_sync_write_pacer_program = {
+    .instructions = scsi_sync_write_pacer_program_instructions,
+    .length = 3,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_sync_write_pacer_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_sync_write_pacer_wrap_target, offset + scsi_sync_write_pacer_wrap);
+    return c;
+}
+#endif
+
+// -------------------- //
+// scsi_sync_read_pacer //
+// -------------------- //
+
+#define scsi_sync_read_pacer_wrap_target 0
+#define scsi_sync_read_pacer_wrap 2
+
+static const uint16_t scsi_sync_read_pacer_program_instructions[] = {
+            //     .wrap_target
+    0x9020, //  0: push   block           side 1     
+    0x0040, //  1: jmp    x--, 0          side 0     
+    0x1002, //  2: jmp    2               side 1     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_sync_read_pacer_program = {
+    .instructions = scsi_sync_read_pacer_program_instructions,
+    .length = 3,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_sync_read_pacer_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_sync_read_pacer_wrap_target, offset + scsi_sync_read_pacer_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+
+// ---------------- //
+// scsi_read_parity //
+// ---------------- //
+
+#define scsi_read_parity_wrap_target 0
+#define scsi_read_parity_wrap 4
+
+static const uint16_t scsi_read_parity_program_instructions[] = {
+            //     .wrap_target
+    0x60c8, //  0: out    isr, 8                     
+    0x8020, //  1: push   block                      
+    0x6038, //  2: out    x, 24                      
+    0x0040, //  3: jmp    x--, 0                     
+    0xc000, //  4: irq    nowait 0                   
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program scsi_read_parity_program = {
+    .instructions = scsi_read_parity_program_instructions,
+    .length = 5,
+    .origin = -1,
+};
+
+static inline pio_sm_config scsi_read_parity_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + scsi_read_parity_wrap_target, offset + scsi_read_parity_wrap);
+    return c;
+}
+#endif
+

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel.pio → lib/ZuluSCSI_platform_RP2040/scsi_accel_target_RP2040.pio


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/scsi_accel.pio.h → lib/ZuluSCSI_platform_RP2040/scsi_accel_target_RP2040.pio.h


+ 4 - 2
lib/ZuluSCSI_platform_RP2040/sd_card_sdio.cpp

@@ -26,7 +26,7 @@
 #ifdef SD_USE_SDIO
 
 #include "ZuluSCSI_log.h"
-#include "rp2040_sdio.h"
+#include "sdio.h"
 #include <hardware/gpio.h>
 #include <SdFat.h>
 #include <SdCard/SdCardInfo.h>
@@ -263,7 +263,7 @@ bool SdioCard::stopTransmission(bool blocking)
     }
     else
     {
-        uint32_t end = millis() + 100;
+        uint32_t end = millis() + 5000;
         while (millis() < end && isBusy())
         {
             if (m_stream_callback)
@@ -421,6 +421,8 @@ bool SdioCard::writeSectors(uint32_t sector, const uint8_t* src, size_t n)
     }
     else
     {
+        // TODO: Instead of CMD12 stopTransmission command, according to SD spec we should send stopTran token.
+        // stopTransmission seems to work in practice.
         return stopTransmission(true);
     }
 }

+ 6 - 4
lib/ZuluSCSI_platform_RP2040/rp2040_sdio.cpp → lib/ZuluSCSI_platform_RP2040/sdio.cpp

@@ -29,17 +29,19 @@
 // https://www.sdcard.org/downloads/pls/
 // "SDIO Physical Layer Simplified Specification Version 8.00"
 
-#include "rp2040_sdio.h"
+#include "sdio.h"
 #include <hardware/pio.h>
 #include <hardware/dma.h>
 #include <hardware/gpio.h>
 #include <ZuluSCSI_platform.h>
 #include <ZuluSCSI_log.h>
 
-#ifdef ZULUSCSI_BS2
-#include "rp2040_sdio_BS2.pio.h"
+#ifdef ZULUSCSI_PICO
+#include "sdio_Pico.pio.h"
+#elif defined(ZULUSCSI_BS2)
+#include "sdio_BS2.pio.h"
 #else
-#include "rp2040_sdio.pio.h"
+#include "sdio_RP2040.pio.h"
 #endif
 
 #define SDIO_PIO pio1

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/rp2040_sdio.h → lib/ZuluSCSI_platform_RP2040/sdio.h


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/rp2040_sdio_BS2.pio → lib/ZuluSCSI_platform_RP2040/sdio_BS2.pio


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/rp2040_sdio_BS2.pio.h → lib/ZuluSCSI_platform_RP2040/sdio_BS2.pio.h


+ 164 - 0
lib/ZuluSCSI_platform_RP2040/sdio_Pico.pio

@@ -0,0 +1,164 @@
+; ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+; 
+; ZuluSCSI™ firmware is licensed under the GPL version 3 or any later version. 
+; 
+; 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/>.
+
+; RP2040 PIO program for implementing SD card access in SDIO mode
+; Run "pioasm rp2040_sdio.pio rp2040_sdio.pio.h" to regenerate the C header from this.
+
+; The RP2040 official work-in-progress code at
+; https://github.com/raspberrypi/pico-extras/tree/master/src/rp2_common/pico_sd_card
+; may be useful reference, but this is independent implementation.
+;
+; For official SDIO specifications, refer to:
+; https://www.sdcard.org/downloads/pls/
+; "SDIO Physical Layer Simplified Specification Version 8.00"
+
+; Clock settings
+; For 3.3V communication the available speeds are:
+; - Default speed: max. 25 MHz clock
+; - High speed:    max. 50 MHz clock
+;
+; From the default RP2040 clock speed of 125 MHz, the closest dividers
+; are 3 for 41.7 MHz and 5 for 25 MHz. The CPU can apply further divider
+; through state machine registers for the initial handshake.
+;
+; Because data is written on the falling edge and read on the rising
+; edge, it is preferrable to have a long 0 state and short 1 state.
+;.define CLKDIV 3
+.define CLKDIV 5
+.define D0 ((CLKDIV + 1) / 2 - 1)
+.define D1 (CLKDIV/2 - 1)
+.define SDIO_CLK_GPIO 10
+
+; State machine 0 is used to:
+; - generate continuous clock on SDIO_CLK
+; - send CMD packets
+; - receive response packets
+;
+; Pin mapping for this state machine:
+; - Sideset    : CLK
+; - IN/OUT/SET : CMD
+; - JMP_PIN    : CMD
+;
+; The commands to send are put on TX fifo and must have two words:
+; Word 0 bits 31-24: Number of bits in command minus one (usually 47)
+; Word 0 bits 23-00: First 24 bits of the command packet, shifted out MSB first
+; Word 1 bits 31-08: Last 24 bits of the command packet, shifted out MSB first
+; Word 1 bits 07-00: Number of bits in response minus one (usually 47), or 0 if no response
+;
+; The response is put on RX fifo, starting with the MSB.
+; Partial last word will be padded with zero bits at the top.
+;
+; The state machine EXECCTRL should be set so that STATUS indicates TX FIFO < 2
+; and that AUTOPULL and AUTOPUSH are enabled.
+
+.program sdio_cmd_clk
+    .side_set 1
+
+    mov OSR, NULL       side 1 [D1]    ; Make sure OSR is full of zeros to prevent autopull
+
+wait_cmd:
+    mov Y, !STATUS      side 0 [D0]    ; Check if TX FIFO has data
+    jmp !Y wait_cmd     side 1 [D1]
+
+load_cmd:
+    out NULL, 32        side 0 [D0]    ; Load first word (trigger autopull)
+    out X, 8            side 1 [D1]    ; Number of bits to send
+    set pins, 1         side 0 [D0]    ; Initial state of CMD is high
+    set pindirs, 1      side 1 [D1]    ; Set SDIO_CMD as output
+
+send_cmd:
+    out pins, 1         side 0 [D0]    ; Write output on falling edge of CLK
+    jmp X-- send_cmd    side 1 [D1]
+
+prep_resp:
+    set pindirs, 0      side 0 [D0]    ; Set SDIO_CMD as input
+    out X, 8            side 1 [D1]    ; Get number of bits in response
+    nop                 side 0 [D0]    ; For clock alignment
+    jmp !X resp_done    side 1 [D1]    ; Check if we expect a response
+
+wait_resp:
+    nop                  side 0 [D0]
+    jmp PIN wait_resp    side 1 [D1]    ; Loop until SDIO_CMD = 0
+
+    ; Note: input bits are read at the same time as we write CLK=0.
+    ; Because the host controls the clock, the read happens before
+    ; the card sees the falling clock edge. This gives maximum time
+    ; for the data bit to settle.
+read_resp:
+    in PINS, 1          side 0 [D0]    ; Read input data bit
+    jmp X-- read_resp   side 1 [D1]    ; Loop to receive all data bits
+
+resp_done:
+    push                side 0 [D0]    ; Push the remaining part of response
+
+; State machine 1 is used to send and receive data blocks.
+; Pin mapping for this state machine:
+; - IN / OUT: SDIO_D0-D3
+; - GPIO defined at beginning of this file: SDIO_CLK
+
+; Data reception program
+; This program will wait for initial start of block token and then
+; receive a data block. The application must set number of nibbles
+; to receive minus 1 to Y register before running this program.
+.program sdio_data_rx
+
+wait_start:
+    mov X, Y                               ; Reinitialize number of nibbles to receive
+    wait 0 pin 0                           ; Wait for zero state on D0
+    wait 1 gpio SDIO_CLK_GPIO  [CLKDIV-1]  ; Wait for rising edge and then whole clock cycle
+
+rx_data:
+    in PINS, 4                 [CLKDIV-2]  ; Read nibble
+    jmp X--, rx_data
+
+; Data transmission program
+;
+; Before running this program, pindirs should be set as output
+; and register X should be initialized with the number of nibbles
+; to send minus 1 (typically 8 + 1024 + 16 + 1 - 1 = 1048)
+; and register Y with the number of response bits minus 1 (typically 31).
+;
+; Words written to TX FIFO must be:
+; - Word 0: start token 0xFFFFFFF0
+; - Word 1-128: transmitted data (512 bytes)
+; - Word 129-130: CRC checksum
+; - Word 131: end token 0xFFFFFFFF
+;
+; After the card reports idle status, RX FIFO will get a word that
+; contains the D0 line response from card.
+
+.program sdio_data_tx
+    wait 0 gpio SDIO_CLK_GPIO  
+    wait 1 gpio SDIO_CLK_GPIO  [CLKDIV + D1 - 1]; Synchronize so that write occurs on falling edge
+
+tx_loop:
+    out PINS, 4                [D0]    ; Write nibble and wait for whole clock cycle
+    jmp X-- tx_loop            [D1]
+
+    set pindirs, 0x00          [D0]    ; Set data bus as input
+
+.wrap_target
+response_loop:
+    in PINS, 1                 [D1]    ; Read D0 on rising edge
+    jmp Y--, response_loop     [D0]
+
+wait_idle:
+    wait 1 pin 0               [D1]    ; Wait for card to indicate idle condition
+    push                       [D0]    ; Push the response token
+.wrap

+ 121 - 0
lib/ZuluSCSI_platform_RP2040/sdio_Pico.pio.h

@@ -0,0 +1,121 @@
+// -------------------------------------------------- //
+// This file is autogenerated by pioasm; do not edit! //
+// -------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// ------------ //
+// sdio_cmd_clk //
+// ------------ //
+
+#define sdio_cmd_clk_wrap_target 0
+#define sdio_cmd_clk_wrap 17
+
+static const uint16_t sdio_cmd_clk_program_instructions[] = {
+            //     .wrap_target
+    0xb1e3, //  0: mov    osr, null       side 1 [1] 
+    0xa24d, //  1: mov    y, !status      side 0 [2] 
+    0x1161, //  2: jmp    !y, 1           side 1 [1] 
+    0x6260, //  3: out    null, 32        side 0 [2] 
+    0x7128, //  4: out    x, 8            side 1 [1] 
+    0xe201, //  5: set    pins, 1         side 0 [2] 
+    0xf181, //  6: set    pindirs, 1      side 1 [1] 
+    0x6201, //  7: out    pins, 1         side 0 [2] 
+    0x1147, //  8: jmp    x--, 7          side 1 [1] 
+    0xe280, //  9: set    pindirs, 0      side 0 [2] 
+    0x7128, // 10: out    x, 8            side 1 [1] 
+    0xa242, // 11: nop                    side 0 [2] 
+    0x1131, // 12: jmp    !x, 17          side 1 [1] 
+    0xa242, // 13: nop                    side 0 [2] 
+    0x11cd, // 14: jmp    pin, 13         side 1 [1] 
+    0x4201, // 15: in     pins, 1         side 0 [2] 
+    0x114f, // 16: jmp    x--, 15         side 1 [1] 
+    0x8220, // 17: push   block           side 0 [2] 
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program sdio_cmd_clk_program = {
+    .instructions = sdio_cmd_clk_program_instructions,
+    .length = 18,
+    .origin = -1,
+};
+
+static inline pio_sm_config sdio_cmd_clk_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + sdio_cmd_clk_wrap_target, offset + sdio_cmd_clk_wrap);
+    sm_config_set_sideset(&c, 1, false, false);
+    return c;
+}
+#endif
+
+// ------------ //
+// sdio_data_rx //
+// ------------ //
+
+#define sdio_data_rx_wrap_target 0
+#define sdio_data_rx_wrap 4
+
+static const uint16_t sdio_data_rx_program_instructions[] = {
+            //     .wrap_target
+    0xa022, //  0: mov    x, y                       
+    0x2020, //  1: wait   0 pin, 0                   
+    0x248A, //  2: wait   1 gpio, 10             [4] 
+    0x4304, //  3: in     pins, 4                [3] 
+    0x0043, //  4: jmp    x--, 3                     
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program sdio_data_rx_program = {
+    .instructions = sdio_data_rx_program_instructions,
+    .length = 5,
+    .origin = -1,
+};
+
+static inline pio_sm_config sdio_data_rx_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + sdio_data_rx_wrap_target, offset + sdio_data_rx_wrap);
+    return c;
+}
+#endif
+
+// ------------ //
+// sdio_data_tx //
+// ------------ //
+
+#define sdio_data_tx_wrap_target 5
+#define sdio_data_tx_wrap 8
+
+static const uint16_t sdio_data_tx_program_instructions[] = {
+    0x200A, //  0: wait   0 gpio, 10                 
+    0x258A, //  1: wait   1 gpio, 10             [5] 
+    0x6204, //  2: out    pins, 4                [2] 
+    0x0142, //  3: jmp    x--, 2                 [1] 
+    0xe280, //  4: set    pindirs, 0             [2] 
+            //     .wrap_target
+    0x4101, //  5: in     pins, 1                [1] 
+    0x0285, //  6: jmp    y--, 5                 [2] 
+    0x21a0, //  7: wait   1 pin, 0               [1] 
+    0x8220, //  8: push   block                  [2] 
+            //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program sdio_data_tx_program = {
+    .instructions = sdio_data_tx_program_instructions,
+    .length = 9,
+    .origin = -1,
+};
+
+static inline pio_sm_config sdio_data_tx_program_get_default_config(uint offset) {
+    pio_sm_config c = pio_get_default_sm_config();
+    sm_config_set_wrap(&c, offset + sdio_data_tx_wrap_target, offset + sdio_data_tx_wrap);
+    return c;
+}
+#endif
+

+ 0 - 0
lib/ZuluSCSI_platform_RP2040/rp2040_sdio.pio → lib/ZuluSCSI_platform_RP2040/sdio_RP2040.pio


+ 0 - 0
lib/ZuluSCSI_platform_RP2040/rp2040_sdio.pio.h → lib/ZuluSCSI_platform_RP2040/sdio_RP2040.pio.h


+ 23 - 6
platformio.ini

@@ -1,7 +1,7 @@
 ; PlatformIO Project Configuration File https://docs.platformio.org/page/projectconf.html
 
 [platformio]
-default_envs = ZuluSCSIv1_0, ZuluSCSIv1_0_mini, ZuluSCSIv1_1, ZuluSCSIv1_4, ZuluSCSI_RP2040,  ZuluSCSI_RP2040_Audio, ZuluSCSI_BS2
+default_envs = ZuluSCSIv1_0, ZuluSCSIv1_0_mini, ZuluSCSIv1_1, ZuluSCSI_RP2040, ZuluSCSI_RP2040_Audio, ZuluSCSI_Pico, ZuluSCSI_BS2
 
 ; Example platform to serve as a base for porting efforts
 [env:template]
@@ -40,8 +40,7 @@ lib_deps =
     SCSI2SD
     CUEParser
 upload_protocol = stlink
-platform_packages = 
-    toolchain-gccarmnoneeabi@1.60301.0
+platform_packages = platformio/toolchain-gccarmnoneeabi@1.100301.220327
     framework-spl-gd32@https://github.com/CommunityGD32Cores/gd32-pio-spl-package.git
 extra_scripts = src/build_bootloader.py
 debug_build_flags = -Os -ggdb -g3
@@ -79,10 +78,11 @@ build_flags =
 
 ; ZuluSCSI RP2040 hardware platform, based on the Raspberry Pi foundation RP2040 microcontroller
 [env:ZuluSCSI_RP2040]
-platform = raspberrypi@1.8.0
+platform = raspberrypi@1.9.0
 framework = arduino
 board = ZuluSCSI_RP2040
 extra_scripts = src/build_bootloader.py
+platform_packages = platformio/toolchain-gccarmnoneeabi@1.100301.220327
 board_build.ldscript = lib/ZuluSCSI_platform_RP2040/rp2040.ld
 ldscript_bootloader = lib/ZuluSCSI_platform_RP2040/rp2040_btldr.ld
 lib_deps =
@@ -107,11 +107,28 @@ extends = env:ZuluSCSI_RP2040
 build_flags =
     ${env:ZuluSCSI_RP2040.build_flags}
     -DENABLE_AUDIO_OUTPUT
+    -DLOGBUFSIZE=8192
+
+; Variant of RP2040 platform, based on Raspberry Pico board and a carrier PCB
+; Part of the ZuluSCSI_RP2040 platform, but with different pins.
+[env:ZuluSCSI_Pico]
+extends = env:ZuluSCSI_RP2040
+build_flags =
+    -O2 -Isrc -ggdb -g3
+    -Wall -Wno-sign-compare -Wno-ignored-qualifiers
+    -DSPI_DRIVER_SELECT=3
+    -DSD_CHIP_SELECT_MODE=2
+    -DENABLE_DEDICATED_SPI=1
+    -DHAS_SDIO_CLASS
+    -DUSE_ARDUINO=1
+    -DZULUSCSI_PICO
+    -DDISABLE_SWO
+
 
 ; Variant of RP2040 platform, based on Raspberry Pico board and a carrier PCB
 ; Differs in pinout from ZuluSCSI_RP2040 platform, but shares most of the code.
 [env:ZuluSCSI_BS2]
-platform = raspberrypi@1.8.0
+platform = raspberrypi@1.9.0
 framework = arduino
 board = ZuluSCSI_RP2040
 extra_scripts = src/build_bootloader.py
@@ -133,7 +150,7 @@ build_flags =
     -DUSE_ARDUINO=1
     -DZULUSCSI_BS2
 
-; ZuluSCSI VF4 hardware platform with GD32F450ZET6 CPU.
+; ZuluSCSI F4 hardware platform with GD32F450ZET6 CPU.
 [env:ZuluSCSIv1_4]
 platform = https://github.com/CommunityGD32Cores/platform-gd32.git
 board = genericGD32F450ZE

+ 24 - 40
src/ZuluSCSI.cpp

@@ -50,6 +50,7 @@
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
+#include "ZuluSCSI_presets.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_initiator.h"
 #include "ROMDrive.h"
@@ -373,51 +374,16 @@ bool findHDDImages()
       bool is_tp = (tolower(name[0]) == 't' && tolower(name[1]) == 'p');
       if (is_hd || is_cd || is_fd || is_mo || is_re || is_tp)
       {
-        // Check file extension
-        // We accept anything except known compressed files
-        bool is_compressed = false;
-        const char *extension = strrchr(name, '.');
-        if (extension)
-        {
-          const char *archive_exts[] = {
-            ".tar", ".tgz", ".gz", ".bz2", ".tbz2", ".xz", ".zst", ".z",
-            ".zip", ".zipx", ".rar", ".lzh", ".lha", ".lzo", ".lz4", ".arj",
-            ".dmg", ".hqx", ".cpt", ".7z", ".s7z",
-            NULL
-          };
-
-          for (int i = 0; archive_exts[i]; i++)
-          {
-            if (strcasecmp(extension, archive_exts[i]) == 0)
-            {
-              is_compressed = true;
-              break;
-            }
-          }
-        }
-
-        if (is_compressed)
-        {
-          logmsg("-- Ignoring compressed file ", name);
-          continue;
-        }
-
-        if (strcasecmp(extension, ".cue") == 0)
-        {
-          continue; // .cue will be handled with corresponding .bin
-        }
-
         // Check if the image should be loaded to microcontroller flash ROM drive
         bool is_romdrive = false;
+        const char *extension = strrchr(name, '.');
         if (extension && strcasecmp(extension, ".rom") == 0)
         {
           is_romdrive = true;
         }
-        else if (extension && strcasecmp(extension, ".rom_loaded") == 0)
-        {
-          // Already loaded ROM drive, ignore the image
-          continue;
-        }
+
+        // skip file if the name indicates it is not a valid image container
+        if (!is_romdrive && !scsiDiskFilenameValid(name)) continue;
 
         // Defaults for Hard Disks
         int id  = 1; // 0 and 3 are common in Macs for physical HD and CD, so avoid them.
@@ -680,8 +646,26 @@ extern "C" void zuluscsi_setup(void)
     }
 
     print_sd_info();
-  
+    
+    char presetName[32];
+    ini_gets("SCSI", "System", "", presetName, sizeof(presetName), CONFIGFILE);
+    preset_config_t defaults = getSystemPreset(presetName);
+    int boot_delay_ms = ini_getl("SCSI", "InitPreDelay", defaults.initPreDelay, CONFIGFILE);
+
+    if (boot_delay_ms > 0)
+    {
+    logmsg("Pre SCSI init boot delay in millis: ", boot_delay_ms);
+      delay(boot_delay_ms);
+    }
     reinitSCSI();
+
+    boot_delay_ms = ini_getl("SCSI", "InitPostDelay", 0, CONFIGFILE);
+    if (boot_delay_ms > 0)
+    {
+      logmsg("Post SCSI init boot delay in millis: ", boot_delay_ms);
+      delay(boot_delay_ms);
+    }
+
   }
 
   logmsg("Initialization complete!");

+ 30 - 1
src/ZuluSCSI_audio.h

@@ -33,7 +33,16 @@
  * This implementation uses the high byte for output port 1 and the low byte
  * for port 0. The two values are averaged to determine final volume level.
  */
-#define DEFAULT_VOLUME_LEVEL 0x3F3F
+#define DEFAULT_VOLUME_LEVEL 0x3F
+#define DEFAULT_VOLUME_LEVEL_2CH DEFAULT_VOLUME_LEVEL << 8 | DEFAULT_VOLUME_LEVEL
+
+/*
+ * Defines the 'enable' masks for the two audio output ports of each device.
+ * If this mask is matched with audio_get_channel() the relevant port will
+ * have audio output to it, otherwise it will be muted, regardless of the
+ * volume level.
+ */
+#define AUDIO_CHANNEL_ENABLE_MASK 0x0201
 
 /*
  * Status codes for audio playback, matching the SCSI 'audio status codes'.
@@ -117,3 +126,23 @@ uint16_t audio_get_volume(uint8_t id);
  * \param vol   The new volume level.
  */
 void audio_set_volume(uint8_t id, uint16_t vol);
+
+/**
+ * Gets the 0x0E channel information for both audio ports. The high byte
+ * corresponds to port 1 and the low byte to port 0. If the bits defined in
+ * AUDIO_CHANNEL_ENABLE_MASK are not set for the respective ports, that
+ * output will be muted, regardless of volume set.
+ *
+ * \param id    SCSI ID to provide channel information for.
+ * \return      The channel information.
+ */
+uint16_t audio_get_channel(uint8_t id);
+
+/**
+ * Sets the 0x0E channel information for a target, as above. See 0x0E mode
+ * page for more.
+ *
+ * \param id    SCSI ID to set channel information for.
+ * \param chn   The new channel information.
+ */
+void audio_set_channel(uint8_t id, uint16_t chn);

+ 4 - 3
src/ZuluSCSI_bootloader.cpp

@@ -62,16 +62,17 @@ bool find_firmware_image(FsFile &file, char name[MAX_FILE_PATH + 1])
 #ifndef PLATFORM_FLASH_SECTOR_ERASE
 bool program_firmware(FsFile &file)
 {
-    uint32_t fwsize = file.size() - PLATFORM_BOOTLOADER_SIZE;
+    uint32_t filesize = file.size();
+    uint32_t fwsize = filesize - PLATFORM_BOOTLOADER_SIZE;
     uint32_t num_pages = (fwsize + PLATFORM_FLASH_PAGE_SIZE - 1) / PLATFORM_FLASH_PAGE_SIZE;
 
     // Make sure the buffer is aligned to word boundary
     static uint32_t buffer32[PLATFORM_FLASH_PAGE_SIZE / 4];
     uint8_t *buffer = (uint8_t*)buffer32;
 
-    if (fwsize > PLATFORM_FLASH_TOTAL_SIZE)
+    if (filesize > PLATFORM_FLASH_TOTAL_SIZE)
     {
-        logmsg("Firmware too large: ", (int)fwsize, " flash size ", (int)PLATFORM_FLASH_TOTAL_SIZE);
+        logmsg("Firmware too large: ", (int)filesize, " flash size ", (int)PLATFORM_FLASH_TOTAL_SIZE);
         return false;
     }
 

+ 396 - 30
src/ZuluSCSI_cdrom.cpp

@@ -186,6 +186,38 @@ static const uint8_t DiscInformation[] =
     0x00,   // 33: number of opc tables
 };
 
+static const uint8_t TrackInformation[] =
+{
+    0x00,   //  0: data length, MSB
+    0x1A,   //  1: data length, LSB
+    0x01,   //  2: track number
+    0x01,   //  3: session number
+    0x00,   //  4: reserved
+    0x04,   //  5: track mode and flags
+    0x8F,   //  6: data mode and flags
+    0x00,   //  7: nwa_v
+    0x00,   //  8: track start address (MSB)
+    0x00,   //  9: .
+    0x00,   // 10: .
+    0x00,   // 11: track start address (LSB)
+    0xFF,   // 12: next writable address (MSB)
+    0xFF,   // 13: .
+    0xFF,   // 14: .
+    0xFF,   // 15: next writable address (LSB)
+    0x00,   // 16: free blocks (MSB)
+    0x00,   // 17: .
+    0x00,   // 18: .
+    0x00,   // 19: free blocks (LSB)
+    0x00,   // 20: fixed packet size (MSB)
+    0x00,   // 21: .
+    0x00,   // 22: .
+    0x00,   // 23: fixed packet size (LSB)
+    0x00,   // 24: track size (MSB)
+    0x00,   // 25: .
+    0x00,   // 26: .
+    0x00,   // 27: track size (LSB)
+};
+
 // Convert logical block address to CD-ROM time
 static void LBA2MSF(int32_t LBA, uint8_t* MSF, bool relative)
 {
@@ -229,7 +261,7 @@ static uint32_t getLeadOutLBA(const CUETrackInfo* lasttrack)
         image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
         uint32_t lastTrackBlocks = (img.file.size() - lasttrack->file_offset)
                 / lasttrack->sector_length;
-        return lasttrack->track_start + lastTrackBlocks + 1;
+        return lasttrack->track_start + lastTrackBlocks;
     }
     else
     {
@@ -384,6 +416,39 @@ void doReadDiscInformationSimple(uint16_t allocationLength)
     scsiDev.phase = DATA_IN;
 }
 
+void doReadTrackInformationSimple(bool track, uint32_t lba, uint16_t allocationLength)
+{
+    uint32_t len = sizeof(TrackInformation);
+    memcpy(scsiDev.data, TrackInformation, len);
+
+    uint32_t capacity = getScsiCapacity(
+            scsiDev.target->cfg->sdSectorStart,
+            scsiDev.target->liveCfg.bytesPerSector,
+            scsiDev.target->cfg->scsiSectors);
+    if (!track && lba >= capacity)
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        // update track size
+        scsiDev.data[24] = capacity >> 24;
+        scsiDev.data[25] = capacity >> 16;
+        scsiDev.data[26] = capacity >> 8;
+        scsiDev.data[27] = capacity;
+
+        if (len > allocationLength)
+        {
+            len = allocationLength;
+        }
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+}
+
 /*********************************/
 /* TOC generation from cue sheet */
 /*********************************/
@@ -765,6 +830,279 @@ void doReadDiscInformation(uint16_t allocationLength)
     scsiDev.phase = DATA_IN;
 }
 
+void doReadTrackInformation(bool track, uint32_t lba, uint16_t allocationLength)
+{
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    CUEParser parser;
+    if (!loadCueSheet(img, parser))
+    {
+        // No CUE sheet, use hardcoded data
+        return doReadTrackInformationSimple(track, lba, allocationLength);
+    }
+
+    // Take the hardcoded header as base
+    uint32_t len = sizeof(TrackInformation);
+    memcpy(scsiDev.data, TrackInformation, len);
+
+    // Step through the tracks until the one requested is found
+    // Result will be placed in mtrack for later use if found
+    bool trackfound = false;
+    uint32_t tracklen = 0;
+    CUETrackInfo mtrack = {0};
+    const CUETrackInfo *trackinfo;
+    while ((trackinfo = parser.next_track()) != NULL)
+    {
+        if (mtrack.track_number != 0) // skip 1st track, just store later
+        {
+            if ((track && lba == mtrack.track_number)
+                || (!track && lba < trackinfo->data_start))
+            {
+                trackfound = true;
+                tracklen = trackinfo->data_start - mtrack.data_start;
+                break;
+            }
+        }
+        mtrack = *trackinfo;
+    }
+    // try the last track as a final attempt if no match found beforehand
+    if (!trackfound)
+    {
+        uint32_t lastLba = getLeadOutLBA(&mtrack);
+        if ((track && lba == mtrack.track_number)
+            || (!track && lba < lastLba))
+        {
+            trackfound = true;
+            tracklen = lastLba - mtrack.data_start;
+        }
+    }
+
+    // bail out if no match found
+    if (!trackfound)
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    // rewrite relevant bytes, starting with track number
+    scsiDev.data[3] = mtrack.track_number;
+
+    // track mode
+    if (mtrack.track_mode == CUETrack_AUDIO)
+    {
+        scsiDev.data[5] = 0x00;
+    }
+
+    // track start
+    uint32_t start = mtrack.data_start;
+    scsiDev.data[8] = start >> 24;
+    scsiDev.data[9] = start >> 16;
+    scsiDev.data[10] = start >> 8;
+    scsiDev.data[11] = start;
+
+    // track size
+    scsiDev.data[24] = tracklen >> 24;
+    scsiDev.data[25] = tracklen >> 16;
+    scsiDev.data[26] = tracklen >> 8;
+    scsiDev.data[27] = tracklen;
+
+    dbgmsg("------ Reporting track ", mtrack.track_number, ", start ", start,
+            ", length ", tracklen);
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+void doGetConfiguration(uint8_t rt, uint16_t startFeature, uint16_t allocationLength)
+{
+    // rt = 0 is all features, rt = 1 is current features,
+    // rt = 2 only startFeature, others reserved
+    if (rt > 2)
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+
+    // write feature header
+    uint32_t len = 8; // length bytes set at end of call
+    scsiDev.data[4] = 0; // reserved
+    scsiDev.data[5] = 0; // reserved
+    if (!img.ejected)
+    {
+        // disk in drive, current profile is CD-ROM
+        scsiDev.data[6] = 0x00;
+        scsiDev.data[7] = 0x08;
+    }
+    else
+    {
+        // no disk, report no current profile
+        scsiDev.data[6] = 0;
+        scsiDev.data[7] = 0;
+    }
+
+    // profile list (0)
+    if ((rt == 2 && 0 == startFeature)
+        || (rt == 1 && startFeature <= 0)
+        || (rt == 0 && startFeature <= 0))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x03; // ver 0, persist=1,current=1
+        scsiDev.data[len++] = 8; // 2 more
+        // CD-ROM profile
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x08;
+        scsiDev.data[len++] = (img.ejected) ? 0x00 : 0x01;
+        scsiDev.data[len++] = 0;
+        // removable disk profile
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x02;
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0;
+    }
+
+    // core feature (1)
+    if ((rt == 2 && startFeature == 1)
+        || (rt == 1 && startFeature <= 1)
+        || (rt == 0 && startFeature <= 1))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x01;
+        scsiDev.data[len++] = 0x0B; // ver 2, persist=1,current=1
+        scsiDev.data[len++] = 8;
+        // physical interface standard (SCSI)
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x01;
+        scsiDev.data[len++] = 0x03; // support INQ2 and DBE
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+    }
+
+    // morphing feature (2)
+    if ((rt == 2 && startFeature == 2)
+        || (rt == 1 && startFeature <= 2)
+        || (rt == 0 && startFeature <= 2))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x02;
+        scsiDev.data[len++] = 0x07; // ver 1, persist=1,current=1
+        scsiDev.data[len++] = 4;
+        scsiDev.data[len++] = 0x02; // OCEvent=1,async=0
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+    }
+
+    // removable medium feature (3)
+    if ((rt == 2 && startFeature == 3)
+        || (rt == 1 && startFeature <= 3)
+        || (rt == 0 && startFeature <= 3))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x03;
+        scsiDev.data[len++] = 0x03; // ver 0, persist=1,current=1
+        scsiDev.data[len++] = 4;
+        scsiDev.data[len++] = 0x28; // matches 0x2A mode page version
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+    }
+
+    // random readable feature (0x10, 16)
+    if ((rt == 2 && startFeature == 16)
+        || (rt == 1 && startFeature <= 16 && !img.ejected)
+        || (rt == 0 && startFeature <= 16))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x10;
+        // ver 0, persist=0,current=drive state
+        scsiDev.data[len++] = (img.ejected) ? 0x00 : 0x01;
+        scsiDev.data[len++] = 8;
+        scsiDev.data[len++] = 0x00; // 2048 (MSB)
+        scsiDev.data[len++] = 0x00; // .
+        scsiDev.data[len++] = 0x08; // .
+        scsiDev.data[len++] = 0x00; // 2048 (LSB)
+        scsiDev.data[len++] = 0x00;
+        // one block min when disk in drive only
+        scsiDev.data[len++] = (img.ejected) ? 0x00 : 0x01;
+        scsiDev.data[len++] = 0x00; // no support for PP error correction (TODO?)
+        scsiDev.data[len++] = 0;
+    }
+
+    // multi-read feature (0x1D, 29)
+    if ((rt == 2 && startFeature == 29)
+        || (rt == 1 && startFeature <= 29 && !img.ejected)
+        || (rt == 0 && startFeature <= 29))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x1D;
+        // ver 0, persist=0,current=drive state
+        scsiDev.data[len++] = (img.ejected) ? 0x00 : 0x01;
+        scsiDev.data[len++] = 0;
+    }
+
+    // CD read feature (0x1E, 30)
+    if ((rt == 2 && startFeature == 30)
+        || (rt == 1 && startFeature <= 30 && !img.ejected)
+        || (rt == 0 && startFeature <= 30))
+    {
+        scsiDev.data[len++] = 0x00;
+        scsiDev.data[len++] = 0x1E;
+        // ver 2, persist=0,current=drive state
+        scsiDev.data[len++] = (img.ejected) ? 0x08 : 0x09;
+        scsiDev.data[len++] = 4;
+        scsiDev.data[len++] = 0x00; // dap=0,c2=0,cd-text=0
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0;
+    }
+
+#ifdef ENABLE_AUDIO_OUTPUT
+    // CD audio feature (0x103, 259)
+    if ((rt == 2 && startFeature == 259)
+        || (rt == 1 && startFeature <= 259 && !img.ejected)
+        || (rt == 0 && startFeature <= 259))
+    {
+        scsiDev.data[len++] = 0x01;
+        scsiDev.data[len++] = 0x03;
+        // ver 1, persist=0,current=drive state
+        scsiDev.data[len++] = (img.ejected) ? 0x04 : 0x05;
+        scsiDev.data[len++] = 4;
+        scsiDev.data[len++] = 0x03; // scan=0,scm=1,sv=1
+        scsiDev.data[len++] = 0;
+        scsiDev.data[len++] = 0x01; // 256 volume levels
+        scsiDev.data[len++] = 0x00; // .
+    }
+#endif
+
+    // finally, rewrite data length to match
+    uint32_t dlen = len - 8;
+    scsiDev.data[0] = dlen >> 24;
+    scsiDev.data[1] = dlen >> 16;
+    scsiDev.data[2] = dlen >> 8;
+    scsiDev.data[3] = dlen;
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
 /****************************************/
 /* CUE sheet check at image load time   */
 /****************************************/
@@ -810,6 +1148,26 @@ bool cdromValidateCueSheet(image_config_t &img)
 /* Ejection and image switching logic */
 /**************************************/
 
+// Close CDROM tray and note media change event
+void cdromCloseTray(image_config_t &img)
+{
+    if (img.ejected)
+    {
+        uint8_t target = img.scsiId & 7;
+        dbgmsg("------ CDROM close tray on ID ", (int)target);
+        img.ejected = false;
+        img.cdrom_events = 2; // New media
+
+        if (scsiDev.boardCfg.flags & S2S_CFG_ENABLE_UNIT_ATTENTION)
+        {
+            dbgmsg("------ Posting UNIT ATTENTION after medium change");
+            scsiDev.targets[target].unitAttention = NOT_READY_TO_READY_TRANSITION_MEDIUM_MAY_HAVE_CHANGED;
+        }
+    }
+}
+
+// Eject CDROM tray if closed, close if open
+// Switch image on ejection.
 void cdromPerformEject(image_config_t &img)
 {
     uint8_t target = img.scsiId & 7;
@@ -822,16 +1180,11 @@ void cdromPerformEject(image_config_t &img)
         dbgmsg("------ CDROM open tray on ID ", (int)target);
         img.ejected = true;
         img.cdrom_events = 3; // Media removal
+        cdromSwitchNextImage(img); // Switch media for next time
     }
     else
     {
-        dbgmsg("------ CDROM close tray on ID ", (int)target);
-        if (!cdromSwitchNextImage(img))
-        {
-            // Reinsert the single image
-            img.ejected = false;
-            img.cdrom_events = 2; // New media
-        }
+        cdromCloseTray(img);
     }
 }
 
@@ -841,16 +1194,16 @@ void cdromReinsertFirstImage(image_config_t &img)
     if (img.image_index > 0)
     {
         // Multiple images for this drive, force restart from first one
-        dbgmsg("---- Restarting from first CD-ROM image");
-        img.image_index = 9;
+        uint8_t target = img.scsiId & 7;
+        dbgmsg("---- Restarting from first CD-ROM image for ID ", (int)target);
+        img.image_index = -1;
+        img.current_image[0] = '\0';
         cdromSwitchNextImage(img);
     }
     else if (img.ejected)
     {
         // Reinsert the single image
-        dbgmsg("---- Closing CD-ROM tray");
-        img.ejected = false;
-        img.cdrom_events = 2; // New media
+        cdromCloseTray(img);
     }
 }
 
@@ -858,14 +1211,9 @@ void cdromReinsertFirstImage(image_config_t &img)
 bool cdromSwitchNextImage(image_config_t &img)
 {
     // Check if we have a next image to load, so that drive is closed next time the host asks.
-    img.image_index++;
     char filename[MAX_FILE_PATH];
     int target_idx = img.scsiId & 7;
-    if (!scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename)))
-    {
-        img.image_index = 0;
-        scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename));
-    }
+    scsiDiskGetNextImageName(img, filename, sizeof(filename));
 
 #ifdef ENABLE_AUDIO_OUTPUT
     // if in progress for this device, terminate audio playback immediately (Annex C)
@@ -882,8 +1230,6 @@ bool cdromSwitchNextImage(image_config_t &img)
 
         if (status)
         {
-            img.ejected = false;
-            img.cdrom_events = 2; // New media
             return true;
         }
     }
@@ -921,7 +1267,7 @@ static void doGetEventStatusNotification(bool immed)
         {
             // We are now reporting to host that the drive is open.
             // Simulate a "close" for next time the host polls.
-            cdromSwitchNextImage(img);
+            cdromCloseTray(img);
         }
     }
     else
@@ -1515,20 +1861,14 @@ extern "C" int scsiCDRomCommand()
             int start = scsiDev.cdb[4] & 1;
             if (start)
             {
-                dbgmsg("------ CDROM close tray on ID ", (int)(img.scsiId & 7));
-                img.ejected = false;
-                img.cdrom_events = 2; // New media
+                cdromCloseTray(img);
             }
             else
             {
+                // Eject and switch image
                 cdromPerformEject(img);
             }
         }
-        else
-        {
-            // flow through to disk handler
-            commandHandled = 0;
-        }
     }
     else if (command == 0x25)
     {
@@ -1614,6 +1954,18 @@ extern "C" int scsiCDRomCommand()
             scsiDev.cdb[8];
         doReadHeader(MSF, lba, allocationLength);
     }
+    else if (command == 0x46)
+    {
+        // GET CONFIGURATION
+        uint8_t rt = (scsiDev.cdb[1] & 0x03);
+        uint16_t startFeature =
+            (((uint16_t) scsiDev.cdb[2]) << 8) +
+            scsiDev.cdb[3];
+        uint16_t allocationLength =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+        doGetConfiguration(rt, startFeature, allocationLength);
+    }
     else if (command == 0x51)
     {
         uint16_t allocationLength =
@@ -1621,6 +1973,19 @@ extern "C" int scsiCDRomCommand()
             scsiDev.cdb[8];
         doReadDiscInformation(allocationLength);
     }
+    else if (command == 0x52)
+    {
+        // READ TRACK INFORMATION
+        bool track = (scsiDev.cdb[1] & 0x01);
+        uint32_t lba = (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint16_t allocationLength =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+        doReadTrackInformation(track, lba, allocationLength);
+    }
     else if (command == 0x4A)
     {
         // Get event status notifications (media change notifications)
@@ -1741,6 +2106,7 @@ extern "C" int scsiCDRomCommand()
             (((uint32_t) scsiDev.cdb[2]) << 8) +
             scsiDev.cdb[3];
         uint32_t blocks = scsiDev.cdb[4];
+        if (blocks == 0) blocks = 256;
 
         doReadCD(lba, blocks, 0, 0x10, 0, true);
     }

+ 5 - 1
src/ZuluSCSI_cdrom.h

@@ -11,7 +11,11 @@
 // Called by scsi.c from SCSI2SD
 extern "C" int scsiCDRomCommand(void);
 
-// Eject the given CD-ROM
+// Close CDROM tray and note media change event
+void cdromCloseTray(image_config_t &img);
+
+// Eject CDROM tray if closed, close if open
+// Switch image on ejection.
 void cdromPerformEject(image_config_t &img);
 
 // Reinsert ejected CD-ROM and restart from first image

+ 4 - 1
src/ZuluSCSI_config.h

@@ -27,7 +27,7 @@
 #include <ZuluSCSI_platform.h>
 
 // Use variables for version number
-#define FW_VER_NUM      "23.05.26"
+#define FW_VER_NUM      "23.09.12"
 #define FW_VER_SUFFIX   "devel"
 #define ZULU_FW_VERSION FW_VER_NUM "-" FW_VER_SUFFIX
 
@@ -56,6 +56,9 @@
 #define HDIMG_BLK_POS 5                 // Position to embed block size numbers
 #define MAX_FILE_PATH 64                // Maximum file name length
 
+// Image definition options
+#define IMAGE_INDEX_MAX 9               // Maximum number of 'IMG0' style statements parsed
+
 // SCSI config
 #define NUM_SCSIID  8          // Maximum number of supported SCSI-IDs (The minimum is 0)
 #define NUM_SCSILUN 1          // Maximum number of LUNs supported     (Currently has to be 1)

+ 268 - 20
src/ZuluSCSI_disk.cpp

@@ -30,6 +30,9 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_presets.h"
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "ZuluSCSI_audio.h"
+#endif
 #include "ZuluSCSI_cdrom.h"
 #include "ImageBackingStore.h"
 #include "ROMDrive.h"
@@ -163,7 +166,16 @@ static image_config_t g_DiskImages[S2S_MAX_TARGETS];
 
 void scsiDiskResetImages()
 {
-    memset(g_DiskImages, 0, sizeof(g_DiskImages));
+    for (int i = 0; i < S2S_MAX_TARGETS; i++)
+    {
+        g_DiskImages[i].clear();
+    }
+}
+
+void image_config_t::clear()
+{
+    static const image_config_t empty; // Statically zero-initialized
+    *this = empty;
 }
 
 void scsiDiskCloseSDCardImages()
@@ -220,6 +232,37 @@ static void formatDriveInfoField(char *field, int fieldsize, bool align_right)
     }
 }
 
+// remove path and extension from filename 
+void extractFileName(const char* path, char* output) {
+
+    const char *lastSlash, *lastDot;
+    int fileNameLength;
+
+    lastSlash = strrchr(path, '/');
+    if (!lastSlash) lastSlash = path;
+        else lastSlash++;
+
+    lastDot = strrchr(lastSlash, '.');
+    if (lastDot && (lastDot > lastSlash)) {
+        fileNameLength = lastDot - lastSlash;
+        strncpy(output, lastSlash, fileNameLength);
+        output[fileNameLength] = '\0';
+    } else {
+        strcpy(output, lastSlash);
+    }
+}
+
+void setNameFromImage(image_config_t &img, const char *filename) {
+
+    char image_name[MAX_FILE_PATH];
+
+    extractFileName(filename, image_name);
+    memset(img.vendor, 0, 8);
+    strncpy(img.vendor, image_name, 8);  
+    memset(img.prodId, 0, 8);
+    strncpy(img.prodId, image_name+8, 8);
+}
+
 // Set default drive vendor / product info after the image file
 // is loaded and the device type is known.
 static void setDefaultDriveInfo(int target_idx)
@@ -389,6 +432,12 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int
         PLATFORM_CONFIG_HOOK(&img);
 #endif
 
+        if (img.name_from_image) 
+        { 
+            setNameFromImage(img, filename); 
+            logmsg("Vendor / product id set from image file name");
+        }
+
         setDefaultDriveInfo(target_idx);
 
         if (img.prefetchbytes > 0)
@@ -425,8 +474,11 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int
 
         return true;
     }
-
-    return false;
+    else
+    {
+        logmsg("---- Failed to load image '", filename, "', ignoring");
+        return false;
+    }
 }
 
 static void checkDiskGeometryDivisible(image_config_t &img)
@@ -446,6 +498,43 @@ static void checkDiskGeometryDivisible(image_config_t &img)
     }
 }
 
+bool scsiDiskFilenameValid(const char* name)
+{
+    // Check file extension
+    const char *extension = strrchr(name, '.');
+    if (extension)
+    {
+        const char *ignore_exts[] = {
+            ".rom_loaded", ".cue", ".txt", ".rtf", ".md", ".nfo", ".pdf", ".doc",
+            NULL
+        };
+        const char *archive_exts[] = {
+            ".tar", ".tgz", ".gz", ".bz2", ".tbz2", ".xz", ".zst", ".z",
+            ".zip", ".zipx", ".rar", ".lzh", ".lha", ".lzo", ".lz4", ".arj",
+            ".dmg", ".hqx", ".cpt", ".7z", ".s7z",
+            NULL
+        };
+
+        for (int i = 0; ignore_exts[i]; i++)
+        {
+            if (strcasecmp(extension, ignore_exts[i]) == 0)
+            {
+                // ignore these without log message
+                return false;
+            }
+        }
+        for (int i = 0; archive_exts[i]; i++)
+        {
+            if (strcasecmp(extension, archive_exts[i]) == 0)
+            {
+                logmsg("-- Ignoring compressed file ", name);
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 // Set target configuration to default values
 static void scsiDiskConfigDefaults(int target_idx)
 {
@@ -462,6 +551,8 @@ static void scsiDiskConfigDefaults(int target_idx)
     img.headsPerCylinder = defaults.headsPerCylinder;
     img.quirks = defaults.quirks;
     img.prefetchbytes = defaults.prefetchBytes;
+    img.reinsert_on_inquiry = true;
+    img.reinsert_after_eject = true;
     memset(img.vendor, 0, sizeof(img.vendor));
     memset(img.prodId, 0, sizeof(img.prodId));
     memset(img.revision, 0, sizeof(img.revision));
@@ -477,12 +568,18 @@ static void scsiDiskLoadConfig(int target_idx, const char *section)
     img.deviceTypeModifier = ini_getl(section, "TypeModifier", img.deviceTypeModifier, CONFIGFILE);
     img.sectorsPerTrack = ini_getl(section, "SectorsPerTrack", img.sectorsPerTrack, CONFIGFILE);
     img.headsPerCylinder = ini_getl(section, "HeadsPerCylinder", img.headsPerCylinder, CONFIGFILE);
-    img.quirks = ini_getl(section, "Quirks", img.quirks, CONFIGFILE);
+    img.quirks = ini_getl(section, "Quirks", img.quirks, CONFIGFILE);  
     img.rightAlignStrings = ini_getbool(section, "RightAlignStrings", 0, CONFIGFILE);
+    img.name_from_image = ini_getbool(section, "NameFromImage", 0, CONFIGFILE);    
     img.prefetchbytes = ini_getl(section, "PrefetchBytes", img.prefetchbytes, CONFIGFILE);
-    img.reinsert_on_inquiry = ini_getbool(section, "ReinsertCDOnInquiry", 1, CONFIGFILE);
-    img.reinsert_after_eject = ini_getbool(section, "ReinsertAfterEject", 1, CONFIGFILE);
+    img.reinsert_on_inquiry = ini_getbool(section, "ReinsertCDOnInquiry", img.reinsert_on_inquiry, CONFIGFILE);
+    img.reinsert_after_eject = ini_getbool(section, "ReinsertAfterEject", img.reinsert_after_eject, CONFIGFILE);
     img.ejectButton = ini_getl(section, "EjectButton", 0, CONFIGFILE);
+#ifdef ENABLE_AUDIO_OUTPUT
+    uint16_t vol = ini_getl(section, "CDAVolume", DEFAULT_VOLUME_LEVEL, CONFIGFILE) & 0xFF;
+    // Set volume on both channels
+    audio_set_volume(target_idx, (vol << 8) | vol);
+#endif
 
     char tmp[32];
     memset(tmp, 0, sizeof(tmp));
@@ -500,21 +597,171 @@ static void scsiDiskLoadConfig(int target_idx, const char *section)
     memset(tmp, 0, sizeof(tmp));
     ini_gets(section, "Serial", "", tmp, sizeof(tmp), CONFIGFILE);
     if (tmp[0]) memcpy(img.serial, tmp, sizeof(img.serial));
+
+    if (strlen(section) == 5 && strncmp(section, "SCSI", 4) == 0) // allow within target [SCSIx] blocks only
+    {
+        ini_gets(section, "ImgDir", "", tmp, sizeof(tmp), CONFIGFILE);
+        if (tmp[0])
+        {
+            logmsg("-- SCSI", target_idx, " using image directory \'", tmp, "'");
+            img.image_directory = true;
+        }
+    }
 }
 
-// Check if image file name is overridden in config
-bool scsiDiskGetImageNameFromConfig(image_config_t &img, char *buf, size_t buflen)
+// Finds filename with the lowest lexical order _after_ the given filename in
+// the given folder. If there is no file after the given one, or if there is
+// no current file, this will return the lowest filename encountered.
+static int findNextImageAfter(image_config_t &img,
+        const char* dirname, const char* filename,
+        char* buf, size_t buflen)
+{
+    FsFile dir;
+    if (dirname[0] == '\0')
+    {
+        logmsg("Image directory name invalid for ID", (img.scsiId & 7));
+        return 0;
+    }
+    if (!dir.open(dirname))
+    {
+        logmsg("Image directory '", dirname, "' couldn't be opened");
+    }
+    if (!dir.isDir())
+    {
+        logmsg("Can't find images in '", dirname, "', not a directory");
+        dir.close();
+        return 0;
+    }
+
+    char first_name[MAX_FILE_PATH] = {'\0'};
+    char candidate_name[MAX_FILE_PATH] = {'\0'};
+    FsFile file;
+    while (file.openNext(&dir, O_RDONLY))
+    {
+        if (file.isDir()) continue;
+        if (!file.getName(buf, MAX_FILE_PATH))
+        {
+            logmsg("Image directory '", dirname, "'had invalid file");
+            continue;
+        }
+        if (!scsiDiskFilenameValid(buf)) continue;
+
+        // keep track of the first item to allow wrapping
+        // without having to iterate again
+        if (first_name[0] == '\0' || strcasecmp(buf, first_name) < 0)
+        {
+            strncpy(first_name, buf, sizeof(first_name));
+        }
+
+        // discard if no selected name, or if candidate is before (or is) selected
+        if (filename[0] == '\0' || strcasecmp(buf, filename) <= 0) continue;
+
+        // if we got this far and the candidate is either 1) not set, or 2) is a
+        // lower item than what has been encountered thus far, it is the best choice
+        if (candidate_name[0] == '\0' || strcasecmp(buf, candidate_name) < 0)
+        {
+            strncpy(candidate_name, buf, sizeof(candidate_name));
+        }
+    }
+
+    if (candidate_name[0] != '\0')
+    {
+        img.image_index++;
+        strncpy(img.current_image, candidate_name, sizeof(img.current_image));
+        strncpy(buf, candidate_name, buflen);
+        return strlen(candidate_name);
+    }
+    else if (first_name[0] != '\0')
+    {
+        img.image_index = 0;
+        strncpy(img.current_image, first_name, sizeof(img.current_image));
+        strncpy(buf, first_name, buflen);
+        return strlen(first_name);
+    }
+    else
+    {
+        logmsg("Image directory '", dirname, "' was empty");
+        return 0;
+    }
+}
+
+int scsiDiskGetNextImageName(image_config_t &img, char *buf, size_t buflen)
 {
     int target_idx = img.scsiId & 7;
 
     char section[6] = "SCSI0";
     section[4] = '0' + target_idx;
 
-    char key[5] = "IMG0";
-    key[3] = '0' + img.image_index;
+    // sanity check: is provided buffer is long enough to store a filename?
+    assert(buflen >= MAX_FILE_PATH);
 
-    ini_gets(section, key, "", buf, buflen, CONFIGFILE);
-    return buf[0] != '\0';
+    if (img.image_directory)
+    {
+        // image directory was found during startup
+        char dirname[MAX_FILE_PATH];
+        char key[] = "ImgDir";
+        int dirlen = ini_gets(section, key, "", dirname, sizeof(dirname), CONFIGFILE);
+        if (!dirlen)
+        {
+            // If image_directory set but ImageDir is not, could be used to
+            // indicate an image directory configured via folder structure.
+            // Not implemented, so treat this as equivalent to missing ImageDir
+            return 0;
+        }
+
+        // find the next filename
+        char nextname[MAX_FILE_PATH];
+        int nextlen = findNextImageAfter(img, dirname, img.current_image, nextname, sizeof(nextname));
+
+        if (nextlen == 0)
+        {
+            logmsg("Image directory was empty for ID", target_idx);
+            return 0;
+        }
+        else if (buflen < nextlen + dirlen + 2)
+        {
+            logmsg("Directory '", dirname, "' and file '", nextname, "' exceed allowed length");
+            return 0;
+        }
+        else
+        {
+            // construct a return value
+            strncpy(buf, dirname, buflen);
+            if (buf[strlen(buf) - 1] != '/') strcat(buf, "/");
+            strcat(buf, nextname);
+            return dirlen + nextlen;
+        }
+    }
+    else
+    {
+        img.image_index++;
+        if (img.image_index > IMAGE_INDEX_MAX || img.image_index < 0)
+        {
+            img.image_index = 0;
+        }
+
+        char key[5] = "IMG0";
+        key[3] = '0' + img.image_index;
+
+        int ret = ini_gets(section, key, "", buf, buflen, CONFIGFILE);
+        if (buf[0] != '\0')
+        {
+            return ret;
+        }
+        else if (img.image_index > 0)
+        {
+            // there may be more than one image but we've ran out of new ones
+            // wrap back to the first image
+            img.image_index = -1;
+            return scsiDiskGetNextImageName(img, buf, buflen);
+        }
+        else
+        {
+            // images are not defined in config
+            img.image_index = -1;
+            return 0;
+        }
+    }
 }
 
 void scsiDiskLoadConfig(int target_idx)
@@ -534,10 +781,11 @@ void scsiDiskLoadConfig(int target_idx)
     // Check if we have image specified by name
     char filename[MAX_FILE_PATH];
     image_config_t &img = g_DiskImages[target_idx];
-    if (scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename)))
+    img.image_index = IMAGE_INDEX_MAX;
+    if (scsiDiskGetNextImageName(img, filename, sizeof(filename)))
     {
         int blocksize = (img.deviceType == S2S_CFG_OPTICAL) ? 2048 : 512;
-        logmsg("-- Opening ", filename, " for id:", target_idx, ", specified in " CONFIGFILE);
+        logmsg("-- Opening '", filename, "' for id:", target_idx, ", specified in " CONFIGFILE);
         scsiDiskOpenHDDImage(target_idx, filename, target_idx, 0, blocksize);
     }
 }
@@ -912,7 +1160,7 @@ static int doTestUnitReady()
         {
             // We are now reporting to host that the drive is open.
             // Simulate a "close" for next time the host polls.
-            cdromSwitchNextImage(img);
+            cdromCloseTray(img);
         }
     }
     else if (unlikely(!(blockDev.state & DISK_PRESENT)))
@@ -1289,7 +1537,7 @@ void scsiDiskStartRead(uint32_t lba, uint32_t blocks)
 
         if (transfer.currentBlock == transfer.blocks)
         {
-            while (!scsiIsWriteFinished(NULL))
+            while (!scsiIsWriteFinished(NULL) && !scsiDev.resetFlag)
             {
                 platform_poll();
                 diskEjectButtonUpdate(false);
@@ -1469,7 +1717,7 @@ static void diskDataIn()
         }
 #endif
 
-        while (!scsiIsWriteFinished(NULL))
+        while (!scsiIsWriteFinished(NULL) && !scsiDev.resetFlag)
         {
             platform_poll();
             diskEjectButtonUpdate(false);
@@ -1729,13 +1977,13 @@ void scsiDiskPoll()
             checkDiskGeometryDivisible(img);
         }
 
-        // Check for Inquiry command to reinsert CD-ROMs on boot
+        // Check for Inquiry command to close CD-ROM tray on boot
         if (command == 0x12)
         {
             image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
             if (img.deviceType == S2S_CFG_OPTICAL && img.reinsert_on_inquiry)
             {
-                cdromReinsertFirstImage(img);
+                cdromCloseTray(img);
             }
         }
     }
@@ -1757,7 +2005,7 @@ void scsiDiskReset()
     g_scsi_prefetch.sector = 0;
 #endif
 
-    // Reinsert any ejected CD-ROMs
+    // Reinsert any ejected CD-ROMs on BUS RESET and restart from first image
     for (int i = 0; i < S2S_MAX_TARGETS; ++i)
     {
         image_config_t &img = g_DiskImages[i];

+ 28 - 6
src/ZuluSCSI_disk.h

@@ -32,6 +32,7 @@
 #include <scsi2sd.h>
 #include <scsiPhy.h>
 #include "ImageBackingStore.h"
+#include "ZuluSCSI_config.h"
 
 extern "C" {
 #include <disk.h>
@@ -42,9 +43,7 @@ extern "C" {
 // Extended configuration stored alongside the normal SCSI2SD target information
 struct image_config_t: public S2S_TargetCfg
 {
-    // There should be only one global instance of this struct per device, so disallow copy constructor.
-    image_config_t() = default;
-    image_config_t(const image_config_t&) = delete;
+    image_config_t() {};
 
     ImageBackingStore file;
 
@@ -61,7 +60,14 @@ struct image_config_t: public S2S_TargetCfg
     // For tape drive emulation, current position in blocks
     uint32_t tape_pos;
 
+    // True if there is a subdirectory of images for this target
+    bool image_directory;
+    // the name of the currently mounted image in a dynamic image directory
+    char current_image[MAX_FILE_PATH];
+
     // Index of image, for when image on-the-fly switching is used for CD drives
+    // This is also used for dynamic directories to track how many images have been seen
+    // Negative value forces restart from first image.
     int image_index;
 
     // Cue sheet file for CD-ROM images
@@ -72,11 +78,21 @@ struct image_config_t: public S2S_TargetCfg
     // This field uses -1 for default when field is not set in .ini
     int rightAlignStrings;
 
+    // Set Vendor / Product Id from image file name
+    bool name_from_image;
+
     // Maximum amount of bytes to prefetch
     int prefetchbytes;
 
     // Warning about geometry settings
     bool geometrywarningprinted;
+
+    // Clear any image state to zeros
+    void clear();
+
+private:
+    // There should be only one global instance of this struct per device, so make copy constructor private.
+    image_config_t(const image_config_t&) = default;
 };
 
 // Should be polled intermittently to update the platform eject buttons.
@@ -93,6 +109,10 @@ void scsiDiskCloseSDCardImages();
 bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int scsi_lun, int blocksize, S2S_CFG_TYPE type = S2S_CFG_FIXED);
 void scsiDiskLoadConfig(int target_idx);
 
+// Checks if a filename extension is appropriate for further processing as a disk image.
+// The current implementation does not check the the filename prefix for validity.
+bool scsiDiskFilenameValid(const char* name);
+
 // Clear the ROM drive header from flash
 bool scsiDiskClearRomDrive();
 // Program ROM drive and rename image file
@@ -105,9 +125,11 @@ bool scsiDiskActivateRomDrive();
 // Returns true if there is at least one image active
 bool scsiDiskCheckAnyImagesConfigured();
 
-// Check if image file name is overridden in config,
-// including image index for multi-image CD-ROM emulation
-bool scsiDiskGetImageNameFromConfig(image_config_t &img, char *buf, size_t buflen);
+// Gets the next image filename for the target, if configured for multiple
+// images. As a side effect this advances image tracking to the next image.
+// Returns the length of the new image filename, or 0 if the target is not
+// configured for multiple images.
+int scsiDiskGetNextImageName(image_config_t &img, char *buf, size_t buflen);
 
 // Get pointer to extended image configuration based on target idx
 image_config_t &scsiDiskGetImageConfig(int target_idx);

+ 16 - 1
src/ZuluSCSI_log_trace.cpp

@@ -49,7 +49,8 @@ static const char *getCommandName(uint8_t cmd)
         case 0x08: return "Read6";
         case 0x0A: return "Write6";
         case 0x0B: return "Seek6";
-        case 0x0F: return "WriteSectorBuffer";
+        case 0x0C: return "Xebec InitializeDriveCharacteristics";
+        case 0x0F: return "Xebec WriteSectorBuffer";
         case 0x10: return "WriteFilemarks";
         case 0x11: return "Space";
         case 0x12: return "Inquiry";
@@ -174,6 +175,9 @@ static void printNewPhase(int phase, bool initiator = false)
             if (!initiator && scsiDev.target->syncOffset > 0)
                 dbgmsg("---- DATA_OUT, syncOffset ", (int)scsiDev.target->syncOffset,
                                     " syncPeriod ", (int)scsiDev.target->syncPeriod);
+	    // log Xebec vendor commands data
+	    else if (scsiDev.cdb[0] == 0x0C || scsiDev.cdb[0] == 0x0F)
+		    g_LogData = true;
             else
                 dbgmsg("---- DATA_OUT");
             break;
@@ -206,6 +210,17 @@ void scsiLogPhaseChange(int new_phase)
         {
             dbgmsg("---- Total IN: ", g_InByteCount, " OUT: ", g_OutByteCount, " CHECKSUM: ", (int)g_DataChecksum);
         }
+	// log Xebec vendor command
+        if (old_phase == DATA_OUT && scsiDev.cdb[0] == 0x0C && g_OutByteCount == 8)
+	{
+		int cylinders = ((uint16_t)scsiDev.data[0] << 8) + scsiDev.data[1];
+		int heads = scsiDev.data[2];
+		int reducedWrite = ((uint16_t)scsiDev.data[3] << 8) + scsiDev.data[4];
+		int writePrecomp = ((uint16_t)scsiDev.data[5] << 8) + scsiDev.data[6];
+		int eccBurst = scsiDev.data[7];
+		dbgmsg("---- Xebec Initialize Drive Characteristics: cylinders=", cylinders, " heads=", heads,
+				" reducedWrite=", reducedWrite, " writePrecomp=", writePrecomp, " eccBurst=", eccBurst);
+	}
         g_InByteCount = g_OutByteCount = 0;
         g_DataChecksum = 0;
 

+ 63 - 2
src/ZuluSCSI_mode.cpp

@@ -62,6 +62,39 @@ static const uint8_t CDROMAudioControlParametersPage[] =
 };
 #endif
 
+// 0x2A CD-ROM Capabilities and Mechanical Status Page
+// This seems to have been standardized in MMC-1 but was de-facto present in
+// earlier SCSI-2 drives. The below mirrors one of those earlier SCSI-2
+// implementations, being is slightly shorter than the spec format but
+// otherwise returning identical information within the same bytes.
+static const uint8_t CDROMCapabilitiesPage[] =
+{
+0x2A, // page code
+0x0E, // page length
+0x00, // CD-R/RW reading not supported
+0x00, // CD-R/RW writing not supported
+#ifdef ENABLE_AUDIO_OUTPUT
+0x01, // byte 4: audio play supported
+#else
+0x00, // byte 4: no features supported
+#endif
+0x03, // byte 5: CD-DA ok with accurate streaming, no other features
+0x28, // byte 6: tray loader, ejection ok, but prevent/allow not supported
+#ifdef ENABLE_AUDIO_OUTPUT
+0x03, // byte 7: separate channel mute and volumes
+#else
+0x00, // byte 7: no features supported
+#endif
+0x05, 0x62, // max read speed, state (8X, ~1378KB/s)
+#ifdef ENABLE_AUDIO_OUTPUT
+0x01, 0x00,  // 256 volume levels supported
+#else
+0x00, 0x00,  // no volume levels supported
+#endif
+0x00, 0x40, // read buffer (64KB)
+0x05, 0x62, // current read speed, matching max speed
+};
+
 static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
 {
     memcpy(&scsiDev.data[dataIdx], pageData, pageLen);
@@ -107,15 +140,20 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
             sizeof(CDROMAudioControlParametersPage));
         if (pc == 0x00)
         {
-            // report current volume level
+            // report current port assignments and volume level
+            uint16_t chn = audio_get_channel(scsiDev.target->targetId);
             uint16_t vol = audio_get_volume(scsiDev.target->targetId);
+            scsiDev.data[idx+8] = chn & 0xFF;
             scsiDev.data[idx+9] = vol & 0xFF;
+            scsiDev.data[idx+10] = chn >> 8;
             scsiDev.data[idx+11] = vol >> 8;
         }
         else if (pc == 0x01)
         {
             // report bits that can be set
+            scsiDev.data[idx+8] = 0xFF;
             scsiDev.data[idx+9] = 0xFF;
+            scsiDev.data[idx+10] = 0xFF;
             scsiDev.data[idx+11] = 0xFF;
         }
         else
@@ -123,7 +161,9 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
             // report defaults for 0x02
             // also report same for 0x03, though we are actually supposed
             // to terminate with CHECK CONDITION and SAVING PARAMETERS NOT SUPPORTED
+            scsiDev.data[idx+8] = AUDIO_CHANNEL_ENABLE_MASK & 0xFF;
             scsiDev.data[idx+9] = DEFAULT_VOLUME_LEVEL & 0xFF;
+            scsiDev.data[idx+10] = AUDIO_CHANNEL_ENABLE_MASK >> 8;
             scsiDev.data[idx+11] = DEFAULT_VOLUME_LEVEL >> 8;
         }
         return sizeof(CDROMAudioControlParametersPage);
@@ -137,6 +177,25 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
 #endif
 }
 
+int modeSenseCDCapabilitiesPage(int pc, int idx, int pageCode, int* pageFound)
+{
+    if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+        && (pageCode == 0x2A || pageCode == 0x3F))
+    {
+        *pageFound = 1;
+        pageIn(
+            pc,
+            idx,
+            CDROMCapabilitiesPage,
+            sizeof(CDROMCapabilitiesPage));
+        return sizeof(CDROMCapabilitiesPage);
+    }
+    else
+    {
+        return 0;
+    }
+}
+
 extern "C"
 int modeSelectCDAudioControlPage(int pageLen, int idx)
 {
@@ -144,8 +203,10 @@ int modeSelectCDAudioControlPage(int pageLen, int idx)
     if (scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
     {
         if (pageLen != 0x0E) return 0;
+        uint16_t chn = (scsiDev.data[idx+10] << 8) + scsiDev.data[idx+8];
         uint16_t vol = (scsiDev.data[idx+11] << 8) + scsiDev.data[idx+9];
-        dbgmsg("------ CD audio control page volume (", vol, ")");
+        dbgmsg("------ CD audio control page channels (", chn, "), volume (", vol, ")");
+        audio_set_channel(scsiDev.target->targetId, chn);
         audio_set_volume(scsiDev.target->targetId, vol);
         return 1;
     }

+ 1 - 0
src/ZuluSCSI_mode.h

@@ -23,5 +23,6 @@
 
 int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound);
 int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound);
+int modeSenseCDCapabilitiesPage(int pc, int idx, int pageCode, int* pageFound);
 
 int modeSelectCDAudioControlPage(int pageLen, int idx);

+ 5 - 0
src/ZuluSCSI_presets.cpp

@@ -27,6 +27,7 @@ preset_config_t getSystemPreset(const char *presetName)
     cfg.enableSelLatch = false;
     cfg.mapLunsToIDs = false;
     cfg.enableParity = true;
+    cfg.initPreDelay = 0;
 
     // System-specific defaults
     if (strequals(presetName, ""))
@@ -46,6 +47,10 @@ preset_config_t getSystemPreset(const char *presetName)
         cfg.enableSCSI2 = false;
         cfg.selectionDelay = 0;
     }
+    else if (strequals(presetName, "MPC3000"))
+    {
+        cfg.initPreDelay = 600;
+    }
     else
     {
         logmsg("Unknown preset name ", presetName, ", using default settings");

+ 1 - 0
src/ZuluSCSI_presets.h

@@ -21,6 +21,7 @@ struct preset_config_t {
     // Default settings that apply to all SCSI IDs
     int selectionDelay;
     int maxSyncSpeed;
+    int initPreDelay;
     bool enableUnitAttention;
     bool enableSCSI2;
     bool enableSelLatch;

+ 3 - 0
zuluscsi.ini

@@ -24,6 +24,8 @@
 #EnableParity = 1 # Enable parity checks on platforms that support it (RP2040)
 #MapLunsToIDs = 0 # For Philips P2000C simulate multiple LUNs
 #MaxSyncSpeed = 10 # Set to 5 or 10 to enable synchronous SCSI mode, 0 to disable
+#InitPreDelay = 0  # How many milliseconds to delay before the SCSI interface is initialized
+#InitPostDelay = 0 # How many milliseconds to delay after the SCSI interface is initialized
 
 # ROM settings
 #DisableROMDrive = 1 # Disable the ROM drive if it has been loaded to flash
@@ -43,6 +45,7 @@
 #ReinsertCDOnInquiry = 1 # Reinsert any ejected CD-ROM image on Inquiry command
 #ReinsertAfterEject = 1 # Reinsert next CD image after eject, if multiple images configured.
 #EjectButton = 0 # Enable eject by button 1 or 2, or set 0 to disable
+#CDAVolume = 63 # Change CD Audio default volume. Maximum 255.
 
 # Settings can be overridden for individual devices.
 #[SCSI2]