瀏覽代碼

Merge branch 'main' into f4-usb

Morio 2 年之前
父節點
當前提交
6d68e4f7f0
共有 58 個文件被更改,包括 2008 次插入189 次删除
  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: 
 on: 
   push:
   push:
   workflow_dispatch:
   workflow_dispatch:
+  pull_request:
 
 
 jobs:
 jobs:
   build_firmware:
   build_firmware:

+ 1 - 6
.gitignore

@@ -1,7 +1,2 @@
 .pio
 .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:
 Examples of valid filenames:
 * `HD5.hda` or `HD5.img`: hard drive with SCSI ID 5
 * `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
 * `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.
 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:
 Empty image files can be created using operating system tools:
 
 
 * Windows: `fsutil file createnew HD1.img 1073741824` (1 GB)
 * 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`
 * 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.
 ZuluSCSI firmware can also create image files itself.
 To do this, create a text file with filename such as `Create 1024M HD40.txt`.
 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.
 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 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
 - INITIATOR: Enable SCSI initiator mode for imaging SCSI drives
 - DEBUG LOG: Enable verbose debug log (saved to `zululog.txt`)
 - DEBUG LOG: Enable verbose debug log (saved to `zululog.txt`)
 - TERMINATION: Enable SCSI termination
 - TERMINATION: Enable SCSI termination
 - BOOTLOADER: Enable built-in USB bootloader, this DIP switch MUST remain off during normal operation.
 - 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
 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_APPLE = 1,
 	S2S_CFG_QUIRKS_OMTI = 2,
 	S2S_CFG_QUIRKS_OMTI = 2,
 	S2S_CFG_QUIRKS_XEBEC = 4,
 	S2S_CFG_QUIRKS_XEBEC = 4,
-	S2S_CFG_QUIRKS_VMS = 8
+	S2S_CFG_QUIRKS_VMS = 8,
+	S2S_CFG_QUIRKS_EMU = 9
 } S2S_CFG_QUIRKS;
 } S2S_CFG_QUIRKS;
 
 
 typedef enum
 typedef enum

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

@@ -196,7 +196,7 @@ void scsiReadBuffer()
 }
 }
 
 
 // Callback after the DATA OUT phase is complete.
 // 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
 	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 scsiSendDiagnostic(void);
 void scsiReceiveDiagnostic(void);
 void scsiReceiveDiagnostic(void);
+void doWriteBuffer(void);
 void scsiWriteBuffer(void);
 void scsiWriteBuffer(void);
-void scsiWriteSectorBuffer(void);
 void scsiReadBuffer(void);
 void scsiReadBuffer(void);
 
 
 #endif
 #endif

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

@@ -242,8 +242,8 @@ static const uint8_t SequentialDeviceConfigPage[] =
 static const uint8_t AppleVendorPage[] =
 static const uint8_t AppleVendorPage[] =
 {
 {
 0x30, // Page code
 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)
 static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
@@ -514,10 +514,10 @@ static void doModeSense(
 		idx += sizeof(SequentialDeviceConfigPage);
 		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))
 		(pageCode == 0x30 || pageCode == 0x3F))
 	{
 	{
 		pageFound = 1;
 		pageFound = 1;

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

@@ -382,14 +382,57 @@ static void process_Command()
 		{
 		{
 			// Completely non-standard
 			// Completely non-standard
 			allocLength = 4;
 			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[1] = (scsiDev.cdb[1] & 0x20) | ((transfer.lba >> 16) & 0x1F);
 			scsiDev.data[2] = transfer.lba >> 8;
 			scsiDev.data[2] = transfer.lba >> 8;
@@ -540,11 +583,6 @@ static void process_Command()
 	{
 	{
 		scsiWriteBuffer();
 		scsiWriteBuffer();
 	}
 	}
-	else if (command == 0x0f &&
-		scsiDev.target->cfg->quirks == S2S_CFG_QUIRKS_XEBEC)
-	{
-		scsiWriteSectorBuffer();
-	}
 	else if (command == 0x3C)
 	else if (command == 0x3C)
 	{
 	{
 		scsiReadBuffer();
 		scsiReadBuffer();

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

@@ -17,12 +17,27 @@
 
 
 #include "scsi.h"
 #include "scsi.h"
 #include "vendor.h"
 #include "vendor.h"
-
+#include "diagnostic.h"
 
 
 // Callback after the DATA OUT phase is complete.
 // Callback after the DATA OUT phase is complete.
 static void doAssignDiskParameters(void)
 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()
 int scsiVendorCommand()
@@ -48,6 +63,23 @@ int scsiVendorCommand()
 		scsiDev.phase = DATA_OUT;
 		scsiDev.phase = DATA_OUT;
 		scsiDev.postDataOutHook = doAssignDiskParameters;
 		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
 	else
 	{
 	{
 		commandHandled = 0;
 		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.
 // Check if a DMA request for SD card read has completed.
 // This is used to optimize the timing of data transfers on SCSI bus.
 // 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();
 bool check_sd_read_done();
 
 
 #endif
 #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)
 extern "C" bool scsiIsWriteFinished(const uint8_t *data)
 {
 {
     // Check if there is still a polling transfer in progress
     // 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
         // Process the transfer piece-by-piece while waiting
         // for SD card to react.
         // for SD card to react.
         int max_count = g_scsi_writereq.count / 8;
         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 "scsi_accel_dma.h"
 #include <ZuluSCSI_log.h>
 #include <ZuluSCSI_log.h>
+#include <ZuluSCSI_disk.h>
+#include <scsi2sd.h>
 #include <gd32f20x_timer.h>
 #include <gd32f20x_timer.h>
 #include <gd32f20x_rcu.h>
 #include <gd32f20x_rcu.h>
 #include <assert.h>
 #include <assert.h>
@@ -67,6 +69,20 @@ static bool g_scsi_dma_use_greenpak;
 
 
 void scsi_accel_timer_dma_init()
 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_state = SCSIDMA_IDLE;
     g_scsi_dma_use_greenpak = false;
     g_scsi_dma_use_greenpak = false;
     rcu_periph_clock_enable(SCSI_TIMER_RCU);
     rcu_periph_clock_enable(SCSI_TIMER_RCU);
@@ -369,6 +385,17 @@ extern "C" void SCSI_TIMER_DMACHB_IRQ()
         }
         }
         else
         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
             // No more data available
             stop_dma();
             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.
 // This is used to optimize the timing of data transfers on SCSI bus.
 bool check_sd_read_done()
 bool check_sd_read_done()
 {
 {
+    if (!m_stream_callback) return false;
+
     return (DMA_CHCTL(DMA1, DMA_CH3) & DMA_CHXCTL_CHEN)
     return (DMA_CHCTL(DMA1, DMA_CH3) & DMA_CHXCTL_CHEN)
         && (DMA_INTF(DMA1) & DMA_FLAG_ADD(DMA_FLAG_FTF, DMA_CH3));
         && (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 <multicore.h>
 #include <USB/PluggableUSBSerial.h>
 #include <USB/PluggableUSBSerial.h>
 #include "audio.h"
 #include "audio.h"
-#include "scsi_accel_rp2040.h"
+#include "scsi_accel_target.h"
 
 
 extern "C" {
 extern "C" {
 
 
@@ -121,27 +121,38 @@ void platform_init()
     /* Check dip switch settings */
     /* Check dip switch settings */
 #ifdef HAS_DIP_SWITCHES
 #ifdef HAS_DIP_SWITCHES
     gpio_conf(DIP_INITIATOR,  GPIO_FUNC_SIO, false, false, false, false, false);
     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_DBGLOG,     GPIO_FUNC_SIO, false, false, false, false, false);
     gpio_conf(DIP_TERM,       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
     delay(10); // 10 ms delay to let pull-ups do their work
-
+# ifndef ZULUSCSI_PICO
     bool dbglog = !gpio_get(DIP_DBGLOG);
     bool dbglog = !gpio_get(DIP_DBGLOG);
     bool termination = !gpio_get(DIP_TERM);
     bool termination = !gpio_get(DIP_TERM);
+# endif
 #else
 #else
     delay(10);
     delay(10);
 #endif
 #endif
 
 
+#ifndef DISABLE_SWO
     /* Initialize logging to SWO pin (UART0) */
     /* Initialize logging to SWO pin (UART0) */
     gpio_conf(SWO_PIN,        GPIO_FUNC_UART,false,false, true,  false, true);
     gpio_conf(SWO_PIN,        GPIO_FUNC_UART,false,false, true,  false, true);
     uart_init(uart0, 1000000);
     uart_init(uart0, 1000000);
     g_uart_initialized = true;
     g_uart_initialized = true;
+#endif
+
     mbed_set_error_hook(mbed_error_hook);
     mbed_set_error_hook(mbed_error_hook);
 
 
     logmsg("Platform: ", g_platform_name);
     logmsg("Platform: ", g_platform_name);
     logmsg("FW Version: ", g_log_firmwareversion);
     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);
     logmsg("DIP switch settings: debug log ", (int)dbglog, ", termination ", (int)termination);
     g_log_debug = dbglog;
     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_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_BSY,    GPIO_FUNC_SIO, true, false, false, true, false);
         gpio_conf(SCSI_IN_RST,    GPIO_FUNC_SIO, true, false, false, true, false);
         gpio_conf(SCSI_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_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_ACK,   GPIO_FUNC_SIO, false,false, true,  true, true);
         gpio_conf(SCSI_OUT_ATN,   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 <stdint.h>
 #include <Arduino.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
 // BS2 hardware variant, using Raspberry Pico board on a carrier PCB
 #include "ZuluSCSI_platform_gpio_BS2.h"
 #include "ZuluSCSI_platform_gpio_BS2.h"
 #else
 #else
 // Normal RP2040 variant, using RP2040 chip directly
 // Normal RP2040 variant, using RP2040 chip directly
-#include "ZuluSCSI_platform_gpio.h"
+#include "ZuluSCSI_platform_gpio_RP2040.h"
 #endif
 #endif
 
 
 #include "scsiHostPhy.h"
 #include "scsiHostPhy.h"
@@ -44,7 +47,11 @@ extern "C" {
 /* These are used in debug output and default SCSI strings */
 /* These are used in debug output and default SCSI strings */
 extern const char *g_platform_name;
 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_NAME "ZuluSCSI BS2"
 # define PLATFORM_REVISION "1.0"
 # define PLATFORM_REVISION "1.0"
 #else
 #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
 // volume information for targets
 static volatile uint16_t volumes[8] = {
 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
 // 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) {
 static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len, uint8_t swap) {
     uint16_t wvol = volumes[audio_owner & 7];
     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
     // limit maximum volume; with my DACs I've had persistent issues
     // with signal clipping when sending data in the highest bit position
     // 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;
     uint16_t widx = 0;
     for (uint16_t i = 0; i < len; i += 2) {
     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));
                 rsamp = (int16_t)(samples[i] + (samples[i + 1] << 8));
             }
             }
             // linear scale to requested audio value
             // 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
             // use 20 bits of value only, which allows ignoring the lowest 8
             // bits during biphase conversion (after including sample shift)
             // bits during biphase conversion (after including sample shift)
             sample = ((uint32_t)rsamp) & 0xFFFFF0;
             sample = ((uint32_t)rsamp) & 0xFFFFF0;
@@ -562,4 +577,12 @@ void audio_set_volume(uint8_t id, uint16_t vol) {
     volumes[id & 7] = 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
 #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
 # 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
     // Selection phase
     scsiLogInitiatorPhaseChange(SELECTION);
     scsiLogInitiatorPhaseChange(SELECTION);
-    dbgmsg("------ SELECTING ", target_id);
+    dbgmsg("------ SELECTING ", target_id, " with initiator ID ", (int)initiator_id);
     SCSI_OUT(SEL, 1);
     SCSI_OUT(SEL, 1);
     delayMicroseconds(5);
     delayMicroseconds(5);
-    SCSI_OUT_DATA(1 << target_id);
+    SCSI_OUT_DATA((1 << target_id) | (1 << initiator_id));
     delayMicroseconds(5);
     delayMicroseconds(5);
     SCSI_OUT(BSY, 0);
     SCSI_OUT(BSY, 0);
 
 
@@ -300,4 +303,4 @@ void scsiHostPhyRelease()
     SCSI_RELEASE_OUTPUTS();
     SCSI_RELEASE_OUTPUTS();
 }
 }
 
 
-#endif
+#endif

+ 1 - 1
lib/ZuluSCSI_platform_RP2040/scsiPhy.cpp

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

+ 12 - 4
lib/ZuluSCSI_platform_RP2040/scsi_accel_host.cpp

@@ -24,7 +24,6 @@
 #include "scsi_accel_host.h"
 #include "scsi_accel_host.h"
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
-#include "scsi_accel_host.pio.h"
 #include <hardware/pio.h>
 #include <hardware/pio.h>
 #include <hardware/dma.h>
 #include <hardware/dma.h>
 #include <hardware/irq.h>
 #include <hardware/irq.h>
@@ -32,6 +31,12 @@
 #include <hardware/sync.h>
 #include <hardware/sync.h>
 
 
 #ifdef PLATFORM_HAS_INITIATOR_MODE
 #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_PIO pio0
 #define SCSI_SM 0
 #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_DB6].ctrl  = GPIO_FUNC_SIO;
         iobank0_hw->io[SCSI_IO_DB7].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_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;
         iobank0_hw->io[SCSI_OUT_ACK].ctrl = GPIO_FUNC_SIO;
     }
     }
     else if (g_scsi_host_state == SCSIHOST_READ)
     else if (g_scsi_host_state == SCSIHOST_READ)
     {
     {
         // Data bus and REQ as input, ACK pin as output
         // 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_DB0].ctrl  = GPIO_FUNC_SIO;
         iobank0_hw->io[SCSI_IO_DB1].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_DB6].ctrl  = GPIO_FUNC_SIO;
         iobank0_hw->io[SCSI_IO_DB7].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_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;
         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_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
-#include "scsi_accel_rp2040.h"
+#include "scsi_accel_target.h"
 #include <hardware/pio.h>
 #include <hardware/pio.h>
 #include <hardware/dma.h>
 #include <hardware/dma.h>
 #include <hardware/irq.h>
 #include <hardware/irq.h>
@@ -38,10 +38,12 @@
 #include <audio.h>
 #include <audio.h>
 #include <multicore.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
 #else
-#include "scsi_accel.pio.h"
+#include "scsi_accel_target_RP2040.pio.h"
 #endif
 #endif
 
 
 // SCSI bus write acceleration uses up to 3 PIO state machines:
 // 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
 #ifdef SD_USE_SDIO
 
 
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
-#include "rp2040_sdio.h"
+#include "sdio.h"
 #include <hardware/gpio.h>
 #include <hardware/gpio.h>
 #include <SdFat.h>
 #include <SdFat.h>
 #include <SdCard/SdCardInfo.h>
 #include <SdCard/SdCardInfo.h>
@@ -263,7 +263,7 @@ bool SdioCard::stopTransmission(bool blocking)
     }
     }
     else
     else
     {
     {
-        uint32_t end = millis() + 100;
+        uint32_t end = millis() + 5000;
         while (millis() < end && isBusy())
         while (millis() < end && isBusy())
         {
         {
             if (m_stream_callback)
             if (m_stream_callback)
@@ -421,6 +421,8 @@ bool SdioCard::writeSectors(uint32_t sector, const uint8_t* src, size_t n)
     }
     }
     else
     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);
         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/
 // https://www.sdcard.org/downloads/pls/
 // "SDIO Physical Layer Simplified Specification Version 8.00"
 // "SDIO Physical Layer Simplified Specification Version 8.00"
 
 
-#include "rp2040_sdio.h"
+#include "sdio.h"
 #include <hardware/pio.h>
 #include <hardware/pio.h>
 #include <hardware/dma.h>
 #include <hardware/dma.h>
 #include <hardware/gpio.h>
 #include <hardware/gpio.h>
 #include <ZuluSCSI_platform.h>
 #include <ZuluSCSI_platform.h>
 #include <ZuluSCSI_log.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
 #else
-#include "rp2040_sdio.pio.h"
+#include "sdio_RP2040.pio.h"
 #endif
 #endif
 
 
 #define SDIO_PIO pio1
 #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 Project Configuration File https://docs.platformio.org/page/projectconf.html
 
 
 [platformio]
 [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
 ; Example platform to serve as a base for porting efforts
 [env:template]
 [env:template]
@@ -40,8 +40,7 @@ lib_deps =
     SCSI2SD
     SCSI2SD
     CUEParser
     CUEParser
 upload_protocol = stlink
 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
     framework-spl-gd32@https://github.com/CommunityGD32Cores/gd32-pio-spl-package.git
 extra_scripts = src/build_bootloader.py
 extra_scripts = src/build_bootloader.py
 debug_build_flags = -Os -ggdb -g3
 debug_build_flags = -Os -ggdb -g3
@@ -79,10 +78,11 @@ build_flags =
 
 
 ; ZuluSCSI RP2040 hardware platform, based on the Raspberry Pi foundation RP2040 microcontroller
 ; ZuluSCSI RP2040 hardware platform, based on the Raspberry Pi foundation RP2040 microcontroller
 [env:ZuluSCSI_RP2040]
 [env:ZuluSCSI_RP2040]
-platform = raspberrypi@1.8.0
+platform = raspberrypi@1.9.0
 framework = arduino
 framework = arduino
 board = ZuluSCSI_RP2040
 board = ZuluSCSI_RP2040
 extra_scripts = src/build_bootloader.py
 extra_scripts = src/build_bootloader.py
+platform_packages = platformio/toolchain-gccarmnoneeabi@1.100301.220327
 board_build.ldscript = lib/ZuluSCSI_platform_RP2040/rp2040.ld
 board_build.ldscript = lib/ZuluSCSI_platform_RP2040/rp2040.ld
 ldscript_bootloader = lib/ZuluSCSI_platform_RP2040/rp2040_btldr.ld
 ldscript_bootloader = lib/ZuluSCSI_platform_RP2040/rp2040_btldr.ld
 lib_deps =
 lib_deps =
@@ -107,11 +107,28 @@ extends = env:ZuluSCSI_RP2040
 build_flags =
 build_flags =
     ${env:ZuluSCSI_RP2040.build_flags}
     ${env:ZuluSCSI_RP2040.build_flags}
     -DENABLE_AUDIO_OUTPUT
     -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
 ; 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.
 ; Differs in pinout from ZuluSCSI_RP2040 platform, but shares most of the code.
 [env:ZuluSCSI_BS2]
 [env:ZuluSCSI_BS2]
-platform = raspberrypi@1.8.0
+platform = raspberrypi@1.9.0
 framework = arduino
 framework = arduino
 board = ZuluSCSI_RP2040
 board = ZuluSCSI_RP2040
 extra_scripts = src/build_bootloader.py
 extra_scripts = src/build_bootloader.py
@@ -133,7 +150,7 @@ build_flags =
     -DUSE_ARDUINO=1
     -DUSE_ARDUINO=1
     -DZULUSCSI_BS2
     -DZULUSCSI_BS2
 
 
-; ZuluSCSI VF4 hardware platform with GD32F450ZET6 CPU.
+; ZuluSCSI F4 hardware platform with GD32F450ZET6 CPU.
 [env:ZuluSCSIv1_4]
 [env:ZuluSCSIv1_4]
 platform = https://github.com/CommunityGD32Cores/platform-gd32.git
 platform = https://github.com/CommunityGD32Cores/platform-gd32.git
 board = genericGD32F450ZE
 board = genericGD32F450ZE

+ 24 - 40
src/ZuluSCSI.cpp

@@ -50,6 +50,7 @@
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_platform.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_log_trace.h"
+#include "ZuluSCSI_presets.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_initiator.h"
 #include "ZuluSCSI_initiator.h"
 #include "ROMDrive.h"
 #include "ROMDrive.h"
@@ -373,51 +374,16 @@ bool findHDDImages()
       bool is_tp = (tolower(name[0]) == 't' && tolower(name[1]) == 'p');
       bool is_tp = (tolower(name[0]) == 't' && tolower(name[1]) == 'p');
       if (is_hd || is_cd || is_fd || is_mo || is_re || is_tp)
       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
         // Check if the image should be loaded to microcontroller flash ROM drive
         bool is_romdrive = false;
         bool is_romdrive = false;
+        const char *extension = strrchr(name, '.');
         if (extension && strcasecmp(extension, ".rom") == 0)
         if (extension && strcasecmp(extension, ".rom") == 0)
         {
         {
           is_romdrive = true;
           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
         // Defaults for Hard Disks
         int id  = 1; // 0 and 3 are common in Macs for physical HD and CD, so avoid them.
         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();
     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();
     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!");
   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
  * 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.
  * 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'.
  * 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.
  * \param vol   The new volume level.
  */
  */
 void audio_set_volume(uint8_t id, uint16_t vol);
 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
 #ifndef PLATFORM_FLASH_SECTOR_ERASE
 bool program_firmware(FsFile &file)
 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;
     uint32_t num_pages = (fwsize + PLATFORM_FLASH_PAGE_SIZE - 1) / PLATFORM_FLASH_PAGE_SIZE;
 
 
     // Make sure the buffer is aligned to word boundary
     // Make sure the buffer is aligned to word boundary
     static uint32_t buffer32[PLATFORM_FLASH_PAGE_SIZE / 4];
     static uint32_t buffer32[PLATFORM_FLASH_PAGE_SIZE / 4];
     uint8_t *buffer = (uint8_t*)buffer32;
     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;
         return false;
     }
     }
 
 

+ 396 - 30
src/ZuluSCSI_cdrom.cpp

@@ -186,6 +186,38 @@ static const uint8_t DiscInformation[] =
     0x00,   // 33: number of opc tables
     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
 // Convert logical block address to CD-ROM time
 static void LBA2MSF(int32_t LBA, uint8_t* MSF, bool relative)
 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;
         image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
         uint32_t lastTrackBlocks = (img.file.size() - lasttrack->file_offset)
         uint32_t lastTrackBlocks = (img.file.size() - lasttrack->file_offset)
                 / lasttrack->sector_length;
                 / lasttrack->sector_length;
-        return lasttrack->track_start + lastTrackBlocks + 1;
+        return lasttrack->track_start + lastTrackBlocks;
     }
     }
     else
     else
     {
     {
@@ -384,6 +416,39 @@ void doReadDiscInformationSimple(uint16_t allocationLength)
     scsiDev.phase = DATA_IN;
     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 */
 /* TOC generation from cue sheet */
 /*********************************/
 /*********************************/
@@ -765,6 +830,279 @@ void doReadDiscInformation(uint16_t allocationLength)
     scsiDev.phase = DATA_IN;
     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   */
 /* CUE sheet check at image load time   */
 /****************************************/
 /****************************************/
@@ -810,6 +1148,26 @@ bool cdromValidateCueSheet(image_config_t &img)
 /* Ejection and image switching logic */
 /* 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)
 void cdromPerformEject(image_config_t &img)
 {
 {
     uint8_t target = img.scsiId & 7;
     uint8_t target = img.scsiId & 7;
@@ -822,16 +1180,11 @@ void cdromPerformEject(image_config_t &img)
         dbgmsg("------ CDROM open tray on ID ", (int)target);
         dbgmsg("------ CDROM open tray on ID ", (int)target);
         img.ejected = true;
         img.ejected = true;
         img.cdrom_events = 3; // Media removal
         img.cdrom_events = 3; // Media removal
+        cdromSwitchNextImage(img); // Switch media for next time
     }
     }
     else
     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)
     if (img.image_index > 0)
     {
     {
         // Multiple images for this drive, force restart from first one
         // 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);
         cdromSwitchNextImage(img);
     }
     }
     else if (img.ejected)
     else if (img.ejected)
     {
     {
         // Reinsert the single image
         // 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)
 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.
     // 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];
     char filename[MAX_FILE_PATH];
     int target_idx = img.scsiId & 7;
     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
 #ifdef ENABLE_AUDIO_OUTPUT
     // if in progress for this device, terminate audio playback immediately (Annex C)
     // if in progress for this device, terminate audio playback immediately (Annex C)
@@ -882,8 +1230,6 @@ bool cdromSwitchNextImage(image_config_t &img)
 
 
         if (status)
         if (status)
         {
         {
-            img.ejected = false;
-            img.cdrom_events = 2; // New media
             return true;
             return true;
         }
         }
     }
     }
@@ -921,7 +1267,7 @@ static void doGetEventStatusNotification(bool immed)
         {
         {
             // We are now reporting to host that the drive is open.
             // We are now reporting to host that the drive is open.
             // Simulate a "close" for next time the host polls.
             // Simulate a "close" for next time the host polls.
-            cdromSwitchNextImage(img);
+            cdromCloseTray(img);
         }
         }
     }
     }
     else
     else
@@ -1515,20 +1861,14 @@ extern "C" int scsiCDRomCommand()
             int start = scsiDev.cdb[4] & 1;
             int start = scsiDev.cdb[4] & 1;
             if (start)
             if (start)
             {
             {
-                dbgmsg("------ CDROM close tray on ID ", (int)(img.scsiId & 7));
-                img.ejected = false;
-                img.cdrom_events = 2; // New media
+                cdromCloseTray(img);
             }
             }
             else
             else
             {
             {
+                // Eject and switch image
                 cdromPerformEject(img);
                 cdromPerformEject(img);
             }
             }
         }
         }
-        else
-        {
-            // flow through to disk handler
-            commandHandled = 0;
-        }
     }
     }
     else if (command == 0x25)
     else if (command == 0x25)
     {
     {
@@ -1614,6 +1954,18 @@ extern "C" int scsiCDRomCommand()
             scsiDev.cdb[8];
             scsiDev.cdb[8];
         doReadHeader(MSF, lba, allocationLength);
         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)
     else if (command == 0x51)
     {
     {
         uint16_t allocationLength =
         uint16_t allocationLength =
@@ -1621,6 +1973,19 @@ extern "C" int scsiCDRomCommand()
             scsiDev.cdb[8];
             scsiDev.cdb[8];
         doReadDiscInformation(allocationLength);
         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)
     else if (command == 0x4A)
     {
     {
         // Get event status notifications (media change notifications)
         // Get event status notifications (media change notifications)
@@ -1741,6 +2106,7 @@ extern "C" int scsiCDRomCommand()
             (((uint32_t) scsiDev.cdb[2]) << 8) +
             (((uint32_t) scsiDev.cdb[2]) << 8) +
             scsiDev.cdb[3];
             scsiDev.cdb[3];
         uint32_t blocks = scsiDev.cdb[4];
         uint32_t blocks = scsiDev.cdb[4];
+        if (blocks == 0) blocks = 256;
 
 
         doReadCD(lba, blocks, 0, 0x10, 0, true);
         doReadCD(lba, blocks, 0, 0x10, 0, true);
     }
     }

+ 5 - 1
src/ZuluSCSI_cdrom.h

@@ -11,7 +11,11 @@
 // Called by scsi.c from SCSI2SD
 // Called by scsi.c from SCSI2SD
 extern "C" int scsiCDRomCommand(void);
 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);
 void cdromPerformEject(image_config_t &img);
 
 
 // Reinsert ejected CD-ROM and restart from first image
 // Reinsert ejected CD-ROM and restart from first image

+ 4 - 1
src/ZuluSCSI_config.h

@@ -27,7 +27,7 @@
 #include <ZuluSCSI_platform.h>
 #include <ZuluSCSI_platform.h>
 
 
 // Use variables for version number
 // 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 FW_VER_SUFFIX   "devel"
 #define ZULU_FW_VERSION FW_VER_NUM "-" FW_VER_SUFFIX
 #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 HDIMG_BLK_POS 5                 // Position to embed block size numbers
 #define MAX_FILE_PATH 64                // Maximum file name length
 #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
 // SCSI config
 #define NUM_SCSIID  8          // Maximum number of supported SCSI-IDs (The minimum is 0)
 #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)
 #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_log.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_presets.h"
 #include "ZuluSCSI_presets.h"
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "ZuluSCSI_audio.h"
+#endif
 #include "ZuluSCSI_cdrom.h"
 #include "ZuluSCSI_cdrom.h"
 #include "ImageBackingStore.h"
 #include "ImageBackingStore.h"
 #include "ROMDrive.h"
 #include "ROMDrive.h"
@@ -163,7 +166,16 @@ static image_config_t g_DiskImages[S2S_MAX_TARGETS];
 
 
 void scsiDiskResetImages()
 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()
 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
 // Set default drive vendor / product info after the image file
 // is loaded and the device type is known.
 // is loaded and the device type is known.
 static void setDefaultDriveInfo(int target_idx)
 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);
         PLATFORM_CONFIG_HOOK(&img);
 #endif
 #endif
 
 
+        if (img.name_from_image) 
+        { 
+            setNameFromImage(img, filename); 
+            logmsg("Vendor / product id set from image file name");
+        }
+
         setDefaultDriveInfo(target_idx);
         setDefaultDriveInfo(target_idx);
 
 
         if (img.prefetchbytes > 0)
         if (img.prefetchbytes > 0)
@@ -425,8 +474,11 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int
 
 
         return true;
         return true;
     }
     }
-
-    return false;
+    else
+    {
+        logmsg("---- Failed to load image '", filename, "', ignoring");
+        return false;
+    }
 }
 }
 
 
 static void checkDiskGeometryDivisible(image_config_t &img)
 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
 // Set target configuration to default values
 static void scsiDiskConfigDefaults(int target_idx)
 static void scsiDiskConfigDefaults(int target_idx)
 {
 {
@@ -462,6 +551,8 @@ static void scsiDiskConfigDefaults(int target_idx)
     img.headsPerCylinder = defaults.headsPerCylinder;
     img.headsPerCylinder = defaults.headsPerCylinder;
     img.quirks = defaults.quirks;
     img.quirks = defaults.quirks;
     img.prefetchbytes = defaults.prefetchBytes;
     img.prefetchbytes = defaults.prefetchBytes;
+    img.reinsert_on_inquiry = true;
+    img.reinsert_after_eject = true;
     memset(img.vendor, 0, sizeof(img.vendor));
     memset(img.vendor, 0, sizeof(img.vendor));
     memset(img.prodId, 0, sizeof(img.prodId));
     memset(img.prodId, 0, sizeof(img.prodId));
     memset(img.revision, 0, sizeof(img.revision));
     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.deviceTypeModifier = ini_getl(section, "TypeModifier", img.deviceTypeModifier, CONFIGFILE);
     img.sectorsPerTrack = ini_getl(section, "SectorsPerTrack", img.sectorsPerTrack, CONFIGFILE);
     img.sectorsPerTrack = ini_getl(section, "SectorsPerTrack", img.sectorsPerTrack, CONFIGFILE);
     img.headsPerCylinder = ini_getl(section, "HeadsPerCylinder", img.headsPerCylinder, 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.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.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);
     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];
     char tmp[32];
     memset(tmp, 0, sizeof(tmp));
     memset(tmp, 0, sizeof(tmp));
@@ -500,21 +597,171 @@ static void scsiDiskLoadConfig(int target_idx, const char *section)
     memset(tmp, 0, sizeof(tmp));
     memset(tmp, 0, sizeof(tmp));
     ini_gets(section, "Serial", "", tmp, sizeof(tmp), CONFIGFILE);
     ini_gets(section, "Serial", "", tmp, sizeof(tmp), CONFIGFILE);
     if (tmp[0]) memcpy(img.serial, tmp, sizeof(img.serial));
     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;
     int target_idx = img.scsiId & 7;
 
 
     char section[6] = "SCSI0";
     char section[6] = "SCSI0";
     section[4] = '0' + target_idx;
     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)
 void scsiDiskLoadConfig(int target_idx)
@@ -534,10 +781,11 @@ void scsiDiskLoadConfig(int target_idx)
     // Check if we have image specified by name
     // Check if we have image specified by name
     char filename[MAX_FILE_PATH];
     char filename[MAX_FILE_PATH];
     image_config_t &img = g_DiskImages[target_idx];
     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;
         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);
         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.
             // We are now reporting to host that the drive is open.
             // Simulate a "close" for next time the host polls.
             // Simulate a "close" for next time the host polls.
-            cdromSwitchNextImage(img);
+            cdromCloseTray(img);
         }
         }
     }
     }
     else if (unlikely(!(blockDev.state & DISK_PRESENT)))
     else if (unlikely(!(blockDev.state & DISK_PRESENT)))
@@ -1289,7 +1537,7 @@ void scsiDiskStartRead(uint32_t lba, uint32_t blocks)
 
 
         if (transfer.currentBlock == transfer.blocks)
         if (transfer.currentBlock == transfer.blocks)
         {
         {
-            while (!scsiIsWriteFinished(NULL))
+            while (!scsiIsWriteFinished(NULL) && !scsiDev.resetFlag)
             {
             {
                 platform_poll();
                 platform_poll();
                 diskEjectButtonUpdate(false);
                 diskEjectButtonUpdate(false);
@@ -1469,7 +1717,7 @@ static void diskDataIn()
         }
         }
 #endif
 #endif
 
 
-        while (!scsiIsWriteFinished(NULL))
+        while (!scsiIsWriteFinished(NULL) && !scsiDev.resetFlag)
         {
         {
             platform_poll();
             platform_poll();
             diskEjectButtonUpdate(false);
             diskEjectButtonUpdate(false);
@@ -1729,13 +1977,13 @@ void scsiDiskPoll()
             checkDiskGeometryDivisible(img);
             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)
         if (command == 0x12)
         {
         {
             image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
             image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
             if (img.deviceType == S2S_CFG_OPTICAL && img.reinsert_on_inquiry)
             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;
     g_scsi_prefetch.sector = 0;
 #endif
 #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)
     for (int i = 0; i < S2S_MAX_TARGETS; ++i)
     {
     {
         image_config_t &img = g_DiskImages[i];
         image_config_t &img = g_DiskImages[i];

+ 28 - 6
src/ZuluSCSI_disk.h

@@ -32,6 +32,7 @@
 #include <scsi2sd.h>
 #include <scsi2sd.h>
 #include <scsiPhy.h>
 #include <scsiPhy.h>
 #include "ImageBackingStore.h"
 #include "ImageBackingStore.h"
+#include "ZuluSCSI_config.h"
 
 
 extern "C" {
 extern "C" {
 #include <disk.h>
 #include <disk.h>
@@ -42,9 +43,7 @@ extern "C" {
 // Extended configuration stored alongside the normal SCSI2SD target information
 // Extended configuration stored alongside the normal SCSI2SD target information
 struct image_config_t: public S2S_TargetCfg
 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;
     ImageBackingStore file;
 
 
@@ -61,7 +60,14 @@ struct image_config_t: public S2S_TargetCfg
     // For tape drive emulation, current position in blocks
     // For tape drive emulation, current position in blocks
     uint32_t tape_pos;
     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
     // 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;
     int image_index;
 
 
     // Cue sheet file for CD-ROM images
     // 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
     // This field uses -1 for default when field is not set in .ini
     int rightAlignStrings;
     int rightAlignStrings;
 
 
+    // Set Vendor / Product Id from image file name
+    bool name_from_image;
+
     // Maximum amount of bytes to prefetch
     // Maximum amount of bytes to prefetch
     int prefetchbytes;
     int prefetchbytes;
 
 
     // Warning about geometry settings
     // Warning about geometry settings
     bool geometrywarningprinted;
     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.
 // 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);
 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);
 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
 // Clear the ROM drive header from flash
 bool scsiDiskClearRomDrive();
 bool scsiDiskClearRomDrive();
 // Program ROM drive and rename image file
 // Program ROM drive and rename image file
@@ -105,9 +125,11 @@ bool scsiDiskActivateRomDrive();
 // Returns true if there is at least one image active
 // Returns true if there is at least one image active
 bool scsiDiskCheckAnyImagesConfigured();
 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
 // Get pointer to extended image configuration based on target idx
 image_config_t &scsiDiskGetImageConfig(int 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 0x08: return "Read6";
         case 0x0A: return "Write6";
         case 0x0A: return "Write6";
         case 0x0B: return "Seek6";
         case 0x0B: return "Seek6";
-        case 0x0F: return "WriteSectorBuffer";
+        case 0x0C: return "Xebec InitializeDriveCharacteristics";
+        case 0x0F: return "Xebec WriteSectorBuffer";
         case 0x10: return "WriteFilemarks";
         case 0x10: return "WriteFilemarks";
         case 0x11: return "Space";
         case 0x11: return "Space";
         case 0x12: return "Inquiry";
         case 0x12: return "Inquiry";
@@ -174,6 +175,9 @@ static void printNewPhase(int phase, bool initiator = false)
             if (!initiator && scsiDev.target->syncOffset > 0)
             if (!initiator && scsiDev.target->syncOffset > 0)
                 dbgmsg("---- DATA_OUT, syncOffset ", (int)scsiDev.target->syncOffset,
                 dbgmsg("---- DATA_OUT, syncOffset ", (int)scsiDev.target->syncOffset,
                                     " syncPeriod ", (int)scsiDev.target->syncPeriod);
                                     " syncPeriod ", (int)scsiDev.target->syncPeriod);
+	    // log Xebec vendor commands data
+	    else if (scsiDev.cdb[0] == 0x0C || scsiDev.cdb[0] == 0x0F)
+		    g_LogData = true;
             else
             else
                 dbgmsg("---- DATA_OUT");
                 dbgmsg("---- DATA_OUT");
             break;
             break;
@@ -206,6 +210,17 @@ void scsiLogPhaseChange(int new_phase)
         {
         {
             dbgmsg("---- Total IN: ", g_InByteCount, " OUT: ", g_OutByteCount, " CHECKSUM: ", (int)g_DataChecksum);
             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_InByteCount = g_OutByteCount = 0;
         g_DataChecksum = 0;
         g_DataChecksum = 0;
 
 

+ 63 - 2
src/ZuluSCSI_mode.cpp

@@ -62,6 +62,39 @@ static const uint8_t CDROMAudioControlParametersPage[] =
 };
 };
 #endif
 #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)
 static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
 {
 {
     memcpy(&scsiDev.data[dataIdx], pageData, pageLen);
     memcpy(&scsiDev.data[dataIdx], pageData, pageLen);
@@ -107,15 +140,20 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
             sizeof(CDROMAudioControlParametersPage));
             sizeof(CDROMAudioControlParametersPage));
         if (pc == 0x00)
         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);
             uint16_t vol = audio_get_volume(scsiDev.target->targetId);
+            scsiDev.data[idx+8] = chn & 0xFF;
             scsiDev.data[idx+9] = vol & 0xFF;
             scsiDev.data[idx+9] = vol & 0xFF;
+            scsiDev.data[idx+10] = chn >> 8;
             scsiDev.data[idx+11] = vol >> 8;
             scsiDev.data[idx+11] = vol >> 8;
         }
         }
         else if (pc == 0x01)
         else if (pc == 0x01)
         {
         {
             // report bits that can be set
             // report bits that can be set
+            scsiDev.data[idx+8] = 0xFF;
             scsiDev.data[idx+9] = 0xFF;
             scsiDev.data[idx+9] = 0xFF;
+            scsiDev.data[idx+10] = 0xFF;
             scsiDev.data[idx+11] = 0xFF;
             scsiDev.data[idx+11] = 0xFF;
         }
         }
         else
         else
@@ -123,7 +161,9 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
             // report defaults for 0x02
             // report defaults for 0x02
             // also report same for 0x03, though we are actually supposed
             // also report same for 0x03, though we are actually supposed
             // to terminate with CHECK CONDITION and SAVING PARAMETERS NOT SUPPORTED
             // 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+9] = DEFAULT_VOLUME_LEVEL & 0xFF;
+            scsiDev.data[idx+10] = AUDIO_CHANNEL_ENABLE_MASK >> 8;
             scsiDev.data[idx+11] = DEFAULT_VOLUME_LEVEL >> 8;
             scsiDev.data[idx+11] = DEFAULT_VOLUME_LEVEL >> 8;
         }
         }
         return sizeof(CDROMAudioControlParametersPage);
         return sizeof(CDROMAudioControlParametersPage);
@@ -137,6 +177,25 @@ int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
 #endif
 #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"
 extern "C"
 int modeSelectCDAudioControlPage(int pageLen, int idx)
 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 (scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
     {
     {
         if (pageLen != 0x0E) return 0;
         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];
         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);
         audio_set_volume(scsiDev.target->targetId, vol);
         return 1;
         return 1;
     }
     }

+ 1 - 0
src/ZuluSCSI_mode.h

@@ -23,5 +23,6 @@
 
 
 int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound);
 int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound);
 int modeSenseCDAudioControlPage(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);
 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.enableSelLatch = false;
     cfg.mapLunsToIDs = false;
     cfg.mapLunsToIDs = false;
     cfg.enableParity = true;
     cfg.enableParity = true;
+    cfg.initPreDelay = 0;
 
 
     // System-specific defaults
     // System-specific defaults
     if (strequals(presetName, ""))
     if (strequals(presetName, ""))
@@ -46,6 +47,10 @@ preset_config_t getSystemPreset(const char *presetName)
         cfg.enableSCSI2 = false;
         cfg.enableSCSI2 = false;
         cfg.selectionDelay = 0;
         cfg.selectionDelay = 0;
     }
     }
+    else if (strequals(presetName, "MPC3000"))
+    {
+        cfg.initPreDelay = 600;
+    }
     else
     else
     {
     {
         logmsg("Unknown preset name ", presetName, ", using default settings");
         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
     // Default settings that apply to all SCSI IDs
     int selectionDelay;
     int selectionDelay;
     int maxSyncSpeed;
     int maxSyncSpeed;
+    int initPreDelay;
     bool enableUnitAttention;
     bool enableUnitAttention;
     bool enableSCSI2;
     bool enableSCSI2;
     bool enableSelLatch;
     bool enableSelLatch;

+ 3 - 0
zuluscsi.ini

@@ -24,6 +24,8 @@
 #EnableParity = 1 # Enable parity checks on platforms that support it (RP2040)
 #EnableParity = 1 # Enable parity checks on platforms that support it (RP2040)
 #MapLunsToIDs = 0 # For Philips P2000C simulate multiple LUNs
 #MapLunsToIDs = 0 # For Philips P2000C simulate multiple LUNs
 #MaxSyncSpeed = 10 # Set to 5 or 10 to enable synchronous SCSI mode, 0 to disable
 #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
 # ROM settings
 #DisableROMDrive = 1 # Disable the ROM drive if it has been loaded to flash
 #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
 #ReinsertCDOnInquiry = 1 # Reinsert any ejected CD-ROM image on Inquiry command
 #ReinsertAfterEject = 1 # Reinsert next CD image after eject, if multiple images configured.
 #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
 #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.
 # Settings can be overridden for individual devices.
 #[SCSI2]
 #[SCSI2]