diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ec4d56..b2a7af9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,12 @@ add_library(tinysd_sdhci INTERFACE) target_sources(tinysd_sdhci INTERFACE src/host/sdhci.c) target_include_directories(tinysd_sdhci INTERFACE include) +# SPI host driver target +add_library(tinysd_spi INTERFACE) +target_sources(tinysd_spi INTERFACE src/host/spi.c) +target_include_directories(tinysd_spi INTERFACE include) +target_link_libraries(tinysd_spi INTERFACE tinysd_frame) + # Frame and CRC helpers target, for software hosts add_library(tinysd_frame INTERFACE) target_sources(tinysd_frame INTERFACE src/frame.c src/crc.c) @@ -26,7 +32,10 @@ target_include_directories(tinysd_frame INTERFACE include) # The whole library add_library(tinysd INTERFACE) -target_link_libraries(tinysd INTERFACE tinysd_card tinysd_sdio tinysd_sdhci tinysd_frame) +target_link_libraries( + tinysd + INTERFACE tinysd_card tinysd_sdio tinysd_sdhci tinysd_spi tinysd_frame +) # Build tests by default only when tinysd is the top-level project option(TINYSD_BUILD_TESTS "Build tinysd tests" ${PROJECT_IS_TOP_LEVEL}) diff --git a/README.md b/README.md index 551197e..9c55f6a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Dependency-free C library for SD cards and SDIO on embedded and bare-metal syste - SD memory card driver (SDSC, SDHC and SDXC) with high speed support - SDIO card driver with byte and block transfers, card interrupts, and high speed support - SDHCI host driver for SD Host Controller versions 1 to 3 +- SPI host driver for SD memory cards on any SPI peripheral - Simple interface for adding your own SD hosts - CRC and framing helpers for software hosts (bit-banged, PIO, etc) - Polling API, no threads, callbacks or interrupts @@ -51,7 +52,7 @@ void tsd_delay_us(uint32_t microseconds) ### Initializing a Host -A host drives the SD bus, and the card drivers talk to it through `struct tsd_host`. +A host drives the SD bus, and the card drivers talk to it through `struct tsd_host`. TinySD has built-in hosts for SDHCI controllers and SPI buses, or you can write your own. For example, if using the built-in SDHCI host controller: @@ -66,11 +67,11 @@ int ret = tsd_sdhci_init(&sdhci, config); struct tsd_host *host = TSD_HOST(&sdhci); ``` -For other hosts, see [Writing Your Own Host](#writing-your-own-host). +See [Hosts](#hosts) for how to set up each built-in host, and how to write your own. ### Using SD Memory Cards -Bring the card up at the bus width and highest clock you want. It runs slower if the card cannot keep up. +Bring the card up on 1 or 4 data lines, at the highest clock you want in kHz, such as 50000 for 50 MHz. A card without high speed runs at 25 MHz instead. An SPI host has one data line, so pass 1. ```c static struct tsd_card card; @@ -87,9 +88,9 @@ ret = tsd_card_write(&card, buffer, 0, 1); ret = tsd_card_sync(&card); ``` -### Bring Your Own Filesystem +#### Bring Your Own Filesystem -TinySD reads and writes raw blocks, so it is usually paired with a filesystem such as [FatFs](https://elm-chan.org/fsw/ff/), [littlefs](https://github.com/littlefs-project/littlefs), etc. +TinySD reads and writes raw blocks, so it is usually paired with a filesystem such as ChaN's [FatFs](https://elm-chan.org/fsw/ff/), [littlefs](https://github.com/littlefs-project/littlefs), etc. For example, when using FatFs: @@ -103,7 +104,6 @@ For example, when using FatFs: | `GET_SECTOR_SIZE` in `disk_ioctl` | `TSD_CARD_BLOCK_SIZE` | | `GET_SECTOR_COUNT` in `disk_ioctl` | `card.blocks` | - ### Using SDIO Devices Bring the card up, then enable the function your device uses and set its block size. @@ -127,13 +127,56 @@ ret = tsd_sdio_transfer(&sdio, 1, 0x100, false, buffer, sizeof(buffer), 64, NULL After init, the ids from the card's CIS are in `sdio.cis`. To read them without bringing the card up, call `tsd_sdio_identify`, which fills in a `struct tsd_sdio_cis` and leaves the card for another driver. -## Running from RAM +## Hosts -To speed up transfers, or to keep them running while flash is unavailable, place `card_io.c`, `sdio_io.c`, `frame.c` and `crc.c` in RAM from your linker script, along with their read-only data. Everything they call must be in RAM too, including your host's operations, `tsd_delay_us` and compiler helpers such as `memcpy`. +### SDHCI + +The SDHCI host drives an SD Host Controller, versions 1 to 3, through its registers. + +```c +static struct tsd_sdhci sdhci; + +struct tsd_sdhci_config config = { + // Register base address, as the CPU sees it + .base = SDHCI_BASE, + + // Map buffers for DMA, or leave these NULL to move data through the FIFO + .dma_map = my_dma_map, + .dma_unmap = my_dma_unmap, +}; +int ret = tsd_sdhci_init(&sdhci, config); +``` + +### SPI + +The SPI host runs a memory card in SPI mode, through four functions you provide. SDIO is not supported on SPI hosts. -## Writing Your Own Host +```c +static struct tsd_spi spi; + +struct tsd_spi_config config = { + // Send bytes and ignore what comes back + .write = my_spi_write, + + // Receive bytes while sending 0xff + .read = my_spi_read, + + // Drive chip select low to select the card, or high to release it + .select = my_spi_select, + + // Run the clock at the highest rate at or below a limit in kHz + .set_clock = my_spi_set_clock, +}; + +// Start the host, which clocks the card awake at 400 kHz +int ret = tsd_spi_init(&spi, config); +``` + +For wiring, see ChaN's excellent [How to Use MMC/SDC](https://elm-chan.org/docs/mmc/mmc_e.html) guide to SD cards in SPI mode. It covers the pinout, the pull-ups and the power a card needs. -For systems without an SDHCI controller, you can write your own host, such as one that bit-bangs the bus or drives it with PIO. Hosts like these can build and check frames with `frame.h` and `crc.h`. +### Your Own Host + +For systems without an SDHCI controller or an SPI peripheral, you can write your own host, such as one that bit-bangs the bus or drives it with PIO. Hosts like these can build and check frames with `frame.h` and `crc.h`. Make a struct that embeds `struct tsd_host` as its first field, and point it at a table of your host's operations. @@ -146,6 +189,7 @@ static const struct tsd_host_api my_api = { .request = my_request, .set_clock = my_set_clock, .set_bus_width = my_set_bus_width, + .card_busy = my_card_busy, }; static struct my_host my_host = {.base.api = &my_api}; @@ -153,6 +197,10 @@ static struct my_host my_host = {.base.api = &my_api}; Then pass `TSD_HOST(&my_host)` to the card drivers. +## Running from RAM + +To speed up transfers, or to keep them running while flash is unavailable, place `card_io.c`, `sdio_io.c`, `frame.c` and `crc.c` in RAM from your linker script, along with their read-only data. Everything they call must be in RAM too, including your host's operations, `tsd_delay_us` and compiler helpers such as `memcpy`. + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/include/tinysd.h b/include/tinysd.h index 0db4001..c561ab4 100644 --- a/include/tinysd.h +++ b/include/tinysd.h @@ -10,4 +10,5 @@ #include #include #include +#include #include diff --git a/include/tinysd/card.h b/include/tinysd/card.h index 695a498..362da99 100644 --- a/include/tinysd/card.h +++ b/include/tinysd/card.h @@ -37,7 +37,7 @@ struct tsd_card { * * @param card the card * @param host the host the card sits on, started by the caller - * @param width the bus width, 1 or 4 + * @param width the bus width, 1 or 4, and 1 on an SPI host * @param clock_khz the highest clock in kHz, held to 25000 for a card without high speed * @return 0 or a negative tsd_error */ diff --git a/include/tinysd/commands.h b/include/tinysd/commands.h index 61ca656..39dfd17 100644 --- a/include/tinysd/commands.h +++ b/include/tinysd/commands.h @@ -77,6 +77,9 @@ #define TSD_IF_COND_VOLTAGE_2_7_3_6V 1 #define TSD_IF_COND_CHECK_PATTERN 0xaa +/** Bytes in the CSD, which SPI mode reads as a data block. */ +#define TSD_CSD_LEN 16 + /** CSD structure, in response word 0, which holds CSD bits 127 to 96. */ #define TSD_CSD_STRUCTURE TSD_BITS(31, 30) diff --git a/include/tinysd/host.h b/include/tinysd/host.h index 33e26d4..8f91c52 100644 --- a/include/tinysd/host.h +++ b/include/tinysd/host.h @@ -115,6 +115,14 @@ struct tsd_host_api { */ void (*set_bus_width)(struct tsd_host *host, uint8_t width); + /** + * Report whether the card holds its data line low while it programs or erases. + * + * @param host the host + * @return 1 while the card is busy, 0 once it is free, or a negative tsd_error + */ + int (*card_busy)(struct tsd_host *host); + /** * Enable or disable the card interrupt. Optional, and NULL for a host without a card interrupt. * @@ -133,6 +141,9 @@ struct tsd_host_api { * @return 0 or a negative tsd_error */ int (*continue_read)(struct tsd_host *host, void *buffer, uint32_t count); + + /** The host runs the card in SPI mode. */ + bool spi; }; /** @@ -199,6 +210,17 @@ static inline void tsd_host_set_bus_width(struct tsd_host *host, uint8_t width) host->api->set_bus_width(host, width); } +/** + * Report whether the card holds its data line low while it programs or erases. + * + * @param host the host + * @return 1 while the card is busy, 0 once it is free, or a negative tsd_error + */ +static inline int tsd_host_card_busy(struct tsd_host *host) +{ + return host->api->card_busy(host); +} + /** * Enable or disable the card interrupt, on a host that has one. * diff --git a/include/tinysd/host/spi.h b/include/tinysd/host/spi.h new file mode 100644 index 0000000..b12d587 --- /dev/null +++ b/include/tinysd/host/spi.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include +#include + +struct tsd_spi; + +/** + * How the host reaches the card's SPI bus, passed to tsd_spi_init. + */ +struct tsd_spi_config { + /** Send bytes, ignoring what comes back. Return 0 or a negative tsd_error. */ + int (*write)(struct tsd_spi *spi, const uint8_t *buffer, uint32_t len); + + /** Receive bytes while sending 0xff. Return 0 or a negative tsd_error. */ + int (*read)(struct tsd_spi *spi, uint8_t *buffer, uint32_t len); + + /** Drive chip select low to select the card, or high to release it. */ + void (*select)(struct tsd_spi *spi, bool selected); + + /** Run the clock at the highest rate at or below a limit in kHz. Return 0 or a negative tsd_error. */ + int (*set_clock)(struct tsd_spi *spi, uint32_t clock_khz); +}; + +/** Macro to cast a generic host instance to an SPI host instance. */ +#define TSD_SPI(host) ((struct tsd_spi *)(host)) + +/** + * A card in SPI mode on an SPI bus. + */ +struct tsd_spi { + /** The host interface, which card drivers take. */ + struct tsd_host host; + + /** The bus's configuration. */ + struct tsd_spi_config config; +}; + +/** + * Initialize an SPI host. + * + * @param spi the host + * @param config the bus's configuration + * @return 0 or a negative tsd_error + */ +int tsd_spi_init(struct tsd_spi *spi, struct tsd_spi_config config); diff --git a/src/card_init.c b/src/card_init.c index e896bb1..69e71dd 100644 --- a/src/card_init.c +++ b/src/card_init.c @@ -47,6 +47,10 @@ static int check_version(struct tsd_card *card, bool *version2) *version2 = false; if (ret == TSD_ERR_TIMEOUT) return 0; + + // Take an illegal command answer in SPI mode as a version 1 card too + if (ret == TSD_ERR_COMMAND && card->host->api->spi && (reply & TSD_SPI_R1_ILLEGAL_COMMAND)) + return 0; if (ret < 0) return ret; @@ -93,7 +97,7 @@ static int set_block_length(struct tsd_card *card) } // Repeat ACMD41 until the card reports ready, offering high capacity to a version 2 card -static int send_op_cond(struct tsd_card *card, bool version2) +static int send_op_cond_sd(struct tsd_card *card, bool version2) { uint32_t argument = TSD_OCR_VOLTAGE_WINDOW | (version2 ? TSD_OCR_CCS : 0); for (uint32_t waited_us = 0; waited_us < POWER_UP_TIMEOUT_MS * 1000; waited_us += POWER_UP_DELAY_US) { @@ -117,7 +121,7 @@ static int send_op_cond(struct tsd_card *card, bool version2) } // Read the card's CSD with CMD9 and count the blocks it holds -static int read_capacity(struct tsd_card *card) +static int read_capacity_sd(struct tsd_card *card) { // Ask the card for its CSD struct tsd_command command = { @@ -155,31 +159,11 @@ static bool switch_to_high_speed(struct tsd_card *card) TSD_SWITCH_FUNCTION_HIGH_SPEED; } -int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, uint32_t clock_khz) +// Initialize the card in SD mode, following Figure 4-2 of the SD specification +static int init_sd(struct tsd_card *card, uint8_t width, uint32_t clock_khz) { - // Refuse a width the bus does not have - if (width != 1 && width != 4) - return TSD_ERR_PARAM; - - // Take the host and start from no address and no read, as a standard capacity card - card->host = host; - card->address = 0; - card->high_capacity = false; - card->blocks = 0; - card->reading = false; - card->next_block = UINT32_MAX; - - // Start at the identification clock on one data line - int ret = tsd_host_set_clock(card->host, IDENTIFICATION_CLOCK_KHZ); - if (ret < 0) - return ret; - tsd_host_set_bus_width(card->host, 1); - - // Let the clock run before the first command - tsd_delay_us(FIRST_COMMAND_DELAY_US); - // Reset the card to idle - ret = tsd_host_send_command(card->host, TSD_CMD_GO_IDLE_STATE, 0, TSD_RESPONSE_NONE, NULL); + int ret = tsd_host_send_command(card->host, TSD_CMD_GO_IDLE_STATE, 0, TSD_RESPONSE_NONE, NULL); if (ret < 0) return ret; @@ -190,7 +174,7 @@ int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, u return ret; // Power the card up with ACMD41 - ret = send_op_cond(card, version2); + ret = send_op_cond_sd(card, version2); if (ret < 0) return ret; @@ -205,7 +189,7 @@ int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, u card->address = relative >> TSD_RCA_SHIFT; // Count the card's blocks, which CMD9 reads before the card is selected - ret = read_capacity(card); + ret = read_capacity_sd(card); if (ret < 0) return ret; @@ -233,3 +217,144 @@ int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, u return tsd_host_set_clock(card->host, clock_khz); } + +// Repeat ACMD41 until the card leaves idle, offering high capacity to a version 2 card +static int send_op_cond_spi(struct tsd_card *card, bool version2) +{ + uint32_t argument = version2 ? TSD_OCR_CCS : 0; + for (uint32_t waited_us = 0; waited_us < POWER_UP_TIMEOUT_MS * 1000; waited_us += POWER_UP_DELAY_US) { + // Ask the card to power up, which answers with only an R1 in SPI mode + uint32_t r1 = 0; + int ret = send_app_command(card, TSD_ACMD_SD_SEND_OP_COND, argument, TSD_RESPONSE_R1, &r1); + if (ret < 0) + return ret; + + // Finish once the card leaves idle + if (!(r1 & TSD_SPI_R1_IN_IDLE_STATE)) + return 0; + + // Wait before asking again + tsd_delay_us(POWER_UP_DELAY_US); + } + + return TSD_ERR_TIMEOUT; +} + +// Read the OCR with CMD58 and note whether the card took high capacity +static int read_ocr_spi(struct tsd_card *card, bool version2) +{ + uint32_t ocr = 0; + int ret = tsd_host_send_command(card->host, TSD_CMD_SPI_READ_OCR, 0, TSD_RESPONSE_R3, &ocr); + if (ret < 0) + return ret; + + card->high_capacity = version2 && (ocr & TSD_OCR_CCS); + + return 0; +} + +// Read the card's CSD with CMD9, which SPI mode sends as a data block, and count the blocks it holds +static int read_capacity_spi(struct tsd_card *card) +{ + // Ask the card for its CSD + uint8_t bytes[TSD_CSD_LEN]; + struct tsd_command command = { + .index = TSD_CMD_SEND_CSD, + .response_type = TSD_RESPONSE_R1, + }; + struct tsd_data data = { + .buffer = bytes, + .block_size = TSD_CSD_LEN, + .blocks = 1, + }; + int ret = tsd_host_request(card->host, &command, &data); + if (ret < 0) + return ret; + + // Gather the bytes into words, most significant first + uint32_t csd[TSD_CSD_LEN / 4]; + for (int i = 0; i < TSD_CSD_LEN / 4; i++) + csd[i] = (uint32_t)bytes[i * 4] << 24 | bytes[i * 4 + 1] << 16 | bytes[i * 4 + 2] << 8 | bytes[i * 4 + 3]; + + return decode_csd(card, csd); +} + +// Initialize the card in SPI mode, following Figure 7-2 of the SD specification +static int init_spi(struct tsd_card *card, uint32_t clock_khz) +{ + // Reset the card to idle, which puts it in SPI mode + int ret = tsd_host_send_command(card->host, TSD_CMD_GO_IDLE_STATE, 0, TSD_RESPONSE_R1, NULL); + if (ret < 0) + return ret; + + // Check with CMD8 whether the card is version 2 + bool version2; + ret = check_version(card, &version2); + if (ret < 0) + return ret; + + // Turn CRC checking on, which the SD specification asks for before ACMD41 + ret = tsd_host_send_command(card->host, TSD_CMD_SPI_CRC_ON_OFF, 1, TSD_RESPONSE_R1, NULL); + if (ret < 0) + return ret; + + // Power the card up with ACMD41 + ret = send_op_cond_spi(card, version2); + if (ret < 0) + return ret; + + // Read whether the card took high capacity with CMD58, since ACMD41 returns only an R1 in SPI mode + ret = read_ocr_spi(card, version2); + if (ret < 0) + return ret; + + // Count the card's blocks + ret = read_capacity_spi(card); + if (ret < 0) + return ret; + + // Set 512-byte blocks on a standard capacity card + ret = set_block_length(card); + if (ret < 0) + return ret; + + // Run a clock above default speed only once the card switches to high speed + if (clock_khz > DEFAULT_SPEED_MAX_KHZ && !switch_to_high_speed(card)) + clock_khz = DEFAULT_SPEED_MAX_KHZ; + + return tsd_host_set_clock(card->host, clock_khz); +} + +int tsd_card_init(struct tsd_card *card, struct tsd_host *host, uint8_t width, uint32_t clock_khz) +{ + // Refuse a width the bus does not have + if (width != 1 && width != 4) + return TSD_ERR_PARAM; + + // Refuse four data lines on an SPI host, which has one + if (host->api->spi && width != 1) + return TSD_ERR_PARAM; + + // Take the host and start from no address and no read, as a standard capacity card + card->host = host; + card->address = 0; + card->high_capacity = false; + card->blocks = 0; + card->reading = false; + card->next_block = UINT32_MAX; + + // Start at the identification clock on one data line + int ret = tsd_host_set_clock(card->host, IDENTIFICATION_CLOCK_KHZ); + if (ret < 0) + return ret; + tsd_host_set_bus_width(card->host, 1); + + // Let the clock run before the first command + tsd_delay_us(FIRST_COMMAND_DELAY_US); + + // Initialize the card on the bus the host runs + if (card->host->api->spi) + return init_spi(card, clock_khz); + + return init_sd(card, width, clock_khz); +} diff --git a/src/card_io.c b/src/card_io.c index 476a438..c6fb1ce 100644 --- a/src/card_io.c +++ b/src/card_io.c @@ -36,6 +36,17 @@ TSD_STATUS_ERASE_PARAM | \ TSD_STATUS_WP_ERASE_SKIP) +// Error bits of the R2 that CMD13 returns in SPI mode +#define SPI_STATUS_ERRORS \ + (TSD_SPI_R2_CARD_IS_LOCKED | \ + TSD_SPI_R2_WP_ERASE_SKIP | \ + TSD_SPI_R2_ERROR | \ + TSD_SPI_R2_CC_ERROR | \ + TSD_SPI_R2_CARD_ECC_FAILED | \ + TSD_SPI_R2_WP_VIOLATION | \ + TSD_SPI_R2_ERASE_PARAM | \ + TSD_SPI_R2_OUT_OF_RANGE) + // 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) { @@ -71,8 +82,8 @@ static int transfer_blocks(struct tsd_card *card, void *buffer, uint32_t block, }; int ret = tsd_host_request(card->host, &command, &data); - // End a multiple block transfer with CMD12, even after a failure, and report the first error - if (count > 1) { + // End a multiple block transfer with CMD12, even after a failure, unless an SPI host already ended it + if (count > 1 && !card->host->api->spi) { // Wait out a busy only after a write, since a read leaves the card nothing to program enum tsd_response stop_type = write ? TSD_RESPONSE_R1B : TSD_RESPONSE_R1; @@ -95,20 +106,30 @@ static int wait_ready(struct tsd_card *card, uint32_t errors, uint64_t timeout_u { uint32_t address = card->address << TSD_RCA_SHIFT; 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); + // Check whether the card still holds its data line low + int ret = tsd_host_card_busy(card->host); if (ret < 0) return ret; - // 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; + // Ask a card that has let go for its status + if (ret == 0) { + // Read the status as an R2 in SPI mode and an R1 otherwise + bool spi = card->host->api->spi; + enum tsd_response type = spi ? TSD_RESPONSE_R2 : TSD_RESPONSE_R1; + uint32_t status = 0; + ret = tsd_host_send_command(card->host, TSD_CMD_SEND_STATUS, address, type, &status); + if (ret < 0) + return ret; + + // Report an error bit the card set + if (status & (spi ? SPI_STATUS_ERRORS : errors)) + return TSD_ERR_CARD; + + // Finish in SPI mode, which has no state, or once the card is ready for data in the transfer state + uint32_t state = TSD_FIELD_GET(TSD_STATUS_CURRENT_STATE, status); + if (spi || ((status & TSD_STATUS_READY_FOR_DATA) && state == TSD_STATUS_STATE_TRAN)) + return 0; + } // Wait before asking again tsd_delay_us(READY_DELAY_US); diff --git a/src/host/sdhci.c b/src/host/sdhci.c index bbe2f51..25cbf3d 100644 --- a/src/host/sdhci.c +++ b/src/host/sdhci.c @@ -741,6 +741,17 @@ static void set_bus_width(struct tsd_host *host, uint8_t width) write8(sdhci, REG_CTRL, host_control); } +// Report the card busy while it holds DAT0 low +static int card_busy(struct tsd_host *host) +{ + struct tsd_sdhci *sdhci = TSD_SDHCI(host); + + // Read the DAT line levels and test DAT0 + uint32_t levels = TSD_FIELD_GET(STATE_DAT_3_0_LEVEL, read32(sdhci, REG_PRESENT_STATE)); + + return !(levels & TSD_BIT(0)); +} + // Let the card interrupt reach the CPU or stop it static void set_card_interrupt(struct tsd_host *host, bool on) { @@ -756,6 +767,7 @@ static const struct tsd_host_api sdhci_api = { .request = send_request, .set_clock = set_clock, .set_bus_width = set_bus_width, + .card_busy = card_busy, .set_card_interrupt = set_card_interrupt, }; diff --git a/src/host/spi.c b/src/host/spi.c new file mode 100644 index 0000000..5664f8f --- /dev/null +++ b/src/host/spi.c @@ -0,0 +1,334 @@ +#include + +#include +#include +#include +#include +#include + +#include + +// Clock the card starts at, in kHz +#define IDENTIFICATION_CLOCK_KHZ 400 + +// Bytes clocked with the card released at power up, 80 clocks covering the 74 the SD specification asks for +#define POWER_UP_BYTES 10 + +// Longest wait for a response, in milliseconds +#define COMMAND_TIMEOUT_MS 200 + +// Longest wait for the busy after an R1b, in milliseconds +#define BUSY_TIMEOUT_MS 3000 + +// Longest wait for a read block to start or a written block to program, in milliseconds +#define DATA_TIMEOUT_MS 10000 + +// Delay between reads while a wait polls, in microseconds +#define WAIT_STEP_US 10 + +// Read bytes while they match value under mask, leaving the first that does not in byte +static int wait_byte(struct tsd_spi *spi, uint8_t mask, uint8_t value, uint32_t timeout_ms, uint8_t *byte) +{ + for (uint32_t waited_us = 0;; waited_us += WAIT_STEP_US) { + // Read a byte and stop once it no longer matches + int ret = spi->config.read(spi, byte, 1); + if (ret < 0) + return ret; + if ((*byte & mask) != value) + return 0; + + // Give up once the timeout has passed, and wait a step before the next read + if (waited_us >= timeout_ms * 1000) + return TSD_ERR_TIMEOUT; + tsd_delay_us(WAIT_STEP_US); + } +} + +// Turn the error bits of an R1 into a tsd_error +static int check_r1(uint8_t r1) +{ + // Report a bad address or argument first, then a command the card refused or received damaged + if (r1 & (TSD_SPI_R1_PARAMETER_ERROR | TSD_SPI_R1_ADDRESS_ERROR)) + return TSD_ERR_PARAM; + if (r1 & (TSD_SPI_R1_ILLEGAL_COMMAND | TSD_SPI_R1_COM_CRC_ERROR)) + return TSD_ERR_COMMAND; + + // Report an erase sequence the card broke off + if (r1 & (TSD_SPI_R1_ERASE_SEQ_ERROR | TSD_SPI_R1_ERASE_RESET)) + return TSD_ERR_CARD; + + return 0; +} + +// Send a command and read its response +static int send_command(struct tsd_spi *spi, struct tsd_command *command) +{ + // Count the bytes after the R1, refusing the SDIO responses + uint32_t extra_len; + switch (command->response_type) { + case TSD_RESPONSE_R2: + // Take the second status byte + extra_len = 1; + break; + + case TSD_RESPONSE_R3: + case TSD_RESPONSE_R7: + // Take the four bytes of the OCR or the interface condition + extra_len = 4; + break; + + case TSD_RESPONSE_R4: + case TSD_RESPONSE_R5: + case TSD_RESPONSE_R6: + // Refuse a response this host does not take + return TSD_ERR_PARAM; + + default: + // Take the R1 alone, which every command returns + extra_len = 0; + break; + } + + // Send a byte of ones so the card is ready, then the frame + uint8_t frame[TSD_FRAME_COMMAND_LEN + 1] = {0xff}; + tsd_frame_build_command(&frame[1], command); + int ret = spi->config.write(spi, frame, sizeof(frame)); + if (ret < 0) + return ret; + + // Skip the byte after the frame, which may hold data after CMD12, then wait for the R1 and its 0 start bit + uint8_t r1; + ret = spi->config.read(spi, &r1, 1); + if (ret < 0) + return ret; + ret = wait_byte(spi, 0x80, 0x80, COMMAND_TIMEOUT_MS, &r1); + if (ret < 0) + return ret; + command->response[0] = r1; + + // Report the error bits of the R1, leaving it in the response, since a card that refuses a command sends no more + ret = check_r1(r1); + if (ret < 0) + return ret; + + // Put an R2's second status byte above the R1, or an R3 or R7's four bytes in its place + uint8_t extra[4]; + if (extra_len) { + ret = spi->config.read(spi, extra, extra_len); + if (ret < 0) + return ret; + } + if (extra_len == 1) + command->response[0] |= (uint32_t)extra[0] << 8; + if (extra_len == 4) + command->response[0] = (uint32_t)extra[0] << 24 | (uint32_t)extra[1] << 16 | (uint32_t)extra[2] << 8 | extra[3]; + + // Wait while the card holds its output low after an R1b + if (command->response_type == TSD_RESPONSE_R1B) { + uint8_t byte; + return wait_byte(spi, 0xff, 0x00, BUSY_TIMEOUT_MS, &byte); + } + + return 0; +} + +// Read one block and check its CRC16, counting it as moved once it arrives +static int read_block(struct tsd_spi *spi, struct tsd_data *data, uint8_t *buffer) +{ + // Wait for the card to start the block, refusing an error token + uint8_t token; + int ret = wait_byte(spi, 0xff, 0xff, DATA_TIMEOUT_MS, &token); + if (ret < 0) + return ret; + if (token != TSD_SPI_TOKEN_START_BLOCK) + return TSD_ERR_DATA; + + // Read the block and its CRC16 + uint8_t crc_bytes[2]; + ret = spi->config.read(spi, buffer, data->block_size); + if (ret < 0) + return ret; + ret = spi->config.read(spi, crc_bytes, sizeof(crc_bytes)); + if (ret < 0) + return ret; + data->done += data->block_size; + + // Check the CRC16 + uint16_t crc = 0; + for (uint32_t i = 0; i < data->block_size; i++) + crc = tsd_crc16_update(crc, buffer[i]); + + return crc == (crc_bytes[0] << 8 | crc_bytes[1]) ? 0 : TSD_ERR_DATA; +} + +// Write one block after a token, counting it as moved once the card takes it, then wait while the card programs it +static int write_block(struct tsd_spi *spi, struct tsd_data *data, const uint8_t *buffer, uint8_t token) +{ + // Compute the CRC16 that follows the block + uint16_t crc = 0; + for (uint32_t i = 0; i < data->block_size; i++) + crc = tsd_crc16_update(crc, buffer[i]); + uint8_t crc_bytes[2] = {(uint8_t)(crc >> 8), (uint8_t)crc}; + + // Send a byte of ones, the token, the block and its CRC16 + uint8_t start[2] = {0xff, token}; + int ret = spi->config.write(spi, start, sizeof(start)); + if (ret < 0) + return ret; + ret = spi->config.write(spi, buffer, data->block_size); + if (ret < 0) + return ret; + ret = spi->config.write(spi, crc_bytes, sizeof(crc_bytes)); + if (ret < 0) + return ret; + + // Check the data response, reporting a write error as the card's and anything else as a failed transfer + uint8_t response; + ret = spi->config.read(spi, &response, 1); + if (ret < 0) + return ret; + uint32_t status = TSD_FIELD_GET(TSD_SPI_DATA_RESPONSE_STATUS, response); + if (status == TSD_SPI_DATA_WRITE_ERROR) + return TSD_ERR_CARD; + if (status != TSD_SPI_DATA_ACCEPTED) + return TSD_ERR_DATA; + data->done += data->block_size; + + // Wait while the card holds its output low + return wait_byte(spi, 0xff, 0x00, DATA_TIMEOUT_MS, &response); +} + +// Move the blocks of a data phase, ending a multiple block write with the stop token +static int transfer_data(struct tsd_spi *spi, struct tsd_data *data) +{ + // Mark each block of a multiple block write with its own token + bool multiple = data->blocks > 1; + uint8_t token = multiple ? TSD_SPI_TOKEN_MULTI_WRITE : TSD_SPI_TOKEN_START_BLOCK; + + uint8_t *buffer = data->buffer; + for (uint32_t i = 0; i < data->blocks; i++) { + // Move one block + int ret; + if (data->write) { + ret = write_block(spi, data, buffer, token); + } else { + ret = read_block(spi, data, buffer); + } + if (ret < 0) + return ret; + + // Step to the next block + buffer += data->block_size; + } + + // Finish here unless a multiple block write needs its stop token + if (!data->write || !multiple) + return 0; + + // Send the stop token and skip the byte after it + uint8_t byte = TSD_SPI_TOKEN_STOP_TRAN; + int ret = spi->config.write(spi, &byte, 1); + if (ret < 0) + return ret; + ret = spi->config.read(spi, &byte, 1); + if (ret < 0) + return ret; + + // Wait while the card holds its output low to program the last block + return wait_byte(spi, 0xff, 0x00, DATA_TIMEOUT_MS, &byte); +} + +// Select the card, run a command and its data phase, then release the card +static int send_request(struct tsd_host *host, struct tsd_command *command, struct tsd_data *data) +{ + struct tsd_spi *spi = TSD_SPI(host); + + // Refuse an open read, since this host has no continue_read + if (data) { + if (data->blocks == 0) + return TSD_ERR_PARAM; + data->done = 0; + } + + // Select the card and run the command + spi->config.select(spi, true); + int ret = send_command(spi, command); + + // Run the data phase, then end a multiple block read or a failed transfer with CMD12, keeping the first error + if (ret == 0 && data) { + ret = transfer_data(spi, data); + if (ret < 0 || (!data->write && data->blocks > 1)) { + struct tsd_command stop = {.index = TSD_CMD_STOP_TRANSMISSION, .response_type = TSD_RESPONSE_R1B}; + int stop_ret = send_command(spi, &stop); + if (ret == 0) + ret = stop_ret; + } + } + + // Release the card, then clock a byte so it lets go of its output, keeping the first error + spi->config.select(spi, false); + uint8_t byte; + int release_ret = spi->config.read(spi, &byte, 1); + + return ret < 0 ? ret : release_ret; +} + +// Run the bus clock at or below a limit +static int set_clock(struct tsd_host *host, uint32_t clock_khz) +{ + struct tsd_spi *spi = TSD_SPI(host); + + // Leave the clock alone when asked to stop it, since it only runs while a transfer does + if (clock_khz == 0) + return 0; + + return spi->config.set_clock(spi, clock_khz); +} + +// Take the one data line SPI mode has +static void set_bus_width(struct tsd_host *host, uint8_t width) +{ +} + +// Report the card busy while it holds its output low +static int card_busy(struct tsd_host *host) +{ + struct tsd_spi *spi = TSD_SPI(host); + + // Select the card and read a byte, which stays low while it programs + uint8_t byte; + spi->config.select(spi, true); + int ret = spi->config.read(spi, &byte, 1); + spi->config.select(spi, false); + if (ret < 0) + return ret; + + return byte != 0xff; +} + +// Operations of the SPI host +static const struct tsd_host_api spi_api = { + .request = send_request, + .set_clock = set_clock, + .set_bus_width = set_bus_width, + .card_busy = card_busy, + .spi = true, +}; + +int tsd_spi_init(struct tsd_spi *spi, struct tsd_spi_config config) +{ + // Keep how to reach the bus and fill in the host interface + spi->host.api = &spi_api; + spi->config = config; + + // Start the clock at the identification rate + int ret = config.set_clock(spi, IDENTIFICATION_CLOCK_KHZ); + if (ret < 0) + return ret; + + // Clock the card with it released, which it needs before its first command + uint8_t bytes[POWER_UP_BYTES]; + config.select(spi, false); + + return config.read(spi, bytes, sizeof(bytes)); +} diff --git a/src/sdio_init.c b/src/sdio_init.c index dac30f5..7ae120e 100644 --- a/src/sdio_init.c +++ b/src/sdio_init.c @@ -294,6 +294,10 @@ int tsd_sdio_identify(struct tsd_host *host, struct tsd_sdio_cis *cis) { struct tsd_sdio card = {0}; + // Refuse an SPI host before CMD0, which would leave the card in SPI mode until a power cycle + if (host->api->spi) + return TSD_ERR_PARAM; + // Enumerate the card to read its ids int ret = enumerate_card(&card, host); @@ -310,6 +314,10 @@ int tsd_sdio_init(struct tsd_sdio *card, struct tsd_host *host, uint8_t width, u if (width != 1 && width != 4) return TSD_ERR_PARAM; + // Refuse an SPI host before CMD0, which would leave the card in SPI mode until a power cycle + if (host->api->spi) + return TSD_ERR_PARAM; + // Enumerate the card int ret = enumerate_card(card, host); if (ret < 0) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e3632ea..4aa4842 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,3 +36,7 @@ target_link_libraries(test_card tinysd_card) # SDIO card driver tests, against the fake host in harness.h add_tinysd_test(test_sdio test_sdio.c) target_link_libraries(test_sdio tinysd_sdio) + +# SPI host tests, against a fake bus +add_tinysd_test(test_spi test_spi.c) +target_link_libraries(test_spi tinysd_spi) diff --git a/test/harness.h b/test/harness.h index f1c042e..449d1d5 100644 --- a/test/harness.h +++ b/test/harness.h @@ -37,6 +37,9 @@ struct recorded_command { struct scripted_reply { int ret; uint32_t response[4]; + + // Bytes a read's data phase receives, or NULL to leave the buffer alone + const uint8_t *data; }; // A host that records each command and answers it from the script @@ -52,6 +55,9 @@ struct fake_host { // Blocks taken through continue_read uint32_t continued_blocks; + // Busy checks left before the card lets go + int busy_checks; + // Function 0 registers and CIS, which CMD52 reads and writes uint8_t f0[HARNESS_F0_SIZE]; }; @@ -97,6 +103,10 @@ static int fake_request(struct tsd_host *host, struct tsd_command *command, stru if (data) data->done = reply->ret == 0 ? data->blocks * data->block_size : 0; + // Fill a read from the script's data + if (data && !data->write && reply->data) + memcpy(data->buffer, reply->data, data->blocks * data->block_size); + return reply->ret; } @@ -121,14 +131,37 @@ static void fake_set_bus_width(struct tsd_host *host, uint8_t width) { } +// Report the card busy for the scripted number of checks +static int fake_card_busy(struct tsd_host *host) +{ + struct fake_host *fake = (struct fake_host *)host; + if (fake->busy_checks == 0) + return 0; + + // Count down one check while the card stays busy + fake->busy_checks--; + + return 1; +} + // Operations of the fake host static const struct tsd_host_api fake_api = { .request = fake_request, .set_clock = fake_set_clock, .set_bus_width = fake_set_bus_width, + .card_busy = fake_card_busy, .continue_read = fake_continue_read, }; +// Operations of the fake host in SPI mode +static const struct tsd_host_api fake_spi_api = { + .request = fake_request, + .set_clock = fake_set_clock, + .set_bus_width = fake_set_bus_width, + .card_busy = fake_card_busy, + .spi = true, +}; + // Clear the script and the record, call from setUp() static inline void harness_reset(void) { @@ -136,6 +169,12 @@ static inline void harness_reset(void) host.base.api = &fake_api; } +// Run the fake host in SPI mode, after harness_reset() +static inline void harness_use_spi(void) +{ + host.base.api = &fake_spi_api; +} + // Return the last recorded command with an index, or NULL when none was sent static inline struct recorded_command *harness_find(uint8_t index) { diff --git a/test/test_card.c b/test/test_card.c index d6a8849..8347be1 100644 --- a/test/test_card.c +++ b/test/test_card.c @@ -14,6 +14,17 @@ static struct tsd_card card; // Blocks the tests read and write, which the fake host leaves alone static uint8_t buffer[8 * TSD_CARD_BLOCK_SIZE]; +// The version 1 CSD of the 32 MB example in the SD specification, as SPI mode sends it +static const uint8_t spi_csd_v1[TSD_CSD_LEN] = {0, 0, 0, 0, 0x00, 0x09, 0x01, 0xf4, 0x00, 0x01, 0x80, 0x00, 0, 0, 0, 0}; + +// The version 2 CSD of the smallest SDHC card in the SD specification, as SPI mode sends it +static const uint8_t spi_csd_v2[TSD_CSD_LEN] = {0x40, 0, 0, 0, 0, 0, 0, 0, 0x10, 0x10, 0, 0, 0, 0, 0, 0}; + +// The switch status of a card that took high speed +static const uint8_t switch_status_high_speed[TSD_SWITCH_STATUS_LEN] = { + [TSD_SWITCH_STATUS_GROUP1] = TSD_SWITCH_FUNCTION_HIGH_SPEED, +}; + // Script a version 1 CSD static void set_csd_v1(uint32_t c_size, uint32_t c_size_mult, uint32_t read_bl_len) { @@ -39,6 +50,17 @@ static void init_high_capacity(void) TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 400)); } +// Bring up a high capacity card in SPI mode, with no error bits in its status +static void init_spi(void) +{ + harness_use_spi(); + host.replies[TSD_ACMD_SD_SEND_OP_COND].response[0] = 0; + host.replies[TSD_CMD_SPI_READ_OCR].response[0] = TSD_OCR_READY | TSD_OCR_CCS; + host.replies[TSD_CMD_SEND_CSD].data = spi_csd_v2; + host.replies[TSD_CMD_SEND_STATUS].response[0] = 0; + TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 400)); +} + void setUp(void) { // Clear the fake host @@ -101,6 +123,97 @@ static void test_init_version1(void) TEST_ASSERT_EQUAL_UINT32(TSD_CARD_BLOCK_SIZE, harness_find(TSD_CMD_SET_BLOCKLEN)->argument); } +// Test that SPI mode turns CRC on, reads the OCR and CSD its own way, and leaves out addressing +static void test_spi_init(void) +{ + harness_use_spi(); + host.replies[TSD_ACMD_SD_SEND_OP_COND].response[0] = 0; + host.replies[TSD_CMD_SPI_READ_OCR].response[0] = TSD_OCR_READY | TSD_OCR_CCS; + host.replies[TSD_CMD_SEND_CSD].data = spi_csd_v2; + + TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 50000)); + TEST_ASSERT_TRUE(card.high_capacity); + TEST_ASSERT_EQUAL_UINT32(4211712, card.blocks); + TEST_ASSERT_EQUAL_UINT32(1, harness_find(TSD_CMD_SPI_CRC_ON_OFF)->argument); + TEST_ASSERT_EQUAL_UINT32(TSD_OCR_CCS, harness_find(TSD_ACMD_SD_SEND_OP_COND)->argument); + TEST_ASSERT_NULL(harness_find(TSD_CMD_SEND_RELATIVE_ADDR)); + TEST_ASSERT_NULL(harness_find(TSD_CMD_SELECT_CARD)); +} + +// Test that SPI mode runs above default speed only once the card switches to high speed +static void test_spi_high_speed(void) +{ + init_spi(); + TEST_ASSERT_EQUAL_UINT32(400, host.clock_khz); + + TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 50000)); + TEST_ASSERT_EQUAL_UINT32(25000, host.clock_khz); + + host.replies[TSD_CMD_SWITCH_FUNC].data = switch_status_high_speed; + TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 50000)); + TEST_ASSERT_EQUAL_UINT32(TSD_SWITCH_HIGH_SPEED, harness_find(TSD_CMD_SWITCH_FUNC)->argument); + TEST_ASSERT_EQUAL_UINT32(50000, host.clock_khz); +} + +// Test that SPI mode takes an illegal command answer to CMD8 as a version 1 card +static void test_spi_init_version1(void) +{ + harness_use_spi(); + host.replies[TSD_CMD_SEND_IF_COND].ret = TSD_ERR_COMMAND; + host.replies[TSD_CMD_SEND_IF_COND].response[0] = TSD_SPI_R1_IN_IDLE_STATE | TSD_SPI_R1_ILLEGAL_COMMAND; + host.replies[TSD_ACMD_SD_SEND_OP_COND].response[0] = 0; + host.replies[TSD_CMD_SEND_CSD].data = spi_csd_v1; + + TEST_ASSERT_EQUAL_INT(0, tsd_card_init(&card, &host.base, 1, 400)); + TEST_ASSERT_FALSE(card.high_capacity); + TEST_ASSERT_EQUAL_UINT32(64032, card.blocks); + TEST_ASSERT_EQUAL_UINT32(0, harness_find(TSD_ACMD_SD_SEND_OP_COND)->argument); + TEST_ASSERT_NOT_NULL(harness_find(TSD_CMD_SET_BLOCKLEN)); +} + +// Test that SPI mode refuses four data lines without a command +static void test_spi_init_width4_refused(void) +{ + harness_use_spi(); + TEST_ASSERT_EQUAL_INT(TSD_ERR_PARAM, tsd_card_init(&card, &host.base, 4, 400)); + TEST_ASSERT_EQUAL_INT(0, host.command_count); +} + +// Test that SPI mode leaves ending a multiple block read or write to the host +static void test_spi_multiple_block_without_cmd12(void) +{ + init_spi(); + + TEST_ASSERT_EQUAL_INT(0, tsd_card_read(&card, buffer, 0, 4)); + TEST_ASSERT_EQUAL_INT(0, tsd_card_write(&card, buffer, 0, 4)); + TEST_ASSERT_EQUAL_INT(1, harness_count(TSD_CMD_READ_MULTIPLE_BLOCK)); + TEST_ASSERT_NULL(harness_find(TSD_CMD_STOP_TRANSMISSION)); +} + +// Test that an SPI write checks the R2 status once, and reports an error bit in it +static void test_spi_write_status(void) +{ + init_spi(); + + TEST_ASSERT_EQUAL_INT(0, tsd_card_write(&card, buffer, 0, 1)); + TEST_ASSERT_EQUAL_INT(1, harness_count(TSD_CMD_SEND_STATUS)); + TEST_ASSERT_EQUAL_INT(TSD_RESPONSE_R2, harness_find(TSD_CMD_SEND_STATUS)->response_type); + + host.replies[TSD_CMD_SEND_STATUS].response[0] = TSD_SPI_R2_WP_VIOLATION; + TEST_ASSERT_EQUAL_INT(TSD_ERR_CARD, tsd_card_write(&card, buffer, 0, 1)); +} + +// Test that an SPI erase waits while the card is busy, then checks the R2 status once +static void test_spi_erase(void) +{ + init_spi(); + host.busy_checks = 3; + + TEST_ASSERT_EQUAL_INT(0, tsd_card_erase(&card, 8, 8)); + TEST_ASSERT_EQUAL_INT(0, host.busy_checks); + TEST_ASSERT_EQUAL_INT(1, harness_count(TSD_CMD_SEND_STATUS)); +} + // Test that a standard capacity card is read by byte offset static void test_read_standard_capacity_by_offset(void) { @@ -159,6 +272,17 @@ static void test_erase(void) TEST_ASSERT_EQUAL_INT(TSD_RESPONSE_R1, harness_find(TSD_CMD_ERASE)->response_type); } +// Test that an erase sends CMD13 only once the card lets go of its data line +static void test_erase_waits_while_busy(void) +{ + init_high_capacity(); + host.busy_checks = 3; + + TEST_ASSERT_EQUAL_INT(0, tsd_card_erase(&card, 8, 8)); + TEST_ASSERT_EQUAL_INT(0, host.busy_checks); + TEST_ASSERT_EQUAL_INT(1, harness_count(TSD_CMD_SEND_STATUS)); +} + // Test that an erase reports an error bit the card sets once it finishes static void test_erase_error(void) { @@ -189,11 +313,19 @@ int main(int argc, char **argv) RUN_TEST(test_capacity_largest_sdxc); RUN_TEST(test_capacity_v3_refused); RUN_TEST(test_init_version1); + RUN_TEST(test_spi_init); + RUN_TEST(test_spi_high_speed); + RUN_TEST(test_spi_init_version1); + RUN_TEST(test_spi_init_width4_refused); + RUN_TEST(test_spi_multiple_block_without_cmd12); + RUN_TEST(test_spi_write_status); + RUN_TEST(test_spi_erase); RUN_TEST(test_read_standard_capacity_by_offset); 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_waits_while_busy); RUN_TEST(test_erase_error); RUN_TEST(test_past_the_end); diff --git a/test/test_sdio.c b/test/test_sdio.c index 0102f18..5a89fbe 100644 --- a/test/test_sdio.c +++ b/test/test_sdio.c @@ -99,6 +99,17 @@ static void test_identify(void) TEST_ASSERT_EQUAL_HEX16(0x1234, fields.card_id); } +// Test that init and identify refuse an SPI host without a command +static void test_spi_host_refused(void) +{ + harness_use_spi(); + + struct tsd_sdio_cis fields; + TEST_ASSERT_EQUAL(TSD_ERR_PARAM, tsd_sdio_init(&card, &host.base, 1, 400)); + TEST_ASSERT_EQUAL(TSD_ERR_PARAM, tsd_sdio_identify(&host.base, &fields)); + TEST_ASSERT_EQUAL(0, host.command_count); +} + // Test that a transfer moves whole blocks in block mode, then the rest in byte mode from where they ended static void test_transfer_split(void) { @@ -222,6 +233,7 @@ int main(int argc, char **argv) RUN_TEST(test_manufacturer_from_cis); RUN_TEST(test_identify); + RUN_TEST(test_spi_host_refused); RUN_TEST(test_transfer_split); RUN_TEST(test_byte_mode_512); RUN_TEST(test_argument_out_of_range); diff --git a/test/test_spi.c b/test/test_spi.c new file mode 100644 index 0000000..a7ce78a --- /dev/null +++ b/test/test_spi.c @@ -0,0 +1,328 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "unity.h" + +// Most bytes one test queues for the card to send or records from the host +#define FAKE_BUS_LEN 2048 + +// Bytes in the blocks the tests move +#define BLOCK_SIZE 512 + +// A bus that records what the host sends and answers reads from a queue, then with 0xff once it runs out +struct fake_bus { + struct tsd_spi spi; + + // Bytes the host sent + uint8_t sent[FAKE_BUS_LEN]; + uint32_t sent_len; + + // Bytes the card sends, and how many the host has read + uint8_t replies[FAKE_BUS_LEN]; + uint32_t reply_len; + uint32_t reply_idx; + + // Chip select and clock as the host last set them + bool selected; + uint32_t clock_khz; +}; + +// The bus the host under test drives +static struct fake_bus bus; + +// Blocks the tests read and write +static uint8_t blocks[2 * BLOCK_SIZE]; + +// Delay nothing, since the fake bus answers at once +void tsd_delay_us(uint32_t microseconds) +{ +} + +// Record the bytes the host sends +static int fake_write(struct tsd_spi *spi, const uint8_t *buffer, uint32_t len) +{ + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(FAKE_BUS_LEN, bus.sent_len + len, "too many bytes sent"); + memcpy(&bus.sent[bus.sent_len], buffer, len); + bus.sent_len += len; + + return 0; +} + +// Answer from the queue, then with 0xff +static int fake_read(struct tsd_spi *spi, uint8_t *buffer, uint32_t len) +{ + for (uint32_t i = 0; i < len; i++) + buffer[i] = bus.reply_idx < bus.reply_len ? bus.replies[bus.reply_idx++] : 0xff; + + return 0; +} + +// Record chip select +static void fake_select(struct tsd_spi *spi, bool selected) +{ + bus.selected = selected; +} + +// Record the clock +static int fake_set_clock(struct tsd_spi *spi, uint32_t clock_khz) +{ + bus.clock_khz = clock_khz; + + return 0; +} + +// Queue bytes for the card to send +static void queue(const uint8_t *bytes, uint32_t len) +{ + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(FAKE_BUS_LEN, bus.reply_len + len, "too many bytes queued"); + memcpy(&bus.replies[bus.reply_len], bytes, len); + bus.reply_len += len; +} + +// Queue one block and its CRC16, with the CRC16 spoiled when asked +static void queue_block(const uint8_t *block, bool bad_crc) +{ + uint16_t crc = 0; + for (uint32_t i = 0; i < BLOCK_SIZE; i++) + crc = tsd_crc16_update(crc, block[i]); + if (bad_crc) + crc ^= 1; + + uint8_t token = TSD_SPI_TOKEN_START_BLOCK; + queue(&token, 1); + queue(block, BLOCK_SIZE); + uint8_t crc_bytes[] = {crc >> 8, crc & 0xff}; + queue(crc_bytes, sizeof(crc_bytes)); +} + +// Run a command with no data phase and return its response word +static int send_command(uint8_t index, uint32_t argument, enum tsd_response response_type, uint32_t *response) +{ + return tsd_host_send_command(&bus.spi.host, index, argument, response_type, response); +} + +// Run a one or two block transfer of the test blocks +static int transfer(uint8_t index, uint32_t count, bool write, uint32_t *done) +{ + struct tsd_command command = {.index = index, .response_type = TSD_RESPONSE_R1}; + struct tsd_data data = {.buffer = blocks, .block_size = BLOCK_SIZE, .blocks = count, .write = write}; + int ret = tsd_host_request(&bus.spi.host, &command, &data); + *done = data.done; + + return ret; +} + +void setUp(void) +{ + // Clear the bus and start the host on it + memset(&bus, 0, sizeof(bus)); + struct tsd_spi_config config = { + .write = fake_write, + .read = fake_read, + .select = fake_select, + .set_clock = fake_set_clock, + }; + TEST_ASSERT_EQUAL_INT(0, tsd_spi_init(&bus.spi, config)); + + // Fill the test blocks with a pattern + for (uint32_t i = 0; i < sizeof(blocks); i++) + blocks[i] = (uint8_t)(i * 7); +} + +void tearDown(void) +{ +} + +// Test that init clocks the card at 400 kHz with it released, and marks the host as SPI +static void test_init(void) +{ + TEST_ASSERT_EQUAL_UINT32(400, bus.clock_khz); + TEST_ASSERT_FALSE(bus.selected); + TEST_ASSERT_EQUAL_UINT32(0, bus.sent_len); + TEST_ASSERT_TRUE(bus.spi.host.api->spi); +} + +// Test that CMD0 goes out after a byte of ones with the CRC7 from the SD specification, and takes an idle R1 +static void test_cmd0(void) +{ + uint8_t replies[] = {0xff, 0xff, TSD_SPI_R1_IN_IDLE_STATE}; + queue(replies, sizeof(replies)); + + uint32_t response = 0; + TEST_ASSERT_EQUAL_INT(0, send_command(TSD_CMD_GO_IDLE_STATE, 0, TSD_RESPONSE_NONE, &response)); + TEST_ASSERT_EQUAL_HEX32(TSD_SPI_R1_IN_IDLE_STATE, response); + + uint8_t expected[] = {0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x95}; + TEST_ASSERT_EQUAL_UINT32(sizeof(expected), bus.sent_len); + TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, bus.sent, sizeof(expected)); + TEST_ASSERT_FALSE(bus.selected); +} + +// Test that an R7 leaves its four bytes in the response word +static void test_r7(void) +{ + uint8_t replies[] = {0xff, TSD_SPI_R1_IN_IDLE_STATE, 0x00, 0x00, 0x01, 0xaa}; + queue(replies, sizeof(replies)); + + uint32_t response = 0; + TEST_ASSERT_EQUAL_INT(0, send_command(TSD_CMD_SEND_IF_COND, 0x1aa, TSD_RESPONSE_R7, &response)); + TEST_ASSERT_EQUAL_HEX32(0x1aa, response); +} + +// Test that an R2 puts its second status byte above the R1 +static void test_r2(void) +{ + uint8_t replies[] = {0xff, 0x00, 0x20}; + queue(replies, sizeof(replies)); + + uint32_t response = 0; + TEST_ASSERT_EQUAL_INT(0, send_command(TSD_CMD_SEND_STATUS, 0, TSD_RESPONSE_R2, &response)); + TEST_ASSERT_EQUAL_HEX32(TSD_SPI_R2_WP_VIOLATION, response); +} + +// Test that the error bits of an R1 become tsd_errors, and that a card that never answers times out +static void test_r1_errors(void) +{ + uint8_t illegal[] = {0xff, TSD_SPI_R1_IN_IDLE_STATE | TSD_SPI_R1_ILLEGAL_COMMAND}; + queue(illegal, sizeof(illegal)); + struct tsd_command command = {.index = TSD_CMD_SEND_IF_COND, .argument = 0x1aa, .response_type = TSD_RESPONSE_R7}; + TEST_ASSERT_EQUAL_INT(TSD_ERR_COMMAND, tsd_host_request(&bus.spi.host, &command, NULL)); + TEST_ASSERT_EQUAL_HEX32(TSD_SPI_R1_IN_IDLE_STATE | TSD_SPI_R1_ILLEGAL_COMMAND, command.response[0]); + + uint8_t address[] = {0xff, TSD_SPI_R1_ADDRESS_ERROR}; + queue(address, sizeof(address)); + TEST_ASSERT_EQUAL_INT(TSD_ERR_PARAM, send_command(TSD_CMD_SET_BLOCKLEN, 0, TSD_RESPONSE_R1, NULL)); + + TEST_ASSERT_EQUAL_INT(TSD_ERR_TIMEOUT, send_command(TSD_CMD_SEND_STATUS, 0, TSD_RESPONSE_R2, NULL)); +} + +// Test that an R1b reads through the busy bytes, leaving the last byte for the release after it +static void test_r1b_busy(void) +{ + uint8_t replies[] = {0xff, 0x00, 0x00, 0x00, 0xff, 0x55}; + queue(replies, sizeof(replies)); + + TEST_ASSERT_EQUAL_INT(0, send_command(TSD_CMD_STOP_TRANSMISSION, 0, TSD_RESPONSE_R1B, NULL)); + TEST_ASSERT_EQUAL_UINT32(sizeof(replies), bus.reply_idx); +} + +// Test that the card reads as busy while it holds its output low, with it selected and then released +static void test_card_busy(void) +{ + uint8_t replies[] = {0x00, 0xff}; + queue(replies, sizeof(replies)); + + TEST_ASSERT_EQUAL_INT(1, tsd_host_card_busy(&bus.spi.host)); + TEST_ASSERT_FALSE(bus.selected); + TEST_ASSERT_EQUAL_INT(0, tsd_host_card_busy(&bus.spi.host)); + TEST_ASSERT_EQUAL_UINT32(0, bus.sent_len); +} + +// Test that a read takes a block after its token and checks its CRC16, counting a block with a bad CRC as moved +static void test_read_block(void) +{ + uint8_t expected[BLOCK_SIZE]; + memcpy(expected, blocks, BLOCK_SIZE); + + uint8_t response[] = {0xff, 0x00, 0xff, 0xff}; + queue(response, sizeof(response)); + queue_block(expected, false); + memset(blocks, 0, sizeof(blocks)); + + uint32_t done = 0; + TEST_ASSERT_EQUAL_INT(0, transfer(TSD_CMD_READ_SINGLE_BLOCK, 1, false, &done)); + TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, blocks, BLOCK_SIZE); + TEST_ASSERT_EQUAL_UINT32(BLOCK_SIZE, done); + + queue(response, sizeof(response)); + queue_block(expected, true); + TEST_ASSERT_EQUAL_INT(TSD_ERR_DATA, transfer(TSD_CMD_READ_SINGLE_BLOCK, 1, false, &done)); + TEST_ASSERT_EQUAL_UINT32(BLOCK_SIZE, done); +} + +// Test that a multiple block read ends with CMD12 while the card is still selected +static void test_read_multiple(void) +{ + uint8_t expected[2 * BLOCK_SIZE]; + memcpy(expected, blocks, sizeof(expected)); + + uint8_t response[] = {0xff, 0x00}; + queue(response, sizeof(response)); + queue_block(expected, false); + queue_block(&expected[BLOCK_SIZE], false); + uint8_t stop[] = {0xff, 0x00, 0xff}; + queue(stop, sizeof(stop)); + memset(blocks, 0, sizeof(blocks)); + + uint32_t done = 0; + TEST_ASSERT_EQUAL_INT(0, transfer(TSD_CMD_READ_MULTIPLE_BLOCK, 2, false, &done)); + TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, blocks, sizeof(expected)); + TEST_ASSERT_EQUAL_UINT32(2 * BLOCK_SIZE, done); + TEST_ASSERT_EQUAL_UINT32(14, bus.sent_len); + TEST_ASSERT_EQUAL_HEX8(0x40 | TSD_CMD_STOP_TRANSMISSION, bus.sent[8]); +} + +// Test that a multiple block write marks each block with its token and ends with the stop token +static void test_write_multiple(void) +{ + uint8_t replies[] = { + 0xff, 0x00, // the byte after the frame, then the R1 + 0x05, 0x00, 0xff, // the first block accepted, then the busy + 0x05, 0x00, 0xff, // the second block accepted, then the busy + 0xff, 0x00, 0xff, // the byte after the stop token, then the busy + }; + queue(replies, sizeof(replies)); + + uint32_t done = 0; + TEST_ASSERT_EQUAL_INT(0, transfer(TSD_CMD_WRITE_MULTIPLE_BLOCK, 2, true, &done)); + TEST_ASSERT_EQUAL_UINT32(2 * BLOCK_SIZE, done); + + uint32_t first = 7; + uint32_t second = first + 2 + BLOCK_SIZE + 2; + TEST_ASSERT_EQUAL_HEX8(0xff, bus.sent[first]); + TEST_ASSERT_EQUAL_HEX8(TSD_SPI_TOKEN_MULTI_WRITE, bus.sent[first + 1]); + TEST_ASSERT_EQUAL_HEX8_ARRAY(blocks, &bus.sent[first + 2], BLOCK_SIZE); + TEST_ASSERT_EQUAL_HEX8(TSD_SPI_TOKEN_MULTI_WRITE, bus.sent[second + 1]); + TEST_ASSERT_EQUAL_HEX8(TSD_SPI_TOKEN_STOP_TRAN, bus.sent[bus.sent_len - 1]); +} + +// Test that a rejected block reports a CRC error as a failed transfer and a write error as the card's, then sends CMD12 +static void test_write_rejected(void) +{ + uint32_t done = 0; + uint8_t crc_bad[] = {0xff, 0x00, 0x0b, 0xff, 0x00, 0xff}; + queue(crc_bad, sizeof(crc_bad)); + TEST_ASSERT_EQUAL_INT(TSD_ERR_DATA, transfer(TSD_CMD_WRITE_BLOCK, 1, true, &done)); + TEST_ASSERT_EQUAL_UINT32(0, done); + TEST_ASSERT_EQUAL_HEX8(0x40 | TSD_CMD_STOP_TRANSMISSION, bus.sent[bus.sent_len - 6]); + + uint8_t write_bad[] = {0xff, 0x00, 0x0d, 0xff, 0x00, 0xff}; + queue(write_bad, sizeof(write_bad)); + TEST_ASSERT_EQUAL_INT(TSD_ERR_CARD, transfer(TSD_CMD_WRITE_BLOCK, 1, true, &done)); +} + +int main(int argc, char **argv) +{ + UNITY_BEGIN(); + + RUN_TEST(test_init); + RUN_TEST(test_cmd0); + RUN_TEST(test_r7); + RUN_TEST(test_r2); + RUN_TEST(test_r1_errors); + RUN_TEST(test_r1b_busy); + RUN_TEST(test_card_busy); + RUN_TEST(test_read_block); + RUN_TEST(test_read_multiple); + RUN_TEST(test_write_multiple); + RUN_TEST(test_write_rejected); + + return UNITY_END(); +}