// Copyright (C) 2013 Michael McMaster // Copyright (C) 2014 Doug Brown // // 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 . #include "device.h" #include "scsi.h" #include "scsiPhy.h" #include "config.h" #include "debug.h" #include "debug.h" #include "disk.h" #include "sd.h" #include "time.h" #include // Global Transfer transfer; // Callback once all data has been read in the data out phase. static void doFormatUnitComplete(void) { // TODO start writing the initialisation pattern to the SD // card 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" configSave( scsiDev.target->cfg->scsiId & CONFIG_TARGET_ID_BITS, scsiDev.target->state.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(); } } static void doReadCapacity() { uint32_t lba = (((uint32) scsiDev.cdb[2]) << 24) + (((uint32) scsiDev.cdb[3]) << 16) + (((uint32) scsiDev.cdb[4]) << 8) + scsiDev.cdb[5]; int pmi = scsiDev.cdb[8] & 1; uint32_t capacity = getScsiCapacity( scsiDev.target->device, scsiDev.target->cfg->sdSectorStart, scsiDev.target->state.bytesPerSector, scsiDev.target->cfg->scsiSectors); 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->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.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->state.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->state.sense.code = NOT_READY; scsiDev.target->state.sense.asc = MEDIUM_NOT_PRESENT; scsiDev.phase = STATUS; } } static void doWrite(uint32 lba, uint32 blocks) { if (unlikely(scsiDev.target->cfg->deviceType == CONFIG_FLOPPY_14MB)) { // Floppies are supposed to be slow. Some systems can't handle a floppy // without an access time CyDelay(10); } uint32_t bytesPerSector = scsiDev.target->state.bytesPerSector; MEDIA_STATE* mediaState = &(scsiDev.target->device->mediaState); if (unlikely(*mediaState & MEDIA_WP) || unlikely(scsiDev.target->cfg->deviceType == CONFIG_OPTICAL) || (scsiDev.target->cfg->storageDevice != CONFIG_STOREDEVICE_SD)) { scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.sense.asc = WRITE_PROTECTED; scsiDev.phase = STATUS; } else if (unlikely(((uint64) lba) + blocks > getScsiCapacity( scsiDev.target->device, scsiDev.target->cfg->sdSectorStart, bytesPerSector, scsiDev.target->cfg->scsiSectors ) )) { scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { transfer.lba = lba; transfer.blocks = blocks; transfer.currentBlock = 0; scsiDev.phase = DATA_OUT; scsiDev.dataLen = bytesPerSector; scsiDev.dataPtr = bytesPerSector; // No need for single-block writes atm. Overhead of the // multi-block write is minimal. transfer.multiBlock = 1; uint32_t sdLBA = SCSISector2SD( scsiDev.target->cfg->sdSectorStart, bytesPerSector, lba); uint32_t sdBlocks = blocks * SDSectorsPerSCSISector(bytesPerSector); sdWriteMultiSectorPrep(sdLBA, sdBlocks); } } static void doRead(uint32 lba, uint32 blocks) { if (unlikely(scsiDev.target->cfg->deviceType == CONFIG_FLOPPY_14MB)) { // Floppies are supposed to be slow. Some systems can't handle a floppy // without an access time CyDelay(10); } uint32_t capacity = getScsiCapacity( scsiDev.target->device, scsiDev.target->cfg->sdSectorStart, scsiDev.target->state.bytesPerSector, scsiDev.target->cfg->scsiSectors); if (unlikely(((uint64) lba) + blocks > capacity)) { scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { transfer.lba = lba; transfer.blocks = blocks; transfer.currentBlock = 0; scsiDev.phase = DATA_IN; scsiDev.dataLen = 0; // No data yet uint32_t bytesPerSector = scsiDev.target->state.bytesPerSector; uint32_t sdSectorPerSCSISector = SDSectorsPerSCSISector(bytesPerSector); uint32_t sdSectors = blocks * sdSectorPerSCSISector; if (( (sdSectors == 1) && !(scsiDev.boardCfg.flags & CONFIG_ENABLE_CACHE) ) || unlikely(((uint64) lba) + blocks == capacity) || (scsiDev.target->cfg->storageDevice != CONFIG_STOREDEVICE_SD) ) { // We get errors on reading the last sector using a multi-sector // read :-( transfer.multiBlock = 0; } else { transfer.multiBlock = 1; uint32_t sdLBA = SCSISector2SD( scsiDev.target->cfg->sdSectorStart, bytesPerSector, lba); sdReadMultiSectorPrep(sdLBA, sdSectors); } } } static void doSeek(uint32 lba) { if (lba >= getScsiCapacity( scsiDev.target->device, scsiDev.target->cfg->sdSectorStart, scsiDev.target->state.bytesPerSector, scsiDev.target->cfg->scsiSectors) ) { scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE; scsiDev.phase = STATUS; } else { CyDelay(10); } } static int doTestUnitReady() { MEDIA_STATE* mediaState = &(scsiDev.target->device->mediaState); int ready = 1; if (likely(*mediaState == (MEDIA_STARTED | MEDIA_PRESENT | MEDIA_INITIALISED))) { // nothing to do. } else if (unlikely(!(*mediaState & MEDIA_STARTED))) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = NOT_READY; scsiDev.target->state.sense.asc = LOGICAL_UNIT_NOT_READY_INITIALIZING_COMMAND_REQUIRED; scsiDev.phase = STATUS; } else if (unlikely(!(*mediaState & MEDIA_PRESENT))) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = NOT_READY; scsiDev.target->state.sense.asc = MEDIUM_NOT_PRESENT; scsiDev.phase = STATUS; } else if (unlikely(!(*mediaState & MEDIA_INITIALISED))) { ready = 0; scsiDev.status = CHECK_CONDITION; scsiDev.target->state.sense.code = NOT_READY; scsiDev.target->state.sense.asc = LOGICAL_UNIT_NOT_READY_CAUSE_NOT_REPORTABLE; scsiDev.phase = STATUS; } return ready; } // Handle direct-access scsi device commands int scsiDiskCommand() { int commandHandled = 1; uint8 command = scsiDev.cdb[0]; if (unlikely(command == 0x1B)) { // START STOP UNIT // Enable or disable media access operations. // Ignore load/eject requests. We can't do that. //int immed = scsiDev.cdb[1] & 1; int start = scsiDev.cdb[4] & 1; MEDIA_STATE* mediaState = &(scsiDev.target->device->mediaState); if (start) { *mediaState = *mediaState | MEDIA_STARTED; if (!(*mediaState & MEDIA_INITIALISED)) { if (*mediaState & MEDIA_PRESENT) { *mediaState = *mediaState | MEDIA_INITIALISED; } } } else { *mediaState &= ~MEDIA_STARTED; } } 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 lba = (((uint32) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; uint32 blocks = scsiDev.cdb[4]; if (unlikely(blocks == 0)) blocks = 256; doRead(lba, blocks); } else if (likely(command == 0x28)) { // READ(10) // Ignore all cache control bits - we don't support a memory cache. uint32 lba = (((uint32) scsiDev.cdb[2]) << 24) + (((uint32) scsiDev.cdb[3]) << 16) + (((uint32) scsiDev.cdb[4]) << 8) + scsiDev.cdb[5]; uint32 blocks = (((uint32) scsiDev.cdb[7]) << 8) + scsiDev.cdb[8]; doRead(lba, blocks); } else if (likely(command == 0x0A)) { // WRITE(6) uint32 lba = (((uint32) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; uint32 blocks = scsiDev.cdb[4]; if (unlikely(blocks == 0)) blocks = 256; doWrite(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 lba = (((uint32) scsiDev.cdb[2]) << 24) + (((uint32) scsiDev.cdb[3]) << 16) + (((uint32) scsiDev.cdb[4]) << 8) + scsiDev.cdb[5]; uint32 blocks = (((uint32) scsiDev.cdb[7]) << 8) + scsiDev.cdb[8]; doWrite(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 lba = (((uint32) scsiDev.cdb[1] & 0x1F) << 16) + (((uint32) scsiDev.cdb[2]) << 8) + scsiDev.cdb[3]; doSeek(lba); } else if (unlikely(command == 0x2B)) { // SEEK(10) uint32 lba = (((uint32) scsiDev.cdb[2]) << 24) + (((uint32) scsiDev.cdb[3]) << 16) + (((uint32) 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->state.sense.code = ILLEGAL_REQUEST; scsiDev.target->state.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 { commandHandled = 0; } return commandHandled; } void scsiDiskPoll() { uint32_t bytesPerSector = scsiDev.target->state.bytesPerSector; if (scsiDev.phase == DATA_IN && transfer.currentBlock != transfer.blocks) { scsiEnterPhase(DATA_IN); int totalSDSectors = transfer.blocks * SDSectorsPerSCSISector(bytesPerSector); uint32_t sdLBA = SCSISector2SD( scsiDev.target->cfg->sdSectorStart, bytesPerSector, transfer.lba); const int sdPerScsi = SDSectorsPerSCSISector(bytesPerSector); int buffers = sizeof(scsiDev.data) / SD_SECTOR_SIZE; int prep = 0; int i = 0; int scsiActive = 0; int sdActive = 0; int isSDDevice = scsiDev.target->cfg->storageDevice == CONFIG_STOREDEVICE_SD; while ((i < totalSDSectors) && likely(scsiDev.phase == DATA_IN) && likely(!scsiDev.resetFlag)) { // Wait for the next DMA interrupt. It's beneficial to halt the // processor to give the DMA controller more memory bandwidth to // work with. int scsiBusy; int sdBusy; { uint8_t intr = CyEnterCriticalSection(); scsiBusy = scsiDMABusy(); sdBusy = isSDDevice && sdDMABusy(); CyExitCriticalSection(intr); } while (scsiBusy && sdBusy && isSDDevice) { uint8_t intr = CyEnterCriticalSection(); scsiBusy = scsiDMABusy(); sdBusy = sdDMABusy(); if (scsiBusy && sdBusy) { __WFI(); } CyExitCriticalSection(intr); } if (isSDDevice) { if (sdActive && !sdBusy && sdReadSectorDMAPoll()) { sdActive = 0; prep++; } } else { S2S_Device* device = scsiDev.target->device; if (sdActive && device->readAsyncPoll(device)) { sdActive = 0; prep++; } } // Usually SD is slower than the SCSI interface. // Prioritise starting the read of the next sector over starting a // SCSI transfer for the last sector // ie. NO "else" HERE. if (!sdActive && (prep - i < buffers) && (prep < totalSDSectors)) { if (isSDDevice) { // Start an SD transfer if we have space. if (transfer.multiBlock) { sdReadMultiSectorDMA(&scsiDev.data[SD_SECTOR_SIZE * (prep % buffers)]); } else { sdReadSingleSectorDMA(sdLBA + prep, &scsiDev.data[SD_SECTOR_SIZE * (prep % buffers)]); } sdActive = 1; } else { // Sync Read onboard flash S2S_Device* device = scsiDev.target->device; device->readAsync(device, sdLBA + prep, 1, &scsiDev.data[SD_SECTOR_SIZE * (prep % buffers)]); sdActive = 1; } } if (scsiActive && !scsiBusy && scsiWriteDMAPoll()) { scsiActive = 0; ++i; } if (!scsiActive && ((prep - i) > 0)) { int dmaBytes = SD_SECTOR_SIZE; if ((i % sdPerScsi) == (sdPerScsi - 1)) { dmaBytes = bytesPerSector % SD_SECTOR_SIZE; if (dmaBytes == 0) dmaBytes = SD_SECTOR_SIZE; } scsiWriteDMA(&scsiDev.data[SD_SECTOR_SIZE * (i % buffers)], dmaBytes); scsiActive = 1; } } if (scsiDev.phase == DATA_IN) { scsiDev.phase = STATUS; } scsiDiskReset(); // Wait for current DMA transfer done then deselect (if reset encountered) if (!isSDDevice) { S2S_Device* device = scsiDev.target->device; while (!device->readAsyncPoll(device)) { } } } else if (scsiDev.phase == DATA_OUT && transfer.currentBlock != transfer.blocks) { scsiEnterPhase(DATA_OUT); const int sdPerScsi = SDSectorsPerSCSISector(bytesPerSector); int totalSDSectors = transfer.blocks * sdPerScsi; int buffers = sizeof(scsiDev.data) / SD_SECTOR_SIZE; int prep = 0; int i = 0; int scsiDisconnected = 0; int scsiComplete = 0; int clearBSY = 0; uint32_t lastActivityTime = getTime_ms(); int scsiActive = 0; int sdActive = 0; while ((i < totalSDSectors) && (likely(scsiDev.phase == DATA_OUT) || // scsiDisconnect keeps our phase. scsiComplete) && likely(!scsiDev.resetFlag)) { // Wait for the next DMA interrupt. It's beneficial to halt the // processor to give the DMA controller more memory bandwidth to // work with. int scsiBusy = 1; int sdBusy = 1; while (scsiBusy && sdBusy) { uint8_t intr = CyEnterCriticalSection(); scsiBusy = scsiDMABusy(); sdBusy = sdDMABusy(); if (scsiBusy && sdBusy) { __WFI(); } CyExitCriticalSection(intr); } if (sdActive && !sdBusy && sdWriteSectorDMAPoll()) { sdActive = 0; i++; } if (!sdActive && ((prep - i) > 0)) { // Start an SD transfer if we have space. sdWriteMultiSectorDMA(&scsiDev.data[SD_SECTOR_SIZE * (i % buffers)]); sdActive = 1; } uint32_t now = getTime_ms(); if (scsiActive && !scsiBusy && scsiReadDMAPoll()) { scsiActive = 0; ++prep; lastActivityTime = now; } if (!scsiActive && ((prep - i) < buffers) && (prep < totalSDSectors) && likely(!scsiDisconnected)) { int dmaBytes = SD_SECTOR_SIZE; if ((prep % sdPerScsi) == (sdPerScsi - 1)) { dmaBytes = bytesPerSector % SD_SECTOR_SIZE; if (dmaBytes == 0) dmaBytes = SD_SECTOR_SIZE; } scsiReadDMA(&scsiDev.data[SD_SECTOR_SIZE * (prep % buffers)], dmaBytes); scsiActive = 1; } else if ( (scsiDev.boardCfg.flags & CONFIG_ENABLE_DISCONNECT) && (scsiActive == 0) && likely(!scsiDisconnected) && unlikely(scsiDev.discPriv) && unlikely(diffTime_ms(lastActivityTime, now) >= 20) && likely(scsiDev.phase == DATA_OUT)) { // We're transferring over the SCSI bus faster than the SD card // can write. There is no more buffer space once we've finished // this SCSI transfer. // The NCR 53C700 interface chips have a 250ms "byte-to-byte" // timeout buffer. SD card writes are supposed to complete // within 200ms, but sometimes they don't. // The NCR 53C700 series is used on HP 9000 workstations. scsiDisconnect(); scsiDisconnected = 1; lastActivityTime = getTime_ms(); } else if (unlikely(scsiDisconnected) && ( (prep == i) || // Buffers empty. // Send some messages every 100ms so we don't timeout. // At a minimum, a reselection involves an IDENTIFY message. unlikely(diffTime_ms(lastActivityTime, now) >= 100) )) { int reconnected = scsiReconnect(); if (reconnected) { scsiDisconnected = 0; lastActivityTime = getTime_ms(); // Don't disconnect immediately. } else if (diffTime_ms(lastActivityTime, getTime_ms()) >= 10000) { // Give up after 10 seconds of trying to reconnect. scsiDev.resetFlag = 1; } } else if ( likely(!scsiComplete) && (sdActive == 1) && (prep == totalSDSectors) && // All scsi data read and buffered likely(!scsiDev.discPriv) && // Prefer disconnect where possible. unlikely(diffTime_ms(lastActivityTime, now) >= 150) && likely(scsiDev.phase == DATA_OUT) && !(scsiDev.cdb[scsiDev.cdbLen - 1] & 0x01) // Not linked command ) { // We're transferring over the SCSI bus faster than the SD card // can write. All data is buffered, and we're just waiting for // the SD card to complete. The host won't let us disconnect. // Some drivers set a 250ms timeout on transfers to complete. // SD card writes are supposed to complete // within 200ms, but sometimes they don'to. // Just pretend we're finished. scsiComplete = 1; process_Status(); clearBSY = process_MessageIn(0); // Will go to BUS_FREE state but keeps BSY asserted } } if (clearBSY) { enter_BusFree(); } while ( !scsiDev.resetFlag && unlikely(scsiDisconnected) && (elapsedTime_ms(lastActivityTime) <= 10000)) { scsiDisconnected = !scsiReconnect(); } if (scsiDisconnected) { // Failed to reconnect scsiDev.resetFlag = 1; } if (scsiDev.phase == DATA_OUT) { if (scsiDev.parityError && (scsiDev.boardCfg.flags & CONFIG_ENABLE_PARITY) && (scsiDev.compatMode >= COMPAT_SCSI2)) { scsiDev.target->state.sense.code = ABORTED_COMMAND; scsiDev.target->state.sense.asc = SCSI_PARITY_ERROR; scsiDev.status = CHECK_CONDITION;; } scsiDev.phase = STATUS; } scsiDiskReset(); } } 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; // Cancel long running commands! if ( ((scsiDev.boardCfg.flags & CONFIG_ENABLE_CACHE) == 0) || (transfer.multiBlock == 0) ) { sdCompleteTransfer(); } transfer.multiBlock = 0; } void scsiDiskInit() { scsiDiskReset(); // WP pin not available for micro-sd // TODO read card WP register #if 0 if (SD_WP_Read()) { blockDev.state = blockDev.state | DISK_WP; } #endif }