/* Initiator mode USB Mass Storage Class connection. * This file binds platform-specific MSC routines to the initiator mode * SCSI bus interface. The call structure is modeled after TinyUSB, but * should be usable with other USB libraries. * * 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 . */ #include "ZuluSCSI_config.h" #include "ZuluSCSI_log.h" #include "ZuluSCSI_log_trace.h" #include "ZuluSCSI_initiator.h" #include "ZuluSCSI_platform_msc.h" #include #include #include #include "SdFat.h" bool g_msc_initiator; #ifndef PLATFORM_HAS_INITIATOR_MODE bool setup_msc_initiator() { return false; } void poll_msc_initiator() {} void init_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) {} uint8_t init_msc_get_maxlun_cb(void) { return 0; } bool init_msc_is_writable_cb (uint8_t lun) { return false; } bool init_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) { return false; } bool init_msc_test_unit_ready_cb(uint8_t lun) { return false; } void init_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) {} int32_t init_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer, uint16_t bufsize) {return -1;} int32_t init_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) {return -1;} int32_t init_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { return -1;} void init_msc_write10_complete_cb(uint8_t lun) {} #else // If there are multiple SCSI devices connected, they are mapped into LUNs for host. static struct { int target_id; uint32_t sectorsize; uint32_t sectorcount; } g_msc_initiator_targets[NUM_SCSIID]; static int g_msc_initiator_target_count; // Prefetch next sector in main loop while USB is transferring previous one. static struct { uint8_t *prefetch_buffer; // Buffer to use for storing the data uint32_t prefetch_bufsize; uint32_t prefetch_lba; // First sector to fetch int prefetch_target_id; // Target to read from size_t prefetch_sectorcount; // Number of sectors to fetch size_t prefetch_sectorsize; bool prefetch_done; // True after prefetch is complete // Periodic status reporting to log output uint32_t status_prev_time; uint32_t status_interval; uint32_t status_reqcount; uint32_t status_bytecount; } g_msc_initiator_state; static int do_read6_or_10(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize, void *buffer); static void scan_targets() { int initiator_id = scsiInitiatorGetOwnID(); uint8_t inquiry_data[36] = {0}; g_msc_initiator_target_count = 0; for (int target_id = 0; target_id < NUM_SCSIID; target_id++) { if (target_id == initiator_id) continue; if (scsiTestUnitReady(target_id)) { uint32_t sectorcount, sectorsize; bool inquiryok = scsiStartStopUnit(target_id, true) && scsiInquiry(target_id, inquiry_data) && scsiInitiatorReadCapacity(target_id, §orcount, §orsize); char vendor_id[9] = {0}; char product_id[17] = {0}; memcpy(vendor_id, &inquiry_data[8], 8); memcpy(product_id, &inquiry_data[16], 16); if (inquiryok) { logmsg("Found SCSI drive with ID ", target_id, ": ", vendor_id, " ", product_id); g_msc_initiator_targets[g_msc_initiator_target_count].target_id = target_id; g_msc_initiator_targets[g_msc_initiator_target_count].sectorcount = sectorcount; g_msc_initiator_targets[g_msc_initiator_target_count].sectorsize = sectorsize; g_msc_initiator_target_count++; } else { logmsg("Detected SCSI device with ID ", target_id, ", but failed to get inquiry response, skipping"); } } } } bool setup_msc_initiator() { logmsg("SCSI Initiator: activating USB MSC mode"); g_msc_initiator = true; if (!ini_getbool("SCSI", "InitiatorMSCDisablePrefetch", false, CONFIGFILE)) { // We can use the device mode buffer for prefetching data in initiator mode g_msc_initiator_state.prefetch_buffer = scsiDev.data; g_msc_initiator_state.prefetch_bufsize = sizeof(scsiDev.data); } g_msc_initiator_state.status_interval = ini_getl("SCSI", "InitiatorMSCStatusInterval", 5000, CONFIGFILE); scsiInitiatorInit(); // Scan for targets scan_targets(); logmsg("SCSI Initiator: found " , g_msc_initiator_target_count, " SCSI drives"); return g_msc_initiator_target_count > 0; } void poll_msc_initiator() { if (g_msc_initiator_target_count == 0) { // Scan for targets until we find one scan_targets(); } uint32_t time_now = millis(); uint32_t delta = time_now - g_msc_initiator_state.status_prev_time; if (g_msc_initiator_state.status_interval > 0 && delta > g_msc_initiator_state.status_interval) { if (g_msc_initiator_state.status_reqcount > 0) { logmsg("USB MSC: ", (int)g_msc_initiator_state.status_reqcount, " commands, ", (int)(g_msc_initiator_state.status_bytecount / delta), " kB/s"); } g_msc_initiator_state.status_reqcount = 0; g_msc_initiator_state.status_bytecount = 0; g_msc_initiator_state.status_prev_time = time_now; } platform_poll(); platform_msc_lock_set(true); // Cannot handle new MSC commands while running prefetch if (g_msc_initiator_state.prefetch_sectorcount > 0 && !g_msc_initiator_state.prefetch_done) { LED_ON(); dbgmsg("Prefetch ", (int)g_msc_initiator_state.prefetch_lba, " + ", (int)g_msc_initiator_state.prefetch_sectorcount, "x", (int)g_msc_initiator_state.prefetch_sectorsize); // Read next block while USB is transferring int status = do_read6_or_10(g_msc_initiator_state.prefetch_target_id, g_msc_initiator_state.prefetch_lba, g_msc_initiator_state.prefetch_sectorcount, g_msc_initiator_state.prefetch_sectorsize, g_msc_initiator_state.prefetch_buffer); if (status == 0) { g_msc_initiator_state.prefetch_done = true; } else { logmsg("Prefetch of sector ", g_msc_initiator_state.prefetch_lba, " failed: status ", status); g_msc_initiator_state.prefetch_sectorcount = 0; } LED_OFF(); } platform_msc_lock_set(false); } static int get_target(uint8_t lun) { if (lun >= g_msc_initiator_target_count) { logmsg("Host requested access to non-existing lun ", (int)lun); return 0; } else { return g_msc_initiator_targets[lun].target_id; } } void init_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) { LED_ON(); g_msc_initiator_state.status_reqcount++; int target = get_target(lun); uint8_t response[36] = {0}; bool status = scsiInquiry(target, response); if (!status) { logmsg("SCSI Inquiry to target ", target, " failed"); } memcpy(vendor_id, &response[8], 8); memcpy(product_id, &response[16], 16); memcpy(product_rev, &response[32], 4); LED_OFF(); } uint8_t init_msc_get_maxlun_cb(void) { return g_msc_initiator_target_count; } bool init_msc_is_writable_cb (uint8_t lun) { LED_ON(); g_msc_initiator_state.status_reqcount++; int target = get_target(lun); uint8_t command[6] = {0x1A, 0x08, 0, 0, 4, 0}; // MODE SENSE(6) uint8_t response[4] = {0}; scsiInitiatorRunCommand(target, command, 6, response, 4, NULL, 0); LED_OFF(); return (response[2] & 0x80) == 0; // Check write protected bit } bool init_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) { LED_ON(); g_msc_initiator_state.status_reqcount++; int target = get_target(lun); uint8_t command[6] = {0x1B, 0x1, 0, 0, 0, 0}; uint8_t response[4] = {0}; if (start) { command[4] |= 1; // Start command[1] = 0; // Immediate } if (load_eject) { command[4] |= 2; } command[4] |= power_condition << 4; int status = scsiInitiatorRunCommand(target, command, sizeof(command), response, sizeof(response), NULL, 0); if (status == 2) { uint8_t sense_key; scsiRequestSense(target, &sense_key); logmsg("START STOP UNIT on target ", target, " failed, sense key ", sense_key); } LED_OFF(); return status == 0; } bool init_msc_test_unit_ready_cb(uint8_t lun) { g_msc_initiator_state.status_reqcount++; return scsiTestUnitReady(get_target(lun)); } void init_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) { g_msc_initiator_state.status_reqcount++; uint32_t sectorcount = 0; uint32_t sectorsize = 0; scsiInitiatorReadCapacity(get_target(lun), §orcount, §orsize); *block_count = sectorcount; *block_size = sectorsize; } int32_t init_msc_scsi_cb(uint8_t lun, const uint8_t scsi_cmd[16], void *buffer, uint16_t bufsize) { LED_ON(); g_msc_initiator_state.status_reqcount++; // NOTE: the TinyUSB API around free-form commands is not very good, // this function could need improvement. // Figure out command length static const uint8_t CmdGroupBytes[8] = {6, 10, 10, 6, 16, 12, 6, 6}; // From SCSI2SD int cmdlen = CmdGroupBytes[scsi_cmd[0] >> 5]; int target = get_target(lun); int status = scsiInitiatorRunCommand(target, scsi_cmd, cmdlen, NULL, 0, (const uint8_t*)buffer, bufsize); LED_OFF(); return status; } static int do_read6_or_10(int target_id, uint32_t start_sector, uint32_t sectorcount, uint32_t sectorsize, void *buffer) { int status; // Read6 command supports 21 bit LBA - max of 0x1FFFFF // ref: https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf pg 134 if (start_sector < 0x1FFFFF && sectorcount <= 256) { // Use READ6 command for compatibility with old SCSI1 drives uint8_t command[6] = {0x08, (uint8_t)(start_sector >> 16), (uint8_t)(start_sector >> 8), (uint8_t)start_sector, (uint8_t)sectorcount, 0x00 }; // Note: we must not call platform poll in the commands, status = scsiInitiatorRunCommand(target_id, command, sizeof(command), (uint8_t*)buffer, sectorcount * sectorsize, NULL, 0); } else { // Use READ10 command for larger number of blocks uint8_t command[10] = {0x28, 0x00, (uint8_t)(start_sector >> 24), (uint8_t)(start_sector >> 16), (uint8_t)(start_sector >> 8), (uint8_t)start_sector, 0x00, (uint8_t)(sectorcount >> 8), (uint8_t)(sectorcount), 0x00 }; status = scsiInitiatorRunCommand(target_id, command, sizeof(command), (uint8_t*)buffer, sectorcount * sectorsize, NULL, 0); } return status; } int32_t init_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) { LED_ON(); int status = 0; int target_id = get_target(lun); int sectorsize = g_msc_initiator_targets[lun].sectorsize; uint32_t sectorcount = bufsize / sectorsize; uint32_t total_sectorcount = sectorcount; uint32_t orig_lba = lba; if (sectorcount == 0) { // Not enough buffer left for a full sector return 0; } if (g_msc_initiator_state.prefetch_done) { int32_t offset = (int32_t)lba - (int32_t)g_msc_initiator_state.prefetch_lba; uint8_t *dest = (uint8_t*)buffer; while (offset >= 0 && offset < g_msc_initiator_state.prefetch_sectorcount && sectorcount > 0) { // Copy sectors from prefetch memcpy(dest, g_msc_initiator_state.prefetch_buffer + sectorsize * offset, sectorsize); dest += sectorsize; offset += 1; lba += 1; sectorcount -= 1; } } if (sectorcount > 0) { dbgmsg("USB Read command ", (int)orig_lba, " + ", (int)total_sectorcount, "x", (int)sectorsize, " got ", (int)(total_sectorcount - sectorcount), " sectors from prefetch"); status = do_read6_or_10(target_id, lba, sectorcount, sectorsize, buffer); lba += sectorcount; } else { dbgmsg("USB Read command ", (int)orig_lba, " + ", (int)total_sectorcount, "x", (int)sectorsize, " fully satisfied from prefetch"); } g_msc_initiator_state.status_reqcount++; g_msc_initiator_state.status_bytecount += total_sectorcount * sectorsize; LED_OFF(); if (status != 0) { uint8_t sense_key; scsiRequestSense(target_id, &sense_key); logmsg("SCSI Initiator read failed: ", status, " sense key ", sense_key); return -1; } if (lba + total_sectorcount <= g_msc_initiator_targets[lun].sectorcount) { int prefetch_sectorcount = total_sectorcount; if (prefetch_sectorcount * sectorsize > g_msc_initiator_state.prefetch_bufsize) { prefetch_sectorcount = g_msc_initiator_state.prefetch_bufsize / sectorsize; } // Request prefetch of the next block while USB transfers the previous one g_msc_initiator_state.prefetch_lba = lba; g_msc_initiator_state.prefetch_target_id = target_id; g_msc_initiator_state.prefetch_sectorcount = total_sectorcount; g_msc_initiator_state.prefetch_sectorsize = sectorsize; g_msc_initiator_state.prefetch_done = false; } return total_sectorcount * sectorsize; } int32_t init_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { int status = -1; int target_id = get_target(lun); int sectorsize = g_msc_initiator_targets[lun].sectorsize; uint32_t start_sector = lba; uint32_t sectorcount = bufsize / sectorsize; if (sectorcount == 0) { // Not a complete sector return 0; } LED_ON(); // Write6 command supports 21 bit LBA - max of 0x1FFFFF if (start_sector < 0x1FFFFF && sectorcount <= 256) { // Use WRITE6 command for compatibility with old SCSI1 drives uint8_t command[6] = {0x0A, (uint8_t)(start_sector >> 16), (uint8_t)(start_sector >> 8), (uint8_t)start_sector, (uint8_t)sectorcount, 0x00 }; status = scsiInitiatorRunCommand(target_id, command, sizeof(command), NULL, 0, buffer, bufsize); } else { // Use WRITE10 command for larger number of blocks uint8_t command[10] = {0x2A, 0x00, (uint8_t)(start_sector >> 24), (uint8_t)(start_sector >> 16), (uint8_t)(start_sector >> 8), (uint8_t)start_sector, 0x00, (uint8_t)(sectorcount >> 8), (uint8_t)(sectorcount), 0x00 }; status = scsiInitiatorRunCommand(target_id, command, sizeof(command), NULL, 0, buffer, bufsize); } g_msc_initiator_state.status_reqcount++; g_msc_initiator_state.status_bytecount += sectorcount * sectorsize; LED_OFF(); if (status != 0) { uint8_t sense_key; scsiRequestSense(target_id, &sense_key); logmsg("SCSI Initiator write failed: ", status, " sense key ", sense_key); return -1; } return sectorcount * sectorsize; } void init_msc_write10_complete_cb(uint8_t lun) { (void)lun; } #endif