From 70e11f31a3419cb1631c247084f18f606f37c17f Mon Sep 17 00:00:00 2001 From: James Smith Date: Sat, 24 Oct 2026 22:04:31 -0700 Subject: [PATCH] Add support for card erase --- README.md | 1 + include/tinysd/card.h | 22 ++++++-- include/tinysd/commands.h | 41 +++++++++++++++ include/tinysd/sdio.h | 2 +- src/card_init.c | 2 +- src/card_io.c | 103 ++++++++++++++++++++++++++++++++------ src/sdhci.c | 4 +- src/sdio_io.c | 2 +- test/test_card.c | 29 ++++++++++- 9 files changed, 179 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4728139..3e8c691 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ For example, when using FatFs: | `disk_read` | `tsd_card_read` | | `disk_write` | `tsd_card_write` | | `CTRL_SYNC` in `disk_ioctl` | `tsd_card_sync` | +| `CTRL_TRIM` in `disk_ioctl` | `tsd_card_erase` | | `GET_SECTOR_SIZE` in `disk_ioctl` | `TSD_CARD_BLOCK_SIZE` | | `GET_SECTOR_COUNT` in `disk_ioctl` | `card.blocks` | diff --git a/include/tinysd/card.h b/include/tinysd/card.h index 5dae84b..695a498 100644 --- a/include/tinysd/card.h +++ b/include/tinysd/card.h @@ -33,7 +33,7 @@ struct tsd_card { }; /** - * Bring a card up at a bus width and clock. + * Initialize a card at a bus width and clock. * * @param card the card * @param host the host the card sits on, started by the caller @@ -44,8 +44,10 @@ struct tsd_card { int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, uint32_t clock_khz); /** - * Read blocks, leaving the read open for the next call to continue on a host that offers continue_read. Call - * tsd_card_sync to end it. + * Read blocks. + * + * On a host that offers continue_read, the read stays open for the next read to continue. A write, an erase, a read + * elsewhere or tsd_card_sync ends it. * * @param card the card * @param buffer receives count blocks, aligned as the host requires @@ -56,7 +58,7 @@ int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, u int tsd_card_read(struct tsd_card *card, void *buffer, uint32_t block, uint32_t count); /** - * Write blocks, all within one call, ending any open read first. + * Write blocks. * * @param card the card * @param buffer count blocks to write, aligned as the host requires @@ -67,7 +69,17 @@ int tsd_card_read(struct tsd_card *card, void *buffer, uint32_t block, uint32_t int tsd_card_write(struct tsd_card *card, const void *buffer, uint32_t block, uint32_t count); /** - * End any open read, leaving the card idle, such as before a power down or for FatFs's CTRL_SYNC. + * Erase blocks. + * + * @param card the card + * @param block the first block + * @param count the number of blocks + * @return 0 or a negative tsd_error + */ +int tsd_card_erase(struct tsd_card *card, uint32_t block, uint32_t count); + +/** + * End any open read. * * @param card the card * @return 0 or a negative tsd_error diff --git a/include/tinysd/commands.h b/include/tinysd/commands.h index 1141b3e..1b36e02 100644 --- a/include/tinysd/commands.h +++ b/include/tinysd/commands.h @@ -69,6 +69,38 @@ /** ACMD6 argument for a 4-bit bus. */ #define TSD_ACMD6_BUS_WIDTH_4BIT 0x2u +/** + * Current state values in the card status, from Table 4-42 of the SD specification. + */ +enum tsd_status_state { + /** Idle State. */ + TSD_STATUS_STATE_IDLE = 0, + + /** Ready State. */ + TSD_STATUS_STATE_READY = 1, + + /** Identification State. */ + TSD_STATUS_STATE_IDENT = 2, + + /** Stand-by State. */ + TSD_STATUS_STATE_STBY = 3, + + /** Transfer State. */ + TSD_STATUS_STATE_TRAN = 4, + + /** Sending-data State. */ + TSD_STATUS_STATE_DATA = 5, + + /** Receive-data State. */ + TSD_STATUS_STATE_RCV = 6, + + /** Programming State. */ + TSD_STATUS_STATE_PRG = 7, + + /** Disconnect State. */ + TSD_STATUS_STATE_DIS = 8, +}; + /** * Command indexes, named as in the SD specification. */ @@ -118,6 +150,15 @@ enum tsd_command_index { /** Write blocks until CMD12. */ TSD_CMD_WRITE_MULTIPLE_BLOCK = 25, + /** Set the first block to erase. */ + TSD_CMD_ERASE_WR_BLK_START = 32, + + /** Set the last block to erase. */ + TSD_CMD_ERASE_WR_BLK_END = 33, + + /** Erase the blocks CMD32 and CMD33 set. */ + TSD_CMD_ERASE = 38, + /** Read or write one register byte of an SDIO card. */ TSD_CMD_IO_RW_DIRECT = 52, diff --git a/include/tinysd/sdio.h b/include/tinysd/sdio.h index 7c4ae95..58826ac 100644 --- a/include/tinysd/sdio.h +++ b/include/tinysd/sdio.h @@ -127,7 +127,7 @@ struct tsd_sdio { int tsd_sdio_identify(struct tsd_sdio *card, struct tsd_host *host); /** - * Bring a card up at a bus width and clock. + * Initialize a card at a bus width and clock. * * @param card the card * @param host the host the card sits on, started by the caller diff --git a/src/card_init.c b/src/card_init.c index a13760d..7ddd664 100644 --- a/src/card_init.c +++ b/src/card_init.c @@ -195,7 +195,7 @@ int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, u if (ret < 0) return ret; - // Select the card by its address, taking an R1 since it has nothing to be busy with, like Linux and Zephyr + // Select the card by its address, taking an R1 since it has nothing to be busy with ret = tsd_host_send_command(card->host, TSD_CMD_SELECT_CARD, card->address << TSD_RCA_SHIFT, TSD_RESPONSE_R1, NULL); if (ret < 0) return ret; diff --git a/src/card_io.c b/src/card_io.c index 2fa82de..476a438 100644 --- a/src/card_io.c +++ b/src/card_io.c @@ -4,15 +4,37 @@ #include #include #include +#include #include // next_block when no read has come before #define NO_BLOCK UINT32_MAX -// Status errors that mark a failed write, leaving out the range error an open write may raise -static const uint32_t write_errors = TSD_STATUS_ADDRESS_ERROR | TSD_STATUS_BLOCK_LEN_ERROR | TSD_STATUS_WP_VIOLATION | - TSD_STATUS_CARD_ECC_FAILED | TSD_STATUS_CC_ERROR | TSD_STATUS_ERROR; +// Longest wait for the card to finish a write, in milliseconds +#define WRITE_TIMEOUT_MS 10000 + +// Time the card may take to erase each block, in milliseconds, from the SD specification +#define ERASE_TIMEOUT_PER_BLOCK_MS 250 + +// Delay between CMD13 tries while the card is busy, in microseconds +#define READY_DELAY_US 125 + +// Status errors that mark a failed write +#define WRITE_ERRORS \ + (TSD_STATUS_ADDRESS_ERROR | \ + TSD_STATUS_BLOCK_LEN_ERROR | \ + TSD_STATUS_WP_VIOLATION | \ + TSD_STATUS_CARD_ECC_FAILED | \ + TSD_STATUS_CC_ERROR | \ + TSD_STATUS_ERROR) + +// Status errors that mark a failed erase +#define ERASE_ERRORS \ + (WRITE_ERRORS | \ + TSD_STATUS_ERASE_SEQ_ERROR | \ + TSD_STATUS_ERASE_PARAM | \ + TSD_STATUS_WP_ERASE_SKIP) // Address a block by number on a high capacity card and by byte offset on the others static uint32_t block_address(struct tsd_card *card, uint32_t block) @@ -61,25 +83,38 @@ static int transfer_blocks(struct tsd_card *card, void *buffer, uint32_t block, ret = stop_ret; // Report an error the card flagged while it took the blocks of a write - if (ret == 0 && write && (status & write_errors)) + if (ret == 0 && write && (status & WRITE_ERRORS)) ret = TSD_ERR_CARD; } return ret; } -// Ask the card for its status with CMD13 and report an error it flags -static int check_status(struct tsd_card *card) +// Poll the card status until it is ready for data +static int wait_ready(struct tsd_card *card, uint32_t errors, uint64_t timeout_us) { - // Ask the card for its status uint32_t address = card->address << TSD_RCA_SHIFT; - uint32_t status = 0; - int ret = tsd_host_send_command(card->host, TSD_CMD_SEND_STATUS, address, TSD_RESPONSE_R1, &status); - if (ret < 0) - return ret; + for (uint64_t waited_us = 0; waited_us < timeout_us; waited_us += READY_DELAY_US) { + // Ask the card for its status + uint32_t status = 0; + int ret = tsd_host_send_command(card->host, TSD_CMD_SEND_STATUS, address, TSD_RESPONSE_R1, &status); + if (ret < 0) + return ret; - // Report an error bit the card set - return status & write_errors ? TSD_ERR_CARD : 0; + // Report an error bit the card set + if (status & errors) + return TSD_ERR_CARD; + + // Finish once the card is ready for data in the transfer state + uint32_t state = TSD_FIELD_GET(TSD_STATUS_CURRENT_STATE, status); + if ((status & TSD_STATUS_READY_FOR_DATA) && state == TSD_STATUS_STATE_TRAN) + return 0; + + // Wait before asking again + tsd_delay_us(READY_DELAY_US); + } + + return TSD_ERR_TIMEOUT; } // End an open read with CMD12, which has no busy after a read, leaving the card ready for another command @@ -167,16 +202,52 @@ int tsd_card_write(struct tsd_card *card, const void *buffer, uint32_t block, ui // Forget where a read would follow on, since the write comes between card->next_block = NO_BLOCK; - // End an open read, since the card takes no other command while it sends + // End an open read int ret = stop_read(card); if (ret < 0) return ret; - // Write the blocks, then check the card's status for an error from programming them + // Write the blocks, then wait for the card to finish programming them ret = transfer_blocks(card, (void *)buffer, block, count, true); if (ret < 0) return ret; - return check_status(card); + + return wait_ready(card, WRITE_ERRORS, WRITE_TIMEOUT_MS * 1000); +} + +int tsd_card_erase(struct tsd_card *card, uint32_t block, uint32_t count) +{ + // Refuse an empty erase or one past the end of the card + if (count == 0 || block >= card->blocks || count > card->blocks - block) + return TSD_ERR_PARAM; + + // Forget where a read would follow on, since the erase comes between + card->next_block = NO_BLOCK; + + // End an open read + int ret = stop_read(card); + if (ret < 0) + return ret; + + // Set the first block + uint32_t first = block_address(card, block); + ret = tsd_host_send_command(card->host, TSD_CMD_ERASE_WR_BLK_START, first, TSD_RESPONSE_R1, NULL); + if (ret < 0) + return ret; + + // Set the last block + uint32_t last = block_address(card, block + count - 1); + ret = tsd_host_send_command(card->host, TSD_CMD_ERASE_WR_BLK_END, last, TSD_RESPONSE_R1, NULL); + if (ret < 0) + return ret; + + // Erase them, taking an R1 since a long erase can outlast the host's busy wait + ret = tsd_host_send_command(card->host, TSD_CMD_ERASE, 0, TSD_RESPONSE_R1, NULL); + if (ret < 0) + return ret; + + // Wait for the card to finish + return wait_ready(card, ERASE_ERRORS, (uint64_t)count * ERASE_TIMEOUT_PER_BLOCK_MS * 1000); } int tsd_card_sync(struct tsd_card *card) diff --git a/src/sdhci.c b/src/sdhci.c index e980647..9ad6c76 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -39,7 +39,7 @@ // SDMA buffer boundary, which a buffer must not cross #define DMA_BOUNDARY (4096u << SDMA_BOUNDARY_512K) -// Delay between status reads while a wait polls, in microseconds, like Linux SDHCI +// Delay between status reads while a wait polls, in microseconds #define WAIT_STEP_US 10 // Longest wait for a command phase, in milliseconds @@ -610,7 +610,7 @@ static int run_command(struct tsd_sdhci *sdhci, struct tsd_command *command, str // Wait for the previous command to leave the command line int ret = wait_register(sdhci, REG_PRESENT_STATE, STATE_CMD_INHIBIT, false, COMMAND_TIMEOUT_MS, NULL); - // Wait for the data lines too, which stay busy while the card programs a write, like Zephyr's TI driver + // Wait for the data lines too, which stay busy while the card programs a write if (ret == 0) ret = wait_register(sdhci, REG_PRESENT_STATE, STATE_DAT_INHIBIT, false, DATA_TIMEOUT_MS, NULL); if (ret < 0) { diff --git a/src/sdio_io.c b/src/sdio_io.c index 86f8e16..ee2877f 100644 --- a/src/sdio_io.c +++ b/src/sdio_io.c @@ -24,7 +24,7 @@ // Highest function number the argument's 3-bit field holds #define FUNCTION_MAX 7 -// Turn the error flags of an R5 into a tsd_error, like Linux +// Turn the error flags of an R5 into a tsd_error static int check_response(uint32_t response) { // Report a general error first, then a bad function or argument diff --git a/test/test_card.c b/test/test_card.c index 6750102..d6a8849 100644 --- a/test/test_card.c +++ b/test/test_card.c @@ -48,6 +48,10 @@ void setUp(void) host.replies[TSD_CMD_SEND_IF_COND].response[0] = 0x1aa; host.replies[TSD_ACMD_SD_SEND_OP_COND].response[0] = TSD_OCR_READY | TSD_OCR_CCS; host.replies[TSD_CMD_SEND_RELATIVE_ADDR].response[0] = 0x00010000; + + // Script a card ready for data in the transfer state, for the CMD13 after a write or an erase + host.replies[TSD_CMD_SEND_STATUS].response[0] = + TSD_STATUS_READY_FOR_DATA | TSD_FIELD_PREP(TSD_STATUS_CURRENT_STATE, TSD_STATUS_STATE_TRAN); } void tearDown(void) @@ -144,7 +148,27 @@ static void test_multiple_block_write(void) TEST_ASSERT_EQUAL_INT(TSD_ERR_CARD, tsd_card_write(&card, buffer, 0, 4)); } -// Test that a read or write past the end is refused without a command +// Test that an erase sets its first and last blocks, then erases without a busy +static void test_erase(void) +{ + init_high_capacity(); + + TEST_ASSERT_EQUAL_INT(0, tsd_card_erase(&card, 8, 8)); + TEST_ASSERT_EQUAL_UINT32(8, harness_find(TSD_CMD_ERASE_WR_BLK_START)->argument); + TEST_ASSERT_EQUAL_UINT32(15, harness_find(TSD_CMD_ERASE_WR_BLK_END)->argument); + TEST_ASSERT_EQUAL_INT(TSD_RESPONSE_R1, harness_find(TSD_CMD_ERASE)->response_type); +} + +// Test that an erase reports an error bit the card sets once it finishes +static void test_erase_error(void) +{ + init_high_capacity(); + host.replies[TSD_CMD_SEND_STATUS].response[0] = TSD_STATUS_ERASE_PARAM; + + TEST_ASSERT_EQUAL_INT(TSD_ERR_CARD, tsd_card_erase(&card, 8, 8)); +} + +// Test that a read, write or erase past the end is refused without a command static void test_past_the_end(void) { init_high_capacity(); @@ -152,6 +176,7 @@ static void test_past_the_end(void) TEST_ASSERT_EQUAL_INT(TSD_ERR_PARAM, tsd_card_read(&card, buffer, card.blocks - 1, 2)); TEST_ASSERT_EQUAL_INT(TSD_ERR_PARAM, tsd_card_write(&card, buffer, card.blocks, 1)); + TEST_ASSERT_EQUAL_INT(TSD_ERR_PARAM, tsd_card_erase(&card, card.blocks - 1, 2)); TEST_ASSERT_EQUAL_INT(sent, host.command_count); } @@ -168,6 +193,8 @@ int main(int argc, char **argv) RUN_TEST(test_read_high_capacity_by_block); RUN_TEST(test_open_read); RUN_TEST(test_multiple_block_write); + RUN_TEST(test_erase); + RUN_TEST(test_erase_error); RUN_TEST(test_past_the_end); return UNITY_END();