Selaa lähdekoodia

Merge branch 'main' into f4-usb

Morio 2 vuotta sitten
vanhempi
sitoutus
61be93ea5b

+ 21 - 0
README.md

@@ -107,6 +107,27 @@ ZuluSCSI RP2040 DIP switch settings are:
 - TERMINATION: Enable SCSI termination
 - BOOTLOADER: Enable built-in USB bootloader, this DIP switch MUST remain off during normal operation.
 
+Physical eject button for CDROM
+-------------------------------
+CD-ROM drives can be configured to eject when a physical button is pressed.
+If multiple image files are configured with `IMG0`..`IMG9` config settings, ejection will switch between them.
+Two separate buttons are supported and they can eject different drives.
+
+    [SCSI1]
+    Type=2 # CDROM drive
+    IMG0 = img0.iso
+    IMG1 = ...
+    EjectButton = 1
+
+On GD32-based ZuluSCSI models (V1.0 and V1.1), buttons are connected to J303 12-pin expansion header.
+Button 1 is connected between `PE5` and `GND`, and button 2 is connected between `PE6` and `GND`.
+Pin locations are also shown in [this image](docs/ZuluSCSI_v1_1_buttons.jpg).
+
+On RP2040-based ZuluSCSI models, buttons are connected to the I2C pins.
+Button 1 is connected between `SDA` and `GND` and button 2 is connected between `SCL` and `GND`.
+On full-size models, the pins are available on expansion header J303 ([image](docs/ZuluSCSI_RP2040_buttons.jpg)).
+On compact model, pins are available on 4-pin I2C header J305 ([image](docs/ZuluSCSI_RP2040_compact_buttons.jpg)).
+
 SCSI initiator mode
 -------------------
 The RP2040 model supports SCSI initiator mode for reading SCSI drives.

BIN
docs/ZuluSCSI_RP2040_buttons.jpg


BIN
docs/ZuluSCSI_RP2040_compact_buttons.jpg


BIN
docs/ZuluSCSI_v1_1_buttons.jpg


+ 81 - 4
lib/CUEParser/test/CUEParser_test.cpp

@@ -7,14 +7,14 @@
 #define TEST(x) \
     if (!(x)) { \
         fprintf(stderr, "\033[31;1mFAILED:\033[22;39m %s:%d %s\n", __FILE__, __LINE__, #x); \
-        status = 1; \
+        status = false; \
     } else { \
         printf("\033[32;1mOK:\033[22;39m %s\n", #x); \
     }
 
-int test_basics()
+bool test_basics()
 {
-    int status = 0;
+    bool status = true;
     const char *cue_sheet = R"(
 FILE "Image Name.bin" BINARY
   TRACK 01 MODE1/2048
@@ -33,6 +33,7 @@ FILE "Sound.wav" WAVE
 
     CUEParser parser(cue_sheet);
 
+    COMMENT("test_basics()");
     COMMENT("Test TRACK 01 (data)");
     const CUETrackInfo *track = parser.next_track();
     TEST(track != NULL);
@@ -108,8 +109,84 @@ FILE "Sound.wav" WAVE
     return status;
 }
 
+bool test_datatracks()
+{
+    bool status = true;
+    const char *cue_sheet = R"(
+FILE "beos-5.0.3-professional-gobe.bin" BINARY
+TRACK 01 MODE1/2352
+    INDEX 01 00:00:00
+TRACK 02 MODE1/2352
+    INDEX 01 10:48:58
+TRACK 03 MODE1/2352
+    INDEX 01 46:07:03
+    )";
+
+    CUEParser parser(cue_sheet);
+
+    COMMENT("test_datatracks()");
+    COMMENT("Test TRACK 01 (data)");
+    const CUETrackInfo *track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "beos-5.0.3-professional-gobe.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 0);
+        TEST(track->track_number == 1);
+        TEST(track->track_mode == CUETrack_MODE1_2352);
+        TEST(track->sector_length == 2352);
+        TEST(track->unstored_pregap_length == 0);
+        TEST(track->data_start == 0);
+        TEST(track->track_start == 0);
+    }
+
+    COMMENT("Test TRACK 02 (data)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 0x6D24560);
+        TEST(track->track_number == 2);
+        TEST(track->track_mode == CUETrack_MODE1_2352);
+        TEST(track->sector_length == 2352);
+        TEST(track->unstored_pregap_length == 0);
+        TEST(track->data_start == ((10 * 60) + 48) * 75 + 58);
+        TEST(track->track_start == ((10 * 60) + 48) * 75 + 58);
+    }
+
+    COMMENT("Test TRACK 03 (data)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 0x1D17E780);
+        TEST(track->track_number == 3);
+        TEST(track->track_mode == CUETrack_MODE1_2352);
+        TEST(track->sector_length == 2352);
+        TEST(track->unstored_pregap_length == 0);
+        TEST(track->data_start == ((46 * 60) + 7) * 75 + 3);
+        TEST(track->track_start == ((46 * 60) + 7) * 75 + 3);
+    }
+
+    track = parser.next_track();
+    TEST(track == NULL);
+
+    return status;
+}
+
 
 int main()
 {
-    return test_basics();
+    if (test_basics() && test_datatracks())
+    {
+        return 0;
+    }
+    else
+    {
+        printf("Some tests failed\n");
+        return 1;
+    }
 }

