소스 검색

Merge pull request #204 from ZuluSCSI/dev_bin_cue

Bin/cue format support and more complete CD-ROM emulation
Alex Perez 2 년 전
부모
커밋
bb8dfe7d6e

+ 11 - 0
README.md

@@ -15,6 +15,17 @@ In addition to the simplified filenames style above, the ZuluSCSI firmware also
 The media type can be set in `zuluscsi.ini`, or directly by the file name prefix.
 Supported prefixes are `HD` (hard drive), `CD` (cd-rom), `FD` (floppy), `MO` (magneto-optical), `RE` (generic removeable media), `TP` (sequential tape drive).
 
+CD-ROM images in BIN/CUE format
+-------------------------------
+The `.iso` format for CD images only supports data track.
+For audio and mixed mode CDs, two files are needed: `.bin` with data and `.cue` with the list of tracks.
+
+To use a BIN/CUE image with ZuluSCSI, name both files with the same part before the extension.
+For example `CD3.bin` and `CD3.cue`.
+The cue file contains the original file name, but it doesn't matter for ZuluSCSI.
+
+BIN/CUE support is currently experimental. Supported track types are `AUDIO`, `MODE1/2048` and `MODE1/2352`.
+
 Creating new image files
 ------------------------
 Empty image files can be created using operating system tools:

+ 9 - 0
lib/CUEParser/library.json

@@ -0,0 +1,9 @@
+{
+    "name": "CUEParser",
+    "version": "1.0.0",
+    "repository": { "type": "git", "url": "https://github.com/ZuluSCSI/ZuluSCSI-firmware.git"},
+    "authors": [{ "name": "Petteri Aimonen", "email": "jpa@git.mail.kapsi.fi" }],
+    "license": "GPL-3.0-or-later",
+    "frameworks": "*",
+    "platforms": "*"
+}

+ 278 - 0
lib/CUEParser/src/CUEParser.cpp

@@ -0,0 +1,278 @@
+/*
+ * Simple CUE sheet parser suitable for embedded systems.
+ *
+ *  Copyright (c) 2023 Rabbit Hole Computing
+ *
+ *  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/>.
+ */
+
+// Refer to e.g. https://www.gnu.org/software/ccd2cue/manual/html_node/CUE-sheet-format.html#CUE-sheet-format
+//
+// Example of a CUE file:
+// FILE "foo bar.bin" BINARY
+//   TRACK 01 MODE1/2048
+//     INDEX 01 00:00:00
+//   TRACK 02 AUDIO
+//     PREGAP 00:02:00
+//     INDEX 01 02:47:20
+//   TRACK 03 AUDIO
+//     INDEX 00 07:55:58
+//     INDEX 01 07:55:65
+
+
+#include "CUEParser.h"
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+CUEParser::CUEParser(): CUEParser("")
+{
+
+}
+
+CUEParser::CUEParser(const char *cue_sheet):
+    m_cue_sheet(cue_sheet)
+{
+    restart();
+}
+
+void CUEParser::restart()
+{
+    m_parse_pos = m_cue_sheet;
+    memset(&m_track_info, 0, sizeof(m_track_info));
+}
+
+const CUETrackInfo *CUEParser::next_track()
+{
+    // Previous track info is needed to track file offset
+    uint32_t prev_data_start = m_track_info.data_start;
+    uint32_t prev_sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode); // Defaults to 2352 before first track
+
+    bool got_track = false;
+    bool got_data = false;
+    while(!(got_track && got_data) && start_line())
+    {
+        if (strncasecmp(m_parse_pos, "FILE ", 5) == 0)
+        {
+            const char *p = read_quoted(m_parse_pos + 5, m_track_info.filename, sizeof(m_track_info.filename));
+            m_track_info.file_mode = parse_file_mode(skip_space(p));
+            m_track_info.file_offset = 0;
+            m_track_info.track_mode = CUETrack_AUDIO;
+            prev_data_start = 0;
+            prev_sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode);
+        }
+        else if (strncasecmp(m_parse_pos, "TRACK ", 6) == 0)
+        {
+            const char *track_num = skip_space(m_parse_pos + 6);
+            char *endptr;
+            m_track_info.track_number = strtoul(track_num, &endptr, 10);
+            m_track_info.track_mode = parse_track_mode(skip_space(endptr));
+            m_track_info.sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode);
+            m_track_info.pregap_start = 0;
+            m_track_info.unstored_pregap_length = 0;
+            m_track_info.data_start = 0;
+            m_track_info.track_start = 0;
+            got_track = true;
+            got_data = false;
+        }
+        else if (strncasecmp(m_parse_pos, "PREGAP ", 7) == 0)
+        {
+            const char *time_str = skip_space(m_parse_pos + 7);
+            m_track_info.unstored_pregap_length = parse_time(time_str);
+            m_track_info.pregap_start = 0;
+        }
+        else if (strncasecmp(m_parse_pos, "INDEX ", 6) == 0)
+        {
+            const char *index_str = skip_space(m_parse_pos + 6);
+            char *endptr;
+            int index = strtoul(index_str, &endptr, 10);
+
+            const char *time_str = skip_space(endptr);
+            uint32_t time = parse_time(time_str);
+
+            if (index == 0)
+            {
+                m_track_info.pregap_start = time;
+                m_track_info.track_start = time;
+            }
+            else if (index == 1)
+            {
+                m_track_info.file_offset += (uint64_t)(time - prev_data_start) * prev_sector_length;
+                m_track_info.data_start = time;
+                got_data = true;
+            }
+        }
+
+        next_line();
+    }
+
+    if (got_data && m_track_info.track_start == 0)
+    {
+        m_track_info.track_start = m_track_info.data_start;
+    }
+
+    if (got_track && got_data)
+        return &m_track_info;
+    else
+        return nullptr;
+}
+
+bool CUEParser::start_line()
+{
+    // Skip initial whitespace
+    while (isspace(*m_parse_pos))
+    {
+        m_parse_pos++;
+    }
+    return *m_parse_pos != '\0';
+}
+
+void CUEParser::next_line()
+{
+    // Find end of current line
+    const char *p = m_parse_pos;
+    while (*p != '\n' && *p != '\0')
+    {
+        p++;
+    }
+
+    // Skip any linefeeds
+    while (*p == '\n' || *p == '\r')
+    {
+        p++;
+    }
+
+    m_parse_pos = p;
+}
+
+const char *CUEParser::skip_space(const char *p) const
+{
+    while (isspace(*p)) p++;
+    return p;
+}
+
+const char *CUEParser::read_quoted(const char *src, char *dest, int dest_size)
+{
+    // Search for starting quote
+    while (*src != '"')
+    {
+        if (*src == '\0' || *src == '\n')
+        {
+            // Unexpected end of line / file
+            dest[0] = '\0';
+            return src;
+        }
+
+        src++;
+    }
+
+    src++;
+
+    // Copy text until ending quote
+    int len = 0;
+    while (*src != '"' && *src != '\0' && *src != '\n')
+    {
+        if (len < dest_size - 1)
+        {
+            dest[len++] = *src;
+        }
+
+        src++;
+    }
+
+    dest[len] = '\0';
+
+    if (*src == '"') src++;
+    return src;
+}
+
+uint32_t CUEParser::parse_time(const char *src)
+{
+    char *endptr;
+    uint32_t minutes = strtoul(src, &endptr, 10);
+    if (*endptr == ':') endptr++;
+    uint32_t seconds = strtoul(endptr, &endptr, 10);
+    if (*endptr == ':') endptr++;
+    uint32_t frames = strtoul(endptr, &endptr, 10);
+
+    return frames + 75 * (seconds + 60 * minutes);
+}
+
+CUEFileMode CUEParser::parse_file_mode(const char *src)
+{
+    if (strncasecmp(src, "BIN", 3) == 0)
+        return CUEFile_BINARY;
+    else if (strncasecmp(src, "MOTOROLA", 8) == 0)
+        return CUEFile_MOTOROLA;
+    else if (strncasecmp(src, "MP3", 3) == 0)
+        return CUEFile_MP3;
+    else if (strncasecmp(src, "WAV", 3) == 0)
+        return CUEFile_WAVE;
+    else if (strncasecmp(src, "AIFF", 4) == 0)
+        return CUEFile_AIFF;
+    else
+        return CUEFile_BINARY; // Default to binary mode
+}
+
+CUETrackMode CUEParser::parse_track_mode(const char *src)
+{
+    if (strncasecmp(src, "AUDIO", 5) == 0)
+        return CUETrack_AUDIO;
+    else if (strncasecmp(src, "CDG", 3) == 0)
+        return CUETrack_CDG;
+    else if (strncasecmp(src, "MODE1/2048", 10) == 0)
+        return CUETrack_MODE1_2048;
+    else if (strncasecmp(src, "MODE1/2352", 10) == 0)
+        return CUETrack_MODE1_2352;
+    else if (strncasecmp(src, "MODE2/2048", 10) == 0)
+        return CUETrack_MODE2_2048;
+    else if (strncasecmp(src, "MODE2/2324", 10) == 0)
+        return CUETrack_MODE2_2324;
+    else if (strncasecmp(src, "MODE2/2336", 10) == 0)
+        return CUETrack_MODE2_2336;
+    else if (strncasecmp(src, "MODE2/2352", 10) == 0)
+        return CUETrack_MODE2_2352;
+    else if (strncasecmp(src, "CDI/2336", 8) == 0)
+        return CUETrack_CDI_2336;
+    else if (strncasecmp(src, "CDI/2352", 8) == 0)
+        return CUETrack_CDI_2352;
+    else
+        return CUETrack_MODE1_2048; // Default to data track
+}
+
+uint32_t CUEParser::get_sector_length(CUEFileMode filemode, CUETrackMode trackmode)
+{
+    if (filemode == CUEFile_BINARY || filemode == CUEFile_MOTOROLA)
+    {
+        switch (trackmode)
+        {
+            case CUETrack_AUDIO:        return 2352;
+            case CUETrack_CDG:          return 2448;
+            case CUETrack_MODE1_2048:   return 2048;
+            case CUETrack_MODE1_2352:   return 2352;
+            case CUETrack_MODE2_2048:   return 2048;
+            case CUETrack_MODE2_2324:   return 2324;
+            case CUETrack_MODE2_2336:   return 2336;
+            case CUETrack_MODE2_2352:   return 2352;
+            case CUETrack_CDI_2336:     return 2336;
+            case CUETrack_CDI_2352:     return 2352;
+            default:                    return 2048;
+        }
+    }
+    else
+    {
+        return 0;
+    }
+}

+ 126 - 0
lib/CUEParser/src/CUEParser.h

@@ -0,0 +1,126 @@
+/*
+ * Simple CUE sheet parser suitable for embedded systems.
+ *
+ *  Copyright (c) 2023 Rabbit Hole Computing
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#ifndef CUE_MAX_FILENAME
+#define CUE_MAX_FILENAME 64
+#endif
+
+enum CUEFileMode
+{
+    CUEFile_BINARY = 0,
+    CUEFile_MOTOROLA,
+    CUEFile_MP3,
+    CUEFile_WAVE,
+    CUEFile_AIFF,
+};
+
+enum CUETrackMode
+{
+    CUETrack_AUDIO = 0,
+    CUETrack_CDG,
+    CUETrack_MODE1_2048,
+    CUETrack_MODE1_2352,
+    CUETrack_MODE2_2048,
+    CUETrack_MODE2_2324,
+    CUETrack_MODE2_2336,
+    CUETrack_MODE2_2352,
+    CUETrack_CDI_2336,
+    CUETrack_CDI_2352,
+};
+
+struct CUETrackInfo
+{
+    // Source file name and file type, and offset to start of track data in bytes.
+    char filename[CUE_MAX_FILENAME+1];
+    CUEFileMode file_mode;
+    uint64_t file_offset;
+
+    // Track number and mode in CD format
+    int track_number;
+    CUETrackMode track_mode;
+
+    // Sector length for this track in bytes in the file, or 0 for audio files
+    uint32_t sector_length;
+
+    // Unstored pregap length, in CD frames, or 0
+    uint32_t unstored_pregap_length;
+
+    // LBA start position of the pregap of this track (in CD frames)
+    uint32_t pregap_start;
+
+    // LBA start position of the data area of this track (in CD frames)
+    uint32_t data_start;
+
+    // Track start, either pregap_start or if no pregap, data_start.
+    uint32_t track_start;
+};
+
+class CUEParser
+{
+public:
+    CUEParser();
+
+    // Initialize the class to parse data from string.
+    // The string must remain valid for the lifetime of this object.
+    CUEParser(const char *cue_sheet);
+
+    // Restart parsing from beginning of file
+    void restart();
+
+    // Get information for next track.
+    // Returns nullptr when there are no more tracks.
+    // The returned pointer remains valid until next call to next_track()
+    // or destruction of this object.
+    const CUETrackInfo *next_track();
+
+protected:
+    const char *m_cue_sheet;
+    const char *m_parse_pos;
+    CUETrackInfo m_track_info;
+
+    // Skip any whitespace at beginning of line.
+    // Returns false if at end of string.
+    bool start_line();
+
+    // Advance parser to next line
+    void next_line();
+
+    // Skip spaces in string, return pointer to first non-space character
+    const char *skip_space(const char *p) const;
+
+    // Read text starting with " and ending with next "
+    // Returns pointer to character after ending quote.
+    const char *read_quoted(const char *src, char *dest, int dest_size);
+
+    // Parse time from MM:SS:FF format to frame number
+    uint32_t parse_time(const char *src);
+
+    // Parse file mode into enum
+    CUEFileMode parse_file_mode(const char *src);
+
+    // Parse track mode into enum
+    CUETrackMode parse_track_mode(const char *src);
+
+    // Get sector length in file from track mode
+    uint32_t get_sector_length(CUEFileMode filemode, CUETrackMode trackmode);
+};

+ 114 - 0
lib/CUEParser/test/CUEParser_test.cpp

@@ -0,0 +1,114 @@
+#include "CUEParser.h"
+#include <stdio.h>
+#include <string.h>
+
+/* Unit test helpers */
+#define COMMENT(x) printf("\n----" x "----\n");
+#define TEST(x) \
+    if (!(x)) { \
+        fprintf(stderr, "\033[31;1mFAILED:\033[22;39m %s:%d %s\n", __FILE__, __LINE__, #x); \
+        status = 1; \
+    } else { \
+        printf("\033[32;1mOK:\033[22;39m %s\n", #x); \
+    }
+
+int test_basics()
+{
+    int status = 0;
+    const char *cue_sheet = R"(
+FILE "Image Name.bin" BINARY
+  TRACK 01 MODE1/2048
+    INDEX 01 00:00:00
+  TRACK 02 AUDIO
+    PREGAP 00:02:00
+    INDEX 01 02:47:20
+  TRACK 03 AUDIO
+    INDEX 00 07:55:58
+    INDEX 01 07:55:65
+FILE "Sound.wav" WAVE
+  TRACK 11 AUDIO
+    INDEX 00 00:00:00
+    INDEX 01 00:02:00
+    )";
+
+    CUEParser parser(cue_sheet);
+
+    COMMENT("Test TRACK 01 (data)");
+    const CUETrackInfo *track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 0);
+        TEST(track->track_number == 1);
+        TEST(track->track_mode == CUETrack_MODE1_2048);
+        TEST(track->sector_length == 2048);
+        TEST(track->unstored_pregap_length == 0);
+        TEST(track->data_start == 0);
+    }
+
+    COMMENT("Test TRACK 02 (audio with pregap)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    uint32_t start2 = ((2 * 60) + 47) * 75 + 20;
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 2048 * start2);
+        TEST(track->track_number == 2);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->sector_length == 2352);
+        TEST(track->unstored_pregap_length == 2 * 75);
+        TEST(track->data_start == start2);
+    }
+
+    COMMENT("Test TRACK 03 (audio with index 0)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    uint32_t start3 = ((7 * 60) + 55) * 75 + 65;
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->file_offset == 2048 * start2 + 2352 * (start3 - start2));
+        TEST(track->track_number == 3);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->sector_length == 2352);
+        TEST(track->pregap_start == ((7 * 60) + 55) * 75 + 58);
+        TEST(track->data_start == start3);
+    }
+
+    COMMENT("Test TRACK 11 (audio from wav)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Sound.wav") == 0);
+        TEST(track->file_mode == CUEFile_WAVE);
+        TEST(track->file_offset == 0);
+        TEST(track->track_number == 11);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->sector_length == 0);
+        TEST(track->pregap_start == 0);
+        TEST(track->data_start == 2 * 75);
+    }
+
+    COMMENT("Test end of file");
+    track = parser.next_track();
+    TEST(track == NULL);
+
+    COMMENT("Test restart");
+    parser.restart();
+    track = parser.next_track();
+    TEST(track != NULL && track->track_number == 1);
+
+    return status;
+}
+
+
+int main()
+{
+    return test_basics();
+}

