// This file implements the main SCSI disk emulation and data streaming. // It is derived from disk.c in SCSI2SD V6. // // Licensed under GPL v3. // Copyright (C) 2013 Michael McMaster // Copyright (C) 2014 Doug Brown // Copyright (C) 2022 Rabbit Hole Computing #include "BlueSCSI_disk.h" #include "BlueSCSI_log.h" #include "BlueSCSI_config.h" #include "BlueSCSI_presets.h" #include "BlueSCSI_cdrom.h" #include "ImageBackingStore.h" #include "ROMDrive.h" #include #include #include #include #include extern "C" { #include #include #include } #ifndef PLATFORM_MAX_SCSI_SPEED #define PLATFORM_MAX_SCSI_SPEED S2S_CFG_SPEED_ASYNC_50 #endif // This can be overridden in platform file to set the size of the transfers // used when reading from SCSI bus and writing to SD card. // When SD card access is fast, these are usually better increased. // If SD card access is roughly same speed as SCSI bus, these can be left at 512 #ifndef PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE #define PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE 512 #endif #ifndef PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE #define PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE 1024 #endif // Optimal size for the last write in a write request. // This is often better a bit smaller than PLATFORM_OPTIMAL_SD_WRITE_SIZE // to reduce the dead time between end of SCSI transfer and finishing of SD write. #ifndef PLATFORM_OPTIMAL_LAST_SD_WRITE_SIZE #define PLATFORM_OPTIMAL_LAST_SD_WRITE_SIZE 512 #endif // Optimal size for read block from SCSI bus // For platforms with nonblocking transfer, this can be large. // For Akai MPC60 compatibility this has to be at least 5120 #ifndef PLATFORM_OPTIMAL_SCSI_READ_BLOCK_SIZE #ifdef PLATFORM_SCSIPHY_HAS_NONBLOCKING_READ #define PLATFORM_OPTIMAL_SCSI_READ_BLOCK_SIZE 65536 #else #define PLATFORM_OPTIMAL_SCSI_READ_BLOCK_SIZE 8192 #endif #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) { scsiRead(data, count, parityError); } void scsiFinishRead(uint8_t* data, uint32_t count, int *parityError) { } bool scsiIsReadFinished(const uint8_t *data) { return true; } #endif /************************************************/ /* ROM drive support (in microcontroller flash) */ /************************************************/ // Check if rom drive exists and activate it bool scsiDiskActivateRomDrive() { #ifndef PLATFORM_HAS_ROM_DRIVE return false; #else log(""); log("=== ROM Drive ==="); uint32_t maxsize = platform_get_romdrive_maxsize() - PLATFORM_ROMDRIVE_PAGE_SIZE; log("Platform supports ROM drive up to ", (int)(maxsize / 1024), " kB"); romdrive_hdr_t hdr = {}; if (!romDriveCheckPresent(&hdr)) { log("---- ROM drive image not detected"); return false; } if (ini_getbool("SCSI", "DisableROMDrive", 0, CONFIGFILE)) { log("---- ROM drive disabled in ini file, not enabling"); return false; } else { debuglog("---- ROM drive enabled"); } long rom_scsi_id = ini_getl("SCSI", "ROMDriveSCSIID", -1, CONFIGFILE); if (rom_scsi_id >= 0 && rom_scsi_id <= 7) { hdr.scsi_id = rom_scsi_id; log("---- ROM drive SCSI id overriden in ini file, changed to ", (int)hdr.scsi_id); } if (s2s_getConfigById(hdr.scsi_id)) { log("---- ROM drive SCSI id ", (int)hdr.scsi_id, " is already in use, not enabling"); return false; } log("---- 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); if (!status) { log("---- ROM drive activation failed"); return false; } else { log("---- Activated ROM drive, SCSI id ", (int)hdr.scsi_id, " size ", (int)(hdr.imagesize / 1024), " kB"); return true; } #endif } /***********************/ /* Image configuration */ /***********************/ extern SdFs SD; SdDevice sdDev = {2, 256 * 1024 * 1024 * 2}; /* For SCSI2SD */ static image_config_t g_DiskImages[S2S_MAX_TARGETS]; void scsiDiskResetImages() { for(int i = 0; i < S2S_MAX_TARGETS; i++) g_DiskImages[i] = image_config_t(); } void scsiDiskCloseSDCardImages() { for (int i = 0; i < S2S_MAX_TARGETS; i++) { if (!g_DiskImages[i].file.isRom()) { g_DiskImages[i].file.close(); } } } // Verify format conformance to SCSI spec: // - Empty bytes filled with 0x20 (space) // - Only values 0x20 to 0x7E // - Left alignment for vendor/product/revision, right alignment for serial. static void formatDriveInfoField(char *field, int fieldsize, bool align_right) { if (align_right) { // Right align and trim spaces on either side int dst = fieldsize - 1; for (int src = fieldsize - 1; src >= 0; src--) { char c = field[src]; if (c < 0x20 || c > 0x7E) c = 0x20; if (c != 0x20 || dst != fieldsize - 1) { field[dst--] = c; } } while (dst >= 0) { field[dst--] = 0x20; } } else { // Left align, preserve spaces in case config tries to manually right-align int dst = 0; for (int src = 0; src < fieldsize; src++) { char c = field[src]; if (c < 0x20 || c > 0x7E) c = 0x20; field[dst++] = c; } while (dst < fieldsize) { field[dst++] = 0x20; } } } // Set default drive vendor / product info after the image file // is loaded and the device type is known. static void setDefaultDriveInfo(int target_idx) { image_config_t &img = g_DiskImages[target_idx]; static const char *driveinfo_fixed[4] = DRIVEINFO_FIXED; static const char *driveinfo_removable[4] = DRIVEINFO_REMOVABLE; static const char *driveinfo_optical[4] = DRIVEINFO_OPTICAL; static const char *driveinfo_floppy[4] = DRIVEINFO_FLOPPY; static const char *driveinfo_magopt[4] = DRIVEINFO_MAGOPT; static const char *driveinfo_tape[4] = DRIVEINFO_TAPE; static const char *apl_driveinfo_fixed[4] = APPLE_DRIVEINFO_FIXED; static const char *apl_driveinfo_removable[4] = APPLE_DRIVEINFO_REMOVABLE; static const char *apl_driveinfo_optical[4] = APPLE_DRIVEINFO_OPTICAL; static const char *apl_driveinfo_floppy[4] = APPLE_DRIVEINFO_FLOPPY; static const char *apl_driveinfo_magopt[4] = APPLE_DRIVEINFO_MAGOPT; static const char *apl_driveinfo_tape[4] = APPLE_DRIVEINFO_TAPE; const char **driveinfo = NULL; if (img.quirks == S2S_CFG_QUIRKS_APPLE) { // Use default drive IDs that are recognized by Apple machines switch (img.deviceType) { case S2S_CFG_FIXED: driveinfo = apl_driveinfo_fixed; break; case S2S_CFG_REMOVEABLE: driveinfo = apl_driveinfo_removable; break; case S2S_CFG_OPTICAL: driveinfo = apl_driveinfo_optical; break; case S2S_CFG_FLOPPY_14MB: driveinfo = apl_driveinfo_floppy; break; case S2S_CFG_MO: driveinfo = apl_driveinfo_magopt; break; case S2S_CFG_SEQUENTIAL: driveinfo = apl_driveinfo_tape; break; default: driveinfo = apl_driveinfo_fixed; break; } } else { // Generic IDs switch (img.deviceType) { case S2S_CFG_FIXED: driveinfo = driveinfo_fixed; break; case S2S_CFG_REMOVEABLE: driveinfo = driveinfo_removable; break; case S2S_CFG_OPTICAL: driveinfo = driveinfo_optical; break; case S2S_CFG_FLOPPY_14MB: driveinfo = driveinfo_floppy; break; case S2S_CFG_MO: driveinfo = driveinfo_magopt; break; case S2S_CFG_SEQUENTIAL: driveinfo = driveinfo_tape; break; default: driveinfo = driveinfo_fixed; break; } } if (img.vendor[0] == '\0') { memset(img.vendor, 0, sizeof(img.vendor)); strncpy(img.vendor, driveinfo[0], sizeof(img.vendor)); } if (img.prodId[0] == '\0') { memset(img.prodId, 0, sizeof(img.prodId)); strncpy(img.prodId, driveinfo[1], sizeof(img.prodId)); } if (img.revision[0] == '\0') { memset(img.revision, 0, sizeof(img.revision)); strncpy(img.revision, driveinfo[2], sizeof(img.revision)); } if (img.serial[0] == '\0') { memset(img.serial, 0, sizeof(img.serial)); strncpy(img.serial, driveinfo[3], sizeof(img.serial)); } if (img.serial[0] == '\0') { // Use SD card serial number cid_t sd_cid; uint32_t sd_sn = 0; if (SD.card()->readCID(&sd_cid)) { sd_sn = sd_cid.psn(); } memset(img.serial, 0, sizeof(img.serial)); const char *nibble = "0123456789ABCDEF"; img.serial[0] = nibble[(sd_sn >> 28) & 0xF]; img.serial[1] = nibble[(sd_sn >> 24) & 0xF]; img.serial[2] = nibble[(sd_sn >> 20) & 0xF]; img.serial[3] = nibble[(sd_sn >> 16) & 0xF]; img.serial[4] = nibble[(sd_sn >> 12) & 0xF]; img.serial[5] = nibble[(sd_sn >> 8) & 0xF]; img.serial[6] = nibble[(sd_sn >> 4) & 0xF]; img.serial[7] = nibble[(sd_sn >> 0) & 0xF]; } int rightAlign = img.rightAlignStrings; formatDriveInfoField(img.vendor, sizeof(img.vendor), rightAlign); formatDriveInfoField(img.prodId, sizeof(img.prodId), rightAlign); formatDriveInfoField(img.revision, sizeof(img.revision), rightAlign); formatDriveInfoField(img.serial, sizeof(img.serial), true); } bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_id, int scsi_lun, int blocksize, S2S_CFG_TYPE type) { image_config_t &img = g_DiskImages[target_idx]; img.file = ImageBackingStore(filename, blocksize); if (img.file.isOpen()) { img.bytesPerSector = blocksize; img.scsiSectors = img.file.size() / blocksize; img.scsiId = scsi_id | S2S_CFG_TARGET_ENABLED; img.sdSectorStart = 0; if (img.scsiSectors == 0) { log("---- Error: image file ", filename, " is empty"); img.file.close(); return false; } uint32_t sector_begin = 0, sector_end = 0; if (img.file.isRom()) { // ROM is always contiguous, no need to log } else if (!img.file.contiguousRange(§or_begin, §or_end)) { log("---- WARNING: file ", filename, " is fragmented, see https://github.com/BlueSCSI/BlueSCSI-v2/wiki/Image-File-Fragmentation"); } if (type == S2S_CFG_OPTICAL) { log("---- Configuring as CD-ROM drive based on image name"); img.deviceType = S2S_CFG_OPTICAL; } else if (type == S2S_CFG_FLOPPY_14MB) { log("---- Configuring as floppy drive based on image name"); img.deviceType = S2S_CFG_FLOPPY_14MB; } else if (type == S2S_CFG_MO) { log("---- Configuring as magneto-optical based on image name"); img.deviceType = S2S_CFG_MO; } else if (type == S2S_CFG_REMOVEABLE) { log("---- Configuring as removable drive based on image name"); img.deviceType = S2S_CFG_REMOVEABLE; } else if (type == S2S_CFG_SEQUENTIAL) { log("---- Configuring as tape drive based on image name"); img.deviceType = S2S_CFG_SEQUENTIAL; } #ifdef PLATFORM_CONFIG_HOOK PLATFORM_CONFIG_HOOK(&img); #endif setDefaultDriveInfo(target_idx); if (img.prefetchbytes != PREFETCH_BUFFER_SIZE) { log("---- Read prefetch enabled: ", (int)img.prefetchbytes, " bytes"); } else if(img.prefetchbytes == 0) { log("---- Read prefetch disabled"); } else { debuglog("---- Read prefetch enabled: ", (int)img.prefetchbytes, " bytes"); } 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()) { log("---- Found CD-ROM CUE sheet at ", cuesheetname); if (!cdromValidateCueSheet(img)) { log("---- Failed to parse cue sheet, using as plain binary image"); img.cuesheetfile.close(); } } else { log("---- No CUE sheet found at ", cuesheetname, ", using as plain binary image"); } } return true; } return false; } static void checkDiskGeometryDivisible(image_config_t &img) { if (!img.geometrywarningprinted) { uint32_t sectorsPerHeadTrack = img.sectorsPerTrack * img.headsPerCylinder; if (img.scsiSectors % sectorsPerHeadTrack != 0) { log("WARNING: Host used command ", scsiDev.cdb[0], " which is affected by drive geometry. Current settings are ", (int)img.sectorsPerTrack, " sectors x ", (int)img.headsPerCylinder, " heads = ", (int)sectorsPerHeadTrack, " but image size of ", (int)img.scsiSectors, " sectors is not divisible. This can cause error messages in diagnostics tools."); img.geometrywarningprinted = true; } } } // Set target configuration to default values static void scsiDiskConfigDefaults(int target_idx) { // Get default values from system preset, if any char presetName[32]; ini_gets("SCSI", "System", "", presetName, sizeof(presetName), CONFIGFILE); preset_config_t defaults = getSystemPreset(presetName); image_config_t &img = g_DiskImages[target_idx]; img.scsiId = target_idx; img.deviceType = S2S_CFG_FIXED; img.deviceTypeModifier = defaults.deviceTypeModifier; img.sectorsPerTrack = defaults.sectorsPerTrack; img.headsPerCylinder = defaults.headsPerCylinder; img.quirks = defaults.quirks; img.prefetchbytes = defaults.prefetchBytes; memset(img.vendor, 0, sizeof(img.vendor)); memset(img.prodId, 0, sizeof(img.prodId)); memset(img.revision, 0, sizeof(img.revision)); memset(img.serial, 0, sizeof(img.serial)); } // Load values for target configuration from given section if they exist. // Otherwise keep current settings. static void scsiDiskLoadConfig(int target_idx, const char *section) { image_config_t &img = g_DiskImages[target_idx]; img.deviceType = ini_getl(section, "Type", img.deviceType, CONFIGFILE); img.deviceTypeModifier = ini_getl(section, "TypeModifier", img.deviceTypeModifier, CONFIGFILE); img.sectorsPerTrack = ini_getl(section, "SectorsPerTrack", img.sectorsPerTrack, CONFIGFILE); img.headsPerCylinder = ini_getl(section, "HeadsPerCylinder", img.headsPerCylinder, CONFIGFILE); img.quirks = ini_getl(section, "Quirks", img.quirks, CONFIGFILE); img.rightAlignStrings = ini_getbool(section, "RightAlignStrings", 0, CONFIGFILE); img.prefetchbytes = ini_getl(section, "PrefetchBytes", img.prefetchbytes, CONFIGFILE); img.reinsert_on_inquiry = ini_getbool(section, "ReinsertCDOnInquiry", 0, CONFIGFILE); img.ejectButton = ini_getl(section, "EjectButton", 0, CONFIGFILE); char tmp[32]; memset(tmp, 0, sizeof(tmp)); ini_gets(section, "Vendor", "", tmp, sizeof(tmp), CONFIGFILE); if (tmp[0]) memcpy(img.vendor, tmp, sizeof(img.vendor)); memset(tmp, 0, sizeof(tmp)); ini_gets(section, "Product", "", tmp, sizeof(tmp), CONFIGFILE); if (tmp[0]) memcpy(img.prodId, tmp, sizeof(img.prodId)); memset(tmp, 0, sizeof(tmp)); ini_gets(section, "Version", "", tmp, sizeof(tmp), CONFIGFILE); if (tmp[0]) memcpy(img.revision, tmp, sizeof(img.revision)); memset(tmp, 0, sizeof(tmp)); ini_gets(section, "Serial", "", tmp, sizeof(tmp), CONFIGFILE); if (tmp[0]) memcpy(img.serial, tmp, sizeof(img.serial)); } // Check if image file name is overridden in config bool scsiDiskGetImageNameFromConfig(image_config_t &img, char *buf, size_t buflen) { int target_idx = img.scsiId & 7; char section[6] = "SCSI0"; section[4] = '0' + target_idx; char key[5] = "IMG0"; key[3] = '0' + img.image_index; ini_gets(section, key, "", buf, buflen, CONFIGFILE); return buf[0] != '\0'; } void scsiDiskLoadConfig(int target_idx) { char section[6] = "SCSI0"; section[4] = '0' + target_idx; // Set default settings scsiDiskConfigDefaults(target_idx); // First load global settings scsiDiskLoadConfig(target_idx, "SCSI"); // Then settings specific to target ID scsiDiskLoadConfig(target_idx, section); // Check if we have image specified by name char filename[MAX_FILE_PATH]; image_config_t &img = g_DiskImages[target_idx]; if (scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename))) { int blocksize = (img.deviceType == S2S_CFG_OPTICAL) ? 2048 : 512; log("-- Opening ", filename, " for id:", target_idx, ", specified in " CONFIGFILE); scsiDiskOpenHDDImage(target_idx, filename, target_idx, 0, blocksize); } } bool scsiDiskCheckAnyImagesConfigured() { for (int i = 0; i < S2S_MAX_TARGETS; i++) { if (g_DiskImages[i].file.isOpen() && (g_DiskImages[i].scsiId & S2S_CFG_TARGET_ENABLED)) { return true; } } return false; } image_config_t &scsiDiskGetImageConfig(int target_idx) { assert(target_idx >= 0 && target_idx < S2S_MAX_TARGETS); return g_DiskImages[target_idx]; } static void diskEjectAction(uint8_t buttonId) { log("Eject button pressed for channel ", buttonId); for (uint8_t i = 0; i < S2S_MAX_TARGETS; i++) { image_config_t img = g_DiskImages[i]; if (img.ejectButton == buttonId) { if (img.deviceType == S2S_CFG_OPTICAL) { cdromPerformEject(img); } } } } uint8_t diskEjectButtonUpdate(bool immediate) { // treat '1' to '0' transitions as eject actions static uint8_t previous = 0x00; uint8_t bitmask = platform_get_buttons(); uint8_t ejectors = (previous ^ bitmask) & previous; previous = bitmask; // defer ejection until the bus is idle static uint8_t deferred = 0x00; if (!immediate) { deferred |= ejectors; return 0; } else { ejectors |= deferred; deferred = 0; if (ejectors) { uint8_t mask = 1; for (uint8_t i = 0; i < 8; i++) { if (ejectors & mask) diskEjectAction(i + 1); mask = mask << 1; } } return ejectors; } } /*******************************/ /* Config handling for SCSI2SD */ /*******************************/ extern "C" void s2s_configInit(S2S_BoardCfg* config) { if (SD.exists(CONFIGFILE)) { log("Reading configuration from " CONFIGFILE); } else { log("Config file " CONFIGFILE " not found, using defaults"); } // Get default values from system preset, if any char presetName[32]; ini_gets("SCSI", "System", "", presetName, sizeof(presetName), CONFIGFILE); preset_config_t defaults = getSystemPreset(presetName); if (defaults.presetName) { log("Active configuration (using system preset \"", defaults.presetName, "\"):"); } else { log("Active configuration:"); } memset(config, 0, sizeof(S2S_BoardCfg)); memcpy(config->magic, "BCFG", 4); config->flags = 0; config->startupDelay = 0; config->selectionDelay = ini_getl("SCSI", "SelectionDelay", defaults.selectionDelay, CONFIGFILE); config->flags6 = 0; config->scsiSpeed = PLATFORM_MAX_SCSI_SPEED; int maxSyncSpeed = ini_getl("SCSI", "MaxSyncSpeed", defaults.maxSyncSpeed, CONFIGFILE); if (maxSyncSpeed < 5 && config->scsiSpeed > S2S_CFG_SPEED_ASYNC_50) config->scsiSpeed = S2S_CFG_SPEED_ASYNC_50; else if (maxSyncSpeed < 10 && config->scsiSpeed > S2S_CFG_SPEED_SYNC_5) config->scsiSpeed = S2S_CFG_SPEED_SYNC_5; if ((int)config->selectionDelay == defaults.selectionDelay) { debuglog("-- SelectionDelay: ", (int)config->selectionDelay); } else { log("-- SelectionDelay: ", (int)config->selectionDelay); } if (ini_getbool("SCSI", "EnableUnitAttention", defaults.enableUnitAttention, CONFIGFILE)) { log("-- EnableUnitAttention is on"); config->flags |= S2S_CFG_ENABLE_UNIT_ATTENTION; } else { debuglog("-- EnableUnitAttention is off"); } if (ini_getbool("SCSI", "EnableSCSI2", defaults.enableSCSI2, CONFIGFILE)) { debuglog("-- EnableSCSI2 is on"); config->flags |= S2S_CFG_ENABLE_SCSI2; } else { log("-- EnableSCSI2 is off"); } if (ini_getbool("SCSI", "EnableSelLatch", defaults.enableSelLatch, CONFIGFILE)) { log("-- EnableSelLatch is on"); config->flags |= S2S_CFG_ENABLE_SEL_LATCH; } else { debuglog("-- EnableSelLatch is off"); } if (ini_getbool("SCSI", "MapLunsToIDs", defaults.mapLunsToIDs, CONFIGFILE)) { log("-- MapLunsToIDs is on"); config->flags |= S2S_CFG_MAP_LUNS_TO_IDS; } else { debuglog("-- MapLunsToIDs is off"); } if (ini_getbool("SCSI", "Debug", 0, CONFIGFILE)) { log("-- Debug is enabled"); } if (ini_getbool("SCSI", "EnableParity", defaults.enableParity, CONFIGFILE)) { debuglog("-- Parity is enabled"); config->flags |= S2S_CFG_ENABLE_PARITY; } else { log("-- Parity is disabled"); } if (ini_getbool("SCSI", "ReinsertCDOnInquiry", defaults.reinsertOnInquiry, CONFIGFILE)) { log("-- ReinsertCDOnInquiry is enabled"); } else { debuglog("-- ReinsertCDOnInquiry is disabled"); } } extern "C" void s2s_debugInit(void) { } extern "C" void s2s_configPoll(void) { } extern "C" void s2s_configSave(int scsiId, uint16_t byesPerSector) { // Modification of config over SCSI bus is not implemented. } extern "C" const S2S_TargetCfg* s2s_getConfigByIndex(int index) { if (index < 0 || index >= S2S_MAX_TARGETS) { return NULL; } else { return &g_DiskImages[index]; } } extern "C" const S2S_TargetCfg* s2s_getConfigById(int scsiId) { int i; for (i = 0; i < S2S_MAX_TARGETS; ++i) { const S2S_TargetCfg* tgt = s2s_getConfigByIndex(i); if ((tgt->scsiId & S2S_CFG_TARGET_ID_BITS) == scsiId && (tgt->scsiId & S2S_CFG_TARGET_ENABLED)) { return tgt; } } return NULL; } /**********************/ /* FormatUnit command */ /**********************/ // Callback once all data has been read in the data out phase. static void doFormatUnitComplete(void) { scsiDev.phase = STATUS; } static void doFormatUnitSkipData(int bytes) { // We may not have enough memory to store the initialisation pattern and // defect list data. Since we're not making use of it yet anyway, just // discard the bytes. scsiEnterPhase(DATA_OUT); int i; for (i = 0; i < bytes; ++i) { scsiReadByte(); } } // Callback from the data out phase. static void doFormatUnitPatternHeader(void) { int defectLength = ((((uint16_t)scsiDev.data[2])) << 8) + scsiDev.data[3]; int patternLength = ((((uint16_t)scsiDev.data[4 + 2])) << 8) + scsiDev.data[4 + 3]; doFormatUnitSkipData(defectLength + patternLength); doFormatUnitComplete(); } // Callback from the data out phase. static void doFormatUnitHeader(void) { int IP = (scsiDev.data[1] & 0x08) ? 1 : 0; int DSP = (scsiDev.data[1] & 0x04) ? 1 : 0; if (! DSP) // disable save parameters { // Save the "MODE SELECT savable parameters" s2s_configSave( scsiDev.target->targetId, scsiDev.target->liveCfg.bytesPerSector); } if (IP) { // We need to read the initialisation pattern header first. scsiDev.dataLen += 4; scsiDev.phase = DATA_OUT; scsiDev.postDataOutHook = doFormatUnitPatternHeader; } else { // Read the defect list data int defectLength = ((((uint16_t)scsiDev.data[2])) << 8) + scsiDev.data[3]; doFormatUnitSkipData(defectLength); doFormatUnitComplete(); } } /************************/ /* ReadCapacity command */ /************************/ static void doReadCapacity() { uint32_t lba = (((uint32_t) scsiDev.cdb[2]) << 24) + (((uint32_t) scsiDev.cdb[3]) << 16) + (((uint32_t) scsiDev.cdb[4]) << 8) + scsiDev.cdb[5]; int pmi = scsiDev.cdb[8] & 1; image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; uint32_t capacity = img.file.size() / bytesPerSector; if (!pmi && lba) { // error. // We don't do anything with the "partial medium indicator", and // assume that delays are constant across each block. But the spec // says we must return this error if pmi is specified incorrectly. scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB; scsiDev.phase = STATUS; } else if (capacity > 0) { uint32_t highestBlock = capacity - 1; scsiDev.data[0] = highestBlock >> 24; scsiDev.data[1] = highestBlock >> 16; scsiDev.data[2] = highestBlock >> 8; scsiDev.data[3] = highestBlock; uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; scsiDev.data[4] = bytesPerSector >> 24; scsiDev.data[5] = bytesPerSector >> 16; scsiDev.data[6] = bytesPerSector >> 8; scsiDev.data[7] = bytesPerSector; scsiDev.dataLen = 8; scsiDev.phase = DATA_IN; } else { scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = NOT_READY; scsiDev.target->sense.asc = MEDIUM_NOT_PRESENT; scsiDev.phase = STATUS; } } /*************************/ /* TestUnitReady command */ /*************************/ static int doTestUnitReady() { int ready = 1; image_config_t &img = *(image_config_t*)scsiDev.target->cfg; if (unlikely(!scsiDev.target->started || !img.file.isOpen())) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = NOT_READY; scsiDev.target->sense.asc = LOGICAL_UNIT_NOT_READY_INITIALIZING_COMMAND_REQUIRED; scsiDev.phase = STATUS; } else if (img.ejected) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = NOT_READY; scsiDev.target->sense.asc = MEDIUM_NOT_PRESENT; scsiDev.phase = STATUS; // We are now reporting to host that the drive is open. // Simulate a "close" for next time the host polls. cdromSwitchNextImage(img); } else if (unlikely(!(blockDev.state & DISK_PRESENT))) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = NOT_READY; scsiDev.target->sense.asc = MEDIUM_NOT_PRESENT; scsiDev.phase = STATUS; } else if (unlikely(!(blockDev.state & DISK_INITIALISED))) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = NOT_READY; scsiDev.target->sense.asc = LOGICAL_UNIT_NOT_READY_CAUSE_NOT_REPORTABLE; scsiDev.phase = STATUS; } return ready; } /****************/ /* Seek command */ /****************/ static void doSeek(uint32_t lba) { image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; uint32_t capacity = img.file.size() / bytesPerSector; if (lba >= capacity) { scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { if (unlikely(scsiDev.target->cfg->deviceType == S2S_CFG_FLOPPY_14MB) || scsiDev.compatMode < COMPAT_SCSI2) { s2s_delay_ms(10); } else { s2s_delay_us(10); } } } /********************************************/ /* Transfer state for read / write commands */ /********************************************/ BlockDevice blockDev = {DISK_PRESENT | DISK_INITIALISED}; Transfer transfer; static struct { uint8_t *buffer; uint32_t bytes_sd; // Number of bytes that have been scheduled for transfer on SD card side uint32_t bytes_scsi; // Number of bytes that have been scheduled for transfer on SCSI side uint32_t bytes_scsi_started; uint32_t sd_transfer_start; int parityError; } g_disk_transfer; #ifdef PREFETCH_BUFFER_SIZE static struct { uint8_t buffer[PREFETCH_BUFFER_SIZE]; uint32_t sector; uint32_t bytes; uint8_t scsiId; } g_scsi_prefetch; #endif /*****************/ /* Write command */ /*****************/ 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 // without an access time s2s_delay_ms(10); } image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; uint32_t capacity = img.file.size() / bytesPerSector; debuglog("------ Write ", (int)blocks, "x", (int)bytesPerSector, " starting at ", (int)lba); if (unlikely(blockDev.state & DISK_WP) || unlikely(scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL) || unlikely(!img.file.isWritable())) { log("WARNING: Host attempted write to read-only drive ID ", (int)(img.scsiId & S2S_CFG_TARGET_ID_BITS)); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = WRITE_PROTECTED; scsiDev.phase = STATUS; } else if (unlikely(((uint64_t) lba) + blocks > capacity)) { log("WARNING: Host attempted write at sector ", (int)lba, "+", (int)blocks, ", exceeding image size ", (int)capacity, " sectors (", (int)bytesPerSector, "B/sector)"); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { transfer.multiBlock = true; transfer.lba = lba; transfer.blocks = blocks; transfer.currentBlock = 0; scsiDev.phase = DATA_OUT; scsiDev.dataLen = 0; scsiDev.dataPtr = 0; #ifdef PREFETCH_BUFFER_SIZE // Invalidate prefetch buffer g_scsi_prefetch.bytes = 0; g_scsi_prefetch.sector = 0; #endif image_config_t &img = *(image_config_t*)scsiDev.target->cfg; if (!img.file.seek((uint64_t)transfer.lba * bytesPerSector)) { log("Seek to ", transfer.lba, " failed for SCSI ID", (int)scsiDev.target->targetId); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = MEDIUM_ERROR; scsiDev.target->sense.asc = NO_SEEK_COMPLETE; scsiDev.phase = STATUS; } } } // Called to transfer next block from SCSI bus. // Usually called from SD card driver during waiting for SD card access. void diskDataOut_callback(uint32_t bytes_complete) { // For best performance, do SCSI reads in blocks of 4 or more bytes bytes_complete &= ~3; if (g_disk_transfer.bytes_scsi_started < g_disk_transfer.bytes_scsi) { // How many bytes remaining in the transfer? uint32_t remain = g_disk_transfer.bytes_scsi - g_disk_transfer.bytes_scsi_started; uint32_t len = remain; // Split read so that it doesn't wrap around buffer edge uint32_t bufsize = sizeof(scsiDev.data); uint32_t start = (g_disk_transfer.bytes_scsi_started % bufsize); if (start + len > bufsize) len = bufsize - start; // Apply platform-specific optimized transfer sizes if (len > PLATFORM_OPTIMAL_SCSI_READ_BLOCK_SIZE) { len = PLATFORM_OPTIMAL_SCSI_READ_BLOCK_SIZE; } // Don't overwrite data that has not yet been written to SD card uint32_t sd_ready_cnt = g_disk_transfer.bytes_sd + bytes_complete; if (g_disk_transfer.bytes_scsi_started + len > sd_ready_cnt + bufsize) len = sd_ready_cnt + bufsize - g_disk_transfer.bytes_scsi_started; // Keep transfers a multiple of sector size. // Macintosh SCSI driver seems to get confused if we have a delay // in middle of a sector. uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; if (remain >= bytesPerSector && len % bytesPerSector != 0) { len -= len % bytesPerSector; } if (len == 0) return; // debuglog("SCSI read ", (int)start, " + ", (int)len); scsiStartRead(&scsiDev.data[start], len, &g_disk_transfer.parityError); g_disk_transfer.bytes_scsi_started += len; } } void diskDataOut() { scsiEnterPhase(DATA_OUT); image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint32_t blockcount = (transfer.blocks - transfer.currentBlock); uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; g_disk_transfer.buffer = scsiDev.data; g_disk_transfer.bytes_scsi = blockcount * bytesPerSector; g_disk_transfer.bytes_sd = 0; g_disk_transfer.bytes_scsi_started = 0; g_disk_transfer.sd_transfer_start = 0; g_disk_transfer.parityError = 0; while (g_disk_transfer.bytes_sd < g_disk_transfer.bytes_scsi && scsiDev.phase == DATA_OUT && !scsiDev.resetFlag) { platform_poll(); diskEjectButtonUpdate(false); // Figure out how many contiguous bytes are available for writing to SD card. uint32_t bufsize = sizeof(scsiDev.data); uint32_t start = g_disk_transfer.bytes_sd % bufsize; uint32_t len = 0; // How much data until buffer edge wrap? uint32_t available = g_disk_transfer.bytes_scsi_started - g_disk_transfer.bytes_sd; if (start + available > bufsize) available = bufsize - start; // Count number of finished sectors if (scsiIsReadFinished(&scsiDev.data[start + available - 1])) { len = available; } else { while (len < available && scsiIsReadFinished(&scsiDev.data[start + len + SD_SECTOR_SIZE - 1])) { len += SD_SECTOR_SIZE; } } // In case the last sector is partial (256 byte SCSI sectors) if (len > available) { len = available; } // Apply platform-specific write size blocks for optimization if (len > PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE) { len = PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE; } uint32_t remain_in_transfer = g_disk_transfer.bytes_scsi - g_disk_transfer.bytes_sd; if (len < bufsize - start && len < remain_in_transfer) { // Use large write blocks in middle of transfer and smaller at the end of transfer. // This improves performance for large writes and reduces latency at end of request. uint32_t min_write_size = PLATFORM_OPTIMAL_MIN_SD_WRITE_SIZE; if (remain_in_transfer <= PLATFORM_OPTIMAL_MAX_SD_WRITE_SIZE) { min_write_size = PLATFORM_OPTIMAL_LAST_SD_WRITE_SIZE; } if (len < min_write_size) { len = 0; } } if (len == 0) { // Nothing ready to transfer, check if we can read more from SCSI bus diskDataOut_callback(0); } else { // Finalize transfer on SCSI side scsiFinishRead(&scsiDev.data[start], len, &g_disk_transfer.parityError); // Check parity error status before writing to SD card if (g_disk_transfer.parityError && (scsiDev.boardCfg.flags & S2S_CFG_ENABLE_PARITY)) { scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ABORTED_COMMAND; scsiDev.target->sense.asc = SCSI_PARITY_ERROR; scsiDev.phase = STATUS; break; } // Start writing to SD card and simultaneously start new SCSI transfers // when buffer space is freed. uint8_t *buf = &scsiDev.data[start]; g_disk_transfer.sd_transfer_start = start; // debuglog("SD write ", (int)start, " + ", (int)len, " ", bytearray(buf, len)); platform_set_sd_callback(&diskDataOut_callback, buf); if (img.file.write(buf, len) != len) { log("SD card write failed: ", SD.sdErrorCode()); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = MEDIUM_ERROR; scsiDev.target->sense.asc = WRITE_ERROR_AUTO_REALLOCATION_FAILED; scsiDev.phase = STATUS; } platform_set_sd_callback(NULL, NULL); g_disk_transfer.bytes_sd += len; } } // Release SCSI bus scsiFinishRead(NULL, 0, &g_disk_transfer.parityError); transfer.currentBlock += blockcount; scsiDev.dataPtr = scsiDev.dataLen = 0; if (transfer.currentBlock == transfer.blocks) { // Verify that all data has been flushed to disk from SdFat cache. // Normally does nothing as we do not change image file size and // data writes are not cached. img.file.flush(); } } /*****************/ /* Read command */ /*****************/ 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 // without an access time s2s_delay_ms(10); } image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; uint32_t capacity = img.file.size() / bytesPerSector; debuglog("------ Read ", (int)blocks, "x", (int)bytesPerSector, " starting at ", (int)lba); if (unlikely(((uint64_t) lba) + blocks > capacity)) { log("WARNING: Host attempted read at sector ", (int)lba, "+", (int)blocks, ", exceeding image size ", (int)capacity, " sectors (", (int)bytesPerSector, "B/sector)"); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { transfer.multiBlock = 1; transfer.lba = lba; transfer.blocks = blocks; transfer.currentBlock = 0; scsiDev.phase = DATA_IN; scsiDev.dataLen = 0; scsiDev.dataPtr = 0; #ifdef PREFETCH_BUFFER_SIZE uint32_t sectors_in_prefetch = g_scsi_prefetch.bytes / bytesPerSector; if (img.scsiId == g_scsi_prefetch.scsiId && transfer.lba >= g_scsi_prefetch.sector && transfer.lba < g_scsi_prefetch.sector + sectors_in_prefetch) { // We have the some sectors already in prefetch cache scsiEnterPhase(DATA_IN); uint32_t start_offset = transfer.lba - g_scsi_prefetch.sector; uint32_t count = sectors_in_prefetch - start_offset; if (count > transfer.blocks) count = transfer.blocks; scsiStartWrite(g_scsi_prefetch.buffer + start_offset * bytesPerSector, count * bytesPerSector); debuglog("------ Found ", (int)count, " sectors in prefetch cache"); transfer.currentBlock += count; } if (transfer.currentBlock == transfer.blocks) { while (!scsiIsWriteFinished(NULL)) { platform_poll(); diskEjectButtonUpdate(false); } scsiFinishWrite(); } #endif if (!img.file.seek((uint64_t)(transfer.lba + transfer.currentBlock) * bytesPerSector)) { log("Seek to ", transfer.lba, " failed for SCSI ID", (int)scsiDev.target->targetId); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = MEDIUM_ERROR; scsiDev.target->sense.asc = NO_SEEK_COMPLETE; scsiDev.phase = STATUS; } } } void diskDataIn_callback(uint32_t bytes_complete) { // On SCSI-1 devices the phase change has some extra delays. // Doing it here lets the SD card transfer proceed in background. scsiEnterPhase(DATA_IN); // For best performance, do writes in blocks of 4 or more bytes if (bytes_complete < g_disk_transfer.bytes_sd) { bytes_complete &= ~3; } // Machintosh SCSI driver can get confused if pauses occur in middle of // a sector, so schedule the transfers in sector sized blocks. if (bytes_complete < g_disk_transfer.bytes_sd) { uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; if (bytes_complete % bytesPerSector != 0) { bytes_complete -= bytes_complete % bytesPerSector; } } if (bytes_complete > g_disk_transfer.bytes_scsi) { // DMA is reading from SD card, bytes_complete bytes have already been read. // Send them to SCSI bus now. uint32_t len = bytes_complete - g_disk_transfer.bytes_scsi; scsiStartWrite(g_disk_transfer.buffer + g_disk_transfer.bytes_scsi, len); g_disk_transfer.bytes_scsi += len; } // Provide a chance for polling request processing scsiIsWriteFinished(NULL); } // Start a data in transfer using given temporary buffer. // diskDataIn() below divides the scsiDev.data buffer to two halves for double buffering. static void start_dataInTransfer(uint8_t *buffer, uint32_t count) { g_disk_transfer.buffer = buffer; g_disk_transfer.bytes_scsi = 0; g_disk_transfer.bytes_sd = count; // Verify that previous write using this buffer has finished uint32_t start = millis(); while (!scsiIsWriteFinished(buffer + count - 1) && !scsiDev.resetFlag) { if ((uint32_t)(millis() - start) > 5000) { log("start_dataInTransfer() timeout waiting for previous to finish"); scsiDev.resetFlag = 1; } platform_poll(); diskEjectButtonUpdate(false); } if (scsiDev.resetFlag) return; // Start transferring from SD card image_config_t &img = *(image_config_t*)scsiDev.target->cfg; platform_set_sd_callback(&diskDataIn_callback, buffer); if (img.file.read(buffer, count) != count) { log("SD card read failed: ", SD.sdErrorCode()); scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = MEDIUM_ERROR; scsiDev.target->sense.asc = UNRECOVERED_READ_ERROR; scsiDev.phase = STATUS; } diskDataIn_callback(count); platform_set_sd_callback(NULL, NULL); platform_poll(); diskEjectButtonUpdate(false); } static void diskDataIn() { // Figure out how many blocks we can fit in buffer uint32_t bytesPerSector = scsiDev.target->liveCfg.bytesPerSector; uint32_t maxblocks = sizeof(scsiDev.data) / bytesPerSector; uint32_t maxblocks_half = maxblocks / 2; // Start transfer in first half of buffer // Waits for the previous first half transfer to finish first. uint32_t remain = (transfer.blocks - transfer.currentBlock); if (remain > 0) { uint32_t transfer_blocks = std::min(remain, maxblocks_half); uint32_t transfer_bytes = transfer_blocks * bytesPerSector; start_dataInTransfer(&scsiDev.data[0], transfer_bytes); transfer.currentBlock += transfer_blocks; } // Start transfer in second half of buffer // Waits for the previous second half transfer to finish first remain = (transfer.blocks - transfer.currentBlock); if (remain > 0) { uint32_t transfer_blocks = std::min(remain, maxblocks_half); uint32_t transfer_bytes = transfer_blocks * bytesPerSector; start_dataInTransfer(&scsiDev.data[maxblocks_half * bytesPerSector], transfer_bytes); transfer.currentBlock += transfer_blocks; } if (transfer.currentBlock == transfer.blocks) { // This was the last block, verify that everything finishes #ifdef PREFETCH_BUFFER_SIZE image_config_t &img = *(image_config_t*)scsiDev.target->cfg; int prefetchbytes = img.prefetchbytes; if (prefetchbytes > PREFETCH_BUFFER_SIZE) prefetchbytes = PREFETCH_BUFFER_SIZE; uint32_t prefetch_sectors = prefetchbytes / bytesPerSector; uint32_t img_sector_count = img.file.size() / bytesPerSector; g_scsi_prefetch.sector = transfer.lba + transfer.blocks; g_scsi_prefetch.bytes = 0; g_scsi_prefetch.scsiId = scsiDev.target->cfg->scsiId; if (g_scsi_prefetch.sector + prefetch_sectors > img_sector_count) { // Don't try to read past image end. prefetch_sectors = img_sector_count - g_scsi_prefetch.sector; } while (!scsiIsWriteFinished(NULL) && prefetch_sectors > 0 && !scsiDev.resetFlag) { platform_poll(); diskEjectButtonUpdate(false); // Check if prefetch buffer is free g_disk_transfer.buffer = g_scsi_prefetch.buffer + g_scsi_prefetch.bytes; if (!scsiIsWriteFinished(g_disk_transfer.buffer) || !scsiIsWriteFinished(g_disk_transfer.buffer + bytesPerSector - 1)) { continue; } // We still have time, prefetch next sectors in case this SCSI request // is part of a longer linear read. g_disk_transfer.bytes_sd = bytesPerSector; g_disk_transfer.bytes_scsi = bytesPerSector; // Tell callback not to send to SCSI platform_set_sd_callback(&diskDataIn_callback, g_disk_transfer.buffer); int status = img.file.read(g_disk_transfer.buffer, bytesPerSector); if (status <= 0) { log("Prefetch read failed"); prefetch_sectors = 0; break; } g_scsi_prefetch.bytes += status; platform_set_sd_callback(NULL, NULL); prefetch_sectors--; } #endif while (!scsiIsWriteFinished(NULL)) { platform_poll(); diskEjectButtonUpdate(false); } scsiFinishWrite(); } } /********************/ /* Command dispatch */ /********************/ // Handle direct-access scsi device commands extern "C" int scsiDiskCommand() { int commandHandled = 1; image_config_t &img = *(image_config_t*)scsiDev.target->cfg; uint8_t command = scsiDev.cdb[0]; if (unlikely(command == 0x1B)) { // START STOP UNIT // Enable or disable media access operations. //int immed = scsiDev.cdb[1] & 1; int start = scsiDev.cdb[4] & 1; if (start) { scsiDev.target->started = 1; } else { scsiDev.target->started = 0; } } else if (unlikely(command == 0x00)) { // TEST UNIT READY doTestUnitReady(); } else if (unlikely(!doTestUnitReady())) { // Status and sense codes already set by doTestUnitReady } else if (likely(command == 0x08)) { // READ(6) uint32_t lba = (((uint32_t) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32_t) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; uint32_t blocks = scsiDev.cdb[4]; if (unlikely(blocks == 0)) blocks = 256; scsiDiskStartRead(lba, blocks); } else if (likely(command == 0x28)) { // READ(10) // Ignore all cache control bits - we don't support a memory cache. 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]; scsiDiskStartRead(lba, blocks); } else if (likely(command == 0x0A)) { // WRITE(6) uint32_t lba = (((uint32_t) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32_t) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; uint32_t blocks = scsiDev.cdb[4]; if (unlikely(blocks == 0)) blocks = 256; scsiDiskStartWrite(lba, blocks); } else if (likely(command == 0x2A) || // WRITE(10) unlikely(command == 0x2E)) // WRITE AND VERIFY { // Ignore all cache control bits - we don't support a memory cache. // Don't bother verifying either. The SD card likely stores ECC // along with each flash row. 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]; scsiDiskStartWrite(lba, blocks); } else if (unlikely(command == 0x04)) { // FORMAT UNIT // We don't really do any formatting, but we need to read the correct // number of bytes in the DATA_OUT phase to make the SCSI host happy. int fmtData = (scsiDev.cdb[1] & 0x10) ? 1 : 0; if (fmtData) { // We need to read the parameter list, but we don't know how // big it is yet. Start with the header. scsiDev.dataLen = 4; scsiDev.phase = DATA_OUT; scsiDev.postDataOutHook = doFormatUnitHeader; } else { // No data to read, we're already finished! } } else if (unlikely(command == 0x25)) { // READ CAPACITY doReadCapacity(); } else if (unlikely(command == 0x0B)) { // SEEK(6) uint32_t lba = (((uint32_t) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32_t) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; doSeek(lba); } else if (unlikely(command == 0x2B)) { // SEEK(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]; doSeek(lba); } else if (unlikely(command == 0x36)) { // LOCK UNLOCK CACHE // We don't have a cache to lock data into. do nothing. } else if (unlikely(command == 0x34)) { // PRE-FETCH. // We don't have a cache to pre-fetch into. do nothing. } else if (unlikely(command == 0x1E)) { // PREVENT ALLOW MEDIUM REMOVAL // Not much we can do to prevent the user removing the SD card. // do nothing. } else if (unlikely(command == 0x01)) { // REZERO UNIT // Set the lun to a vendor-specific state. Ignore. } else if (unlikely(command == 0x35)) { // SYNCHRONIZE CACHE // We don't have a cache. do nothing. } else if (unlikely(command == 0x2F)) { // VERIFY // TODO: When they supply data to verify, we should read the data and // verify it. If they don't supply any data, just say success. if ((scsiDev.cdb[1] & 0x02) == 0) { // They are asking us to do a medium verification with no data // comparison. Assume success, do nothing. } else { // TODO. This means they are supplying data to verify against. // Technically we should probably grab the data and compare it. scsiDev.status = CHECK_CONDITION; scsiDev.target->sense.code = ILLEGAL_REQUEST; scsiDev.target->sense.asc = INVALID_FIELD_IN_CDB; scsiDev.phase = STATUS; } } else if (unlikely(command == 0x37)) { // READ DEFECT DATA uint32_t allocLength = (((uint16_t)scsiDev.cdb[7]) << 8) | scsiDev.cdb[8]; scsiDev.data[0] = 0; scsiDev.data[1] = scsiDev.cdb[1]; scsiDev.data[2] = 0; scsiDev.data[3] = 0; scsiDev.dataLen = 4; if (scsiDev.dataLen > allocLength) { scsiDev.dataLen = allocLength; } scsiDev.phase = DATA_IN; } else if (img.file.isRom()) { // Special handling for ROM drive to make SCSI2SD code report it as read-only blockDev.state |= DISK_WP; commandHandled = scsiModeCommand(); blockDev.state &= ~DISK_WP; } else { commandHandled = 0; } return commandHandled; } extern "C" void scsiDiskPoll() { if (scsiDev.phase == DATA_IN && transfer.currentBlock != transfer.blocks) { diskDataIn(); } else if (scsiDev.phase == DATA_OUT && transfer.currentBlock != transfer.blocks) { diskDataOut(); } if (scsiDev.phase == STATUS && scsiDev.target) { // Check if the command is affected by drive geometry. // Affected commands are: // 0x1A MODE SENSE command of pages 0x03 (device format), 0x04 (disk geometry) or 0x3F (all pages) // 0x1C RECEIVE DIAGNOSTICS RESULTS uint8_t command = scsiDev.cdb[0]; uint8_t pageCode = scsiDev.cdb[2] & 0x3F; if ((command == 0x1A && (pageCode == 0x03 || pageCode == 0x04 || pageCode == 0x3F)) || command == 0x1C) { image_config_t &img = *(image_config_t*)scsiDev.target->cfg; checkDiskGeometryDivisible(img); } // Check for Inquiry command to reinsert CD-ROMs on boot if (command == 0x12) { image_config_t &img = *(image_config_t*)scsiDev.target->cfg; if (img.deviceType == S2S_CFG_OPTICAL && img.reinsert_on_inquiry) { cdromReinsertFirstImage(img); } } } } extern "C" void scsiDiskReset() { scsiDev.dataPtr = 0; scsiDev.savedDataPtr = 0; scsiDev.dataLen = 0; // transfer.lba = 0; // Needed in Request Sense to determine failure transfer.blocks = 0; transfer.currentBlock = 0; transfer.multiBlock = 0; #ifdef PREFETCH_BUFFER_SIZE g_scsi_prefetch.bytes = 0; g_scsi_prefetch.sector = 0; #endif // Reinsert any ejected CD-ROMs for (int i = 0; i < S2S_MAX_TARGETS; ++i) { image_config_t &img = g_DiskImages[i]; if (img.deviceType == S2S_CFG_OPTICAL) { cdromReinsertFirstImage(img); } } } extern "C" void scsiDiskInit() { scsiDiskReset(); }