+ 19 - 55
lib/SCSI2SD/src/firmware/mode.c

@@ -21,6 +21,7 @@
 #include "mode.h"
 #include "disk.h"
 #include "inquiry.h"
+#include "ZuluSCSI_mode.h"
 
 #include <string.h>
 
@@ -220,33 +221,6 @@ static const uint8_t ControlModePage[] =
 0x00, 0x00 // AEN holdoff period.
 };
 
-#ifdef ENABLE_AUDIO_OUTPUT
-static const uint8_t CDROMCDParametersPage[] =
-{
-0x0D, // page code
-0x06, // page length
-0x00, // reserved
-0x0D, // reserved, inactivity time 8 min
-0x00, 0x3C, // 60 seconds per MSF M unit
-0x00, 0x4B  // 75 frames per MSF S unit
-};
-
-static const uint8_t CDROMAudioControlParametersPage[] =
-{
-0x0E, // page code
-0x0E, // page length
-0x04, // 'Immed' bit set, 'SOTC' bit not set
-0x00, // reserved
-0x00, // reserved
-0x80, // 1 LBAs/sec multip
-0x00, 0x4B, // 75 LBAs/sec
-0x03, 0xFF, // output port 0 active, max volume
-0x03, 0xFF, // output port 1 active, max volume
-0x00, 0x00, // output port 2 inactive
-0x00, 0x00 // output port 3 inactive
-};
-#endif
-
 static const uint8_t SequentialDeviceConfigPage[] =
 {
 0x10, // page code
@@ -420,7 +394,8 @@ static void doModeSense(
 		}
 	}
 
-	if (pageCode == 0x03 || pageCode == 0x3F)
+	if ((pageCode == 0x03 || pageCode == 0x3F) &&
+		(scsiDev.target->cfg->deviceType != S2S_CFG_OPTICAL))
 	{
 		pageFound = 1;
 		pageIn(pc, idx, FormatDevicePage, sizeof(FormatDevicePage));
@@ -445,7 +420,8 @@ static void doModeSense(
 		idx += sizeof(FormatDevicePage);
 	}
 
-	if (pageCode == 0x04 || pageCode == 0x3F)
+	if ((pageCode == 0x04 || pageCode == 0x3F) &&
+		(scsiDev.target->cfg->deviceType != S2S_CFG_OPTICAL))
 	{
 		pageFound = 1;
 		if ((scsiDev.compatMode >= COMPAT_SCSI2))
@@ -523,31 +499,8 @@ static void doModeSense(
 		idx += sizeof(ControlModePage);
 	}
 
-#ifdef ENABLE_AUDIO_OUTPUT
-	if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
-		&& (pageCode == 0x0D || pageCode == 0x3F))
-	{
-		pageFound = 1;
-		pageIn(
-			pc,
-			idx,
-			CDROMCDParametersPage,
-			sizeof(CDROMCDParametersPage));
-		idx += sizeof(CDROMCDParametersPage);
-	}
-
-	if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
-		&& (pageCode == 0x0E || pageCode == 0x3F))
-	{
-		pageFound = 1;
-		pageIn(
-			pc,
-			idx,
-			CDROMAudioControlParametersPage,
-			sizeof(CDROMAudioControlParametersPage));
-		idx += sizeof(CDROMAudioControlParametersPage);
-	}
-#endif
+	idx += modeSenseCDDevicePage(pc, idx, pageCode, &pageFound);
+	idx += modeSenseCDAudioControlPage(pc, idx, pageCode, &pageFound);
 
 	if ((scsiDev.target->cfg->deviceType == S2S_CFG_SEQUENTIAL) &&
 		(pageCode == 0x10 || pageCode == 0x3F))