+ 7 - 0
lib/CUEParser/test/Makefile

@@ -0,0 +1,7 @@
+# Run basic unit tests for the CUEParser library
+
+all: CUEParser_test
+	./CUEParser_test
+
+CUEParser_test: CUEParser_test.cpp ../src/CUEParser.cpp
+	g++ -Wall -Wextra -o $@ -I ../src $^

+ 0 - 368
lib/SCSI2SD/src/firmware/cdrom.c

@@ -1,368 +0,0 @@
-//	Copyright (C) 2014 Michael McMaster <michael@codesrc.com>
-//
-//	This file is part of SCSI2SD.
-//
-//	SCSI2SD 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.
-//
-//	SCSI2SD 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 SCSI2SD.  If not, see <http://www.gnu.org/licenses/>.
-
-#include "scsi.h"
-#include "config.h"
-#include "cdrom.h"
-
-#include <string.h>
-
-static const uint8_t SimpleTOC[] =
-{
-	0x00, // toc length, MSB
-	0x12, // toc length, LSB
-	0x01, // First track number
-	0x01, // Last track number,
-	// TRACK 1 Descriptor
-	0x00, // reserved
-	0x14, // Q sub-channel encodes current position, Digital track
-	0x01, // Track 1,
-	0x00, // Reserved
-	0x00,0x00,0x00,0x00, // Track start sector (LBA)
-	0x00, // reserved
-	0x14, // Q sub-channel encodes current position, Digital track
-	0xAA, // Leadout Track
-	0x00, // Reserved
-	0x00,0x00,0x00,0x00, // Track start sector (LBA)
-};
-
-static const uint8_t LeadoutTOC[] =
-{
-	0x00, // toc length, MSB
-	0x0A, // toc length, LSB
-	0x01, // First track number
-	0x01, // Last track number,
-	0x00, // reserved
-	0x14, // Q sub-channel encodes current position, Digital track
-	0xAA, // Leadout Track
-	0x00, // Reserved
-	0x00,0x00,0x00,0x00, // Track start sector (LBA)
-};
-
-static const uint8_t SessionTOC[] =
-{
-	0x00, // toc length, MSB
-	0x0A, // toc length, LSB
-	0x01, // First session number
-	0x01, // Last session number,
-	// TRACK 1 Descriptor
-	0x00, // reserved
-	0x14, // Q sub-channel encodes current position, Digital track
-	0x01, // First track number in last complete session
-	0x00, // Reserved
-	0x00,0x00,0x00,0x00 // LBA of first track in last session
-};
-
-
-static const uint8_t FullTOC[] =
-{
-	0x00, // toc length, MSB
-	0x44, // toc length, LSB
-	0x01, // First session number
-	0x01, // Last session number,
-	// A0 Descriptor
-	0x01, // session number
-	0x14, // ADR/Control
-	0x00, // TNO
-	0xA0, // POINT
-	0x00, // Min
-	0x00, // Sec
-	0x00, // Frame
-	0x00, // Zero
-	0x01, // First Track number.
-	0x00, // Disc type 00 = Mode 1
-	0x00,  // PFRAME
-	// A1
-	0x01, // session number
-	0x14, // ADR/Control
-	0x00, // TNO
-	0xA1, // POINT
-	0x00, // Min
-	0x00, // Sec
-	0x00, // Frame
-	0x00, // Zero
-	0x01, // Last Track number
-	0x00, // PSEC
-	0x00,  // PFRAME
-	// A2
-	0x01, // session number
-	0x14, // ADR/Control
-	0x00, // TNO
-	0xA2, // POINT
-	0x00, // Min
-	0x00, // Sec
-	0x00, // Frame
-	0x00, // Zero
-	0x79, // LEADOUT position BCD
-	0x59, // leadout PSEC BCD
-	0x74, // leadout PFRAME BCD
-	// TRACK 1 Descriptor
-	0x01, // session number
-	0x14, // ADR/Control
-	0x00, // TNO
-	0x01, // Point
-	0x00, // Min
-	0x00, // Sec
-	0x00, // Frame
-	0x00, // Zero
-	0x00, // PMIN
-	0x00, // PSEC
-	0x00,  // PFRAME
-	// b0
-	0x01, // session number
-	0x54, // ADR/Control
-	0x00, // TNO
-	0xB1, // POINT
-	0x79, // Min BCD
-	0x59, // Sec BCD
-	0x74, // Frame BCD
-	0x00, // Zero
-	0x79, // PMIN BCD
-	0x59, // PSEC BCD
-	0x74,  // PFRAME BCD
-	// c0
-	0x01, // session number
-	0x54, // ADR/Control
-	0x00, // TNO
-	0xC0, // POINT
-	0x00, // Min
-	0x00, // Sec
-	0x00, // Frame
-	0x00, // Zero
-	0x00, // PMIN
-	0x00, // PSEC
-	0x00  // PFRAME
-};
-
-static void LBA2MSF(uint32_t LBA, uint8_t* MSF)
-{
-	MSF[0] = 0; // reserved.
-	MSF[3] = LBA % 75; // M
-	uint32_t rem = LBA / 75;
-
-	MSF[2] = rem % 60; // S
-	MSF[1] = rem / 60;
-
-}
-
-static void doReadTOC(int MSF, uint8_t track, uint16_t allocationLength)
-{
-	if (track == 0xAA)
-	{
-		// 0xAA requests only lead-out track information (reports capacity)
-		uint32_t len = sizeof(LeadoutTOC);
-		memcpy(scsiDev.data, LeadoutTOC, len);
-
-		uint32_t capacity = getScsiCapacity(
-			scsiDev.target->cfg->sdSectorStart,
-			scsiDev.target->liveCfg.bytesPerSector,
-			scsiDev.target->cfg->scsiSectors);
-
-		// Replace start of leadout track
-		if (MSF)
-		{
-			LBA2MSF(capacity, scsiDev.data + 8);
-		}
-		else
-		{
-			scsiDev.data[8] = capacity >> 24;
-			scsiDev.data[9] = capacity >> 16;
-			scsiDev.data[10] = capacity >> 8;
-			scsiDev.data[11] = capacity;
-		}
-
-		if (len > allocationLength)
-		{
-			len = allocationLength;
-		}
-		scsiDev.dataLen = len;
-		scsiDev.phase = DATA_IN;
-	}
-	else if (track <= 1)
-	{
-		// We only support track 1.
-		// track 0 means "return all tracks"
-		uint32_t len = sizeof(SimpleTOC);
-		memcpy(scsiDev.data, SimpleTOC, len);
-
-		uint32_t capacity = getScsiCapacity(
-			scsiDev.target->cfg->sdSectorStart,
-			scsiDev.target->liveCfg.bytesPerSector,
-			scsiDev.target->cfg->scsiSectors);
-
-		// Replace start of leadout track
-		if (MSF)
-		{
-			LBA2MSF(capacity, scsiDev.data + 0x10);
-		}
-		else
-		{
-			scsiDev.data[0x10] = capacity >> 24;
-			scsiDev.data[0x11] = capacity >> 16;
-			scsiDev.data[0x12] = capacity >> 8;
-			scsiDev.data[0x13] = capacity;
-		}
-
-		if (len > allocationLength)
-		{
-			len = allocationLength;
-		}
-		scsiDev.dataLen = len;
-		scsiDev.phase = DATA_IN;
-	}
-	else
-	{
-		scsiDev.status = CHECK_CONDITION;
-		scsiDev.target->sense.code = ILLEGAL_REQUEST;
-		scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
-		scsiDev.phase = STATUS;
-	}
-}
-
-static void doReadSessionInfo(uint8_t session, uint16_t allocationLength)
-{
-	uint32_t len = sizeof(SessionTOC);
-	memcpy(scsiDev.data, SessionTOC, len);
-
-	if (len > allocationLength)
-	{
-		len = allocationLength;
-	}
-	scsiDev.dataLen = len;
-	scsiDev.phase = DATA_IN;
-}
-
-static inline uint8_t
-fromBCD(uint8_t val)
-{
-	return ((val >> 4) * 10) + (val & 0xF);
-}
-
-static void doReadFullTOC(int convertBCD, uint8_t session, uint16_t allocationLength)
-{
-	// We only support session 1.
-	if (session > 1)
-	{
-		scsiDev.status = CHECK_CONDITION;
-		scsiDev.target->sense.code = ILLEGAL_REQUEST;
-		scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
-		scsiDev.phase = STATUS;
-	}
-	else
-	{
-		uint32_t len = sizeof(FullTOC);
-		memcpy(scsiDev.data, FullTOC, len);
-
-		if (convertBCD)
-		{
-			int descriptor = 4;
-			while (descriptor < len)
-			{
-				int i;
-				for (i = 0; i < 7; ++i)
-				{
-					scsiDev.data[descriptor + i] =
-						fromBCD(scsiDev.data[descriptor + 4 + i]);
-				}
-				descriptor += 11;
-			}
-
-		}
-
-		if (len > allocationLength)
-		{
-			len = allocationLength;
-		}
-		scsiDev.dataLen = len;
-		scsiDev.phase = DATA_IN;
-	}
-}
-
-static uint8_t SimpleHeader[] =
-{
-	0x01, // 2048byte user data, L-EC in 288 byte aux field.
-	0x00, // reserved
-	0x00, // reserved
-	0x00, // reserved
-	0x00,0x00,0x00,0x00 // Track start sector (LBA or MSF)
-};
-
-void doReadHeader(int MSF, uint32_t lba, uint16_t allocationLength)
-{
-	uint32_t len = sizeof(SimpleHeader);
-	memcpy(scsiDev.data, SimpleHeader, len);
-	if (len > allocationLength)
-	{
-		len = allocationLength;
-	}
-	scsiDev.dataLen = len;
-	scsiDev.phase = DATA_IN;
-}
-
-
-// Handle direct-access scsi device commands
-int scsiCDRomCommand()
-{
-	int commandHandled = 1;
-
-	uint8_t command = scsiDev.cdb[0];
-	if (command == 0x43)
-	{
-		// CD-ROM Read TOC
-		int MSF = scsiDev.cdb[1] & 0x02 ? 1 : 0;
-		uint8_t track = scsiDev.cdb[6];
-		uint16_t allocationLength =
-			(((uint32_t) scsiDev.cdb[7]) << 8) +
-			scsiDev.cdb[8];
-
-		// Reject MMC commands for now, otherwise the TOC data format
-		// won't be understood.
-		// The "format" field is reserved for SCSI-2
-		uint8_t format = scsiDev.cdb[2] & 0x0F;
-		switch (format)
-		{
-			case 0: doReadTOC(MSF, track, allocationLength); break; // SCSI-2
-			case 1: doReadSessionInfo(MSF, allocationLength); break; // MMC2
-			case 2: doReadFullTOC(0, track, allocationLength); break; // MMC2
-			case 3: doReadFullTOC(1, track, allocationLength); break; // MMC2
-			default:
-			{
-				scsiDev.status = CHECK_CONDITION;
-				scsiDev.target->sense.code = ILLEGAL_REQUEST;
-				scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
-				scsiDev.phase = STATUS;
-			}
-		}
-	}
-	else if (command == 0x44)
-	{
-		// CD-ROM Read Header
-		int MSF = scsiDev.cdb[1] & 0x02 ? 1 : 0;
-		uint32_t lba = 0; // IGNORED for now
-		uint16_t allocationLength =
-			(((uint32_t) scsiDev.cdb[7]) << 8) +
-			scsiDev.cdb[8];
-		doReadHeader(MSF, lba, allocationLength);
-	}
-	else
-	{
-		commandHandled = 0;
-	}
-
-	return commandHandled;
-}
-

+ 4 - 0
platformio.ini

@@ -21,6 +21,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_template
     SCSI2SD
+    CUEParser
 
 ; ZuluSCSI V1.0 hardware platform with GD32F205 CPU.
 [env:ZuluSCSIv1_0]
@@ -37,6 +38,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_GD32F205
     SCSI2SD
+    CUEParser
 upload_protocol = stlink
 platform_packages = 
     toolchain-gccarmnoneeabi@1.60301.0
@@ -88,6 +90,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_RP2040
     SCSI2SD
+    CUEParser
 build_flags =
     -O2 -Isrc -ggdb -g3
     -Wall -Wno-sign-compare -Wno-ignored-qualifiers
@@ -119,6 +122,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_RP2040
     SCSI2SD
+    CUEParser
 build_flags =
     -O2 -Isrc -ggdb -g3
     -Wall -Wno-sign-compare -Wno-ignored-qualifiers

+ 315 - 0
src/ImageBackingStore.cpp

