diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80e903a..eb1633a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,31 @@ jobs: # dmsdio links against dmsdio_port_if (headers only), # not dmsdio_port itself, so building only these two targets # never touches src/port/*/port.c - safe to build natively here - # regardless of which CPU family the port defaults to. + # regardless of which CPU family the port defaults to. Leave + # DMOD_CPU_FAMILY at its default (stm32f7): dmod_link_modules' + # header fetch for dmgpio keys off of it too, and dmgpio has never + # published an x86_64 package - only stm32f4/stm32f7, which is + # also all its release pipeline ever builds. Overriding it to + # x86_64 here 404s that fetch, since dmgpio's headers don't come + # from a family-independent package (see dmspi's own CI for the + # same reasoning - dmsdio has the identical dmgpio dependency for + # its optional card-detect friend). cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 - cmake --build . --target dmsdio test_dmsdio + cmake --build . --target dmsdio test_dmsdio dmod_generate_manifest + + - name: Build x86_64 dmsdio_port mock + run: | + # Separate build dir/configure so DMOD_CPU_FAMILY=x86_64 here can't + # affect the dmgpio header fetch above. dmsdio_port itself has no + # x86_64 release (it's real SDMMC register access) - this builds + # src/port/x86_64/port.c, the mocked SD card dmsdio_test.c drives + # through mock_control.h, purely so dmod_loader has something to + # satisfy dmsdio's runtime dmsdio_port dependency with below. + mkdir -p build_dmsdio_port_x86_64 + cd build_dmsdio_port_x86_64 + cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 -DDMOD_CPU_FAMILY=x86_64 + cmake --build . --target dmsdio_port + cp dmf/dmsdio_port.dmf dmf/dmsdio_port.dmd ../build/dmf/ - name: Verify module files run: | @@ -37,11 +59,36 @@ jobs: test -f build/dmf/dmsdio.dmf test -f build/dmf/dmsdio_version.txt test -f build/dmf/test_dmsdio.dmf + test -f build/dmf/dmsdio_port.dmf echo "Module files present" - name: Run tests with dmod_loader run: | export DMOD_DMF_DIR=$(pwd)/build/dmf + # dmod_link_modules only fetches headers at configure time; + # dmsdio's own runtime dependencies (dmini, dmgpio) still need to + # be staged into build/dmf before dmod_loader can resolve them, + # same as the ctest target defined in tests/CMakeLists.txt already + # does. dmsdio.dmd also lists dmsdio_port, which has no published + # x86_64 release (only the real stm32f4/stm32f7 hardware ports + # are) - the build step above already placed our own x86_64 + # dmsdio_port.dmf (the mock) in build/dmf, so `|| true` just lets + # dmf-get's redundant remote lookup for it fail without masking a + # real failure to fetch dmini/dmgpio/dmosi. + dmf-get install -d build/dmf/dmsdio.dmd -y || true + dmf-get install -d build/dmf/test_dmsdio.dmd -y + # test_dmsdio.dmf only ever looks "dmsdio" up by name at runtime + # (Dmod_GetModuleContext("dmsdio") - see dmsdio_test.c, the same + # DIF-consumer pattern dmdevfs.c uses for every driver it mounts), + # so nothing in its compiled binary references dmsdio's own + # signature the way it does dmsdio_port's/dmini's Module API + # calls - neither dmsdio.dmd nor test_dmsdio.dmd's dependency + # scan (both installed above) can see a DIF-by-name lookup like + # that, so "dmsdio" never gets fetched as anyone's dependency. + # It's already built as its own target above, though - install + # it explicitly by name from that same local build/package tree + # (see tests/CMakeLists.txt's ctest wiring for the identical fix). + dmf-get install dmsdio -m build/packages/manifest-local.dmm -y dmod_loader build/dmf/test_dmsdio.dmf discover-cpu-families: diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c51264..b43a3a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,9 +76,12 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ) # To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) +dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi + dmini + dmgpio + dmosi +) add_subdirectory(src/port) target_link_libraries(${DMOD_MODULE_NAME} ${DMOD_MODULE_NAME}_port_if) diff --git a/include/dmsdio.h b/include/dmsdio.h index 0535ea9..f0dbae7 100644 --- a/include/dmsdio.h +++ b/include/dmsdio.h @@ -1,51 +1,30 @@ #ifndef DMSDIO_H #define DMSDIO_H -#include -#include -#include -#include "dmod_types.h" #include "dmsdio_defs.h" +#include "dmsdio_types.h" /** - * Public API for the dmsdio module. - * - * Functions are declared with the dmod_dmsdio_api(...) macro - dmod's - * standard pattern for functions callable from other modules (or from this - * module's own tests/), resolved dynamically by the loader rather than - * through normal static linkage. See dm_sw_ring/include/dm_sw_ring.h for a - * fully worked real-world example of the same shape. - * - * Definitions in src/dmsdio.c use the matching - * dmod_dmsdio_api_declaration(...) macro - a plain C function - * definition here will NOT satisfy these declarations at link time. - * - * This is an example interface using the usual "opaque handle" pattern - - * replace the handle, functions, and struct definition in - * src/dmsdio.c with your module's real API. + * @brief SD host controller configuration */ - -/* Opaque handle - the real struct is defined in src/dmsdio.c */ -typedef struct dmsdio* dmsdio_t; +typedef struct +{ + dmsdio_instance_t instance; /**< Host controller instance number (0-based) */ + dmsdio_bus_width_t max_bus_width; /**< Never negotiate wider than this, even if the card and board wiring support it */ + bool allow_high_speed; /**< Attempt CMD6 High Speed switch after identification */ + uint32_t command_timeout_ms;/**< Per-command response deadline */ + uint32_t data_timeout_ms; /**< Per-block-transfer deadline */ + uint32_t max_retries; /**< Retries for a CRC/timeout error before giving up */ +} dmsdio_config_t; /** - * Create a new dmsdio instance. + * @brief Validate a configuration structure without touching hardware. * - * @return A valid handle on success, or NULL on allocation failure. - */ -dmod_dmsdio_api(1.0, dmsdio_t, _create, ( void )); - -/** - * Destroy an instance created by dmsdio_create(). Safe to call with - * NULL. - */ -dmod_dmsdio_api(1.0, void, _destroy, ( dmsdio_t handle )); - -/** - * Example accessor - replace with your module's real API. + * Does not require the dmsdio_port module to be loaded, so it is safe to + * call from any context, including off-target unit tests. * - * @return true if handle is a valid, non-NULL instance. + * @return true if the configuration is self-consistent, false otherwise. */ -dmod_dmsdio_api(1.0, bool, _is_valid, ( dmsdio_t handle )); +dmod_dmsdio_api(1.0, bool, _validate_config, ( const dmsdio_config_t *config )); #endif // DMSDIO_H diff --git a/include/dmsdio_port.h b/include/dmsdio_port.h index 3c8e9c4..eb9ec70 100644 --- a/include/dmsdio_port.h +++ b/include/dmsdio_port.h @@ -3,31 +3,90 @@ #include "dmod_types.h" #include "dmsdio_port_defs.h" +#include "dmsdio_types.h" -/* - * Declare architecture-independent port API functions here, following the - * dmod_dmsdio_port_api(version, return_type, _suffix, (args)) pattern, - * e.g.: +/** + * @brief Architecture-independent SD host controller port * - * dmod_dmsdio_port_api(1.0, int, _configure, ( int some_arg ) ); + * Every function here is a raw hardware primitive - no SD command sequence, + * register field, or card-state logic belongs in a port implementation + * (src/port//port.c). dmsdio.c owns 100% of the protocol: which + * commands to send in which order, how to parse OCR/CID/CSD/SCR/card + * status, retry/timeout policy, and byte<->block address translation. A + * port only needs to know how to clock a command out, capture whatever + * response the caller says to expect, and move already-block-aligned data + * in or out - see dmsdio_response_type_t's own comment for exactly how + * little a port needs to understand about response *content*. * - * Each declaration here must have a matching *definition* in - * src/port//port.c (or a shared src/port/_common/ file), written - * with the dmod_dmsdio_port_api_declaration(...) macro instead: + * Whether a port uses the host controller's own internal DMA engine (e.g. + * STM32F7 SDMMC's IDMA) or leases a stream from dmdma (e.g. STM32F4 SDIO, + * which has no IDMA of its own and needs a real DMA2 stream) is entirely a + * port implementation detail, invisible from here. + */ + +/* --- Lifecycle --- */ + +dmod_dmsdio_port_api(1.0, int, _init, ( dmsdio_instance_t instance ) ); +dmod_dmsdio_port_api(1.0, int, _deinit, ( dmsdio_instance_t instance ) ); + +/* --- Bus configuration --- + * + * dmsdio.c always calls _set_speed_mode(dmsdio_speed_mode_identification) + * (<= 400 kHz) before the very first command of a fresh identification + * sequence, and never raises the bus width above 1-bit until after CMD7 + * (card selected) - matching the mandatory power-up sequencing in the SD + * Physical Layer spec, section "Bus Protocol". + */ + +dmod_dmsdio_port_api(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width ) ); +dmod_dmsdio_port_api(1.0, int, _set_speed_mode, ( dmsdio_instance_t instance, dmsdio_speed_mode_t mode ) ); + +/* --- Commands --- * - * dmod_dmsdio_port_api_declaration(1.0, int, _configure, ( int some_arg ) ) - * { - * ... - * } + * Sends command index `cmd_index` (already including the CMD55 prefix for + * an ACMD, if any - dmsdio.c issues CMD55 as its own, separate + * _send_command() call) with `argument`, then waits for and captures + * whichever response shape `response_type` says to expect - dmsdio_response_none + * returns immediately once the command itself has been clocked out. * - * Note this is a *different* mechanism from the dmod_init()/dmod_deinit() - * module lifecycle hooks already defined in port.c - don't declare `_init`/ - * `_deinit` here too, that name collides with the lifecycle hooks and (unlike - * them) requires an explicit dmod_dmsdio_port_api_declaration(...) - * definition to avoid an undefined-reference link error. + * A response's CRC/index-check bits must be verified by the port + * (dmsdio_error_command_crc on mismatch) except for dmsdio_response_r3, + * whose CRC field is defined by the spec to be all 1s and is not checked. * - * See dmfmc/include/dmfmc_port.h and dmfmc/src/port/stm32_common/stm32_common.c - * for a fully worked example. + * dmsdio_response_r1b is captured identically to dmsdio_response_r1 - the + * port does not need to (and must not try to) wait out the card's DAT0 + * busy signal itself. Whenever dmsdio.c actually needs to know a card has + * finished an internal operation (post-erase, post-write), it polls + * CMD13/SEND_STATUS's READY_FOR_DATA bit for that, entirely at the + * protocol layer - this is deliberate: not every controller exposes a raw + * DAT0 level to software, and CMD13 polling works identically everywhere. */ +dmod_dmsdio_port_api(1.0, dmsdio_error_t, _send_command, + ( dmsdio_instance_t instance, uint8_t cmd_index, uint32_t argument, + dmsdio_response_type_t response_type, dmsdio_response_t *response ) ); + +/* --- Data transfer --- + * + * dmsdio.c has already issued the matching command via _send_command() + * before calling either of these - CMD17/18 for a read, CMD24/25 for a + * write, ACMD51 for an SCR read (block_size=8, block_count=1), ACMD13 for + * an SD Status read (block_size=64, block_count=1) - and, for the + * multi-block case, will issue CMD12 itself afterward; these calls only + * drive the data phase itself. block_size is *not* fixed at + * DMSDIO_BLOCK_SIZE: SCR and SD Status are transferred as a single + * narrower block each, same as on the wire. Both block_size and + * block_count must be >= 1 (validated by dmsdio.c before the call). + * + * Neither call waits for the card to finish internally programming what + * was written - only that the bytes were clocked onto the bus. dmsdio.c + * polls CMD13/SEND_STATUS itself afterward to know when a write has + * actually landed (see dmsdio_response_r1b's own comment above). + */ + +dmod_dmsdio_port_api(1.0, dmsdio_error_t, _read_blocks, + ( dmsdio_instance_t instance, void *buffer, uint32_t block_size, uint32_t block_count ) ); +dmod_dmsdio_port_api(1.0, dmsdio_error_t, _write_blocks, + ( dmsdio_instance_t instance, const void *buffer, uint32_t block_size, uint32_t block_count ) ); + #endif // DMSDIO_PORT_H diff --git a/include/dmsdio_types.h b/include/dmsdio_types.h new file mode 100644 index 0000000..b62a4b2 --- /dev/null +++ b/include/dmsdio_types.h @@ -0,0 +1,223 @@ +#ifndef DMSDIO_TYPES_H +#define DMSDIO_TYPES_H + +#include +#include +#include + +/** + * @brief SD host controller instance (0-based, e.g. 0 = SDMMC1/SDIO1) + */ +typedef uint8_t dmsdio_instance_t; + +/** + * @brief Card generation counter + * + * Bumped every time a card is (re)identified after insertion. A handle or + * cached value tagged with a generation older than the context's current + * one refers to a card that is no longer there (removed, or replaced with a + * different card while nobody was watching) - see "card-generation + * identifier" in dmsdio.c. + */ +typedef uint32_t dmsdio_generation_t; + +/** + * @brief Card capacity/addressing family + * + * Determines whether CMD17/18/24/25's argument is a byte offset (SDSC) or a + * 512-byte block index (SDHC/SDXC) - see CMD_ARGUMENT in the Physical Layer + * spec. Never set by a port - only dmsdio.c's own CMD8/ACMD41/CMD58(OCR) + * sequencing determines it. + */ +typedef enum +{ + dmsdio_card_type_none = 0, /**< No card identified (socket empty, or identification failed) */ + dmsdio_card_type_sdsc_v1, /**< SDSC, pre-2.0 card (no CMD8 response) - byte addressed */ + dmsdio_card_type_sdsc_v2, /**< SDSC, 2.0+ card, OCR CCS=0 - byte addressed */ + dmsdio_card_type_sdhc, /**< SDHC, OCR CCS=1, capacity <= 32 GiB - block addressed */ + dmsdio_card_type_sdxc, /**< SDXC, OCR CCS=1, capacity > 32 GiB - block addressed */ +} dmsdio_card_type_t; + +/** + * @brief Data bus width + */ +typedef enum +{ + dmsdio_bus_width_1bit = 0, + dmsdio_bus_width_4bit, +} dmsdio_bus_width_t; + +/** + * @brief Bus speed mode + * + * dmsdio_speed_mode_identification is only ever used internally during + * dmsdio.c's own card-identification sequence (CMD0..ACMD41) - it is not a + * valid argument to dmsdio_port_set_speed_mode() from anywhere else. + */ +typedef enum +{ + dmsdio_speed_mode_identification = 0, /**< <= 400 kHz, open-drain-safe - CMD0..ACMD41 only */ + dmsdio_speed_mode_default_speed, /**< <= 25 MHz */ + dmsdio_speed_mode_high_speed, /**< <= 50 MHz - only after a successful CMD6 switch */ +} dmsdio_speed_mode_t; + +/** + * @brief Expected response shape for a command sent through dmsdio_port_send_command() + * + * Matches the SD Physical Layer spec's response types. The port only needs + * to know how many bits to clock in and where the CRC/index-check bits are - + * it does not interpret payload bits (OCR fields, RCA, card status, etc.); + * that is dmsdio.c's job. + */ +typedef enum +{ + dmsdio_response_none = 0, /**< No response expected (e.g. CMD0) */ + dmsdio_response_r1, /**< 48-bit, card status - normal command response */ + dmsdio_response_r1b, /**< R1, plus the card may hold DAT0 low afterward (busy) */ + dmsdio_response_r2, /**< 136-bit - CID or CSD register contents */ + dmsdio_response_r3, /**< 48-bit, OCR register, no CRC (CRC field is all 1s) */ + dmsdio_response_r6, /**< 48-bit, published RCA + a 16-bit status subset */ + dmsdio_response_r7, /**< 48-bit, card interface condition (echoed check pattern + voltage) */ +} dmsdio_response_type_t; + +/** + * @brief Raw response payload from dmsdio_port_send_command() + * + * A big-endian 128-bit container, right-aligned: words[3] is always the + * least-significant 32 bits, words[0] the most-significant. The port + * strips all framing (start bit, transmission bit, command index/reserved + * bits, CRC7, end bit) before filling this in - only content bits remain: + * + * - dmsdio_response_none: unused (all callers pass response == NULL) + * - dmsdio_response_r1/_r1b/_r3/_r6/_r7 (48-bit response, 32 content + * bits): words[3] holds the 32-bit content, words[0..2] are 0 + * - dmsdio_response_r2 (136-bit response, the full 128-bit CID or CSD + * register as its content): words[0] holds bits [127:96], ..., + * words[3] holds bits [31:0] - exactly how the Physical Layer spec + * itself tables CID/CSD field bit ranges + * + * dmsdio.c's response parsers (parse_r1_card_status()/parse_r3_ocr()/ + * parse_cid()/parse_csd()/...) know the exact field layout within that for + * each type. + */ +typedef struct +{ + uint32_t words[4]; /**< words[0] = most significant, words[3] = least significant */ +} dmsdio_response_t; + +/** + * @brief Error a port primitive can report + * + * A superset of what any single primitive can actually return - e.g. + * dmsdio_port_send_command() never returns dmsdio_error_data_crc. Kept in + * one enum (rather than per-primitive enums) so dmsdio.c's retry/recovery + * logic in dmsdio.c can treat every port call's result uniformly. + */ +typedef enum +{ + dmsdio_error_none = 0, + dmsdio_error_no_response, /**< Command timed out waiting for a response */ + dmsdio_error_command_crc, /**< Response CRC (or index) check failed */ + dmsdio_error_data_timeout, /**< Data phase timed out (no card activity within the deadline) */ + dmsdio_error_data_crc, /**< Data block CRC check failed */ + dmsdio_error_removed, /**< Card was physically removed mid-operation */ + dmsdio_error_invalid_argument, /**< Bad argument to a primitive (e.g. block_count == 0) */ + dmsdio_error_not_supported, /**< This port/hardware cannot do what was asked */ +} dmsdio_error_t; + +/** + * @brief SD sector size in bytes + * + * Fixed at 512 for every card family this driver supports - SDHC/SDXC are + * defined to always use 512-byte blocks, and every SDSC card in practice + * accepts CMD16 SET_BLOCKLEN(512) even though its CSD's READ_BL_LEN may + * advertise a larger native block size. + */ +#define DMSDIO_BLOCK_SIZE 512U + +/** + * @brief Parsed CID (Card Identification) register - R2 response to CMD2 + */ +typedef struct +{ + uint8_t manufacturer_id; /**< MID */ + char oem_id[3]; /**< OID, NUL-terminated */ + char product_name[6]; /**< PNM, NUL-terminated */ + uint8_t product_revision; /**< PRV, packed BCD (major<<4 | minor) */ + uint32_t serial_number; /**< PSN */ + uint16_t manufacturing_year; /**< Decoded from MDT (base year 2000) */ + uint8_t manufacturing_month; /**< Decoded from MDT, 1-12 */ +} dmsdio_cid_t; + +/** + * @brief Parsed fields of the CSD (Card-Specific Data) register actually + * used by this driver - R2 response to CMD9 + * + * capacity_blocks and max_transfer_speed_hz are derived from different CSD + * fields depending on csd_structure_version (1.0 for SDSC, 2.0 for + * SDHC/SDXC) - dmsdio.c's csd parser hides that difference here. + */ +typedef struct +{ + uint8_t csd_structure_version; /**< 0 = CSD 1.0 (SDSC), 1 = CSD 2.0 (SDHC/SDXC) */ + uint64_t capacity_blocks; /**< Total addressable 512-byte blocks */ + uint32_t max_transfer_speed_hz; /**< Decoded from TAAC/TRAN_SPEED */ + bool read_only; /**< Permanent write protection (CSD PERM_WRITE_PROTECT) */ +} dmsdio_csd_t; + +/** + * @brief Parsed fields of the SCR (SD Configuration Register) actually used + * by this driver - read via ACMD51 (a 512-bit-wide single-block data + * transfer, not a command response) + */ +typedef struct +{ + uint8_t sd_spec_version; /**< SD_SPEC/SD_SPEC3/SD_SPEC4/SD_SPECX, encoded as the physical spec's minor version */ + bool supports_4bit_bus; /**< SD_BUS_WIDTHS bit 2 */ +} dmsdio_scr_t; + +/** + * @brief Snapshot of everything dmsdio.c knows about the currently inserted + * card - returned by dmsdio_ioctl_cmd_get_card_info + */ +typedef struct +{ + dmsdio_card_type_t card_type; + dmsdio_generation_t generation; + dmsdio_cid_t cid; + dmsdio_csd_t csd; + dmsdio_scr_t scr; + dmsdio_bus_width_t bus_width; + dmsdio_speed_mode_t speed_mode; +} dmsdio_card_info_t; + +/** + * @brief IOCTL commands for the DMSDIO device + * + * Sent to the card sub-node (/dev/dmsdioN/0), not the host context itself. + */ +typedef enum +{ + dmsdio_ioctl_cmd_get_card_info = 1, /**< arg = dmsdio_card_info_t* - snapshot of the current card */ + dmsdio_ioctl_cmd_get_generation, /**< arg = dmsdio_generation_t* - current card generation */ + dmsdio_ioctl_cmd_erase, /**< arg = dmsdio_erase_range_t* - erase a byte range (CMD32/33/38) */ + + dmsdio_ioctl_cmd_max +} dmsdio_ioctl_cmd_t; + +/** + * @brief Byte range to erase, used with dmsdio_ioctl_cmd_erase + */ +typedef struct +{ + uint64_t offset; /**< Must be block-aligned (a multiple of DMSDIO_BLOCK_SIZE) */ + uint64_t size; /**< Must be a multiple of DMSDIO_BLOCK_SIZE */ +} dmsdio_erase_range_t; + +/** + * @brief Opaque driver context type (forward declaration) + */ +struct dmdrvi_context; +typedef struct dmdrvi_context *dmdrvi_context_t; + +#endif /* DMSDIO_TYPES_H */ diff --git a/src/dmsdio.c b/src/dmsdio.c index c005222..6aa4bae 100644 --- a/src/dmsdio.c +++ b/src/dmsdio.c @@ -1,84 +1,1280 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod.h" #include "dmsdio.h" +#include "dmsdio_port.h" +#include "dmdrvi.h" +#include "dmini.h" +#include "dmgpio.h" +#include "dmgpio_types.h" +#include "dmosi.h" -/* Example internal state - replace with your module's real fields. */ -struct dmsdio +#include +#include + +/* "DSDI" packed into a uint32_t - see dmdrvi context magic-field convention */ +#define DMSDIO_CONTEXT_MAGIC 0x44534449UL + +/* ---- SD command indices (Physical Layer spec, section 4.7) ---- */ +#define SD_CMD_GO_IDLE_STATE 0 +#define SD_CMD_ALL_SEND_CID 2 +#define SD_CMD_SEND_RELATIVE_ADDR 3 +#define SD_CMD_SWITCH_FUNC 6 +#define SD_CMD_SELECT_DESELECT_CARD 7 +#define SD_CMD_SEND_IF_COND 8 +#define SD_CMD_SEND_CSD 9 +#define SD_CMD_STOP_TRANSMISSION 12 +#define SD_CMD_SEND_STATUS 13 +#define SD_CMD_SET_BLOCKLEN 16 +#define SD_CMD_READ_SINGLE_BLOCK 17 +#define SD_CMD_READ_MULTIPLE_BLOCK 18 +#define SD_CMD_WRITE_BLOCK 24 +#define SD_CMD_WRITE_MULTIPLE_BLOCK 25 +#define SD_CMD_ERASE_WR_BLK_START 32 +#define SD_CMD_ERASE_WR_BLK_END 33 +#define SD_CMD_ERASE 38 +#define SD_CMD_APP_CMD 55 + +/* ACMD indices - always preceded by CMD55 (APP_CMD) with the card's RCA */ +#define SD_ACMD_SET_BUS_WIDTH 6 +#define SD_ACMD_SD_STATUS 13 +#define SD_ACMD_SEND_OP_COND 41 +#define SD_ACMD_SEND_SCR 51 + +/* CMD8 argument / R7 fields */ +#define SD_CMD8_VOLTAGE_2V7_3V6 0x00000100UL +#define SD_CMD8_CHECK_PATTERN 0x000000AAUL +#define SD_CMD8_ARGUMENT (SD_CMD8_VOLTAGE_2V7_3V6 | SD_CMD8_CHECK_PATTERN) + +/* ACMD41 (OCR) argument / response bits */ +#define SD_OCR_VOLTAGE_WINDOW_2V7_3V6 0x00FF8000UL +#define SD_OCR_HCS (1UL << 30) /* Host Capacity Support (argument) / Card Capacity Status (response) */ +#define SD_OCR_BUSY (1UL << 31) /* 0 = card still powering up */ + +/* R1 card status error bits actually acted on (Physical Layer spec, section 4.10.1) */ +#define SD_STATUS_OUT_OF_RANGE (1UL << 31) +#define SD_STATUS_ADDRESS_ERROR (1UL << 30) +#define SD_STATUS_BLOCK_LEN_ERROR (1UL << 29) +#define SD_STATUS_CARD_ECC_FAILED (1UL << 21) +#define SD_STATUS_CC_ERROR (1UL << 20) +#define SD_STATUS_ERROR (1UL << 19) +#define SD_STATUS_ERROR_MASK (SD_STATUS_OUT_OF_RANGE | SD_STATUS_ADDRESS_ERROR | SD_STATUS_BLOCK_LEN_ERROR | \ + SD_STATUS_CARD_ECC_FAILED | SD_STATUS_CC_ERROR | SD_STATUS_ERROR) + +/* SD Status / Switch Function status share the same 64-byte (512-bit) shape */ +#define SD_SWITCH_STATUS_SIZE 64U +#define SD_SCR_SIZE 8U + +/* Bounded loop guards - real hardware and the mocked port must both make + * progress well inside these, so hitting the cap always means "genuinely + * unreachable/broken", never "just needs one more spin". */ +#define SD_OP_COND_MAX_ATTEMPTS 1000U /* ACMD41 busy-poll, spec allows up to ~1s */ +#define SD_MAX_BLOCKS_PER_TRANSFER 256U /* keeps any single port call's duration bounded */ + +/** + * @brief One host controller context - one dmdrvi_create() call, one + * physical SDMMC/SDIO peripheral instance + * + * Everything about the currently-inserted card (if any) lives directly in + * this struct rather than a separately-allocated "card" object - there is + * only ever one card slot per host controller, and dmsdio_generation_t is + * what lets an already-open handle notice the card it was opened against + * is gone (see dmsdio_handle_t below). + */ +struct dmdrvi_context { - bool valid; + uint32_t magic; + dmsdio_config_t config; + dmosi_mutex_t lock; /**< Serializes every command/data-phase sequence below */ + + bool card_present; /**< Whether /dev/dmsdioN/0 is currently announced */ + dmsdio_card_info_t card; /**< Valid only while card_present */ + uint16_t rca; /**< Relative Card Address, valid only while card_present */ + + /* Optional card-detect friend GPIO (dmspi's chip-select friend_group + * pattern) - card_detect_path == NULL means no CD line is wired for + * this instance, and presence is instead re-checked by attempting + * identification itself (see rescan_card()). */ + char *card_detect_path; + bool card_detect_present_high; + + /* Hot-plug worker - see docs/README.md "Hot-plug detection". The GPIO + * interrupt handler (ISR context) only pushes a dummy byte into + * hotplug_queue; every debounce delay, re-read, and card + * re-identification happens in hotplug_thread (task context), since + * re-identifying a card means exchanging real commands over the bus. */ + dmosi_thread_t hotplug_thread; + dmosi_queue_t hotplug_queue; + volatile bool hotplug_stop; }; -dmod_dmsdio_api_declaration(1.0, dmsdio_t, _create, ( void )) +/** + * @brief Handle dmdrvi_open()/_close() hand back and forth for the card sub-node + * + * Captures the card generation at open() time so a read/write/ioctl issued + * through a handle opened against a card that has since been removed (and + * possibly replaced by a different one) fails with -ENODEV instead of + * silently operating on/confusing itself with whatever is inserted now. + */ +typedef struct +{ + dmdrvi_context_t context; + dmsdio_generation_t generation; +} dmsdio_handle_t; + +static int is_valid_context(dmdrvi_context_t context) +{ + return context != NULL && context->magic == DMSDIO_CONTEXT_MAGIC; +} + +/* ---- Response field extraction ---- + * + * See dmsdio_response_t's own doc comment for the words[] packing this + * relies on. + */ + +static uint32_t r1_status(const dmsdio_response_t *response) +{ + return response->words[3]; +} + +static dmsdio_error_t check_r1_errors(const dmsdio_response_t *response) +{ + return ((r1_status(response) & SD_STATUS_ERROR_MASK) != 0) ? dmsdio_error_data_crc : dmsdio_error_none; +} + +static uint32_t r3_ocr(const dmsdio_response_t *response) +{ + return response->words[3]; +} + +static uint16_t r6_rca(const dmsdio_response_t *response) +{ + return (uint16_t)(response->words[3] >> 16); +} + +static bool r7_matches_check_pattern(const dmsdio_response_t *response) +{ + return (response->words[3] & 0xFFFUL) == SD_CMD8_ARGUMENT; +} + +/** + * @brief Parse a 128-bit CID register (see dmsdio_response_t's packing) + * + * Field bit ranges are as tabled in the Physical Layer spec, section 5.1. + */ +static void parse_cid(const dmsdio_response_t *response, dmsdio_cid_t *cid) +{ + const uint32_t *w = response->words; + + memset(cid, 0, sizeof(*cid)); + cid->manufacturer_id = (uint8_t)(w[0] >> 24); /* MID [127:120] */ + cid->oem_id[0] = (char)(w[0] >> 16); /* OID [119:112] */ + cid->oem_id[1] = (char)(w[0] >> 8); /* OID [111:104] */ + cid->oem_id[2] = '\0'; + cid->product_name[0] = (char)(w[0]); /* PNM [103:96] */ + cid->product_name[1] = (char)(w[1] >> 24); /* PNM [95:88] */ + cid->product_name[2] = (char)(w[1] >> 16); /* PNM [87:80] */ + cid->product_name[3] = (char)(w[1] >> 8); /* PNM [79:72] */ + cid->product_name[4] = (char)(w[1]); /* PNM [71:64] */ + cid->product_name[5] = '\0'; + cid->product_revision = (uint8_t)(w[2] >> 24); /* PRV [63:56] */ + cid->serial_number = (uint32_t)((w[2] & 0x00FFFFFFUL) << 8) | (uint8_t)(w[3] >> 24); /* PSN [55:24] */ + + uint16_t mdt = (uint16_t)((w[3] >> 8) & 0x0FFFU); /* MDT [19:8] */ + cid->manufacturing_year = (uint16_t)(2000U + (mdt >> 4)); + cid->manufacturing_month = (uint8_t)(mdt & 0x0FU); +} + +/** + * @brief Parse a 128-bit CSD register into just the fields this driver uses + * + * CSD structure version 0 (SDSC) and version 1 (SDHC/SDXC) lay capacity out + * completely differently (Physical Layer spec, section 5.3.2 vs 5.3.3) - + * everything else this driver cares about (TAAC/TRAN_SPEED -> a + * conservative max_transfer_speed_hz, PERM_WRITE_PROTECT) is at the same + * bit offset in both. + */ +static void parse_csd(const dmsdio_response_t *response, dmsdio_csd_t *csd) { - /* Dmod_Malloc/Dmod_Free (SAL) are dmod's own heap functions - embedded - * targets don't necessarily link a libc allocator, so use these instead - * of malloc()/free() in module code. */ - struct dmsdio *instance = Dmod_Malloc(sizeof(*instance)); - if (instance == NULL) + const uint32_t *w = response->words; + + memset(csd, 0, sizeof(*csd)); + csd->csd_structure_version = (uint8_t)(w[0] >> 30); /* CSD_STRUCTURE [127:126] */ + + /* TRAN_SPEED [103:96] - the low byte of w[0] (w[0] covers bits + * [127:96]) - only the two transfer-rate-unit/time-value combinations + * real SD cards actually use are decoded; anything else conservatively + * falls back to the mandatory default-speed rate so a card is never + * driven faster than it is safe to. */ + uint8_t tran_speed = (uint8_t)(w[0] & 0xFFU); + csd->max_transfer_speed_hz = (tran_speed == 0x32) ? 25000000U /* 25 MHz - default speed */ + : (tran_speed == 0x5A) ? 50000000U /* 50 MHz - high speed */ + : 25000000U; + + if (csd->csd_structure_version == 0) { - return NULL; + /* CSD 1.0 (SDSC): C_SIZE [73:62] (12 bits), C_SIZE_MULT [49:47] (3 bits), + * READ_BL_LEN [83:80] (4 bits). Capacity in bytes = + * (C_SIZE+1) * 2^(C_SIZE_MULT+2) * 2^READ_BL_LEN, then converted to + * DMSDIO_BLOCK_SIZE-sized blocks regardless of the card's native + * READ_BL_LEN (see DMSDIO_BLOCK_SIZE's own comment). */ + uint32_t read_bl_len = (w[1] >> 16) & 0x0FU; /* [83:80] */ + uint32_t c_size = ((w[1] & 0x000003FFUL) << 2) | ((w[2] >> 30) & 0x3U); /* [73:62] */ + uint32_t c_size_mult = (w[2] >> 15) & 0x7U; /* [49:47] */ + uint64_t capacity_bytes = (uint64_t)(c_size + 1U) << (c_size_mult + 2U + read_bl_len); + csd->capacity_blocks = capacity_bytes / DMSDIO_BLOCK_SIZE; + } + else + { + /* CSD 2.0 (SDHC/SDXC): C_SIZE [69:48] (22 bits). Capacity in + * DMSDIO_BLOCK_SIZE (512-byte) blocks = (C_SIZE+1) * 1024. */ + uint32_t c_size = ((w[1] & 0x0000003FUL) << 16) | (w[2] >> 16); /* [69:48] */ + csd->capacity_blocks = (uint64_t)(c_size + 1U) * 1024ULL; } - instance->valid = true; - return instance; + csd->read_only = ((w[3] >> 13) & 0x1U) != 0; /* PERM_WRITE_PROTECT [12] */ } -dmod_dmsdio_api_declaration(1.0, void, _destroy, ( dmsdio_t handle )) +/** + * @brief Parse the 8-byte SCR register (Physical Layer spec, section 5.6) + */ +static void parse_scr(const uint8_t *buffer, dmsdio_scr_t *scr) { - Dmod_Free(handle); + memset(scr, 0, sizeof(*scr)); + scr->sd_spec_version = (uint8_t)(buffer[0] & 0x0FU); /* SD_SPEC [59:56] */ + scr->supports_4bit_bus = (buffer[1] & 0x04U) != 0; /* SD_BUS_WIDTHS bit 2 [50] */ +} + +/* ---- Command layer ---- + * + * Every SD command in this driver funnels through send_cmd()/send_acmd() so + * retry policy lives in exactly one place. Retries only ever apply to + * transient errors (no response / CRC) - a structural error (bad argument, + * card physically gone) fails immediately, retrying it would just waste + * the same command_timeout_ms up to max_retries times for no reason. + */ + +static bool is_retryable(dmsdio_error_t err) +{ + return err == dmsdio_error_no_response || err == dmsdio_error_command_crc; +} + +static dmsdio_error_t send_cmd(dmdrvi_context_t ctx, uint8_t cmd_index, uint32_t argument, + dmsdio_response_type_t response_type, dmsdio_response_t *response) +{ + dmsdio_error_t err = dmsdio_error_none; + + for (uint32_t attempt = 0; attempt <= ctx->config.max_retries; attempt++) + { + err = dmsdio_port_send_command(ctx->config.instance, cmd_index, argument, response_type, response); + if (!is_retryable(err)) + { + return err; + } + } + return err; +} + +static dmsdio_error_t send_acmd(dmdrvi_context_t ctx, uint16_t rca, uint8_t acmd_index, uint32_t argument, + dmsdio_response_type_t response_type, dmsdio_response_t *response) +{ + dmsdio_response_t app_cmd_response; + dmsdio_error_t err = send_cmd(ctx, SD_CMD_APP_CMD, (uint32_t)rca << 16, dmsdio_response_r1, &app_cmd_response); + if (err != dmsdio_error_none) + { + return err; + } + + return send_cmd(ctx, acmd_index, argument, response_type, response); } -dmod_dmsdio_api_declaration(1.0, bool, _is_valid, ( dmsdio_t handle )) +/** + * @brief Map a dmsdio_error_t (or an R1 status error already checked by the + * caller) to the errno-compatible value dmdrvi's API surface expects + */ +static int to_errno(dmsdio_error_t err) +{ + switch (err) + { + case dmsdio_error_none: return 0; + case dmsdio_error_no_response: return -ETIMEDOUT; + case dmsdio_error_command_crc: return -EIO; + case dmsdio_error_data_timeout: return -ETIMEDOUT; + case dmsdio_error_data_crc: return -EIO; + case dmsdio_error_removed: return -ENODEV; + case dmsdio_error_invalid_argument: return -EINVAL; + case dmsdio_error_not_supported: + default: return -ENOTSUP; + } +} + +/* ---- Card identification ---- */ + +static bool is_block_addressed(dmsdio_card_type_t type) +{ + return type == dmsdio_card_type_sdhc || type == dmsdio_card_type_sdxc; +} + +static uint32_t block_address_argument(dmdrvi_context_t ctx, uint64_t block_index) +{ + if (is_block_addressed(ctx->card.card_type)) + { + return (uint32_t)block_index; + } + return (uint32_t)(block_index * DMSDIO_BLOCK_SIZE); +} + +/** + * @brief ACMD51 (SEND_SCR) - card must already be selected (transfer state) + */ +static dmsdio_error_t read_scr(dmdrvi_context_t ctx, uint16_t rca, dmsdio_scr_t *scr) +{ + uint8_t buffer[SD_SCR_SIZE]; + dmsdio_response_t response; + + dmsdio_error_t err = send_acmd(ctx, rca, SD_ACMD_SEND_SCR, 0, dmsdio_response_r1, &response); + if (err == dmsdio_error_none) + { + err = check_r1_errors(&response); + } + if (err != dmsdio_error_none) + { + return err; + } + + err = dmsdio_port_read_blocks(ctx->config.instance, buffer, SD_SCR_SIZE, 1); + if (err != dmsdio_error_none) + { + return err; + } + + parse_scr(buffer, scr); + return dmsdio_error_none; +} + +/** + * @brief Attempt the CMD6 High Speed switch; leaves the card's function + * selection unchanged (default speed) on any failure + * + * The card must already be selected. Only function group 1 (access mode) + * is touched - every other group's nibble in the argument is 0xF ("no + * change"), matching the Physical Layer spec's CMD6 argument layout. + */ +static bool switch_to_high_speed(dmdrvi_context_t ctx) { - return handle != NULL && handle->valid; + uint8_t status[SD_SWITCH_STATUS_SIZE]; + dmsdio_response_t response; + + /* mode=1 (switch, bit31), function group 1 = 1 (high speed), every + * other group = 0xF (no change). */ + uint32_t argument = 0x80FFFFF1UL; + + dmsdio_error_t err = send_cmd(ctx, SD_CMD_SWITCH_FUNC, argument, dmsdio_response_r1, &response); + if (err == dmsdio_error_none) + { + err = check_r1_errors(&response); + } + if (err != dmsdio_error_none) + { + return false; + } + + err = dmsdio_port_read_blocks(ctx->config.instance, status, SD_SWITCH_STATUS_SIZE, 1); + if (err != dmsdio_error_none) + { + return false; + } + + /* Byte 16, low nibble = function group 1's now-selected function + * (Physical Layer spec, "Switch Function Status"). 1 = high speed was + * actually accepted; anything else (most commonly 0xF, "not + * supported") means the card stayed at default speed. */ + return (status[16] & 0x0FU) == 0x1U; } /** - * @brief Pre-initialization function for the module. + * @brief Full card (re-)identification sequence - CMD0 through CMD7 plus + * SCR/High-Speed setup, run with the card-detect debounced and the + * bus already idle * - * @note This function is optional. You can remove it if you don't need it. + * On success, populates ctx->card/ctx->rca and bumps ctx->card.generation; + * on failure, ctx->card_present is left false (or, if a card was + * previously present, becomes false - see rescan_card()). + */ +static bool identify_card(dmdrvi_context_t ctx) +{ + dmsdio_response_t response; + dmsdio_generation_t next_generation = ctx->card.generation + 1U; + + dmsdio_port_set_bus_width(ctx->config.instance, dmsdio_bus_width_1bit); + dmsdio_port_set_speed_mode(ctx->config.instance, dmsdio_speed_mode_identification); + + /* CMD0: reset to idle state. No response expected - if there is no + * card, or it is unresponsive, this alone can never fail; the very + * next command (CMD8/ACMD41) is what actually detects "nothing here". */ + send_cmd(ctx, SD_CMD_GO_IDLE_STATE, 0, dmsdio_response_none, NULL); + + /* CMD8: SEND_IF_COND. A real response with a matching echo means a + * 2.0+ card (proceed with HCS set in ACMD41, below); no response at + * all means either a pre-2.0 card or nothing responding - both are + * handled identically by ACMD41 without HCS, and ACMD41's own timeout + * is what ultimately reports "no card". */ + bool is_v2_or_later = (send_cmd(ctx, SD_CMD_SEND_IF_COND, SD_CMD8_ARGUMENT, dmsdio_response_r7, &response) == dmsdio_error_none) + && r7_matches_check_pattern(&response); + + uint32_t ocr = 0; + uint32_t acmd41_argument = SD_OCR_VOLTAGE_WINDOW_2V7_3V6 | (is_v2_or_later ? SD_OCR_HCS : 0U); + bool powered_up = false; + + for (uint32_t attempt = 0; attempt < SD_OP_COND_MAX_ATTEMPTS; attempt++) + { + if (send_acmd(ctx, 0, SD_ACMD_SEND_OP_COND, acmd41_argument, dmsdio_response_r3, &response) != dmsdio_error_none) + { + return false; /* No card, or not an SD card - ACMD41 itself is unanswered */ + } + + ocr = r3_ocr(&response); + if ((ocr & SD_OCR_BUSY) != 0) + { + powered_up = true; + break; + } + + Dmod_ThreadSleep(1); + } + + if (!powered_up) + { + return false; /* Card never finished powering up within our budget */ + } + + dmsdio_card_type_t card_type; + if (!is_v2_or_later) + { + card_type = dmsdio_card_type_sdsc_v1; + } + else if ((ocr & SD_OCR_HCS) == 0) + { + card_type = dmsdio_card_type_sdsc_v2; + } + else + { + card_type = dmsdio_card_type_sdhc; /* refined to sdxc below, once capacity is known */ + } + + /* CMD2: ALL_SEND_CID. */ + if (send_cmd(ctx, SD_CMD_ALL_SEND_CID, 0, dmsdio_response_r2, &response) != dmsdio_error_none) + { + return false; + } + dmsdio_cid_t cid; + parse_cid(&response, &cid); + + /* CMD3: SEND_RELATIVE_ADDR - card moves from identification to + * stand-by state and publishes its RCA. */ + if (send_cmd(ctx, SD_CMD_SEND_RELATIVE_ADDR, 0, dmsdio_response_r6, &response) != dmsdio_error_none) + { + return false; + } + uint16_t rca = r6_rca(&response); + + /* Past CMD3 the card is in stand-by/data-transfer state - safe to + * raise the clock before the CSD/CID work that follows. */ + dmsdio_port_set_speed_mode(ctx->config.instance, dmsdio_speed_mode_default_speed); + + /* CMD9: SEND_CSD (stand-by state). */ + if (send_cmd(ctx, SD_CMD_SEND_CSD, (uint32_t)rca << 16, dmsdio_response_r2, &response) != dmsdio_error_none) + { + return false; + } + dmsdio_csd_t csd; + parse_csd(&response, &csd); + + if (card_type == dmsdio_card_type_sdhc && (csd.capacity_blocks * DMSDIO_BLOCK_SIZE) > (32ULL * 1024 * 1024 * 1024)) + { + card_type = dmsdio_card_type_sdxc; + } + + /* CMD7: SELECT_DESELECT_CARD - moves to transfer state; every data + * command (SCR, block I/O, switch function) requires this first. */ + if (send_cmd(ctx, SD_CMD_SELECT_DESELECT_CARD, (uint32_t)rca << 16, dmsdio_response_r1b, &response) != dmsdio_error_none) + { + return false; + } + if (check_r1_errors(&response) != dmsdio_error_none) + { + return false; + } + + /* SDSC's native block length can be anything the CSD's READ_BL_LEN + * says - force it to DMSDIO_BLOCK_SIZE so every card family behaves + * identically from here on (SDHC/SDXC ignore CMD16 entirely). */ + if (!is_block_addressed(card_type)) + { + if (send_cmd(ctx, SD_CMD_SET_BLOCKLEN, DMSDIO_BLOCK_SIZE, dmsdio_response_r1, &response) != dmsdio_error_none || + check_r1_errors(&response) != dmsdio_error_none) + { + return false; + } + } + + dmsdio_scr_t scr; + if (read_scr(ctx, rca, &scr) != dmsdio_error_none) + { + return false; + } + + dmsdio_bus_width_t bus_width = dmsdio_bus_width_1bit; + if (scr.supports_4bit_bus && ctx->config.max_bus_width == dmsdio_bus_width_4bit) + { + dmsdio_response_t acmd6_response; + if (send_acmd(ctx, rca, SD_ACMD_SET_BUS_WIDTH, 0x2U /* 4-bit */, dmsdio_response_r1, &acmd6_response) == dmsdio_error_none && + check_r1_errors(&acmd6_response) == dmsdio_error_none) + { + dmsdio_port_set_bus_width(ctx->config.instance, dmsdio_bus_width_4bit); + bus_width = dmsdio_bus_width_4bit; + } + } + + dmsdio_speed_mode_t speed_mode = dmsdio_speed_mode_default_speed; + if (ctx->config.allow_high_speed && csd.max_transfer_speed_hz > 25000000U) + { + if (switch_to_high_speed(ctx)) + { + dmsdio_port_set_speed_mode(ctx->config.instance, dmsdio_speed_mode_high_speed); + speed_mode = dmsdio_speed_mode_high_speed; + } + } + + ctx->rca = rca; + ctx->card.card_type = card_type; + ctx->card.generation = next_generation; + ctx->card.cid = cid; + ctx->card.csd = csd; + ctx->card.scr = scr; + ctx->card.bus_width = bus_width; + ctx->card.speed_mode = speed_mode; + return true; +} + +/* ---- Block I/O ---- */ + +/** + * @brief SD_STATUS_ERROR_MASK's counterpart for the CURRENT_STATE field - + * polls CMD13/SEND_STATUS until the card reports READY_FOR_DATA + * (bit 8) or ctx->config.data_timeout_ms elapses * - * This function is called when the module enabling is in progress. + * Used after a write's data phase and after an erase - both leave the card + * internally busy (committing to flash) for longer than the bus transfer + * itself took, well past when dmsdio_port_write_blocks()/_send_command() + * already returned. + */ +static dmsdio_error_t wait_ready_for_data(dmdrvi_context_t ctx) +{ + /* Card status bit 8 (Physical Layer spec, section 4.10.1) - see + * SD_STATUS_ERROR_MASK's neighbouring bits for the same response. */ + const uint32_t READY_FOR_DATA = (1UL << 8); + + for (uint32_t waited_ms = 0; waited_ms < ctx->config.data_timeout_ms; waited_ms++) + { + dmsdio_response_t response; + dmsdio_error_t err = send_cmd(ctx, SD_CMD_SEND_STATUS, (uint32_t)ctx->rca << 16, dmsdio_response_r1, &response); + if (err != dmsdio_error_none) + { + return err; + } + err = check_r1_errors(&response); + if (err != dmsdio_error_none) + { + return err; + } + + if ((r1_status(&response) & READY_FOR_DATA) != 0) + { + return dmsdio_error_none; + } + + Dmod_ThreadSleep(1); + } + + return dmsdio_error_data_timeout; +} + +/** + * @brief Read/write raw DMSDIO_BLOCK_SIZE-aligned blocks * - * You can use this function to load the required dependencies, such as - * other modules. Please be aware that the module is not fully initialized, - * so not all the API functions are available - you can check if the API - * is connected by calling the Dmod_IsFunctionConnected() function. + * Picks CMD17/CMD24 (single) vs. CMD18/CMD25 (multiple, terminated by + * CMD12) based on block_count - never a protocol decision the caller needs + * to make itself. */ -void dmod_preinit(void) +static dmsdio_error_t transfer_blocks(dmdrvi_context_t ctx, uint64_t start_block, void *buffer, + uint32_t block_count, bool is_write) { - if(Dmod_IsFunctionConnected( Dmod_Printf )) + uint32_t argument = block_address_argument(ctx, start_block); + uint8_t cmd_index = is_write + ? ((block_count > 1) ? SD_CMD_WRITE_MULTIPLE_BLOCK : SD_CMD_WRITE_BLOCK) + : ((block_count > 1) ? SD_CMD_READ_MULTIPLE_BLOCK : SD_CMD_READ_SINGLE_BLOCK); + + dmsdio_response_t response; + dmsdio_error_t err = send_cmd(ctx, cmd_index, argument, dmsdio_response_r1, &response); + if (err == dmsdio_error_none) { - Dmod_Printf("API is connected!\n"); + err = check_r1_errors(&response); } + + if (err == dmsdio_error_none) + { + err = is_write + ? dmsdio_port_write_blocks(ctx->config.instance, buffer, DMSDIO_BLOCK_SIZE, block_count) + : dmsdio_port_read_blocks(ctx->config.instance, buffer, DMSDIO_BLOCK_SIZE, block_count); + } + + if (block_count > 1) + { + /* Always issue STOP_TRANSMISSION for a multi-block transfer, even + * on error, so the card is not left mid-stream for whatever + * command comes next. */ + dmsdio_response_t stop_response; + send_cmd(ctx, SD_CMD_STOP_TRANSMISSION, 0, dmsdio_response_r1b, &stop_response); + } + + if (err == dmsdio_error_none && is_write) + { + err = wait_ready_for_data(ctx); + } + + return err; } /** - * @brief Initialization function for the module. + * @brief Read/write an arbitrary byte range, handling non-block-aligned + * offsets/sizes via read-modify-write on the boundary blocks * - * This function is called when the module is enabled. - * Please use this function to initialize the module, for instance: - * - initialize the module variables - * - initialize the module hardware - * - allocate memory + * Splits the range into up to three pieces: a partial leading block, a run + * of whole blocks transferred directly (no scratch buffer), and a partial + * trailing block - exactly the "direct full-sector transfers and safe + * partial-sector read-modify-write" acceptance criterion. is_write selects + * between reading into `data` and writing from `data`. */ +static int transfer_bytes(dmdrvi_context_t ctx, void *data, size_t size, uint64_t offset, bool is_write) +{ + uint8_t *cursor = (uint8_t *)data; + size_t remaining = size; + uint64_t byte_offset = offset; + size_t done = 0; + + while (remaining > 0) + { + uint64_t block_index = byte_offset / DMSDIO_BLOCK_SIZE; + uint32_t block_offset = (uint32_t)(byte_offset % DMSDIO_BLOCK_SIZE); + + if (block_offset == 0 && remaining >= DMSDIO_BLOCK_SIZE) + { + uint64_t whole_blocks = remaining / DMSDIO_BLOCK_SIZE; + if (whole_blocks > SD_MAX_BLOCKS_PER_TRANSFER) + { + whole_blocks = SD_MAX_BLOCKS_PER_TRANSFER; + } + + dmsdio_error_t err = transfer_blocks(ctx, block_index, cursor, (uint32_t)whole_blocks, is_write); + if (err != dmsdio_error_none) + { + return (done > 0) ? (int)done : to_errno(err); + } + + size_t chunk = (size_t)whole_blocks * DMSDIO_BLOCK_SIZE; + cursor += chunk; + byte_offset += chunk; + remaining -= chunk; + done += chunk; + } + else + { + uint8_t scratch[DMSDIO_BLOCK_SIZE]; + uint32_t take = DMSDIO_BLOCK_SIZE - block_offset; + if ((size_t)take > remaining) + { + take = (uint32_t)remaining; + } + + dmsdio_error_t err = transfer_blocks(ctx, block_index, scratch, 1, false); + if (err != dmsdio_error_none) + { + return (done > 0) ? (int)done : to_errno(err); + } + + if (is_write) + { + memcpy(scratch + block_offset, cursor, take); + err = transfer_blocks(ctx, block_index, scratch, 1, true); + if (err != dmsdio_error_none) + { + return (done > 0) ? (int)done : to_errno(err); + } + } + else + { + memcpy(cursor, scratch + block_offset, take); + } + + cursor += take; + byte_offset += take; + remaining -= take; + done += take; + } + } + + return (int)done; +} + +/* ---- Hot-plug ---- */ + +static void announce_card(dmdrvi_context_t ctx) +{ + if (ctx->card_present) + { + return; + } + + ctx->card_present = true; + + dmdrvi_dev_num_t dev_num; + dev_num.major = ctx->config.instance; + dev_num.minor = 0; + dev_num.flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR; + dev_num.alt_name[0] = '\0'; + dmdrvi_device_available(ctx, &dev_num); + + DMOD_LOG_INFO("dmsdio%u: card present (%llu blocks, generation %u)\n", + (unsigned)ctx->config.instance, (unsigned long long)ctx->card.csd.capacity_blocks, (unsigned)ctx->card.generation); +} + +static void unannounce_card(dmdrvi_context_t ctx) +{ + if (!ctx->card_present) + { + return; + } + + ctx->card_present = false; + + dmdrvi_dev_num_t dev_num; + dev_num.major = ctx->config.instance; + dev_num.minor = 0; + dev_num.flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR; + dev_num.alt_name[0] = '\0'; + dmdrvi_device_unavailable(ctx, &dev_num); + + DMOD_LOG_INFO("dmsdio%u: card removed\n", (unsigned)ctx->config.instance); +} + +/** + * @brief Re-check whether a card is present and (re-)identify it if so + * + * Called both once at dmdrvi_create() time (a card may already be sitting + * in the slot at boot) and from the hot-plug worker thread on every + * debounced card-detect transition. When there is no CD line at all, this + * is also the *only* detection mechanism - callers with no CD friend + * configured are expected to call this periodically themselves (see + * dmod_init()'s poll timer) instead of relying on hardware interrupts. + */ +static void rescan_card(dmdrvi_context_t ctx) +{ + dmosi_mutex_lock(ctx->lock); + + if (identify_card(ctx)) + { + announce_card(ctx); + } + else + { + unannounce_card(ctx); + } + + dmosi_mutex_unlock(ctx->lock); +} + +static void hotplug_worker(void *arg) +{ + dmdrvi_context_t ctx = (dmdrvi_context_t)arg; + uint8_t dummy; + + /* Debounce: wait for a burst of edge interrupts (bouncing contacts) to + * go quiet for DMSDIO_DEBOUNCE_MS before trusting the new state - a + * short, fixed settle time is enough since we always re-derive the + * real state from the CD pin itself (or the protocol probe) rather + * than trusting which edge fired. */ + while (dmosi_queue_receive(ctx->hotplug_queue, &dummy, -1) == 0) + { + if (ctx->hotplug_stop) + { + break; + } + + Dmod_ThreadSleep(50); + + /* Drain any further events that arrived during the settle time - + * they all resolve to the same rescan. */ + while (dmosi_queue_receive(ctx->hotplug_queue, &dummy, 0) == 0) + { + } + + rescan_card(ctx); + } +} + +static void card_detect_interrupt(dmdrvi_context_t ctx, dmgpio_port_t port, dmgpio_pins_mask_t pins, dmgpio_pins_mask_t state) +{ + (void)port; (void)pins; (void)state; + + /* ISR context - only enqueue, never touch the bus here. */ + uint8_t dummy = 0; + dmosi_queue_send(ctx->hotplug_queue, &dummy, 0); +} + +/* ---- Configuration ---- */ + +static int check_config_parameters(const dmsdio_config_t *cfg) +{ + if (cfg->command_timeout_ms == 0 || cfg->data_timeout_ms == 0) + { + DMOD_LOG_ERROR("dmsdio: timeouts must be non-zero\n"); + return -EINVAL; + } + return 0; +} + +dmod_dmsdio_api_declaration(1.0, bool, _validate_config, ( const dmsdio_config_t *config )) +{ + return (config != NULL) && (check_config_parameters(config) == 0); +} + +static const char *detect_config_section(dmini_context_t ini) +{ + if (dmini_has_key(ini, "dmsdio", "instance")) + { + return "dmsdio"; + } + if (dmini_section_count(ini) > 0) + { + const char *first = dmini_section_name(ini, 0); + if (first != NULL) + { + return first; + } + } + return "dmsdio"; +} + +static void read_config_parameters(dmsdio_config_t *cfg, dmini_context_t ini) +{ + const char *section = detect_config_section(ini); + + cfg->instance = (dmsdio_instance_t)dmini_get_int(ini, section, "instance", 0); + cfg->max_bus_width = (dmini_get_int(ini, section, "max_bus_width", 4) >= 4) + ? dmsdio_bus_width_4bit : dmsdio_bus_width_1bit; + cfg->allow_high_speed = dmini_get_int(ini, section, "high_speed", 1) != 0; + cfg->command_timeout_ms = (uint32_t)dmini_get_int(ini, section, "command_timeout_ms", 100); + cfg->data_timeout_ms = (uint32_t)dmini_get_int(ini, section, "data_timeout_ms", 500); + cfg->max_retries = (uint32_t)dmini_get_int(ini, section, "max_retries", 3); +} + +/* ---- DMOD lifecycle ---- */ + int dmod_init(const Dmod_Config_t *Config) { - Dmod_Printf("Hello, World!\n"); + DMOD_LOG_INFO("DMSDIO driver module initialized\n"); return 0; } +int dmod_deinit(void) +{ + DMOD_LOG_INFO("DMSDIO driver module deinitialized\n"); + return 0; +} + +/* ---- DMDRVI interface ---- */ + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, dmdrvi_context_t, _create, + ( dmini_context_t config, dmdrvi_dev_num_t* dev_num )) +{ + if (config == NULL || dev_num == NULL) + { + DMOD_LOG_ERROR("dmsdio: invalid parameters to dmsdio_dmdrvi_create\n"); + return NULL; + } + + dmdrvi_context_t ctx = Dmod_Malloc(sizeof(struct dmdrvi_context)); + if (ctx == NULL) + { + return NULL; + } + memset(ctx, 0, sizeof(*ctx)); + ctx->magic = DMSDIO_CONTEXT_MAGIC; + + read_config_parameters(&ctx->config, config); + if (check_config_parameters(&ctx->config) != 0) + { + Dmod_Free(ctx); + return NULL; + } + + ctx->lock = dmosi_mutex_create(false); + ctx->hotplug_queue = dmosi_queue_create(sizeof(uint8_t), 4); + if (ctx->lock == NULL || ctx->hotplug_queue == NULL) + { + DMOD_LOG_ERROR("dmsdio: failed to allocate synchronization primitives\n"); + dmosi_mutex_destroy(ctx->lock); + dmosi_queue_destroy(ctx->hotplug_queue); + Dmod_Free(ctx); + return NULL; + } + + if (dmsdio_port_init(ctx->config.instance) != 0) + { + DMOD_LOG_ERROR("dmsdio: failed to initialize instance %u\n", (unsigned)ctx->config.instance); + dmosi_mutex_destroy(ctx->lock); + dmosi_queue_destroy(ctx->hotplug_queue); + Dmod_Free(ctx); + return NULL; + } + + ctx->hotplug_thread = dmosi_thread_create(hotplug_worker, ctx, 1, 2048, "dmsdio_hotplug", NULL); + if (ctx->hotplug_thread == NULL) + { + DMOD_LOG_ERROR("dmsdio: failed to start hot-plug worker\n"); + } + + /* Host context itself is announced as major-only (no card yet); the + * card sub-node is announced separately by rescan_card() once/if a + * card is actually found. */ + dev_num->major = ctx->config.instance; + dev_num->minor = 0; + dev_num->flags = DMDRVI_NUM_MAJOR; + dev_num->alt_name[0] = '\0'; + + rescan_card(ctx); + + return ctx; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _free, ( dmdrvi_context_t context )) +{ + if (!is_valid_context(context)) + { + return; + } + + context->hotplug_stop = true; + if (context->hotplug_queue != NULL) + { + uint8_t dummy = 0; + dmosi_queue_send(context->hotplug_queue, &dummy, 0); /* wake the worker so it can observe hotplug_stop */ + } + if (context->hotplug_thread != NULL) + { + dmosi_thread_join(context->hotplug_thread); + dmosi_thread_destroy(context->hotplug_thread); + } + dmosi_queue_destroy(context->hotplug_queue); + + if (context->card_detect_path != NULL) + { + void *file = Dmod_FileOpen(context->card_detect_path, "r+"); + if (file != NULL) + { + Dmod_Ioctl(file, dmgpio_ioctl_cmd_set_interrupt_handler, NULL); + Dmod_FileClose(file); + } + Dmod_Free(context->card_detect_path); + } + + dmsdio_port_deinit(context->config.instance); + dmosi_mutex_destroy(context->lock); + context->magic = 0; + Dmod_Free(context); +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _friend_changed, + ( dmdrvi_context_t context, const dmdrvi_friend_info_t* info )) +{ + if (!is_valid_context(context) || info == NULL || info->friend_role == NULL || + strcmp(info->friend_role, "card_detect") != 0) + { + return; + } + + if (info->state != dmdrvi_dev_state_ready || info->node_path == NULL) + { + Dmod_Free(context->card_detect_path); + context->card_detect_path = NULL; + return; + } + + char *path = Dmod_StrDup(info->node_path); + if (path == NULL) + { + DMOD_LOG_ERROR("dmsdio: failed to retain card-detect GPIO path\n"); + return; + } + + Dmod_Free(context->card_detect_path); + context->card_detect_path = path; + + void *file = Dmod_FileOpen(path, "r+"); + if (file == NULL) + { + DMOD_LOG_ERROR("dmsdio: failed to open card-detect GPIO: %s\n", path); + return; + } + + dmgpio_interrupt_handler_t handler = (dmgpio_interrupt_handler_t)card_detect_interrupt; + Dmod_Ioctl(file, dmgpio_ioctl_cmd_set_interrupt_handler, &handler); + Dmod_FileClose(file); + + /* A card may already be present/absent before this friend link was + * even wired up - resolve the real state immediately instead of + * waiting for the first edge. */ + rescan_card(context); +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void*, _open, + ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t* dev_num )) +{ + (void)flags; + + if (!is_valid_context(context) || dev_num == NULL || dev_num->minor != 0 || !context->card_present) + { + DMOD_LOG_ERROR("dmsdio: invalid open (no card present or invalid dev_num)\n"); + return NULL; + } + + dmsdio_handle_t *handle = Dmod_Malloc(sizeof(*handle)); + if (handle == NULL) + { + return NULL; + } + + handle->context = context; + handle->generation = context->card.generation; + return handle; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _close, ( dmdrvi_context_t context, void* handle )) +{ + (void)context; + Dmod_Free(handle); +} + /** - * @brief De-initialization function for the module. + * @brief Validates a handle against the context's *current* card generation * - * This function is called when the module is disabled. - * Please use this function to de-initialize the module, for instance: - * - free memory - * - de-initialize the module hardware - * - de-initialize the module variables + * Returns NULL (and the caller should treat that as -ENODEV) if the handle + * was opened against a card that is no longer the one in the slot - either + * because it was removed, or removed and replaced while this handle stayed + * open. */ -int dmod_deinit(void) +static dmsdio_handle_t *validate_handle(dmdrvi_context_t context, void *handle) +{ + if (!is_valid_context(context) || handle == NULL) + { + return NULL; + } + + dmsdio_handle_t *h = (dmsdio_handle_t *)handle; + if (h->context != context || !context->card_present || h->generation != context->card.generation) + { + return NULL; + } + + return h; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, dmdrvi_ssize_t, _read, + ( dmdrvi_context_t context, void* handle, void* buffer, size_t size, dmdrvi_offset_t offset )) +{ + if (offset < 0) + { + return -EINVAL; + } + if (size > (size_t)INT64_MAX) + { + return -EOVERFLOW; + } + + dmsdio_handle_t *h = validate_handle(context, handle); + if (h == NULL) + { + return -ENODEV; + } + if (buffer == NULL || size == 0) + { + return 0; + } + + uint64_t capacity_bytes = context->card.csd.capacity_blocks * DMSDIO_BLOCK_SIZE; + if ((uint64_t)offset >= capacity_bytes) + { + return 0; /* EOF */ + } + + size_t clamped_size = size; + if ((uint64_t)offset + clamped_size > capacity_bytes) + { + clamped_size = (size_t)(capacity_bytes - (uint64_t)offset); + } + + dmosi_mutex_lock(context->lock); + int result = transfer_bytes(context, buffer, clamped_size, (uint64_t)offset, false); + dmosi_mutex_unlock(context->lock); + + if (result < 0 && to_errno(dmsdio_error_removed) == result) + { + unannounce_card(context); + } + return (dmdrvi_ssize_t)result; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, dmdrvi_ssize_t, _write, + ( dmdrvi_context_t context, void* handle, const void* buffer, size_t size, dmdrvi_offset_t offset )) +{ + if (offset < 0) + { + return -EINVAL; + } + if (size > (size_t)INT64_MAX) + { + return -EOVERFLOW; + } + + dmsdio_handle_t *h = validate_handle(context, handle); + if (h == NULL) + { + return -ENODEV; + } + if (buffer == NULL || size == 0) + { + return 0; + } + if (context->card.csd.read_only) + { + return -EROFS; + } + + uint64_t capacity_bytes = context->card.csd.capacity_blocks * DMSDIO_BLOCK_SIZE; + if ((uint64_t)offset >= capacity_bytes) + { + return -ENOSPC; + } + + size_t clamped_size = size; + if ((uint64_t)offset + clamped_size > capacity_bytes) + { + clamped_size = (size_t)(capacity_bytes - (uint64_t)offset); + } + + dmosi_mutex_lock(context->lock); + int result = transfer_bytes(context, (void *)buffer, clamped_size, (uint64_t)offset, true); + dmosi_mutex_unlock(context->lock); + + if (result < 0 && to_errno(dmsdio_error_removed) == result) + { + unannounce_card(context); + } + return (dmdrvi_ssize_t)result; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _ioctl, + ( dmdrvi_context_t context, void* handle, int command, void* arg )) { - Dmod_Printf("Goodbye, World!\n"); + dmsdio_handle_t *h = validate_handle(context, handle); + if (h == NULL) + { + return -ENODEV; + } + if (command < 0 || command >= dmsdio_ioctl_cmd_max) + { + return -EINVAL; + } + + switch ((dmsdio_ioctl_cmd_t)command) + { + case dmsdio_ioctl_cmd_get_card_info: + if (arg == NULL) return -EINVAL; + *(dmsdio_card_info_t *)arg = context->card; + return 0; + + case dmsdio_ioctl_cmd_get_generation: + if (arg == NULL) return -EINVAL; + *(dmsdio_generation_t *)arg = context->card.generation; + return 0; + + case dmsdio_ioctl_cmd_erase: + { + if (arg == NULL) return -EINVAL; + if (context->card.csd.read_only) return -EROFS; + + dmsdio_erase_range_t *range = (dmsdio_erase_range_t *)arg; + if ((range->offset % DMSDIO_BLOCK_SIZE) != 0 || (range->size % DMSDIO_BLOCK_SIZE) != 0 || range->size == 0) + { + return -EINVAL; + } + + uint64_t start_block = range->offset / DMSDIO_BLOCK_SIZE; + uint64_t end_block = start_block + (range->size / DMSDIO_BLOCK_SIZE) - 1U; + + dmosi_mutex_lock(context->lock); + dmsdio_response_t response; + dmsdio_error_t err = send_cmd(context, SD_CMD_ERASE_WR_BLK_START, block_address_argument(context, start_block), dmsdio_response_r1, &response); + if (err == dmsdio_error_none) err = check_r1_errors(&response); + if (err == dmsdio_error_none) + { + err = send_cmd(context, SD_CMD_ERASE_WR_BLK_END, block_address_argument(context, end_block), dmsdio_response_r1, &response); + if (err == dmsdio_error_none) err = check_r1_errors(&response); + } + if (err == dmsdio_error_none) + { + err = send_cmd(context, SD_CMD_ERASE, 0, dmsdio_response_r1b, &response); + if (err == dmsdio_error_none) err = check_r1_errors(&response); + } + if (err == dmsdio_error_none) + { + err = wait_ready_for_data(context); + } + dmosi_mutex_unlock(context->lock); + + return to_errno(err); + } + + default: + return -EINVAL; + } +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _flush, ( dmdrvi_context_t context, void* handle )) +{ + dmsdio_handle_t *h = validate_handle(context, handle); + if (h == NULL) + { + return -ENODEV; + } + /* Every transfer_blocks() write already waits for the card to report + * READY_FOR_DATA before returning (see wait_ready_for_data()) - there + * is nothing left in flight to wait for here. */ + return 0; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _stat, + ( dmdrvi_context_t context, const char* path, dmdrvi_stat_t* stat )) +{ + (void)path; + + if (!is_valid_context(context) || stat == NULL) + { + return -EINVAL; + } + + stat->size = context->card_present ? (dmdrvi_size_t)(context->card.csd.capacity_blocks * DMSDIO_BLOCK_SIZE) : 0; + stat->mode = context->card_present && context->card.csd.read_only ? 0444 : 0666; return 0; } diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index 43dbb92..935f119 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -28,9 +28,10 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} ) # To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) +dmod_link_modules(${DMOD_MODULE_NAME} + dmclk_port + dmosi +) target_include_directories(${DMOD_MODULE_NAME} BEFORE PRIVATE ${CMAKE_SOURCE_DIR}/include diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index b7bcafb..16d952d 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -1,6 +1,34 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmsdio_port.h" #include "dmod.h" +#include "dmclk_port.h" +#include "stm32f7_sdmmc_regs.h" + +/* Single hardware instance supported today (SDMMC1) - `instance` is + * accepted (and validated) for API symmetry with every other multi-instance + * dmod port, but every register access below goes straight to + * STM32F7_SDMMC1 rather than indexing an instance table. */ +static int validate_instance(dmsdio_instance_t instance) +{ + return instance == 0 ? 0 : -1; +} + +/* Generous, fixed iteration bounds for polling loops - real hardware and a + * correctly wired card complete every one of these in a handful of bus + * cycles; only a genuinely wedged bus or a removed card ever exhausts one, + * and the caller (dmsdio.c) has its own retry/timeout policy on top of + * this being a hard, bounded backstop rather than the primary timeout. */ +#define SPIN_ITERATIONS 1000000UL + +static uint32_t block_size_log2(uint32_t block_size) +{ + uint32_t log2 = 0; + while ((1UL << log2) < block_size) + { + log2++; + } + return log2; +} /* ---- DMOD lifecycle ---- */ @@ -16,14 +44,223 @@ int dmod_deinit(void) return 0; } -/* ---- API implementation ---- - * - * Implement the dmod_dmsdio_port_api_declaration(...) functions - * declared in include/dmsdio_port.h here. Register an interrupt - * handler if needed, e.g.: - * - * DMOD_IRQ_HANDLER(SOME_IRQn) - * { - * // handle interrupt - * } - */ +/* ---- Lifecycle ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _init, ( dmsdio_instance_t instance )) +{ + if (validate_instance(instance) != 0) + { + return -1; + } + + STM32F7_RCC_APB2ENR |= RCC_APB2ENR_SDMMC1EN; + + /* Power-cycle sequence per RM0385 "SDMMC card power supply + * management" - off, configure a safe default clock (disabled, + * slowest divider, 1-bit), then on, and hold long enough for the card + * to see well over the mandatory 74 SDMMC_CK cycles before the first + * command (CMD0) goes out. */ + STM32F7_SDMMC1->POWER = SDMMC_POWER_PWRCTRL_OFF; + for (volatile uint32_t i = 0; i < 10000UL; i++) { } + + STM32F7_SDMMC1->CLKCR = SDMMC_CLKCR_CLKDIV_Msk; /* clock disabled, slowest divider, 1-bit */ + STM32F7_SDMMC1->POWER = SDMMC_POWER_PWRCTRL_ON; + for (volatile uint32_t i = 0; i < 10000UL; i++) { } + + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _deinit, ( dmsdio_instance_t instance )) +{ + if (validate_instance(instance) != 0) + { + return -1; + } + + STM32F7_SDMMC1_IDMACTRL = 0; + STM32F7_SDMMC1->POWER = SDMMC_POWER_PWRCTRL_OFF; + STM32F7_RCC_APB2ENR &= ~RCC_APB2ENR_SDMMC1EN; + return 0; +} + +/* ---- Bus configuration ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width )) +{ + if (validate_instance(instance) != 0) + { + return -1; + } + + uint32_t clkcr = STM32F7_SDMMC1->CLKCR; + clkcr &= ~SDMMC_CLKCR_WIDBUS_Msk; + clkcr |= (width == dmsdio_bus_width_4bit) ? SDMMC_CLKCR_WIDBUS_4BIT : SDMMC_CLKCR_WIDBUS_1BIT; + STM32F7_SDMMC1->CLKCR = clkcr; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_speed_mode, ( dmsdio_instance_t instance, dmsdio_speed_mode_t mode )) +{ + if (validate_instance(instance) != 0) + { + return -1; + } + + uint32_t target_hz; + switch (mode) + { + case dmsdio_speed_mode_identification: target_hz = 400000UL; break; + case dmsdio_speed_mode_high_speed: target_hz = 50000000UL; break; + case dmsdio_speed_mode_default_speed: + default: target_hz = 25000000UL; break; + } + + /* SDMMC_CK = SDIOCLK / (CLKDIV + 2) - see RM0385 CLKCR - so the + * divider for a given target is (SDIOCLK / target) - 2, clamped to + * what the 8-bit field can hold and never allowed to go negative + * (which would mean "target is faster than this port can reach"). */ + dmclk_frequency_t sdioclk = dmclk_port_get_domain_frequency(dmclk_domain_sdio); + + uint32_t clkdiv = 0xFFUL; /* slowest possible until proven otherwise */ + if (sdioclk > 0) + { + uint64_t divider = sdioclk / target_hz; + clkdiv = (divider > 2ULL) ? (uint32_t)(divider - 2ULL) : 0UL; + if (clkdiv > 0xFFUL) + { + clkdiv = 0xFFUL; + } + } + + uint32_t clkcr = STM32F7_SDMMC1->CLKCR; + clkcr &= ~SDMMC_CLKCR_CLKDIV_Msk; + clkcr |= (clkdiv << SDMMC_CLKCR_CLKDIV_Pos) | SDMMC_CLKCR_CLKEN | SDMMC_CLKCR_HWFC_EN; + STM32F7_SDMMC1->CLKCR = clkcr; + return 0; +} + +/* ---- Commands ---- */ + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _send_command, + ( dmsdio_instance_t instance, uint8_t cmd_index, uint32_t argument, + dmsdio_response_type_t response_type, dmsdio_response_t *response )) +{ + if (validate_instance(instance) != 0) + { + return dmsdio_error_invalid_argument; + } + + STM32F7_SDMMC1->ICR = SDMMC_STATIC_FLAGS; + STM32F7_SDMMC1->ARG = argument; + + uint32_t waitresp = (response_type == dmsdio_response_none) ? SDMMC_CMD_WAITRESP_NONE + : (response_type == dmsdio_response_r2) ? SDMMC_CMD_WAITRESP_LONG + : SDMMC_CMD_WAITRESP_SHORT; + + STM32F7_SDMMC1->CMD = ((uint32_t)cmd_index & SDMMC_CMD_CMDINDEX_Msk) | waitresp | SDMMC_CMD_CPSMEN; + + uint32_t wait_mask = (response_type == dmsdio_response_none) + ? SDMMC_STA_CMDSENT + : (SDMMC_STA_CMDREND | SDMMC_STA_CTIMEOUT | SDMMC_STA_CCRCFAIL); + + uint32_t sta = 0; + uint32_t timeout = SPIN_ITERATIONS; + do + { + sta = STM32F7_SDMMC1->STA; + } while ((sta & wait_mask) == 0 && --timeout > 0); + + STM32F7_SDMMC1->ICR = SDMMC_STATIC_FLAGS; + + if (timeout == 0 || (sta & SDMMC_STA_CTIMEOUT) != 0) + { + return dmsdio_error_no_response; + } + /* R3's CRC field is defined by the spec to be all 1s - a real card + * always fails this check for it, so it must never be treated as an + * error for that response type specifically. */ + if ((sta & SDMMC_STA_CCRCFAIL) != 0 && response_type != dmsdio_response_r3) + { + return dmsdio_error_command_crc; + } + + if (response != NULL) + { + if (response_type == dmsdio_response_r2) + { + response->words[0] = STM32F7_SDMMC1->RESP1; + response->words[1] = STM32F7_SDMMC1->RESP2; + response->words[2] = STM32F7_SDMMC1->RESP3; + response->words[3] = STM32F7_SDMMC1->RESP4; + } + else + { + response->words[0] = 0; + response->words[1] = 0; + response->words[2] = 0; + response->words[3] = STM32F7_SDMMC1->RESP1; + } + } + + return dmsdio_error_none; +} + +/* ---- Data transfer (SDMMC's own internal IDMA - no dmdma involved) ---- */ + +static dmsdio_error_t run_data_phase(void *buffer, uint32_t block_size, uint32_t block_count, bool is_write) +{ + STM32F7_SDMMC1->ICR = SDMMC_STATIC_FLAGS; + STM32F7_SDMMC1->DTIMER = 0xFFFFFFFFUL; /* see SPIN_ITERATIONS - the real timeout bound is the software spin below */ + STM32F7_SDMMC1->DLEN = block_size * block_count; + STM32F7_SDMMC1->DCTRL = (block_size_log2(block_size) << SDMMC_DCTRL_DBLOCKSIZE_Pos) + | (is_write ? SDMMC_DCTRL_DTDIR_WRITE : SDMMC_DCTRL_DTDIR_READ) + | SDMMC_DCTRL_DTMODE_BLOCK + | SDMMC_DCTRL_DTEN; + + STM32F7_SDMMC1_IDMABASE0 = (uint32_t)(uintptr_t)buffer; + STM32F7_SDMMC1_IDMACTRL = SDMMC_IDMACTRL_IDMAEN; + + uint32_t wait_mask = SDMMC_STA_DATAEND | SDMMC_STA_DCRCFAIL | SDMMC_STA_DTIMEOUT | SDMMC_STA_TXUNDERR | SDMMC_STA_RXOVERR; + uint32_t sta = 0; + uint32_t timeout = SPIN_ITERATIONS; + do + { + sta = STM32F7_SDMMC1->STA; + } while ((sta & wait_mask) == 0 && --timeout > 0); + + STM32F7_SDMMC1_IDMACTRL = 0; + STM32F7_SDMMC1->ICR = SDMMC_STATIC_FLAGS; + + if (timeout == 0 || (sta & SDMMC_STA_DTIMEOUT) != 0) + { + return dmsdio_error_data_timeout; + } + if ((sta & (SDMMC_STA_DCRCFAIL | SDMMC_STA_TXUNDERR | SDMMC_STA_RXOVERR)) != 0) + { + return dmsdio_error_data_crc; + } + + return dmsdio_error_none; +} + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _read_blocks, + ( dmsdio_instance_t instance, void *buffer, uint32_t block_size, uint32_t block_count )) +{ + if (validate_instance(instance) != 0 || buffer == NULL || block_size == 0 || block_count == 0) + { + return dmsdio_error_invalid_argument; + } + + return run_data_phase(buffer, block_size, block_count, false); +} + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _write_blocks, + ( dmsdio_instance_t instance, const void *buffer, uint32_t block_size, uint32_t block_count )) +{ + if (validate_instance(instance) != 0 || buffer == NULL || block_size == 0 || block_count == 0) + { + return dmsdio_error_invalid_argument; + } + + return run_data_phase((void *)buffer, block_size, block_count, true); +} diff --git a/src/port/stm32f7/stm32f7_sdmmc_regs.h b/src/port/stm32f7/stm32f7_sdmmc_regs.h new file mode 100644 index 0000000..492b5cc --- /dev/null +++ b/src/port/stm32f7/stm32f7_sdmmc_regs.h @@ -0,0 +1,105 @@ +#ifndef STM32F7_SDMMC_REGS_H +#define STM32F7_SDMMC_REGS_H + +#include + +/** + * @brief STM32F7 SDMMC1 register map (RM0385, section 32 "SDMMC") + * + * Field/bit names match the reference manual exactly so this file is the + * single place to cross-check against it - port.c never pokes a bit offset + * directly, only these named macros. + */ + +#define STM32F7_SDMMC1_BASE 0x40011000UL +#define STM32F7_RCC_BASE 0x40023800UL + +typedef struct +{ + volatile uint32_t POWER; /* 0x00 */ + volatile uint32_t CLKCR; /* 0x04 */ + volatile uint32_t ARG; /* 0x08 */ + volatile uint32_t CMD; /* 0x0C */ + volatile uint32_t RESPCMD; /* 0x10 */ + volatile uint32_t RESP1; /* 0x14 */ + volatile uint32_t RESP2; /* 0x18 */ + volatile uint32_t RESP3; /* 0x1C */ + volatile uint32_t RESP4; /* 0x20 */ + volatile uint32_t DTIMER; /* 0x24 */ + volatile uint32_t DLEN; /* 0x28 */ + volatile uint32_t DCTRL; /* 0x2C */ + volatile uint32_t DCOUNT; /* 0x30 */ + volatile uint32_t STA; /* 0x34 */ + volatile uint32_t ICR; /* 0x38 */ + volatile uint32_t MASK; /* 0x3C */ + uint32_t RESERVED0[2]; /* 0x40, 0x44 */ + volatile uint32_t FIFOCNT; /* 0x48 */ + uint32_t RESERVED1[13]; /* 0x4C..0x7C */ + volatile uint32_t FIFO; /* 0x80 */ +} STM32F7_SDMMC_TypeDef; + +#define STM32F7_SDMMC1 ((STM32F7_SDMMC_TypeDef *)STM32F7_SDMMC1_BASE) + +/* --- IDMA (internal DMA), a separate register block starting at +0x50 that + * overlaps FIFOCNT/reserved space in the struct above by design (RM0385 + * lists them as part of the same peripheral, at fixed absolute offsets - + * accessed via their own pointers instead of extending the struct, since + * IDMACTRL/IDMABSIZE (0x50/0x54) sit *before* FIFOCNT's neighbours end and + * IDMABASE0 (0x58) through the end of FIFO's reserved run). --- */ +#define STM32F7_SDMMC1_IDMACTRL (*(volatile uint32_t *)(STM32F7_SDMMC1_BASE + 0x50UL)) +#define STM32F7_SDMMC1_IDMABSIZE (*(volatile uint32_t *)(STM32F7_SDMMC1_BASE + 0x54UL)) +#define STM32F7_SDMMC1_IDMABASE0 (*(volatile uint32_t *)(STM32F7_SDMMC1_BASE + 0x58UL)) + +/* POWER */ +#define SDMMC_POWER_PWRCTRL_OFF 0x0UL +#define SDMMC_POWER_PWRCTRL_ON 0x3UL + +/* CLKCR */ +#define SDMMC_CLKCR_CLKDIV_Pos 0U +#define SDMMC_CLKCR_CLKDIV_Msk (0xFFUL << SDMMC_CLKCR_CLKDIV_Pos) +#define SDMMC_CLKCR_CLKEN (1UL << 8) +#define SDMMC_CLKCR_WIDBUS_1BIT (0x0UL << 11) +#define SDMMC_CLKCR_WIDBUS_4BIT (0x1UL << 11) +#define SDMMC_CLKCR_WIDBUS_Msk (0x3UL << 11) +#define SDMMC_CLKCR_NEGEDGE (1UL << 13) +#define SDMMC_CLKCR_HWFC_EN (1UL << 14) + +/* CMD */ +#define SDMMC_CMD_CMDINDEX_Pos 0U +#define SDMMC_CMD_CMDINDEX_Msk (0x3FUL << SDMMC_CMD_CMDINDEX_Pos) +#define SDMMC_CMD_WAITRESP_NONE (0x0UL << 6) +#define SDMMC_CMD_WAITRESP_SHORT (0x1UL << 6) +#define SDMMC_CMD_WAITRESP_LONG (0x3UL << 6) +#define SDMMC_CMD_WAITRESP_Msk (0x3UL << 6) +#define SDMMC_CMD_CPSMEN (1UL << 10) + +/* STA / ICR (same bit positions in both registers) */ +#define SDMMC_STA_CCRCFAIL (1UL << 0) +#define SDMMC_STA_DCRCFAIL (1UL << 1) +#define SDMMC_STA_CTIMEOUT (1UL << 2) +#define SDMMC_STA_DTIMEOUT (1UL << 3) +#define SDMMC_STA_TXUNDERR (1UL << 4) +#define SDMMC_STA_RXOVERR (1UL << 5) +#define SDMMC_STA_CMDREND (1UL << 6) +#define SDMMC_STA_CMDSENT (1UL << 7) +#define SDMMC_STA_DATAEND (1UL << 8) +#define SDMMC_STA_DBCKEND (1UL << 10) +#define SDMMC_STA_RXACT (1UL << 13) + +#define SDMMC_STATIC_FLAGS 0x1FE00FFFUL /* every bit ICR is allowed to clear */ + +/* DCTRL */ +#define SDMMC_DCTRL_DTEN (1UL << 0) +#define SDMMC_DCTRL_DTDIR_WRITE (0x0UL << 1) /* controller -> card */ +#define SDMMC_DCTRL_DTDIR_READ (0x1UL << 1) /* card -> controller */ +#define SDMMC_DCTRL_DTMODE_BLOCK (0x0UL << 2) +#define SDMMC_DCTRL_DBLOCKSIZE_Pos 4U + +/* IDMACTRL */ +#define SDMMC_IDMACTRL_IDMAEN (1UL << 0) + +/* RCC (see stm32_common_regs.h in other modules for the shared base) */ +#define STM32F7_RCC_APB2ENR (*(volatile uint32_t *)(STM32F7_RCC_BASE + 0x44UL)) +#define RCC_APB2ENR_SDMMC1EN (1UL << 11) + +#endif /* STM32F7_SDMMC_REGS_H */ diff --git a/src/port/x86_64/config.cmake b/src/port/x86_64/config.cmake new file mode 100644 index 0000000..e360bee --- /dev/null +++ b/src/port/x86_64/config.cmake @@ -0,0 +1,6 @@ +# DMOD_TOOLS_NAME must match a directory under dmod/configs/arch/... +# Known mappings used elsewhere in the ecosystem: +# stm32f7 -> arch/armv7/cortex-m7 +# stm32f4 -> arch/armv7/cortex-m4 +# x86_64 -> arch/x86_64 +set(DMOD_TOOLS_NAME "arch/x86_64" CACHE STRING "Name of the tools configuration") diff --git a/src/port/x86_64/mock_control.h b/src/port/x86_64/mock_control.h new file mode 100644 index 0000000..adf269e --- /dev/null +++ b/src/port/x86_64/mock_control.h @@ -0,0 +1,63 @@ +#ifndef DMSDIO_MOCK_CONTROL_H +#define DMSDIO_MOCK_CONTROL_H + +#include "dmsdio_port_defs.h" +#include "dmsdio_types.h" + +/** + * @brief Test-only control surface for the x86_64 mock dmsdio_port + * + * Declared under the same "dmsdio_port" module identity as dmsdio_port.h + * (same dmod_dmsdio_port_api(...) macro) so it patches/registers exactly + * the same way - real hardware ports never see this header, only + * tests/dmsdio_test.c and this mock's own port.c do. + */ + +/** + * @brief Reset the mock to a fresh "just powered on" card of the given + * shape and clear every injected fault + * + * capacity_blocks only needs to be *reported* accurately via CSD - the + * mock's backing store is a small fixed-size buffer (see port.c), so tests + * exercising real data must stay within it regardless of what capacity + * they configure here. + */ +dmod_dmsdio_port_api(1.0, void, _mock_reset, ( dmsdio_card_type_t card_type, uint64_t capacity_blocks ) ); + +/** + * @brief Simulate the card being physically absent - every command and + * data-phase call starts failing with dmsdio_error_removed until + * the next _mock_reset() + */ +dmod_dmsdio_port_api(1.0, void, _mock_set_removed, ( bool removed ) ); + +/** + * @brief Whether the mock card should accept the CMD6 High Speed switch + * (mock_reset() defaults this to true) + */ +dmod_dmsdio_port_api(1.0, void, _mock_set_high_speed_supported, ( bool supported ) ); + +/** + * @brief Whether the mock card should report SCR SD_BUS_WIDTHS 4-bit + * support (mock_reset() defaults this to true) + */ +dmod_dmsdio_port_api(1.0, void, _mock_set_4bit_supported, ( bool supported ) ); + +/** + * @brief Make the next `count` dmsdio_port_send_command()/_read_blocks()/ + * _write_blocks() calls fail with `error`, then resume normal + * behaviour - lets a test exercise a transient fault that resolves + * within dmsdio.c's own retry budget, or one that exhausts it, + * with the same knob (compare `count` against the config's + * max_retries the test itself constructed). + */ +dmod_dmsdio_port_api(1.0, void, _mock_inject_error, ( dmsdio_error_t error, uint32_t count ) ); + +/** + * @brief Direct access to the mock's backing store, for a test to seed + * data before a read or verify it after a write without going + * through the driver under test + */ +dmod_dmsdio_port_api(1.0, uint8_t*, _mock_get_backing_store, ( void ) ); + +#endif /* DMSDIO_MOCK_CONTROL_H */ diff --git a/src/port/x86_64/port.c b/src/port/x86_64/port.c new file mode 100644 index 0000000..4e1ece5 --- /dev/null +++ b/src/port/x86_64/port.c @@ -0,0 +1,388 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmsdio_port.h" +#include "mock_control.h" +#include "dmod.h" + +#include + +/** + * @brief x86_64 host-native mock of a single SD card, for dmsdio_test.c + * + * Understands exactly the CMD/ACMD sequence dmsdio.c actually sends (see + * identify_card()/transfer_blocks() there) and nothing else - this is not + * a general-purpose SD simulator, just enough of one to drive dmsdio.c's + * own state machine through every path the mocked protocol tests exercise. + */ + +#define MOCK_BACKING_BLOCKS 64U /* small on purpose - tests only ever touch a handful of blocks */ +#define MOCK_RCA 0x1234U + +typedef struct +{ + bool app_cmd_pending; /* true immediately after CMD55 - next index is an ACMD */ + bool selected; /* true after CMD7 */ + bool removed; + + dmsdio_card_type_t card_type; + uint64_t capacity_blocks; + bool high_speed_supported; + bool bus_4bit_supported; + bool high_speed_selected; + + dmsdio_error_t inject_error; + uint32_t inject_count; + + uint8_t backing_store[MOCK_BACKING_BLOCKS * DMSDIO_BLOCK_SIZE]; +} mock_state_t; + +static mock_state_t g_mock; + +/* ---- Encoders - the exact inverse of dmsdio.c's parse_cid()/parse_csd()/ + * parse_scr(), field-for-field. Kept private to this file: no real port + * ever needs to *build* a response, only decode one. ---- */ + +static void encode_cid(dmsdio_response_t *r) +{ + memset(r, 0, sizeof(*r)); + uint8_t mid = 0x03; + char oid[2] = { 'D', 'M' }; + char pnm[5] = { 'M', 'O', 'C', 'K', '0' }; + uint8_t prv = 0x10; + uint32_t psn = 0xDEADBEEFUL; + uint8_t mdt_year_offset = 25; /* 2025 */ + uint8_t mdt_month = 6; + + r->words[0] = ((uint32_t)mid << 24) | ((uint32_t)(uint8_t)oid[0] << 16) | + ((uint32_t)(uint8_t)oid[1] << 8) | (uint32_t)(uint8_t)pnm[0]; + r->words[1] = ((uint32_t)(uint8_t)pnm[1] << 24) | ((uint32_t)(uint8_t)pnm[2] << 16) | + ((uint32_t)(uint8_t)pnm[3] << 8) | (uint32_t)(uint8_t)pnm[4]; + r->words[2] = ((uint32_t)prv << 24) | ((psn >> 8) & 0x00FFFFFFUL); + uint16_t mdt = (uint16_t)(((uint32_t)mdt_year_offset << 4) | mdt_month); + r->words[3] = ((psn & 0xFFUL) << 24) | ((uint32_t)mdt << 8) | 0x01UL; +} + +static void encode_csd(dmsdio_response_t *r, dmsdio_card_type_t card_type, uint64_t capacity_blocks, bool high_speed_supported) +{ + memset(r, 0, sizeof(*r)); + + bool is_block_addressed = (card_type == dmsdio_card_type_sdhc || card_type == dmsdio_card_type_sdxc); + uint8_t structure_version = is_block_addressed ? 1U : 0U; + uint8_t tran_speed = high_speed_supported ? 0x5AU : 0x32U; /* 50 MHz : 25 MHz */ + + r->words[0] = ((uint32_t)structure_version << 30) | tran_speed; /* CSD_STRUCTURE [127:126], TRAN_SPEED [103:96] */ + + if (!is_block_addressed) + { + /* CSD 1.0: READ_BL_LEN=9 (512), C_SIZE_MULT=0 -> + * capacity_blocks = (C_SIZE+1) * 4. */ + uint32_t read_bl_len = 9U; + uint32_t c_size_mult = 0U; + uint32_t c_size = (uint32_t)(capacity_blocks / 4U); + c_size = (c_size > 0U) ? (c_size - 1U) : 0U; + if (c_size > 0xFFFU) c_size = 0xFFFU; + + r->words[1] |= (read_bl_len & 0xFU) << 16; /* READ_BL_LEN [83:80] */ + r->words[1] |= (c_size >> 2) & 0x3FFU; /* C_SIZE[73:64] */ + r->words[2] |= (c_size & 0x3U) << 30; /* C_SIZE[63:62] */ + r->words[2] |= (c_size_mult & 0x7U) << 15; /* C_SIZE_MULT [49:47] */ + } + else + { + /* CSD 2.0: capacity_blocks = (C_SIZE+1) * 1024. */ + uint32_t c_size = (uint32_t)(capacity_blocks / 1024U); + c_size = (c_size > 0U) ? (c_size - 1U) : 0U; + + r->words[1] |= (c_size >> 16) & 0x3FU; /* C_SIZE [69:64] */ + r->words[2] |= (c_size & 0xFFFFU) << 16; /* C_SIZE [63:48] */ + } +} + +static void encode_scr(uint8_t *buffer, bool bus_4bit_supported) +{ + memset(buffer, 0, DMSDIO_BLOCK_SIZE > 8 ? 8 : DMSDIO_BLOCK_SIZE); + buffer[0] = 0x02; /* SCR_STRUCTURE=0, SD_SPEC=2 (Version 2.00) */ + buffer[1] = bus_4bit_supported ? 0x05U : 0x01U; /* SD_BUS_WIDTHS: bit0=1bit always, bit2=4bit */ +} + +static void encode_switch_status(uint8_t *buffer, bool accept_high_speed) +{ + memset(buffer, 0, 64); + buffer[16] = accept_high_speed ? 0x01U : 0x0FU; /* group 1 selected function, low nibble */ +} + +/* ---- Mock control ---- */ + +dmod_dmsdio_port_api_declaration(1.0, void, _mock_reset, ( dmsdio_card_type_t card_type, uint64_t capacity_blocks )) +{ + memset(&g_mock, 0, sizeof(g_mock)); + g_mock.card_type = card_type; + g_mock.capacity_blocks = capacity_blocks; + g_mock.high_speed_supported = true; + g_mock.bus_4bit_supported = true; + + for (uint32_t i = 0; i < sizeof(g_mock.backing_store); i++) + { + g_mock.backing_store[i] = (uint8_t)i; + } +} + +dmod_dmsdio_port_api_declaration(1.0, void, _mock_set_removed, ( bool removed )) +{ + g_mock.removed = removed; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _mock_set_high_speed_supported, ( bool supported )) +{ + g_mock.high_speed_supported = supported; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _mock_set_4bit_supported, ( bool supported )) +{ + g_mock.bus_4bit_supported = supported; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _mock_inject_error, ( dmsdio_error_t error, uint32_t count )) +{ + g_mock.inject_error = error; + g_mock.inject_count = count; +} + +dmod_dmsdio_port_api_declaration(1.0, uint8_t*, _mock_get_backing_store, ( void )) +{ + return g_mock.backing_store; +} + +/* Applied uniformly by every primitive below - a single knob for "the next + * N low-level operations fail", regardless of whether that operation is a + * command or a data phase (a real transient bus fault would not + * distinguish either). */ +static bool consume_injected_error(dmsdio_error_t *out_error) +{ + if (g_mock.removed) + { + *out_error = dmsdio_error_removed; + return true; + } + if (g_mock.inject_count > 0) + { + g_mock.inject_count--; + *out_error = g_mock.inject_error; + return true; + } + return false; +} + +/* ---- DMOD lifecycle ---- */ + +int dmod_init(const Dmod_Config_t *Config) +{ + Dmod_Printf("dmsdio port module initialized (x86_64 mock)\n"); + return 0; +} + +int dmod_deinit(void) +{ + Dmod_Printf("dmsdio port module deinitialized (x86_64 mock)\n"); + return 0; +} + +/* ---- Lifecycle ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _init, ( dmsdio_instance_t instance )) +{ + (void)instance; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _deinit, ( dmsdio_instance_t instance )) +{ + (void)instance; + return 0; +} + +/* ---- Bus configuration - no real bus to configure ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width )) +{ + (void)instance; (void)width; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_speed_mode, ( dmsdio_instance_t instance, dmsdio_speed_mode_t mode )) +{ + (void)instance; (void)mode; + return 0; +} + +/* ---- Commands ---- */ + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _send_command, + ( dmsdio_instance_t instance, uint8_t cmd_index, uint32_t argument, + dmsdio_response_type_t response_type, dmsdio_response_t *response )) +{ + (void)instance; + + dmsdio_error_t injected; + if (consume_injected_error(&injected)) + { + g_mock.app_cmd_pending = false; + return injected; + } + + bool is_acmd = g_mock.app_cmd_pending; + g_mock.app_cmd_pending = false; + + if (response != NULL) + { + memset(response, 0, sizeof(*response)); + } + + if (!is_acmd) + { + switch (cmd_index) + { + case 0: /* GO_IDLE_STATE */ + return dmsdio_error_none; + + case 8: /* SEND_IF_COND - every mocked card is 2.0+ */ + if (response != NULL) response->words[3] = argument & 0xFFFUL; + return dmsdio_error_none; + + case 2: /* ALL_SEND_CID */ + if (response != NULL) encode_cid(response); + return dmsdio_error_none; + + case 3: /* SEND_RELATIVE_ADDR */ + if (response != NULL) response->words[3] = (uint32_t)MOCK_RCA << 16; + return dmsdio_error_none; + + case 9: /* SEND_CSD */ + if (response != NULL) encode_csd(response, g_mock.card_type, g_mock.capacity_blocks, g_mock.high_speed_supported); + return dmsdio_error_none; + + case 7: /* SELECT_DESELECT_CARD */ + g_mock.selected = (argument != 0); + return dmsdio_error_none; + + case 6: /* SWITCH_FUNC (mode=1, group1) - only meaningful once selected */ + g_mock.high_speed_selected = g_mock.high_speed_supported; + return dmsdio_error_none; + + case 12: /* STOP_TRANSMISSION */ + case 13: /* SEND_STATUS - READY_FOR_DATA always set in the mock */ + if (response != NULL) response->words[3] = (1UL << 8); + return dmsdio_error_none; + + case 16: /* SET_BLOCKLEN */ + case 17: /* READ_SINGLE_BLOCK */ + case 18: /* READ_MULTIPLE_BLOCK */ + case 24: /* WRITE_BLOCK */ + case 25: /* WRITE_MULTIPLE_BLOCK */ + if ((uint64_t)(argument / DMSDIO_BLOCK_SIZE) >= g_mock.capacity_blocks && + (g_mock.card_type == dmsdio_card_type_sdhc || g_mock.card_type == dmsdio_card_type_sdxc)) + { + if (response != NULL) response->words[3] = (1UL << 31); /* OUT_OF_RANGE */ + } + return dmsdio_error_none; + + case 32: /* ERASE_WR_BLK_START */ + case 33: /* ERASE_WR_BLK_END */ + case 38: /* ERASE */ + return dmsdio_error_none; + + case 55: /* APP_CMD */ + g_mock.app_cmd_pending = true; + return dmsdio_error_none; + + default: + return dmsdio_error_none; + } + } + + /* ACMD */ + switch (cmd_index) + { + case 41: /* SD_SEND_OP_COND */ + if (response != NULL) + { + uint32_t ocr = (1UL << 31); /* power-up done, immediately, no busy-poll needed in tests */ + bool is_hc = (g_mock.card_type == dmsdio_card_type_sdhc || g_mock.card_type == dmsdio_card_type_sdxc); + if (is_hc) ocr |= (1UL << 30); + response->words[3] = ocr; + } + return dmsdio_error_none; + + case 6: /* SET_BUS_WIDTH */ + return dmsdio_error_none; + + case 51: /* SEND_SCR */ + case 13: /* SD_STATUS */ + default: + return dmsdio_error_none; + } +} + +/* ---- Data transfer ---- */ + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _read_blocks, + ( dmsdio_instance_t instance, void *buffer, uint32_t block_size, uint32_t block_count )) +{ + (void)instance; + + dmsdio_error_t injected; + if (consume_injected_error(&injected)) + { + return injected; + } + + if (block_size == DMSDIO_BLOCK_SIZE) + { + /* Real sector read - serve straight from the backing store, wrapping + * so a test can address any block index without needing a + * full-capacity-sized buffer. */ + for (uint32_t i = 0; i < block_count; i++) + { + uint32_t offset = (i % MOCK_BACKING_BLOCKS) * DMSDIO_BLOCK_SIZE; + memcpy((uint8_t *)buffer + (size_t)i * block_size, g_mock.backing_store + offset, block_size); + } + } + else if (block_size == 8U) + { + encode_scr((uint8_t *)buffer, g_mock.bus_4bit_supported); + } + else if (block_size == 64U) + { + encode_switch_status((uint8_t *)buffer, g_mock.high_speed_supported); + } + else + { + return dmsdio_error_not_supported; + } + + return dmsdio_error_none; +} + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_error_t, _write_blocks, + ( dmsdio_instance_t instance, const void *buffer, uint32_t block_size, uint32_t block_count )) +{ + (void)instance; + + dmsdio_error_t injected; + if (consume_injected_error(&injected)) + { + return injected; + } + + if (block_size != DMSDIO_BLOCK_SIZE) + { + return dmsdio_error_not_supported; + } + + for (uint32_t i = 0; i < block_count; i++) + { + uint32_t offset = (i % MOCK_BACKING_BLOCKS) * DMSDIO_BLOCK_SIZE; + memcpy(g_mock.backing_store + offset, (const uint8_t *)buffer + (size_t)i * block_size, block_size); + } + + return dmsdio_error_none; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36b6e18..582b0fa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,15 +4,29 @@ set(DMOD_AUTHOR_NAME "Patryk Kubiak") set(DMOD_STACK_SIZE 1024) set(DMOD_PRIORITY 0) +# Runs against the x86_64 mock in src/port/x86_64/port.c (see +# mock_control.h) - only meaningful in a build configured with +# -DDMOD_CPU_FAMILY=x86_64; a stm32f4/stm32f7-configured build still +# compiles this (dmod_add_test builds for whatever DMOD_TOOLS_NAME is +# active) but nothing here ever runs it through dmod_loader on real +# hardware, since ctest never targets the flashed board. dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} dmsdio_test.c + fake_mal.c ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../src/port/x86_64 ) -target_link_libraries(${DMOD_MODULE_NAME} dmsdio_if) +target_link_libraries(${DMOD_MODULE_NAME} dmsdio_if dmsdio_port_if) + +dmod_link_modules(${DMOD_MODULE_NAME} + dmsdio + dmdrvi + dmini +) # Register the test module with ctest - this replays the same two steps CI # runs (see .github/templates/ci.yml.template): install the test module's @@ -22,10 +36,20 @@ find_program(DMF_GET_EXECUTABLE dmf-get HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PA find_program(DMOD_LOADER_EXECUTABLE dmod_loader HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) if(DMF_GET_EXECUTABLE AND DMOD_LOADER_EXECUTABLE) + # test_dmsdio only ever looks "dmsdio" up by name at runtime + # (Dmod_GetModuleContext("dmsdio") - see dmsdio_test.c), so nothing in + # its compiled binary references dmsdio's own signature the way it does + # dmsdio_port's/dmini's Module API calls - todmd's dependency scan + # can't see a DIF-by-name lookup like that, so "dmsdio" never makes it + # into test_dmsdio(-local).dmd on its own (confirmed: dmod_link_modules() + # above only *constrains the version* of dependencies the scan already + # found, it can't add ones the scan missed - see todmd --help). Install + # it explicitly, from the same local manifest every other local + # dependency here already resolves through. add_test( NAME ${DMOD_MODULE_NAME} COMMAND ${CMAKE_COMMAND} -E env DMOD_DMF_DIR=${DMOD_DMF_DIR} - bash -c "${DMF_GET_EXECUTABLE} install -d ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}-local.dmd -y && ${DMOD_LOADER_EXECUTABLE} ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}.dmf" + bash -c "${DMF_GET_EXECUTABLE} install -d ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}-local.dmd -y && ${DMF_GET_EXECUTABLE} install dmsdio -m ${CMAKE_BINARY_DIR}/packages/manifest-local.dmm -y && ${DMOD_LOADER_EXECUTABLE} ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}.dmf" ) else() message(WARNING "dmf-get/dmod_loader not found - 'ctest' will not run ${DMOD_MODULE_NAME}. Build the tools first with -DDMOD_BUILD_TOOLS=ON") diff --git a/tests/dmsdio_test.c b/tests/dmsdio_test.c index 7c4f660..845492f 100644 --- a/tests/dmsdio_test.c +++ b/tests/dmsdio_test.c @@ -1,32 +1,329 @@ -#define DMOD_ENABLE_REGISTRATION ON +/* Needed so this translation unit gets real storage for the dmdrvi DIF + * signature string constants it looks up below - see dmdevfs.c/dmvfs.c + * for the same pattern on every other real DIF consumer in the + * ecosystem. */ +#define ENABLE_DIF_REGISTRATIONS ON #include "dmod_test.h" #include "dmsdio.h" +#include "dmdrvi.h" +#include "dmini.h" +#include "mock_control.h" +#include "fake_mal.h" -static dmsdio_t g_handle = NULL; +#include +#include + +/* DMOD_USE_STDLIB=OFF builds don't link memcmp() - see dmdma_test_lease.c's + * own parse_uint() for the same reasoning. */ +static bool bytes_equal(const void *a, const void *b, size_t size) +{ + const uint8_t *pa = (const uint8_t *)a; + const uint8_t *pb = (const uint8_t *)b; + for (size_t i = 0; i < size; i++) + { + if (pa[i] != pb[i]) + { + return false; + } + } + return true; +} + +/** + * Mocked SD protocol tests - dmsdio.c is exercised exactly as any real + * consumer (dmdevfs) would, through the dmdrvi DIF looked up by module + * name (dmsdio genuinely is one of several dmdrvi implementers - dmuart, + * dmspi, dmdma, ... - so unlike dmdma's lease API, this really is DIF's + * 1:N territory). The x86_64 "dmsdio_port" bundled here is the mock in + * src/port/x86_64/port.c, controlled through mock_control.h. + */ + +static dmod_dmdrvi_create_t dmdrvi_create; +static dmod_dmdrvi_free_t dmdrvi_free; +static dmod_dmdrvi_open_t dmdrvi_open; +static dmod_dmdrvi_close_t dmdrvi_close; +static dmod_dmdrvi_read_t dmdrvi_read; +static dmod_dmdrvi_write_t dmdrvi_write; +static dmod_dmdrvi_ioctl_t dmdrvi_ioctl; +static dmod_dmdrvi_stat_t dmdrvi_stat; + +static dmdrvi_context_t g_context; void dmod_test_setup(void) { - g_handle = dmsdio_create(); + Dmod_Context_t *module = Dmod_GetModuleContext("dmsdio"); + + dmdrvi_create = (dmod_dmdrvi_create_t)Dmod_GetDifFunction(module, dmod_dmdrvi_create_sig); + dmdrvi_free = (dmod_dmdrvi_free_t)Dmod_GetDifFunction(module, dmod_dmdrvi_free_sig); + dmdrvi_open = (dmod_dmdrvi_open_t)Dmod_GetDifFunction(module, dmod_dmdrvi_open_sig); + dmdrvi_close = (dmod_dmdrvi_close_t)Dmod_GetDifFunction(module, dmod_dmdrvi_close_sig); + dmdrvi_read = (dmod_dmdrvi_read_t)Dmod_GetDifFunction(module, dmod_dmdrvi_read_sig); + dmdrvi_write = (dmod_dmdrvi_write_t)Dmod_GetDifFunction(module, dmod_dmdrvi_write_sig); + dmdrvi_ioctl = (dmod_dmdrvi_ioctl_t)Dmod_GetDifFunction(module, dmod_dmdrvi_ioctl_sig); + dmdrvi_stat = (dmod_dmdrvi_stat_t)Dmod_GetDifFunction(module, dmod_dmdrvi_stat_sig); + + g_context = NULL; + g_fake_mal_card_available = false; } void dmod_test_teardown(void) { - dmsdio_destroy(g_handle); - g_handle = NULL; + if (g_context != NULL) + { + dmdrvi_free(g_context); + g_context = NULL; + } +} + +static dmini_context_t make_config(void) +{ + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, + "[dmsdio]\n" + "instance=0\n" + "max_bus_width=4\n" + "high_speed=1\n" + "command_timeout_ms=100\n" + "data_timeout_ms=100\n" + "max_retries=3\n"); + return ini; } -DMOD_TEST_STEP(dmsdio_create) +/** + * @brief mock_reset() + dmdrvi_create() - the create() call itself runs + * the full identification sequence (see rescan_card() in dmsdio.c), + * so a successful return here already exercises CMD0..CMD7/SCR/etc. + */ +static dmdrvi_context_t create_with_card(dmsdio_card_type_t card_type, uint64_t capacity_blocks) { - DMOD_TEST_EXPECT_NOT_NULL(g_handle); + dmsdio_port_mock_reset(card_type, capacity_blocks); + + dmini_context_t ini = make_config(); + dmdrvi_dev_num_t host_dev_num; + dmdrvi_context_t context = dmdrvi_create(ini, &host_dev_num); + dmini_destroy(ini); + return context; } -DMOD_TEST_STEP(dmsdio_is_valid) +static void *open_card(dmdrvi_context_t context) { - DMOD_TEST_EXPECT_TRUE(dmsdio_is_valid(g_handle)); + /* dmsdio.c always announces the card sub-node as {major=instance, + * minor=0} - see announce_card() - so the test does not need + * g_fake_mal_last_dev_num for this, only for asserting hot-plug + * actually fired. */ + dmdrvi_dev_num_t dev_num; + dev_num.major = 0; + dev_num.minor = 0; + dev_num.flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR; + return dmdrvi_open(context, 0, &dev_num); +} + +/* ---- Card identification across every supported family ---- */ + +DMOD_TEST_STEP(identify_sdsc_v1) +{ + g_context = create_with_card(dmsdio_card_type_sdsc_v1, 8192); + DMOD_TEST_EXPECT_NOT_NULL(g_context); + DMOD_TEST_EXPECT_TRUE(g_fake_mal_card_available); + + void *handle = open_card(g_context); + DMOD_TEST_EXPECT_NOT_NULL(handle); + + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_card_info, &info), 0); + DMOD_TEST_EXPECT_EQ(info.card_type, dmsdio_card_type_sdsc_v1); + DMOD_TEST_EXPECT_EQ(info.csd.capacity_blocks, 8192ULL); } -DMOD_TEST_STEP(dmsdio_destroy_null) +DMOD_TEST_STEP(identify_sdsc_v2) { - /* Destroying NULL must not crash. */ - dmsdio_destroy(NULL); + g_context = create_with_card(dmsdio_card_type_sdsc_v2, 8192); + DMOD_TEST_EXPECT_NOT_NULL(g_context); + + void *handle = open_card(g_context); + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_card_info, &info), 0); + DMOD_TEST_EXPECT_EQ(info.card_type, dmsdio_card_type_sdsc_v2); +} + +DMOD_TEST_STEP(identify_sdhc) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8000000); /* ~3.8 GiB */ + DMOD_TEST_EXPECT_NOT_NULL(g_context); + + void *handle = open_card(g_context); + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_card_info, &info), 0); + DMOD_TEST_EXPECT_EQ(info.card_type, dmsdio_card_type_sdhc); + DMOD_TEST_EXPECT_EQ(info.csd.capacity_blocks, 8000000ULL); +} + +/** + * @brief Capacity chosen so that capacity_blocks * 512 exceeds UINT32_MAX + * (2^32 - 1 = 4294967295) - if any layer between CSD parsing and + * dmdrvi_stat()/get_card_info truncated to 32 bits, this value + * would come back wrong. + */ +DMOD_TEST_STEP(identify_sdxc_reports_64bit_capacity_without_truncation) +{ + const uint64_t capacity_blocks = 4200000000ULL; /* ~2 TiB - exceeds UINT32_MAX bytes */ + const uint64_t capacity_bytes = capacity_blocks * DMSDIO_BLOCK_SIZE; + DMOD_TEST_EXPECT_TRUE(capacity_bytes > 0xFFFFFFFFULL); + + g_context = create_with_card(dmsdio_card_type_sdhc /* refined to sdxc by capacity, see identify_card() */, capacity_blocks); + DMOD_TEST_EXPECT_NOT_NULL(g_context); + + void *handle = open_card(g_context); + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_card_info, &info), 0); + DMOD_TEST_EXPECT_EQ(info.card_type, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(info.csd.capacity_blocks, capacity_blocks); + + dmdrvi_stat_t stat; + DMOD_TEST_EXPECT_EQ(dmdrvi_stat(g_context, "", &stat), 0); + DMOD_TEST_EXPECT_EQ((uint64_t)stat.size, capacity_bytes); +} + +/* ---- Block I/O ---- */ + +DMOD_TEST_STEP(read_write_roundtrip_full_sector) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + + uint8_t write_buf[DMSDIO_BLOCK_SIZE]; + for (size_t i = 0; i < sizeof(write_buf); i++) + { + write_buf[i] = (uint8_t)(i * 3 + 7); + } + + dmdrvi_ssize_t written = dmdrvi_write(g_context, handle, write_buf, sizeof(write_buf), (dmdrvi_offset_t)(DMSDIO_BLOCK_SIZE * 2)); + DMOD_TEST_EXPECT_EQ(written, (dmdrvi_ssize_t)sizeof(write_buf)); + + uint8_t read_buf[DMSDIO_BLOCK_SIZE] = {0}; + dmdrvi_ssize_t read = dmdrvi_read(g_context, handle, read_buf, sizeof(read_buf), (dmdrvi_offset_t)(DMSDIO_BLOCK_SIZE * 2)); + DMOD_TEST_EXPECT_EQ(read, (dmdrvi_ssize_t)sizeof(read_buf)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(write_buf, read_buf, sizeof(write_buf))); +} + +/** + * @brief Writes a small, unaligned chunk spanning parts of two sectors - + * the surrounding, untouched bytes of both sectors must survive + * the read-modify-write exactly as they were. + */ +DMOD_TEST_STEP(partial_sector_read_modify_write_preserves_surrounding_bytes) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + + /* Seed two full sectors with a known pattern first. */ + uint8_t seed[DMSDIO_BLOCK_SIZE * 2]; + for (size_t i = 0; i < sizeof(seed); i++) + { + seed[i] = (uint8_t)i; + } + DMOD_TEST_EXPECT_EQ(dmdrvi_write(g_context, handle, seed, sizeof(seed), 0), (dmdrvi_ssize_t)sizeof(seed)); + + /* Overwrite 10 bytes straddling the sector boundary (offset 508..517). */ + uint8_t patch[10]; + memset(patch, 0xAA, sizeof(patch)); + dmdrvi_offset_t patch_offset = DMSDIO_BLOCK_SIZE - 5; + DMOD_TEST_EXPECT_EQ(dmdrvi_write(g_context, handle, patch, sizeof(patch), patch_offset), (dmdrvi_ssize_t)sizeof(patch)); + + uint8_t verify[DMSDIO_BLOCK_SIZE * 2]; + DMOD_TEST_EXPECT_EQ(dmdrvi_read(g_context, handle, verify, sizeof(verify), 0), (dmdrvi_ssize_t)sizeof(verify)); + + /* Untouched prefix/suffix must be byte-for-byte the original seed. */ + DMOD_TEST_EXPECT_TRUE(bytes_equal(verify, seed, (size_t)patch_offset)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(verify + patch_offset, patch, sizeof(patch))); + size_t after = (size_t)patch_offset + sizeof(patch); + DMOD_TEST_EXPECT_TRUE(bytes_equal(verify + after, seed + after, sizeof(seed) - after)); +} + +/* ---- Error handling ---- */ + +DMOD_TEST_STEP(transient_crc_error_recovered_by_retry) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + + /* max_retries=3 in make_config() - 2 failures must still succeed. */ + dmsdio_port_mock_inject_error(dmsdio_error_command_crc, 2); + + uint8_t buf[DMSDIO_BLOCK_SIZE]; + dmdrvi_ssize_t result = dmdrvi_read(g_context, handle, buf, sizeof(buf), 0); + DMOD_TEST_EXPECT_EQ(result, (dmdrvi_ssize_t)sizeof(buf)); +} + +DMOD_TEST_STEP(persistent_crc_error_exhausts_retries) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + + /* Far more failures than max_retries=3 can absorb. */ + dmsdio_port_mock_inject_error(dmsdio_error_command_crc, 1000); + + uint8_t buf[DMSDIO_BLOCK_SIZE]; + dmdrvi_ssize_t result = dmdrvi_read(g_context, handle, buf, sizeof(buf), 0); + DMOD_TEST_EXPECT_EQ(result, -EIO); +} + +DMOD_TEST_STEP(timeout_during_identification_reports_no_card) +{ + dmsdio_port_mock_reset(dmsdio_card_type_sdsc_v1, 8192); + dmsdio_port_mock_inject_error(dmsdio_error_no_response, 1000); + + dmini_context_t ini = make_config(); + dmdrvi_dev_num_t host_dev_num; + g_context = dmdrvi_create(ini, &host_dev_num); + dmini_destroy(ini); + + /* create() itself must still succeed (the host controller exists even + * with no usable card in the slot) - only the card sub-node must never + * have been announced. */ + DMOD_TEST_EXPECT_NOT_NULL(g_context); + DMOD_TEST_EXPECT_FALSE(g_fake_mal_card_available); +} + +DMOD_TEST_STEP(removal_during_transfer_reported_precisely) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + DMOD_TEST_EXPECT_TRUE(g_fake_mal_card_available); + + dmsdio_port_mock_set_removed(true); + + uint8_t buf[DMSDIO_BLOCK_SIZE]; + dmdrvi_ssize_t result = dmdrvi_read(g_context, handle, buf, sizeof(buf), 0); + DMOD_TEST_EXPECT_EQ(result, -ENODEV); + DMOD_TEST_EXPECT_FALSE(g_fake_mal_card_available); +} + +/** + * @brief dmsdio_ioctl_cmd_get_generation reflects the generation an + * already-open handle was captured against + * + * Full hot-swap (remove card A, insert a *different* card B without the + * host controller itself being recreated, old handle must be rejected) is + * exactly what dmsdio_generation_t exists for, but actually triggering a + * second identify_card() pass needs the GPIO-backed card-detect friend + * (see dmsdio.c's rescan_card()/hotplug_worker()) - not something this + * protocol-only mock reaches into. That path is covered on real hardware + * (see docs/README.md "Hot-plug detection" testing notes) by physically + * swapping the card; this step only checks the generation plumbing a + * handle actually carries is correct. + */ +DMOD_TEST_STEP(handle_generation_matches_card_generation) +{ + g_context = create_with_card(dmsdio_card_type_sdhc, 8192); + void *handle = open_card(g_context); + DMOD_TEST_EXPECT_NOT_NULL(handle); + + dmsdio_generation_t generation = 0; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_generation, &generation), 0); + DMOD_TEST_EXPECT_NE(generation, 0U); + + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(dmdrvi_ioctl(g_context, handle, dmsdio_ioctl_cmd_get_card_info, &info), 0); + DMOD_TEST_EXPECT_EQ(info.generation, generation); } diff --git a/tests/fake_mal.c b/tests/fake_mal.c new file mode 100644 index 0000000..db103a9 --- /dev/null +++ b/tests/fake_mal.c @@ -0,0 +1,34 @@ +/** + * @brief Minimal dmdrvi "device_available"/"device_unavailable" MAL + * implementation for dmsdio_test.c + * + * In production this MAL is implemented by dmdevfs (which turns each + * announcement into a mounted device file). These tests exercise dmsdio.c + * directly, with no dmdevfs anywhere in the picture, so this file plays + * dmdevfs's part just enough to let dmsdio.c's own + * dmdrvi_device_available()/_unavailable() calls resolve to *something* + * instead of an unpatched MAL pointer - it only records the dev_num it was + * given and whether the card is currently "available", which is all the + * test steps in dmsdio_test.c need to construct their own dev_num for + * dmdrvi_open() and to assert hot-plug worked. + */ +#define DMOD_ENABLE_REGISTRATION ON +#define DMOD_MAL_dmdrvi +#include "dmdrvi.h" + +dmdrvi_dev_num_t g_fake_mal_last_dev_num; +bool g_fake_mal_card_available = false; + +void dmdrvi_device_available(dmdrvi_context_t context, const dmdrvi_dev_num_t *dev_num) +{ + (void)context; + g_fake_mal_last_dev_num = *dev_num; + g_fake_mal_card_available = true; +} + +void dmdrvi_device_unavailable(dmdrvi_context_t context, const dmdrvi_dev_num_t *dev_num) +{ + (void)context; + g_fake_mal_last_dev_num = *dev_num; + g_fake_mal_card_available = false; +} diff --git a/tests/fake_mal.h b/tests/fake_mal.h new file mode 100644 index 0000000..097be38 --- /dev/null +++ b/tests/fake_mal.h @@ -0,0 +1,10 @@ +#ifndef DMSDIO_TEST_FAKE_MAL_H +#define DMSDIO_TEST_FAKE_MAL_H + +#include "dmdrvi.h" +#include + +extern dmdrvi_dev_num_t g_fake_mal_last_dev_num; +extern bool g_fake_mal_card_available; + +#endif /* DMSDIO_TEST_FAKE_MAL_H */