@@ -670,10 +623,16 @@ static void doModeSelect(void)
 
 		while (idx < scsiDev.dataLen)
 		{
+			// Change from SCSI2SD: if code page is 0x0 (vendor-specific) it
+			// will not follow the normal page mode format and cannot be
+			// parsed, but isn't necessarily an error. Instead, just treat it
+			// as an 'end of data' field and allow normal command completion.
+			int pageCode = scsiDev.data[idx] & 0x3F;
+			if (pageCode == 0) goto out;
+
 			int pageLen = scsiDev.data[idx + 1];
 			if (idx + 2 + pageLen > scsiDev.dataLen) goto bad;
 
-			int pageCode = scsiDev.data[idx] & 0x3F;
 			switch (pageCode)
 			{
 			case 0x03: // Format Device Page
@@ -699,6 +658,11 @@ static void doModeSelect(void)
 				}
 			}
 			break;
+			case 0x0E: // CD audio control page
+			{
+				if (!modeSelectCDAudioControlPage(pageLen, idx)) goto bad;
+			}
+			break;
 			//default:
 
 				// Easiest to just ignore for now. We'll get here when changing

+ 25 - 1
lib/ZuluSCSI_platform_GD32F205/ZuluSCSI_platform.cpp

@@ -218,6 +218,10 @@ void platform_init()
     gpio_bit_set(LED_PORT, LED_PINS);
     gpio_init(LED_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_2MHZ, LED_PINS);
 
+    // Ejection buttons
+    gpio_init(EJECT_1_PORT, GPIO_MODE_IPU, 0, EJECT_1_PIN);
+    gpio_init(EJECT_2_PORT, GPIO_MODE_IPU, 0, EJECT_2_PIN);
+
     // SWO trace pin on PB3
     gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3);
 }
@@ -495,7 +499,27 @@ void platform_poll()
 
 uint8_t platform_get_buttons()
 {
-    return 0;
+    // Buttons are active low: internal pull-up is enabled,
+    // and when button is pressed the pin goes low.
+    uint8_t buttons = 0;
+    if (!gpio_input_bit_get(EJECT_1_PORT, EJECT_1_PIN))   buttons |= 1;
+    if (!gpio_input_bit_get(EJECT_2_PORT, EJECT_2_PIN))   buttons |= 2;
+
+    // Simple debouncing logic: handle button releases after 100 ms delay.
+    static uint32_t debounce;
+    static uint8_t buttons_debounced = 0;
+
+    if (buttons != 0)
+    {
+        buttons_debounced = buttons;
+        debounce = millis();
+    }
+    else if ((uint32_t)(millis() - debounce) > 100)
+    {
+        buttons_debounced = 0;
+    }
+
+    return buttons_debounced;
 }
 
 /***********************/

+ 8 - 0
lib/ZuluSCSI_platform_GD32F205/ZuluSCSI_v1_0_gpio.h

@@ -138,3 +138,11 @@
 #define LED_PINS     (LED_I_PIN | LED_E_PIN)
 #define LED_ON()     gpio_bit_reset(LED_PORT, LED_PINS)
 #define LED_OFF()    gpio_bit_set(LED_PORT, LED_PINS)
+
+// Ejection buttons are available on expansion header J303.
+// PE5 = channel 1, PE6 = channel 2
+// Connect button between GPIO and GND pin.
+#define EJECT_1_PORT    GPIOE
+#define EJECT_1_PIN     GPIO_PIN_5
+#define EJECT_2_PORT    GPIOE
+#define EJECT_2_PIN     GPIO_PIN_6

+ 9 - 1
lib/ZuluSCSI_platform_GD32F205/ZuluSCSI_v1_1_gpio.h

@@ -182,4 +182,12 @@
 #define LED_E_PIN    GPIO_PIN_5
 #define LED_PINS     (LED_I_PIN | LED_E_PIN)
 #define LED_ON()     gpio_bit_reset(LED_PORT, LED_PINS)