@@ -0,0 +1,315 @@
+/**
+ * ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+ *
+ * This file is licensed under the GPL version 3 or any later version. 
+ * It is derived from disk.c in SCSI2SD V6
+ *
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+**/
+
+#include "ImageBackingStore.h"
+#include <SdFat.h>
+#include <ZuluSCSI_platform.h>
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_config.h"
+#include <minIni.h>
+#include <strings.h>
+#include <string.h>
+#include <assert.h>
+
+ImageBackingStore::ImageBackingStore()
+{
+    m_israw = false;
+    m_isrom = false;
+    m_isreadonly_attr = false;
+    m_blockdev = nullptr;
+    m_bgnsector = m_endsector = m_cursector = 0;
+}
+
+ImageBackingStore::ImageBackingStore(const char *filename, uint32_t scsi_block_size): ImageBackingStore()
+{
+    if (strncasecmp(filename, "RAW:", 4) == 0)
+    {
+        char *endptr, *endptr2;
+        m_bgnsector = strtoul(filename + 4, &endptr, 0);
+        m_endsector = strtoul(endptr + 1, &endptr2, 0);
+
+        if (*endptr != ':' || *endptr2 != '\0')
+        {
+            logmsg("Invalid format for raw filename: ", filename);
+            return;
+        }
+
+        if ((scsi_block_size % SD_SECTOR_SIZE) != 0)
+        {
+            logmsg("SCSI block size ", (int)scsi_block_size, " is not supported for RAW partitions (must be divisible by 512 bytes)");
+            return;
+        }
+
+        m_israw = true;
+        m_blockdev = SD.card();
+
+        uint32_t sectorCount = SD.card()->sectorCount();
+        if (m_endsector >= sectorCount)
+        {
+            logmsg("Limiting RAW image mapping to SD card sector count: ", (int)sectorCount);
+            m_endsector = sectorCount - 1;
+        }
+    }
+    else if (strncasecmp(filename, "ROM:", 4) == 0)
+    {
+        if (!romDriveCheckPresent(&m_romhdr))
+        {
+            m_romhdr.imagesize = 0;
+        }
+        else
+        {
+            m_isrom = true;
+        }
+    }
+    else
+    {
+        m_isreadonly_attr = !!(FS_ATTRIB_READ_ONLY & SD.attrib(filename));
+        if (m_isreadonly_attr)
+        {
+            m_fsfile = SD.open(filename, O_RDONLY);
+            logmsg("---- Image file is read-only, writes disabled");
+        }
+        else
+        {
+            m_fsfile = SD.open(filename, O_RDWR);
+        }
+
+        uint32_t sectorcount = m_fsfile.size() / SD_SECTOR_SIZE;
+        uint32_t begin = 0, end = 0;
+        if (m_fsfile.contiguousRange(&begin, &end) && end >= begin + sectorcount
+            && (scsi_block_size % SD_SECTOR_SIZE) == 0)
+        {
+            // Convert to raw mapping, this avoids some unnecessary
+            // access overhead in SdFat library.
+            // If non-aligned offsets are later requested, it automatically falls
+            // back to SdFat access mode.
+            m_israw = true;
+            m_blockdev = SD.card();
+            m_bgnsector = begin;
+
+            if (end != begin + sectorcount)
+            {
+                uint32_t allocsize = end - begin + 1;
+                // Due to issue #80 in ZuluSCSI version 1.0.8 and 1.0.9 the allocated size was mistakenly reported to SCSI controller.
+                // If the drive was formatted using those versions, you may have problems accessing it with newer firmware.
+                // The old behavior can be restored with setting  [SCSI] UseFATAllocSize = 1 in config file.
+
+                if (ini_getbool("SCSI", "UseFATAllocSize", 0, CONFIGFILE))
+                {
+                    sectorcount = allocsize;
+                }
+            }
+
+            m_endsector = begin + sectorcount - 1;
+            m_fsfile.flush(); // Note: m_fsfile is also kept open as a fallback.
+        }
+    }
+}
+
+bool ImageBackingStore::isOpen()
+{
+    if (m_israw)
+        return (m_blockdev != NULL);
+    else if (m_isrom)
+        return (m_romhdr.imagesize > 0);
+    else
+        return m_fsfile.isOpen();
+}
+
+bool ImageBackingStore::isWritable()
+{
+    return !m_isrom && !m_isreadonly_attr;
+}
+
+bool ImageBackingStore::isRom()
+{
+    return m_isrom;
+}
+
+bool ImageBackingStore::close()
+{
+    if (m_israw)
+    {
+        m_blockdev = nullptr;
+        return true;
+    }
+    else if (m_isrom)
+    {
+        m_romhdr.imagesize = 0;
+        return true;
+    }
+    else
+    {
+        return m_fsfile.close();
+    }
+}
+
+uint64_t ImageBackingStore::size()
+{
+    if (m_israw && m_blockdev)
+    {
+        return (uint64_t)(m_endsector - m_bgnsector + 1) * SD_SECTOR_SIZE;
+    }
+    else if (m_isrom)
+    {
+        return m_romhdr.imagesize;
+    }
+    else
+    {
+        return m_fsfile.size();
+    }
+}
+
+bool ImageBackingStore::contiguousRange(uint32_t* bgnSector, uint32_t* endSector)
+{
+    if (m_israw && m_blockdev)
+    {
+        *bgnSector = m_bgnsector;
+        *endSector = m_endsector;
+        return true;
+    }
+    else if (m_isrom)
+    {
+        *bgnSector = 0;
+        *endSector = 0;
+        return true;
+    }
+    else
+    {
+        return m_fsfile.contiguousRange(bgnSector, endSector);
+    }
+}
+
+bool ImageBackingStore::seek(uint64_t pos)
+{
+    uint32_t sectornum = pos / SD_SECTOR_SIZE;
+
+    if (m_israw && (uint64_t)sectornum * SD_SECTOR_SIZE != pos)
+    {
+        dbgmsg("---- Unaligned access to image, falling back to SdFat access mode");
+        m_israw = false;
+    }
+
+    if (m_israw)
+    {
+        m_cursector = m_bgnsector + sectornum;
+        return (m_cursector <= m_endsector);
+    }
+    else if (m_isrom)
+    {
+        uint32_t sectornum = pos / SD_SECTOR_SIZE;
+        assert((uint64_t)sectornum * SD_SECTOR_SIZE == pos);
+        m_cursector = sectornum;
+        return m_cursector * SD_SECTOR_SIZE < m_romhdr.imagesize;
+    }
+    else
+    {
+        return m_fsfile.seek(pos);
+    }
+}
+
+ssize_t ImageBackingStore::read(void* buf, size_t count)
+{
+    uint32_t sectorcount = count / SD_SECTOR_SIZE;
+    if (m_israw && (uint64_t)sectorcount * SD_SECTOR_SIZE != count)
+    {
+        dbgmsg("---- Unaligned access to image, falling back to SdFat access mode");
+        m_israw = false;
+    }
+
+    if (m_israw && m_blockdev)
+    {
+        if (m_blockdev->readSectors(m_cursector, (uint8_t*)buf, sectorcount))
+        {
+            m_cursector += sectorcount;
+            return count;
+        }
+        else
+        {
+            return -1;
+        }
+    }
+    else if (m_isrom)
+    {
+        uint32_t sectorcount = count / SD_SECTOR_SIZE;
+        assert((uint64_t)sectorcount * SD_SECTOR_SIZE == count);
+        uint32_t start = m_cursector * SD_SECTOR_SIZE;
+        if (romDriveRead((uint8_t*)buf, start, count))
+        {
+            m_cursector += sectorcount;
+            return count;
+        }
+        else
+        {
+            return -1;
+        }
+    }
+    else
+    {
+        return m_fsfile.read(buf, count);
+    }
+}
+
+ssize_t ImageBackingStore::write(const void* buf, size_t count)
+{
+    uint32_t sectorcount = count / SD_SECTOR_SIZE;
+    if (m_israw && (uint64_t)sectorcount * SD_SECTOR_SIZE != count)
+    {
+        dbgmsg("---- Unaligned access to image, falling back to SdFat access mode");
+        m_israw = false;
+    }
+
+    if (m_israw && m_blockdev)
+    {
+        if (m_blockdev->writeSectors(m_cursector, (const uint8_t*)buf, sectorcount))
+        {
+            m_cursector += sectorcount;
+            return count;
+        }
+        else
+        {
+            return 0;
+        }
+    }
+    else if (m_isrom)
+    {
+        logmsg("ERROR: attempted to write to ROM drive");
+        return 0;
+    }
+    else  if (m_isreadonly_attr)
+    {
+        logmsg("ERROR: attempted to write to a read only image");
+        return 0;
+    }
+    else
+    {
+        return m_fsfile.write(buf, count);
+    }
+}
+
+void ImageBackingStore::flush()
+{
+    if (!m_israw && !m_isrom && !m_isreadonly_attr)
+    {
+        m_fsfile.flush();
+    }
+}

+ 84 - 0
src/ImageBackingStore.h

@@ -0,0 +1,84 @@
+/* Access layer to image files associated with a SCSI device.
+ * Currently supported image storage modes:
+ *
+ * - Files on SD card
+ * - Raw SD card partitions
+ * - Microcontroller flash ROM drive
+ */
+
+#pragma once
+#include <stdint.h>
+#include <unistd.h>
+#include <SdFat.h>
+#include "ROMDrive.h"
+
+extern "C" {
+#include <scsi.h>
+}
+
+// SD card sector size is always 512 bytes
+extern SdFs SD;
+#define SD_SECTOR_SIZE 512
+
+// This class wraps SdFat library FsFile to allow access
+// through either FAT filesystem or as a raw sector range.
+//
+// Raw access is activated by using filename like "RAW:0:12345"
+// where the numbers are the first and last sector.
+//
+// If the platform supports a ROM drive, it is activated by using
+// filename "ROM:".
+class ImageBackingStore
+{
+public:
+    // Empty image, cannot be accessed
+    ImageBackingStore();
+
+    // Parse image file parameters from filename.
+    // Special filename formats:
+    //    RAW:start:end
+    //    ROM:
+    ImageBackingStore(const char *filename, uint32_t scsi_block_size);
+
+    // Can the image be read?
+    bool isOpen();
+
+    // Can the image be written?
+    bool isWritable();
+
+    // Is this internal ROM drive in microcontroller flash?
+    bool isRom();
+
+    // Close the image so that .isOpen() will return false.
+    bool close();
+
+    // Return image size in bytes
+    uint64_t size();
+
+    // Check if the image sector range is contiguous, and the image is on
+    // SD card, return the sector numbers.
+    bool contiguousRange(uint32_t* bgnSector, uint32_t* endSector);
+
+    // Set current position for following read/write operations
+    bool seek(uint64_t pos);
+
+    // Read data from the image file, returns number of bytes read, or negative on error.
+    ssize_t read(void* buf, size_t count);
+
+    // Write data to image file, returns number of bytes written, or negative on error.
+    ssize_t write(const void* buf, size_t count);
+
+    // Flush any pending changes to filesystem
+    void flush();
+
+protected:
+    bool m_israw;
+    bool m_isrom;
+    bool m_isreadonly_attr;
+    romdrive_hdr_t m_romhdr;
+    FsFile m_fsfile;
+    SdCard *m_blockdev;
+    uint32_t m_bgnsector;
+    uint32_t m_endsector;
+    uint32_t m_cursector;
+};

+ 178 - 0
src/ROMDrive.cpp

@@ -0,0 +1,178 @@
+/**
+ * ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+ * Function romDriveClear() Copyright (c) 2023 Eric Helgeson
+ *
+ * This file is licensed under the GPL version 3 or any later version. 
+ * It is derived from disk.c in SCSI2SD V6
+ *
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+**/
+
+#include "ROMDrive.h"
+#include <SdFat.h>
+#include <ZuluSCSI_platform.h>
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_config.h"
+#include <strings.h>
+#include <string.h>
+
+extern "C" {
+#include <scsi.h>
+}
+
+extern SdFs SD;
+
+#ifndef PLATFORM_HAS_ROM_DRIVE
+
+bool romDriveCheckPresent(romdrive_hdr_t *hdr)
+{
+    return false;
+}
+
+bool romDriveClear()
+{
+    logmsg("---- Platform does not support ROM drive");
+    return false;
+}
+
+bool scsiDiskProgramRomDrive(const char *filename, int scsi_id, int blocksize, S2S_CFG_TYPE type)
+{
+    logmsg("---- Platform does not support ROM drive");
+    return false;
+}
+
+bool romDriveRead(uint8_t *buf, uint32_t start, uint32_t count)
+{
+    return false;
+}
+
+#else
+
+// Check if the romdrive is present
+bool romDriveCheckPresent(romdrive_hdr_t *hdr)
+{
+    romdrive_hdr_t tmp;
+    if (!hdr) hdr = &tmp;
+
+    if (!platform_read_romdrive((uint8_t*)hdr, 0, sizeof(romdrive_hdr_t)))
+    {
+        return false;
+    }
+
+    if (memcmp(hdr->magic, "ROMDRIVE", 8) != 0)
+    {
+        return false;
+    }
+
+    if (hdr->imagesize <= 0 || hdr->scsi_id < 0 || hdr->scsi_id > 8)
+    {
+        return false;
+    }
+
+    return true;
+}
+
+// Clear the drive metadata header
+bool romDriveClear()
+{
+    romdrive_hdr_t hdr = {0x0};
+
+    if (!platform_write_romdrive((const uint8_t*)&hdr, 0, PLATFORM_ROMDRIVE_PAGE_SIZE))
+    {
+        logmsg("-- Failed to clear ROM drive");
+        return false;
+    }
+    logmsg("-- Cleared ROM drive");
+    SD.remove("CLEAR_ROM");
+    return true;
+}
+
+// Load an image file to romdrive
+bool scsiDiskProgramRomDrive(const char *filename, int scsi_id, int blocksize, S2S_CFG_TYPE type)
+{
+    FsFile file = SD.open(filename, O_RDONLY);
+    if (!file.isOpen())
+    {
+        logmsg("---- Failed to open: ", filename);
+        return false;
+    }
+
+    uint64_t filesize = file.size();
+    uint32_t maxsize = platform_get_romdrive_maxsize() - PLATFORM_ROMDRIVE_PAGE_SIZE;
+
+    logmsg("---- SCSI ID: ", scsi_id, " blocksize ", blocksize, " type ", (int)type);
+    logmsg("---- ROM drive maximum size is ", (int)maxsize,
+          " bytes, image file is ", (int)filesize, " bytes");
+
+    if (filesize > maxsize)
+    {
+        logmsg("---- Image size exceeds ROM space, not loading");
+        file.close();
+        return false;
+    }
+
+    romdrive_hdr_t hdr = {};
+    memcpy(hdr.magic, "ROMDRIVE", 8);
+    hdr.scsi_id = scsi_id;
+    hdr.imagesize = filesize;
+    hdr.blocksize = blocksize;
+    hdr.drivetype = type;
+
+    // Program the drive metadata header
+    if (!platform_write_romdrive((const uint8_t*)&hdr, 0, PLATFORM_ROMDRIVE_PAGE_SIZE))
+    {
+        logmsg("---- Failed to program ROM drive header");
+        file.close();
+        return false;
+    }
+
+    // Program the drive contents
+    uint32_t pages = (filesize + PLATFORM_ROMDRIVE_PAGE_SIZE - 1) / PLATFORM_ROMDRIVE_PAGE_SIZE;
+    for (uint32_t i = 0; i < pages; i++)
+    {
+        if (i % 2)
+            LED_ON();
+        else
+            LED_OFF();
+
+        if (file.read(scsiDev.data, PLATFORM_ROMDRIVE_PAGE_SIZE) <= 0 ||
+            !platform_write_romdrive(scsiDev.data, (i + 1) * PLATFORM_ROMDRIVE_PAGE_SIZE, PLATFORM_ROMDRIVE_PAGE_SIZE))
+        {
+            logmsg("---- Failed to program ROM drive page ", (int)i);
+            file.close();
+            return false;
+        }
+    }
+
+    LED_OFF();
+
+    file.close();
+
+    char newname[MAX_FILE_PATH * 2] = "";
+    strlcat(newname, filename, sizeof(newname));
+    strlcat(newname, "_loaded", sizeof(newname));
+    SD.rename(filename, newname);
+    logmsg("---- ROM drive programming successful, image file renamed to ", newname);
+
+    return true;
+}
+
+bool romDriveRead(uint8_t *buf, uint32_t start, uint32_t count)
+{
+    return platform_read_romdrive(buf, start + PLATFORM_ROMDRIVE_PAGE_SIZE, count);
+}
+
+#endif

+ 32 - 0
src/ROMDrive.h

@@ -0,0 +1,32 @@
+/* Access layer to microcontroller internal flash ROM drive storage.
+ * Can store a small disk image.
+ */
+
+#pragma once
+#include <stdint.h>
+#include <unistd.h>
+#include <scsi2sd.h>
+
+// Header used for storing the rom drive parameters in flash
+struct romdrive_hdr_t {
+    char magic[8]; // "ROMDRIVE"
+    int scsi_id;
+    uint32_t imagesize;
+    uint32_t blocksize;
+    S2S_CFG_TYPE drivetype;
+    uint32_t reserved[32];
+};
+
+// Return true if ROM drive is found.
+// If hdr is not NULL, it will receive the ROM drive header information.
+// If flash is empty, returns false.
+bool romDriveCheckPresent(romdrive_hdr_t *hdr = nullptr);
+
+// Clear any existing ROM drive, returning flash to empty state
+bool romDriveClear();
+
+// Program ROM drive image to flash
+bool romDriveProgram(const char *filename, int scsi_id, int blocksize, S2S_CFG_TYPE type);
+
+// Read data from rom drive main data area
+bool romDriveRead(uint8_t *buf, uint32_t start, uint32_t count);

