diff --git a/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.h b/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.h index c3fcd304..000165a0 100644 --- a/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.h +++ b/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.h @@ -158,6 +158,15 @@ void platform_use_sca_led(void); // Specific error code tied to the MCU when the SD card is not detected uint8_t platform_no_sd_card_on_init_error_code(); +// True only if the given SdCard::errorCode() means the card itself refused to +// store the data. Everything else is a transfer that did not complete, which +// the host should retry rather than treat as a bad sector. +bool platform_sd_error_is_medium_defect(uint8_t sd_error); + +// SdCard::errorCode() value for a transfer that ran out of time. Mirrors +// SDIO_ERR_DATA_TIMEOUT; sd_card_sdio.cpp static_asserts the two agree. +#define SD_ERROR_DATA_TIMEOUT 5 + // Query whether initiator mode is enabled on targets with PLATFORM_HAS_INITIATOR_MODE bool platform_is_initiator_mode_enabled(); diff --git a/lib/BlueSCSI_platform_RP2MCU/rp2040-template.ld b/lib/BlueSCSI_platform_RP2MCU/rp2040-template.ld index 2039b271..bb35aec9 100644 --- a/lib/BlueSCSI_platform_RP2MCU/rp2040-template.ld +++ b/lib/BlueSCSI_platform_RP2MCU/rp2040-template.ld @@ -160,6 +160,11 @@ SECTIONS *(.text*verify_extracted_firmware*) *(.text*crc32_update*) + /* Only reached after an SD transfer has already failed */ + *(.text*diskWriteErrorSense*) + *(.text*sdio_log_data_timeout*) + *(.text*platform_sd_error_is_medium_defect*) + /* BlueSCSI_disk functions that are only used during init */ *BlueSCSI_settings.cpp.o(.text .text*) *QuirksCheck.cpp.o(.text .text*) diff --git a/lib/BlueSCSI_platform_RP2MCU/sd_card_sdio.cpp b/lib/BlueSCSI_platform_RP2MCU/sd_card_sdio.cpp index 3107b59b..7d758aa8 100644 --- a/lib/BlueSCSI_platform_RP2MCU/sd_card_sdio.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/sd_card_sdio.cpp @@ -713,6 +713,17 @@ uint32_t SdioCard::errorLine() const return g_sdio_error_line; } +static_assert(SD_ERROR_DATA_TIMEOUT == SDIO_ERR_DATA_TIMEOUT, + "SD_ERROR_DATA_TIMEOUT must mirror sdio_status_t"); + +bool platform_sd_error_is_medium_defect(uint8_t sd_error) +{ + // SDIO_ERR_WRITE_FAIL is the card answering "I could not store this". + // Timeouts and CRC errors are the transfer failing, and the data on the + // card is untouched. + return sd_error == SDIO_ERR_WRITE_FAIL; +} + bool SdioCard::isBusy() { #if SDIO_D0 > 31 diff --git a/lib/BlueSCSI_platform_RP2MCU/sdio.cpp b/lib/BlueSCSI_platform_RP2MCU/sdio.cpp index 5303a4ce..4ea1c09b 100644 --- a/lib/BlueSCSI_platform_RP2MCU/sdio.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/sdio.cpp @@ -35,6 +35,7 @@ #if defined(SD_USE_SDIO) && !defined(SD_USE_RP2350_SDIO) #include "sdio.h" +#include "sdio_write_response.h" #include #include //#include @@ -67,6 +68,13 @@ // Maximum number of 512 byte blocks to transfer in one request #define SDIO_MAX_BLOCKS 256 +// The SD spec lets a card hold DAT0 busy up to 250ms per block after a write, +// so a deadline that covers a whole multi-block burst fails legitimate cards: +// a 64 block burst is entitled to 16s and used to get 1s in total. Time each +// block separately, and cap the burst only to keep the 15s watchdog clear. +#define SDIO_BLOCK_TIMEOUT_MS 1000 +#define SDIO_BURST_TIMEOUT_MS 4000 + enum sdio_transfer_state_t { SDIO_IDLE, SDIO_RX, SDIO_TX, SDIO_TX_WAIT_IDLE}; static struct { @@ -78,7 +86,8 @@ static struct { pio_sm_config pio_cfg_data_tx; sdio_transfer_state_t transfer_state; - uint32_t transfer_start_time; + uint32_t transfer_start_time; // restarted on every completed block + uint32_t burst_start_time; // start of the whole multi-block transfer uint32_t *data_buf; uint32_t blocks_done; // Number of blocks transferred so far uint32_t total_blocks; // Total number of blocks to transfer @@ -605,6 +614,7 @@ sdio_status_t rp2040_sdio_rx_start(uint8_t *buffer, uint32_t num_blocks, uint32_ g_sdio.transfer_state = SDIO_RX; g_sdio.transfer_start_time = platform_millis(); + g_sdio.burst_start_time = g_sdio.transfer_start_time; g_sdio.data_buf = (uint32_t*)buffer; g_sdio.blocks_done = 0; g_sdio.total_blocks = num_blocks; @@ -714,6 +724,24 @@ static void sdio_verify_rx_checksums(uint32_t maxcount) } } +// Reports a stalled transfer. Kept out of line so the argument marshalling does +// not sit in RAM alongside the poll functions on RP2040. +static void __attribute__((noinline)) sdio_log_data_timeout(const char *which, uint sm, uint32_t pc_offset) +{ + if (!g_record_sdio_errors) return; + + // ST 3 (SDIO_TX_WAIT_IDLE) means the data left and the card is still busy; + // ST 2 (SDIO_TX) or 1 (SDIO_RX) means the block itself never finished. + logmsg(which, " timeout, " + "PIO PC: ", (int)pio_sm_get_pc(SDIO_PIO, sm) - (int)pc_offset, + " RXF: ", (int)pio_sm_get_rx_fifo_level(SDIO_PIO, sm), + " TXF: ", (int)pio_sm_get_tx_fifo_level(SDIO_PIO, sm), + " DMA CNT: ", dma_hw->ch[SDIO_DMA_CH].al2_transfer_count, + " BD: ", g_sdio.blocks_done, + " TB: ", g_sdio.total_blocks, + " ST: ", (int)g_sdio.transfer_state); +} + sdio_status_t rp2040_sdio_rx_poll(uint32_t *bytes_complete) { // Was everything done when the previous rx_poll() finished? @@ -732,7 +760,13 @@ sdio_status_t rp2040_sdio_rx_poll(uint32_t *bytes_complete) // Compute how many complete 512 byte SDIO blocks have been transferred // When transfer ends, dma_ctrl_block_count == g_sdio.total_blocks * 2 + 1 - g_sdio.blocks_done = (dma_ctrl_block_count - 1) / 2; + uint32_t blocks_done = (dma_ctrl_block_count - 1) / 2; + if (blocks_done != g_sdio.blocks_done) + { + // Progress: this block is not the one that is stuck. + g_sdio.transfer_start_time = platform_millis(); + g_sdio.blocks_done = blocks_done; + } // NOTE: When all blocks are done, rx_poll() still returns SDIO_BUSY once. // This provides a chance to start the SCSI transfer before the last checksums @@ -758,14 +792,10 @@ sdio_status_t rp2040_sdio_rx_poll(uint32_t *bytes_complete) return SDIO_ERR_DATA_CRC; } } - else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > 1000) + else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > SDIO_BLOCK_TIMEOUT_MS || + (uint32_t)(platform_millis() - g_sdio.burst_start_time) > SDIO_BURST_TIMEOUT_MS) { - dbgmsg("rp2040_sdio_rx_poll() timeout, " - "PIO PC: ", (int)pio_sm_get_pc(SDIO_PIO, SDIO_DATA_SM) - (int)g_sdio.pio_data_rx_offset, - " RXF: ", (int)pio_sm_get_rx_fifo_level(SDIO_PIO, SDIO_DATA_SM), - " TXF: ", (int)pio_sm_get_tx_fifo_level(SDIO_PIO, SDIO_DATA_SM), - " DMA CNT: ", dma_hw->ch[SDIO_DMA_CH].al2_transfer_count, - " BD: ", g_sdio.blocks_done); + sdio_log_data_timeout("rp2040_sdio_rx_poll()", SDIO_DATA_SM, g_sdio.pio_data_rx_offset); rp2040_sdio_stop(); return SDIO_ERR_DATA_TIMEOUT; } @@ -859,6 +889,7 @@ sdio_status_t rp2040_sdio_tx_start(const uint8_t *buffer, uint32_t num_blocks) g_sdio.transfer_state = SDIO_TX; g_sdio.transfer_start_time = platform_millis(); + g_sdio.burst_start_time = g_sdio.transfer_start_time; g_sdio.data_buf = (uint32_t*)buffer; g_sdio.blocks_done = 0; g_sdio.total_blocks = num_blocks; @@ -883,74 +914,42 @@ sdio_status_t rp2040_sdio_tx_start(const uint8_t *buffer, uint32_t num_blocks) sdio_status_t check_sdio_write_response(uint32_t card_response) { #ifdef ULTRA_SDIO - uint8_t wr_status = card_response & 0xF8; + sdio_write_response_t resp = sdio_classify_write_response_ultra(card_response); #else uint8_t wr_status = card_response & 0x1F; -#endif - // 0 | 3 status bits | 1 + // 0 | 3 status bits | 1 // 0b00101 = data accepted // 0b01011 = CRC error // 0b01101 = Write Error - -#ifdef ULTRA_SDIO - if (wr_status == 0x28 || - (wr_status == 0x50 && g_sdio_cid.mid == 0x41)) // Kensington card behavior is different -#else - if (wr_status == 0b101) + sdio_write_response_t resp = SDIO_WR_UNKNOWN; + if (wr_status == 0b101) resp = SDIO_WR_ACCEPTED; + else if (wr_status == 0b1011) resp = SDIO_WR_CRC_ERROR; + else if (wr_status == 0b1101) resp = SDIO_WR_WRITE_ERROR; #endif + + if (resp == SDIO_WR_ACCEPTED) { return SDIO_OK; } -#ifdef ULTRA_SDIO - else if (wr_status == 0x58) -#else - else if (wr_status == 0b1011) -#endif - { - if (g_record_sdio_errors) { - logmsg("SDIO card reports write CRC error, S: ", card_response, " M: ", (uint8_t)g_sdio.speed_mode); - } -#ifdef SDIO_DEBUG - // Debug Indicate - sio_hw->gpio_hi_set = 0b00100000; - asm volatile ("nop \n nop"); - sio_hw->gpio_hi_clr = 0b11100000; -#endif - return SDIO_ERR_WRITE_CRC; - } -#ifdef ULTRA_SDIO - else if (wr_status == 0x30 || wr_status == 0x68) -#else - else if (wr_status == 0b1101) -#endif + if (g_record_sdio_errors) { - if (g_record_sdio_errors) { + if (resp == SDIO_WR_CRC_ERROR) + logmsg("SDIO card reports write CRC error, S: ", card_response, " M: ", (uint8_t)g_sdio.speed_mode); + else if (resp == SDIO_WR_WRITE_ERROR) logmsg("SDIO card reports write failure, S: ", card_response, " M: ", (uint8_t)g_sdio.speed_mode); - } - -#ifdef SDIO_DEBUG - // Debug Indicate - sio_hw->gpio_hi_set = 0b00100000; - asm volatile ("nop \n nop"); - sio_hw->gpio_hi_clr = 0b11100000; -#endif - return SDIO_ERR_WRITE_FAIL; - } - else - { - if (g_record_sdio_errors) { + else logmsg("SDIO card reports unknown write S: ", card_response, " M: ", (uint8_t)g_sdio.speed_mode); - } + } #ifdef SDIO_DEBUG - // Debug Indicate - sio_hw->gpio_hi_set = 0b00100000; - asm volatile ("nop \n nop"); - sio_hw->gpio_hi_clr = 0b11100000; + // Debug Indicate + sio_hw->gpio_hi_set = 0b00100000; + asm volatile ("nop \n nop"); + sio_hw->gpio_hi_clr = 0b11100000; #endif - return SDIO_ERR_WRITE_FAIL; - } + + return (resp == SDIO_WR_CRC_ERROR) ? SDIO_ERR_WRITE_CRC : SDIO_ERR_WRITE_FAIL; } // When a block finishes, this IRQ handler starts the next one @@ -997,6 +996,7 @@ static void rp2040_sdio_tx_irq() } g_sdio.blocks_done++; + g_sdio.transfer_start_time = platform_millis(); if (g_sdio.blocks_done < g_sdio.total_blocks) { sdio_start_next_block_tx(); @@ -1037,13 +1037,10 @@ sdio_status_t rp2040_sdio_tx_poll(uint32_t *bytes_complete) rp2040_sdio_stop(); return g_sdio.wr_status; } - else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > 1000) + else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > SDIO_BLOCK_TIMEOUT_MS || + (uint32_t)(platform_millis() - g_sdio.burst_start_time) > SDIO_BURST_TIMEOUT_MS) { - dbgmsg("rp2040_sdio_tx_poll() timeout, " - "PIO PC: ", (int)pio_sm_get_pc(SDIO_PIO, SDIO_CMD_SM) - (int)g_sdio.pio_data_tx_offset, - " RXF: ", (int)pio_sm_get_rx_fifo_level(SDIO_PIO, SDIO_CMD_SM), - " TXF: ", (int)pio_sm_get_tx_fifo_level(SDIO_PIO, SDIO_CMD_SM), - " DMA CNT: ", dma_hw->ch[SDIO_DMA_CH].al2_transfer_count); + sdio_log_data_timeout("rp2040_sdio_tx_poll()", SDIO_CMD_SM, g_sdio.pio_data_tx_offset); rp2040_sdio_stop(); return SDIO_ERR_DATA_TIMEOUT; diff --git a/lib/BlueSCSI_platform_RP2MCU/sdio_write_response.h b/lib/BlueSCSI_platform_RP2MCU/sdio_write_response.h new file mode 100644 index 00000000..0351b920 --- /dev/null +++ b/lib/BlueSCSI_platform_RP2MCU/sdio_write_response.h @@ -0,0 +1,60 @@ +/** + * BlueSCSI™ - Copyright (c) 2026 Eric Helgeson + * + * BlueSCSI™ firmware is licensed under the GPL version 3 or any later version. + * + * https://www.gnu.org/licenses/gpl-3.0.html + * ---- + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +// Decoding of the SD data response token the card drives on DAT0 after each +// written block. Kept free of SDK dependencies so it can be unit tested. + +#pragma once + +#include + +enum sdio_write_response_t { + SDIO_WR_ACCEPTED, + SDIO_WR_CRC_ERROR, + SDIO_WR_WRITE_ERROR, + SDIO_WR_UNKNOWN +}; + +// The token is five bits, 0 s s s 1: +// 0b00101 accepted, 0b01011 CRC error, 0b01101 write error. +// The Ultra PIO shifts it into an 8 bit register, so an aligned token lands in +// bits 7:3 - 0x28, 0x58, 0x68. +// +// The crc_wait loop in sdio_tx_w_clock polls DAT0 for the start bit while it +// clocks. Cards whose output delay puts that edge past the sample point make +// the loop run one more time, and the token is shifted a bit left - 0x50, +// 0xB0, 0xD0. Those values cannot occur aligned, since an aligned token always +// begins with a zero start bit and ends with a one, so decoding both forms is +// unambiguous. Seen on Kensington (MID 0x41) and Phison SD128 (MID 0x27). +static inline sdio_write_response_t sdio_classify_write_response_ultra(uint32_t card_response) +{ + switch (card_response & 0xF8) + { + case 0x28: + case 0x50: return SDIO_WR_ACCEPTED; + case 0x58: + case 0xB0: return SDIO_WR_CRC_ERROR; + case 0x30: + case 0x68: + case 0xD0: return SDIO_WR_WRITE_ERROR; + default: return SDIO_WR_UNKNOWN; + } +} diff --git a/src/BlueSCSI_disk.cpp b/src/BlueSCSI_disk.cpp index 98d11a3f..a4206896 100644 --- a/src/BlueSCSI_disk.cpp +++ b/src/BlueSCSI_disk.cpp @@ -2183,6 +2183,27 @@ static void diskSpecialDataOutStop() scsiDev.dataLen = 0; } +// Pick the sense for a failed SD write. Only a card that refused the data is a +// medium defect; 0x0C02 tells the host the sector is dead and unreallocatable, +// so it retires the block instead of retrying. A transfer that timed out or +// failed CRC has damaged nothing and belongs under ABORTED COMMAND, which hosts +// do retry. +void diskWriteErrorSense(uint8_t sd_error, uint8_t *sense_key, uint16_t *asc) +{ + if (platform_sd_error_is_medium_defect(sd_error)) + { + *sense_key = MEDIUM_ERROR; + *asc = WRITE_ERROR_AUTO_REALLOCATION_FAILED; + } + else + { + *sense_key = ABORTED_COMMAND; + *asc = (sd_error == SD_ERROR_DATA_TIMEOUT) + ? LOGICAL_UNIT_COMMUNICATION_TIMEOUT + : LOGICAL_UNIT_COMMUNICATION_FAILURE; + } +} + static void diskSpecialDataOutError(uint8_t sense_code, uint16_t asc) { diskSpecialDataOutStop(); @@ -2504,9 +2525,13 @@ static void diskWriteVerifyDataOut() if (img.file.write(writeBuffer, chunkBytes) != chunkBytes) { + uint8_t sd_error = SD.card()->errorCode(); + uint8_t sense_key; + uint16_t asc; + diskWriteErrorSense(sd_error, &sense_key, &asc); logmsg("SD card write failed during WRITE AND VERIFY at sector ", (int)chunkLba, - " SCSI ID", (int)scsiDev.target->targetId, " error ", SD.sdErrorCode()); - diskSpecialDataOutError(MEDIUM_ERROR, WRITE_ERROR_AUTO_REALLOCATION_FAILED); + " SCSI ID", (int)scsiDev.target->targetId, " error ", (int)sd_error); + diskSpecialDataOutError(sense_key, asc); return; } @@ -2522,7 +2547,7 @@ static void diskWriteVerifyDataOut() if (img.file.read(verifyBuffer, chunkBytes) != chunkBytes) { logmsg("SD card read failed during WRITE AND VERIFY at sector ", (int)chunkLba, - " SCSI ID", (int)scsiDev.target->targetId, " error ", SD.sdErrorCode()); + " SCSI ID", (int)scsiDev.target->targetId, " error ", (int)SD.card()->errorCode()); diskSpecialDataOutError(MEDIUM_ERROR, UNRECOVERED_READ_ERROR); return; } @@ -2712,10 +2737,14 @@ void diskDataOut() platform_set_sd_callback(&diskDataOut_callback, buf); if (img.file.write(buf, len) != len) { - logmsg("SD card write failed: ", SD.sdErrorCode()); + uint8_t sd_error = SD.card()->errorCode(); + uint8_t sense_key; + uint16_t asc; + diskWriteErrorSense(sd_error, &sense_key, &asc); + logmsg("SD card write failed: ", (int)sd_error); scsiDev.status = CHECK_CONDITION; - scsiDev.target->sense.code = MEDIUM_ERROR; - scsiDev.target->sense.asc = WRITE_ERROR_AUTO_REALLOCATION_FAILED; + scsiDev.target->sense.code = sense_key; + scsiDev.target->sense.asc = asc; scsiDev.phase = STATUS; } platform_set_sd_callback(NULL, NULL); diff --git a/src/BlueSCSI_disk.h b/src/BlueSCSI_disk.h index 3e5b4ce8..e088b3b8 100644 --- a/src/BlueSCSI_disk.h +++ b/src/BlueSCSI_disk.h @@ -205,4 +205,8 @@ char scsiEncodeID(uint8_t scsi_id); // Decode a filename ID character ('0'-'9'/'A'-'F') to 0..15, or -1. int scsiDecodeID(char c); + +// Maps an SdCard::errorCode() to the sense a failed write should report. +void diskWriteErrorSense(uint8_t sd_error, uint8_t *sense_key, uint16_t *asc); + #endif /* BLUESCSI_DISK_H */