-#define LED_OFF()    gpio_bit_set(LED_PORT, LED_PINS)
+#define LED_OFF()    gpio_bit_set(LED_PORT, LED_PINS)
+
+// Ejection buttons are available on expansion header J303.
+// PE5 = channel 1, PE6 = channel 2
+// Connect button between GPIO and GND pin.
+#define EJECT_1_PORT    GPIOE
+#define EJECT_1_PIN     GPIO_PIN_5
+#define EJECT_2_PORT    GPIOE
+#define EJECT_2_PIN     GPIO_PIN_6

+ 24 - 6
lib/ZuluSCSI_platform_RP2040/ZuluSCSI_platform.cpp

@@ -621,14 +621,32 @@ void platform_poll()
 
 uint8_t platform_get_buttons()
 {
-#ifdef ENABLE_AUDIO_OUTPUT
-    uint8_t pins = 0x00;
+    uint8_t buttons = 0;
+
+#if defined(ENABLE_AUDIO_OUTPUT)
     // pulled to VCC via resistor, sinking when pressed
-    if (!gpio_get(GPIO_EXP_SPARE)) pins |= 0x01;
-    return pins;
-#else
-    return 0;
+    if (!gpio_get(GPIO_EXP_SPARE)) buttons |= 1;
+#elif defined(GPIO_I2C_SDA)
+    // SDA = button 1, SCL = button 2
+    if (!gpio_get(GPIO_I2C_SDA)) buttons |= 1;
+    if (!gpio_get(GPIO_I2C_SCL)) buttons |= 2;
 #endif
+
+    // Simple debouncing logic: handle button releases after 100 ms delay.
+    static uint32_t debounce;
+    static uint8_t buttons_debounced = 0;
+
+    if (buttons != 0)
+    {
+        buttons_debounced = buttons;
+        debounce = millis();
+    }
+    else if ((uint32_t)(millis() - debounce) > 100)
+    {
+        buttons_debounced = 0;
+    }
+
+    return buttons_debounced;
 }
 
 /*****************************************/

+ 34 - 16
lib/ZuluSCSI_platform_RP2040/audio.cpp

@@ -140,6 +140,12 @@ static uint32_t fleft;
 // historical playback status information
 static audio_status_code audio_last_status[8] = {ASC_NO_STATUS};
 
+// volume information for targets
+static volatile uint16_t volumes[8] = {
+    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL,
+    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL
+};
+
 // mechanism for cleanly stopping DMA units
 static volatile bool audio_stopping = false;
 