+ 8 - 2
src/ZuluSCSI.cpp

@@ -52,6 +52,7 @@
 #include "ZuluSCSI_log_trace.h"
 #include "ZuluSCSI_disk.h"
 #include "ZuluSCSI_initiator.h"
+#include "ROMDrive.h"
 
 SdFs SD;
 FsFile g_logfile;
@@ -347,7 +348,7 @@ bool findHDDImages()
       if(strcasecmp(name, "CLEAR_ROM") == 0)
       {
         logmsg("-- Special filename: '", name, "'");
-        scsiDiskClearRomDrive();
+        romDriveClear();
         continue;
       }
 
@@ -401,6 +402,11 @@ bool findHDDImages()
           continue;
         }
 
+        if (strcasecmp(extension, ".cue") == 0)
+        {
+          continue; // .cue will be handled with corresponding .bin
+        }
+
         // Check if the image should be loaded to microcontroller flash ROM drive
         bool is_romdrive = false;
         if (extension && strcasecmp(extension, ".rom") == 0)
@@ -642,7 +648,7 @@ extern "C" void zuluscsi_setup(void)
     logmsg("SD card init failed, sdErrorCode: ", (int)SD.sdErrorCode(),
            " sdErrorData: ", (int)SD.sdErrorData());
 
-    if (scsiDiskCheckRomDrive())
+    if (romDriveCheckPresent())
     {
       reinitSCSI();
       if (g_romdrive_active)

+ 1442 - 0
src/ZuluSCSI_cdrom.cpp

@@ -0,0 +1,1442 @@
+/* Advanced CD-ROM drive emulation.
+ * Adds a few capabilities on top of the SCSI2SD CD-ROM emulation:
+ *
+ * - bin/cue support for support of multiple tracks
+ * - on the fly image switching
+ *
+ * SCSI2SD V6 - Copyright (C) 2014 Michael McMaster <michael@codesrc.com>
+ * ZuluSCSI™ - Copyright (c) 2023 Rabbit Hole Computing™
+ *
+ * This file is licensed under the GPL version 3 or any later version. 
+ * It is derived from cdrom.c in SCSI2SD V6
+ *
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include <string.h>
+#include "ZuluSCSI_cdrom.h"
+#include "ZuluSCSI_log.h"
+#include "ZuluSCSI_config.h"
+#include <CUEParser.h>
+#include <assert.h>
+
+extern "C" {
+#include <scsi.h>
+}
+
+/******************************************/
+/* Basic TOC generation without cue sheet */
+/******************************************/
+
+static const uint8_t SimpleTOC[] =
+{
+    0x00, // toc length, MSB
+    0x12, // toc length, LSB
+    0x01, // First track number
+    0x01, // Last track number,
+    // TRACK 1 Descriptor
+    0x00, // reserved
+    0x14, // Q sub-channel encodes current position, Digital track
+    0x01, // Track 1,
+    0x00, // Reserved
+    0x00,0x00,0x00,0x00, // Track start sector (LBA)
+    0x00, // reserved
+    0x14, // Q sub-channel encodes current position, Digital track
+    0xAA, // Leadout Track
+    0x00, // Reserved
+    0x00,0x00,0x00,0x00, // Track start sector (LBA)
+};
+
+static const uint8_t LeadoutTOC[] =
+{
+    0x00, // toc length, MSB
+    0x0A, // toc length, LSB
+    0x01, // First track number
+    0x01, // Last track number,
+    0x00, // reserved
+    0x14, // Q sub-channel encodes current position, Digital track
+    0xAA, // Leadout Track
+    0x00, // Reserved
+    0x00,0x00,0x00,0x00, // Track start sector (LBA)
+};
+
+static const uint8_t SessionTOC[] =
+{
+    0x00, // toc length, MSB
+    0x0A, // toc length, LSB
+    0x01, // First session number
+    0x01, // Last session number,
+    // TRACK 1 Descriptor
+    0x00, // reserved
+    0x14, // Q sub-channel encodes current position, Digital track
+    0x01, // First track number in last complete session
+    0x00, // Reserved
+    0x00,0x00,0x00,0x00 // LBA of first track in last session
+};
+
+
+static const uint8_t FullTOC[] =
+{
+    0x00, //  0: toc length, MSB
+    0x44, //  1: toc length, LSB
+    0x01, //  2: First session number
+    0x01, //  3: Last session number,
+    // A0 Descriptor
+    0x01, //  4: session number
+    0x14, //  5: ADR/Control
+    0x00, //  6: TNO
+    0xA0, //  7: POINT
+    0x00, //  8: Min
+    0x00, //  9: Sec
+    0x00, // 10: Frame
+    0x00, // 11: Zero
+    0x01, // 12: First Track number.
+    0x00, // 13: Disc type 00 = Mode 1
+    0x00, // 14: PFRAME
+    // A1
+    0x01, // 15: session number
+    0x14, // 16: ADR/Control
+    0x00, // 17: TNO
+    0xA1, // 18: POINT
+    0x00, // 19: Min
+    0x00, // 20: Sec
+    0x00, // 21: Frame
+    0x00, // 22: Zero
+    0x01, // 23: Last Track number
+    0x00, // 24: PSEC
+    0x00, // 25: PFRAME
+    // A2
+    0x01, // 26: session number
+    0x14, // 27: ADR/Control
+    0x00, // 28: TNO
+    0xA2, // 29: POINT
+    0x00, // 30: Min
+    0x00, // 31: Sec
+    0x00, // 32: Frame
+    0x00, // 33: Zero
+    0x79, // 34: LEADOUT position BCD
+    0x59, // 35: leadout PSEC BCD
+    0x74, // 36: leadout PFRAME BCD
+    // TRACK 1 Descriptor
+    0x01, // 37: session number
+    0x14, // 38: ADR/Control
+    0x00, // 39: TNO
+    0x01, // 40: Point
+    0x00, // 41: Min
+    0x00, // 42: Sec
+    0x00, // 43: Frame
+    0x00, // 44: Zero
+    0x00, // 45: PMIN
+    0x00, // 46: PSEC
+    0x00, // 47: PFRAME
+    // b0
+    0x01, // 48: session number
+    0x54, // 49: ADR/Control
+    0x00, // 50: TNO
+    0xB1, // 51: POINT
+    0x79, // 52: Min BCD
+    0x59, // 53: Sec BCD
+    0x74, // 54: Frame BCD
+    0x00, // 55: Zero
+    0x79, // 56: PMIN BCD
+    0x59, // 57: PSEC BCD
+    0x74, // 58: PFRAME BCD
+    // c0
+    0x01, // 59: session number
+    0x54, // 60: ADR/Control
+    0x00, // 61: TNO
+    0xC0, // 62: POINT
+    0x00, // 63: Min
+    0x00, // 64: Sec
+    0x00, // 65: Frame
+    0x00, // 66: Zero
+    0x00, // 67: PMIN
+    0x00, // 68: PSEC
+    0x00  // 69: PFRAME
+};
+
+static uint8_t SimpleHeader[] =
+{
+    0x01, // 2048byte user data, L-EC in 288 byte aux field.
+    0x00, // reserved
+    0x00, // reserved
+    0x00, // reserved
+    0x00,0x00,0x00,0x00 // Track start sector (LBA or MSF)
+};
+
+static const uint8_t DiscInformation[] =
+{
+    0x00,   //  0: disc info length, MSB
+    0x20,   //  1: disc info length, LSB
+    0x0E,   //  2: disc status (finalized, single session non-rewritable)
+    0x01,   //  3: first track number
+    0x01,   //  4: number of sessions (LSB)
+    0x01,   //  5: first track in last session (LSB)
+    0x01,   //  6: last track in last session (LSB)
+    0x00,   //  7: format status (0x00 = non-rewritable, no barcode, no disc id)
+    0x00,   //  8: disc type (0x00 = CD-ROM)
+    0x00,   //  9: number of sessions (MSB)
+    0x00,   // 10: first track in last session (MSB)
+    0x00,   // 11: last track in last session (MSB)
+    0x00,   // 12: disc ID (MSB)
+    0x00,   // 13: .
+    0x00,   // 14: .
+    0x00,   // 15: disc ID (LSB)
+    0x00,   // 16: last session lead-in start (MSB)
+    0x00,   // 17: .
+    0x00,   // 18: .
+    0x00,   // 19: last session lead-in start (LSB)
+    0x00,   // 20: last possible lead-out start (MSB)
+    0x00,   // 21: .
+    0x00,   // 22: .
+    0x00,   // 23: last possible lead-out start (LSB)
+    0x00,   // 24: disc bar code (MSB)
+    0x00,   // 25: .
+    0x00,   // 26: .
+    0x00,   // 27: .
+    0x00,   // 28: .
+    0x00,   // 29: .
+    0x00,   // 30: .
+    0x00,   // 31: disc bar code (LSB)
+    0x00,   // 32: disc application code
+    0x00,   // 33: number of opc tables
+};
+
+// Convert logical block address to CD-ROM time in formatted TOC format
+static void LBA2MSF(uint32_t LBA, uint8_t* MSF)
+{
+    MSF[0] = 0; // reserved.
+    MSF[3] = LBA % 75; // Frames
+    uint32_t rem = LBA / 75;
+
+    MSF[2] = rem % 60; // Seconds
+    MSF[1] = rem / 60; // Minutes
+}
+
+static void doReadTOCSimple(bool MSF, uint8_t track, uint16_t allocationLength)
+{
+    if (track == 0xAA)
+    {
+        // 0xAA requests only lead-out track information (reports capacity)
+        uint32_t len = sizeof(LeadoutTOC);
+        memcpy(scsiDev.data, LeadoutTOC, len);
+
+        uint32_t capacity = getScsiCapacity(
+            scsiDev.target->cfg->sdSectorStart,
+            scsiDev.target->liveCfg.bytesPerSector,
+            scsiDev.target->cfg->scsiSectors);
+
+        // Replace start of leadout track
+        if (MSF)
+        {
+            LBA2MSF(capacity, scsiDev.data + 8);
+        }
+        else
+        {
+            scsiDev.data[8] = capacity >> 24;
+            scsiDev.data[9] = capacity >> 16;
+            scsiDev.data[10] = capacity >> 8;
+            scsiDev.data[11] = capacity;
+        }
+
+        if (len > allocationLength)
+        {
+            len = allocationLength;
+        }
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+    else if (track <= 1)
+    {
+        // We only support track 1.
+        // track 0 means "return all tracks"
+        uint32_t len = sizeof(SimpleTOC);
+        memcpy(scsiDev.data, SimpleTOC, len);
+
+        uint32_t capacity = getScsiCapacity(
+            scsiDev.target->cfg->sdSectorStart,
+            scsiDev.target->liveCfg.bytesPerSector,
+            scsiDev.target->cfg->scsiSectors);
+
+        // Replace start of leadout track
+        if (MSF)
+        {
+            LBA2MSF(capacity, scsiDev.data + 0x10);
+        }
+        else
+        {
+            scsiDev.data[0x10] = capacity >> 24;
+            scsiDev.data[0x11] = capacity >> 16;
+            scsiDev.data[0x12] = capacity >> 8;
+            scsiDev.data[0x13] = capacity;
+        }
+
+        if (len > allocationLength)
+        {
+            len = allocationLength;
+        }
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+    else
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+    }
+}
+
+static void doReadSessionInfoSimple(uint8_t session, uint16_t allocationLength)
+{
+    uint32_t len = sizeof(SessionTOC);
+    memcpy(scsiDev.data, SessionTOC, len);
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+static inline uint8_t
+fromBCD(uint8_t val)
+{
+    return ((val >> 4) * 10) + (val & 0xF);
+}
+
+static void doReadFullTOCSimple(int convertBCD, uint8_t session, uint16_t allocationLength)
+{
+    // We only support session 1.
+    if (session > 1)
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        uint32_t len = sizeof(FullTOC);
+        memcpy(scsiDev.data, FullTOC, len);
+
+        if (convertBCD)
+        {
+            int descriptor = 4;
+            while (descriptor < len)
+            {
+                int i;
+                for (i = 0; i < 7; ++i)
+                {
+                    scsiDev.data[descriptor + i] =
+                        fromBCD(scsiDev.data[descriptor + 4 + i]);
+                }
+                descriptor += 11;
+            }
+
+        }
+
+        if (len > allocationLength)
+        {
+            len = allocationLength;
+        }
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+}
+
+void doReadHeaderSimple(bool MSF, uint32_t lba, uint16_t allocationLength)
+{
+    uint32_t len = sizeof(SimpleHeader);
+    memcpy(scsiDev.data, SimpleHeader, len);
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+void doReadDiscInformationSimple(uint16_t allocationLength)
+{
+    uint32_t len = sizeof(DiscInformation);
+    memcpy(scsiDev.data, DiscInformation, len);
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+/*********************************/
+/* TOC generation from cue sheet */
+/*********************************/
+
+// Fetch track info based on LBA
+static void getTrackFromLBA(CUEParser &parser, uint32_t lba, CUETrackInfo *result)
+{
+    // Track info in case we have no .cue file
+    result->file_mode = CUEFile_BINARY;
+    result->track_mode = CUETrack_MODE1_2048;
+    result->sector_length = 2048;
+    result->track_number = 1;
+
+    const CUETrackInfo *tmptrack;
+    while ((tmptrack = parser.next_track()) != NULL)
+    {
+        if (tmptrack->track_start <= lba)
+        {
+            *result = *tmptrack;
+        }
+        else
+        {
+            break;
+        }
+    }
+}
+
+// Format track info read from cue sheet into the format used by ReadTOC command.
+// Refer to T10/1545-D MMC-4 Revision 5a, "Response Format 0000b: Formatted TOC"
+static void formatTrackInfo(const CUETrackInfo *track, uint8_t *dest, bool use_MSF_time)
+{
+    uint8_t control_adr = 0x14; // Digital track
+
+    if (track->track_mode == CUETrack_AUDIO)
+    {
+        control_adr = 0x10; // Audio track
+    }
+
+    dest[0] = 0; // Reserved
+    dest[1] = control_adr;
+    dest[2] = track->track_number;
+    dest[3] = 0; // Reserved
+
+    if (use_MSF_time)
+    {
+        // Time in minute-second-frame format
+        LBA2MSF(track->data_start, &dest[4]);
+    }
+    else
+    {
+        // Time as logical block address
+        dest[4] = (track->data_start >> 24) & 0xFF;
+        dest[5] = (track->data_start >> 16) & 0xFF;
+        dest[6] = (track->data_start >>  8) & 0xFF;
+        dest[7] = (track->data_start >>  0) & 0xFF;
+    }
+}
+
+// Load data from CUE sheet for the given device,
+// using the second half of scsiDev.data buffer for temporary storage.
+// Returns false if no cue sheet or it could not be opened.
+static bool loadCueSheet(image_config_t &img, CUEParser &parser)
+{
+    if (!img.cuesheetfile.isOpen())
+    {
+        return false;
+    }
+
+    // Use second half of scsiDev.data as the buffer for cue sheet text
+    size_t halfbufsize = sizeof(scsiDev.data) / 2;
+    char *cuebuf = (char*)&scsiDev.data[halfbufsize];
+    img.cuesheetfile.seek(0);
+    int len = img.cuesheetfile.read(cuebuf, halfbufsize);
+
+    if (len <= 0)
+    {
+        return false;
+    }
+
+    cuebuf[len] = '\0';
+    parser = CUEParser(cuebuf);
+    return true;
+}
+
+static void doReadTOC(bool MSF, uint8_t track, 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 doReadTOCSimple(MSF, track, allocationLength);
+    }
+
+    // Format track info
+    uint8_t *trackdata = &scsiDev.data[4];
+    int trackcount = 0;
+    int firsttrack = -1;
+    int lasttrack = -1;
+    const CUETrackInfo *trackinfo;
+    while ((trackinfo = parser.next_track()) != NULL)
+    {
+        if (firsttrack < 0) firsttrack = trackinfo->track_number;
+        lasttrack = trackinfo->track_number;
+
+        if (track == 0 || track == trackinfo->track_number)
+        {
+            formatTrackInfo(trackinfo, &trackdata[8 * trackcount], MSF);
+            trackcount += 1;
+        }
+    }
+
+    // Format lead-out track info
+    if (track == 0 || track == 0xAA)
+    {
+        CUETrackInfo leadout = {};
+        leadout.track_number = 0xAA;
+        leadout.track_mode = CUETrack_MODE1_2048;
+        leadout.data_start = img.scsiSectors;
+        formatTrackInfo(&leadout, &trackdata[8 * trackcount], MSF);
+        trackcount += 1;
+    }
+
+    // Format response header
+    uint16_t toc_length = 2 + trackcount * 8;
+    scsiDev.data[0] = toc_length >> 8;
+    scsiDev.data[1] = toc_length & 0xFF;
+    scsiDev.data[2] = firsttrack;
+    scsiDev.data[3] = lasttrack;
+
+    if (trackcount == 0)
+    {
+        // Unknown track requested
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        uint32_t len = 2 + toc_length;
+
+        if (len > allocationLength)
+        {
+            len = allocationLength;
+        }
+
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+}
+
+static void doReadSessionInfo(uint8_t session, 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 doReadSessionInfoSimple(session, allocationLength);
+    }
+
+    uint32_t len = sizeof(SessionTOC);
+    memcpy(scsiDev.data, SessionTOC, len);
+
+    // Replace first track info in the session table
+    // based on data from CUE sheet.
+    const CUETrackInfo *trackinfo = parser.next_track();
+    if (trackinfo)
+    {
+        formatTrackInfo(trackinfo, &scsiDev.data[4], false);
+    }
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+// Convert logical block address to CD-ROM time in the raw TOC format
+static void LBA2MSFRaw(uint32_t LBA, uint8_t* MSF)
+{
+    MSF[2] = LBA % 75; // Frames
+    uint32_t rem = LBA / 75;
+
+    MSF[1] = rem % 60; // Seconds
+    MSF[0] = rem / 60; // Minutes
+}
+
+// Convert logical block address to CD-ROM time in binary coded decimal format
+static void LBA2MSFBCD(uint32_t LBA, uint8_t* MSF)
+{
+    uint8_t fra = LBA % 75;
+    uint32_t rem = LBA / 75;
+    uint8_t sec = rem % 60;
+    uint8_t min = rem / 60;
+
+    MSF[0] = ((min / 10) << 4) | (min % 10);
+    MSF[1] = ((sec / 10) << 4) | (sec % 10);
+    MSF[2] = ((fra / 10) << 4) | (fra % 10);
+}
+
+// Format track info read from cue sheet into the format used by ReadFullTOC command.
+// Refer to T10/1545-D MMC-4 Revision 5a, "Response Format 0010b: Raw TOC"
+static void formatRawTrackInfo(const CUETrackInfo *track, uint8_t *dest)
+{
+    uint8_t control_adr = 0x14; // Digital track
+
+    if (track->track_mode == CUETrack_AUDIO)
+    {
+        control_adr = 0x10; // Audio track
+    }
+
+    dest[0] = 0x01; // Session always 1
+    dest[1] = control_adr;
+    dest[2] = 0x00; // "TNO", always 0?
+    dest[3] = track->track_number; // "POINT", contains track number
+
+    if (track->pregap_start > 0)
+    {
+        LBA2MSFRaw(track->pregap_start, &dest[4]);
+    }
+    else
+    {
+        LBA2MSFRaw(track->data_start, &dest[4]);
+    }
+
+    dest[7] = 0; // HOUR
+
+    LBA2MSFBCD(track->data_start, &dest[8]);
+}
+
+static void doReadFullTOC(int convertBCD, uint8_t session, 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 doReadFullTOCSimple(convertBCD, session, allocationLength);
+    }
+
+    // We only support session 1.
+    if (session > 1)
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    // Take the beginning of the hardcoded TOC as base
+    uint32_t len = 4 + 11 * 3; // Header, A0, A1, A2
+    memcpy(scsiDev.data, FullTOC, len);
+
+    // Add track descriptors
+    int trackcount = 0;
+    int firsttrack = -1;
+    int lasttrack = -1;
+    const CUETrackInfo *trackinfo;
+    while ((trackinfo = parser.next_track()) != NULL)
+    {
+        if (firsttrack < 0) firsttrack = trackinfo->track_number;
+        lasttrack = trackinfo->track_number;
+
+        formatRawTrackInfo(trackinfo, &scsiDev.data[len]);
+        trackcount += 1;
+        len += 11;
+    }
+
+    // First and last track numbers
+    scsiDev.data[12] = firsttrack;
+    scsiDev.data[23] = lasttrack;
+
+    // Leadout track position
+    LBA2MSFBCD(img.scsiSectors, &scsiDev.data[34]);
+
+    // Append recordable disc records b0 and c0 indicating non-recordable disc
+    memcpy(scsiDev.data + len, &FullTOC[48], 22);
+    len += 22;
+
+    // Correct the record length in header
+    uint16_t toclen = len - 2;
+    scsiDev.data[0] = toclen >> 8;
+    scsiDev.data[1] = toclen & 0xFF;
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+// SCSI-3 MMC Read Header command, seems to be deprecated in later standards.
+// Refer to ANSI X3.304-1997
+void doReadHeader(bool MSF, 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 doReadHeaderSimple(MSF, lba, allocationLength);
+    }
+
+    // Take the hardcoded header as base
+    uint32_t len = sizeof(SimpleHeader);
+    memcpy(scsiDev.data, SimpleHeader, len);
+
+    // Search the track with the requested LBA
+    CUETrackInfo trackinfo = {};
+    getTrackFromLBA(parser, lba, &trackinfo);
+
+    // Track mode (audio / data)
+    if (trackinfo.track_mode == CUETrack_AUDIO)
+    {
+        scsiDev.data[0] = 0;
+    }
+
+    // Track start
+    if (MSF)
+    {
+        LBA2MSF(trackinfo.data_start, &scsiDev.data[4]);
+    }
+    else
+    {
+        scsiDev.data[4] = (trackinfo.data_start >> 24) & 0xFF;
+        scsiDev.data[5] = (trackinfo.data_start >> 16) & 0xFF;
+        scsiDev.data[6] = (trackinfo.data_start >>  8) & 0xFF;
+        scsiDev.data[7] = (trackinfo.data_start >>  0) & 0xFF;
+    }
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+void doReadDiscInformation(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 doReadDiscInformationSimple(allocationLength);
+    }
+
+    // Take the hardcoded header as base
+    uint32_t len = sizeof(DiscInformation);
+    memcpy(scsiDev.data, DiscInformation, len);
+
+    // Find first and last track number
+    int firsttrack = -1;
+    int lasttrack = -1;
+    const CUETrackInfo *trackinfo;
+    while ((trackinfo = parser.next_track()) != NULL)
+    {
+        if (firsttrack < 0) firsttrack = trackinfo->track_number;
+        lasttrack = trackinfo->track_number;
+    }
+
+    scsiDev.data[3] = firsttrack;
+    scsiDev.data[5] = firsttrack;
+    scsiDev.data[6] = lasttrack;
+
+    if (len > allocationLength)
+    {
+        len = allocationLength;
+    }
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+/****************************************/
+/* CUE sheet check at image load time   */
+/****************************************/
+
+bool cdromValidateCueSheet(image_config_t &img)
+{
+    CUEParser parser;
+    if (!loadCueSheet(img, parser))
+    {
+        return false;
+    }
+
+    const CUETrackInfo *trackinfo;
+    int trackcount = 0;
+    while ((trackinfo = parser.next_track()) != NULL)
+    {
+        trackcount++;
+
+        if (trackinfo->track_mode != CUETrack_AUDIO &&
+            trackinfo->track_mode != CUETrack_MODE1_2048 &&
+            trackinfo->track_mode != CUETrack_MODE1_2352)
+        {
+            logmsg("---- Warning: track ", trackinfo->track_number, " has unsupported mode ", (int)trackinfo->track_mode);
+        }
+
+        if (trackinfo->file_mode != CUEFile_BINARY)
+        {
+            logmsg("---- Unsupported CUE data file mode ", (int)trackinfo->file_mode);
+        }
+    }
+
+    if (trackcount == 0)
+    {
+        logmsg("---- Opened cue sheet but no valid tracks found");
+        return false;
+    }
+
+    logmsg("---- Cue sheet loaded with ", (int)trackcount, " tracks");
+    return true;
+}
+
+/**************************************/
+/* Ejection and image switching logic */
+/**************************************/
+
+// Reinsert any ejected CDROMs on reboot
+void cdromReinsertFirstImage(image_config_t &img)
+{
+    if (img.image_index > 0)
+    {
+        // Multiple images for this drive, force restart from first one
+        dbgmsg("---- Restarting from first CD-ROM image");
+        img.image_index = 9;
+        cdromSwitchNextImage(img);
+    }
+    else if (img.ejected)
+    {
+        // Reinsert the single image
+        dbgmsg("---- Closing CD-ROM tray");
+        img.ejected = false;
+        img.cdrom_events = 2; // New media
+    }
+}
+
+// Check if we have multiple CD-ROM images to cycle when drive is ejected.
+bool cdromSwitchNextImage(image_config_t &img)
+{
+    // Check if we have a next image to load, so that drive is closed next time the host asks.
+    img.image_index++;
+    char filename[MAX_FILE_PATH];
+    int target_idx = img.scsiId & 7;
+    if (!scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename)))
+    {
+        img.image_index = 0;
+        scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename));
+    }
+
+    if (filename[0] != '\0')
+    {
+        logmsg("Switching to next CD-ROM image for ", target_idx, ": ", filename);
+        img.file.close();
+        bool status = scsiDiskOpenHDDImage(target_idx, filename, target_idx, 0, 2048);
+
+        if (status)
+        {
+            img.ejected = false;
+            img.cdrom_events = 2; // New media
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static void doGetEventStatusNotification(bool immed)
+{
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+
+    if (!immed)
+    {
+        // Asynchronous notification not supported
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+    }
+    else if (img.cdrom_events)
+    {
+        scsiDev.data[0] = 0;
+        scsiDev.data[1] = 6; // EventDataLength
+        scsiDev.data[2] = 0x04; // Media status events
+        scsiDev.data[3] = 0x04; // Supported events
+        scsiDev.data[4] = img.cdrom_events;
+        scsiDev.data[5] = 0x01; // Power status
+        scsiDev.data[6] = 0; // Start slot
+        scsiDev.data[7] = 0; // End slot
+        scsiDev.dataLen = 8;
+        scsiDev.phase = DATA_IN;
+        img.cdrom_events = 0;
+
+        if (img.ejected)
+        {
+            // We are now reporting to host that the drive is open.
+            // Simulate a "close" for next time the host polls.
+            cdromSwitchNextImage(img);
+        }
+    }
+    else
+    {
+        scsiDev.data[0] = 0;
+        scsiDev.data[1] = 2; // EventDataLength
+        scsiDev.data[2] = 0x00; // Media status events
+        scsiDev.data[3] = 0x04; // Supported events
+        scsiDev.dataLen = 4;
+        scsiDev.phase = DATA_IN;
+    }
+}
+
+/**************************************/
+/* CD-ROM audio playback              */
+/**************************************/
+
+// TODO: where to store these? Should be updated as playback progresses.
+static CDROMAudioPlaybackStatus g_cdrom_audio_status;
+static uint32_t g_cdrom_audio_lba;
+
+void cdromGetAudioPlaybackStatus(CDROMAudioPlaybackStatus *status, uint32_t *current_lba)
+{
+    if (status) *status = g_cdrom_audio_status;
+    if (current_lba) *current_lba = g_cdrom_audio_lba;
+}
+
+static void doPlayAudio(uint32_t lba, uint32_t length)
+{
+    logmsg("------ CD-ROM Play Audio starting at ", lba, " for ", length, " sectors");
+
+    g_cdrom_audio_lba = lba;
+    g_cdrom_audio_status = CDROMAudio_Stopped; // Playback not actually implemented
+
+    scsiDev.status = 0;
+    scsiDev.phase = STATUS;
+}
+
+static void doPauseResumeAudio(bool resume)
+{
+    logmsg("------ CD-ROM ", resume ? "resume" : "pause", " audio playback");
+
+    scsiDev.status = 0;
+    scsiDev.phase = STATUS;
+}
+
+static void doMechanismStatus(uint16_t allocation_length)
+{
+    uint8_t *buf = scsiDev.data;
+
+    CDROMAudioPlaybackStatus status;
+    uint32_t lba;
+    cdromGetAudioPlaybackStatus(&status, &lba);
+
+    *buf++ = 0x00; // No fault state
+    *buf++ = (status == CDROMAudio_Playing) ? 0x20 : 0x00; // Currently playing?
+    *buf++ = (lba >> 16) & 0xFF;
+    *buf++ = (lba >> 8) & 0xFF;
+    *buf++ = (lba >> 0) & 0xFF;
+    *buf++ = 0; // No CD changer
+    *buf++ = 0;
+    *buf++ = 0;
+
+    int len = 8;
+    if (len > allocation_length) len = allocation_length;
+
+    scsiDev.dataLen = len;
+    scsiDev.phase = DATA_IN;
+}
+
+/*******************************************/
+/* CD-ROM data reading in low level format */
+/*******************************************/
+
+static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
+                     uint8_t main_channel, uint8_t sub_channel)
+{
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    CUEParser parser;
+    if (!loadCueSheet(img, parser)
+        && (sector_type == 0 || sector_type == 2)
+        && main_channel == 0x10 && sub_channel == 0)
+    {
+        // Simple case, return sector data directly
+        scsiDiskStartRead(lba, length);
+        return;
+    }
+
+    // Search the track with the requested LBA
+    // Supplies dummy data if no cue sheet is active.
+    CUETrackInfo trackinfo = {};
+    getTrackFromLBA(parser, lba, &trackinfo);
+
+    // Figure out the data offset in the file
+    uint64_t offset = trackinfo.file_offset + trackinfo.sector_length * (lba - trackinfo.data_start);
+    dbgmsg("------ Read CD: ", (int)length, " sectors starting at ", (int)lba,
+           ", track number ", trackinfo.track_number, ", sector size ", (int)trackinfo.sector_length,
+           ", main channel ", main_channel, ", sub channel ", sub_channel,
+           ", data offset in file ", (int)offset);
+
+    // Verify sector type
+    if (sector_type != 0)
+    {
+        bool sector_type_ok = false;
+        if (sector_type == 1 && trackinfo.track_mode == CUETrack_AUDIO)
+        {
+            sector_type_ok = true;
+        }
+        else if (sector_type == 2 && trackinfo.track_mode == CUETrack_MODE1_2048)
+        {
+            sector_type_ok = true;
+        }
+
+        if (!sector_type_ok)
+        {
+            dbgmsg("---- Failed sector type check, host requested ", (int)sector_type, " CUE file has ", (int)trackinfo.track_mode);
+            scsiDev.status = CHECK_CONDITION;
+            scsiDev.target->sense.code = ILLEGAL_REQUEST;
+            scsiDev.target->sense.asc = 0x6400; // ILLEGAL MODE FOR THIS TRACK
+            scsiDev.phase = STATUS;
+            return;
+        }
+    }
+
+    // Select fields to transfer
+    // Refer to table 351 in T10/1545-D MMC-4 Revision 5a
+    // Only the mandatory cases are supported.
+    int sector_length = 0;
+    int skip_begin = 0;
+    bool add_fake_headers = false;
+
+    if (main_channel == 0)
+    {
+        // No actual data requested, just sector type check or subchannel
+        sector_length = 0;
+    }
+    else if (trackinfo.track_mode == CUETrack_AUDIO)
+    {
+        // Transfer whole 2352 byte audio sectors from file to host
+        sector_length = 2352;
+    }
+    else if (trackinfo.track_mode == CUETrack_MODE1_2048 && main_channel == 0x10)
+    {
+        // Transfer whole 2048 byte data sectors from file to host
+        sector_length = 2048;
+    }
+    else if (trackinfo.track_mode == CUETrack_MODE1_2048 && (main_channel & 0xB8) == 0xB8)
+    {
+        // Transfer 2048 bytes of data from file and fake the headers
+        sector_length = 2048;
+        add_fake_headers = true;
+        dbgmsg("------ Host requested ECC data but image file lacks it, replacing with zeros");
+    }
+    else if (trackinfo.track_mode == CUETrack_MODE1_2352 && main_channel == 0x10)
+    {
+        // Transfer the 2048 byte payload of data sector to host.
+        sector_length = 2048;
+        skip_begin = 16;
+    }
+    else if (trackinfo.track_mode == CUETrack_MODE1_2352 && (main_channel & 0xB8) == 0xB8)
+    {
+        // Transfer whole 2352 byte data sector with ECC to host
+        sector_length = 2352;
+    }
+    else
+    {
+        dbgmsg("---- Unsupported channel request for track type ", (int)trackinfo.track_mode);
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = 0x6400; // ILLEGAL MODE FOR THIS TRACK
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    bool field_q_subchannel = false;
+    if (sub_channel == 2)
+    {
+        // Include position information in Q subchannel
+        field_q_subchannel = true;
+    }
+    else if (sub_channel != 0)
+    {
+        dbgmsg("---- Unsupported subchannel request");
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    scsiDev.phase = DATA_IN;
+    scsiDev.dataLen = 0;
+    scsiDev.dataPtr = 0;
+    scsiEnterPhase(DATA_IN);
+
+    // Use two buffers alternately for formatting sector data
+    uint32_t result_length = sector_length + (field_q_subchannel ? 16 : 0) + (add_fake_headers ? 304 : 0);
+    uint8_t *buf0 = scsiDev.data;
+    uint8_t *buf1 = scsiDev.data + result_length;
+
+    // Format the sectors for transfer
+    for (uint32_t idx = 0; idx < length; idx++)
+    {
+        platform_poll();
+
+        img.file.seek(offset + idx * trackinfo.sector_length + skip_begin);
+
+        // Verify that previous write using this buffer has finished
+        uint8_t *buf = ((idx & 1) ? buf1 : buf0);
+        uint8_t *bufstart = buf;
+        uint32_t start = millis();
+        while (!scsiIsWriteFinished(buf + result_length - 1) && !scsiDev.resetFlag)
+        {
+            if ((uint32_t)(millis() - start) > 5000)
+            {
+                logmsg("doReadCD() timeout waiting for previous to finish");
+                scsiDev.resetFlag = 1;
+            }
+            platform_poll();
+        }
+        if (scsiDev.resetFlag) break;
+
+        if (add_fake_headers)
+        {
+            // 12-byte data sector sync pattern
+            *buf++ = 0x00;
+            for (int i = 0; i < 10; i++)
+            {
+                *buf++ = 0xFF;
+            }
+            *buf++ = 0x00;
+
+            // 4-byte data sector header
+            LBA2MSFBCD(lba + idx, buf);
+            buf += 3;
+            *buf++ = 0x01; // Mode 1
+        }
+
+        if (sector_length > 0)
+        {
+            // User data
+            img.file.read(buf, sector_length);
+            buf += sector_length;
+        }
+
+        if (add_fake_headers)
+        {
+            // 288 bytes of ECC
+            memset(buf, 0, 288);
+            buf += 288;
+        }
+
+        if (field_q_subchannel)
+        {
+            // Formatted Q subchannel data
+            // Refer to table 354 in T10/1545-D MMC-4 Revision 5a
+            *buf++ = (trackinfo.track_mode == CUETrack_AUDIO ? 0x10 : 0x14); // Control & ADR
+            *buf++ = trackinfo.track_number;
+            *buf++ = (lba + idx >= trackinfo.data_start) ? 1 : 0; // Index number (0 = pregap)
+            LBA2MSFRaw(lba + idx, buf); buf += 3;
+            *buf++ = 0;
+            LBA2MSFRaw(lba + idx, buf); buf += 3;
+            *buf++ = 0; *buf++ = 0; // CRC (optional)
+            *buf++ = 0; *buf++ = 0; *buf++ = 0; // (pad)
+            *buf++ = 0; // No P subchannel
+        }
+
+        assert(buf == bufstart + result_length);
+        scsiStartWrite(bufstart, result_length);
+    }
+
+    scsiFinishWrite();
+
+    scsiDev.status = 0;
+    scsiDev.phase = STATUS;
+}
+
+static void doReadSubchannel(bool time, bool subq, uint8_t parameter, uint8_t track_number, uint16_t allocation_length)
+{
+    uint8_t *buf = scsiDev.data;
+
+    if (parameter == 0x01)
+    {
+        CDROMAudioPlaybackStatus audiostatus;
+        uint32_t lba;
+        cdromGetAudioPlaybackStatus(&audiostatus, &lba);
+        dbgmsg("------ Get audio playback position: status ", (int)audiostatus, " lba ", (int)lba);
+
+        // Fetch current track info
+        image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+        CUEParser parser;
+        CUETrackInfo trackinfo = {};
+        loadCueSheet(img, parser);
+        getTrackFromLBA(parser, lba, &trackinfo);
+
+        // Request sub channel data at current playback position
+        *buf++ = 0; // Reserved
+
+        switch (audiostatus)
+        {
+            case CDROMAudio_Playing: *buf++ = 0x11; break;
+            case CDROMAudio_Paused:  *buf++ = 0x12; break;
+            case CDROMAudio_Stopped: *buf++ = 0x13; break;
+            default: *buf++ = 0; break;
+        }
+
+        int len = 12;
+        *buf++ = 0;  // Subchannel data length (MSB)
+        *buf++ = len; // Subchannel data length (LSB)
+        *buf++ = 0x01; // Subchannel data format
+        *buf++ = (trackinfo.track_mode == CUETrack_AUDIO ? 0x10 : 0x14);
+        *buf++ = trackinfo.track_number;
+        *buf++ = (lba >= trackinfo.data_start) ? 1 : 0; // Index number (0 = pregap)
+        *buf++ = (lba >> 24) & 0xFF; // Absolute block address
+        *buf++ = (lba >> 16) & 0xFF;
+        *buf++ = (lba >>  8) & 0xFF;
+        *buf++ = (lba >>  0) & 0xFF;
+
+        uint32_t relpos = (uint32_t)((int32_t)lba - (int32_t)trackinfo.data_start);
+        *buf++ = (relpos >> 24) & 0xFF; // Track relative position (may be negative)
+        *buf++ = (relpos >> 16) & 0xFF;
+        *buf++ = (relpos >>  8) & 0xFF;
+        *buf++ = (relpos >>  0) & 0xFF;
+
+        if (len > allocation_length) len = allocation_length;
+        scsiDev.dataLen = len;
+        scsiDev.phase = DATA_IN;
+    }
+    else
+    {
+        dbgmsg("---- Unsupported subchannel request");
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+}
+
+/**************************************/
+/* CD-ROM command dispatching         */
+/**************************************/
+
+// Handle direct-access scsi device commands
+extern "C" int scsiCDRomCommand()
+{
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    int commandHandled = 1;
+
+    uint8_t command = scsiDev.cdb[0];
+    if (command == 0x1B && (scsiDev.cdb[4] & 2))
+    {
+        // CD-ROM load & eject
+        int start = scsiDev.cdb[4] & 1;
+        if (start)
+        {
+            dbgmsg("------ CDROM close tray");
+            img.ejected = false;
+            img.cdrom_events = 2; // New media
+        }
+        else
+        {
+            dbgmsg("------ CDROM open tray");
+            img.ejected = true;
+            img.cdrom_events = 3; // Media removal
+        }
+    }
+    else if (command == 0x43)
+    {
+        // CD-ROM Read TOC
+        bool MSF = (scsiDev.cdb[1] & 0x02);
+        uint8_t track = scsiDev.cdb[6];
+        uint16_t allocationLength =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+
+        // Reject MMC commands for now, otherwise the TOC data format
+        // won't be understood.
+        // The "format" field is reserved for SCSI-2
+        uint8_t format = scsiDev.cdb[2] & 0x0F;
+        switch (format)
+        {
+            case 0: doReadTOC(MSF, track, allocationLength); break; // SCSI-2
+            case 1: doReadSessionInfo(MSF, allocationLength); break; // MMC2
+            case 2: doReadFullTOC(0, track, allocationLength); break; // MMC2
+            case 3: doReadFullTOC(1, track, allocationLength); break; // MMC2
+            default:
+            {
+                scsiDev.status = CHECK_CONDITION;
+                scsiDev.target->sense.code = ILLEGAL_REQUEST;
+                scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
+                scsiDev.phase = STATUS;
+            }
+        }
+    }
+    else if (command == 0x44)
+    {
+        // CD-ROM Read Header
+        bool MSF = (scsiDev.cdb[1] & 0x02);
+        uint32_t lba = 0; // IGNORED for now
+        uint16_t allocationLength =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+        doReadHeader(MSF, lba, allocationLength);
+    }
+    else if (command == 0x51)
+    {
+        uint16_t allocationLength =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+        doReadDiscInformation(allocationLength);
+    }
+    else if (command == 0x4A)
+    {
+        // Get event status notifications (media change notifications)
+        bool immed = scsiDev.cdb[1] & 1;
+        doGetEventStatusNotification(immed);
+    }
+    else if (command == 0x45)
+    {
+        // PLAY AUDIO (10)
+        uint32_t lba =
+            (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint32_t blocks =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+
+        doPlayAudio(lba, blocks);
+    }
+    else if (command == 0xA5)
+    {
+        // PLAY AUDIO (12)
+        uint32_t lba =
+            (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint32_t blocks =
+            (((uint32_t) scsiDev.cdb[6]) << 24) +
+            (((uint32_t) scsiDev.cdb[7]) << 16) +
+            (((uint32_t) scsiDev.cdb[8]) << 8) +
+            scsiDev.cdb[9];
+
+        doPlayAudio(lba, blocks);
+    }
+    else if (command == 0x47)
+    {
+        // PLAY AUDIO (MSF)
+        uint32_t start = (scsiDev.cdb[3] * 60 + scsiDev.cdb[4]) * 75 + scsiDev.cdb[5];
+        uint32_t end   = (scsiDev.cdb[6] * 60 + scsiDev.cdb[7]) * 75 + scsiDev.cdb[8];
+
+        doPlayAudio(start, end - start);
+    }
+    else if (command == 0x4B)
+    {
+        // PAUSE/RESUME AUDIO
+        doPauseResumeAudio(scsiDev.cdb[8] & 1);
+    }
+    else if (command == 0xBD)
+    {
+        // Mechanism status
+        uint16_t allocationLength = (((uint32_t) scsiDev.cdb[8]) << 8) + scsiDev.cdb[9];
+        doMechanismStatus(allocationLength);
+    }
+    else if (command == 0xBB)
+    {
+        // Set CD speed (just ignored)
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else if (command == 0xBE)
+    {
+        // ReadCD (in low level format)
+        uint8_t sector_type = (scsiDev.cdb[1] >> 2) & 7;
+        uint32_t lba =
+            (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint32_t blocks =
+            (((uint32_t) scsiDev.cdb[6]) << 16) +
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            (((uint32_t) scsiDev.cdb[8]));
+        uint8_t main_channel = scsiDev.cdb[9];
+        uint8_t sub_channel = scsiDev.cdb[10];
+
+        doReadCD(lba, blocks, sector_type, main_channel, sub_channel);
+    }
+    else if (command == 0xB9)
+    {
+        // ReadCD MSF
+        uint8_t sector_type = (scsiDev.cdb[1] >> 2) & 7;
+        uint32_t start = (scsiDev.cdb[3] * 60 + scsiDev.cdb[4]) * 75 + scsiDev.cdb[5];
+        uint32_t end   = (scsiDev.cdb[6] * 60 + scsiDev.cdb[7]) * 75 + scsiDev.cdb[8];
+        uint8_t main_channel = scsiDev.cdb[9];
+        uint8_t sub_channel = scsiDev.cdb[10];
+
+        doReadCD(start, end - start, sector_type, main_channel, sub_channel);
+    }
+    else if (command == 0x42)
+    {
+        // Read subchannel data
+        bool time = (scsiDev.cdb[1] & 0x02);
+        bool subq = (scsiDev.cdb[2] & 0x40);
+        uint8_t parameter = scsiDev.cdb[3];
+        uint8_t track_number = scsiDev.cdb[6];
+        uint16_t allocationLength = (((uint32_t) scsiDev.cdb[7]) << 8) + scsiDev.cdb[8];
+
+        doReadSubchannel(time, subq, parameter, track_number, allocationLength);
+    }
+    else if (command == 0x28)
+    {
+        // READ(10) for CDs (may need sector translation for cue file handling)
+        uint32_t lba =
+            (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint32_t blocks =
+            (((uint32_t) scsiDev.cdb[7]) << 8) +
+            scsiDev.cdb[8];
+
+        doReadCD(lba, blocks, 0, 0x10, 0);
+    }
+    else if (command == 0xA8)
+    {
+        // READ(12) for CDs (may need sector translation for cue file handling)
+        uint32_t lba =
+            (((uint32_t) scsiDev.cdb[2]) << 24) +
+            (((uint32_t) scsiDev.cdb[3]) << 16) +
+            (((uint32_t) scsiDev.cdb[4]) << 8) +
+            scsiDev.cdb[5];
+        uint32_t blocks =
+            (((uint32_t) scsiDev.cdb[6]) << 24) +
+            (((uint32_t) scsiDev.cdb[7]) << 16) +
+            (((uint32_t) scsiDev.cdb[8]) << 8) +
+            scsiDev.cdb[9];
+
+        doReadCD(lba, blocks, 0, 0x10, 0);
+    }
+    else
+    {
+        commandHandled = 0;
+    }
+
+    return commandHandled;
+}

+ 30 - 0
src/ZuluSCSI_cdrom.h

@@ -0,0 +1,30 @@
+// Advanced CD-ROM drive emulation.
+// Adds a few capabilities on top of the SCSI2SD CD-ROM emulation:
+//
+// - bin/cue support for support of multiple tracks
+// - on the fly image switching
+
+#pragma once
+
+#include "ZuluSCSI_disk.h"
+
+// Called by scsi.c from SCSI2SD
+extern "C" int scsiCDRomCommand(void);
+
+// Reinsert ejected CD-ROM and restart from first image
+void cdromReinsertFirstImage(image_config_t &img);
+
+// Switch to next CD-ROM image if multiple have been configured
+bool cdromSwitchNextImage(image_config_t &img);
+
+// Check if the currently loaded cue sheet for the image can be parsed
+// and print warnings about unsupported track types
+bool cdromValidateCueSheet(image_config_t &img);
+
+// Audio playback status
+enum CDROMAudioPlaybackStatus {
+    CDROMAudio_Stopped = 0,
+    CDROMAudio_Playing = 1,
+    CDROMAudio_Paused = 2
+};
+void cdromGetAudioPlaybackStatus(CDROMAudioPlaybackStatus *status, uint32_t *current_lba);

+ 52 - 592
src/ZuluSCSI_disk.cpp

@@ -30,6 +30,9 @@
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_presets.h"
+#include "ZuluSCSI_cdrom.h"
+#include "ImageBackingStore.h"
+#include "ROMDrive.h"
 #include <minIni.h>
 #include <string.h>
 #include <strings.h>
@@ -76,14 +79,6 @@ extern "C" {
 #endif
 #endif
 
-#ifndef PLATFORM_HAS_ROM_DRIVE
-// Dummy defines for platforms without ROM drive support
-#define PLATFORM_ROMDRIVE_PAGE_SIZE 1024
-uint32_t platform_get_romdrive_maxsize() { return 0; }
-bool platform_read_romdrive(uint8_t *dest, uint32_t start, uint32_t count) { return false; }
-bool platform_write_romdrive(const uint8_t *data, uint32_t start, uint32_t count) { return false; }
-#endif
-
 #ifndef PLATFORM_SCSIPHY_HAS_NONBLOCKING_READ
 // For platforms that do not have non-blocking read from SCSI bus
 void scsiStartRead(uint8_t* data, uint32_t count, int *parityError)
@@ -100,157 +95,22 @@ bool scsiIsReadFinished(const uint8_t *data)
 }
 #endif
 
-// SD card sector size is always 512 bytes
-extern SdFs SD;
-#define SD_SECTOR_SIZE 512
-
 /************************************************/
 /* ROM drive support (in microcontroller flash) */
 /************************************************/
 
-struct romdrive_hdr_t {
-    char magic[8]; // "ROMDRIVE"
-    int scsi_id;
-    uint32_t imagesize;
-    uint32_t blocksize;
-    S2S_CFG_TYPE drivetype;
-    uint32_t reserved[32];
-};
-
-// Check if the romdrive is present
-static bool check_romdrive(romdrive_hdr_t *hdr)
-{
-    if (!platform_read_romdrive((uint8_t*)hdr, 0, sizeof(romdrive_hdr_t)))
-    {
-        return false;
-    }
-
-    if (memcmp(hdr->magic, "ROMDRIVE", 8) != 0)
-    {
-        return false;
-    }
-
-    if (hdr->imagesize <= 0 || hdr->scsi_id < 0 || hdr->scsi_id > 8)
-    {
-        return false;
-    }
-
-    return true;
-}
-
-// Clear the drive metadata header
-bool scsiDiskClearRomDrive()
-{
-#ifndef PLATFORM_HAS_ROM_DRIVE
-    logmsg("---- Platform does not support ROM drive");
-    return false;
-#else
-    romdrive_hdr_t hdr = {0x0};
-
-    if (!platform_write_romdrive((const uint8_t*)&hdr, 0, PLATFORM_ROMDRIVE_PAGE_SIZE))
-    {
-        logmsg("-- Failed to clear ROM drive");
-        return false;
-    }
-    logmsg("-- Cleared ROM drive");
-    SD.remove("CLEAR_ROM");
-    return true;
-#endif
-}
-
-// Load an image file to romdrive
-bool scsiDiskProgramRomDrive(const char *filename, int scsi_id, int blocksize, S2S_CFG_TYPE type)
-{
-#ifndef PLATFORM_HAS_ROM_DRIVE
-    logmsg("---- Platform does not support ROM drive");
-    return false;
-#endif
-
-    FsFile file = SD.open(filename, O_RDONLY);
-    if (!file.isOpen())
-    {
-        logmsg("---- Failed to open: ", filename);
-        return false;
-    }
-
-    uint64_t filesize = file.size();
-    uint32_t maxsize = platform_get_romdrive_maxsize() - PLATFORM_ROMDRIVE_PAGE_SIZE;
-
-    logmsg("---- SCSI ID: ", scsi_id, " blocksize ", blocksize, " type ", (int)type);
-    logmsg("---- ROM drive maximum size is ", (int)maxsize,
-          " bytes, image file is ", (int)filesize, " bytes");
-    
-    if (filesize > maxsize)
-    {
-        logmsg("---- Image size exceeds ROM space, not loading");
-        file.close();
-        return false;
-    }
-
-    romdrive_hdr_t hdr = {};
-    memcpy(hdr.magic, "ROMDRIVE", 8);
-    hdr.scsi_id = scsi_id;
-    hdr.imagesize = filesize;
-    hdr.blocksize = blocksize;
-    hdr.drivetype = type;
-
-    // Program the drive metadata header
-    if (!platform_write_romdrive((const uint8_t*)&hdr, 0, PLATFORM_ROMDRIVE_PAGE_SIZE))
-    {
-        logmsg("---- Failed to program ROM drive header");
-        file.close();
-        return false;
-    }
-    
-    // Program the drive contents
-    uint32_t pages = (filesize + PLATFORM_ROMDRIVE_PAGE_SIZE - 1) / PLATFORM_ROMDRIVE_PAGE_SIZE;
-    for (uint32_t i = 0; i < pages; i++)
-    {
-        if (i % 2)
-            LED_ON();
-        else
-            LED_OFF();
-
-        if (file.read(scsiDev.data, PLATFORM_ROMDRIVE_PAGE_SIZE) <= 0 ||
-            !platform_write_romdrive(scsiDev.data, (i + 1) * PLATFORM_ROMDRIVE_PAGE_SIZE, PLATFORM_ROMDRIVE_PAGE_SIZE))
-        {
-            logmsg("---- Failed to program ROM drive page ", (int)i);
-            file.close();
-            return false;
-        }
-    }
-
-    LED_OFF();
-
-    file.close();
-
-    char newname[MAX_FILE_PATH * 2] = "";
-    strlcat(newname, filename, sizeof(newname));
-    strlcat(newname, "_loaded", sizeof(newname));
-    SD.rename(filename, newname);
-    logmsg("---- ROM drive programming successful, image file renamed to ", newname);
-
-    return true;
-}
-
-bool scsiDiskCheckRomDrive()
-{
-    romdrive_hdr_t hdr = {};
-    return check_romdrive(&hdr);
-}
-
 // Check if rom drive exists and activate it
 bool scsiDiskActivateRomDrive()
 {
 #ifndef PLATFORM_HAS_ROM_DRIVE
     return false;
-#endif
+#else
 
     uint32_t maxsize = platform_get_romdrive_maxsize() - PLATFORM_ROMDRIVE_PAGE_SIZE;
     logmsg("-- Platform supports ROM drive up to ", (int)(maxsize / 1024), " kB");
 
     romdrive_hdr_t hdr = {};
-    if (!check_romdrive(&hdr))
+    if (!romDriveCheckPresent(&hdr))
     {
         logmsg("---- ROM drive image not detected");
         return false;
@@ -275,8 +135,6 @@ bool scsiDiskActivateRomDrive()
         return false;
     }
 
-
-
     logmsg("---- Activating ROM drive, SCSI id ", (int)hdr.scsi_id, " size ", (int)(hdr.imagesize / 1024), " kB");
     bool status = scsiDiskOpenHDDImage(hdr.scsi_id, "ROM:", hdr.scsi_id, 0, hdr.blocksize, hdr.drivetype);
 
@@ -289,329 +147,18 @@ bool scsiDiskActivateRomDrive()
     {
         return true;
     }
+
+#endif
 }
 
 
 /***********************/
-/* Backing image files */
+/* Image configuration */
 /***********************/
 
 extern SdFs SD;
 SdDevice sdDev = {2, 256 * 1024 * 1024 * 2}; /* For SCSI2SD */
 
-// This class wraps SdFat library FsFile to allow access
-// through either FAT filesystem or as a raw sector range.
-//
-// Raw access is activated by using filename like "RAW:0:12345"
-// where the numbers are the first and last sector.
-//
-// If the platform supports a ROM drive, it is activated by using
-// filename "ROM:".
-class ImageBackingStore
-{
-public:
-    ImageBackingStore()
-    {
-        m_israw = false;
-        m_isrom = false;
-        m_isreadonly_attr = false;
-        m_blockdev = nullptr;
-        m_bgnsector = m_endsector = m_cursector = 0;
-    }
-
-    ImageBackingStore(const char *filename, uint32_t scsi_block_size): ImageBackingStore()
-    {
-        if (strncasecmp(filename, "RAW:", 4) == 0)
-        {
-            char *endptr, *endptr2;
-            m_bgnsector = strtoul(filename + 4, &endptr, 0);
-            m_endsector = strtoul(endptr + 1, &endptr2, 0);
-
-            if (*endptr != ':' || *endptr2 != '\0')
-            {
-                logmsg("Invalid format for raw filename: ", filename);
-                return;
-            }
-
-            if ((scsi_block_size % SD_SECTOR_SIZE) != 0)
-            {
-                logmsg("SCSI block size ", (int)scsi_block_size, " is not supported for RAW partitions (must be divisible by 512 bytes)");
-                return;
-            }
-
-            m_israw = true;
-            m_blockdev = SD.card();
-
-            uint32_t sectorCount = SD.card()->sectorCount();
-            if (m_endsector >= sectorCount)
-            {
-                logmsg("Limiting RAW image mapping to SD card sector count: ", (int)sectorCount);
-                m_endsector = sectorCount - 1;
-            }
-        }
-        else if (strncasecmp(filename, "ROM:", 4) == 0)
-        {
-            if (!check_romdrive(&m_romhdr))
-            {
-                m_romhdr.imagesize = 0;
-            }
-            else
-            {
-                m_isrom = true;
-            }
-        }
-        else
-        {
-            m_isreadonly_attr = !!(FS_ATTRIB_READ_ONLY & SD.attrib(filename));
-            if (m_isreadonly_attr)
-            {
-                m_fsfile = SD.open(filename, O_RDONLY);
-                logmsg("---- Image file is read-only, writes disabled");
-            }
-            else
-            {
-                m_fsfile = SD.open(filename, O_RDWR);
-            }
-
-            uint32_t sectorcount = m_fsfile.size() / SD_SECTOR_SIZE;
-            uint32_t begin = 0, end = 0;
-            if (m_fsfile.contiguousRange(&begin, &end) && end >= begin + sectorcount
-                && (scsi_block_size % SD_SECTOR_SIZE) == 0)
-            {
-                // Convert to raw mapping, this avoids some unnecessary
-                // access overhead in SdFat library.
-                m_israw = true;
-                m_blockdev = SD.card();
-                m_bgnsector = begin;
-
-                if (end != begin + sectorcount)
-                {
-                    uint32_t allocsize = end - begin + 1;
-                    // Due to issue #80 in ZuluSCSI version 1.0.8 and 1.0.9 the allocated size was mistakenly reported to SCSI controller.
-                    // If the drive was formatted using those versions, you may have problems accessing it with newer firmware.
-                    // The old behavior can be restored with setting  [SCSI] UseFATAllocSize = 1 in config file.
-
-                    if (ini_getbool("SCSI", "UseFATAllocSize", 0, CONFIGFILE))
-                    {
-                        sectorcount = allocsize;
-                    }
-                }
-
-                m_endsector = begin + sectorcount - 1;
-                m_fsfile.close();
-            }
-        }
-    }
-
-    bool isWritable()
-    {
-        return !m_isrom && !m_isreadonly_attr;
-    }
-
-    bool isRom()
-    {
-        return m_isrom;
-    }
-
-    bool isOpen()
-    {
-        if (m_israw)
-            return (m_blockdev != NULL);
-        else if (m_isrom)
-            return (m_romhdr.imagesize > 0);
-        else
-            return m_fsfile.isOpen();
-    }
-
-    bool close()
-    {
-        if (m_israw)
-        {
-            m_blockdev = nullptr;
-            return true;
-        }
-        else if (m_isrom)
-        {
-            m_romhdr.imagesize = 0;
-            return true;
-        }
-        else
-        {
-            return m_fsfile.close();
-        }
-    }
-
-    uint64_t size()
-    {
-        if (m_israw && m_blockdev)
-        {
-            return (uint64_t)(m_endsector - m_bgnsector + 1) * SD_SECTOR_SIZE;
-        }
-        else if (m_isrom)
-        {
-            return m_romhdr.imagesize;
-        }
-        else
-        {
-            return m_fsfile.size();
-        }
-    }
-
-    bool contiguousRange(uint32_t* bgnSector, uint32_t* endSector)
-    {
-        if (m_israw && m_blockdev)
-        {
-            *bgnSector = m_bgnsector;
-            *endSector = m_endsector;
-            return true;
-        }
-        else if (m_isrom)
-        {
-            *bgnSector = 0;
-            *endSector = 0;
-            return true;
-        }
-        else
-        {
-            return m_fsfile.contiguousRange(bgnSector, endSector);
-        }
-    }
-
-    bool seek(uint64_t pos)
-    {
-        if (m_israw)
-        {
-            uint32_t sectornum = pos / SD_SECTOR_SIZE;
-            assert((uint64_t)sectornum * SD_SECTOR_SIZE == pos);
-            m_cursector = m_bgnsector + sectornum;
-            return (m_cursector <= m_endsector);
-        }
-        else if (m_isrom)
-        {
-            uint32_t sectornum = pos / SD_SECTOR_SIZE;
-            assert((uint64_t)sectornum * SD_SECTOR_SIZE == pos);
-            m_cursector = sectornum;
-            return m_cursector * SD_SECTOR_SIZE < m_romhdr.imagesize;
-        }
-        else
-        {
-            return m_fsfile.seek(pos);
-        }
-    }
-
-    int read(void* buf, size_t count)
-    {
-        if (m_israw && m_blockdev)
-        {
-            uint32_t sectorcount = count / SD_SECTOR_SIZE;
-            assert((uint64_t)sectorcount * SD_SECTOR_SIZE == count);
-            if (m_blockdev->readSectors(m_cursector, (uint8_t*)buf, sectorcount))
-            {
-                m_cursector += sectorcount;
-                return count;
-            }
-            else
-            {
-                return -1;
-            }
-        }
-        else if (m_isrom)
-        {
-            uint32_t sectorcount = count / SD_SECTOR_SIZE;
-            assert((uint64_t)sectorcount * SD_SECTOR_SIZE == count);
-            uint32_t start = m_cursector * SD_SECTOR_SIZE + PLATFORM_ROMDRIVE_PAGE_SIZE;
-            if (platform_read_romdrive((uint8_t*)buf, start, count))
-            {
-                m_cursector += sectorcount;
-                return count;
-            }
-            else
-            {
-                return -1;
-            }
-        }
-        else
-        {
-            return m_fsfile.read(buf, count);
-        }
-    }
-
-    size_t write(const void* buf, size_t count)
-    {
-        if (m_israw && m_blockdev)
-        {
-            uint32_t sectorcount = count / SD_SECTOR_SIZE;
-            assert((uint64_t)sectorcount * SD_SECTOR_SIZE == count);
-            if (m_blockdev->writeSectors(m_cursector, (const uint8_t*)buf, sectorcount))
-            {
-                m_cursector += sectorcount;
-                return count;
-            }
-            else
-            {
-                return 0;
-            }
-        }
-        else if (m_isrom)
-        {
-            logmsg("ERROR: attempted to write to ROM drive");
-            return 0;
-        }
-        else  if (m_isreadonly_attr)
-        {
-            logmsg("ERROR: attempted to write to a read only image");
-            return 0;
-        }
-        else
-        {
-            return m_fsfile.write(buf, count);
-        }
-    }
-
-    void flush()
-    {
-        if (!m_israw && !m_isrom && !m_isreadonly_attr)
-        {
-            m_fsfile.flush();
-        }
-    }
-
-private:
-    bool m_israw;
-    bool m_isrom;
-    bool m_isreadonly_attr;
-    romdrive_hdr_t m_romhdr;
-    FsFile m_fsfile;
-    SdCard *m_blockdev;
-    uint32_t m_bgnsector;
-    uint32_t m_endsector;
-    uint32_t m_cursector;
-};
-
-struct image_config_t: public S2S_TargetCfg
-{
-    ImageBackingStore file;
-
-    // For CD-ROM drive ejection
-    bool ejected;
-    uint8_t cdrom_events;
-    bool reinsert_on_inquiry;
-
-    // Index of image, for when image on-the-fly switching is used for CD drives
-    int image_index;
-
-    // Right-align vendor / product type strings (for Apple)
-    // Standard SCSI uses left alignment
-    // This field uses -1 for default when field is not set in .ini
-    int rightAlignStrings;
-
-    // Maximum amount of bytes to prefetch
-    int prefetchbytes;
-
-    // Warning about geometry settings
-    bool geometrywarningprinted;
-};
-
 static image_config_t g_DiskImages[S2S_MAX_TARGETS];
 
 void scsiDiskResetImages()
@@ -839,6 +386,29 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int
             logmsg("---- Read prefetch disabled");
         }
 
+        if (img.deviceType == S2S_CFG_OPTICAL &&
+            strncasecmp(filename + strlen(filename) - 4, ".bin", 4) == 0)
+        {
+            char cuesheetname[MAX_FILE_PATH + 1] = {0};
+            strncpy(cuesheetname, filename, strlen(filename) - 4);
+            strlcat(cuesheetname, ".cue", sizeof(cuesheetname));
+            img.cuesheetfile = SD.open(cuesheetname, O_RDONLY);
+
+            if (img.cuesheetfile.isOpen())
+            {
+                logmsg("---- Found CD-ROM CUE sheet at ", cuesheetname);
+                if (!cdromValidateCueSheet(img))
+                {
+                    logmsg("---- Failed to parse cue sheet, using as plain binary image");
+                    img.cuesheetfile.close();
+                }
+            }
+            else
+            {
+                logmsg("---- No CUE sheet found at ", cuesheetname, ", using as plain binary image");
+            }
+        }
+
         return true;
     }
 
@@ -916,9 +486,9 @@ static void scsiDiskLoadConfig(int target_idx, const char *section)
 }
 
 // Check if image file name is overridden in config
-static bool get_image_name(int target_idx, char *buf, size_t buflen)
+bool scsiDiskGetImageNameFromConfig(image_config_t &img, char *buf, size_t buflen)
 {
-    image_config_t &img = g_DiskImages[target_idx];
+    int target_idx = img.scsiId & 7;
 
     char section[6] = "SCSI0";
     section[4] = '0' + target_idx;
@@ -946,9 +516,9 @@ void scsiDiskLoadConfig(int target_idx)
 
     // Check if we have image specified by name
     char filename[MAX_FILE_PATH];
-    if (get_image_name(target_idx, filename, sizeof(filename)))
+    image_config_t &img = g_DiskImages[target_idx];
+    if (scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename)))
     {
-        image_config_t &img = g_DiskImages[target_idx];
         int blocksize = (img.deviceType == S2S_CFG_OPTICAL) ? 2048 : 512;
         logmsg("-- Opening ", filename, " for id:", target_idx, ", specified in " CONFIGFILE);
         scsiDiskOpenHDDImage(target_idx, filename, target_idx, 0, blocksize);
@@ -968,6 +538,12 @@ bool scsiDiskCheckAnyImagesConfigured()
     return false;
 }
 
+image_config_t &scsiDiskGetImageConfig(int target_idx)
+{
+    assert(target_idx >= 0 && target_idx < S2S_MAX_TARGETS);
+    return g_DiskImages[target_idx];
+}
+
 /*******************************/
 /* Config handling for SCSI2SD */
 /*******************************/
@@ -1239,57 +815,6 @@ static void doReadCapacity()
 /* TestUnitReady command */
 /*************************/
 
-// Check if we have multiple CD-ROM images to cycle when drive is ejected.
-static bool checkNextCDImage()
-{
-    // Check if we have a next image to load, so that drive is closed next time the host asks.
-    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
-    img.image_index++;
-    char filename[MAX_FILE_PATH];
-    int target_idx = img.scsiId & 7;
-    if (!get_image_name(target_idx, filename, sizeof(filename)))
-    {
-        img.image_index = 0;
-        get_image_name(target_idx, filename, sizeof(filename));
-    }
-
-    if (filename[0] != '\0')
-    {
-        logmsg("Switching to next CD-ROM image for ", target_idx, ": ", filename);
-        image_config_t &img = g_DiskImages[target_idx];
-        img.file.close();
-        bool status = scsiDiskOpenHDDImage(target_idx, filename, target_idx, 0, 2048);
-
-        if (status)
-        {
-            img.ejected = false;
-            img.cdrom_events = 2; // New media
-            return true;
-        }
-    }
-
-    return false;
-}
-
-// Reinsert any ejected CDROMs on reboot
-static void reinsertCDROM(image_config_t &img)
-{
-    if (img.image_index > 0)
-    {
-        // Multiple images for this drive, force restart from first one
-        dbgmsg("---- Restarting from first CD-ROM image");
-        img.image_index = 9;
-        checkNextCDImage();
-    }
-    else if (img.ejected)
-    {
-        // Reinsert the single image
-        dbgmsg("---- Closing CD-ROM tray");
-        img.ejected = false;
-        img.cdrom_events = 2; // New media
-    }
-}
-
 static int doTestUnitReady()
 {
     int ready = 1;
@@ -1312,7 +837,7 @@ static int doTestUnitReady()
 
         // We are now reporting to host that the drive is open.
         // Simulate a "close" for next time the host polls.
-        checkNextCDImage();
+        cdromSwitchNextImage(img);
     }
     else if (unlikely(!(blockDev.state & DISK_PRESENT)))
     {
@@ -1333,50 +858,6 @@ static int doTestUnitReady()
     return ready;
 }
 
-static void doGetEventStatusNotification(bool immed)
-{
-    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
-
-    if (!immed)
-    {
-        // Asynchronous notification not supported
-        scsiDev.status = CHECK_CONDITION;
-        scsiDev.target->sense.code = ILLEGAL_REQUEST;
-        scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB;
-        scsiDev.phase = STATUS;
-    }
-    else if (img.cdrom_events)
-    {
-        scsiDev.data[0] = 0;
-        scsiDev.data[1] = 6; // EventDataLength
-        scsiDev.data[2] = 0x04; // Media status events
-        scsiDev.data[3] = 0x04; // Supported events
-        scsiDev.data[4] = img.cdrom_events;
-        scsiDev.data[5] = 0x01; // Power status
-        scsiDev.data[6] = 0; // Start slot
-        scsiDev.data[7] = 0; // End slot
-        scsiDev.dataLen = 8;
-        scsiDev.phase = DATA_IN;
-        img.cdrom_events = 0;
-
-        if (img.ejected)
-        {
-            // We are now reporting to host that the drive is open.
-            // Simulate a "close" for next time the host polls.
-            checkNextCDImage();
-        }
-    }
-    else
-    {
-        scsiDev.data[0] = 0;
-        scsiDev.data[1] = 2; // EventDataLength
-        scsiDev.data[2] = 0x00; // Media status events
-        scsiDev.data[3] = 0x04; // Supported events
-        scsiDev.dataLen = 4;
-        scsiDev.phase = DATA_IN;
-    }
-}
-
 /****************/
 /* Seek command */
 /****************/
@@ -1437,7 +918,7 @@ static struct {
 /* Write command */
 /*****************/
 
-static void doWrite(uint32_t lba, uint32_t blocks)
+void scsiDiskStartWrite(uint32_t lba, uint32_t blocks)
 {
     if (unlikely(scsiDev.target->cfg->deviceType == S2S_CFG_FLOPPY_14MB)) {
         // Floppies are supposed to be slow. Some systems can't handle a floppy
@@ -1678,7 +1159,7 @@ void diskDataOut()
 /* Read command */
 /*****************/
 
-static void doRead(uint32_t lba, uint32_t blocks)
+void scsiDiskStartRead(uint32_t lba, uint32_t blocks)
 {
     if (unlikely(scsiDev.target->cfg->deviceType == S2S_CFG_FLOPPY_14MB)) {
         // Floppies are supposed to be slow. Some systems can't handle a floppy
@@ -1935,24 +1416,8 @@ int scsiDiskCommand()
         // Enable or disable media access operations.
         //int immed = scsiDev.cdb[1] & 1;
         int start = scsiDev.cdb[4] & 1;
-	    int loadEject = scsiDev.cdb[4] & 2;
-	
-        if (loadEject && img.deviceType == S2S_CFG_OPTICAL)
-        {
-            if (start)
-            {
-                dbgmsg("------ CDROM close tray");
-                img.ejected = false;
-                img.cdrom_events = 2; // New media
-            }
-            else
-            {
-                dbgmsg("------ CDROM open tray");
-                img.ejected = true;
-                img.cdrom_events = 3; // Media removal
-            }
-        }
-        else if (start)
+
+        if (start)
         {
             scsiDev.target->started = 1;
         }
@@ -1966,11 +1431,6 @@ int scsiDiskCommand()
         // TEST UNIT READY
         doTestUnitReady();
     }
-    else if (command == 0x4A)
-    {
-        bool immed = scsiDev.cdb[1] & 1;
-        doGetEventStatusNotification(immed);
-    }
     else if (unlikely(!doTestUnitReady()))
     {
         // Status and sense codes already set by doTestUnitReady
@@ -1984,7 +1444,7 @@ int scsiDiskCommand()
             scsiDev.cdb[3];
         uint32_t blocks = scsiDev.cdb[4];
         if (unlikely(blocks == 0)) blocks = 256;
-        doRead(lba, blocks);
+        scsiDiskStartRead(lba, blocks);
     }
     else if (likely(command == 0x28))
     {
@@ -2000,7 +1460,7 @@ int scsiDiskCommand()
             (((uint32_t) scsiDev.cdb[7]) << 8) +
             scsiDev.cdb[8];
 
-        doRead(lba, blocks);
+        scsiDiskStartRead(lba, blocks);
     }
     else if (likely(command == 0x0A))
     {
@@ -2011,7 +1471,7 @@ int scsiDiskCommand()
             scsiDev.cdb[3];
         uint32_t blocks = scsiDev.cdb[4];
         if (unlikely(blocks == 0)) blocks = 256;
-        doWrite(lba, blocks);
+        scsiDiskStartWrite(lba, blocks);
     }
     else if (likely(command == 0x2A) || // WRITE(10)
         unlikely(command == 0x2E)) // WRITE AND VERIFY
@@ -2029,7 +1489,7 @@ int scsiDiskCommand()
             (((uint32_t) scsiDev.cdb[7]) << 8) +
             scsiDev.cdb[8];
 
-        doWrite(lba, blocks);
+        scsiDiskStartWrite(lba, blocks);
     }
     else if (unlikely(command == 0x04))
     {
@@ -2193,7 +1653,7 @@ void scsiDiskPoll()
             image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
             if (img.deviceType == S2S_CFG_OPTICAL && img.reinsert_on_inquiry)
             {
-                reinsertCDROM(img);
+                cdromReinsertFirstImage(img);
             }
         }
     }
@@ -2221,7 +1681,7 @@ void scsiDiskReset()
         image_config_t &img = g_DiskImages[i];
         if (img.deviceType == S2S_CFG_OPTICAL)
         {
-            reinsertCDROM(img);
+            cdromReinsertFirstImage(img);
         }
     }
 }

+ 43 - 0
src/ZuluSCSI_disk.h

@@ -31,6 +31,7 @@
 #include <stdint.h>
 #include <scsi2sd.h>
 #include <scsiPhy.h>
+#include "ImageBackingStore.h"
 
 extern "C" {
 #include <disk.h>
@@ -38,6 +39,34 @@ extern "C" {
 #include <scsi.h>
 }
 
+// Extended configuration stored alongside the normal SCSI2SD target information
+struct image_config_t: public S2S_TargetCfg
+{
+    ImageBackingStore file;
+
+    // For CD-ROM drive ejection
+    bool ejected;
+    uint8_t cdrom_events;
+    bool reinsert_on_inquiry;
+
+    // Index of image, for when image on-the-fly switching is used for CD drives
+    int image_index;
+
+    // Cue sheet file for CD-ROM images
+    FsFile cuesheetfile;
+
+    // Right-align vendor / product type strings (for Apple)
+    // Standard SCSI uses left alignment
+    // This field uses -1 for default when field is not set in .ini
+    int rightAlignStrings;
+
+    // Maximum amount of bytes to prefetch
+    int prefetchbytes;
+
+    // Warning about geometry settings
+    bool geometrywarningprinted;
+};
+
 void scsiDiskResetImages();
 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);
@@ -53,3 +82,17 @@ bool scsiDiskActivateRomDrive();
 
 // Returns true if there is at least one image active
 bool scsiDiskCheckAnyImagesConfigured();
+
+// Check if image file name is overridden in config,
+// including image index for multi-image CD-ROM emulation
+bool scsiDiskGetImageNameFromConfig(image_config_t &img, char *buf, size_t buflen);
+
+// Get pointer to extended image configuration based on target idx
+image_config_t &scsiDiskGetImageConfig(int target_idx);
+
+// Start data transfer from disk image to SCSI bus
+// Can be called by device type specific command implementations (such as READ CD)
+void scsiDiskStartRead(uint32_t lba, uint32_t blocks);
+
+// Start data transfer from SCSI bus to disk image
+void scsiDiskStartWrite(uint32_t lba, uint32_t blocks);

+ 10 - 0
src/ZuluSCSI_log_trace.cpp

@@ -74,9 +74,19 @@ static const char *getCommandName(uint8_t cmd)
         case 0x43: return "CDROM Read TOC";
         case 0x44: return "CDROM Read Header";
         case 0x4A: return "GetEventStatusNotification";
+        case 0x51: return "CDROM ReadDiscInformation";
+        case 0x45: return "CDROM PlayAudio10";
+        case 0xA5: return "CDROM PlayAudio12";
+        case 0x47: return "CDROM PlayAudioMSF";
+        case 0x48: return "CDROM PauseResume";
+        case 0xBB: return "CDROM SetCDSpeed";
+        case 0xBD: return "CDROM MechanismStatus";
+        case 0xBE: return "ReadCD";
+        case 0xB9: return "ReadCDMSF";
         case 0x55: return "ModeSelect10";
         case 0x5A: return "ModeSense10";
         case 0xAC: return "Erase12";
+        case 0xA8: return "Read12";
         case 0xC0: return "OMTI-5204 DefineFlexibleDiskFormat";
         case 0xC2: return "OMTI-5204 AssignDiskParameters";
         default:   return "Unknown";

+ 1 - 1
zuluscsi.ini

@@ -52,7 +52,7 @@
 
 # If IMG0..IMG9 are specified, they are cycled after each CD eject command.
 #IMG0 = FirstCD.iso
-#IMG1 = SecondCD.iso
+#IMG1 = SecondCD.bin
 
 # Raw sector range from SD card can be passed through
 # Format is RAW:first_sector:last_sector where sector numbers can be decimal or hex.