diff --git a/components/kaos/storage/src/storage_ao.c b/components/kaos/storage/src/storage_ao.c index b450a44..64c6599 100644 --- a/components/kaos/storage/src/storage_ao.c +++ b/components/kaos/storage/src/storage_ao.c @@ -4626,8 +4626,10 @@ static void sd_test_publish(Storage * const me, uint32_t kb_s) /** * @brief Put the slot back on the everyday width, whatever the run did. * - * Asking costs nothing. A card that is on another width is remounted, and - * that is a call to the card. + * Asking costs nothing. A card that is not mounted the way an everyday mount + * mounts it - on one line by request, or on four without the write check - is + * remounted, and that is a call to the card. A card the driver keeps on one + * line, because it took no write on four, is where an everyday mount puts it. * * @param me The AO. * @param then What to go on with when the remount returns. @@ -4637,11 +4639,9 @@ static void sd_test_publish(Storage * const me, uint32_t kb_s) */ static bool sd_test_restore_width(Storage * const me, SdThen then) { - int32_t width = 0; - (void)kdrv_sd_set(KDRV_SD_WIDTH_REQ, (int32_t)KDRV_SD_WIDTH_USE); - (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); - if ( (int32_t)KDRV_SD_WIDTH_USE == width ) + (void)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1); + if ( kdrv_sd_mounted_everyday() ) { return false; } @@ -5473,16 +5473,20 @@ static void sd_script_start(Storage * const me, bool verify) me->t_capacity_mb = 0U; me->t_seed = (uint32_t)khal_time_us(); - /* Whatever an earlier run or a bench session left in the knob: the row - * judges the slot's four lines, sd_verify the card on the width it is - * used at. */ + /* Whatever an earlier run or a bench session left in the knobs: the row + * judges the slot's four lines, mounted as asked with no write check - + * a card that takes no write there would otherwise be narrowed before the + * row looked, and the slot failed for the card's fault - and sd_verify the + * card on the width it is used at, checked as every everyday mount is. */ if ( verify ) { (void)kdrv_sd_set(KDRV_SD_WIDTH_REQ, (int32_t)KDRV_SD_WIDTH_USE); + (void)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1); } else { (void)kdrv_sd_set(KDRV_SD_WIDTH_REQ, (int32_t)KBSP_SD_WIDTH); + (void)kdrv_sd_set(KDRV_SD_CHECK_REQ, 0); } /* Mount semantics rather than the probe's: somebody just asked for this diff --git a/components/kaos/storage/src/storage_verbs.c b/components/kaos/storage/src/storage_verbs.c index 7efd2a3..6a31677 100644 --- a/components/kaos/storage/src/storage_verbs.c +++ b/components/kaos/storage/src/storage_verbs.c @@ -81,6 +81,7 @@ static bool sd_field_row(uint16_t field, console_field_t *out) [KDRV_SD_FREE_MB] = "How much of that filesystem is unused.", [KDRV_SD_WIDTH_REQ] = "Data lines the NEXT mount asks for: 1 or 4, nothing between. Does not touch the card, and lasts until the board resets.", [KDRV_SD_FREQ_REQ_KHZ] = "Clock ceiling the NEXT mount asks for. 40000 is what this board is validated at; higher is a bench experiment.", + [KDRV_SD_CHECK_REQ] = "Whether the NEXT mount on four lines proves the card takes a write before keeping it: 1, the default, or 0 for none. Does not touch the card, and lasts until the board resets.", }; _Static_assert((sizeof(SD_FIELD_DESC) / sizeof(SD_FIELD_DESC[0])) == (size_t)KDRV_SD_FIELD_MAX, diff --git a/components/kdrv/storage/include/kdrv_sd.h b/components/kdrv/storage/include/kdrv_sd.h index 0dce12f..65b7dd9 100644 --- a/components/kdrv/storage/include/kdrv_sd.h +++ b/components/kdrv/storage/include/kdrv_sd.h @@ -10,6 +10,7 @@ #include #include +#include "kbsp.h" #include "kode_defs.h" #ifdef __cplusplus @@ -53,7 +54,9 @@ typedef enum { KDRV_SD_HIGH_CAPACITY, /**< OCR CCS: addressed by block, not by byte. */ KDRV_SD_BUS_WIDTH, /**< Data lines in use: 1 or 4. THE one to * watch - a card that came up on one line - * has three that did not reach the slot. */ + * either has three that did not reach the slot + * or took no write on four; the log says + * which. */ KDRV_SD_FREQ_KHZ, /**< What the host clock ended up at. */ KDRV_SD_CARD_MAX_KHZ, /**< What the card said it could take. */ KDRV_SD_RCA, /**< Relative card address. */ @@ -69,11 +72,15 @@ typedef enum { KDRV_SD_TOTAL_MB, /**< How big the filesystem is. */ KDRV_SD_FREE_MB, /**< How much of it is unused. */ - /* What the next mount will ask for. Writable; neither touches the card. */ + /* What the next mount will ask for. Writable; none touches the card. */ KDRV_SD_WIDTH_REQ, /**< Data lines to ask for: 1 or 4, nothing * between. Default KDRV_SD_WIDTH_USE. */ KDRV_SD_FREQ_REQ_KHZ, /**< Clock ceiling to ask for. Default * KBSP_SD_FREQ_KHZ. */ + KDRV_SD_CHECK_REQ, /**< Whether a mount on four lines proves the + * card takes a write before it is kept: 1 or + * 0. Default 1. 0 mounts on the lines asked + * for, with no check and no memory of cards. */ KDRV_SD_FIELD_MAX } kdrv_sd_field_t; @@ -87,16 +94,13 @@ typedef struct { int32_t min; /**< Smallest value the field can hold. */ int32_t max; /**< Largest. */ uint16_t step; /**< Resolution; always 1 here. */ - bool writable; /**< True for the two knobs and nothing else. */ + bool writable; /**< True for the next-mount knobs and nothing else. */ } kdrv_sd_field_info_t; -/** @brief Data lines an everyday mount asks for: one. The counterfeit cards - * shipped with some units read on four lines and fail to write on - * them, and a failed write leaves the controller, which the C5's - * link shares, unable to bring a card up again until the P4 resets. - * Every card reads and writes on one. The slot's four lines - * (KBSP_SD_WIDTH) are what `test component=sd` judges. */ -#define KDRV_SD_WIDTH_USE 1U +/** @brief Data lines an everyday mount asks for: the slot's four. A card that + * takes no write on them is mounted on one (see kdrv_sd_mount()), and + * `width_req` overrides this for the next mount. */ +#define KDRV_SD_WIDTH_USE KBSP_SD_WIDTH /* ---- How long a call may take -------------------------------------------- * The bounds a caller that runs these on a task of its own waits by. They are @@ -104,16 +108,35 @@ typedef struct { * under it. */ -/** @brief The wait for the card's rail to rise behind Q1, before the first - * bring-up of a boot. */ +/** @brief The wait for the card's rail to rise behind Q1, after the supply is + * switched on. */ #define KDRV_SD_POWER_SETTLE_MS 50U +/** @brief How long the card's supply stays off when the driver cycles it. */ +#define KDRV_SD_POWER_OFF_MS 100U +/** @brief One cycle of the card's supply: off for its time, then on and + * settled. */ +#define KDRV_SD_CYCLE_MS (KDRV_SD_POWER_OFF_MS + KDRV_SD_POWER_SETTLE_MS) /** @brief The most one try at the card may take: the identification and the * volume's mount behind it, each command waiting out the host's own * timeout. */ #define KDRV_SD_TRY_MAX_MS 3000U -/** @brief A mount: the rail, four data lines and then, when the card does - * not take them, one. */ -#define KDRV_SD_MOUNT_MAX_MS (KDRV_SD_POWER_SETTLE_MS + (2U * KDRV_SD_TRY_MAX_MS)) +/** @brief The most the write check on four lines may take: one sector read and + * the same sector written back, each given as long as a try. */ +#define KDRV_SD_CHECK_MAX_MS (2U * KDRV_SD_TRY_MAX_MS) +/** @brief How many times a card that takes no write on four lines is mounted + * on one, each after a cycle of its supply, before it is reported as a + * card that would not come up. */ +#define KDRV_SD_ONE_LINE_TRIES 2U +/** @brief A mount: the rail, a try on one line for a card remembered as one + * that takes no write on four, which another card in the slot then + * repeats on four, the write check behind that, and, when the card + * takes no write on four lines, cycles of its supply, each followed + * by a try on one. A card that does not come up on four lines at all + * takes two tries, which is less. */ +#define KDRV_SD_MOUNT_MAX_MS (KDRV_SD_POWER_SETTLE_MS + (2U * KDRV_SD_TRY_MAX_MS) \ + + KDRV_SD_CHECK_MAX_MS \ + + (KDRV_SD_ONE_LINE_TRIES \ + * (KDRV_SD_CYCLE_MS + KDRV_SD_TRY_MAX_MS))) /** @brief A transfer of 32 KB of sectors, which is the largest one asked. */ #define KDRV_SD_SECTORS_MAX_MS 5000U /** @brief A format of the largest card the slot takes, with the volume's size @@ -123,11 +146,30 @@ typedef struct { /* ---- Mounting ------------------------------------------------------------ */ /** - * @brief Mount the card, four data lines first and then one. + * @brief Mount the card on the lines `width_req` asks for: four first and then + * one, by default. * - * The retry is not a policy, it is what makes a counterfeit card readable: the - * cards shipped with some units advertise 4-bit in their SCR and then fail - * 4-bit writes. Reads work either way. What a 1-bit mount MEANS is the + * The fall to one line is not a policy, it is what makes a counterfeit card + * usable: the cards shipped with some units advertise 4-bit in their SCR, read + * on four lines and fail the first write there. A mount that comes up on four + * lines therefore proves the card takes a write: it reads the card's last + * sector, where no partition lives, and writes the same bytes back. When that + * read or write fails, or there is no memory to try it, the card is unmounted, + * its supply is cycled and it is mounted on one line, up to + * KDRV_SD_ONE_LINE_TRIES times with a cycle before each, and the outcome is + * told at NOTICE. A card that comes up on none of them is reported as one that + * would not come up. A mount that does not come up on four lines at all falls + * to one line without that check, and a mount asked on one line never writes. + * + * What a card took is remembered for the boot, in RAM, for the last card + * judged: it is known by what its CID says of it. A card that took the write is + * not checked again, and a card that took none is mounted on one line from the + * start, with no failed write and no cycle of its supply; if the card that + * answers there is another, it is judged as a card nobody judged. A different + * card is judged afresh, and a check that could not be made is no verdict. + * With `check_req` at 0 the card is mounted on the lines asked for, with no + * check and without consulting or changing what is remembered: the production + * test, which judges the slot, mounts that way. What a 1-bit mount MEANS is the * caller's to judge - the production test fails on it, because on a healthy * board all four lines reach the slot. * @@ -168,6 +210,18 @@ kode_status_t kdrv_sd_park(void); /** @brief Whether a card is currently mounted. */ bool kdrv_sd_mounted(void); +/** + * @brief Whether the mounted card sits where an everyday mount puts it. + * + * True for a card mounted with `width_req` at KDRV_SD_WIDTH_USE and the write + * check on, whatever width that came to: four lines for a card that took a + * write there, one for a card that took none or would not come up on four. + * False with nothing mounted, and for a mount asked on other terms: one line + * by request, or four lines with the check off. A caller that wants the + * everyday width back asks for it and remounts only when this says false. + */ +bool kdrv_sd_mounted_everyday(void); + /** @brief The card's mount point string, mounted or not. */ const char *kdrv_sd_mount_point(void); @@ -191,9 +245,10 @@ kode_status_t kdrv_sd_get(kdrv_sd_field_t f, int32_t *value); /** * @brief Write one field. * - * Only the two next-mount knobs can be written, and neither reaches the card: - * they are read by the next kdrv_sd_mount(). `width_req` takes 1 or 4 and - * nothing between, because those are the only widths an SD slot has. + * Only the next-mount knobs can be written, and none reaches the card: they + * are read by the next kdrv_sd_mount(). `width_req` takes 1 or 4 and nothing + * between, because those are the only widths an SD slot has; `check_req` takes + * 1 or 0. * * @param[in] f Which field. * @param[in] value Its new value, in the field's unit. diff --git a/components/kdrv/storage/src/kdrv_sd.c b/components/kdrv/storage/src/kdrv_sd.c index 652829d..1040f72 100644 --- a/components/kdrv/storage/src/kdrv_sd.c +++ b/components/kdrv/storage/src/kdrv_sd.c @@ -10,6 +10,7 @@ #include "kbsp.h" #include "khal_gpio.h" +#include "khal_system.h" #include "khal_time.h" #include "khal_sdmmc.h" #include "kode_log.h" @@ -29,6 +30,7 @@ static bool s_powered; static uint8_t s_width_req = (uint8_t)KDRV_SD_WIDTH_USE; static uint32_t s_freq_req = (uint32_t)KBSP_SD_FREQ_KHZ; +static bool s_check_req = true; /* ESP-IDF tells an empty slot in five lines a mount: the card's protocol, the * FAT mount and the pins the slot takes again, as errors. Every probe holds @@ -41,6 +43,43 @@ static uint32_t s_freq_req = (uint32_t)KBSP_SD_FREQ_KHZ; _Static_assert((SD_PROBE_TAG_COUNT + SD_OTHER_HOLDS) <= KODE_LOG_HOLD_MAX, "klog has a slot for each probe tag beside the other holds"); +/* A mount that comes up on four lines is proved with one sector: read, then + * written back as it was. */ + +#define SD_WIDTH_NARROW 1U /**< One data line, the width every card takes. */ +#define SD_CHECK_SECTORS 1U /**< Sectors the write check reads and writes back. */ +/** @brief Alignment of the check's buffer: a cache line, which a transfer to + * or from external memory needs. */ +#define SD_CHECK_ALIGN 64U + +/* What the driver remembers of the last card it judged, in RAM for the boot: + * what the card took on four lines, and who it is. A card is known by what the + * snapshot holds of its CID - manufacturer, OEM, name, revision, serial and + * date - and a card that differs in any of them is another. */ + +/** @brief What the last card judged took on four lines. */ +typedef enum { + SD_VERDICT_NONE = 0, /**< No card has been judged since the boot. */ + SD_VERDICT_FOUR, /**< It took a write there: four lines it is. */ + SD_VERDICT_ONE /**< It took none: one line it is. */ +} sd_verdict_t; + +/** @brief What a card is known by. */ +typedef struct { + uint8_t mfg_id; /**< CID manufacturer. */ + uint16_t oem_id; /**< CID OEM or application. */ + char name[KHAL_SDMMC_NAME_LEN]; /**< CID product name. */ + uint8_t revision; /**< CID product revision. */ + uint32_t serial; /**< CID serial number. */ + uint16_t date; /**< CID manufacturing date, raw. */ +} card_id_t; + +static sd_verdict_t s_verdict = SD_VERDICT_NONE; +static card_id_t s_judged; /**< Who s_verdict is about; valid with a verdict. */ + +/** @brief Whether the mount that stands was asked on everyday terms. */ +static bool s_everyday; + /* ---- The table ----------------------------------------------------------- */ /* Where a field's value comes from, which is also what decides its refusal. */ @@ -115,6 +154,7 @@ static const field_t FIELDS[KDRV_SD_FIELD_MAX] = { [KDRV_SD_WIDTH_REQ] = KNOB("width_req", "bits", 1, 4), [KDRV_SD_FREQ_REQ_KHZ] = KNOB("freq_req_khz", "kHz", 400, 50000), + [KDRV_SD_CHECK_REQ] = KNOB("check_req", "bool", 0, 1), }; /** @brief Megabytes in @p sectors of @p sector_size bytes. */ @@ -210,6 +250,7 @@ static void snap_field(kdrv_sd_field_t f, int32_t *value) case KDRV_SD_HOSTED: *value = khal_sdmmc_hosted() ? 1 : 0; break; case KDRV_SD_WIDTH_REQ: *value = (int32_t)s_width_req; break; case KDRV_SD_FREQ_REQ_KHZ: *value = (int32_t)s_freq_req; break; + case KDRV_SD_CHECK_REQ: *value = s_check_req ? 1 : 0; break; default: *value = 0; @@ -263,6 +304,10 @@ kode_status_t kdrv_sd_set(kdrv_sd_field_t f, int32_t value) } s_width_req = (uint8_t)value; } + else if ( KDRV_SD_CHECK_REQ == f ) + { + s_check_req = (0 != value); + } else { s_freq_req = (uint32_t)value; @@ -318,6 +363,7 @@ kode_status_t kdrv_sd_format(void) * goes with it: answering questions about a card nobody can reach is * exactly what the snapshot must not do. */ (void)memset(&s_snap, 0, sizeof(s_snap)); + s_everyday = false; } return st; } @@ -345,6 +391,11 @@ bool kdrv_sd_mounted(void) return khal_sdmmc_mounted(); } +bool kdrv_sd_mounted_everyday(void) +{ + return khal_sdmmc_mounted() && s_everyday; +} + void kdrv_sd_set_hosted(bool hosted) { khal_sdmmc_set_hosted(hosted); @@ -384,6 +435,23 @@ static void card_power(void) } } +/** + * @brief Cut the card's supply for its off time and switch it on again. + * + * What brings a card back to answering after a failed transfer: the host + * cannot be reset for it, because the controller is shared with the C5's link. + */ +static void card_cycle(void) +{ + if ( kdrv_sd_park() != KODE_OK ) + { + KLOG_W(TAG, "SD_PWR (GPIO%u) could not be driven; the card's supply " + "is not cycled", (unsigned)KBSP_PIN_SD_PWR); + } + khal_delay_ms(KDRV_SD_POWER_OFF_MS); + card_power(); +} + /** * @brief Hold ESP-IDF's account of an empty slot quiet, or let it speak again. * @@ -411,34 +479,259 @@ static void probe_quiet(bool quiet) } } +/** @brief What @p c is known by. */ +static card_id_t id_of(const khal_sdmmc_card_t *c) +{ + card_id_t id; + + (void)memset(&id, 0, sizeof(id)); + id.mfg_id = c->mfg_id; + id.oem_id = c->oem_id; + (void)memcpy(id.name, c->name, sizeof(id.name)); + id.revision = c->revision; + id.serial = c->serial; + id.date = c->date; + return id; +} + +/** @brief Whether @p c is the card the last verdict is about. */ +static bool is_judged(const khal_sdmmc_card_t *c) +{ + card_id_t const id = id_of(c); + + return (SD_VERDICT_NONE != s_verdict) + && (id.mfg_id == s_judged.mfg_id) + && (id.oem_id == s_judged.oem_id) + && (strncmp(id.name, s_judged.name, sizeof(id.name)) == 0) + && (id.revision == s_judged.revision) + && (id.serial == s_judged.serial) + && (id.date == s_judged.date); +} + /** - * @brief Mount four data lines first and then one, quietly after a miss. + * @brief Prove the card takes a write on the lines it came up on, leaving what + * it holds as it was. * - * @return What khal_sdmmc_mount() said of the last try. + * Reads the card's last sector and writes the same bytes back. No partition + * lives that far out, so a write cut short by a power loss costs nothing. + * + * @param[out] asked true when the card was asked and its answer is its + * verdict; false when the check could not be made. + * @return NULL when the read and the write-back both passed; otherwise why the + * card is not trusted on these lines, as a phrase for the log. + */ +static const char *card_check_write(bool *asked) +{ + const char *why = NULL; + void *buf = NULL; + + *asked = false; + if ( (0U != s_snap.sectors) && (0U != s_snap.sector_size) ) + { + buf = khal_alloc_psram(SD_CHECK_ALIGN, + (size_t)s_snap.sector_size * SD_CHECK_SECTORS); + } + if ( NULL == buf ) + { + return "there is no sector or no memory to check a write on four lines"; + } + + uint32_t const last = s_snap.sectors - SD_CHECK_SECTORS; + + *asked = true; + if ( kdrv_sd_read_sectors(last, SD_CHECK_SECTORS, buf) != KODE_OK ) + { + why = "the card's last sector cannot be read on four lines"; + } + else if ( kdrv_sd_write_sectors(last, SD_CHECK_SECTORS, buf) != KODE_OK ) + { + why = "the card takes no write on four lines"; + } + else + { + /* both transfers passed */ + } + khal_free(buf); + return why; +} + +/** + * @brief Take a card that was not shown to take a write on four lines down to + * one: unmount it, cycle its supply and mount it on one line, again + * after another cycle if it does not answer. + * + * Nothing is left describing the four-line mount. The host is not touched + * beyond the unmount and the mount: the C5's link shares its controller. + * + * @param why What the check found, as it is told at NOTICE once the card is up. + * @return KODE_OK with s_snap filled in for the one-line mount; KODE_DENIED + * when khal refused a mount without a try; KODE_ERROR with s_snap + * cleared when the card came up on none of the tries. + */ +static kode_status_t card_narrow(const char *why) +{ + kode_status_t st = KODE_ERROR; + uint8_t left = (uint8_t)KDRV_SD_ONE_LINE_TRIES; + + khal_sdmmc_unmount(); + (void)memset(&s_snap, 0, sizeof(s_snap)); + + while ( (0U < left) && (KODE_ERROR == st) ) + { + --left; + card_cycle(); + st = khal_sdmmc_mount(SD_WIDTH_NARROW, s_freq_req, &s_snap); + if ( (KODE_OK != st) && (KODE_DENIED != st) ) + { + /* A card that answered on four lines is no empty slot, whatever + * this try said: the caller reports it as one that would not come + * up. */ + KLOG_D(TAG, "1-bit mount after the supply's cycle failed (%d)", + (int)st); + st = KODE_ERROR; + } + } + if ( KODE_OK == st ) + { + KLOG_N(TAG, "%s; it is mounted on one line", why); + } + return st; +} + +/** + * @brief Settle a mount that came up on four lines: kept when the card is + * known to take a write there or proves to, narrowed to one otherwise. + * + * What a card took is remembered for the boot, so a card is judged once: a + * card that took the write is not checked again, and one that did not is + * mounted on one line from then on (card_mount_known()). A check that could + * not be made is no verdict. + * + * @return KODE_OK with s_snap describing the mount that stands; what + * card_narrow() answers when the card was narrowed. + */ +static kode_status_t card_settle(void) +{ + card_id_t const id = id_of(&s_snap); + bool asked = false; + + if ( (SD_VERDICT_FOUR == s_verdict) && is_judged(&s_snap) ) + { + KLOG_D(TAG, "the card took a write on four lines before; it is not " + "checked again"); + return KODE_OK; + } + + const char * const why = card_check_write(&asked); + + if ( NULL == why ) + { + s_judged = id; + s_verdict = SD_VERDICT_FOUR; + return KODE_OK; + } + + kode_status_t const st = card_narrow(why); + + if ( (KODE_OK == st) && asked ) + { + s_judged = id; + s_verdict = SD_VERDICT_ONE; + } + return st; +} + +/** + * @brief Mount on the lines asked for: four first and then one, a four-line + * mount settled by the write check when the check is asked for. + * + * @return What khal_sdmmc_mount() said of the last try, or card_settle()'s + * answer when the card came up on four lines. + */ +static kode_status_t card_mount_asked(void) +{ + kode_status_t st = khal_sdmmc_mount(s_width_req, s_freq_req, &s_snap); + + if ( (KODE_OK != st) && (KODE_DENIED != st) && (KODE_TIMEOUT != st) + && (SD_WIDTH_NARROW < s_width_req) ) + { + /* Four lines first, then one. A card that does not honour the 4-bit + * mode it advertises in its SCR is still worth having on one line. + * An empty slot is empty at one line too. */ + KLOG_D(TAG, "4-bit bring-up failed; retrying at 1-bit"); + st = khal_sdmmc_mount(SD_WIDTH_NARROW, s_freq_req, &s_snap); + } + if ( s_check_req && (KODE_OK == st) && (SD_WIDTH_NARROW < s_snap.bus_width) ) + { + /* Up on four lines. The counterfeit cards shipped with some units + * read there and fail the first write, and a failed write leaves the + * controller unable to bring a card up until the card's supply is + * cycled, so the write is tried here, on the card's last sector, and + * not by the first file. */ + st = card_settle(); + } + return st; +} + +/** + * @brief Mount on one line first, as the last card judged is mounted, and + * take the four-line way when another card is the one that answers. + * + * @return What khal_sdmmc_mount() said of a card that does not answer, or + * card_mount_asked()'s answer for a card that was not judged. + */ +static kode_status_t card_mount_known(void) +{ + kode_status_t st = khal_sdmmc_mount(SD_WIDTH_NARROW, s_freq_req, &s_snap); + + if ( KODE_OK != st ) + { + return st; + } + if ( is_judged(&s_snap) ) + { + KLOG_D(TAG, "the card took no write on four lines before; it is " + "mounted on one"); + return KODE_OK; + } + /* Another card: let go of it, and it is judged as a card nobody judged. */ + KLOG_D(TAG, "a card that was not judged is in the slot"); + khal_sdmmc_unmount(); + (void)memset(&s_snap, 0, sizeof(s_snap)); + return card_mount_asked(); +} + +/** + * @brief Mount the card, quietly after a miss. + * + * With the check asked for, a card remembered as one that takes no write on + * four lines is mounted on one at once; every other mount is card_mount_asked(). + * With the check off the mount is as asked and nothing is remembered or + * consulted. + * + * @return What the mount said of the card. */ static kode_status_t card_mount(void) { kode_status_t st; probe_quiet(true); - st = khal_sdmmc_mount(s_width_req, s_freq_req, &s_snap); - if ( (KODE_OK != st) && (KODE_DENIED != st) && (KODE_TIMEOUT != st) - && (4U == s_width_req) ) + if ( s_check_req && (SD_WIDTH_NARROW < s_width_req) + && (SD_VERDICT_ONE == s_verdict) ) { - /* Four lines first, then one. The fallback is not paranoia: the - * custom cards shipped with some units are counterfeit and do not - * honour the 4-bit mode they advertise in their SCR. Reads are what - * kodeOS needs and those work either way - it is 4-bit *writes* that - * fail on those cards. An empty slot is empty at one line too. */ - KLOG_D(TAG, "4-bit bring-up failed; retrying at 1-bit"); - st = khal_sdmmc_mount(1U, s_freq_req, &s_snap); + st = card_mount_known(); + } + else + { + st = card_mount_asked(); } probe_quiet(false); return st; } /** - * @brief Power the card and mount its volume, four data lines first and then one. + * @brief Power the card and mount its volume, four data lines first and then + * one, a four-line mount proved to take a write. * * @return KODE_OK with s_snap filled in; KODE_DENIED when no mount was tried * (the link holds the controller and is down) or khal refused it @@ -452,6 +745,7 @@ static kode_status_t card_up(void) * a repeat is not news. */ static bool s_missing = false; + s_everyday = false; card_power(); if ( !khal_sdmmc_ready() ) { @@ -463,7 +757,8 @@ static kode_status_t card_up(void) if ( KODE_OK == st ) { - s_missing = false; + s_missing = false; + s_everyday = s_check_req && ((uint8_t)KDRV_SD_WIDTH_USE == s_width_req); } else if ( KODE_DENIED == st ) { @@ -535,6 +830,7 @@ void kdrv_sd_unmount(void) } khal_sdmmc_unmount(); (void)memset(&s_snap, 0, sizeof(s_snap)); + s_everyday = false; KLOG_I(TAG, "card unmounted"); } diff --git a/tests/host/cases/test_console_sd.c b/tests/host/cases/test_console_sd.c index eff70f7..0cf2569 100644 --- a/tests/host/cases/test_console_sd.c +++ b/tests/host/cases/test_console_sd.c @@ -331,7 +331,7 @@ TEST_CASE("sd_help lists every field, paced, and closes with ok", "[console_sd]" fake_klog_reset(); (void)qf_support_tick(AO_Console, 1U); TEST_ASSERT_TRUE( - replied(">|field=\"freq_req_khz\" unit=kHz min=400 max=50000 rw=rw")); + replied(">|field=\"check_req\" unit=bool min=0 max=1 rw=rw")); TEST_ASSERT_TRUE(replied(">ok")); } diff --git a/tests/host/cases/test_kdrv_sd.c b/tests/host/cases/test_kdrv_sd.c index 498fa82..dc05281 100644 --- a/tests/host/cases/test_kdrv_sd.c +++ b/tests/host/cases/test_kdrv_sd.c @@ -1,17 +1,21 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /** * @file test_kdrv_sd.c - * @brief kdrv_sd: the card's table, the bring-up retry, and the two knobs. + * @brief kdrv_sd: the card's table, the bring-up retry, the write check on + * four lines, and the two knobs. */ #include #include "unity.h" +#include "Mockkhal_gpio.h" + #include "fakes.h" #include "kbsp.h" #include "kdrv_sd.h" #include "kode_log.h" #include "khal_sdmmc.h" +#include "khal_time.h" /** @brief A card whose every field is a different, recognisable number. */ static void install_card(void) @@ -155,7 +159,7 @@ TEST_CASE("a mount takes the card's identity, and every field reads it back", TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_HIGH_CAPACITY)); /* What was negotiated, not what the card offers: an everyday mount asks - * for one data line, whatever the card's SCR says it can do. */ + * for the slot's four data lines, whatever the card's SCR says it can do. */ TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, field(KDRV_SD_BUS_WIDTH)); TEST_ASSERT_EQUAL_INT32(40000, field(KDRV_SD_FREQ_KHZ)); TEST_ASSERT_EQUAL_INT32(50000, field(KDRV_SD_CARD_MAX_KHZ)); @@ -201,19 +205,15 @@ TEST_CASE("the two volume fields come from the filesystem, not the card", /* ---- The bring-up retry ---------------------------------------------------- */ -/* Asked for four lines, the driver tries four first, then one. The - * counterfeit cards advertise 4-bit in their SCR and do not honour it, and a - * card that reads on one line is still worth having - what a 1-bit mount - * MEANS is judged higher up. Only the card row asks for four now, so the case - * asks for them itself. */ +/* Asked for four lines, the driver tries four first, then one. A card that + * does not honour the 4-bit mode it advertises in its SCR is still worth + * having on one line - what a 1-bit mount MEANS is judged higher up. */ TEST_CASE("a card that will not come up on four lines is mounted on one", "[kdrv_sd]") { unsigned const before = fake_sdmmc_mounts(); kdrv_sd_unmount(); - TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, - (int32_t)KBSP_SD_WIDTH)); fake_sdmmc_set_max_width(1U); TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); @@ -418,6 +418,663 @@ TEST_CASE("a mount khal refuses is not retried and is not a missing card", TEST_ASSERT_EQUAL_UINT(1U, told_no_card()); } +/* ---- The write check on four lines ---------------------------------------- */ + +/** @brief Bytes in a sector of the cards the fake holds. */ +#define TEST_SECTOR_BYTES 512U + +/** @brief The last sector of the card a reset installs. */ +static uint32_t default_last_sector(void) +{ + khal_sdmmc_card_t c; + + fake_sdmmc_default_card(&c); + return c.sectors - 1U; +} + +/** + * @brief Assert call @p idx of the seam's trace reads @p want. + * @param idx 0-based. + * @param want The call, as the fake spells it. + */ +static void trace_is(unsigned idx, const char *want) +{ + char what[48]; + const char *got = fake_sdmmc_trace_line(idx); + + (void)snprintf(what, sizeof(what), "call %u of the trace", idx); + TEST_ASSERT_NOT_NULL_MESSAGE(got, what); + TEST_ASSERT_EQUAL_STRING_MESSAGE(want, got, what); +} + +/** @brief The same for a call that names a number: ` `. */ +static void trace_is_n(unsigned idx, const char *name, uint32_t n) +{ + char want[40]; + + (void)snprintf(want, sizeof(want), "%s %lu", name, (unsigned long)n); + trace_is(idx, want); +} + +/** + * @brief How many captured lines at @p level carry @p text. + * @param level 'N', 'W', 'E'...: the line's level letter. + * @param text What the line says. + */ +static unsigned lines_saying(char level, const char *text) +{ + unsigned n = 0U; + + for ( unsigned l = 0U; fake_klog_count() > l; ++l ) + { + const char *line = fake_klog_line(l); + + if ( (NULL != line) && (level == line[1]) && (NULL != strstr(line, text)) ) + { + ++n; + } + } + return n; +} + +/** + * @brief The card's supply, written into the seam's trace so its order + * against the mounts shows in one list. + */ +static kode_status_t supply_to_trace(uint8_t pin, bool level, int calls) +{ + (void)calls; + TEST_ASSERT_EQUAL_UINT8((uint8_t)KBSP_PIN_SD_PWR, pin); + if ( level ) + { + fake_sdmmc_trace_note("supply off"); /* SD_PWR high is Q1 off */ + } + else + { + fake_sdmmc_trace_note("supply on"); + } + return KODE_OK; +} + +/** @brief Watch SD_PWR: from here on every drive of it shows in the trace. */ +static void watch_supply(void) +{ + Mockkhal_gpio_Init(); + khal_gpio_output_StubWithCallback(&supply_to_trace); + khal_gpio_driven_low_IgnoreAndReturn(false); +} + +/** @brief Stop watching SD_PWR. */ +static void unwatch_supply(void) +{ + Mockkhal_gpio_Verify(); + Mockkhal_gpio_Destroy(); +} + +/** @brief Unmount, put a card nobody has judged in the slot, and forget + * everything counted, ready for one mount. */ +static void ready_one_mount(void) +{ + kdrv_sd_unmount(); + fake_sdmmc_fresh_card(); + fake_sdmmc_trace_reset(); + fake_psram_reset(); + fake_klog_reset(); +} + +/* The slot is wired for four lines and an everyday mount uses them. */ +TEST_CASE("an everyday mount asks for the slot's four data lines", "[kdrv_sd]") +{ + kdrv_sd_unmount(); + + TEST_ASSERT_EQUAL_UINT32((uint32_t)KBSP_SD_WIDTH, (uint32_t)KDRV_SD_WIDTH_USE); + TEST_ASSERT_EQUAL_INT32((int32_t)KBSP_SD_WIDTH, field(KDRV_SD_WIDTH_REQ)); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT8((uint8_t)KBSP_SD_WIDTH, fake_sdmmc_last_width()); + TEST_ASSERT_EQUAL_INT32((int32_t)KBSP_SD_WIDTH, field(KDRV_SD_BUS_WIDTH)); +} + +/* The proof is a sector read and the same bytes written back at the far end: + * no partition lives in the last sector, and what the card holds there is + * what it held. */ +TEST_CASE("a four-line mount whose last sector takes the write-back stays on " + "four lines and changes nothing", "[kdrv_sd]") +{ + uint8_t pattern[TEST_SECTOR_BYTES]; + uint8_t back[TEST_SECTOR_BYTES]; + uint32_t const last = default_last_sector(); + + for ( unsigned i = 0U; sizeof(pattern) > i; ++i ) + { + pattern[i] = (uint8_t)((i * 7U) ^ 0xA5U); + } + /* A pattern in the last sector, put there through a mount on one line, + * which checks nothing. */ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, 1)); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_write_sectors(last, 1U, pattern)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, + (int32_t)KBSP_SD_WIDTH)); + ready_one_mount(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT(3U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + trace_is_n(1U, "read", last); + trace_is_n(2U, "write", last); + + (void)memset(back, 0, sizeof(back)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_read_sectors(last, 1U, back)); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE(pattern, back, sizeof(pattern), + "the check changed what the card holds"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(fake_psram_alloc_count(), fake_psram_free_count(), + "the check's buffer is given back"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, lines_saying('N', "four lines"), + "a card that passes is not news"); + + /* A card already mounted is not checked again. */ + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT(4U, fake_sdmmc_trace_count()); /* the read above */ +} + +/* What a counterfeit card does: it reads on four lines and fails the first + * write there. The controller is shared with the C5's link, so the way home is + * the card's own supply, not the host. */ +TEST_CASE("a write-back that fails ends in an unmount, a supply cycle and a " + "one-line mount", "[kdrv_sd]") +{ + uint32_t const last = default_last_sector(); + uint32_t t0; + + fake_sdmmc_fail_wide_writes(true); + watch_supply(); + ready_one_mount(); + t0 = khal_time_ms(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(8U, fake_sdmmc_trace_count()); + trace_is(0U, "supply on"); + trace_is_n(1U, "mount", 4U); + trace_is_n(2U, "read", last); + trace_is_n(3U, "write", last); + trace_is(4U, "unmount"); + trace_is(5U, "supply off"); + trace_is(6U, "supply on"); + trace_is_n(7U, "mount", 1U); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(KDRV_SD_POWER_OFF_MS + KDRV_SD_POWER_SETTLE_MS, + khal_time_ms() - t0, + "the supply is off for its time and settles"); + + /* The snapshot describes the mount that stands. */ + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_MOUNTED)); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('N', "takes no write on four lines")); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, loud_lines(), "a card that works is no fault"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(fake_psram_alloc_count(), fake_psram_free_count(), + "the check's buffer is given back"); + unwatch_supply(); +} + +TEST_CASE("a read-back that fails ends the same way", "[kdrv_sd]") +{ + uint32_t const last = default_last_sector(); + + fake_sdmmc_fail_wide_reads(true); + watch_supply(); + ready_one_mount(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(7U, fake_sdmmc_trace_count()); + trace_is(0U, "supply on"); + trace_is_n(1U, "mount", 4U); + trace_is_n(2U, "read", last); + trace_is(3U, "unmount"); /* no write was tried after the read failed */ + trace_is(4U, "supply off"); + trace_is(5U, "supply on"); + trace_is_n(6U, "mount", 1U); + + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('N', "cannot be read on four lines")); + TEST_ASSERT_EQUAL_UINT(0U, fake_sdmmc_raw_writes()); + unwatch_supply(); +} + +/* The card may need its supply cycled, or a moment, before it answers again. */ +TEST_CASE("a one-line mount that fails is tried once more after another cycle", + "[kdrv_sd]") +{ + uint32_t const last = default_last_sector(); + uint32_t t0; + + fake_sdmmc_fail_wide_writes(true); + fake_sdmmc_fail_mounts(1U, 1U); + watch_supply(); + ready_one_mount(); + t0 = khal_time_ms(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(11U, fake_sdmmc_trace_count()); + trace_is(0U, "supply on"); + trace_is_n(1U, "mount", 4U); + trace_is_n(2U, "read", last); + trace_is_n(3U, "write", last); + trace_is(4U, "unmount"); + trace_is(5U, "supply off"); + trace_is(6U, "supply on"); + trace_is_n(7U, "mount", 1U); /* does not answer */ + trace_is(8U, "supply off"); + trace_is(9U, "supply on"); + trace_is_n(10U, "mount", 1U); + TEST_ASSERT_EQUAL_UINT32(2U * (KDRV_SD_POWER_OFF_MS + KDRV_SD_POWER_SETTLE_MS), + khal_time_ms() - t0); + + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('N', "takes no write on four lines")); + TEST_ASSERT_EQUAL_UINT(0U, loud_lines()); + unwatch_supply(); +} + +/* A card that answered on four lines and on neither try on one is not an empty + * slot, whatever the last try said: the report is the one for a card that would + * not come up. */ +TEST_CASE("a card that comes up on neither is reported as one that would not " + "come up", "[kdrv_sd]") +{ + int32_t value = 0; + + fake_sdmmc_fail_wide_writes(true); + fake_sdmmc_fail_mounts(1U, 2U); + watch_supply(); + ready_one_mount(); + + TEST_ASSERT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(11U, fake_sdmmc_trace_count()); + trace_is_n(10U, "mount", 1U); + TEST_ASSERT_FALSE(kdrv_sd_mounted()); + TEST_ASSERT_EQUAL_INT(KODE_DENIED, (int)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &value)); + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('E', "would not come up")); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, told_no_card(), "the card was there"); + TEST_ASSERT_EQUAL_UINT(0U, lines_saying('N', "takes no write")); + TEST_ASSERT_EQUAL_UINT(fake_psram_alloc_count(), fake_psram_free_count()); + unwatch_supply(); +} + +TEST_CASE("a mount asked on one line does no write check", "[kdrv_sd]") +{ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, 1)); + ready_one_mount(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 1U); + TEST_ASSERT_EQUAL_UINT(0U, fake_sdmmc_raw_writes()); + TEST_ASSERT_EQUAL_UINT(0U, fake_psram_alloc_count()); +} + +TEST_CASE("an empty slot does no write check", "[kdrv_sd]") +{ + ready_one_mount(); + fake_sdmmc_set_present(false); + + TEST_ASSERT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + TEST_ASSERT_EQUAL_UINT(0U, fake_psram_alloc_count()); +} + +TEST_CASE("a mount khal refuses does no write check", "[kdrv_sd]") +{ + ready_one_mount(); + fake_sdmmc_set_deny(true); + + TEST_ASSERT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + TEST_ASSERT_EQUAL_UINT(0U, fake_psram_alloc_count()); +} + +TEST_CASE("a card that came up on one line after the four-line try failed is " + "not checked either", "[kdrv_sd]") +{ + ready_one_mount(); + fake_sdmmc_set_max_width(1U); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(2U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + trace_is_n(1U, "mount", 1U); + TEST_ASSERT_EQUAL_UINT(0U, fake_sdmmc_raw_writes()); +} + +/* A check that cannot run proves nothing, and a card that is not proven is + * mounted where every card works. */ +TEST_CASE("with no memory for the check the card is mounted on one line", + "[kdrv_sd]") +{ + ready_one_mount(); + fake_psram_refuse(1U); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(3U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + trace_is(1U, "unmount"); + trace_is_n(2U, "mount", 1U); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('N', "no memory")); +} + +/* ---- The check knob and what the driver remembers of a card ---------------- */ + +/* The production row judges the slot, so its four lines are mounted as asked: + * no check, and no memory of what an earlier mount decided. */ +TEST_CASE("a four-line mount with the check off does no check and ignores what " + "is remembered", "[kdrv_sd]") +{ + fake_sdmmc_fail_wide_writes(true); + ready_one_mount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* judged: one line */ + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 0)); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); + + /* What was remembered is still there when the check is back on. */ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1)); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 1U); +} + +TEST_CASE("a card judged good on four lines is not checked again this boot", + "[kdrv_sd]") +{ + ready_one_mount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT(3U, fake_sdmmc_trace_count()); /* mount, read, write */ + + for ( unsigned i = 0U; 3U > i; ++i ) + { + kdrv_sd_unmount(); + fake_sdmmc_trace_reset(); + fake_psram_reset(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(1U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + TEST_ASSERT_EQUAL_UINT(0U, fake_sdmmc_raw_writes()); + TEST_ASSERT_EQUAL_UINT(0U, fake_psram_alloc_count()); + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); + } +} + +/* A card that took no write on four lines is where the slot was left: the next + * everyday mount goes to one line, with no failed write and no cycle of the + * supply to get there. */ +TEST_CASE("a card judged one line is mounted on one line on later everyday " + "mounts", "[kdrv_sd]") +{ + fake_sdmmc_fail_wide_writes(true); + watch_supply(); + ready_one_mount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* judged */ + TEST_ASSERT_EQUAL_UINT(1U, lines_saying('N', "takes no write on four lines")); + + for ( unsigned i = 0U; 3U > i; ++i ) + { + kdrv_sd_unmount(); + fake_sdmmc_trace_reset(); + fake_klog_reset(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(2U, fake_sdmmc_trace_count()); + trace_is(0U, "supply on"); + trace_is_n(1U, "mount", 1U); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, lines_saying('N', "takes no write"), + "the verdict is told once"); + } + unwatch_supply(); +} + +/* Only the card that was judged is mounted on one line on sight: another one + * in the slot takes the way a card nobody judged takes. */ +TEST_CASE("a different card in the slot of a card judged one line takes the " + "four-line path", "[kdrv_sd]") +{ + uint32_t const last = default_last_sector(); + + fake_sdmmc_fail_wide_writes(true); + ready_one_mount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* judged: one line */ + + kdrv_sd_unmount(); + fake_sdmmc_fail_wide_writes(false); + fake_sdmmc_fresh_card(); /* another card */ + fake_sdmmc_trace_reset(); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(5U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 1U); /* whose is it? */ + trace_is(1U, "unmount"); + trace_is_n(2U, "mount", 4U); + trace_is_n(3U, "read", last); + trace_is_n(4U, "write", last); + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); +} + +/** @brief Which field of a card's identity a case changes. */ +typedef enum { + ID_MFG, + ID_OEM, + ID_NAME, + ID_REVISION, + ID_SERIAL, + ID_DATE, + ID_COUNT +} id_field_t; + +/** @brief A card that differs from @p c in the one identifying field @p f. */ +static void other_card(khal_sdmmc_card_t *c, id_field_t f) +{ + switch ( f ) + { + case ID_MFG: c->mfg_id ^= 0x01U; break; + case ID_OEM: c->oem_id ^= 0x0100U; break; + case ID_NAME: c->name[0] = 'Z'; break; + case ID_REVISION: c->revision ^= 0x01U; break; + case ID_SERIAL: c->serial ^= 0x00010000U; break; + case ID_DATE: c->date ^= 0x0001U; break; + default: break; + } +} + +/* What the driver knows a card by is what the snapshot already holds of its + * CID: the manufacturer, the OEM, the name, the revision, the serial and the + * date. Any one of them different is another card. */ +TEST_CASE("a card that differs in any field of its identity is judged afresh", + "[kdrv_sd]") +{ + for ( unsigned f = 0U; (unsigned)ID_COUNT > f; ++f ) + { + khal_sdmmc_card_t a; + khal_sdmmc_card_t b; + + fake_sdmmc_default_card(&a); + a.serial = 0xAAAA0000U + f; + b = a; + other_card(&b, (id_field_t)f); + + kdrv_sd_unmount(); + fake_sdmmc_set_card(&a); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* A judged good */ + + kdrv_sd_unmount(); + fake_sdmmc_set_card(&b); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT_MESSAGE(3U, fake_sdmmc_trace_count(), + "B was not judged: mount, read, write"); + + /* The same card again is not judged again. */ + kdrv_sd_unmount(); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, fake_sdmmc_trace_count(), + "B was judged again"); + } +} + +TEST_CASE("a card judged good that comes back after another is judged afresh", + "[kdrv_sd]") +{ + khal_sdmmc_card_t a; + khal_sdmmc_card_t b; + + fake_sdmmc_default_card(&a); + a.serial = 0xAAAA0001U; + b = a; + b.serial = 0xBBBB0002U; + + kdrv_sd_unmount(); + fake_sdmmc_set_card(&a); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* A: good */ + kdrv_sd_unmount(); + fake_sdmmc_set_card(&b); + fake_sdmmc_fail_wide_writes(true); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* B: one line */ + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + + kdrv_sd_unmount(); + fake_sdmmc_set_card(&a); + fake_sdmmc_fail_wide_writes(false); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); /* A is new to it again */ + + TEST_ASSERT_EQUAL_UINT(5U, fake_sdmmc_trace_count()); /* mount 1, unmount, 4, read, write */ + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); +} + +/* A mount whose check or memory could not say anything is not the verdict of a + * card: a card that would not come up on four lines is tried on four again. */ +TEST_CASE("a card that would not come up on four lines leaves no verdict", + "[kdrv_sd]") +{ + ready_one_mount(); + fake_sdmmc_set_max_width(1U); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + kdrv_sd_unmount(); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(2U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + trace_is_n(1U, "mount", 1U); +} + +/* A check that could not be made says nothing about the card: the next mount + * judges it, and finds it takes the write. */ +TEST_CASE("a check that could not be made is no verdict", "[kdrv_sd]") +{ + uint32_t const last = default_last_sector(); + + ready_one_mount(); + fake_psram_refuse(1U); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + + kdrv_sd_unmount(); + fake_sdmmc_trace_reset(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_UINT(3U, fake_sdmmc_trace_count()); + trace_is_n(0U, "mount", 4U); + trace_is_n(1U, "read", last); + trace_is_n(2U, "write", last); + TEST_ASSERT_EQUAL_INT32(4, field(KDRV_SD_BUS_WIDTH)); +} + +/* What a caller that wants the everyday width back needs to know: the card is + * where an everyday mount puts it, which for a card that took no write on four + * lines is one. */ +TEST_CASE("a card narrowed by the check counts as mounted the everyday way", + "[kdrv_sd]") +{ + kdrv_sd_unmount(); + TEST_ASSERT_FALSE_MESSAGE(kdrv_sd_mounted_everyday(), "nothing is mounted"); + + fake_sdmmc_fail_wide_writes(true); + ready_one_mount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_TRUE_MESSAGE(kdrv_sd_mounted_everyday(), "narrowed by the check"); + + /* The same card mounted from the memory of it. */ + kdrv_sd_unmount(); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); + + /* One line asked for is not the everyday mount. */ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, 1)); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_FALSE_MESSAGE(kdrv_sd_mounted_everyday(), "one line by request"); + + /* Neither is four lines with the check off. */ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_WIDTH_REQ, + (int32_t)KDRV_SD_WIDTH_USE)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 0)); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_FALSE_MESSAGE(kdrv_sd_mounted_everyday(), "four lines, unchecked"); + + /* Everyday terms again, and so is the mount. */ + kdrv_sd_unmount(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1)); + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); + kdrv_sd_unmount(); + TEST_ASSERT_FALSE(kdrv_sd_mounted_everyday()); +} + +/* A card that will not come up on four lines is on one line the way every + * everyday mount of it ends: a caller has no better width to ask for. */ +TEST_CASE("a card that fell to one line at bring-up counts as mounted the " + "everyday way", "[kdrv_sd]") +{ + ready_one_mount(); + fake_sdmmc_set_max_width(1U); + + TEST_ASSERT_NOT_NULL(kdrv_sd_mount()); + + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_BUS_WIDTH)); + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); +} + /* ---- The two knobs --------------------------------------------------------- */ TEST_CASE("width_req takes one line or four and nothing between", "[kdrv_sd]") @@ -442,6 +1099,17 @@ TEST_CASE("freq_req_khz is bounded at both ends", "[kdrv_sd]") TEST_ASSERT_EQUAL_INT32(50000, field(KDRV_SD_FREQ_REQ_KHZ)); } +TEST_CASE("check_req takes one or zero and nothing else", "[kdrv_sd]") +{ + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_CHECK_REQ)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 0)); + TEST_ASSERT_EQUAL_INT32(0, field(KDRV_SD_CHECK_REQ)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1)); + TEST_ASSERT_EQUAL_INT(KODE_DENIED, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 2)); + TEST_ASSERT_EQUAL_INT(KODE_DENIED, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, -1)); + TEST_ASSERT_EQUAL_INT32(1, field(KDRV_SD_CHECK_REQ)); +} + /* The knobs are only worth having if the next mount reads them. */ TEST_CASE("the next mount asks for what the knobs say", "[kdrv_sd]") { @@ -496,7 +1164,7 @@ TEST_CASE("every field has a name, a unit and a range, and resolves to itself", } } -TEST_CASE("exactly two fields are writable, and they are the knobs", "[kdrv_sd]") +TEST_CASE("exactly three fields are writable, and they are the knobs", "[kdrv_sd]") { unsigned writable = 0U; @@ -507,9 +1175,10 @@ TEST_CASE("exactly two fields are writable, and they are the knobs", "[kdrv_sd]" ++writable; } } - TEST_ASSERT_EQUAL_UINT(2U, writable); + TEST_ASSERT_EQUAL_UINT(3U, writable); TEST_ASSERT_TRUE(kdrv_sd_field_info(KDRV_SD_WIDTH_REQ)->writable); TEST_ASSERT_TRUE(kdrv_sd_field_info(KDRV_SD_FREQ_REQ_KHZ)->writable); + TEST_ASSERT_TRUE(kdrv_sd_field_info(KDRV_SD_CHECK_REQ)->writable); } TEST_CASE("a field nobody named is refused, and so is one past the table", diff --git a/tests/host/cases/test_storage_ao.c b/tests/host/cases/test_storage_ao.c index abcfe28..0fe8eee 100644 --- a/tests/host/cases/test_storage_ao.c +++ b/tests/host/cases/test_storage_ao.c @@ -2392,7 +2392,7 @@ TEST_CASE("the card row leaves nothing of its own behind and does not format", * middle of the run did with it: leaving a unit on four data lines after a * production test would leave a counterfeit card one failed write away from a * controller that only a reset brings back. */ -TEST_CASE("the card row hands the slot back at one data line", +TEST_CASE("the card row hands the slot back on the everyday width", "[storage_ao]") { bool ok[SD_TEST_STEPS]; @@ -2408,6 +2408,90 @@ TEST_CASE("the card row hands the slot back at one data line", TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, width); } +/** @brief How many calls the card seam traced whose text starts with @p what. */ +static unsigned traced(const char *what) +{ + unsigned n = 0U; + + for ( unsigned i = 0U; fake_sdmmc_trace_count() > i; ++i ) + { + const char * const line = fake_sdmmc_trace_line(i); + + if ( (NULL != line) && (strncmp(line, what, strlen(what)) == 0) ) + { + ++n; + } + } + return n; +} + +/* The row judges the slot, so its four lines are mounted as asked. The check + * that keeps a counterfeit card off four lines would narrow the card before + * the row looked, and a bad card would fail a good board. */ +TEST_CASE("the card row's four-line mount does no write check", "[storage_ao]") +{ + sd_script_fresh(); + fake_sdmmc_fail_wide_writes(true); /* reads on four lines, takes no write */ + fake_sdmmc_fresh_card(); + fake_sdmmc_trace_reset(); + + ask_sd_test(); + TEST_ASSERT_TRUE(run_sd_job()); /* the script's remount */ + + TEST_ASSERT_EQUAL_UINT(2U, fake_sdmmc_trace_count()); + TEST_ASSERT_EQUAL_STRING("unmount", fake_sdmmc_trace_line(0U)); + TEST_ASSERT_EQUAL_STRING("mount 4", fake_sdmmc_trace_line(1U)); + + run_sd_script(); + const StorageTestEvt * const t = take_phase(SD_TEST_IDENTIFY); + TEST_ASSERT_TRUE_MESSAGE(t->ok, "the card was narrowed before the row looked"); + TEST_ASSERT_EQUAL_UINT8(4U, t->width); +} + +/* A card that takes no write on four lines is the card's fault, not the + * slot's: the row passes it, and the mount that follows the row - the slot + * handed back on the everyday width - is the checked one, which leaves the + * card on one line. The row asks for nothing more after that: a card on the + * line it was judged to need is where an everyday mount puts it. */ +TEST_CASE("the card row on a card that takes no write on four lines passes the " + "slot and hands the card back on one line", "[storage_ao]") +{ + unsigned const chunks = (KODE_SD_TEST_KB * 1024U) / (32U * 1024U); + int32_t width = 0; + + sd_script_fresh(); + fake_sdmmc_fail_wide_writes(true); + fake_sdmmc_fresh_card(); + fake_sdmmc_trace_reset(); + + ask_sd_test(); + run_sd_script(); + + const StorageTestEvt *t = take_phase(SD_TEST_IDENTIFY); + TEST_ASSERT_TRUE(t->ok); + TEST_ASSERT_EQUAL_UINT8(4U, t->width); + t = take_phase(SD_TEST_READ4); + TEST_ASSERT_TRUE(t->ok); + TEST_ASSERT_EQUAL_UINT8(4U, t->width); + t = take_phase(SD_TEST_READ1); + TEST_ASSERT_TRUE(t->ok); + TEST_ASSERT_EQUAL_UINT8(1U, t->width); + TEST_ASSERT_TRUE(take_phase(SD_TEST_WRITE)->ok); + TEST_ASSERT_TRUE(take_phase(SD_TEST_VERIFY)->ok); + TEST_ASSERT_TRUE_MESSAGE(take_verdict("sd"), "a bad card condemned a good slot"); + + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, traced("write"), + "one failed write: the checked mount after " + "the row"); + TEST_ASSERT_EQUAL_UINT(2U, traced("mount 4")); /* the row's, the restore's */ + TEST_ASSERT_EQUAL_UINT_MESSAGE((2U * chunks) + 3U, sd_jobs_run, + "a card on one line was remounted for it"); + TEST_ASSERT_EQUAL_UINT(0U, fake_khal_worker_pending_for(SD_WORKER)); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width)); + TEST_ASSERT_EQUAL_INT32(1, width); + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); +} + /* Three of the four data lines not reaching the slot is a defect of the unit, * and every phase below would be measuring the wrong thing. */ TEST_CASE("a card that comes up on one data line fails the board's verdict", @@ -2438,14 +2522,15 @@ TEST_CASE("an empty slot fails the board's verdict", "[storage_ao]") } /* The heart of the row: a data line that does not carry shows up as a read - * that will not complete, because the peripheral checks a CRC per line. */ + * that will not complete, because the peripheral checks a CRC per line. The + * one read the mount makes to prove the card passes; the row's are behind it. */ TEST_CASE("a four-line read that will not complete fails the board's verdict", "[storage_ao]") { bool ok[SD_TEST_STEPS]; sd_script_fresh(); - fake_sdmmc_fail_reads(true); + fake_sdmmc_fail_reads_after(1U); ask_sd_test(); run_sd_script(); @@ -3073,9 +3158,9 @@ TEST_CASE("a done event that is not the call's is dropped by its sequence", * create() may place it on the internal partition and a PSRAM stack must not * touch that flash. So the count of calls the worker makes is the audit: a * remount to start, the four-line chunks, a remount onto one line and its - * chunks - and no call for the file. The file is written on that one line, - * the width the slot is handed back at, so no remount comes before it or - * after the verdict. */ + * chunks, a remount back onto the everyday width - and no call for the file. + * The file is written on that width, the one the slot is handed back at, so + * no remount comes after the verdict. */ TEST_CASE("the card row's remounts and raw reads are jobs; its file is not", "[storage_ao][worker]") { @@ -3085,7 +3170,7 @@ TEST_CASE("the card row's remounts and raw reads are jobs; its file is not", ask_sd_test(); run_sd_script(); - TEST_ASSERT_EQUAL_UINT((2U * chunks) + 2U, sd_jobs_run); + TEST_ASSERT_EQUAL_UINT((2U * chunks) + 3U, sd_jobs_run); TEST_ASSERT_EQUAL_UINT(0U, fake_khal_worker_pending_for(SD_WORKER)); TEST_ASSERT_TRUE(take_phase(SD_TEST_IDENTIFY)->ok); } @@ -3106,6 +3191,23 @@ TEST_CASE("sd_verify's probe and format are jobs", "[storage_ao][worker]") TEST_ASSERT_TRUE(take_verdict("sd_verify")); } +/* sd_verify judges the card on the width it is used at: through the checked + * mount, whatever an earlier run or a bench session left in the knob. */ +TEST_CASE("sd_verify mounts the card the everyday way", "[storage_ao]") +{ + sd_script_fresh(); + TEST_ASSERT_EQUAL_INT(KODE_OK, (int)kdrv_sd_set(KDRV_SD_CHECK_REQ, 0)); + fake_sdmmc_fresh_card(); + fake_sdmmc_trace_reset(); + + ask_sd_verify(); + TEST_ASSERT_TRUE(run_sd_job()); /* the script's remount */ + + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, traced("write"), "the card was not checked"); + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); + run_sd_script(); +} + TEST_CASE("a card row with no room for a call is refused and answered", "[storage_ao][worker]") { @@ -3172,10 +3274,11 @@ TEST_CASE("a call that overruns inside the card row ends the run with a false " "the AO stayed wedged after the late return"); } -/* An overrun in the four-line read leaves the slot on four data lines: it is - * put back on one when the call returns, not skipped. */ -TEST_CASE("a call that overruns on four data lines puts the slot back on one " - "when it returns", "[storage_ao][worker]") +/* An overrun in the four-line read leaves the slot on four lines the row asked + * for without the write check: it is put back through the checked mount when + * the call returns, not skipped. */ +TEST_CASE("a call that overruns on four data lines puts the slot back through " + "the checked mount when it returns", "[storage_ao][worker]") { unsigned const steps = QF_SUPPORT_STEPS(KDRV_SD_SECTORS_MAX_MS + KODE_STORAGE_JOB_MARGIN_MS); @@ -3189,6 +3292,7 @@ TEST_CASE("a call that overruns on four data lines puts the slot back on one " TEST_ASSERT_EQUAL_UINT(1U, fake_khal_worker_pending_for(SD_WORKER)); (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); TEST_ASSERT_EQUAL_INT32((int32_t)KBSP_SD_WIDTH, width); + TEST_ASSERT_FALSE(kdrv_sd_mounted_everyday()); ao_spy_drain(); (void)qf_support_tick(AO_Storage, steps); @@ -3198,18 +3302,18 @@ TEST_CASE("a call that overruns on four data lines puts the slot back on one " TEST_ASSERT_TRUE(run_sd_job()); /* the late chunk comes back... */ TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, fake_khal_worker_pending_for(SD_WORKER), - "the width was not asked for back"); + "the card was left without its check"); TEST_ASSERT_TRUE(run_sd_job()); /* ...and the remount puts it right. */ + TEST_ASSERT_TRUE(kdrv_sd_mounted_everyday()); (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, width); TEST_ASSERT_EQUAL_UINT(0U, ao_spy_count()); } -/* An overrun in the one-line read leaves the slot on the width it is handed - * back at already: the late return owes nothing, and asking the card for a - * remount anyway would be a call for no reason. */ -TEST_CASE("a call that overruns on one data line owes no remount when it " - "returns", "[storage_ao][worker]") +/* An overrun in the one-line read leaves the slot on one data line: it is put + * back on the width it is handed back at when the call returns, not skipped. */ +TEST_CASE("a call that overruns on one data line puts the slot back on four " + "when it returns", "[storage_ao][worker]") { unsigned const steps = QF_SUPPORT_STEPS(KDRV_SD_SECTORS_MAX_MS + KODE_STORAGE_JOB_MARGIN_MS); @@ -3232,7 +3336,7 @@ TEST_CASE("a call that overruns on one data line owes no remount when it " (void)qf_support_pump(AO_Storage, 8U); TEST_ASSERT_EQUAL_UINT(1U, fake_khal_worker_pending_for(SD_WORKER)); (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); - TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, width); + TEST_ASSERT_EQUAL_INT32(1, width); ao_spy_drain(); (void)qf_support_tick(AO_Storage, steps); @@ -3240,10 +3344,10 @@ TEST_CASE("a call that overruns on one data line owes no remount when it " TEST_ASSERT_FALSE_MESSAGE(take_verdict("sd"), "an overrun is the slot's, not a pass"); - TEST_ASSERT_TRUE(run_sd_job()); /* the late chunk comes back */ - TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, fake_khal_worker_pending_for(SD_WORKER), - "a remount was asked for a slot already " - "on one data line"); + TEST_ASSERT_TRUE(run_sd_job()); /* the late chunk comes back... */ + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, fake_khal_worker_pending_for(SD_WORKER), + "the width was not asked for back"); + TEST_ASSERT_TRUE(run_sd_job()); /* ...and the remount puts it right. */ (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, width); TEST_ASSERT_EQUAL_UINT(0U, ao_spy_count()); @@ -3316,22 +3420,18 @@ TEST_CASE("an unmount the worker cannot take is answered and leaves the card " * phase has been judged, so no second record is made for a phase that passed * (here the identify's, already published), and the width stays owed - also * when the call that pays it overruns in turn. The identify ends the run on - * four lines (the card is smaller than the test), so the slot has a line to - * be put back on; the controller then goes away, as it does while the C5's - * link is down, so no remount reaches one line until it is back. */ + * one line (the card will not come up on more), so the slot has lines to be + * put back on; the controller then goes away, as it does while the C5's link + * is down, so no remount reaches the everyday width until it is back. */ TEST_CASE("an overrun of the final width restore adds no record and keeps the " "width owed", "[storage_ao][worker]") { unsigned const steps = QF_SUPPORT_STEPS(KDRV_SD_MOUNT_MAX_MS + KODE_STORAGE_JOB_MARGIN_MS); - khal_sdmmc_card_t card; - int32_t width = 0; + int32_t width = 0; sd_script_fresh(); - fake_sdmmc_default_card(&card); - card.sectors = 2U * ((KODE_SD_TEST_KB * 1024U) - / (uint32_t)card.sector_size); - fake_sdmmc_set_card(&card); /* the identify fails on four lines */ + fake_sdmmc_set_max_width(1U); /* the identify fails: one line */ ask_sd_test(); TEST_ASSERT_TRUE(run_sd_job()); /* the script's remount */ @@ -3358,6 +3458,7 @@ TEST_CASE("an overrun of the final width restore adds no record and keeps the " /* The controller is back, and the remount that pays the width lands. */ fake_sdmmc_set_deny(false); + fake_sdmmc_set_max_width(4U); TEST_ASSERT_TRUE(run_sd_job()); (void)kdrv_sd_get(KDRV_SD_BUS_WIDTH, &width); TEST_ASSERT_EQUAL_INT32((int32_t)KDRV_SD_WIDTH_USE, width); diff --git a/tests/host/main/fakes/fake_kdrv_media.c b/tests/host/main/fakes/fake_kdrv_media.c index a439489..484cbb1 100644 --- a/tests/host/main/fakes/fake_kdrv_media.c +++ b/tests/host/main/fakes/fake_kdrv_media.c @@ -95,10 +95,11 @@ void fake_media_reset(void) kdrv_sd_unmount(); fake_sdmmc_reset(); - /* The two next-mount knobs are statics in a driver that is real here, so + /* The next-mount knobs are statics in a driver that is real here, so * they outlive a case unless somebody puts them back. */ (void)kdrv_sd_set(KDRV_SD_WIDTH_REQ, (int32_t)KDRV_SD_WIDTH_USE); (void)kdrv_sd_set(KDRV_SD_FREQ_REQ_KHZ, (int32_t)KBSP_SD_FREQ_KHZ); + (void)kdrv_sd_set(KDRV_SD_CHECK_REQ, 1); /* Both media come back MOUNTED, which models a board that probed them at * boot. It is not cosmetic: kdrv_storage latches "the card was tried" once diff --git a/tests/host/main/fakes/fake_khal_sdmmc.c b/tests/host/main/fakes/fake_khal_sdmmc.c index caa3195..9616a42 100644 --- a/tests/host/main/fakes/fake_khal_sdmmc.c +++ b/tests/host/main/fakes/fake_khal_sdmmc.c @@ -39,9 +39,20 @@ static bool s_present = true; static bool s_no_volume; /**< A card answers, with no FAT volume on it. */ static bool s_fail_writes; static bool s_fail_reads; +static bool s_fail_wide_writes; /**< Writes fail while the mount is wider than one line. */ +static bool s_fail_wide_reads; /**< Reads fail while the mount is wider than one line. */ +static bool s_reads_limited; /**< Reads fail once s_reads_left have passed. */ +static unsigned s_reads_left; /**< Reads that still succeed while limited. */ +static uint8_t s_fail_mount_width; /**< The width the failing mounts ask for. */ +static unsigned s_fail_mount_left; /**< How many more mounts of it fail. */ static bool s_fail_format; static uint32_t s_read_ms; /**< What one raw read is made to cost. */ +/** @brief The serial the first card of the run carries; each later one has the next. */ +#define SD_SERIAL_FIRST 0xDEAD0000U + +static uint32_t s_serial_next = SD_SERIAL_FIRST; /**< Not reset: no two cards of a run share one. */ + static khal_sdmmc_card_t s_card; /**< What the next mount reports. */ static khal_sdmmc_card_t s_live; /**< What the current mount reported. */ static uint8_t s_max_width = 4U; /**< The widest mount that succeeds. */ @@ -52,6 +63,15 @@ static bool s_hosted; static unsigned s_mounts; static uint8_t s_last_width; static uint32_t s_last_khz; +static unsigned s_raw_writes; + +/** @brief Calls the trace keeps. */ +#define SD_TRACE_MAX 256U +/** @brief Characters of one traced call, the terminator included. */ +#define SD_TRACE_LEN 40U + +static char s_trace[SD_TRACE_MAX][SD_TRACE_LEN]; +static unsigned s_trace_count; /* ---- The directory ------------------------------------------------------- */ @@ -207,6 +227,7 @@ void fake_sdmmc_reset(void) (void)memset(s_store, 0, sizeof(s_store)); default_card(&s_card); + fake_sdmmc_fresh_card(); (void)memset(&s_live, 0, sizeof(s_live)); s_mounted = false; @@ -216,6 +237,12 @@ void fake_sdmmc_reset(void) s_no_volume = false; s_fail_writes = false; s_fail_reads = false; + s_fail_wide_writes = false; + s_fail_wide_reads = false; + s_reads_limited = false; + s_reads_left = 0U; + s_fail_mount_width = 0U; + s_fail_mount_left = 0U; s_fail_format = false; s_read_ms = 0U; s_max_width = 4U; @@ -225,6 +252,8 @@ void fake_sdmmc_reset(void) s_mounts = 0U; s_last_width = 0U; s_last_khz = 0U; + s_raw_writes = 0U; + s_trace_count = 0U; } const char *fake_sdmmc_dir(void) @@ -256,6 +285,12 @@ void fake_sdmmc_default_card(khal_sdmmc_card_t *out) default_card(out); } +void fake_sdmmc_fresh_card(void) +{ + s_card.serial = s_serial_next; + ++s_serial_next; +} + void fake_sdmmc_set_max_width(uint8_t width) { s_max_width = width; @@ -281,6 +316,28 @@ void fake_sdmmc_fail_reads(bool fail) s_fail_reads = fail; } +void fake_sdmmc_fail_reads_after(unsigned ok) +{ + s_reads_limited = true; + s_reads_left = ok; +} + +void fake_sdmmc_fail_wide_writes(bool fail) +{ + s_fail_wide_writes = fail; +} + +void fake_sdmmc_fail_wide_reads(bool fail) +{ + s_fail_wide_reads = fail; +} + +void fake_sdmmc_fail_mounts(uint8_t width, unsigned n) +{ + s_fail_mount_width = width; + s_fail_mount_left = n; +} + void fake_sdmmc_set_read_ms(uint32_t ms) { s_read_ms = ms; @@ -341,6 +398,56 @@ uint32_t fake_sdmmc_last_khz(void) return s_last_khz; } +unsigned fake_sdmmc_raw_writes(void) +{ + return s_raw_writes; +} + +void fake_sdmmc_trace_reset(void) +{ + s_trace_count = 0U; + s_raw_writes = 0U; +} + +unsigned fake_sdmmc_trace_count(void) +{ + return s_trace_count; +} + +const char *fake_sdmmc_trace_line(unsigned idx) +{ + if ( (idx >= s_trace_count) || (SD_TRACE_MAX <= idx) ) + { + return NULL; + } + return s_trace[idx]; +} + +void fake_sdmmc_trace_note(const char *what) +{ + if ( SD_TRACE_MAX > s_trace_count ) + { + (void)snprintf(s_trace[s_trace_count], SD_TRACE_LEN, "%s", what); + } + ++s_trace_count; +} + +/** @brief Trace one call of the seam: its name and, when it has one, its number. */ +static void trace_call(const char *name, uint32_t arg, bool has_arg) +{ + char line[SD_TRACE_LEN]; + + if ( has_arg ) + { + (void)snprintf(line, sizeof(line), "%s %lu", name, (unsigned long)arg); + } + else + { + (void)snprintf(line, sizeof(line), "%s", name); + } + fake_sdmmc_trace_note(line); +} + /* ---- khal_sdmmc ---------------------------------------------------------- */ const char *khal_sdmmc_mount_point(void) @@ -422,6 +529,7 @@ kode_status_t khal_sdmmc_mount(uint8_t width, uint32_t max_khz, return KODE_DENIED; } ++s_mounts; + trace_call("mount", (uint32_t)width, true); if ( s_refuse || s_deny ) { return KODE_DENIED; @@ -429,6 +537,11 @@ kode_status_t khal_sdmmc_mount(uint8_t width, uint32_t max_khz, s_last_width = width; s_last_khz = max_khz; + if ( (0U < s_fail_mount_left) && (width == s_fail_mount_width) ) + { + --s_fail_mount_left; + return KODE_TIMEOUT; + } kode_status_t const st = identify(width, max_khz); if ( KODE_OK != st ) @@ -445,6 +558,7 @@ kode_status_t khal_sdmmc_mount(uint8_t width, uint32_t max_khz, void khal_sdmmc_unmount(void) { + trace_call("unmount", 0U, false); s_mounted = false; } @@ -552,16 +666,25 @@ kode_status_t khal_sdmmc_read_sectors(uint32_t first, uint32_t count, void *dst) { return KODE_DENIED; } + trace_call("read", first, true); /* Charged before the failure check, so a case can make a read both slow * and doomed if it wants to. */ if ( 0U != s_read_ms ) { fake_time_advance_ms(s_read_ms); } - if ( s_fail_reads ) + if ( s_fail_reads || (s_fail_wide_reads && (1U < s_live.bus_width)) ) { return KODE_ERROR; } + if ( s_reads_limited ) + { + if ( 0U == s_reads_left ) + { + return KODE_ERROR; + } + --s_reads_left; + } if ( (first + count) > s_live.sectors ) { return KODE_ERROR; @@ -590,7 +713,8 @@ kode_status_t khal_sdmmc_write_sectors(uint32_t first, uint32_t count, { return KODE_DENIED; } - if ( s_fail_writes ) + trace_call("write", first, true); + if ( s_fail_writes || (s_fail_wide_writes && (1U < s_live.bus_width)) ) { return KODE_ERROR; } @@ -598,6 +722,7 @@ kode_status_t khal_sdmmc_write_sectors(uint32_t first, uint32_t count, { return KODE_ERROR; } + ++s_raw_writes; const uint8_t *in = (const uint8_t *)src; for ( uint32_t i = 0U; i < count; ++i ) diff --git a/tests/host/main/fakes/fakes.h b/tests/host/main/fakes/fakes.h index efc1403..4897f67 100644 --- a/tests/host/main/fakes/fakes.h +++ b/tests/host/main/fakes/fakes.h @@ -443,6 +443,15 @@ void fake_sdmmc_set_card(const khal_sdmmc_card_t *card); /** @brief The healthy card a reset installs, to be edited and set back. */ void fake_sdmmc_default_card(khal_sdmmc_card_t *out); +/** + * @brief Make the card in the slot one nobody has judged: the same card, with + * a serial no earlier card of the run had. + * + * Every reset does this, so a verdict the driver kept on one case's card is + * about a card no later case has. + */ +void fake_sdmmc_fresh_card(void); + /** * @brief The widest mount that succeeds. * @param width 1 makes a 4-bit mount fail, which is the counterfeit card's @@ -492,6 +501,58 @@ void fake_sdmmc_set_read_ms(uint32_t ms); /** @brief Make the format fail, which leaves nothing mounted. */ void fake_sdmmc_fail_format(bool fail); +/** + * @brief Let @p ok more raw sector reads succeed, then make every one fail. + * + * A card whose first read after the mount's own check is the one that does + * not complete. Disarmed by fake_sdmmc_reset(). + * + * @param ok How many reads succeed first. + */ +void fake_sdmmc_fail_reads_after(unsigned ok); + +/** + * @brief Make every raw sector write fail while the mount is on more than one + * data line. + * + * The counterfeit card, exactly: it reads on four lines and fails the first + * write there, and takes the same write on one. + */ +void fake_sdmmc_fail_wide_writes(bool fail); + +/** @brief Make every raw sector read fail while the mount is on more than one + * data line. */ +void fake_sdmmc_fail_wide_reads(bool fail); + +/** + * @brief Make the next @p n mounts asked on @p width fail with KODE_TIMEOUT, + * as a card that has not recovered from a failed transfer does. + * @param width Data lines the failing mounts ask for. + * @param n How many of them fail; the one after answers as the card does. + */ +void fake_sdmmc_fail_mounts(uint8_t width, unsigned n); + +/** @brief Forget the calls the seam has traced and the writes it has counted. */ +void fake_sdmmc_trace_reset(void); + +/** @brief How many calls the seam has traced since the last forget. */ +unsigned fake_sdmmc_trace_count(void); + +/** + * @brief One traced call, in the order they were made: `mount `, + * `unmount`, `read `, `write `, or a note. + * @param idx 0-based. + * @return The text, or NULL past the last. + */ +const char *fake_sdmmc_trace_line(unsigned idx); + +/** @brief Put a line of a case's own into the trace, such as the card's supply + * going off, so the order of everything shows in one list. */ +void fake_sdmmc_trace_note(const char *what); + +/** @brief Raw sector writes the seam has taken since the trace was last forgotten. */ +unsigned fake_sdmmc_raw_writes(void); + /** * @brief Take the controller away, as a hosted mount finds it while the C5's * link is down: khal_sdmmc_ready() says false and a mount is refused