@@ -158,31 +164,35 @@ static uint8_t invert = 0; // biphase encode help: set if last wire bit was '1'
  * output.
  */
 static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len, uint8_t swap) {
+    uint16_t wvol = volumes[audio_owner & 7];
+    uint8_t vol = ((wvol >> 8) + (wvol & 0xFF)) >> 1; // average of both values
+    // limit maximum volume; with my DACs I've had persistent issues
+    // with signal clipping when sending data in the highest bit position
+    vol = vol >> 2;
+
     uint16_t widx = 0;
     for (uint16_t i = 0; i < len; i += 2) {
         uint32_t sample = 0;
         uint8_t parity = 0;
         if (samples != NULL) {
+            int32_t rsamp;
             if (swap) {
-                sample = samples[i + 1] + (samples[i] << 8);
+                rsamp = (int16_t)(samples[i + 1] + (samples[i] << 8));
             } else {
-                sample = samples[i] + (samples[i + 1] << 8);
+                rsamp = (int16_t)(samples[i] + (samples[i + 1] << 8));
             }
-            // determine parity, simplified to one lookup via an XOR
-            parity = (sample >> 8) ^ sample;
+            // linear scale to requested audio value
+            rsamp *= vol;
+            // use 20 bits of value only, which allows ignoring the lowest 8
+            // bits during biphase conversion (after including sample shift)
+            sample = ((uint32_t)rsamp) & 0xFFFFF0;
+
+            // determine parity, simplified to one lookup via XOR
+            parity = ((sample >> 16) ^ (sample >> 8)) ^ sample;
             parity = snd_parity[parity];
 
-            /*
-             * Shift sample into the correct bit positions of the sub-frame. This
-             * would normally be << 12, but with my DACs I've had persistent issues
-             * with signal clipping when sending data in the highest bit position.
-             */
-            sample = sample << 11;
-            if (sample & 0x04000000) {
-                // handle two's complement
-                sample |= 0x08000000;
-                parity++;
-            }
+            // shift sample into the correct bit positions of the sub-frame.
+            sample = sample << 4;
         }
 
         // if needed, establish even parity with P bit
@@ -202,7 +212,7 @@ static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len,
         if (invert) wp = ~wp;
         invert = wp & 1;
         wire_patterns[widx++] = wp;
-        // next 8 bits (only high 4 have data)
+        // next 8 bits
         wp = biphase[(uint8_t) (sample >> 8)];
         if (invert) wp = ~wp;
         invert = wp & 1;
@@ -544,4 +554,12 @@ audio_status_code audio_get_status_code(uint8_t id) {
     return tmp;
 }
 
+uint16_t audio_get_volume(uint8_t id) {
+    return volumes[id & 7];
+}
+
+void audio_set_volume(uint8_t id, uint16_t vol) {
+    volumes[id & 7] = vol;
+}
+
 #endif // ENABLE_AUDIO_OUTPUT

+ 31 - 0
src/ZuluSCSI_audio.h

@@ -24,6 +24,17 @@
 #include <stdint.h>
 #include "ImageBackingStore.h"
 
+/*
+ * Starting volume level for audio output, with 0 being muted and 255 being
+ * max volume. SCSI-2 says this should be 25% of maximum by default, MMC-1
+ * says 100%. Testing shows this tends to be obnoxious at high volumes, so
+ * go with SCSI-2.
+ *
+ * This implementation uses the high byte for output port 1 and the low byte
+ * for port 0. The two values are averaged to determine final volume level.
+ */
+#define DEFAULT_VOLUME_LEVEL 0x3F3F
+
 /*
  * Status codes for audio playback, matching the SCSI 'audio status codes'.
  *
@@ -86,3 +97,23 @@ void audio_stop(uint8_t id);
  * \return      The matching audio status code.
  */
 audio_status_code audio_get_status_code(uint8_t id);
+
+/**
+ * Gets the current volume level for a target. This is a pair of 8-bit values
+ * ranging from 0-255 that are averaged together to determine the final output
+ * level, where 0 is muted and 255 is maximum volume. The high byte corresponds
+ * to 0x0E channel 1 and the low byte to 0x0E channel 0. See the spec's mode
+ * page documentation for more details.
+ *
+ * \param id    SCSI ID to provide volume for.
+ * \return      The matching volume level.
+ */
+uint16_t audio_get_volume(uint8_t id);
+
+/**
+ * Sets the volume level for a target, as above. See 0x0E mode page for more.
+ *
+ * \param id    SCSI ID to set volume for.
+ * \param vol   The new volume level.
+ */
+void audio_set_volume(uint8_t id, uint16_t vol);

+ 21 - 4
src/ZuluSCSI_cdrom.cpp

@@ -817,9 +817,22 @@ void cdromPerformEject(image_config_t &img)
     // terminate audio playback if active on this target (MMC-1 Annex C)
     audio_stop(target);
 #endif
-    dbgmsg("------ CDROM open tray on ID ", (int)target);
-    img.ejected = true;
-    img.cdrom_events = 3; // Media removal
+    if (!img.ejected)
+    {
+        dbgmsg("------ CDROM open tray on ID ", (int)target);
+        img.ejected = true;
+        img.cdrom_events = 3; // Media removal
+    }
+    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
+        }
+    }
 }
 
 // Reinsert any ejected CDROMs on reboot
@@ -904,7 +917,7 @@ static void doGetEventStatusNotification(bool immed)
         scsiDev.phase = DATA_IN;
         img.cdrom_events = 0;
 
-        if (img.ejected)
+        if (img.ejected && img.reinsert_after_eject)
         {
             // We are now reporting to host that the drive is open.
             // Simulate a "close" for next time the host polls.
@@ -1492,6 +1505,10 @@ extern "C" int scsiCDRomCommand()
     uint8_t command = scsiDev.cdb[0];
     if (command == 0x1B)
     {
+#if ENABLE_AUDIO_OUTPUT
+        // terminate audio playback if active on this target (MMC-1 Annex C)
+        audio_stop(img.scsiId & 7);
+#endif
         if ((scsiDev.cdb[4] & 2))
         {
             // CD-ROM load & eject

+ 19 - 5
src/ZuluSCSI_disk.cpp

@@ -174,6 +174,8 @@ void scsiDiskCloseSDCardImages()
         {
             g_DiskImages[i].file.close();
         }
+
+        g_DiskImages[i].cuesheetfile.close();
     }
 }
 
@@ -326,6 +328,7 @@ static void setDefaultDriveInfo(int target_idx)
 bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int scsi_lun, int blocksize, S2S_CFG_TYPE type)
 {
     image_config_t &img = g_DiskImages[target_idx];
+    img.cuesheetfile.close();
     img.file = ImageBackingStore(filename, blocksize);
 
     if (img.file.isOpen())
@@ -478,6 +481,7 @@ static void scsiDiskLoadConfig(int target_idx, const char *section)
     img.rightAlignStrings = ini_getbool(section, "RightAlignStrings", 0, CONFIGFILE);
     img.prefetchbytes = ini_getl(section, "PrefetchBytes", img.prefetchbytes, CONFIGFILE);
     img.reinsert_on_inquiry = ini_getbool(section, "ReinsertCDOnInquiry", 1, CONFIGFILE);
+    img.reinsert_after_eject = ini_getbool(section, "ReinsertAfterEject", 1, CONFIGFILE);
     img.ejectButton = ini_getl(section, "EjectButton", 0, CONFIGFILE);
 
     char tmp[32];
@@ -559,18 +563,25 @@ image_config_t &scsiDiskGetImageConfig(int target_idx)
 
 static void diskEjectAction(uint8_t buttonId)
 {
-    logmsg("Eject button pressed for channel ", buttonId);
+    bool found = false;
     for (uint8_t i = 0; i < S2S_MAX_TARGETS; i++)
     {
-        image_config_t img = g_DiskImages[i];
+        image_config_t &img = g_DiskImages[i];
         if (img.ejectButton == buttonId)
         {
             if (img.deviceType == S2S_CFG_OPTICAL)
             {
+                found = true;
+                logmsg("Eject button ", (int)buttonId, " pressed, passing to CD drive SCSI", (int)i);
                 cdromPerformEject(img);
             }
         }
     }
+
+    if (!found)
+    {
+        logmsg("Eject button ", (int)buttonId, " pressed, but no drives with EjectButton=", (int)buttonId, " setting found!");
+    }
 }
 
 uint8_t diskEjectButtonUpdate(bool immediate)
@@ -897,9 +908,12 @@ static int doTestUnitReady()
         scsiDev.target->sense.asc = MEDIUM_NOT_PRESENT;
         scsiDev.phase = STATUS;
 
-        // We are now reporting to host that the drive is open.
-        // Simulate a "close" for next time the host polls.
-        cdromSwitchNextImage(img);
+        if (img.reinsert_after_eject)
+        {
+            // We are now reporting to host that the drive is open.
+            // Simulate a "close" for next time the host polls.
+            cdromSwitchNextImage(img);
+        }
     }
     else if (unlikely(!(blockDev.state & DISK_PRESENT)))
     {

+ 6 - 1
src/ZuluSCSI_disk.h

@@ -42,12 +42,17 @@ extern "C" {
 // Extended configuration stored alongside the normal SCSI2SD target information
 struct image_config_t: public S2S_TargetCfg
 {
+    // There should be only one global instance of this struct per device, so disallow copy constructor.
+    image_config_t() = default;
+    image_config_t(const image_config_t&) = delete;
+
     ImageBackingStore file;
 
     // For CD-ROM drive ejection
     bool ejected;
     uint8_t cdrom_events;
-    bool reinsert_on_inquiry;
+    bool reinsert_on_inquiry; // Reinsert on Inquiry command (to reinsert automatically after boot)
+    bool reinsert_after_eject; // Reinsert next image after ejection
 
     // selects a physical button channel that will cause an eject action
     // default option of '0' disables this functionality

+ 159 - 0
src/ZuluSCSI_mode.cpp

@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2013 Michael McMaster <michael@codesrc.com>
+ * Copyright (C) 2014 Doug Brown <doug@downtowndougbrown.com>
+ * Copyright (C) 2019 Landon Rodgers <g.landon.rodgers@gmail.com>
+ * ZuluSCSI™ - Copyright (c) 2023 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/>.
+**/
+
+#include <stdint.h>
+#include <string.h>
+
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "ZuluSCSI_audio.h"
+#endif
+#include "ZuluSCSI_cdrom.h"
+#include "ZuluSCSI_log.h"
+
+extern "C" {
+#include "ZuluSCSI_mode.h"
+}
+
+static const uint8_t CDROMCDParametersPage[] =
+{
+0x0D, // page code
+0x06, // page length
+0x00, // reserved
+0x0D, // reserved, inactivity time 8 min
+0x00, 0x3C, // 60 seconds per MSF M unit
+0x00, 0x4B  // 75 frames per MSF S unit
+};
+
+#ifdef ENABLE_AUDIO_OUTPUT
+static const uint8_t CDROMAudioControlParametersPage[] =
+{
+0x0E, // page code
+0x0E, // page length
+0x04, // 'Immed' bit set, 'SOTC' bit not set
+0x00, // reserved
+0x00, // reserved
+0x80, // 1 LBAs/sec multip
+0x00, 0x4B, // 75 LBAs/sec
+0x01, 0xFF, // output port 0 active, max volume
+0x02, 0xFF, // output port 1 active, max volume
+0x00, 0x00, // output port 2 inactive
+0x00, 0x00 // output port 3 inactive
+};
+#endif
+
+static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
+{
+    memcpy(&scsiDev.data[dataIdx], pageData, pageLen);
+
+    if (pc == 0x01) // Mask out (un)changable values
+    {
+        memset(&scsiDev.data[dataIdx+2], 0, pageLen - 2);
+    }
+}
+
+extern "C"
+int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound)
+{
+    if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+        && (pageCode == 0x0D || pageCode == 0x3F))
+    {
+        *pageFound = 1;
+        pageIn(
+            pc,
+            idx,
+            CDROMCDParametersPage,
+            sizeof(CDROMCDParametersPage));
+        return sizeof(CDROMCDParametersPage);
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+extern "C"
+int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
+{
+#ifdef ENABLE_AUDIO_OUTPUT
+    if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+        && (pageCode == 0x0E || pageCode == 0x3F))
+    {
+        *pageFound = 1;
+        pageIn(
+            pc,
+            idx,
+            CDROMAudioControlParametersPage,
+            sizeof(CDROMAudioControlParametersPage));
+        if (pc == 0x00)
+        {
+            // report current volume level
+            uint16_t vol = audio_get_volume(scsiDev.target->targetId);
+            scsiDev.data[idx+9] = vol & 0xFF;
+            scsiDev.data[idx+11] = vol >> 8;
+        }
+        else if (pc == 0x01)
+        {
+            // report bits that can be set
+            scsiDev.data[idx+9] = 0xFF;
+            scsiDev.data[idx+11] = 0xFF;
+        }
+        else
+        {
+            // report defaults for 0x02
+            // also report same for 0x03, though we are actually supposed
+            // to terminate with CHECK CONDITION and SAVING PARAMETERS NOT SUPPORTED
+            scsiDev.data[idx+9] = DEFAULT_VOLUME_LEVEL & 0xFF;
+            scsiDev.data[idx+11] = DEFAULT_VOLUME_LEVEL >> 8;
+        }
+        return sizeof(CDROMAudioControlParametersPage);
+    }
+    else
+    {
+        return 0;
+    }
+#else
+    return 0;
+#endif
+}
+
+extern "C"
+int modeSelectCDAudioControlPage(int pageLen, int idx)
+{
+#ifdef ENABLE_AUDIO_OUTPUT
+    if (scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+    {
+        if (pageLen != 0x0E) return 0;
+        uint16_t vol = (scsiDev.data[idx+11] << 8) + scsiDev.data[idx+9];
+        dbgmsg("------ CD audio control page volume (", vol, ")");
+        audio_set_volume(scsiDev.target->targetId, vol);
+        return 1;
+    }
+    else
+    {
+        return 0;
+    }
+#else
+    return 0;
+#endif
+}

+ 27 - 0
src/ZuluSCSI_mode.h

@@ -0,0 +1,27 @@
+/**
+ * ZuluSCSI™ - Copyright (c) 2023 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/>.
+**/
+
+#pragma once
+
+int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound);
+int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound);
+
+int modeSelectCDAudioControlPage(int pageLen, int idx);

+ 2 - 0
zuluscsi.ini

@@ -41,6 +41,8 @@
 #RightAlignStrings = 0 # Right-align SCSI vendor / product strings, defaults on if Quirks = 1
 #PrefetchBytes = 8192 # Maximum number of bytes to prefetch after a read request, 0 to disable
 #ReinsertCDOnInquiry = 1 # Reinsert any ejected CD-ROM image on Inquiry command
+#ReinsertAfterEject = 1 # Reinsert next CD image after eject, if multiple images configured.
+#EjectButton = 0 # Enable eject by button 1 or 2, or set 0 to disable
 
 # Settings can be overridden for individual devices.
 #[SCSI2]