diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80e903a..624fb94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,17 +19,31 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Build dmsdio core module + - name: Build dmsdio core module and tests run: | mkdir -p build cd build - # 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. - cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 + # The core module and its test run natively here, but DMOD_CPU_FAMILY + # has to stay at its default (stm32f7): dmsdio needs dmgpio's + # headers (card-detect friend) and dmgpio only publishes stm32 + # packages, so a DMOD_CPU_FAMILY=x86_64 header fetch would 404. + # That is the only reason DMOD_TOOLS_NAME is overridden by hand - + # everywhere else the tools follow from DMOD_CPU_FAMILY. + cmake .. -DDMOD_TOOLS_NAME=arch/x86_64 cmake --build . --target dmsdio test_dmsdio + - name: Build simulated-card dmsdio_port (x86_64) + run: | + # src/port/x86_64 is not a hardware port: it simulates an SD card + # at the command level so the whole protocol in dmsdio can run on + # this runner. DMOD_CPU_FAMILY=x86_64 alone selects the host tools + # (src/port/x86_64/config.cmake). + mkdir -p build_port_x86_64 + cd build_port_x86_64 + cmake .. -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: | echo "Checking for module files..." @@ -37,11 +51,16 @@ 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 + # Runtime dependencies of dmsdio/test_dmsdio (see build/dmf/*.dmd). + # dmsdio_port is the simulated card built above - it is never + # published for x86_64, so it is not fetched here. + dmf-get install dmini -y dmod_loader build/dmf/test_dmsdio.dmf discover-cpu-families: @@ -58,9 +77,12 @@ jobs: - name: List available CPU families id: list-families run: | - # Find all CPU families by looking for directories containing port.c + # Find all CPU families by looking for directories containing port.c. + # x86_64 is the simulated card used by the test job above, not a + # hardware family. CPU_FAMILIES=$(find src/port -mindepth 2 -maxdepth 2 -name "port.c" -type f -printf '%h\n' | \ xargs -n1 basename | \ + grep -vx x86_64 | \ sort | \ jq -R -s -c 'split("\n") | map(select(length > 0))') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc1b7c0..1b931a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,8 +19,10 @@ jobs: - name: List available CPU families id: list-families run: | + # x86_64 is the simulated SD card used by the tests - never released. CPU_FAMILIES=$(find src/port -mindepth 2 -maxdepth 2 -name "port.c" -type f -printf '%h\n' | \ xargs -n1 basename | \ + grep -vx x86_64 | \ sort | \ jq -R -s -c 'split("\n") | map(select(length > 0))') diff --git a/.gitignore b/.gitignore index c2af1f8..856a344 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ build/ *.tmp *.swp *~ + +# Local CI builds +build_*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c51264..909c616 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ set(DMOD_BUILD_TEMPLATES OFF CACHE BOOL "Build templates") FetchContent_MakeAvailable(dmod) project(dmsdio VERSION ${DMOD_MODULE_VERSION} - DESCRIPTION "dmsdio DMOD library module" + DESCRIPTION "SD memory card driver (SDSC/SDHC/SDXC) for DMOD" LANGUAGES C CXX) set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") @@ -65,6 +65,25 @@ set(DMOD_DIF_IMPLS ) dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} # List of source files - can include C and C++ files src/dmsdio.c + src/dmsdio_card.c + src/dmsdio_cmd.c + src/dmsdio_config.c + src/dmsdio_decode.c + src/dmsdio_detect.c + src/dmsdio_ident.c + src/dmsdio_io.c + src/dmsdio_xfer.c +) + +# dmdrvi 2.1: the host node implements the monitor ioctls (presence is +# monitored by dmdevfs' dmdevmon service, see docs/api-reference.md). +# dmgpio 1.1: its ioctls start at DMDRVI_IOCTL_CUSTOM_BASE - the card detect +# read (dmgpio_ioctl_cmd_get_high/low_pins_state) has other values in 1.0. +dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi@>=2.1 + dmini + dmosi + dmgpio@>=1.1 ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE @@ -75,11 +94,6 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) -# To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) - add_subdirectory(src/port) target_link_libraries(${DMOD_MODULE_NAME} ${DMOD_MODULE_NAME}_port_if) enable_testing() diff --git a/Makefile b/Makefile index bed51b5..4caccca 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ # ############################################################################# # -# This is an example of a simple library module. +# dmsdio - SD memory card driver (SDSC/SDHC/SDXC). +# +# The CMake build is the reference build: it also fetches the headers of +# dmdrvi, dmini, dmosi and dmgpio (dmod_link_modules) and builds +# dmsdio_port and the tests. Pass their include directories through +# DMOD_INC_DIRS when building with make. # # ############################################################################# DMOD_DIR=@DMOD_DIR@ @@ -24,7 +29,15 @@ DMOD_MODULE_VERSION=0.1 DMOD_AUTHOR_NAME=Patryk Kubiak # The list of C sources -DMOD_CSOURCES=src/dmsdio.c +DMOD_CSOURCES=src/dmsdio.c \ + src/dmsdio_card.c \ + src/dmsdio_cmd.c \ + src/dmsdio_config.c \ + src/dmsdio_decode.c \ + src/dmsdio_detect.c \ + src/dmsdio_ident.c \ + src/dmsdio_io.c \ + src/dmsdio_xfer.c # The list of C++ sources DMOD_CXXSOURCES= diff --git a/README.md b/README.md index 31f5462..320c713 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,30 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/choco-technologies/dmsdio/actions/workflows/ci.yml/badge.svg)](https://github.com/choco-technologies/dmsdio/actions/workflows/ci.yml) -dmsdio DMOD library module. +SD memory card driver for DMOD: SDSC v1/v2, SDHC and SDXC cards exposed as +a 64-bit block device through dmdrvi 2.0. ## Description -TODO: describe what this module does. +- Complete, hardware-independent SD protocol: identification (CMD0, CMD8, + ACMD41, CMD2, CMD3, CMD9, CMD7), OCR/CID/CSD/SCR/SD Status decoding, + 400 kHz identification, default speed and CMD6 High Speed, 1-bit/4-bit + bus negotiation. +- SDSC byte addressing and SDHC/SDXC block addressing; full 64-bit + capacity and offsets. +- Single/multi-block read and write with CMD12, CMD13 ready polling, erase + and discard, retry with recovery, precise errors. +- Arbitrary byte offsets: direct full-sector transfers, read-modify-write + for partial sectors. +- dmdrvi device model: persistent host node `/dev/dmsdio0` plus the + hot-plugged card node `/dev/dmsdio0/0`; handles are invalidated with a + card generation identifier. +- No threads in the driver: the host node implements the dmdrvi 2.1 + monitor contract (`DMDRVI_IOCTL_MONITOR_GET_POLICY`/`_EVENT`/`_REFRESH`). + dmdevfs reports it as a `monitor` device and the generic + [dmdevmon](https://github.com/choco-technologies/dmdevfs/blob/main/services/dmdevmon/README.md) service drives it - card detect + edge interrupt, settling, optional polling. The card node is reported as a + `block` device (e.g. for automount). ## Building @@ -31,59 +50,86 @@ make DMOD_MODE=DMOD_MODULE DMOD_DIR=/path/to/dmod ## Testing -Tests are built automatically alongside the module (see `tests/`). Once built, -run them with `ctest`: +`tests/dmsdio_test.c` runs the complete protocol against the simulated SD +card of the x86_64 port (`src/port/x86_64`): SDSC v1/v2, SDHC and SDXC +identification, 64-bit capacity and offsets, sector and unaligned I/O, +erase, write protection, CRC/timeout/malformed-response errors, retries, +removal during a transfer, stale handles and the monitor commands. The test +uses the driver the way dmdevfs does - through the dmdrvi DIF - implements +the dmdrvi MAL to observe hot-plug notifications and calls the monitor +ioctls the way dmdevmon does. ```bash -cd build -ctest --output-on-failure -``` - -`ctest` installs the test module's dependencies with `dmf-get` and then runs -it through `dmod_loader`. To run it manually instead: +# core module and test: built for the host, but DMOD_CPU_FAMILY stays at its +# default (stm32f7) because dmgpio only publishes stm32 headers +mkdir -p build && cd build +cmake .. -DDMOD_TOOLS_NAME=arch/x86_64 +cmake --build . --target dmsdio test_dmsdio +cd .. + +# simulated card port +mkdir -p build_port_x86_64 && cd build_port_x86_64 +cmake .. -DDMOD_CPU_FAMILY=x86_64 +cmake --build . --target dmsdio_port +cp dmf/dmsdio_port.dmf dmf/dmsdio_port.dmd ../build/dmf/ +cd .. -```bash export DMOD_DMF_DIR=$(pwd)/build/dmf -dmf-get install -d ${DMOD_DMF_DIR}/test_dmsdio-local.dmd -y +dmf-get install dmini -y dmod_loader build/dmf/test_dmsdio.dmf ``` ## Usage - - -This library module provides functions that can be used by other modules: +dmsdio is a dmdrvi driver loaded by dmdevfs from an ini configuration (see +[docs/configuration.md](docs/configuration.md)): -```c -#include "dmsdio.h" +```ini +[dmsdio] +instance=1 +bus_width=4 ``` +Card presence is monitored by dmdevfs' dmdevmon service: install its +`dmdevmon.rules` and `dmdevmon@.ini` into libsystemd's rules and units +directories, and libsystemd starts `dmdevmon@dmsdio0` as soon as dmdevfs +reports `/dev/dmsdio0`. Its first `REFRESH` identifies a card already in the +slot; `/dev/dmsdio0/0` then appears and can be used with the regular file +API using 64-bit offsets, `DMDRVI_IOCTL_BLOCK_GET_INFO` reports the +geometry. Without a monitor no card is ever identified. + ## API -| Function | Description | -|----------|-------------| -| `dmsdio_create()` | Create a new `dmsdio_t` instance. | -| `dmsdio_destroy()` | Destroy an instance created by `_create()`. | -| `dmsdio_is_valid()` | Check whether a handle is a valid instance. | +| Item | Description | +|------|-------------| +| dmdrvi DIF (`dmdrvi_create`, `_open`, `_read`, `_write`, `_ioctl`, `_flush`, `_stat`, `_friend_changed`, ...) | Device model, see [docs/api-reference.md](docs/api-reference.md) | +| `DMDRVI_IOCTL_MONITOR_GET_POLICY` / `_EVENT` / `_REFRESH` | dmdrvi monitor contract on the host node | +| `dmsdio_ioctl_cmd_get_host_info` / `_get_card_info` | Driver specific ioctls | +| `dmsdio_decode_csd()` / `_cid()` / `_scr()` / `_ssr()` | Register decoders | -See [include/dmsdio.h](include/dmsdio.h) for the full -declarations and [docs/api-reference.md](docs/api-reference.md) for the -complete reference. +See [include/dmsdio.h](include/dmsdio.h) and +[include/dmsdio_types.h](include/dmsdio_types.h) for the declarations and +[docs/api-reference.md](docs/api-reference.md) for the complete reference. ## Documentation See the `docs/` directory: - **[api-reference.md](docs/api-reference.md)** - Complete API documentation +- **[configuration.md](docs/configuration.md)** - Configuration keys and card detect +- **[port-implementation.md](docs/port-implementation.md)** - Port API contract View documentation using `dmf-man dmsdio`. ## Hardware Port This module ships two DMOD modules: the architecture-independent -`dmsdio` and `dmsdio_port`, which contains the -architecture-specific implementation. The active architecture is selected via -`DMOD_CPU_FAMILY` (default: `stm32f7`): +`dmsdio` (all SD protocol logic) and `dmsdio_port`, which contains the +protocol-free host controller primitives. The active architecture is +selected via `DMOD_CPU_FAMILY` (default: `stm32f7`): `stm32f4` (SDIO) and +`stm32f7` (SDMMC1) share their implementation in `src/port/stm32_common`; +`x86_64` is the simulated card used by the tests. Board configurations for +STM32F746G-DISCO and STM32F407G-DISC1 are in [configs/](configs/README.md). ```bash cmake .. -DDMOD_CPU_FAMILY=stm32f7 @@ -96,8 +142,21 @@ another architecture. Port-specific files: ├── include/dmsdio_port.h ├── src/port/ │ ├── CMakeLists.txt -│ └── stm32f7/ +│ ├── stm32_common/ # SDIO/SDMMC host shared by F4 and F7 +│ │ ├── stm32_common.c # lifecycle, clock, commands, IRQ +│ │ ├── stm32_common.h +│ │ └── stm32_data.c # data path: DMA2 lease (dmdma), FIFO for small reads +│ ├── stm32f4/ +│ │ ├── config.cmake +│ │ └── port.c +│ ├── stm32f7/ +│ │ ├── config.cmake +│ │ └── port.c +│ └── x86_64/ # simulated SD card (tests only) │ ├── config.cmake +│ ├── dmsdio_mock.h +│ ├── mock_card.c/.h +│ ├── mock_registers.c │ └── port.c └── dmsdio_port.dmr ``` @@ -107,9 +166,20 @@ another architecture. Port-specific files: dmsdio/ ├── docs/ # Documentation (markdown format) ├── include/ # Public headers -│ └── dmsdio.h +│ ├── dmsdio.h +│ ├── dmsdio_port.h +│ └── dmsdio_types.h ├── src/ -│ └── dmsdio.c +│ ├── dmsdio.c # dmdrvi device model +│ ├── dmsdio_card.c # attach/detach, card generation +│ ├── dmsdio_cmd.c # command layer, errno mapping +│ ├── dmsdio_config.c # ini configuration +│ ├── dmsdio_decode.c # CID/CSD/SCR/SD Status decoders +│ ├── dmsdio_detect.c # card detect pin, lock-free MONITOR_EVENT +│ ├── dmsdio_ident.c # identification and bus negotiation +│ ├── dmsdio_io.c # byte-offset I/O, read-modify-write +│ ├── dmsdio_xfer.c # block transfers, erase, retry/recovery +│ └── port/ ├── tests/ │ ├── CMakeLists.txt │ └── dmsdio_test.c diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..f8b06fe --- /dev/null +++ b/configs/README.md @@ -0,0 +1,19 @@ +# dmsdio Configuration Files + +Board configurations loaded by dmdevfs to bring the SD card slot up. Each +file configures the SD bus pins through dmgpio (AF12, pull-ups on CMD and +D0-D3), the optional card-detect GPIO (`friend_role=card_detect`) and the +dmsdio device itself, all in one `friends_group`. The pin sources are cited +in each file - cross-check them against your own board revision before +flashing. + +``` +configs/ +└── board/ + ├── stm32f407g-disc1/ + │ └── sdio.ini # external microSD breakout on SDIO + └── stm32f746g-disco/ + └── sdmmc1.ini # on-board microSD slot on SDMMC1 +``` + +See [../docs/configuration.md](../docs/configuration.md) for every dmsdio key. diff --git a/configs/board/stm32f407g-disc1/sdio.ini b/configs/board/stm32f407g-disc1/sdio.ini new file mode 100644 index 0000000..a5c02fb --- /dev/null +++ b/configs/board/stm32f407g-disc1/sdio.ini @@ -0,0 +1,118 @@ +; +; STM32F407G-DISC1 with an external microSD breakout on SDIO. +; +; Pins: PC8-PC11 = D0-D3, PC12 = CK, PD2 = CMD (the only SDIO pin mapping of +; STM32F407, RM0090 / DS8626 AF table, AF12), card detect wired to PD3. +; Wire the breakout's card-detect switch to PD3 and GND; if the breakout has +; no switch, remove the [sd0_cd] section and set poll_interval_ms instead. +; +; PC10 and PC12 are also routed to the on-board CS43L22 audio DAC (I2S3 +; SCK/SD). Keep the codec in reset (PD4 low, its reset state) while the SD +; card is used. +; +; SDIO is clocked from PLL48CLK (dmclk_domain_sdio): dmclk must produce +; 48 MHz (PLLQ=7 at 168 MHz), otherwise dmsdio_port refuses to start. +; +; Data moves by DMA: dmsdio_port leases DMA2 stream 3 (or 6), channel 4, from +; dmdma when the host starts, so the DMA2 controller must already be +; configured - load dmdma's configs/board/stm32f4-discovery/dma.ini (driver_order=2) +; together with this file. The dmsdio section below uses driver_order=3 so +; it is created after it. +; +; D0 (SDIO/SDMMC1_D0) +[sd0_d0] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC8 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D1 +[sd0_d1] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC9 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D2 +[sd0_d2] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC10 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D3 +[sd0_d3] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC11 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; CK - no pull-up on the clock line +[sd0_ck] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC12 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=none + +; CMD +[sd0_cmd] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PD2 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; Card detect: switch to GND when a card is inserted (active low). +[sd0_cd] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +friend_role=card_detect +pin=PD3 +mode=input +pull=up +interrupt_trigger=both_edges +interrupt_handler=sd0_card_detect + +[sd0] +driver_name=dmsdio +driver_order=3 +friends_group=sd0 +instance=1 +bus_width=4 +max_clock_hz=50000000 +high_speed=true +card_detect_active_level=low +; Presence monitoring policy (dmdrvi monitor contract), carried out by +; dmdevfs' dmdevmon service: the card detect edge interrupt instead of polling. +monitor_event_handler=sd0_card_detect +poll_interval_ms=0 diff --git a/configs/board/stm32f746g-disco/sdmmc1.ini b/configs/board/stm32f746g-disco/sdmmc1.ini new file mode 100644 index 0000000..e886670 --- /dev/null +++ b/configs/board/stm32f746g-disco/sdmmc1.ini @@ -0,0 +1,116 @@ +; +; STM32F746G-DISCO on-board microSD slot (CN3) on SDMMC1. +; +; Pins (UM1907, microSD section; same assignment as the ST BSP +; stm32746g_discovery_sd.c): PC8-PC11 = D0-D3, PC12 = CK, PD2 = CMD, +; PC13 = uSD_Detect (low when a card is inserted). +; AF12 = SDMMC1 on every one of these pins (RM0385 / DS10916 AF table). +; D0-D3 and CMD need pull-ups (SD Physical Layer spec, bus topology); the +; clock line does not. +; +; SDMMC1 is clocked from CLK48 (dmclk_domain_sdio): dmclk must produce +; 48 MHz (PLLQ=9 at 216 MHz), otherwise dmsdio_port refuses to start. +; +; Data moves by DMA: dmsdio_port leases DMA2 stream 3 (or 6), channel 4, from +; dmdma when the host starts, so the DMA2 controller must already be +; configured - load dmdma's configs/board/stm32f746g-disco/dma.ini (driver_order=2) +; together with this file. The dmsdio section below uses driver_order=3 so +; it is created after it. +; +; D0 (SDIO/SDMMC1_D0) +[sd0_d0] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC8 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D1 +[sd0_d1] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC9 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D2 +[sd0_d2] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC10 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; D3 +[sd0_d3] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC11 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; CK - no pull-up on the clock line +[sd0_ck] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PC12 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=none + +; CMD +[sd0_cmd] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +pin=PD2 +mode=alternate +alternate_function=12 +speed=maximum +output_circuit=push_pull +pull=up + +; Card detect: switch to GND when a card is inserted (active low). +[sd0_cd] +driver_name=dmgpio +driver_order=2 +friends_group=sd0 +friend_role=card_detect +pin=PC13 +mode=input +pull=up +interrupt_trigger=both_edges +interrupt_handler=sd0_card_detect + +[sd0] +driver_name=dmsdio +driver_order=3 +friends_group=sd0 +instance=1 +bus_width=4 +max_clock_hz=50000000 +high_speed=true +card_detect_active_level=low +; Presence monitoring policy (dmdrvi monitor contract), carried out by +; dmdevfs' dmdevmon service: the card detect edge interrupt instead of polling. +monitor_event_handler=sd0_card_detect +poll_interval_ms=0 diff --git a/dmsdio.dmr b/dmsdio.dmr index 6c2ea51..20f753e 100644 --- a/dmsdio.dmr +++ b/dmsdio.dmr @@ -18,6 +18,9 @@ version=./${module}_version.txt => ${destination}/${module}_version.txt [origin= # Module documentation in markdown format for dmf-man tool docs=./docs => ${destination}/${module}/docs [origin=${repo_dir}/docs] +# Board configurations +configs=./configs => ${destination}/${module}/configs [origin=${repo_dir}/configs] + # README file readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/README.md] diff --git a/docs/README.md b/docs/README.md index f88b479..2ef0d1e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,23 @@ # dmsdio Documentation -Welcome to the dmsdio module documentation. +SD memory card driver (SDSC v1/v2, SDHC, SDXC) exposing a 64-bit block +device through dmdrvi 2.0, monitored through the dmdrvi 2.1 monitor contract. ## Contents -- **[api-reference.md](api-reference.md)** - Complete API documentation +- **[api-reference.md](api-reference.md)** - device nodes, I/O semantics, ioctls, errors, types +- **[configuration.md](configuration.md)** - ini keys, card detect, presence monitoring +- **[port-implementation.md](port-implementation.md)** - port API contract and adding a CPU family ## Quick Reference ```c -#include "dmsdio.h" +#include "dmsdio.h" /* register decoders, dmsdio_types.h */ +``` + +```text +/dev/dmsdio0 host node (status, monitor ioctls) +/dev/dmsdio0/0 card node (block device, hot-plugged) ``` View documentation using `dmf-man`: diff --git a/docs/api-reference.md b/docs/api-reference.md index cbad02b..9fb1434 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,12 +1,166 @@ # dmsdio API Reference -Document the module's public types and functions here as you add them to -`include/dmsdio.h`. +dmsdio is an SD memory card driver (SDSC v1/v2, SDHC, SDXC) for the dmdrvi +2.0 device model. All SD protocol handling lives in the architecture +independent `dmsdio` module; `dmsdio_port` only provides raw host controller +primitives (see [port-implementation.md](port-implementation.md)). + +## Device nodes + +| Node | dmdrvi numbering | Lifetime | Purpose | +|------|------------------|----------|---------| +| `/dev/dmsdioN` | `DMDRVI_NUM_MAJOR`, major = N | Persistent, created by `dmdrvi_create()` | Host status and monitor ioctls | +| `/dev/dmsdioN/0` | `DMDRVI_NUM_MAJOR \| DMDRVI_NUM_MINOR`, minor 0 | Announced with `dmdrvi_device_available()` by `DMDRVI_IOCTL_MONITOR_REFRESH` once a card is identified *and* dmdevfs has reported the host node through `dmdrvi_path_ready()`; withdrawn with `dmdrvi_device_unavailable()` by the `REFRESH` that finds the card gone | 64-bit byte-addressed block device | + +`N` defaults to `instance - 1` (SDMMC1 → `/dev/dmsdio0`), see +[configuration.md](configuration.md). + +The driver starts no threads and does not touch the card in +`dmdrvi_create()`. Presence is driven through the dmdrvi monitor contract on +the host node (see [Monitor commands](#monitor-commands) and +[configuration.md](configuration.md#presence-monitoring)): dmdevfs reports +the host node - which answers `DMDRVI_IOCTL_MONITOR_GET_POLICY` - as a +`monitor` device, and the monitor service it starts (dmdevmon) calls +`REFRESH` right away, which identifies a card already in the slot. The card +node answers `DMDRVI_IOCTL_BLOCK_GET_INFO`, so dmdevfs reports it as a +`block` device. The driver itself never talks to libsystemd. + +The card node is only announced after `dmdrvi_path_ready()` for the host +node, because dmdevfs ignores hot-plug notices for a context it has not +registered yet. + +## Card node (`/dev/dmsdioN/0`) + +### read / write + +```c +dmdrvi_ssize_t dmdrvi_read (ctx, handle, void* buf, size_t size, dmdrvi_offset_t offset); +dmdrvi_ssize_t dmdrvi_write(ctx, handle, const void* buf, size_t size, dmdrvi_offset_t offset); +``` + +* `offset` is a 64-bit byte offset; any offset and length are accepted. + Whole blocks are transferred directly between the caller's buffer and the + card (CMD17/CMD18, CMD24/CMD25 + CMD12, split into at most + `max_blocks_per_transfer` blocks per command) when the port accepts the + buffer for that direction (`dmsdio_port_buffer_is_direct()`: 32-byte + aligned, reachable, and fast enough - on STM32 a write from external + SDRAM is not). Other whole blocks go through the bounce buffer, up to + `bounce_blocks` blocks per command; the port allocates it + (`dmsdio_port_buffer_alloc()`, internal SRAM on STM32). A partial block at + either end of the range goes through the bounce buffer too, and writes + read-modify-write it so neighbouring bytes are preserved. +* Reads return the byte count, clipped at the end of the card, and `0` at + or past the end. Writes starting at or past the end return `-ENOSPC`, + writes crossing the end are clipped. +* A write returns once the card reports `READY_FOR_DATA` in the transfer + state again (CMD13 polling), i.e. once the data is programmed. +* Operations of one host are serialized; one request is never interleaved + with another. + +### flush + +Waits until the card is back in the transfer state and ready for data. + +### ioctl + +| Command | `arg` | Result | +|---------|-------|--------| +| `DMDRVI_IOCTL_BLOCK_GET_INFO` | `dmdrvi_block_info_t*` | `logical_block_size` = 512, `block_count` (64-bit), `erase_block_size` = 512 when the card supports erase, flags `REMOVABLE`, `ERASE_SUPPORTED`, `DISCARD_SUPPORTED` (SD Status `DISCARD_SUPPORT`), `READ_ONLY` (CSD write protection) | +| `DMDRVI_IOCTL_BLOCK_ERASE` | `const dmdrvi_block_range_t*` | CMD32/CMD33/CMD38, waits for completion. Range must be 512-byte aligned and inside the card | +| `DMDRVI_IOCTL_BLOCK_DISCARD` | `const dmdrvi_block_range_t*` | CMD38 with the discard argument; `-ENOTSUP` unless advertised | +| `dmsdio_ioctl_cmd_get_host_info` | `dmsdio_host_info_t*` | See below | +| `dmsdio_ioctl_cmd_get_card_info` | `dmsdio_card_info_t*` | Raw and decoded CID/CSD/SCR/SD Status, capacity, bus width, clock, High Speed | + +### stat + +`dmdrvi_stat()` on the card node reports the full 64-bit capacity as +`size` (`0444` when the card is write protected, `0666` otherwise). + +## Host node (`/dev/dmsdioN`) + +Data transfers return `-ENOTSUP`; `stat` reports size 0. + +| Command | `arg` | Result | +|---------|-------|--------| +| `dmsdio_ioctl_cmd_get_host_info` | `dmsdio_host_info_t*` | `card_attached`, `generation`, `scan_count`, `last_error`, `retry_count` | +| `dmsdio_ioctl_cmd_get_card_info` | `dmsdio_card_info_t*` | Card snapshot, `-ENODEV` without a card | + +### Monitor commands + +The dmdrvi 2.1 monitor contract, answered on the host node only - on the +card node they return `-ENOTTY`, so dmdevfs does not report the card node +as monitored. + +| Command | `arg` | Result | +|---------|-------|--------| +| `DMDRVI_IOCTL_MONITOR_GET_POLICY` | `dmdrvi_monitor_policy_t*` | `event_handler` (`monitor_event_handler`, `""` = none), `settle_ms` (`monitor_settle_ms`), `poll_interval_ms` from the ini section | +| `DMDRVI_IOCTL_MONITOR_EVENT` | `NULL` | Samples card detect **without taking the driver lock**. When the slot is empty, a transfer in progress is abandoned with `-ENODEV`; the next `REFRESH` detaches the card. Never announces or withdraws nodes. `0` (also without a card detect pin), `-EIO` pin unreadable | +| `DMDRVI_IOCTL_MONITOR_REFRESH` | `NULL` | Under the driver lock: samples card detect, verifies an attached card (CMD13) or identifies a new one, then announces or withdraws the card node to match. The only place the node is announced or withdrawn. `0` when a card is attached afterwards, `-ENODEV` when the slot is empty, another negative errno when identification failed | + +A transfer that finds the card gone (it stops answering, or `EVENT` flagged +the slot empty) detaches it at once: handles go stale and I/O fails with +`-ENODEV`. The node itself is withdrawn by the next `REFRESH` - after the +card detect edge's settle time, or at the next poll. + +## Card generation and stale handles + +Every attach and every detach increments the host's card generation +(`dmsdio_host_info_t.generation`). A card node handle remembers the +generation it was opened for: + +* no card attached any more → `-ENODEV` +* a different card (or the same card re-inserted) attached → `-ESTALE` + +Close the old handle and open the card node again after a new +`dmdrvi_device_available()` notification. + +## Errors + +| Error | Meaning | +|-------|---------| +| `-EBADMSG` | CRC error on a response or data block (with `retries=0`) | +| `-ETIMEDOUT` | Command, data or busy timeout (with `retries=0`) | +| `-EIO` | A transient error (CRC, timeout, FIFO/DMA fault) persisted through all `retries` extra attempts, or a card-internal error (ECC, CC_ERROR, ...) | +| `-EPROTO` | Malformed response: wrong command index, bad CMD8 echo, inconsistent registers, illegal command. Never retried | +| `-ENODEV` | No card, or the card disappeared (also during a transfer). The next `REFRESH` withdraws the card node | +| `-ESTALE` | Handle belongs to a card generation that is no longer attached | +| `-EROFS` | Card is write protected | +| `-EINVAL` | Invalid argument or range (negative offset, misaligned erase range, card reported OUT_OF_RANGE/ADDRESS_ERROR) | +| `-EOVERFLOW` | `size` larger than `INT64_MAX` | +| `-EBADF` | Write on a read-only handle or read on a write-only handle | +| `-ENOSPC` | Write starting at or beyond the end of the card | +| `-ENOTSUP` | Operation not available on this node / card | +| `-ENOTTY` | Unknown ioctl | + +Transient errors are retried after bringing the card back to the transfer +state (port abort, CMD12 when the card is still sending/receiving, CMD13 +until ready). A card that stops answering CMD13 during that recovery is +treated as removed. ## Types -_(none yet)_ +Declared in `include/dmsdio_types.h`: + +* `dmsdio_card_type_t` - `sdsc_v1`, `sdsc_v2`, `sdhc`, `sdxc` +* `dmsdio_card_info_t` - `type`, `generation`, `rca`, `ocr`, `cid_raw[4]`, + `csd_raw[4]` (most significant word first), decoded `cid`, `csd`, `scr`, + `ssr`, `capacity_bytes`, `block_count`, `block_addressing`, `high_speed`, + `write_protected`, `bus_width`, `clock_hz` +* `dmsdio_host_info_t` - `instance`, `card_attached`, `generation`, + `scan_count`, `last_error`, `retry_count` +* `dmsdio_cid_t`, `dmsdio_csd_t`, `dmsdio_scr_t`, `dmsdio_ssr_t` - decoded registers + +## Functions (Built-in API) + +Pure register decoders, also used by the driver itself: -## Functions +```c +int dmsdio_decode_csd(const uint32_t raw[4], dmsdio_csd_t* csd); +int dmsdio_decode_cid(const uint32_t raw[4], dmsdio_cid_t* cid); +int dmsdio_decode_scr(const uint8_t raw[8], dmsdio_scr_t* scr); +int dmsdio_decode_ssr(const uint8_t raw[64], dmsdio_ssr_t* ssr); +``` -_(none yet)_ +`dmsdio_decode_csd()` supports CSD v1.0 (SDSC) and v2.0 (SDHC/SDXC) and +returns `-ENOTSUP` for CSD v3.0 (SDUC). All return `-EINVAL` for NULL +arguments and `-EPROTO` for inconsistent contents. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..9dc67fc --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,108 @@ +# dmsdio Configuration + +dmsdio is configured through dmdevfs with an ini file. The settings are read +from the section dmdevfs activates for the device (board files describing +several devices), otherwise from a `[dmsdio]` section, otherwise from the +first section containing an `instance` key. + +```ini +[dmsdio] +instance=1 +bus_width=4 +max_clock_hz=50000000 +high_speed=true +card_detect_active_level=low +monitor_event_handler=sd_card_detect +poll_interval_ms=0 +``` + +| Key | Default | Description | +|-----|---------|-------------| +| `instance` | `1` | Host controller instance (1 = SDMMC1/SDIO) | +| `major` | `instance - 1` | Device number: `/dev/dmsdio` and `/dev/dmsdio/0` | +| `bus_width` | `4` | Widest bus wired on the board (`1` or `4`). 4-bit is used only when the card's SCR supports it | +| `max_clock_hz` | `50000000` | Upper clock limit. High Speed (50 MHz class) is only attempted above 25 MHz | +| `high_speed` | `true` | Allow the CMD6 High Speed switch. The clock is raised only after the card's switch status confirms it | +| `retries` | `3` | Extra attempts for transient errors (0-16). With `0`, errors are reported precisely instead of as `-EIO` | +| `init_timeout_ms` | `1000` | ACMD41 power-up limit | +| `read_timeout_ms` | `250` | Per-block read access limit | +| `write_timeout_ms` | `500` | Per-block write busy limit, also used for CMD13 ready polling | +| `erase_timeout_ms` | `3000` | Minimum erase busy limit per 4 MiB chunk (raised from SD Status `ERASE_TIMEOUT` when larger) | +| `max_blocks_per_transfer` | `128` | Split larger requests into several multi-block commands | +| `bounce_blocks` | `8` | Bounce buffer size in blocks (4 KiB by default), taken from the port's DMA-capable memory - dmod-boot's `dma` heap on STM32. Caller buffers the port cannot use directly (misaligned, or writes from external SDRAM) move through it, this many blocks per command | +| `card_detect_active_level` | `low` | Card detect level meaning "card inserted" (`low` or `high`) | +| `monitor_event_handler` | none | dmhaman handler name the card-detect GPIO's `interrupt_handler` points to (at most 31 characters) | +| `monitor_settle_ms` | `50` | Quiet time after the last card detect edge before the card is re-checked | +| `poll_interval_ms` | `1000` | Periodic presence re-check; `0` disables it | + +The last three keys are the dmdrvi monitor policy, under the names dmdrvi +prescribes for every driver: dmsdio does not act on them itself, it hands +them out through `DMDRVI_IOCTL_MONITOR_GET_POLICY` (see below). + +## Card detect + +Card detect is optional. It is a dmgpio device in the same `friends_group` +as the dmsdio device, with `friend_role=card_detect`, interrupts on both +edges and its `interrupt_handler` set to the name used for +`monitor_event_handler`. For example (board file with one section per +device): + +```ini +[sd_card] +driver_name=dmsdio +friends_group=sd0 +instance=1 +bus_width=4 +card_detect_active_level=low +monitor_event_handler=sd0_card_detect +poll_interval_ms=0 + +[sd_card_detect] +driver_name=dmgpio +friends_group=sd0 +friend_role=card_detect +pin=PD3 +mode=input +pull=up +interrupt_trigger=both_edges +interrupt_handler=sd0_card_detect +``` + +The SD bus pins themselves (CK, CMD, D0-D3: AF12, pull-ups on CMD and +D0-D3) are dmgpio devices of the same group - see the complete board files +in [../configs/](../configs/README.md) for STM32F746G-DISCO and +STM32F407G-DISC1. + +dmdevfs reports the GPIO node to dmsdio through `dmdrvi_friend_changed()`; +the driver samples it on every scan. + +## Presence monitoring + +The driver starts no threads and does not touch the card in +`dmdrvi_create()`. Everything that happens over time is driven through the +dmdrvi monitor contract on the host node by dmdevfs' generic +[dmdevmon](https://github.com/choco-technologies/dmdevfs/blob/main/services/dmdevmon/README.md) service, one instance per host: + +1. dmdevfs reports the host node `/dev/dmsdio`, which answers + `DMDRVI_IOCTL_MONITOR_GET_POLICY`, to libsystemd as a `monitor` device + named `dmsdio`. +2. The `[class=monitor]` rule from `dmdevmon.rules` starts + `dmdevmon@dmsdio` from the `dmdevmon@.ini` template, with the node + path as its argument. +3. dmdevmon reads the policy (`monitor_event_handler`, `monitor_settle_ms`, + `poll_interval_ms`), registers the dmhaman handler - it runs in interrupt + context and only wakes dmdevmon - and calls `DMDRVI_IOCTL_MONITOR_REFRESH` + once, which identifies a card already in the slot. +4. On an edge dmdevmon calls `DMDRVI_IOCTL_MONITOR_EVENT` - lock-free, so a + transfer running on a removed card is aborted with `-ENODEV` at once - + and, once the pin has been quiet for `monitor_settle_ms`, `REFRESH`, + which identifies, verifies (CMD13) or detaches the card under the driver + lock and announces or withdraws `/dev/dmsdio/0`. +5. Without a card detect pin, `REFRESH` runs every `poll_interval_ms`. With + neither, dmdevmon refreshes once and exits. + +Install `dmdevmon.rules` into libsystemd's rules directory and +`dmdevmon@.ini` into its units directory (both ship in the dmdevmon package +under `configs/`). Without a monitor no card is ever identified. + +Use a distinct `monitor_event_handler` name per host. diff --git a/docs/port-implementation.md b/docs/port-implementation.md index 1e8e53e..ca8bc82 100644 --- a/docs/port-implementation.md +++ b/docs/port-implementation.md @@ -1,24 +1,134 @@ -# Adding a New MCU Port to dmsdio - -`dmsdio_port` currently supports `stm32f7`. The port is split from -the core `dmsdio` module so a new architecture can be added without -touching architecture-independent logic. - -## Steps to add another architecture - -1. Create `src/port//config.cmake`, setting `DMOD_TOOLS_NAME` for the - target architecture (see `src/port/stm32f7/config.cmake` for the pattern - - it must match a directory under `dmod/configs/arch/...`). -2. Create `src/port//port.c` implementing the - `dmod_dmsdio_port_api_declaration(...)` functions declared in - `include/dmsdio_port.h`, plus `dmod_init`/`dmod_deinit` and any - `DMOD_IRQ_HANDLER(...)` needed. -3. If the underlying peripheral IP is identical across families, factor the - shared logic into `src/port/_common/` and keep each - `src/port//port.c` a thin wrapper (lifecycle + IRQ only) - see - `dmfmc/src/port/stm32_common/` for a real example of this split. -4. Build by selecting the new family: - `cmake .. -DDMOD_CPU_FAMILY=`. -5. Do not introduce a module-specific variable (e.g. `_MCU_SERIES`) for - this - `DMOD_CPU_FAMILY` is the ecosystem-wide convention, already wired - into `dmf-get` package resolution. +# Implementing a dmsdio Port + +`dmsdio` is split into two DMOD modules: + +* **`dmsdio`** - everything SD specific: identification (CMD0, CMD8, + ACMD41, CMD2, CMD3, CMD9, CMD7), register decoding (OCR, CID, CSD, SCR, + SD Status, CMD6 switch status), SDSC byte vs. SDHC/SDXC block + addressing, 400 kHz / default speed / High Speed clocking, 1-bit/4-bit + negotiation, single/multi-block transfers, CMD12, CMD13 ready polling, + erase/discard, retry, recovery, hot-plug and the dmdrvi device model. +* **`dmsdio_port`** - protocol-free host controller primitives selected by + `DMOD_CPU_FAMILY`. + +A port must never decide which command to send, interpret a response or a +card register, or convert byte offsets to card addresses. + +## Port API + +Declared in [`include/dmsdio_port.h`](../include/dmsdio_port.h): + +| Function | Contract | +|----------|----------| +| `_host_init(instance)` | Acquire clocks, pins, DMA and IRQs and reset the controller. Any failed resource acquisition is returned as an error, never silently degraded | +| `_host_deinit(instance)` | Release everything acquired by `_host_init` | +| `_set_power(instance, on)` | Card power and clock output. After power-on the clock must run so the core's wait provides the 74 initialization clocks | +| `_set_clock(instance, max_hz, &actual_hz)` | Program the highest clock not above `max_hz` and report it. The core rejects an identification clock above 400 kHz | +| `_set_bus_width(instance, width)` | Host side data bus width (1 or 4) | +| `_execute(instance, &command, data, &response)` | Send one command, capture the response shape requested by `command.response`, and run the optional data phase. The data path must be armed before the command is sent. Completion and errors are interrupt driven and bounded by the command timeout and `data->timeout_ms` (per block) | +| `_abort(instance)` | Stop an interrupted data phase and return the controller to idle. Called after every failed `_execute`; must not send commands to the card | +| `_buffer_alloc(instance, size)` | Allocate a `DMSDIO_TRANSFER_ALIGNMENT` aligned buffer the data path always moves at full bus speed (the core's bounce buffer). `NULL` if the port has none - the core then uses its own heap | +| `_buffer_free(instance, buffer)` | Release a buffer from `_buffer_alloc` | +| `_buffer_is_direct(instance, buffer, length, direction)` | Whether the core may run a data phase straight from/to a caller's buffer. False for buffers the port cannot use or cannot sustain the bus rate with; the core bounces those. A policy, not a limit `_execute` enforces | + +### Responses + +| `dmsdio_response_type_t` | Capture | CRC | +|--------------------------|---------|-----| +| `dmsdio_response_none` | nothing | - | +| `dmsdio_response_short` | 32-bit payload in `words[0]`, command index in `index` | checked | +| `dmsdio_response_short_busy` | as short; the port may return before DAT0 is released | checked | +| `dmsdio_response_short_no_crc` | as short (R3) | not checked | +| `dmsdio_response_long` | bits 127:0 in `words[0..3]`, most significant word first | checked | + +Map controller outcomes to `dmsdio_status_t`: `cmd_timeout`, `cmd_crc`, +`data_timeout`, `data_crc`, `overrun` (FIFO/DMA fault), `invalid`, +`not_supported`. The core converts them to errno values and runs the retry +and recovery policy. + +### Data phase + +`dmsdio_data_t` describes `block_count` blocks of `block_size` bytes (512 +for card data, 8 for SCR, 64 for SD Status / switch status). Buffers passed +for 512-byte transfers are `DMSDIO_TRANSFER_ALIGNMENT` (32) byte aligned - one +Cortex-M7 cache line; +the core bounces every buffer `_buffer_is_direct()` rejects through the +buffer from `_buffer_alloc()`. + +## Adding a CPU family + +1. Create `src/port//config.cmake` setting `DMOD_TOOLS_NAME` (it must + match a directory under `dmod/configs/arch/...`). +2. Create `src/port//port.c` defining `DMOD_ENABLE_REGISTRATION`, + implementing every `dmod_dmsdio_port_api_declaration(...)` from + `include/dmsdio_port.h` plus `dmod_init`/`dmod_deinit` and the + `DMOD_IRQ_HANDLER(...)`s it needs. +3. Share only register and sequencing logic that is identical between + families in `src/port/_common/`. +4. Build with `cmake .. -DDMOD_CPU_FAMILY=`. Do not introduce a + module-specific MCU series variable. + +## Existing ports + +| Family | Status | +|--------|--------| +| `stm32f4` | SDIO (RM0090 section 31), one instance. Thin wrapper over `stm32_common` | +| `stm32f7` | SDMMC1, RM0385/RM0410 section 35. Thin wrapper over `stm32_common`. SDMMC2 (F76x/F77x only) is not registered: packages are per family and its IRQ 103 does not exist on F74x/F75x | +| `x86_64` | Simulated SD card for the automated tests (`dmsdio_mock.h`: card types SDSC v1/v2, SDHC, SDXC, fault injection, removal, counters). Never released - CI and the release workflow exclude it from the hardware matrix | + +### STM32 (`src/port/stm32_common`) + +STM32F4 SDIO and STM32F7 SDMMC are the same host IP: identical register +offsets and bit positions for everything the port uses. The shared code in +`stm32_common/` (`stm32_common.c`: lifecycle, clock, commands, IRQ; +`stm32_data.c`: data path) implements the whole port API; each family's +`port.c` only provides: + +* `stm32_sdio_instances[]` - base address, `RCC_APB2ENR`/`RCC_APB2RSTR` bit, NVIC IRQ and DMA controller/streams/channel per instance, +* `stm32_sdio_family_error_flags` - `STBITERR` on F4 (the bit is reserved on F7), +* `stm32_sdio_family_dma_reachable()` - memory DMA2 cannot access (F4 CCM), +* `stm32_sdio_family_has_dcache` - whether cache maintenance applies (F7), +* `dmod_init`/`dmod_deinit` and the `DMOD_IRQ_HANDLER`s. + +Implementation notes: + +* The kernel clock is read from `dmclk_port_get_domain_frequency(dmclk_domain_sdio)` + (48 MHz CLK48/PLL48CLK). `_host_init` fails with `-EIO` when it is not running. + `SDIO_CK = SDIOCLK / (CLKDIV + 2)`, or `SDIOCLK` itself with `BYPASS` when the + requested maximum is at least the source (High Speed: 48 MHz). +* Commands complete on `CMDREND`/`CMDSENT`/`CCRCFAIL`/`CTIMEOUT` interrupts. + `CCRCFAIL` is ignored for R3 (`dmsdio_response_short_no_crc`). +* Block data moves by DMA (`stm32_data.c`). `_host_init` leases DMA2 + stream 3 - or stream 6 when 3 is taken - on channel 4 from dmdma + (RM0090 table 43 / RM0385 table 27) and fails with `-EBUSY` when neither + is free; there is no silent PIO fallback. The stream runs in FIFO mode + with a full threshold, 4-beat word bursts on both sides and the SDIO as + flow controller (`dmdma_lease_start_ex()`, RM0090 31.3.2 / RM0385 35.3.2), + so interrupt latency can no longer overrun the SDIO FIFO. For reads the + DMA stream and the DPSM are armed before the command; for writes the DMA + stream is started before the command and the DPSM only after a valid + response. A transfer completes when both `DATAEND` and the DMA completion + have been seen. DMA buffers must start on and span whole 32-byte lines + (`DMSDIO_TRANSFER_ALIGNMENT`, the core bounces other buffers) and be + reachable by DMA2 (not F4 CCM RAM). +* Memory for writes: DMA2 cannot read FMC/FSMC external memory + (`0x60000000`-`0xDFFFFFFF`, e.g. the SDRAM dmod-boot puts module memory in + on STM32F746G-DISCO) fast enough to keep the FIFO fed at 48 MHz / 4-bit - + such writes end in `TXUNDERR`, since hardware flow control is off. + `_buffer_is_direct()` therefore rejects writes from there, and + `_buffer_alloc()` hands out memory from dmod-boot's `dma` heap (internal + SRAM - DTCM on STM32F7, reached through the AHBS port, never cached), or + the regular heap on targets without one. Reads into external memory keep + up and stay direct. +* D-cache (Cortex-M7): whenever `SCB->CCR.DC` is set, the DMA buffer is + cleaned before a write, cleaned+invalidated before a read and invalidated + again after it (by address, `DCCMVAC`/`DCCIMVAC`/`DCIMVAC`). With the cache + disabled, and always on F4 (`stm32_sdio_family_has_dcache = false`), this + is a no-op - enabling the cache later needs no driver change. +* Reads that fit the 32-word FIFO (SCR, SD Status, CMD6 switch status) do + not use DMA: the FIFO holds the whole transfer and is read at `DATAEND`. +* The DMA2 controller has to be configured through dmdma before dmsdio is + created (dmdma's board `dma.ini` with `driver_order=2`, dmsdio with 3). + Requires dmdma with `dmdma_lease_start_ex()` (stream options). +* Pins (AF12, pull-ups on CMD/D0-D3) are configured by dmgpio from the board + files in [`configs/`](../configs/README.md), not by the port. diff --git a/include/dmsdio.h b/include/dmsdio.h index 0535ea9..ea319f3 100644 --- a/include/dmsdio.h +++ b/include/dmsdio.h @@ -6,46 +6,51 @@ #include #include "dmod_types.h" #include "dmsdio_defs.h" +#include "dmsdio_types.h" /** - * Public API for the dmsdio module. + * @file dmsdio.h + * @brief Public API of the dmsdio SD memory card driver. * - * 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. + * The device itself is used through the dmdrvi 2.0 interface (a persistent + * host node /dev/dmsdioN plus a hot-plugged card node /dev/dmsdioN/0), see + * docs/api-reference.md. The functions below are pure register decoders, + * shared with the driver itself, that consumers can use to interpret the + * raw registers returned by dmsdio_ioctl_cmd_get_card_info. + */ + +/** + * @brief Decode a raw CSD register. * - * 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. + * @param raw Raw register, most significant word first + * @param csd Output * - * 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. + * @return 0 on success, -EINVAL on NULL arguments, -ENOTSUP for an + * unsupported CSD structure version (e.g. SDUC), -EPROTO for a CSD whose + * fields are inconsistent. */ - -/* Opaque handle - the real struct is defined in src/dmsdio.c */ -typedef struct dmsdio* dmsdio_t; +dmod_dmsdio_api(1.0, int, _decode_csd, ( const uint32_t raw[4], dmsdio_csd_t* csd )); /** - * Create a new dmsdio instance. + * @brief Decode a raw CID register. * - * @return A valid handle on success, or NULL on allocation failure. + * @return 0 on success, -EINVAL on NULL arguments. */ -dmod_dmsdio_api(1.0, dmsdio_t, _create, ( void )); +dmod_dmsdio_api(1.0, int, _decode_cid, ( const uint32_t raw[4], dmsdio_cid_t* cid )); /** - * Destroy an instance created by dmsdio_create(). Safe to call with - * NULL. + * @brief Decode the 8-byte SCR as received on the data lines. + * + * @return 0 on success, -EINVAL on NULL arguments, -EPROTO when the SCR + * structure version is unknown or no supported bus width is reported. */ -dmod_dmsdio_api(1.0, void, _destroy, ( dmsdio_t handle )); +dmod_dmsdio_api(1.0, int, _decode_scr, ( const uint8_t raw[8], dmsdio_scr_t* scr )); /** - * Example accessor - replace with your module's real API. + * @brief Decode the 64-byte SD Status as received on the data lines. * - * @return true if handle is a valid, non-NULL instance. + * @return 0 on success, -EINVAL on NULL arguments. */ -dmod_dmsdio_api(1.0, bool, _is_valid, ( dmsdio_t handle )); +dmod_dmsdio_api(1.0, int, _decode_ssr, ( const uint8_t raw[64], dmsdio_ssr_t* ssr )); #endif // DMSDIO_H diff --git a/include/dmsdio_port.h b/include/dmsdio_port.h index 3c8e9c4..57e8cc5 100644 --- a/include/dmsdio_port.h +++ b/include/dmsdio_port.h @@ -3,31 +3,117 @@ #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.: +/** + * @file dmsdio_port.h + * @brief Hardware port of the SD host controller. * - * dmod_dmsdio_port_api(1.0, int, _configure, ( int some_arg ) ); + * Every function here is a protocol-free hardware primitive. The port knows + * how to power the bus, program the clock divider and bus width, clock one + * command out, capture the requested response shape and run the attached + * data phase. It never decides which command to send, never interprets a + * response or card register, and never converts byte offsets to block + * addresses - all of that lives in the core dmsdio module. * - * 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 moves data with an internal IDMA engine, a dmdma lease or + * the FIFO is an implementation detail invisible from here. All calls are + * made from thread context and serialized per instance by the core module. + */ + +/* --- Lifecycle --- */ + +/** + * @brief Acquire and reset the controller (clocks, pins, DMA, IRQ). * - * dmod_dmsdio_port_api_declaration(1.0, int, _configure, ( int some_arg ) ) - * { - * ... - * } + * @return 0 on success or a negative errno value; a failure to acquire any + * required resource must be reported, never silently degraded. + */ +dmod_dmsdio_port_api(1.0, int, _host_init, ( dmsdio_instance_t instance )); + +/** @brief Release everything acquired by _host_init(). */ +dmod_dmsdio_port_api(1.0, int, _host_deinit, ( dmsdio_instance_t instance )); + +/* --- Bus configuration --- */ + +/** + * @brief Switch card power (and the bus clock output) on or off. * - * 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. + * After power on the port must keep the clock running so the core can + * provide the mandatory 74 initialization clocks by waiting. + */ +dmod_dmsdio_port_api(1.0, int, _set_power, ( dmsdio_instance_t instance, bool on )); + +/** + * @brief Program the highest bus clock not exceeding max_hz. + * + * @param actual_hz Receives the programmed frequency (may be NULL) + * + * @return 0 on success, negative errno when no divider can satisfy the + * request (e.g. the source clock is unknown). + */ +dmod_dmsdio_port_api(1.0, int, _set_clock, ( dmsdio_instance_t instance, uint32_t max_hz, uint32_t* actual_hz )); + +/** @brief Select the host side data bus width. */ +dmod_dmsdio_port_api(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width )); + +/* --- Transport --- */ + +/** + * @brief Execute one command and, if data != NULL, its data phase. + * + * The data path must be armed before the command is sent. The call returns + * once the response was captured and the data phase (if any) completed or + * failed; completion and errors must be interrupt driven and bounded by + * the command timeout and data->timeout_ms respectively. + * + * CRC checking applies to every response type except + * dmsdio_response_short_no_crc. For dmsdio_response_short_busy the port may + * return before DAT0 is released - the core polls card status itself. + * + * @param response Receives the captured response (may be NULL when + * command->response is dmsdio_response_none) + */ +dmod_dmsdio_port_api(1.0, dmsdio_status_t, _execute, + ( dmsdio_instance_t instance, const dmsdio_command_t* command, + const dmsdio_data_t* data, dmsdio_response_t* response )); + +/** + * @brief Abort an interrupted data phase and return the controller to idle. + * + * Called by the core after any failed _execute() before recovery commands + * are sent. Must not touch the card - the core issues STOP itself. + */ +dmod_dmsdio_port_api(1.0, void, _abort, ( dmsdio_instance_t instance )); + +/* --- Buffers --- */ + +/** + * @brief Allocate a buffer the port can always use for a data phase. + * + * DMSDIO_TRANSFER_ALIGNMENT aligned, in memory the data path moves at the + * full bus clock (e.g. internal SRAM a DMA controller reads fast enough to + * keep the host FIFO fed). The core bounces through it whatever + * _buffer_is_direct() rejects. + * + * @return NULL if no such memory is available (the core then falls back to + * its own heap). + */ +dmod_dmsdio_port_api(1.0, void*, _buffer_alloc, ( dmsdio_instance_t instance, size_t size )); + +/** @brief Release a buffer returned by _buffer_alloc(). */ +dmod_dmsdio_port_api(1.0, void, _buffer_free, ( dmsdio_instance_t instance, void* buffer )); + +/** + * @brief Whether a data phase can move @p length bytes directly from/to @p buffer. * - * See dmfmc/include/dmfmc_port.h and dmfmc/src/port/stm32_common/stm32_common.c - * for a fully worked example. + * False when the buffer breaks the port's alignment rules, lies where the + * data path cannot reach it, or cannot sustain the bus rate in that + * direction (e.g. writes from external SDRAM, which starve the host FIFO at + * 48 MHz). _execute() may still accept such a buffer - this is a policy the + * core follows to avoid slow or failing transfers, not a hard limit. */ +dmod_dmsdio_port_api(1.0, bool, _buffer_is_direct, + ( dmsdio_instance_t instance, const void* buffer, size_t length, dmsdio_direction_t direction )); #endif // DMSDIO_PORT_H diff --git a/include/dmsdio_types.h b/include/dmsdio_types.h new file mode 100644 index 0000000..cfd540d --- /dev/null +++ b/include/dmsdio_types.h @@ -0,0 +1,237 @@ +#ifndef DMSDIO_TYPES_H +#define DMSDIO_TYPES_H + +#include +#include +#include +#include "dmdrvi_ioctl.h" + +/** + * @file dmsdio_types.h + * @brief Types shared by dmsdio, dmsdio_port and dmsdio consumers. + * + * Everything that describes SD protocol behavior (command sequencing, + * register layouts, addressing) is interpreted exclusively by the core + * dmsdio module. The port-facing types below only describe *how* to clock a + * command and its data phase, never *which* command to send. + */ + +/** Logical block size used by every SD memory card supported by dmsdio. */ +#define DMSDIO_BLOCK_SIZE 512u + +/** + * Buffer alignment for direct (zero-copy) port data transfers: one 32-byte + * data cache line (Cortex-M7), so a port can clean/invalidate a DMA buffer + * without touching neighbouring data, and 4-word DMA bursts never cross a + * 1 KB boundary. Misaligned caller buffers are bounced through an aligned + * block. + */ +#define DMSDIO_TRANSFER_ALIGNMENT 32u + +/** SD host controller instance (1-based, e.g. 1 = SDMMC1/SDIO). */ +typedef uint8_t dmsdio_instance_t; + +/* ====================================================================== + * Port-facing types + * ====================================================================== */ + +/** Data bus width. */ +typedef enum +{ + dmsdio_bus_width_1bit = 1, /**< DAT0 only */ + dmsdio_bus_width_4bit = 4, /**< DAT0-DAT3 */ +} dmsdio_bus_width_t; + +/** + * @brief Shape of the response a port must capture for a command. + * + * The port never interprets response content. It only has to know how many + * bits to capture and whether the controller's CRC check applies. + */ +typedef enum +{ + dmsdio_response_none = 0, /**< No response expected */ + dmsdio_response_short, /**< 48-bit response, CRC checked */ + dmsdio_response_short_busy, /**< 48-bit response followed by busy on DAT0, CRC checked */ + dmsdio_response_short_no_crc, /**< 48-bit response whose CRC field is not valid */ + dmsdio_response_long, /**< 136-bit response, CRC checked */ +} dmsdio_response_type_t; + +/** Direction of a data phase. */ +typedef enum +{ + dmsdio_direction_read = 0, /**< Card to host */ + dmsdio_direction_write, /**< Host to card */ +} dmsdio_direction_t; + +/** + * @brief Result of a port-level operation. + * + * These are raw transport outcomes. The core module maps them to errno + * values and decides whether a retry makes sense. + */ +typedef enum +{ + dmsdio_status_ok = 0, /**< Command (and data phase) completed */ + dmsdio_status_cmd_timeout, /**< No response within the command timeout */ + dmsdio_status_cmd_crc, /**< Response CRC mismatch */ + dmsdio_status_data_timeout, /**< Data phase did not complete in time */ + dmsdio_status_data_crc, /**< Data block CRC mismatch */ + dmsdio_status_overrun, /**< FIFO overrun/underrun or DMA fault */ + dmsdio_status_invalid, /**< Invalid parameters passed to the port */ + dmsdio_status_not_supported, /**< Operation not supported by the controller */ +} dmsdio_status_t; + +/** A single command to clock out on the CMD line. */ +typedef struct +{ + uint8_t index; /**< Command index (0-63) */ + uint32_t argument; /**< 32-bit command argument */ + dmsdio_response_type_t response; /**< Expected response shape */ +} dmsdio_command_t; + +/** + * @brief Optional data phase attached to a command. + * + * The port arms the data path before issuing the command so that a read + * data block following the response is never missed. + */ +typedef struct +{ + void* buffer; /**< Source/destination, DMSDIO_TRANSFER_ALIGNMENT aligned */ + uint32_t block_size; /**< Bytes per block (1-2048) */ + uint32_t block_count; /**< Number of blocks (>= 1) */ + dmsdio_direction_t direction; /**< Transfer direction */ + uint32_t timeout_ms; /**< Upper bound for each block (read access or write busy) */ +} dmsdio_data_t; + +/** + * @brief Captured command response. + * + * Short responses store the 32-bit payload (bits 39:8) in words[0]. + * Long responses store bits 127:0 of the 128-bit register, most significant + * word first (words[0] = bits 127:96), with bit 0 undefined. + */ +typedef struct +{ + uint32_t words[4]; /**< Response payload */ + uint8_t index; /**< Command index field of a short response */ +} dmsdio_response_t; + +/* ====================================================================== + * Card description + * ====================================================================== */ + +/** Detected card family. */ +typedef enum +{ + dmsdio_card_type_none = 0, /**< No card attached */ + dmsdio_card_type_sdsc_v1, /**< SD 1.x standard capacity, byte addressed */ + dmsdio_card_type_sdsc_v2, /**< SD 2.0+ standard capacity, byte addressed */ + dmsdio_card_type_sdhc, /**< High capacity (<= 32 GiB), block addressed */ + dmsdio_card_type_sdxc, /**< Extended capacity (> 32 GiB), block addressed */ +} dmsdio_card_type_t; + +/** Decoded Card Identification register (CID). */ +typedef struct +{ + uint8_t manufacturer_id; /**< MID */ + char oem_id[3]; /**< OID, null terminated */ + char product_name[6]; /**< PNM, null terminated */ + uint8_t product_revision; /**< PRV (BCD n.m) */ + uint32_t serial_number; /**< PSN */ + uint16_t manufacture_year; /**< Full year (2000-2255) */ + uint8_t manufacture_month; /**< 1-12 */ +} dmsdio_cid_t; + +/** Decoded Card Specific Data register (CSD). */ +typedef struct +{ + uint8_t structure; /**< CSD_STRUCTURE (0 = v1, 1 = v2) */ + uint8_t tran_speed; /**< TRAN_SPEED raw code */ + uint16_t ccc; /**< Card command classes bitmap */ + uint8_t read_bl_len; /**< READ_BL_LEN (log2 of bytes) */ + uint64_t capacity_bytes; /**< User area capacity */ + bool erase_blk_en; /**< ERASE_BLK_EN */ + uint8_t sector_size; /**< SECTOR_SIZE + 1 (in write blocks) */ + bool perm_write_protect; /**< PERM_WRITE_PROTECT */ + bool tmp_write_protect; /**< TMP_WRITE_PROTECT */ +} dmsdio_csd_t; + +/** Decoded SD Configuration Register (SCR). */ +typedef struct +{ + uint8_t sd_spec; /**< SD_SPEC */ + bool sd_spec3; /**< SD_SPEC3 */ + bool data_stat_after_erase; /**< Erased data reads as 1s when true */ + bool bus_width_1bit; /**< 1-bit bus supported */ + bool bus_width_4bit; /**< 4-bit bus supported */ + bool cmd23_supported; /**< SET_BLOCK_COUNT supported */ +} dmsdio_scr_t; + +/** Decoded SD Status register (SSR) fields used by the driver. */ +typedef struct +{ + uint8_t bus_width_code; /**< DAT_BUS_WIDTH (0 = 1-bit, 2 = 4-bit) */ + uint8_t speed_class; /**< SPEED_CLASS */ + uint32_t au_size_bytes; /**< Allocation unit size, 0 = undefined */ + uint16_t erase_size; /**< ERASE_SIZE (AUs per erase timeout), 0 = unsupported */ + uint8_t erase_timeout_s; /**< ERASE_TIMEOUT in seconds */ + uint8_t erase_offset_s; /**< ERASE_OFFSET in seconds */ + bool discard_supported; /**< DISCARD_SUPPORT */ +} dmsdio_ssr_t; + +/** Snapshot of the attached card, see dmsdio_ioctl_cmd_get_card_info. */ +typedef struct +{ + dmsdio_card_type_t type; /**< Card family */ + uint32_t generation; /**< Card generation identifier */ + uint16_t rca; /**< Relative card address */ + uint32_t ocr; /**< Operating conditions register */ + uint32_t cid_raw[4]; /**< Raw CID, MSW first */ + uint32_t csd_raw[4]; /**< Raw CSD, MSW first */ + dmsdio_cid_t cid; /**< Decoded CID */ + dmsdio_csd_t csd; /**< Decoded CSD */ + dmsdio_scr_t scr; /**< Decoded SCR */ + dmsdio_ssr_t ssr; /**< Decoded SD Status */ + uint64_t capacity_bytes; /**< Addressable capacity */ + uint64_t block_count; /**< Capacity in DMSDIO_BLOCK_SIZE blocks */ + bool block_addressing; /**< SDHC/SDXC block addressing */ + bool high_speed; /**< High Speed mode active */ + bool write_protected; /**< CSD write protection active */ + dmsdio_bus_width_t bus_width; /**< Negotiated data bus width */ + uint32_t clock_hz; /**< Actual bus clock */ +} dmsdio_card_info_t; + +/** Host controller state, see dmsdio_ioctl_cmd_get_host_info. */ +typedef struct +{ + dmsdio_instance_t instance; /**< Controller instance */ + bool card_attached; /**< A card is identified and exposed */ + uint32_t generation; /**< Changes on every attach and detach */ + uint32_t scan_count; /**< Completed presence scans */ + int last_error; /**< Last transport error (negative errno) or 0 */ + uint32_t retry_count; /**< Transfers recovered by a retry */ +} dmsdio_host_info_t; + +/* ====================================================================== + * Driver-specific ioctl commands (start at DMDRVI_IOCTL_CUSTOM_BASE) + * ====================================================================== */ + +/** Base of the dmsdio private ioctl range: dmdrvi's DMDRVI_IOCTL_CUSTOM_BASE. */ +#define DMSDIO_IOCTL_BASE DMDRVI_IOCTL_CUSTOM_BASE + +/* + * Presence monitoring uses dmdrvi's class-independent monitor commands on + * the host node (DMDRVI_IOCTL_MONITOR_GET_POLICY/_EVENT/_REFRESH, see + * docs/api-reference.md) - there are no dmsdio-specific ones. + */ +typedef enum +{ + /** arg: dmsdio_host_info_t* (host and card nodes) */ + dmsdio_ioctl_cmd_get_host_info = DMSDIO_IOCTL_BASE, + /** arg: dmsdio_card_info_t* (host and card nodes, -ENODEV without a card) */ + dmsdio_ioctl_cmd_get_card_info, +} dmsdio_ioctl_cmd_t; + +#endif /* DMSDIO_TYPES_H */ diff --git a/src/dmsdio.c b/src/dmsdio.c index c005222..f5a05d9 100644 --- a/src/dmsdio.c +++ b/src/dmsdio.c @@ -1,84 +1,515 @@ #define DMOD_ENABLE_REGISTRATION ON -#include "dmod.h" -#include "dmsdio.h" +#include "dmsdio_internal.h" +#include "dmsdio_sd.h" +#include +#include -/* Example internal state - replace with your module's real fields. */ -struct dmsdio +/* + * dmdrvi 2.0 device model. + * + * /dev/dmsdioN persistent host node (major only). Status and monitor ioctls. + * /dev/dmsdioN/0 card node (major+minor 0), announced through + * dmdrvi_device_available() once a card is identified and + * withdrawn through dmdrvi_device_unavailable() when it + * goes away. Byte-addressed 64-bit block device. + * + * All bus operations of one host are serialized by ctx->lock. Handles to the + * card node carry the card generation they were opened for. + * + * The driver starts no threads of its own and implements the dmdrvi 2.1 + * monitor contract on the host node instead: GET_POLICY hands out the + * presence policy from its ini section, EVENT samples the card detect pin + * without the lock, REFRESH identifies, verifies or detaches the card and is + * the only place the card node is announced or withdrawn. dmdevfs reports + * the host node as a "monitor" device, and the monitor service it starts + * (dmdevmon) calls these - including one REFRESH right away, which picks up + * a card present at boot. + */ + +#define BOUNCE_ALIGNMENT 32u /* cache line: safe for DMA cache maintenance */ + +static bool is_valid_context(struct dmdrvi_context* ctx) { - bool valid; -}; + return ctx != NULL && ctx->magic == DMSDIO_CONTEXT_MAGIC; +} + +static bool is_valid_handle(const dmsdio_handle_t* handle) +{ + return handle != NULL && handle->magic == DMSDIO_HANDLE_MAGIC; +} -dmod_dmsdio_api_declaration(1.0, dmsdio_t, _create, ( void )) +/* Lock held: is the handle's card still the attached one? */ +static int check_card_handle(struct dmdrvi_context* ctx, const dmsdio_handle_t* handle) { - /* 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) + if (!handle->is_card) { - return NULL; + return -ENOTSUP; } + if (!dmsdio_card_attached(ctx)) + { + return -ENODEV; + } + return (handle->generation == ctx->card.generation) ? 0 : -ESTALE; +} - instance->valid = true; - return instance; +/* + * The bounce buffer comes from the port - memory its data path moves at the + * full bus clock (internal SRAM on STM32, see dmsdio_port_buffer_alloc()) - + * and only from the regular heap if the port has none. + */ +static uint8_t* allocate_bounce(struct dmdrvi_context* ctx) +{ + size_t size = (size_t)ctx->config.bounce_blocks * DMSDIO_BLOCK_SIZE; + ctx->bounce = dmsdio_port_buffer_alloc(ctx->config.instance, size); + ctx->bounce_from_port = (ctx->bounce != NULL); + if (ctx->bounce == NULL) + { + DMOD_LOG_WARN("dmsdio: no port buffer for %u blocks, bouncing through the heap\n", + (unsigned)ctx->config.bounce_blocks); + ctx->bounce = Dmod_AlignedMalloc(size, BOUNCE_ALIGNMENT); + } + return ctx->bounce; } -dmod_dmsdio_api_declaration(1.0, void, _destroy, ( dmsdio_t handle )) +static void free_bounce(struct dmdrvi_context* ctx) { - Dmod_Free(handle); + if (ctx->bounce_from_port) + { + dmsdio_port_buffer_free(ctx->config.instance, ctx->bounce); + } + else + { + Dmod_Free(ctx->bounce); + } + ctx->bounce = NULL; } -dmod_dmsdio_api_declaration(1.0, bool, _is_valid, ( dmsdio_t handle )) +static void destroy_context(struct dmdrvi_context* ctx, bool port_ready) { - return handle != NULL && handle->valid; + dmsdio_detect_release(ctx); + if (port_ready) + { + dmsdio_port_set_power(ctx->config.instance, false); + dmsdio_port_host_deinit(ctx->config.instance); + } + if (ctx->lock != NULL) + { + dmosi_mutex_destroy(ctx->lock); + } + if (ctx->cd_lock != NULL) + { + dmosi_mutex_destroy(ctx->cd_lock); + } + free_bounce(ctx); + ctx->magic = 0; + Dmod_Free(ctx); } -/** - * @brief Pre-initialization function for the module. - * - * @note This function is optional. You can remove it if you don't need it. - * - * This function is called when the module enabling is in progress. - * - * 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. - */ -void dmod_preinit(void) +static int allocate_resources(struct dmdrvi_context* ctx) { - if(Dmod_IsFunctionConnected( Dmod_Printf )) + allocate_bounce(ctx); + ctx->lock = dmosi_mutex_create(false); + ctx->cd_lock = dmosi_mutex_create(false); + if (ctx->bounce == NULL || ctx->lock == NULL || ctx->cd_lock == NULL) { - Dmod_Printf("API is connected!\n"); + DMOD_LOG_ERROR("dmsdio: out of memory\n"); + return -ENOMEM; } + return 0; } -/** - * @brief Initialization function for the module. - * - * 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 - */ +/* ---- DMOD lifecycle ---- */ + int dmod_init(const Dmod_Config_t *Config) { - Dmod_Printf("Hello, World!\n"); + (void)Config; return 0; } -/** - * @brief De-initialization function for the module. - * - * 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 - */ int dmod_deinit(void) { - Dmod_Printf("Goodbye, World!\n"); return 0; } + +/* ---- dmdrvi: context ---- */ + +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) + { + return NULL; + } + struct dmdrvi_context* ctx = Dmod_Malloc(sizeof(*ctx)); + if (ctx == NULL) + { + return NULL; + } + memset(ctx, 0, sizeof(*ctx)); + ctx->magic = DMSDIO_CONTEXT_MAGIC; + + if (dmsdio_config_read(config, &ctx->config) != 0 || allocate_resources(ctx) != 0) + { + destroy_context(ctx, false); + return NULL; + } + int ret = dmsdio_port_host_init(ctx->config.instance); + if (ret != 0) + { + DMOD_LOG_ERROR("dmsdio: host %u initialization failed (%d)\n", (unsigned)ctx->config.instance, ret); + destroy_context(ctx, false); + return NULL; + } + + memset(dev_num, 0, sizeof(*dev_num)); + dev_num->flags = DMDRVI_NUM_MAJOR; + dev_num->major = ctx->config.major; + return ctx; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _free, ( dmdrvi_context_t context )) +{ + if (!is_valid_context(context)) + { + return; + } + /* dmdevfs is tearing this context down itself - no unavailable notice. */ + destroy_context(context, true); +} + +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; + } + bool ready = (info->state == dmdrvi_dev_state_ready && info->node_path != NULL); + dmsdio_detect_set_cd_path(context, ready ? info->node_path : NULL); +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _path_ready, + ( dmdrvi_context_t context, const dmdrvi_dev_num_t* dev_num, const char* path )) +{ + if (!is_valid_context(context) || dev_num == NULL || (dev_num->flags & DMDRVI_NUM_MINOR) != 0) + { + return; /* only the host node's registration matters */ + } + (void)path; + /* From now on dmdevfs accepts the card node - the next REFRESH announces it. */ + dmsdio_lock(context); + context->host_ready = true; + dmsdio_unlock(context); +} + +/* ---- dmdrvi: handles ---- */ + +static bool is_card_dev_num(const dmdrvi_dev_num_t* dev_num) +{ + return (dev_num->flags & DMDRVI_NUM_MINOR) != 0; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void*, _open, + ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t* dev_num )) +{ + if (!is_valid_context(context) || dev_num == NULL || + (is_card_dev_num(dev_num) && dev_num->minor != DMSDIO_CARD_MINOR)) + { + return NULL; + } + dmsdio_handle_t* handle = Dmod_Malloc(sizeof(*handle)); + if (handle == NULL) + { + return NULL; + } + handle->magic = DMSDIO_HANDLE_MAGIC; + handle->is_card = is_card_dev_num(dev_num); + handle->flags = flags; + + dmsdio_lock(context); + bool usable = !handle->is_card || dmsdio_card_attached(context); + handle->generation = context->card.generation; + dmsdio_unlock(context); + + if (!usable) + { + handle->magic = 0; + Dmod_Free(handle); + return NULL; + } + return handle; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, void, _close, ( dmdrvi_context_t context, void* handle )) +{ + dmsdio_handle_t* h = (dmsdio_handle_t*)handle; + if (!is_valid_context(context) || !is_valid_handle(h)) + { + return; + } + h->magic = 0; + Dmod_Free(h); +} + +/* ---- dmdrvi: data path ---- */ + +static int check_io(struct dmdrvi_context* ctx, const dmsdio_handle_t* h, const void* buffer, + size_t size, dmdrvi_offset_t offset, int forbidden_flag) +{ + if (offset < 0) + { + return -EINVAL; + } +#if SIZE_MAX > INT64_MAX + if ((uint64_t)size > (uint64_t)INT64_MAX) + { + return -EOVERFLOW; + } +#endif + if (!is_valid_context(ctx) || !is_valid_handle(h) || (buffer == NULL && size != 0)) + { + return -EINVAL; + } + if (!h->is_card) + { + return -ENOTSUP; + } + return (h->flags == forbidden_flag) ? -EBADF : 0; +} + +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 )) +{ + dmsdio_handle_t* h = (dmsdio_handle_t*)handle; + int ret = check_io(context, h, buffer, size, offset, DMDRVI_O_WRONLY); + if (ret != 0 || size == 0) + { + return ret; + } + dmsdio_lock(context); + dmdrvi_ssize_t result = check_card_handle(context, h); + if (result == 0) + { + result = dmsdio_io_read(context, (uint8_t*)buffer, size, (uint64_t)offset); + } + dmsdio_unlock(context); + return 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 )) +{ + dmsdio_handle_t* h = (dmsdio_handle_t*)handle; + int ret = check_io(context, h, buffer, size, offset, DMDRVI_O_RDONLY); + if (ret != 0 || size == 0) + { + return ret; + } + dmsdio_lock(context); + dmdrvi_ssize_t result = check_card_handle(context, h); + if (result == 0) + { + result = dmsdio_io_write(context, (const uint8_t*)buffer, size, (uint64_t)offset); + } + dmsdio_unlock(context); + return result; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _flush, ( dmdrvi_context_t context, void* handle )) +{ + dmsdio_handle_t* h = (dmsdio_handle_t*)handle; + if (!is_valid_context(context) || !is_valid_handle(h)) + { + return -EINVAL; + } + if (!h->is_card) + { + return 0; + } + dmsdio_lock(context); + int ret = check_card_handle(context, h); + if (ret == 0) + { + ret = dmsdio_cmd_wait_ready(context, context->config.write_timeout_ms); + } + dmsdio_unlock(context); + return ret; +} + +/* ---- dmdrvi: control ---- */ + +static void fill_host_info(struct dmdrvi_context* ctx, dmsdio_host_info_t* info) +{ + info->instance = ctx->config.instance; + info->card_attached = dmsdio_card_attached(ctx); + info->generation = ctx->generation; + info->scan_count = ctx->scan_count; + info->last_error = ctx->last_error; + info->retry_count = ctx->retry_count; +} + +static void fill_block_info(struct dmdrvi_context* ctx, dmdrvi_block_info_t* info) +{ + const dmsdio_card_info_t* card = &ctx->card; + bool erase = (card->csd.ccc & SD_CCC_ERASE) != 0; + info->logical_block_size = DMSDIO_BLOCK_SIZE; + info->erase_block_size = erase ? DMSDIO_BLOCK_SIZE : 0; + info->block_count = card->block_count; + info->flags = DMDRVI_BLOCK_FLAG_REMOVABLE + | (erase ? DMDRVI_BLOCK_FLAG_ERASE_SUPPORTED : 0u) + | (card->ssr.discard_supported ? DMDRVI_BLOCK_FLAG_DISCARD_SUPPORTED : 0u) + | (card->write_protected ? DMDRVI_BLOCK_FLAG_READ_ONLY : 0u); +} + +/* Lock held. Controls available on the host node (and the card node). */ +static int host_ioctl(struct dmdrvi_context* ctx, const dmsdio_handle_t* h, int command, void* arg) +{ + switch (command) + { + case dmsdio_ioctl_cmd_get_host_info: + fill_host_info(ctx, (dmsdio_host_info_t*)arg); + return 0; + case dmsdio_ioctl_cmd_get_card_info: + { + int ret = h->is_card ? check_card_handle(ctx, h) + : (dmsdio_card_attached(ctx) ? 0 : -ENODEV); + if (ret == 0) + { + *(dmsdio_card_info_t*)arg = ctx->card; + } + return ret; + } + default: + return -ENOTTY; + } +} + +/* Lock held. Block device controls of the card node. */ +static int card_ioctl(struct dmdrvi_context* ctx, const dmsdio_handle_t* h, int command, void* arg) +{ + int ret = check_card_handle(ctx, h); + if (ret != 0) + { + return ret; + } + dmdrvi_block_info_t info; + fill_block_info(ctx, &info); + switch (command) + { + case DMDRVI_IOCTL_BLOCK_GET_INFO: + *(dmdrvi_block_info_t*)arg = info; + return 0; + case DMDRVI_IOCTL_BLOCK_ERASE: + case DMDRVI_IOCTL_BLOCK_DISCARD: + { + bool discard = (command == DMDRVI_IOCTL_BLOCK_DISCARD); + uint32_t needed = discard ? DMDRVI_BLOCK_FLAG_DISCARD_SUPPORTED : DMDRVI_BLOCK_FLAG_ERASE_SUPPORTED; + if ((info.flags & needed) == 0) + { + return -ENOTSUP; + } + return (h->flags == DMDRVI_O_RDONLY) ? -EBADF + : dmsdio_io_erase_range(ctx, (const dmdrvi_block_range_t*)arg, discard); + } + default: + return -ENOTTY; + } +} + +static bool is_block_ioctl(int command) +{ + return command == DMDRVI_IOCTL_BLOCK_GET_INFO || command == DMDRVI_IOCTL_BLOCK_ERASE || + command == DMDRVI_IOCTL_BLOCK_DISCARD; +} + +/* + * dmdrvi monitor contract - host node only, so dmdevfs reports just the host + * as a monitored node; on the card node these are unknown commands. + */ +static int monitor_ioctl(struct dmdrvi_context* ctx, int command, void* arg) +{ + switch (command) + { + case DMDRVI_IOCTL_MONITOR_GET_POLICY: + if (arg == NULL) + { + return -EINVAL; + } + *(dmdrvi_monitor_policy_t*)arg = ctx->config.monitor; /* constant after create */ + return 0; + case DMDRVI_IOCTL_MONITOR_EVENT: + /* Deliberately lock-free, see dmsdio_detect_event(). */ + return dmsdio_detect_event(ctx); + case DMDRVI_IOCTL_MONITOR_REFRESH: + { + dmsdio_lock(ctx); + int ret = dmsdio_card_scan(ctx); + dmsdio_unlock(ctx); + return ret; + } + default: + return -ENOTTY; + } +} + +static bool is_monitor_ioctl(int command) +{ + return command == DMDRVI_IOCTL_MONITOR_GET_POLICY || command == DMDRVI_IOCTL_MONITOR_EVENT || + command == DMDRVI_IOCTL_MONITOR_REFRESH; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _ioctl, + ( dmdrvi_context_t context, void* handle, int command, void* arg )) +{ + dmsdio_handle_t* h = (dmsdio_handle_t*)handle; + if (!is_valid_context(context) || !is_valid_handle(h)) + { + return -EINVAL; + } + if (is_monitor_ioctl(command)) + { + return h->is_card ? -ENOTTY : monitor_ioctl(context, command, arg); + } + if (arg == NULL) + { + return -EINVAL; + } + dmsdio_lock(context); + int ret = is_block_ioctl(command) + ? (h->is_card ? card_ioctl(context, h, command, arg) : -ENOTTY) + : host_ioctl(context, h, command, arg); + dmsdio_unlock(context); + return ret; +} + +/* "/dev/dmsdio0/0" (or "/dmsdio0/0" relative to the mount) is the card node. */ +static bool is_card_path(const char* path) +{ + const char* last = strrchr(path, '/'); + return last != NULL && last != path && strcmp(last + 1, "0") == 0; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmsdio, int, _stat, + ( dmdrvi_context_t context, const char* path, dmdrvi_stat_t* stat )) +{ + if (!is_valid_context(context) || path == NULL || stat == NULL) + { + return -EINVAL; + } + stat->size = 0; + stat->mode = 0666; + if (!is_card_path(path)) + { + return 0; + } + dmsdio_lock(context); + int ret = dmsdio_card_attached(context) ? 0 : -ENODEV; + if (ret == 0) + { + stat->size = context->card.capacity_bytes; + stat->mode = context->card.write_protected ? 0444 : 0666; + } + dmsdio_unlock(context); + return ret; +} diff --git a/src/dmsdio_card.c b/src/dmsdio_card.c new file mode 100644 index 0000000..204ca81 --- /dev/null +++ b/src/dmsdio_card.c @@ -0,0 +1,182 @@ +#include "dmsdio_internal.h" +#include + +/* + * Card attach/detach bookkeeping. Every function here runs with the + * context lock held. + * + * Each attach and each detach bumps ctx->generation. Handles remember the + * generation they were opened for, so a handle opened for a card that has + * since been removed (or replaced by another card) is rejected with -ESTALE + * instead of silently touching the wrong medium. + * + * Attach and detach only change the driver's own state. The card node is + * announced and withdrawn in one place - at the end of dmsdio_card_scan(), + * i.e. DMDRVI_IOCTL_MONITOR_REFRESH - as the dmdrvi monitor contract + * requires. A transfer that finds the card gone detaches it at once (the + * handles go stale, I/O fails with -ENODEV), and the next REFRESH withdraws + * the node. + */ + +static const char* type_name(dmsdio_card_type_t type) +{ + switch (type) + { + case dmsdio_card_type_sdsc_v1: return "SDSC v1"; + case dmsdio_card_type_sdsc_v2: return "SDSC v2"; + case dmsdio_card_type_sdhc: return "SDHC"; + case dmsdio_card_type_sdxc: return "SDXC"; + default: return "none"; + } +} + +static void notify(struct dmdrvi_context* ctx, bool available) +{ + dmdrvi_dev_num_t num; + memset(&num, 0, sizeof(num)); + num.flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR; + num.major = ctx->config.major; + num.minor = DMSDIO_CARD_MINOR; + + if (available) + { + dmdrvi_device_available(ctx, &num); + } + else + { + dmdrvi_device_unavailable(ctx, &num); + } + ctx->card_announced = available; +} + +/* + * Make the announced card node match the attached card: withdraw a node + * announced for a card that is gone (or was swapped for another one), then + * announce the attached card. dmdevfs drops hot-plug notices for a context + * it has not registered yet, so nothing is announced before + * dmdrvi_path_ready() reported the host node. + */ +static void sync_announcement(struct dmdrvi_context* ctx) +{ + bool attached = dmsdio_card_attached(ctx); + if (ctx->card_announced && (!attached || ctx->announced_generation != ctx->card.generation)) + { + notify(ctx, false); + } + if (attached && !ctx->card_announced && ctx->host_ready) + { + ctx->announced_generation = ctx->card.generation; + notify(ctx, true); + } +} + +bool dmsdio_card_attached(const struct dmdrvi_context* ctx) +{ + return ctx->card.type != dmsdio_card_type_none; +} + +static bool retry_identification(int error) +{ + return error == -ETIMEDOUT || error == -EBADMSG || error == -EIO; +} + +static int attach(struct dmdrvi_context* ctx) +{ + dmsdio_card_info_t card; + int previous_error = ctx->last_error; + int ret = dmsdio_ident_run(ctx, &card); + for (uint32_t i = 0; i < ctx->config.retries && retry_identification(ret); i++) + { + ret = dmsdio_ident_run(ctx, &card); + } + if (ret != 0) + { + dmsdio_port_set_power(ctx->config.instance, false); + /* An empty slot is not an error - keep the last real one. */ + ctx->last_error = (ret == -ENODEV) ? previous_error : ret; + if (ret != -ENODEV) + { + DMOD_LOG_ERROR("dmsdio%u: card identification failed (%d)\n", + (unsigned)ctx->config.major, ret); + } + return ret; + } + ctx->generation++; + card.generation = ctx->generation; + ctx->card = card; + ctx->last_error = 0; + DMOD_LOG_INFO("dmsdio%u: %s card, %lu blocks, %u-bit, %u Hz%s\n", + (unsigned)ctx->config.major, type_name(card.type), + (unsigned long)card.block_count, (unsigned)card.bus_width, + (unsigned)card.clock_hz, card.high_speed ? " (High Speed)" : ""); + return 0; +} + +void dmsdio_card_detach(struct dmdrvi_context* ctx) +{ + if (!dmsdio_card_attached(ctx)) + { + return; + } + memset(&ctx->card, 0, sizeof(ctx->card)); + ctx->generation++; + dmsdio_port_set_power(ctx->config.instance, false); + DMOD_LOG_INFO("dmsdio%u: card removed\n", (unsigned)ctx->config.major); +} + +int dmsdio_card_lost(struct dmdrvi_context* ctx, int error) +{ + dmsdio_card_detach(ctx); + ctx->last_error = error; + return error; +} + +/* A card that still answers CMD13 with our RCA is the same card. */ +static int verify(struct dmdrvi_context* ctx) +{ + uint32_t status = 0; + int ret = dmsdio_cmd_status(ctx, &status); + if (ret != 0) + { + ret = dmsdio_cmd_status(ctx, &status); + } + if (ret != 0) + { + dmsdio_card_lost(ctx, -ENODEV); + return -ENODEV; + } + return 0; +} + +/* Settle the card state: detach, verify (CMD13) or identify. */ +static int settle(struct dmdrvi_context* ctx) +{ + bool present = true; + int cd = dmsdio_detect_read_cd(ctx, &present); + if (cd == 0 && !present) + { + dmsdio_card_detach(ctx); + return -ENODEV; + } + + bool was_attached = dmsdio_card_attached(ctx); + int ret = was_attached ? verify(ctx) : attach(ctx); + if (ret == -ENODEV && was_attached && cd == 0) + { + /* Card detect says present but the old card vanished: a new one? */ + ret = attach(ctx); + } + return ret; +} + +/* DMDRVI_IOCTL_MONITOR_REFRESH - the only place the card node is announced or withdrawn. */ +int dmsdio_card_scan(struct dmdrvi_context* ctx) +{ + /* Card detect is sampled again in settle() - this scan settles any + * removal flagged through DMDRVI_IOCTL_MONITOR_EVENT. */ + ctx->removal_pending = false; + int ret = settle(ctx); + sync_announcement(ctx); + ctx->scan_count++; + return ret; +} diff --git a/src/dmsdio_cmd.c b/src/dmsdio_cmd.c new file mode 100644 index 0000000..833a17f --- /dev/null +++ b/src/dmsdio_cmd.c @@ -0,0 +1,178 @@ +#include "dmsdio_internal.h" +#include "dmsdio_sd.h" + +/* + * Command layer: one SD command at a time, mapped to errno values. + * + * Every transport failure is followed by dmsdio_port_abort() so the + * controller is idle before the caller decides how to recover. + */ + +int dmsdio_cmd_errno(dmsdio_status_t status) +{ + switch (status) + { + case dmsdio_status_ok: return 0; + case dmsdio_status_cmd_timeout: return -ETIMEDOUT; + case dmsdio_status_data_timeout: return -ETIMEDOUT; + case dmsdio_status_cmd_crc: return -EBADMSG; + case dmsdio_status_data_crc: return -EBADMSG; + case dmsdio_status_invalid: return -EINVAL; + case dmsdio_status_not_supported: return -ENOTSUP; + case dmsdio_status_overrun: + default: return -EIO; + } +} + +/* Map error bits of an R1 card status to the most specific errno. */ +static int r1_errno(uint32_t status) +{ + if ((status & SD_R1_ERRORS) == 0) + { + return 0; + } + if (status & (SD_R1_OUT_OF_RANGE | SD_R1_ADDRESS_ERROR | SD_R1_BLOCK_LEN_ERROR | + SD_R1_ERASE_SEQ_ERROR | SD_R1_ERASE_PARAM)) + { + return -EINVAL; + } + if (status & (SD_R1_WP_VIOLATION | SD_R1_WP_ERASE_SKIP)) + { + return -EROFS; + } + if (status & SD_R1_COM_CRC_ERROR) + { + return -EBADMSG; + } + if (status & SD_R1_ILLEGAL_COMMAND) + { + return -EPROTO; + } + if (status & SD_R1_LOCK_UNLOCK_FAILED) + { + return -EACCES; + } + return -EIO; +} + +static bool has_index_field(dmsdio_response_type_t type) +{ + return type == dmsdio_response_short || type == dmsdio_response_short_busy; +} + +static int execute(struct dmdrvi_context* ctx, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + dmsdio_status_t status = dmsdio_port_execute(ctx->config.instance, cmd, data, resp); + int ret = dmsdio_cmd_errno(status); + if (ret == 0 && has_index_field(cmd->response) && resp->index != cmd->index) + { + DMOD_LOG_ERROR("dmsdio: CMD%u answered with index %u\n", cmd->index, resp->index); + ret = -EPROTO; + } + if (ret != 0) + { + dmsdio_port_abort(ctx->config.instance); + ctx->last_error = ret; + } + return ret; +} + +int dmsdio_cmd_send(struct dmdrvi_context* ctx, uint8_t index, uint32_t arg, + dmsdio_response_type_t type, dmsdio_response_t* resp) +{ + dmsdio_response_t local = { { 0 }, 0 }; + dmsdio_command_t cmd = { index, arg, type }; + return execute(ctx, &cmd, NULL, resp != NULL ? resp : &local); +} + +static int app_prefix(struct dmdrvi_context* ctx) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_send(ctx, SD_CMD_APP_CMD, (uint32_t)ctx->card.rca << 16, + dmsdio_response_short, &resp); + if (ret == 0 && (resp.words[0] & SD_R1_APP_CMD) == 0) + { + DMOD_LOG_ERROR("dmsdio: card did not accept APP_CMD\n"); + ret = -EPROTO; + } + return ret; +} + +int dmsdio_cmd_app(struct dmdrvi_context* ctx, uint8_t index, uint32_t arg, + dmsdio_response_type_t type, dmsdio_response_t* resp) +{ + int ret = app_prefix(ctx); + return ret != 0 ? ret : dmsdio_cmd_send(ctx, index, arg, type, resp); +} + +int dmsdio_cmd_data(struct dmdrvi_context* ctx, bool app, uint8_t index, uint32_t arg, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + dmsdio_response_t local = { { 0 }, 0 }; + dmsdio_command_t cmd = { index, arg, dmsdio_response_short }; + resp = (resp != NULL) ? resp : &local; + + int ret = app ? app_prefix(ctx) : 0; + if (ret == 0) + { + ret = execute(ctx, &cmd, data, resp); + } + return ret != 0 ? ret : r1_errno(resp->words[0]); +} + +int dmsdio_cmd_status(struct dmdrvi_context* ctx, uint32_t* status) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_send(ctx, SD_CMD_SEND_STATUS, (uint32_t)ctx->card.rca << 16, + dmsdio_response_short, &resp); + if (ret == 0) + { + *status = resp.words[0]; + } + return ret; +} + +int dmsdio_cmd_stop(struct dmdrvi_context* ctx) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_send(ctx, SD_CMD_STOP_TRANSMISSION, 0, dmsdio_response_short_busy, &resp); + /* OUT_OF_RANGE is legal here when a multi-block read ended at the last block. */ + return ret != 0 ? ret : r1_errno(resp.words[0] & ~SD_R1_OUT_OF_RANGE); +} + +static bool deadline_passed(Dmod_Timestamp_t start, uint32_t waited_ms, uint32_t timeout_ms) +{ + Dmod_Timestamp_t now = Dmod_GetUptime(); + return waited_ms >= timeout_ms || (now > start && now - start >= timeout_ms); +} + +int dmsdio_cmd_wait_ready(struct dmdrvi_context* ctx, uint32_t timeout_ms) +{ + Dmod_Timestamp_t start = Dmod_GetUptime(); + uint32_t waited_ms = 0; + for (;;) + { + uint32_t status = 0; + int ret = dmsdio_cmd_status(ctx, &status); + if (ret == 0) + { + ret = r1_errno(status); + } + if (ret != 0) + { + return ret; + } + if ((status & SD_R1_READY_FOR_DATA) && SD_R1_STATE(status) == SD_STATE_TRAN) + { + return 0; + } + if (deadline_passed(start, waited_ms, timeout_ms)) + { + ctx->last_error = -ETIMEDOUT; + return -ETIMEDOUT; + } + dmosi_thread_sleep(1); + waited_ms++; + } +} diff --git a/src/dmsdio_config.c b/src/dmsdio_config.c new file mode 100644 index 0000000..0b90623 --- /dev/null +++ b/src/dmsdio_config.c @@ -0,0 +1,164 @@ +#include "dmsdio_internal.h" +#include + +#define DEFAULT_SECTION "dmsdio" +#define DEFAULT_MAX_CLOCK_HZ 50000000 +#define DEFAULT_RETRIES 3 +#define DEFAULT_INIT_TIMEOUT_MS 1000 +#define DEFAULT_READ_TIMEOUT_MS 250 +#define DEFAULT_WRITE_TIMEOUT_MS 500 +#define DEFAULT_ERASE_TIMEOUT_MS 3000 +#define DEFAULT_MAX_BLOCKS 128 +#define DEFAULT_BOUNCE_BLOCKS 8 +#define DEFAULT_SETTLE_MS 50 +#define DEFAULT_POLL_INTERVAL_MS 1000 +#define MAX_RETRIES 16 + +/* + * Where the settings live: + * 1. the section dmdevfs made active for this device (multi-device board + * files) - addressed as the NULL section, + * 2. a [dmsdio] section, + * 3. the first section with an `instance` key (device-named section). + */ +static const char* find_section(dmini_context_t ini) +{ + if (dmini_has_key(ini, NULL, "instance")) + { + return NULL; + } + if (dmini_has_section(ini, DEFAULT_SECTION)) + { + return DEFAULT_SECTION; + } + int count = dmini_section_count(ini); + for (int i = 0; i < count; i++) + { + const char* name = dmini_section_name(ini, i); + if (name != NULL && dmini_has_key(ini, name, "instance")) + { + return name; + } + } + return DEFAULT_SECTION; +} + +static const char* label(const char* section) +{ + return (section != NULL) ? section : "active section"; +} + +static bool read_bool(dmini_context_t ini, const char* section, const char* key, bool def) +{ + const char* value = dmini_get_string(ini, section, key, NULL); + if (value == NULL) + { + return def; + } + return strcmp(value, "true") == 0 || strcmp(value, "1") == 0 || + strcmp(value, "yes") == 0 || strcmp(value, "on") == 0; +} + +static uint32_t read_u32(dmini_context_t ini, const char* section, const char* key, uint32_t def) +{ + int value = dmini_get_int(ini, section, key, (int)def); + return value < 0 ? def : (uint32_t)value; +} + +static int read_timeouts(dmini_context_t ini, const char* section, dmsdio_config_t* c) +{ + c->retries = read_u32(ini, section, "retries", DEFAULT_RETRIES); + c->init_timeout_ms = read_u32(ini, section, "init_timeout_ms", DEFAULT_INIT_TIMEOUT_MS); + c->read_timeout_ms = read_u32(ini, section, "read_timeout_ms", DEFAULT_READ_TIMEOUT_MS); + c->write_timeout_ms = read_u32(ini, section, "write_timeout_ms", DEFAULT_WRITE_TIMEOUT_MS); + c->erase_timeout_ms = read_u32(ini, section, "erase_timeout_ms", DEFAULT_ERASE_TIMEOUT_MS); + + if (c->retries > MAX_RETRIES || c->init_timeout_ms == 0 || c->read_timeout_ms == 0 || + c->write_timeout_ms == 0 || c->erase_timeout_ms == 0) + { + DMOD_LOG_ERROR("dmsdio: invalid retry/timeout configuration in [%s]\n", label(section)); + return -EINVAL; + } + return 0; +} + +static int read_bus(dmini_context_t ini, const char* section, dmsdio_config_t* c) +{ + int instance = dmini_get_int(ini, section, "instance", 1); + int width = dmini_get_int(ini, section, "bus_width", 4); + + c->max_clock_hz = read_u32(ini, section, "max_clock_hz", DEFAULT_MAX_CLOCK_HZ); + c->high_speed = read_bool(ini, section, "high_speed", true); + c->max_blocks_per_transfer = read_u32(ini, section, "max_blocks_per_transfer", DEFAULT_MAX_BLOCKS); + c->bounce_blocks = read_u32(ini, section, "bounce_blocks", DEFAULT_BOUNCE_BLOCKS); + + if (instance < 1 || instance > 255 || (width != 1 && width != 4) || + c->max_clock_hz < 400000u || c->max_blocks_per_transfer == 0 || c->bounce_blocks == 0) + { + DMOD_LOG_ERROR("dmsdio: invalid instance/bus configuration in [%s]\n", label(section)); + return -EINVAL; + } + c->instance = (dmsdio_instance_t)instance; + c->max_bus_width = (width == 4) ? dmsdio_bus_width_4bit : dmsdio_bus_width_1bit; + + int major = dmini_get_int(ini, section, "major", instance - 1); + if (major < 0 || major > 255) + { + DMOD_LOG_ERROR("dmsdio: invalid major number in [%s]\n", label(section)); + return -EINVAL; + } + c->major = (uint8_t)major; + return 0; +} + +static int read_card_detect(dmini_context_t ini, const char* section, dmsdio_config_t* c) +{ + const char* level = dmini_get_string(ini, section, "card_detect_active_level", "low"); + if (strcmp(level, "low") != 0 && strcmp(level, "high") != 0) + { + DMOD_LOG_ERROR("dmsdio: card_detect_active_level must be low or high\n"); + return -EINVAL; + } + c->cd_active_high = strcmp(level, "high") == 0; + return 0; +} + +/* The dmdrvi monitor policy, under the key names dmdrvi recommends. */ +static int read_monitor(dmini_context_t ini, const char* section, dmsdio_config_t* c) +{ + const char* handler = dmini_get_string(ini, section, "monitor_event_handler", NULL); + dmdrvi_monitor_policy_t* policy = &c->monitor; + + policy->settle_ms = read_u32(ini, section, "monitor_settle_ms", DEFAULT_SETTLE_MS); + policy->poll_interval_ms = read_u32(ini, section, "poll_interval_ms", DEFAULT_POLL_INTERVAL_MS); + int length = Dmod_SnPrintf(policy->event_handler, sizeof(policy->event_handler), + "%s", (handler != NULL) ? handler : ""); + if (length < 0 || (size_t)length >= sizeof(policy->event_handler)) + { + DMOD_LOG_ERROR("dmsdio: monitor_event_handler longer than %u characters\n", + (unsigned)(DMDRVI_MONITOR_HANDLER_NAME_MAX - 1u)); + return -EINVAL; + } + return 0; +} + +int dmsdio_config_read(dmini_context_t ini, dmsdio_config_t* config) +{ + memset(config, 0, sizeof(*config)); + const char* section = find_section(ini); + + int ret = read_bus(ini, section, config); + if (ret == 0) + { + ret = read_timeouts(ini, section, config); + } + if (ret == 0) + { + ret = read_card_detect(ini, section, config); + } + if (ret == 0) + { + ret = read_monitor(ini, section, config); + } + return ret; +} diff --git a/src/dmsdio_decode.c b/src/dmsdio_decode.c new file mode 100644 index 0000000..114f4e6 --- /dev/null +++ b/src/dmsdio_decode.c @@ -0,0 +1,163 @@ +#include "dmsdio_internal.h" +#include + +/* + * Register decoders. Bit positions follow the SD Physical Layer + * Simplified Specification (CID: 5.2, CSD: 5.3, SCR: 5.6, SD Status: 4.10.2). + */ + +#define CSD_CAPACITY_UNIT_V2 (512ull * 1024ull) + +/* Extract `width` bits ending at `msb` from a 128-bit register (MSW first). */ +static uint32_t reg128_bits(const uint32_t raw[4], unsigned msb, unsigned width) +{ + unsigned lsb = msb + 1u - width; + uint32_t value = 0; + for (unsigned i = 0; i < width; i++) + { + unsigned bit = lsb + i; + uint32_t word = raw[3u - bit / 32u]; + value |= ((word >> (bit % 32u)) & 1u) << i; + } + return value; +} + +/* Extract `width` bits ending at `msb` from a big-endian byte register. */ +static uint32_t bytes_bits(const uint8_t* raw, size_t total_bits, unsigned msb, unsigned width) +{ + unsigned lsb = msb + 1u - width; + uint32_t value = 0; + for (unsigned i = 0; i < width; i++) + { + unsigned bit = lsb + i; + uint8_t byte = raw[total_bits / 8u - 1u - bit / 8u]; + value |= (uint32_t)((byte >> (bit % 8u)) & 1u) << i; + } + return value; +} + +static int decode_csd_capacity(const uint32_t raw[4], dmsdio_csd_t* csd) +{ + if (csd->structure == 0) + { + uint64_t c_size = reg128_bits(raw, 73, 12); + uint32_t mult = reg128_bits(raw, 49, 3); + if (csd->read_bl_len < 9 || csd->read_bl_len > 11) + { + return -EPROTO; + } + csd->capacity_bytes = (c_size + 1u) << (mult + 2u + csd->read_bl_len); + return 0; + } + if (csd->structure == 1) + { + uint64_t c_size = reg128_bits(raw, 69, 22); + if (csd->read_bl_len != 9) + { + return -EPROTO; + } + csd->capacity_bytes = (c_size + 1u) * CSD_CAPACITY_UNIT_V2; + return 0; + } + /* CSD v3 (SDUC) needs extended addressing that dmsdio does not implement. */ + return -ENOTSUP; +} + +dmod_dmsdio_api_declaration(1.0, int, _decode_csd, ( const uint32_t raw[4], dmsdio_csd_t* csd )) +{ + if (raw == NULL || csd == NULL) + { + return -EINVAL; + } + memset(csd, 0, sizeof(*csd)); + csd->structure = (uint8_t)reg128_bits(raw, 127, 2); + csd->tran_speed = (uint8_t)reg128_bits(raw, 103, 8); + csd->ccc = (uint16_t)reg128_bits(raw, 95, 12); + csd->read_bl_len = (uint8_t)reg128_bits(raw, 83, 4); + csd->erase_blk_en = reg128_bits(raw, 46, 1) != 0; + csd->sector_size = (uint8_t)(reg128_bits(raw, 45, 7) + 1u); + csd->perm_write_protect = reg128_bits(raw, 13, 1) != 0; + csd->tmp_write_protect = reg128_bits(raw, 12, 1) != 0; + + int ret = decode_csd_capacity(raw, csd); + if (ret == 0 && csd->capacity_bytes == 0) + { + ret = -EPROTO; + } + return ret; +} + +dmod_dmsdio_api_declaration(1.0, int, _decode_cid, ( const uint32_t raw[4], dmsdio_cid_t* cid )) +{ + if (raw == NULL || cid == NULL) + { + return -EINVAL; + } + memset(cid, 0, sizeof(*cid)); + cid->manufacturer_id = (uint8_t)reg128_bits(raw, 127, 8); + for (unsigned i = 0; i < 2; i++) + { + cid->oem_id[i] = (char)reg128_bits(raw, 119 - 8 * i, 8); + } + for (unsigned i = 0; i < 5; i++) + { + cid->product_name[i] = (char)reg128_bits(raw, 103 - 8 * i, 8); + } + cid->product_revision = (uint8_t)reg128_bits(raw, 63, 8); + cid->serial_number = reg128_bits(raw, 55, 32); + cid->manufacture_year = (uint16_t)(2000u + reg128_bits(raw, 19, 8)); + cid->manufacture_month = (uint8_t)reg128_bits(raw, 11, 4); + return 0; +} + +dmod_dmsdio_api_declaration(1.0, int, _decode_scr, ( const uint8_t raw[8], dmsdio_scr_t* scr )) +{ + if (raw == NULL || scr == NULL) + { + return -EINVAL; + } + memset(scr, 0, sizeof(*scr)); + uint32_t structure = bytes_bits(raw, 64, 63, 4); + uint32_t bus_widths = bytes_bits(raw, 64, 51, 4); + uint32_t cmd_support = bytes_bits(raw, 64, 33, 2); + + scr->sd_spec = (uint8_t)bytes_bits(raw, 64, 59, 4); + scr->data_stat_after_erase = bytes_bits(raw, 64, 55, 1) != 0; + scr->sd_spec3 = bytes_bits(raw, 64, 47, 1) != 0; + scr->bus_width_1bit = (bus_widths & 0x1u) != 0; + scr->bus_width_4bit = (bus_widths & 0x4u) != 0; + scr->cmd23_supported = (cmd_support & 0x2u) != 0; + + return (structure != 0 || !scr->bus_width_1bit) ? -EPROTO : 0; +} + +static uint32_t au_size_bytes(uint32_t code) +{ + static const uint32_t large_mib[] = { 8, 12, 16, 24, 32, 64 }; + if (code == 0) + { + return 0; + } + if (code <= 9) + { + return (16u * 1024u) << (code - 1u); + } + return large_mib[code - 10u] * 1024u * 1024u; +} + +dmod_dmsdio_api_declaration(1.0, int, _decode_ssr, ( const uint8_t raw[64], dmsdio_ssr_t* ssr )) +{ + if (raw == NULL || ssr == NULL) + { + return -EINVAL; + } + memset(ssr, 0, sizeof(*ssr)); + ssr->bus_width_code = (uint8_t)bytes_bits(raw, 512, 511, 2); + ssr->speed_class = (uint8_t)bytes_bits(raw, 512, 447, 8); + ssr->au_size_bytes = au_size_bytes(bytes_bits(raw, 512, 431, 4)); + ssr->erase_size = (uint16_t)bytes_bits(raw, 512, 423, 16); + ssr->erase_timeout_s = (uint8_t)bytes_bits(raw, 512, 407, 6); + ssr->erase_offset_s = (uint8_t)bytes_bits(raw, 512, 401, 2); + ssr->discard_supported = bytes_bits(raw, 512, 313, 1) != 0; + return 0; +} diff --git a/src/dmsdio_detect.c b/src/dmsdio_detect.c new file mode 100644 index 0000000..4db2c4d --- /dev/null +++ b/src/dmsdio_detect.c @@ -0,0 +1,84 @@ +#include "dmsdio_internal.h" + +/* + * Card detect pin. + * + * Card detect is an optional dmgpio friend (friend_role=card_detect in the + * same friend_group). The driver only samples it - during a scan + * (DMDRVI_IOCTL_MONITOR_REFRESH) and for DMDRVI_IOCTL_MONITOR_EVENT. + * Waiting for its edges, settling and periodic polling are the job of the + * monitor service (dmdevfs' dmdevmon), which calls those two ioctls on the + * host node. + */ + +int dmsdio_detect_read_cd(struct dmdrvi_context* ctx, bool* present) +{ + dmosi_mutex_lock(ctx->cd_lock); + void* file = (ctx->cd_path != NULL) ? Dmod_FileOpen(ctx->cd_path, "r") : NULL; + bool configured = (ctx->cd_path != NULL); + dmosi_mutex_unlock(ctx->cd_lock); + if (file == NULL) + { + return configured ? -EIO : -ENOENT; + } + dmgpio_pins_mask_t active = 0; + int command = ctx->config.cd_active_high ? dmgpio_ioctl_cmd_get_high_pins_state + : dmgpio_ioctl_cmd_get_low_pins_state; + int ret = Dmod_Ioctl(file, command, &active); + Dmod_FileClose(file); + if (ret != 0) + { + return -EIO; + } + *present = (active != 0); + return 0; +} + +/* + * DMDRVI_IOCTL_MONITOR_EVENT - a card detect edge. + * + * Runs without the context lock: it has to get through while a transfer + * holds it, so that transfer bails out with -ENODEV instead of running into + * its timeouts. Only ever raises removal_pending; the next scan (REFRESH, + * under the lock) settles the card state and clears it. Without a card + * detect pin there is nothing to sample - the event is left to REFRESH. + */ +int dmsdio_detect_event(struct dmdrvi_context* ctx) +{ + bool present = true; + int ret = dmsdio_detect_read_cd(ctx, &present); + if (ret == -ENOENT) + { + return 0; + } + if (ret == 0 && !present) + { + ctx->removal_pending = true; + } + return ret; +} + +void dmsdio_detect_set_cd_path(struct dmdrvi_context* ctx, const char* path) +{ + char* copy = (path != NULL) ? Dmod_StrDup(path) : NULL; + if (path != NULL && copy == NULL) + { + DMOD_LOG_ERROR("dmsdio%u: cannot retain card detect path\n", (unsigned)ctx->config.major); + return; + } + dmosi_mutex_lock(ctx->cd_lock); + Dmod_Free(ctx->cd_path); + ctx->cd_path = copy; + dmosi_mutex_unlock(ctx->cd_lock); +} + +void dmsdio_detect_release(struct dmdrvi_context* ctx) +{ + if (ctx->cd_lock != NULL) + { + dmosi_mutex_lock(ctx->cd_lock); + Dmod_Free(ctx->cd_path); + ctx->cd_path = NULL; + dmosi_mutex_unlock(ctx->cd_lock); + } +} diff --git a/src/dmsdio_ident.c b/src/dmsdio_ident.c new file mode 100644 index 0000000..eda37ce --- /dev/null +++ b/src/dmsdio_ident.c @@ -0,0 +1,388 @@ +#include "dmsdio_internal.h" +#include "dmsdio_sd.h" +#include + +/* + * Card identification and bus negotiation (SD Physical Layer Simplified + * Specification, 4.2 "Card Identification Mode" and 4.3 "Data Transfer + * Mode"). Runs with the context lock held. + */ + +#define SSR_BUS_WIDTH_4BIT 2u +#define SWITCH_GROUP1_SUPPORT 415u /* bits 415:400 */ +#define SWITCH_GROUP1_RESULT 379u /* bits 379:376 */ + +static int power_cycle(struct dmdrvi_context* ctx) +{ + dmsdio_instance_t instance = ctx->config.instance; + uint32_t actual_hz = 0; + + int ret = dmsdio_port_set_power(instance, false); + if (ret == 0) + { + dmosi_thread_sleep(1); + ret = dmsdio_port_set_power(instance, true); + } + if (ret == 0) + { + ret = dmsdio_port_set_bus_width(instance, dmsdio_bus_width_1bit); + } + if (ret == 0) + { + ret = dmsdio_port_set_clock(instance, SD_CLOCK_IDENTIFICATION_HZ, &actual_hz); + } + if (ret == 0 && (actual_hz == 0 || actual_hz > SD_CLOCK_IDENTIFICATION_HZ)) + { + DMOD_LOG_ERROR("dmsdio: identification clock %u Hz out of range\n", (unsigned)actual_hz); + ret = -ERANGE; + } + if (ret == 0) + { + dmosi_thread_sleep(SD_POWER_UP_DELAY_MS); + } + return ret; +} + +/* CMD8: a v1.x card does not answer, a v2.0+ card must echo the pattern. */ +static int send_if_cond(struct dmdrvi_context* ctx, bool* v2) +{ + dmsdio_response_t resp; + uint32_t arg = SD_IF_COND_VHS_27_36 | SD_IF_COND_CHECK_PATTERN; + int ret = dmsdio_cmd_send(ctx, SD_CMD_SEND_IF_COND, arg, dmsdio_response_short, &resp); + if (ret == -ETIMEDOUT) + { + *v2 = false; + return 0; + } + if (ret != 0) + { + return ret; + } + if ((resp.words[0] & 0xFFFu) != arg) + { + DMOD_LOG_ERROR("dmsdio: CMD8 echo mismatch (0x%03x)\n", (unsigned)(resp.words[0] & 0xFFFu)); + return -EPROTO; + } + *v2 = true; + return 0; +} + +static int op_cond_once(struct dmdrvi_context* ctx, uint32_t arg, uint32_t* ocr) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_app(ctx, SD_ACMD_SD_SEND_OP_COND, arg, dmsdio_response_short_no_crc, &resp); + if (ret == 0) + { + *ocr = resp.words[0]; + } + return ret; +} + +/* ACMD41 until the card reports power-up complete. */ +static int send_op_cond(struct dmdrvi_context* ctx, bool v2, uint32_t* ocr) +{ + uint32_t arg = SD_OCR_VOLTAGE_WINDOW | (v2 ? SD_OCR_HCS : 0u); + uint32_t waited = 0; + for (;;) + { + int ret = op_cond_once(ctx, arg, ocr); + if (ret == -ETIMEDOUT && waited == 0) + { + return -ENODEV; /* nothing answers CMD55: no SD memory card */ + } + if (ret != 0) + { + return ret; + } + if ((*ocr & SD_OCR_VOLTAGE_WINDOW) == 0) + { + return -ENOTSUP; + } + if (*ocr & SD_OCR_BUSY) + { + return 0; + } + if (waited >= ctx->config.init_timeout_ms) + { + DMOD_LOG_ERROR("dmsdio: card did not finish power-up in %u ms\n", + (unsigned)ctx->config.init_timeout_ms); + return -ETIMEDOUT; + } + dmosi_thread_sleep(SD_ACMD41_POLL_MS); + waited += SD_ACMD41_POLL_MS; + } +} + +static int read_cid_and_rca(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_send(ctx, SD_CMD_ALL_SEND_CID, 0, dmsdio_response_long, &resp); + if (ret != 0) + { + return ret; + } + memcpy(card->cid_raw, resp.words, sizeof(card->cid_raw)); + dmsdio_decode_cid(card->cid_raw, &card->cid); + + ret = dmsdio_cmd_send(ctx, SD_CMD_SEND_RELATIVE_ADDR, 0, dmsdio_response_short, &resp); + if (ret != 0) + { + return ret; + } + card->rca = (uint16_t)(resp.words[0] >> 16); + if ((resp.words[0] & SD_R6_ERRORS) != 0 || card->rca == 0) + { + DMOD_LOG_ERROR("dmsdio: invalid CMD3 response 0x%08x\n", (unsigned)resp.words[0]); + return -EPROTO; + } + ctx->card.rca = card->rca; /* used by the CMD55/CMD13 helpers from here on */ + return 0; +} + +static int read_csd(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + dmsdio_response_t resp; + int ret = dmsdio_cmd_send(ctx, SD_CMD_SEND_CSD, (uint32_t)card->rca << 16, + dmsdio_response_long, &resp); + if (ret != 0) + { + return ret; + } + memcpy(card->csd_raw, resp.words, sizeof(card->csd_raw)); + ret = dmsdio_decode_csd(card->csd_raw, &card->csd); + if (ret != 0) + { + DMOD_LOG_ERROR("dmsdio: unsupported or malformed CSD (%d)\n", ret); + return ret; + } + card->capacity_bytes = card->csd.capacity_bytes; + card->block_count = card->capacity_bytes / DMSDIO_BLOCK_SIZE; + card->write_protected = card->csd.perm_write_protect || card->csd.tmp_write_protect; + return 0; +} + +/* Cross-check OCR (CCS) against the CSD structure and name the card family. */ +static int classify(dmsdio_card_info_t* card, bool v2) +{ + bool ccs = (card->ocr & SD_OCR_HCS) != 0; + card->block_addressing = ccs; + if (!ccs) + { + card->type = v2 ? dmsdio_card_type_sdsc_v2 : dmsdio_card_type_sdsc_v1; + return card->csd.structure == 0 ? 0 : -EPROTO; + } + if (!v2 || card->csd.structure != 1) + { + return -EPROTO; + } + card->type = (card->capacity_bytes > 32ull * 1024u * 1024u * 1024u) + ? dmsdio_card_type_sdxc : dmsdio_card_type_sdhc; + return 0; +} + +static int identify(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + bool v2 = false; + int ret = power_cycle(ctx); + if (ret == 0) + { + ret = dmsdio_cmd_send(ctx, SD_CMD_GO_IDLE_STATE, 0, dmsdio_response_none, NULL); + } + if (ret == 0) + { + ret = send_if_cond(ctx, &v2); + } + if (ret == 0) + { + ret = send_op_cond(ctx, v2, &card->ocr); + } + if (ret == 0) + { + ret = read_cid_and_rca(ctx, card); + } + if (ret == 0) + { + ret = read_csd(ctx, card); + } + return ret == 0 ? classify(card, v2) : ret; +} + +static int set_clock(struct dmdrvi_context* ctx, dmsdio_card_info_t* card, uint32_t hz) +{ + return dmsdio_port_set_clock(ctx->config.instance, + dmsdio_min_u32(hz, ctx->config.max_clock_hz), &card->clock_hz); +} + +static int select_card(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + int ret = set_clock(ctx, card, SD_CLOCK_DEFAULT_SPEED_HZ); + if (ret == 0) + { + ret = dmsdio_cmd_send(ctx, SD_CMD_SELECT_CARD, (uint32_t)card->rca << 16, + dmsdio_response_short_busy, NULL); + } + if (ret == 0) + { + ret = dmsdio_cmd_wait_ready(ctx, ctx->config.write_timeout_ms); + } + if (ret == 0 && !card->block_addressing) + { + dmsdio_response_t resp; + ret = dmsdio_cmd_send(ctx, SD_CMD_SET_BLOCKLEN, DMSDIO_BLOCK_SIZE, + dmsdio_response_short, &resp); + } + return ret; +} + +static int read_register(struct dmdrvi_context* ctx, uint8_t acmd, uint32_t size) +{ + dmsdio_data_t data = { + .buffer = ctx->bounce, + .block_size = size, + .block_count = 1, + .direction = dmsdio_direction_read, + .timeout_ms = ctx->config.read_timeout_ms, + }; + return dmsdio_cmd_data(ctx, true, acmd, 0, &data, NULL); +} + +static int read_scr(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + int ret = read_register(ctx, SD_ACMD_SEND_SCR, SD_SCR_SIZE); + if (ret == 0) + { + ret = dmsdio_decode_scr(ctx->bounce, &card->scr); + } + return ret; +} + +static int negotiate_bus_width(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + card->bus_width = dmsdio_bus_width_1bit; + if (ctx->config.max_bus_width != dmsdio_bus_width_4bit || !card->scr.bus_width_4bit) + { + return 0; + } + /* Disconnect the card's internal DAT3 pull-up before driving 4 lines. */ + int ret = dmsdio_cmd_app(ctx, SD_ACMD_SET_CLR_CARD_DETECT, 0, dmsdio_response_short, NULL); + if (ret == 0) + { + ret = dmsdio_cmd_app(ctx, SD_ACMD_SET_BUS_WIDTH, 2, dmsdio_response_short, NULL); + } + if (ret == 0) + { + ret = dmsdio_port_set_bus_width(ctx->config.instance, dmsdio_bus_width_4bit); + } + if (ret == 0) + { + card->bus_width = dmsdio_bus_width_4bit; + } + return ret; +} + +static int read_ssr(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + int ret = read_register(ctx, SD_ACMD_SD_STATUS, SD_SSR_SIZE); + if (ret == 0) + { + ret = dmsdio_decode_ssr(ctx->bounce, &card->ssr); + } + if (ret == 0 && card->bus_width == dmsdio_bus_width_4bit && + card->ssr.bus_width_code != SSR_BUS_WIDTH_4BIT) + { + DMOD_LOG_ERROR("dmsdio: card did not switch to the 4-bit bus\n"); + ret = -EPROTO; + } + return ret; +} + +/* Extract `width` bits ending at `msb` from the 512-bit switch status. */ +static uint32_t switch_bits(const uint8_t* raw, unsigned msb, unsigned width) +{ + uint32_t value = 0; + for (unsigned i = 0; i < width; i++) + { + unsigned bit = msb + 1u - width + i; + value |= (uint32_t)((raw[63u - bit / 8u] >> (bit % 8u)) & 1u) << i; + } + return value; +} + +static int switch_function(struct dmdrvi_context* ctx, uint32_t arg) +{ + dmsdio_data_t data = { + .buffer = ctx->bounce, + .block_size = SD_SWITCH_STATUS_SIZE, + .block_count = 1, + .direction = dmsdio_direction_read, + .timeout_ms = ctx->config.read_timeout_ms, + }; + return dmsdio_cmd_data(ctx, false, SD_CMD_SWITCH_FUNC, arg, &data, NULL); +} + +/* CMD6 High Speed switch - succeeds only if the returned status confirms it. */ +static int switch_high_speed(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + card->high_speed = false; + if (!ctx->config.high_speed || card->scr.sd_spec < 1 || + (card->csd.ccc & SD_CCC_SWITCH) == 0 || ctx->config.max_clock_hz <= SD_CLOCK_DEFAULT_SPEED_HZ) + { + return 0; + } + int ret = switch_function(ctx, SD_SWITCH_CHECK | SD_SWITCH_HIGH_SPEED); + if (ret != 0 || (switch_bits(ctx->bounce, SWITCH_GROUP1_SUPPORT, 16) & 0x2u) == 0) + { + return ret; + } + ret = switch_function(ctx, SD_SWITCH_SET | SD_SWITCH_HIGH_SPEED); + if (ret != 0) + { + return ret; + } + if (switch_bits(ctx->bounce, SWITCH_GROUP1_RESULT, 4) != SD_SWITCH_HIGH_SPEED) + { + DMOD_LOG_WARN("dmsdio: card refused High Speed, staying at default speed\n"); + return 0; + } + card->high_speed = true; + return set_clock(ctx, card, SD_CLOCK_HIGH_SPEED_HZ); +} + +static int configure_transfer_mode(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + int ret = select_card(ctx, card); + if (ret == 0) + { + ret = read_scr(ctx, card); + } + if (ret == 0) + { + ret = negotiate_bus_width(ctx, card); + } + if (ret == 0) + { + ret = read_ssr(ctx, card); + } + if (ret == 0) + { + ret = switch_high_speed(ctx, card); + } + return ret; +} + +int dmsdio_ident_run(struct dmdrvi_context* ctx, dmsdio_card_info_t* card) +{ + memset(card, 0, sizeof(*card)); + ctx->card.rca = 0; + + int ret = identify(ctx, card); + if (ret == 0) + { + ret = configure_transfer_mode(ctx, card); + } + if (ret != 0) + { + card->type = dmsdio_card_type_none; + } + return ret; +} diff --git a/src/dmsdio_internal.h b/src/dmsdio_internal.h new file mode 100644 index 0000000..f836d07 --- /dev/null +++ b/src/dmsdio_internal.h @@ -0,0 +1,127 @@ +#ifndef DMSDIO_INTERNAL_H +#define DMSDIO_INTERNAL_H + +#include "dmod.h" +#include "dmsdio.h" +#include "dmsdio_port.h" +#include "dmdrvi.h" +#include "dmini.h" +#include "dmosi.h" +#include "dmgpio_types.h" +#include + +/* + * Only dmsdio.c defines DMOD_ENABLE_REGISTRATION before including this + * header: it owns the API registration records for every header listed + * above, and therefore also every dmdrvi DIF implementation. + */ + +/* Magic numbers - 'SDIO' for driver contexts, 'SDFH' for open handles. */ +#define DMSDIO_CONTEXT_MAGIC 0x5344494Fu +#define DMSDIO_HANDLE_MAGIC 0x53444648u + +/* Card node minor number below the host node (/dev/dmsdioN/0). */ +#define DMSDIO_CARD_MINOR 0u + +/** + * @brief Configuration read from the dmdrvi ini section. + */ +typedef struct +{ + dmsdio_instance_t instance; /**< Controller instance (1-based) */ + uint8_t major; /**< Device major number */ + dmsdio_bus_width_t max_bus_width; /**< Widest bus the board wires up */ + uint32_t max_clock_hz; /**< Upper clock limit */ + bool high_speed; /**< Allow CMD6 High Speed switch */ + uint32_t retries; /**< Extra attempts for transient errors */ + uint32_t init_timeout_ms; /**< ACMD41 power-up limit */ + uint32_t read_timeout_ms; /**< Read data phase limit */ + uint32_t write_timeout_ms; /**< Write data phase / busy limit */ + uint32_t erase_timeout_ms; /**< Minimum erase busy limit */ + uint32_t max_blocks_per_transfer;/**< Split larger requests */ + uint32_t bounce_blocks; /**< Size of the bounce buffer in blocks */ + bool cd_active_high; /**< Card detect polarity */ + /* Presence monitoring policy - not used by the driver itself, handed to + * the monitor service through DMDRVI_IOCTL_MONITOR_GET_POLICY. */ + dmdrvi_monitor_policy_t monitor; +} dmsdio_config_t; + +/** + * @brief Driver context, one per configured host controller. + */ +struct dmdrvi_context +{ + uint32_t magic; /**< DMSDIO_CONTEXT_MAGIC */ + dmsdio_config_t config; /**< Parsed configuration */ + dmosi_mutex_t lock; /**< Serializes all bus operations */ + uint8_t* bounce; /**< config.bounce_blocks blocks for RMW/bounce, see dmsdio_io.c */ + bool bounce_from_port;/**< bounce came from dmsdio_port_buffer_alloc() */ + dmsdio_card_info_t card; /**< Attached card, type none if absent */ + uint32_t generation; /**< Bumped on every attach/detach */ + uint32_t scan_count; /**< Completed presence scans */ + int last_error; /**< Last transport error */ + uint32_t retry_count; /**< Operations recovered by retry */ + volatile bool removal_pending;/**< Card detect says removed; set without the lock */ + bool host_ready; /**< dmdevfs knows the host node (path_ready) */ + bool card_announced; /**< Card node announced to dmdevfs */ + uint32_t announced_generation; /**< Card generation the announcement is for */ + dmosi_mutex_t cd_lock; /**< Guards cd_path (never held across bus work) */ + char* cd_path; /**< Card detect GPIO node, NULL = none */ +}; + +/** Open file handle - pins the card generation it was opened for. */ +typedef struct +{ + uint32_t magic; /**< DMSDIO_HANDLE_MAGIC */ + bool is_card; /**< Card node (true) or host node */ + uint32_t generation; /**< Card generation at open time */ + int flags; /**< DMDRVI_O_* */ +} dmsdio_handle_t; + +/* --- dmsdio_config.c --- */ +int dmsdio_config_read(dmini_context_t ini, dmsdio_config_t* config); + +/* --- dmsdio_cmd.c: single commands and card status --- */ +int dmsdio_cmd_errno(dmsdio_status_t status); +int dmsdio_cmd_send(struct dmdrvi_context* ctx, uint8_t index, uint32_t arg, + dmsdio_response_type_t type, dmsdio_response_t* resp); +int dmsdio_cmd_app(struct dmdrvi_context* ctx, uint8_t index, uint32_t arg, + dmsdio_response_type_t type, dmsdio_response_t* resp); +int dmsdio_cmd_data(struct dmdrvi_context* ctx, bool app, uint8_t index, uint32_t arg, + const dmsdio_data_t* data, dmsdio_response_t* resp); +int dmsdio_cmd_status(struct dmdrvi_context* ctx, uint32_t* status); +int dmsdio_cmd_wait_ready(struct dmdrvi_context* ctx, uint32_t timeout_ms); +int dmsdio_cmd_stop(struct dmdrvi_context* ctx); + +/* --- dmsdio_ident.c: identification and bus negotiation --- */ +int dmsdio_ident_run(struct dmdrvi_context* ctx, dmsdio_card_info_t* card); + +/* --- dmsdio_xfer.c: block transfers, erase and recovery --- */ +int dmsdio_xfer_read(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count); +int dmsdio_xfer_write(struct dmdrvi_context* ctx, uint64_t lba, const void* buf, uint32_t count); +int dmsdio_xfer_erase(struct dmdrvi_context* ctx, uint64_t lba, uint64_t count, bool discard); + +/* --- dmsdio_io.c: byte-offset I/O --- */ +dmdrvi_ssize_t dmsdio_io_read(struct dmdrvi_context* ctx, uint8_t* buf, size_t size, uint64_t offset); +dmdrvi_ssize_t dmsdio_io_write(struct dmdrvi_context* ctx, const uint8_t* buf, size_t size, uint64_t offset); +int dmsdio_io_erase_range(struct dmdrvi_context* ctx, const dmdrvi_block_range_t* range, bool discard); + +/* --- dmsdio_card.c: attach/detach bookkeeping --- */ +int dmsdio_card_scan(struct dmdrvi_context* ctx); +void dmsdio_card_detach(struct dmdrvi_context* ctx); +int dmsdio_card_lost(struct dmdrvi_context* ctx, int error); +bool dmsdio_card_attached(const struct dmdrvi_context* ctx); + +/* --- dmsdio_detect.c: card detect pin --- */ +void dmsdio_detect_set_cd_path(struct dmdrvi_context* ctx, const char* path); +int dmsdio_detect_read_cd(struct dmdrvi_context* ctx, bool* present); +int dmsdio_detect_event(struct dmdrvi_context* ctx); +void dmsdio_detect_release(struct dmdrvi_context* ctx); + +/* --- helpers --- */ +static inline void dmsdio_lock(struct dmdrvi_context* ctx) { dmosi_mutex_lock(ctx->lock); } +static inline void dmsdio_unlock(struct dmdrvi_context* ctx) { dmosi_mutex_unlock(ctx->lock); } + +static inline uint32_t dmsdio_min_u32(uint32_t a, uint32_t b) { return a < b ? a : b; } + +#endif /* DMSDIO_INTERNAL_H */ diff --git a/src/dmsdio_io.c b/src/dmsdio_io.c new file mode 100644 index 0000000..7729c5f --- /dev/null +++ b/src/dmsdio_io.c @@ -0,0 +1,184 @@ +#include "dmsdio_internal.h" +#include + +/* + * Byte-offset I/O on top of 512-byte blocks. Runs with the context lock + * held and a card attached. + * + * Every request is cut into steps, each one command: + * - direct: whole blocks moved straight between the caller's buffer and + * the card (up to max_blocks_per_transfer), when the port accepts that + * buffer for the direction (dmsdio_port_buffer_is_direct(): aligned, + * reachable, and fast enough - on STM32F7 a write from external SDRAM is + * not, it underruns the host FIFO at 48 MHz); + * - bounced: whole blocks the port does not accept go through the context's + * bounce buffer (up to bounce_blocks per command) - memory the port + * allocated, so the data phase itself always runs at full speed; + * - partial: a partial block at either end goes through the first block of + * the bounce buffer; writes read-modify-write it so bytes outside the + * requested range are preserved. + */ + +#define BLOCK ((uint64_t)DMSDIO_BLOCK_SIZE) + +typedef enum +{ + step_direct, + step_bounced, + step_partial, +} step_kind_t; + +typedef struct +{ + step_kind_t kind; + uint64_t lba; /* first block touched by this step */ + uint32_t skip; /* bytes to skip inside that block (partial only) */ + uint32_t blocks; /* blocks moved by the command */ + size_t length; /* bytes consumed from the caller's buffer */ +} io_step_t; + +static uint64_t capacity(const struct dmdrvi_context* ctx) +{ + return ctx->card.block_count * BLOCK; +} + +static uint32_t min_blocks(uint64_t blocks, uint32_t limit) +{ + return (blocks > limit) ? limit : (uint32_t)blocks; +} + +/* Plan the next piece of a request starting at `offset` with `remaining` bytes. */ +static io_step_t plan_step(const struct dmdrvi_context* ctx, const void* buf, uint64_t offset, + size_t remaining, dmsdio_direction_t direction) +{ + io_step_t step; + step.lba = offset / BLOCK; + step.skip = (uint32_t)(offset % BLOCK); + + if (step.skip != 0 || remaining < BLOCK) + { + size_t in_block = (size_t)(BLOCK - step.skip); + step.kind = step_partial; + step.blocks = 1; + step.length = (remaining < in_block) ? remaining : in_block; + return step; + } + step.blocks = min_blocks(remaining / BLOCK, ctx->config.max_blocks_per_transfer); + step.kind = step_direct; + if (!dmsdio_port_buffer_is_direct(ctx->config.instance, buf, step.blocks * BLOCK, direction)) + { + step.kind = step_bounced; + step.blocks = min_blocks(step.blocks, ctx->config.bounce_blocks); + } + step.length = (size_t)(step.blocks * BLOCK); + return step; +} + +static int read_step(struct dmdrvi_context* ctx, const io_step_t* step, uint8_t* dst) +{ + if (step->kind == step_direct) + { + return dmsdio_xfer_read(ctx, step->lba, dst, step->blocks); + } + int ret = dmsdio_xfer_read(ctx, step->lba, ctx->bounce, step->blocks); + if (ret == 0) + { + memcpy(dst, ctx->bounce + step->skip, step->length); + } + return ret; +} + +static int write_step(struct dmdrvi_context* ctx, const io_step_t* step, const uint8_t* src) +{ + if (step->kind == step_direct) + { + return dmsdio_xfer_write(ctx, step->lba, src, step->blocks); + } + int ret = 0; + if (step->kind == step_partial && step->length != DMSDIO_BLOCK_SIZE) + { + ret = dmsdio_xfer_read(ctx, step->lba, ctx->bounce, 1); + } + if (ret == 0) + { + memcpy(ctx->bounce + step->skip, src, step->length); + ret = dmsdio_xfer_write(ctx, step->lba, ctx->bounce, step->blocks); + } + return ret; +} + +/* Clamp a request to the card; returns the byte count to transfer. */ +static size_t clamp_request(const struct dmdrvi_context* ctx, size_t size, uint64_t offset) +{ + uint64_t available = capacity(ctx) - offset; + return ((uint64_t)size > available) ? (size_t)available : size; +} + +dmdrvi_ssize_t dmsdio_io_read(struct dmdrvi_context* ctx, uint8_t* buf, size_t size, uint64_t offset) +{ + if (offset >= capacity(ctx)) + { + return 0; + } + size_t total = clamp_request(ctx, size, offset); + size_t done = 0; + while (done < total) + { + io_step_t step = plan_step(ctx, buf + done, offset + done, total - done, dmsdio_direction_read); + int ret = read_step(ctx, &step, buf + done); + if (ret != 0) + { + return ret; + } + done += step.length; + } + return (dmdrvi_ssize_t)done; +} + +dmdrvi_ssize_t dmsdio_io_write(struct dmdrvi_context* ctx, const uint8_t* buf, size_t size, uint64_t offset) +{ + if (ctx->card.write_protected) + { + return -EROFS; + } + if (offset >= capacity(ctx)) + { + return -ENOSPC; + } + size_t total = clamp_request(ctx, size, offset); + size_t done = 0; + while (done < total) + { + io_step_t step = plan_step(ctx, buf + done, offset + done, total - done, dmsdio_direction_write); + int ret = write_step(ctx, &step, buf + done); + if (ret != 0) + { + return ret; + } + done += step.length; + } + return (dmdrvi_ssize_t)done; +} + +int dmsdio_io_erase_range(struct dmdrvi_context* ctx, const dmdrvi_block_range_t* range, bool discard) +{ + if (range->offset < 0 || (range->offset % (dmdrvi_offset_t)BLOCK) != 0 || + (range->length % BLOCK) != 0) + { + return -EINVAL; + } + uint64_t offset = (uint64_t)range->offset; + if (offset > capacity(ctx) || range->length > capacity(ctx) - offset) + { + return -EINVAL; + } + if (ctx->card.write_protected) + { + return -EROFS; + } + if (range->length == 0) + { + return 0; + } + return dmsdio_xfer_erase(ctx, offset / BLOCK, range->length / BLOCK, discard); +} diff --git a/src/dmsdio_sd.h b/src/dmsdio_sd.h new file mode 100644 index 0000000..910accf --- /dev/null +++ b/src/dmsdio_sd.h @@ -0,0 +1,108 @@ +#ifndef DMSDIO_SD_H +#define DMSDIO_SD_H + +/* + * SD memory card protocol constants (SD Physical Layer Simplified + * Specification, sections 4.7 "Commands" and 4.10 "Card Status"). + * Private to the core module - ports never see these. + */ + +/* --- Commands --- */ +#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_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 + +/* --- Application commands (preceded by CMD55) --- */ +#define SD_ACMD_SET_BUS_WIDTH 6 +#define SD_ACMD_SD_STATUS 13 +#define SD_ACMD_SD_SEND_OP_COND 41 +#define SD_ACMD_SET_CLR_CARD_DETECT 42 +#define SD_ACMD_SEND_SCR 51 + +/* --- CMD8 (SEND_IF_COND) --- */ +#define SD_IF_COND_VHS_27_36 0x100u +#define SD_IF_COND_CHECK_PATTERN 0xAAu + +/* --- OCR / ACMD41 --- */ +#define SD_OCR_VOLTAGE_WINDOW 0x00FF8000u /* 2.7 - 3.6 V */ +#define SD_OCR_HCS 0x40000000u /* Host capacity support / CCS */ +#define SD_OCR_BUSY 0x80000000u /* Power-up complete */ + +/* --- R1 card status bits --- */ +#define SD_R1_OUT_OF_RANGE (1u << 31) +#define SD_R1_ADDRESS_ERROR (1u << 30) +#define SD_R1_BLOCK_LEN_ERROR (1u << 29) +#define SD_R1_ERASE_SEQ_ERROR (1u << 28) +#define SD_R1_ERASE_PARAM (1u << 27) +#define SD_R1_WP_VIOLATION (1u << 26) +#define SD_R1_LOCK_UNLOCK_FAILED (1u << 24) +#define SD_R1_COM_CRC_ERROR (1u << 23) +#define SD_R1_ILLEGAL_COMMAND (1u << 22) +#define SD_R1_CARD_ECC_FAILED (1u << 21) +#define SD_R1_CC_ERROR (1u << 20) +#define SD_R1_ERROR (1u << 19) +#define SD_R1_CSD_OVERWRITE (1u << 16) +#define SD_R1_WP_ERASE_SKIP (1u << 15) +#define SD_R1_READY_FOR_DATA (1u << 8) +#define SD_R1_APP_CMD (1u << 5) +#define SD_R1_AKE_SEQ_ERROR (1u << 3) + +#define SD_R1_ERRORS 0xFDF98008u +#define SD_R1_STATE(status) (((status) >> 9) & 0xFu) + +/* --- R6 (SEND_RELATIVE_ADDR) status bits 23, 22, 19 --- */ +#define SD_R6_ERRORS 0xE000u + +/* --- Card states --- */ +#define SD_STATE_IDLE 0u +#define SD_STATE_READY 1u +#define SD_STATE_IDENT 2u +#define SD_STATE_STBY 3u +#define SD_STATE_TRAN 4u +#define SD_STATE_DATA 5u +#define SD_STATE_RCV 6u +#define SD_STATE_PRG 7u + +/* --- CMD6 (SWITCH_FUNC) --- */ +#define SD_SWITCH_CHECK 0x00FFFFF0u +#define SD_SWITCH_SET 0x80FFFFF0u +#define SD_SWITCH_HIGH_SPEED 0x1u +#define SD_SWITCH_STATUS_SIZE 64u + +/* --- Card command classes (CSD CCC) --- */ +#define SD_CCC_ERASE (1u << 5) +#define SD_CCC_SWITCH (1u << 10) + +/* --- CMD38 arguments --- */ +#define SD_ERASE_ARG 0x00000000u +#define SD_DISCARD_ARG 0x00000001u + +/* --- Registers transferred on the data lines --- */ +#define SD_SCR_SIZE 8u +#define SD_SSR_SIZE 64u + +/* --- Clocks --- */ +#define SD_CLOCK_IDENTIFICATION_HZ 400000u +#define SD_CLOCK_DEFAULT_SPEED_HZ 25000000u +#define SD_CLOCK_HIGH_SPEED_HZ 50000000u + +/* --- Timing --- */ +#define SD_POWER_UP_DELAY_MS 2u /* >= 1 ms + 74 clocks at 400 kHz */ +#define SD_ACMD41_POLL_MS 10u + +#endif /* DMSDIO_SD_H */ diff --git a/src/dmsdio_xfer.c b/src/dmsdio_xfer.c new file mode 100644 index 0000000..cefbe01 --- /dev/null +++ b/src/dmsdio_xfer.c @@ -0,0 +1,204 @@ +#include "dmsdio_internal.h" +#include "dmsdio_sd.h" + +/* + * Block transfers with retry and recovery. Runs with the context lock held + * and a card attached. Callers pass already validated block ranges. + * + * Error policy: + * - CRC errors, timeouts and FIFO/DMA faults are transient: the card is + * brought back to the transfer state and the whole operation retried. + * Once `retries` extra attempts failed the result is -EIO; with + * retries=0 the precise cause (-EBADMSG, -ETIMEDOUT, -EIO) is returned. + * - Malformed responses (-EPROTO) and card status errors are returned + * immediately. + * - A card that stops answering CMD13 during recovery is treated as + * removed: it is detached and the operation fails with -ENODEV. + */ + +#define ERASE_CHUNK_BLOCKS 8192u /* 4 MiB per CMD38 */ +#define RECOVERY_STATUS_TRIES 2 + +typedef int (*xfer_op_t)(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count); + +static uint32_t card_address(const struct dmdrvi_context* ctx, uint64_t lba) +{ + return ctx->card.block_addressing ? (uint32_t)lba : (uint32_t)(lba * DMSDIO_BLOCK_SIZE); +} + +static int read_op(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count) +{ + dmsdio_data_t data = { + .buffer = buf, + .block_size = DMSDIO_BLOCK_SIZE, + .block_count = count, + .direction = dmsdio_direction_read, + .timeout_ms = ctx->config.read_timeout_ms, + }; + uint8_t index = (count > 1) ? SD_CMD_READ_MULTIPLE_BLOCK : SD_CMD_READ_SINGLE_BLOCK; + int ret = dmsdio_cmd_data(ctx, false, index, card_address(ctx, lba), &data, NULL); + if (count > 1) + { + int stop = dmsdio_cmd_stop(ctx); + ret = (ret != 0) ? ret : stop; + } + return ret; +} + +static int write_op(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count) +{ + dmsdio_data_t data = { + .buffer = buf, + .block_size = DMSDIO_BLOCK_SIZE, + .block_count = count, + .direction = dmsdio_direction_write, + .timeout_ms = ctx->config.write_timeout_ms, + }; + uint8_t index = (count > 1) ? SD_CMD_WRITE_MULTIPLE_BLOCK : SD_CMD_WRITE_BLOCK; + int ret = dmsdio_cmd_data(ctx, false, index, card_address(ctx, lba), &data, NULL); + if (count > 1) + { + int stop = dmsdio_cmd_stop(ctx); + ret = (ret != 0) ? ret : stop; + } + return (ret != 0) ? ret : dmsdio_cmd_wait_ready(ctx, ctx->config.write_timeout_ms); +} + +/* Erase busy limit: SD Status based estimate, never below the configured floor. */ +static uint32_t erase_timeout_ms(const struct dmdrvi_context* ctx, uint32_t count) +{ + const dmsdio_ssr_t* ssr = &ctx->card.ssr; + uint32_t timeout = ctx->config.erase_timeout_ms; + if (ssr->erase_size != 0 && ssr->erase_timeout_s != 0 && ssr->au_size_bytes != 0) + { + uint64_t bytes = (uint64_t)count * DMSDIO_BLOCK_SIZE; + uint64_t aus = (bytes + ssr->au_size_bytes - 1u) / ssr->au_size_bytes; + uint64_t ms = aus * ssr->erase_timeout_s * 1000u / ssr->erase_size + + (uint64_t)ssr->erase_offset_s * 1000u; + timeout = (ms > timeout) ? (uint32_t)ms : timeout; + } + return timeout; +} + +static int erase_common(struct dmdrvi_context* ctx, uint64_t lba, uint32_t count, uint32_t arg) +{ + int ret = dmsdio_cmd_data(ctx, false, SD_CMD_ERASE_WR_BLK_START, card_address(ctx, lba), NULL, NULL); + if (ret == 0) + { + ret = dmsdio_cmd_data(ctx, false, SD_CMD_ERASE_WR_BLK_END, + card_address(ctx, lba + count - 1u), NULL, NULL); + } + if (ret == 0) + { + dmsdio_response_t resp; + ret = dmsdio_cmd_send(ctx, SD_CMD_ERASE, arg, dmsdio_response_short_busy, &resp); + } + return (ret != 0) ? ret : dmsdio_cmd_wait_ready(ctx, erase_timeout_ms(ctx, count)); +} + +static int erase_op(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count) +{ + (void)buf; + return erase_common(ctx, lba, count, SD_ERASE_ARG); +} + +static int discard_op(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count) +{ + (void)buf; + return erase_common(ctx, lba, count, SD_DISCARD_ARG); +} + +/* + * Bring the card back to the transfer state after a failed operation. + * Returns -ENODEV when the card no longer answers at all. + */ +static int recover(struct dmdrvi_context* ctx) +{ + uint32_t status = 0; + int ret = -ETIMEDOUT; + for (int i = 0; i < RECOVERY_STATUS_TRIES && ret != 0; i++) + { + ret = dmsdio_cmd_status(ctx, &status); + if (ret == -ETIMEDOUT && ctx->removal_pending) + { + break; + } + } + if (ret == -ETIMEDOUT) + { + return -ENODEV; + } + if (ret != 0) + { + return ret; + } + uint32_t state = SD_R1_STATE(status); + if (state == SD_STATE_DATA || state == SD_STATE_RCV) + { + dmsdio_cmd_stop(ctx); + } + return dmsdio_cmd_wait_ready(ctx, ctx->config.write_timeout_ms); +} + +static bool is_transient(int error) +{ + return error == -ETIMEDOUT || error == -EBADMSG || error == -EIO; +} + +static int run(struct dmdrvi_context* ctx, xfer_op_t op, uint64_t lba, void* buf, uint32_t count) +{ + int last = 0; + for (uint32_t attempt = 0; attempt <= ctx->config.retries; attempt++) + { + if (ctx->removal_pending) + { + return dmsdio_card_lost(ctx, -ENODEV); + } + last = op(ctx, lba, buf, count); + if (last == 0) + { + ctx->retry_count += (attempt > 0) ? 1u : 0u; + return 0; + } + int rec = recover(ctx); + if (rec == -ENODEV || ctx->removal_pending) + { + return dmsdio_card_lost(ctx, -ENODEV); + } + if (!is_transient(last) || rec != 0) + { + return last; + } + DMOD_LOG_WARN("dmsdio: transfer at block %lu failed (%d), attempt %u\n", + (unsigned long)lba, last, (unsigned)(attempt + 1u)); + } + return (ctx->config.retries == 0) ? last : -EIO; +} + +int dmsdio_xfer_read(struct dmdrvi_context* ctx, uint64_t lba, void* buf, uint32_t count) +{ + return run(ctx, read_op, lba, buf, count); +} + +int dmsdio_xfer_write(struct dmdrvi_context* ctx, uint64_t lba, const void* buf, uint32_t count) +{ + /* The port only reads from buf in the write direction. */ + return run(ctx, write_op, lba, (void*)buf, count); +} + +int dmsdio_xfer_erase(struct dmdrvi_context* ctx, uint64_t lba, uint64_t count, bool discard) +{ + xfer_op_t op = discard ? discard_op : erase_op; + while (count > 0) + { + uint32_t chunk = (count > ERASE_CHUNK_BLOCKS) ? ERASE_CHUNK_BLOCKS : (uint32_t)count; + int ret = run(ctx, op, lba, NULL, chunk); + if (ret != 0) + { + return ret; + } + lba += chunk; + count -= chunk; + } + return 0; +} diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index 43dbb92..86c24cf 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -16,6 +16,22 @@ set(DMOD_STACK_SIZE 1024) # Path to the DMR file for this module set(DMOD_DMR_PATH ${CMAKE_SOURCE_DIR}/dmsdio_port.dmr) +# Extra sources of a CPU family. Every STM32 family shares the same +# SDIO/SDMMC host IP (see src/port/stm32_common), the x86_64 family is the +# simulated card used by the tests. +set(FAMILY_SOURCES "") +if(DMOD_CPU_FAMILY MATCHES "^stm32") + set(FAMILY_SOURCES + stm32_common/stm32_common.c + stm32_common/stm32_data.c + ) +elseif(DMOD_CPU_FAMILY STREQUAL "x86_64") + set(FAMILY_SOURCES + x86_64/mock_card.c + x86_64/mock_registers.c + ) +endif() + # # dmod_add_library - create a library module # it has the same signature as add_library @@ -25,16 +41,30 @@ set(DMOD_DMR_PATH ${CMAKE_SOURCE_DIR}/dmsdio_port.dmr) dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} # List of source files - can include C and C++ files ${DMOD_CPU_FAMILY}/port.c + ${FAMILY_SOURCES} ) -# To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) +# One dmod_link_modules() call: each call rewrites the target's version +# requirements file. +# dmdrvi: headers only - dmsdio_types.h numbers the dmsdio ioctls from +# DMDRVI_IOCTL_CUSTOM_BASE. +# dmdma 0.4: dmdma_lease_start_ex() (FIFO/burst/flow-controller options) +# dmheap: bounce buffers from dmod-boot's "dma" heap (internal SRAM). +set(DMSDIO_PORT_MODULES dmdrvi@>=2.0) +if(DMOD_CPU_FAMILY MATCHES "^stm32") + list(APPEND DMSDIO_PORT_MODULES + dmclk_port + dmosi + dmdma@>=0.4 + dmheap + ) +endif() +dmod_link_modules(${DMOD_MODULE_NAME} ${DMSDIO_PORT_MODULES}) target_include_directories(${DMOD_MODULE_NAME} BEFORE PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/${DMOD_CPU_FAMILY} ) target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c new file mode 100644 index 0000000..5b511dd --- /dev/null +++ b/src/port/stm32_common/stm32_common.c @@ -0,0 +1,427 @@ +/* DMOD_ENABLE_REGISTRATION is intentionally NOT set here: each family's + * port.c (which includes dmsdio_port.h with the flag on) is the single + * translation unit that emits this module's API registration table. See + * dmspi/src/port/stm32_common/stm32_common.c for the same split. */ +#include "dmod.h" +#include "dmosi.h" +#include "dmclk_port.h" +#include "stm32_common.h" +#include +#include + +/* + * STM32F4 SDIO / STM32F7 SDMMC host - protocol-free primitives only. + * + * This file: lifecycle, clocking, bus width, the command path (CPSM with + * CMDREND/CMDSENT/CCRCFAIL/CTIMEOUT interrupts) and the shared interrupt + * handler. The data path (DMA for block data) lives in stm32_data.c. + * + * Hardware flow control (CLKCR.HWFC_EN) is not used: ES0182 (STM32F40x/41x + * errata) documents SDIO_CK glitches with it enabled. + * + * Every wait is bounded: the command wait by STM32_CMD_TIMEOUT_MS on top of + * the hardware 64-clock response timeout, the data wait by the per-block + * timeout requested by the core (also programmed into DTIMER). + */ + +#define STM32_CMD_TIMEOUT_MS 100 + + +static stm32_sdio_state_t g_state[STM32_SDIO_MAX_INSTANCES]; + +/* ====================================================================== + * Helpers + * ====================================================================== */ + +static const stm32_sdio_instance_desc_t* get_desc(dmsdio_instance_t instance) +{ + if (instance < 1 || instance > stm32_sdio_instance_count || instance > STM32_SDIO_MAX_INSTANCES) + { + return NULL; + } + return &stm32_sdio_instances[instance - 1]; +} + +volatile stm32_sdio_t* stm32_sdio_regs(dmsdio_instance_t instance) +{ + return (volatile stm32_sdio_t*)stm32_sdio_instances[instance - 1].base; +} + +static stm32_sdio_state_t* get_ready_state(dmsdio_instance_t instance) +{ + if (get_desc(instance) == NULL || !g_state[instance - 1].initialized) + { + return NULL; + } + return &g_state[instance - 1]; +} + +uint32_t stm32_sdio_data_error_flags(void) +{ + return STM32_SDIO_DATA_ERRORS | stm32_sdio_family_error_flags; +} + +static uint32_t static_flags(void) +{ + return STM32_SDIO_STATIC_FLAGS | stm32_sdio_family_error_flags; +} + +static void nvic_enable_irq(uint32_t irqn) +{ + /* Priority at or below dmosi's limit: the handler posts a semaphore. */ + volatile uint8_t *nvic_ip = (volatile uint8_t *)0xE000E400UL; + volatile uint32_t *nvic_iser = (volatile uint32_t *)0xE000E100UL; + nvic_ip[irqn] = (uint8_t)dmosi_get_min_interrupt_priority(); + nvic_iser[irqn >> 5U] = 1U << (irqn & 0x1FU); +} + +static void nvic_disable_irq(uint32_t irqn) +{ + volatile uint32_t *nvic_icer = (volatile uint32_t *)0xE000E180UL; + nvic_icer[irqn >> 5U] = 1U << (irqn & 0x1FU); +} + +/* Registers written back to back need 3 SDIOCLK + 2 PCLK2 cycles (RM0090 31.9). */ +static void register_settle(void) +{ + dmclk_port_delay_us(1); +} + +/* MASK is also modified by the ISR - thread-side read-modify-write must not interleave. */ +void stm32_sdio_mask_enable(volatile stm32_sdio_t* regs, uint32_t bits) +{ + Dmod_EnterCritical(); + regs->MASK |= bits; + Dmod_ExitCritical(); +} + +static void drain_semaphore(stm32_sdio_state_t* st) +{ + while (dmosi_semaphore_wait(st->done, 1, 0) == 0) + { + } +} + +/* Wait until the ISR cleared *busy. Returns false on timeout. */ +bool stm32_sdio_wait(stm32_sdio_state_t* st, volatile bool* busy, uint32_t timeout_ms) +{ + int32_t timeout = (timeout_ms > (uint32_t)INT32_MAX) ? INT32_MAX : (int32_t)timeout_ms; + while (*busy) + { + if (dmosi_semaphore_wait(st->done, 1, timeout) != 0) + { + return !*busy; + } + } + return true; +} + +/* ====================================================================== + * Lifecycle + * ====================================================================== */ + +void stm32_sdio_common_init(void) +{ + memset(g_state, 0, sizeof(g_state)); +} + +void stm32_sdio_common_deinit(void) +{ + for (uint8_t i = 1; i <= STM32_SDIO_MAX_INSTANCES; i++) + { + if (get_ready_state(i) != NULL) + { + dmsdio_port_host_deinit(i); + } + } +} + +static void reset_peripheral(const stm32_sdio_instance_desc_t* desc) +{ + STM32_RCC_APB2ENR |= (1U << desc->apb2_bit); + (void)STM32_RCC_APB2ENR; /* read back: clock active before access */ + STM32_RCC_APB2RSTR |= (1U << desc->apb2_bit); + STM32_RCC_APB2RSTR &= ~(1U << desc->apb2_bit); +} + +dmod_dmsdio_port_api_declaration(1.0, int, _host_init, ( dmsdio_instance_t instance )) +{ + const stm32_sdio_instance_desc_t* desc = get_desc(instance); + if (desc == NULL) + { + DMOD_LOG_ERROR("dmsdio_port: no SDIO/SDMMC instance %u on this family\n", (unsigned)instance); + return -ENODEV; + } + stm32_sdio_state_t* st = &g_state[instance - 1]; + if (st->initialized) + { + return -EBUSY; + } + if (dmclk_port_get_domain_frequency(dmclk_domain_sdio) == 0) + { + DMOD_LOG_ERROR("dmsdio_port: SDIO kernel clock (CLK48) is not running\n"); + return -EIO; + } + memset(st, 0, sizeof(*st)); + st->done = dmosi_semaphore_create(0, 3); + if (st->done == NULL) + { + return -ENOMEM; + } + int ret = stm32_sdio_data_init(desc, st); + if (ret != 0) + { + dmosi_semaphore_destroy(st->done); + memset(st, 0, sizeof(*st)); + return ret; + } + reset_peripheral(desc); + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + regs->MASK = 0; + regs->ICR = static_flags(); + regs->POWER = STM32_SDIO_POWER_OFF; + regs->CLKCR = 0; + nvic_enable_irq(desc->irqn); + st->initialized = true; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _host_deinit, ( dmsdio_instance_t instance )) +{ + stm32_sdio_state_t* st = get_ready_state(instance); + if (st == NULL) + { + return -ENODEV; + } + const stm32_sdio_instance_desc_t* desc = get_desc(instance); + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + nvic_disable_irq(desc->irqn); + regs->MASK = 0; + regs->DCTRL = 0; + regs->CLKCR = 0; + regs->POWER = STM32_SDIO_POWER_OFF; + STM32_RCC_APB2ENR &= ~(1U << desc->apb2_bit); + stm32_sdio_data_deinit(st); + dmosi_semaphore_destroy(st->done); + memset(st, 0, sizeof(*st)); + return 0; +} + +/* ====================================================================== + * Bus configuration + * ====================================================================== */ + +dmod_dmsdio_port_api_declaration(1.0, int, _set_power, ( dmsdio_instance_t instance, bool on )) +{ + if (get_ready_state(instance) == NULL) + { + return -ENODEV; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + if (on) + { + regs->POWER = STM32_SDIO_POWER_ON; + register_settle(); + regs->CLKCR |= STM32_SDIO_CLKCR_CLKEN; + } + else + { + regs->CLKCR &= ~STM32_SDIO_CLKCR_CLKEN; + register_settle(); + regs->POWER = STM32_SDIO_POWER_OFF; + } + register_settle(); + return 0; +} + +/* SDIO_CK = SDIOCLK / (CLKDIV + 2), or SDIOCLK itself with BYPASS. */ +static int compute_divider(uint32_t source_hz, uint32_t max_hz, uint32_t* clkcr_bits, uint32_t* actual_hz) +{ + if (max_hz >= source_hz) + { + *clkcr_bits = STM32_SDIO_CLKCR_BYPASS; + *actual_hz = source_hz; + return 0; + } + uint32_t divisor = (source_hz + max_hz - 1U) / max_hz; /* round up: never exceed max_hz */ + divisor = (divisor < 2U) ? 2U : divisor; + if (divisor - 2U > STM32_SDIO_CLKCR_CLKDIV_Msk) + { + return -ERANGE; + } + *clkcr_bits = divisor - 2U; + *actual_hz = source_hz / divisor; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_clock, ( dmsdio_instance_t instance, uint32_t max_hz, uint32_t* actual_hz )) +{ + stm32_sdio_state_t* st = get_ready_state(instance); + if (st == NULL || max_hz == 0) + { + return (st == NULL) ? -ENODEV : -EINVAL; + } + uint32_t source = (uint32_t)dmclk_port_get_domain_frequency(dmclk_domain_sdio); + uint32_t bits = 0; + uint32_t actual = 0; + int ret = (source == 0) ? -EIO : compute_divider(source, max_hz, &bits, &actual); + if (ret != 0) + { + DMOD_LOG_ERROR("dmsdio_port: cannot derive %u Hz from %u Hz\n", (unsigned)max_hz, (unsigned)source); + return ret; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + uint32_t clkcr = regs->CLKCR & ~(STM32_SDIO_CLKCR_CLKDIV_Msk | STM32_SDIO_CLKCR_BYPASS); + regs->CLKCR = clkcr | bits; + register_settle(); + st->clock_hz = actual; + if (actual_hz != NULL) + { + *actual_hz = actual; + } + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width )) +{ + if (get_ready_state(instance) == NULL) + { + return -ENODEV; + } + if (width != dmsdio_bus_width_1bit && width != dmsdio_bus_width_4bit) + { + return -EINVAL; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + uint32_t clkcr = regs->CLKCR & ~STM32_SDIO_CLKCR_WIDBUS_Msk; + regs->CLKCR = clkcr | ((width == dmsdio_bus_width_4bit) ? STM32_SDIO_CLKCR_WIDBUS_4BIT : 0U); + register_settle(); + return 0; +} + +/* ====================================================================== + * Interrupt handler + * ====================================================================== */ + +void stm32_sdio_irq_handler(dmsdio_instance_t instance) +{ + stm32_sdio_state_t* st = get_ready_state(instance); + if (st == NULL) + { + return; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + uint32_t sta = regs->STA; + + if (st->cmd_busy && (sta & STM32_SDIO_CMD_FLAGS)) + { + regs->MASK &= ~STM32_SDIO_CMD_FLAGS; + regs->ICR = STM32_SDIO_CMD_FLAGS; + st->cmd_status = sta; + st->cmd_busy = false; + dmosi_semaphore_post(st->done, 1); + } + stm32_sdio_data_irq(regs, st, sta); +} + +/* ====================================================================== + * Transport + * ====================================================================== */ + +static uint32_t command_register(const dmsdio_command_t* command) +{ + uint32_t value = (command->index & STM32_SDIO_CMD_INDEX_Msk) | STM32_SDIO_CMD_CPSMEN; + switch (command->response) + { + case dmsdio_response_none: return value; + case dmsdio_response_long: return value | STM32_SDIO_CMD_WAITRESP_LONG; + default: return value | STM32_SDIO_CMD_WAITRESP_SHORT; + } +} + +static dmsdio_status_t command_result(const dmsdio_command_t* command, uint32_t sta) +{ + if (sta & STM32_SDIO_STA_CTIMEOUT) + { + return dmsdio_status_cmd_timeout; + } + /* R3 carries no valid CRC: the CPSM always flags CCRCFAIL for it. */ + if ((sta & STM32_SDIO_STA_CCRCFAIL) && command->response != dmsdio_response_short_no_crc) + { + return dmsdio_status_cmd_crc; + } + return dmsdio_status_ok; +} + +static dmsdio_status_t send_command(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, + const dmsdio_command_t* command, dmsdio_response_t* response) +{ + uint32_t done = (command->response == dmsdio_response_none) + ? STM32_SDIO_STA_CMDSENT + : (STM32_SDIO_STA_CMDREND | STM32_SDIO_STA_CCRCFAIL | STM32_SDIO_STA_CTIMEOUT); + regs->ICR = STM32_SDIO_CMD_FLAGS; + st->cmd_busy = true; + stm32_sdio_mask_enable(regs, done); + regs->ARG = command->argument; + regs->CMD = command_register(command); + if (!stm32_sdio_wait(st, &st->cmd_busy, STM32_CMD_TIMEOUT_MS)) + { + return dmsdio_status_cmd_timeout; + } + dmsdio_status_t result = command_result(command, st->cmd_status); + if (result == dmsdio_status_ok && response != NULL) + { + response->index = (uint8_t)(regs->RESPCMD & STM32_SDIO_CMD_INDEX_Msk); + for (unsigned i = 0; i < 4; i++) + { + response->words[i] = regs->RESP[i]; + } + } + return result; +} + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_status_t, _execute, + ( dmsdio_instance_t instance, const dmsdio_command_t* command, + const dmsdio_data_t* data, dmsdio_response_t* response )) +{ + stm32_sdio_state_t* st = get_ready_state(instance); + if (st == NULL || command == NULL || command->index > STM32_SDIO_CMD_INDEX_Msk) + { + return dmsdio_status_invalid; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + drain_semaphore(st); + dmsdio_status_t status = (data != NULL) + ? stm32_sdio_data_arm(regs, st, get_desc(instance), data) + : dmsdio_status_ok; + if (status == dmsdio_status_ok) + { + status = send_command(regs, st, command, response); + } + if (status == dmsdio_status_ok && data != NULL) + { + status = stm32_sdio_data_run(regs, st, data); + } + if (status != dmsdio_status_ok) + { + dmsdio_port_abort(instance); + } + return status; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _abort, ( dmsdio_instance_t instance )) +{ + stm32_sdio_state_t* st = get_ready_state(instance); + if (st == NULL) + { + return; + } + volatile stm32_sdio_t* regs = stm32_sdio_regs(instance); + regs->MASK = 0; + regs->CMD = 0; /* stop the CPSM */ + stm32_sdio_data_abort(regs, st); + register_settle(); + regs->ICR = static_flags(); + st->cmd_busy = false; + drain_semaphore(st); +} diff --git a/src/port/stm32_common/stm32_common.h b/src/port/stm32_common/stm32_common.h new file mode 100644 index 0000000..a616e65 --- /dev/null +++ b/src/port/stm32_common/stm32_common.h @@ -0,0 +1,178 @@ +#ifndef STM32_COMMON_H +#define STM32_COMMON_H + +#include +#include +#include +#include "dmsdio_port.h" +#include "dmosi.h" +#include "dmdma.h" + +/** + * @brief STM32 SDIO (F4, RM0090 section 31) / SDMMC (F7, RM0385 section 35) + * register layout. + * + * Both families implement the same host IP revision: identical register + * offsets and identical bit positions for every field this port uses. The + * differences are limited to which instances exist (base address, RCC bit, + * IRQ number - see stm32_sdio_instances[] in each family's port.c) and to + * STBITERR (STA/ICR/MASK bit 9), which only exists on F4 + * (stm32_sdio_family_error_flags). + */ +typedef struct +{ + volatile uint32_t POWER; /**< 0x00 Power control */ + volatile uint32_t CLKCR; /**< 0x04 Clock control */ + volatile uint32_t ARG; /**< 0x08 Argument */ + volatile uint32_t CMD; /**< 0x0C Command */ + volatile uint32_t RESPCMD; /**< 0x10 Command response index */ + volatile uint32_t RESP[4]; /**< 0x14-0x20 Response 1..4 */ + volatile uint32_t DTIMER; /**< 0x24 Data timer */ + volatile uint32_t DLEN; /**< 0x28 Data length */ + volatile uint32_t DCTRL; /**< 0x2C Data control */ + volatile uint32_t DCOUNT; /**< 0x30 Data counter */ + volatile uint32_t STA; /**< 0x34 Status */ + volatile uint32_t ICR; /**< 0x38 Interrupt clear */ + volatile uint32_t MASK; /**< 0x3C Interrupt mask */ + volatile uint32_t RESERVED0[2]; /**< 0x40-0x44 */ + volatile uint32_t FIFOCNT; /**< 0x48 FIFO counter */ + volatile uint32_t RESERVED1[13];/**< 0x4C-0x7C */ + volatile uint32_t FIFO; /**< 0x80 Data FIFO */ +} stm32_sdio_t; + +/* POWER */ +#define STM32_SDIO_POWER_OFF 0x0U +#define STM32_SDIO_POWER_ON 0x3U + +/* CLKCR */ +#define STM32_SDIO_CLKCR_CLKDIV_Msk 0xFFU +#define STM32_SDIO_CLKCR_CLKEN (1U << 8) +#define STM32_SDIO_CLKCR_BYPASS (1U << 10) +#define STM32_SDIO_CLKCR_WIDBUS_Msk (3U << 11) +#define STM32_SDIO_CLKCR_WIDBUS_4BIT (1U << 11) + +/* CMD */ +#define STM32_SDIO_CMD_INDEX_Msk 0x3FU +#define STM32_SDIO_CMD_WAITRESP_SHORT (1U << 6) +#define STM32_SDIO_CMD_WAITRESP_LONG (3U << 6) +#define STM32_SDIO_CMD_CPSMEN (1U << 10) + +/* DCTRL */ +#define STM32_SDIO_DCTRL_DTEN (1U << 0) +#define STM32_SDIO_DCTRL_DTDIR_READ (1U << 1) +#define STM32_SDIO_DCTRL_DMAEN (1U << 3) +#define STM32_SDIO_DCTRL_DBLOCKSIZE_Pos 4U + +/* STA / ICR / MASK */ +#define STM32_SDIO_STA_CCRCFAIL (1U << 0) +#define STM32_SDIO_STA_DCRCFAIL (1U << 1) +#define STM32_SDIO_STA_CTIMEOUT (1U << 2) +#define STM32_SDIO_STA_DTIMEOUT (1U << 3) +#define STM32_SDIO_STA_TXUNDERR (1U << 4) +#define STM32_SDIO_STA_RXOVERR (1U << 5) +#define STM32_SDIO_STA_CMDREND (1U << 6) +#define STM32_SDIO_STA_CMDSENT (1U << 7) +#define STM32_SDIO_STA_DATAEND (1U << 8) +#define STM32_SDIO_STA_STBITERR (1U << 9) /**< F4 only */ +#define STM32_SDIO_STA_DBCKEND (1U << 10) +#define STM32_SDIO_STA_CMDACT (1U << 11) +#define STM32_SDIO_STA_TXFIFOHE (1U << 14) +#define STM32_SDIO_STA_RXFIFOHF (1U << 15) +#define STM32_SDIO_STA_TXFIFOF (1U << 16) +#define STM32_SDIO_STA_RXFIFOF (1U << 17) +#define STM32_SDIO_STA_TXFIFOE (1U << 18) +#define STM32_SDIO_STA_RXDAVL (1U << 21) + +#define STM32_SDIO_CMD_FLAGS (STM32_SDIO_STA_CCRCFAIL | STM32_SDIO_STA_CTIMEOUT | \ + STM32_SDIO_STA_CMDREND | STM32_SDIO_STA_CMDSENT) +#define STM32_SDIO_DATA_ERRORS (STM32_SDIO_STA_DCRCFAIL | STM32_SDIO_STA_DTIMEOUT | \ + STM32_SDIO_STA_TXUNDERR | STM32_SDIO_STA_RXOVERR) +#define STM32_SDIO_STATIC_FLAGS (STM32_SDIO_CMD_FLAGS | STM32_SDIO_DATA_ERRORS | \ + STM32_SDIO_STA_DATAEND | STM32_SDIO_STA_DBCKEND) + +#define STM32_SDIO_FIFO_WORDS 32U +#define STM32_SDIO_FIFO_BYTES (STM32_SDIO_FIFO_WORDS * 4U) +/* DMA buffers: a 32-byte D-cache line (Cortex-M7) - which also keeps every + * 16-byte INC4 word burst inside a 1 KB boundary. */ +#define STM32_SDIO_DMA_ALIGNMENT 32U +#define STM32_SDIO_MAX_DATA_LENGTH 0x01FFFFFFU +#define STM32_SDIO_MAX_INSTANCES 2U + +/** RCC registers used by this port (same offsets on F4 and F7). */ +#define STM32_RCC_BASE 0x40023800UL +#define STM32_RCC_APB2RSTR (*(volatile uint32_t *)(STM32_RCC_BASE + 0x24UL)) +#define STM32_RCC_APB2ENR (*(volatile uint32_t *)(STM32_RCC_BASE + 0x44UL)) + +/** + * @brief Per-instance hardware description, provided by each family's port.c. + */ +typedef struct +{ + uint32_t base; /**< Peripheral base address */ + uint32_t apb2_bit; /**< Bit in RCC_APB2ENR / RCC_APB2RSTR */ + uint32_t irqn; /**< NVIC IRQ number */ + uint8_t dma_controller; /**< dmdma controller (1 = DMA2) */ + uint8_t dma_streams[2]; /**< Preferred and alternate stream */ + uint8_t dma_channel; /**< DMA request channel (CHSEL) */ +} stm32_sdio_instance_desc_t; + +/** Instances of this family, indexed by (instance - 1). Defined in /port.c. */ +extern const stm32_sdio_instance_desc_t stm32_sdio_instances[]; +extern const uint8_t stm32_sdio_instance_count; + +/** Family-only data error flags (STBITERR on F4, 0 on F7). Defined in /port.c. */ +extern const uint32_t stm32_sdio_family_error_flags; + +/** Whether DMA2 can reach [address, address + length) - e.g. not F4 CCM RAM. Defined in /port.c. */ +bool stm32_sdio_family_dma_reachable(const void* address, size_t length); + +/** Whether the core has a data cache (Cortex-M7: true, Cortex-M4: false). Defined in /port.c. */ +extern const bool stm32_sdio_family_has_dcache; + +/** + * @brief Per-instance runtime state (shared by stm32_common.c and stm32_data.c). + */ +typedef struct +{ + bool initialized; + dmosi_semaphore_t done; /* posted per finished phase (SDIO IRQ, DMA callback) */ + uint32_t clock_hz; /* current SDIO_CK */ + dmdma_lease_t dma; /* leased DMA2 stream */ + volatile bool cmd_busy; + volatile uint32_t cmd_status; + volatile bool data_busy; /* DPSM running (until DATAEND or a data error) */ + volatile uint32_t data_status; + volatile bool dma_busy; /* DMA stream running */ + volatile uint32_t dma_event; + bool data_read; + bool data_dma; /* data phase uses DMA (else: FIFO read at DATAEND) */ + uint32_t dctrl; /* DCTRL value of the armed data phase */ + void* dma_buffer; /* buffer of the running DMA transfer */ + uint32_t dma_length; /* its length in bytes */ + uint32_t* cursor; /* FIFO-only reads: destination */ + uint32_t words_left; +} stm32_sdio_state_t; + +/* --- stm32_common.c helpers used by stm32_data.c --- */ +volatile stm32_sdio_t* stm32_sdio_regs(dmsdio_instance_t instance); +uint32_t stm32_sdio_data_error_flags(void); +void stm32_sdio_mask_enable(volatile stm32_sdio_t* regs, uint32_t bits); +bool stm32_sdio_wait(stm32_sdio_state_t* st, volatile bool* busy, uint32_t timeout_ms); + +/* --- stm32_data.c: data path (DMA for block data, FIFO for small reads) --- */ +int stm32_sdio_data_init(const stm32_sdio_instance_desc_t* desc, stm32_sdio_state_t* st); +void stm32_sdio_data_deinit(stm32_sdio_state_t* st); +dmsdio_status_t stm32_sdio_data_arm(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, + const stm32_sdio_instance_desc_t* desc, const dmsdio_data_t* data); +dmsdio_status_t stm32_sdio_data_run(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, const dmsdio_data_t* data); +void stm32_sdio_data_irq(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, uint32_t sta); +void stm32_sdio_data_abort(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st); + +/** Reset the shared per-instance state; called from each family's dmod_init()/dmod_deinit(). */ +void stm32_sdio_common_init(void); +void stm32_sdio_common_deinit(void); + +/** Shared ISR body, called from each family's DMOD_IRQ_HANDLER. */ +void stm32_sdio_irq_handler(dmsdio_instance_t instance); + +#endif /* STM32_COMMON_H */ diff --git a/src/port/stm32_common/stm32_data.c b/src/port/stm32_common/stm32_data.c new file mode 100644 index 0000000..30a4cb7 --- /dev/null +++ b/src/port/stm32_common/stm32_data.c @@ -0,0 +1,388 @@ +/* No DMOD_ENABLE_REGISTRATION here - see stm32_common.c. */ +#include "dmod.h" +#include "dmosi.h" +#include "dmdma.h" +#include "dmheap.h" +#include "stm32_common.h" +#include + +/* + * SDIO/SDMMC data path. + * + * Block data moves by DMA: a DMA2 stream leased from dmdma (RM0090 table 43 / + * RM0385 table 27: stream 3 or 6, channel 4) in FIFO mode with a full + * threshold, 4-beat word bursts on both sides and the SDIO as flow + * controller (RM0090 31.3.2 "SDIO APB2 interface", RM0385 35.3.2) - the + * setup the host needs, since it issues burst requests and ends the + * transfer itself once DLEN bytes moved. The CPU never touches the FIFO, so + * interrupt latency cannot overrun it. + * + * Reads that fit the 32-word FIFO (SCR, SD Status, CMD6 switch status) do + * not use DMA: the whole transfer is buffered by the FIFO and read out at + * DATAEND. + * + * D-cache (Cortex-M7): whenever SCB->CCR.DC is set, DMA buffers are cleaned + * before a write, cleaned+invalidated before a read (so no dirty line can be + * evicted over the incoming data) and invalidated again after it (lines the + * CPU speculatively refilled meanwhile). DMA buffers are therefore required + * to start on and span whole 32-byte cache lines; with the cache disabled + * (or on Cortex-M4) nothing is done. + * + * Ordering: for reads the DMA stream and the DPSM are enabled before the + * command is sent; for writes the DMA stream is started before the command + * (it only fills the FIFO) and the DPSM is enabled after a valid response, + * so no data reaches the card before the command was accepted. + */ + +#define STM32_DMA_MAX_ELEMENTS 0xFFFFU +#define STM32_DMA_FINISH_SLACK_MS 50U +#define STM32_DATA_TIMEOUT_SLACK_MS 50U + +#define DMA_TERMINAL_EVENTS (dmdma_event_complete | dmdma_event_error | \ + dmdma_event_timeout | dmdma_event_aborted) + +/* ---- Cortex-M7 data cache maintenance by address (ARMv7-M SCB) ---- */ + +#define SCB_CCR (*(volatile uint32_t *)0xE000ED14UL) +#define SCB_CCR_DC (1UL << 16) +#define SCB_DCIMVAC (*(volatile uint32_t *)0xE000EF5CUL) /* invalidate */ +#define SCB_DCCMVAC (*(volatile uint32_t *)0xE000EF68UL) /* clean */ +#define SCB_DCCIMVAC (*(volatile uint32_t *)0xE000EF70UL) /* clean + invalidate */ +#define DCACHE_LINE 32U + +static bool dcache_enabled(void) +{ + return stm32_sdio_family_has_dcache && (SCB_CCR & SCB_CCR_DC) != 0U; +} + +static void dcache_by_address(volatile uint32_t* operation, const void* buffer, uint32_t length) +{ + if (!dcache_enabled() || length == 0U) + { + return; + } + uintptr_t line = (uintptr_t)buffer & ~(uintptr_t)(DCACHE_LINE - 1U); + uintptr_t end = (uintptr_t)buffer + length; + __asm__ volatile ("dsb" ::: "memory"); + for (; line < end; line += DCACHE_LINE) + { + *operation = (uint32_t)line; + } + __asm__ volatile ("dsb\n\tisb" ::: "memory"); +} + +/* Before the DMA starts: writes need the data in RAM, reads must not have + * dirty lines that could later be evicted over what the DMA wrote. */ +static void dcache_before_dma(stm32_sdio_state_t* st) +{ + dcache_by_address(st->data_read ? &SCB_DCCIMVAC : &SCB_DCCMVAC, st->dma_buffer, st->dma_length); +} + +/* After a read: drop lines the CPU may have speculatively refilled. */ +static void dcache_after_dma(stm32_sdio_state_t* st) +{ + if (st->data_read) + { + dcache_by_address(&SCB_DCIMVAC, st->dma_buffer, st->dma_length); + } +} + +/* dmdma lease callback - interrupt context (or synchronous from _abort). */ +static void dma_callback(dmdma_lease_t lease, dmdma_event_t event, void* user_ptr) +{ + (void)lease; + stm32_sdio_state_t* st = (stm32_sdio_state_t*)user_ptr; + if (!st->dma_busy || (event & DMA_TERMINAL_EVENTS) == 0) + { + return; + } + st->dma_event = (uint32_t)event; + st->dma_busy = false; + dmosi_semaphore_post(st->done, 1); +} + +int stm32_sdio_data_init(const stm32_sdio_instance_desc_t* desc, stm32_sdio_state_t* st) +{ + for (unsigned i = 0; i < 2 && st->dma == NULL; i++) + { + st->dma = dmdma_lease_acquire(desc->dma_controller, desc->dma_streams[i]); + } + if (st->dma == NULL) + { + DMOD_LOG_ERROR("dmsdio_port: DMA%u streams %u and %u are both unavailable\n", + (unsigned)desc->dma_controller + 1U, (unsigned)desc->dma_streams[0], + (unsigned)desc->dma_streams[1]); + return -EBUSY; + } + if (dmdma_lease_set_callback(st->dma, dma_callback, st) != 0) + { + stm32_sdio_data_deinit(st); + return -EIO; + } + return 0; +} + +void stm32_sdio_data_deinit(stm32_sdio_state_t* st) +{ + if (st->dma != NULL) + { + dmdma_lease_release(st->dma); + st->dma = NULL; + } +} + +static uint32_t block_size_code(uint32_t block_size) +{ + uint32_t code = 0; + while ((1U << code) < block_size && code < 14U) + { + code++; + } + return ((1U << code) == block_size) ? code : UINT32_MAX; +} + +static dmsdio_status_t validate(const dmsdio_data_t* data, uint32_t* code) +{ + uint64_t length = (uint64_t)data->block_size * data->block_count; + *code = block_size_code(data->block_size); + if (data->buffer == NULL || data->block_count == 0 || *code == UINT32_MAX || + length > STM32_SDIO_MAX_DATA_LENGTH || ((uintptr_t)data->buffer % 4U) != 0 || + (data->block_size % 4U) != 0) + { + return dmsdio_status_invalid; + } + return dmsdio_status_ok; +} + +static int start_dma(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, + const stm32_sdio_instance_desc_t* desc, const dmsdio_data_t* data, uint32_t length) +{ + void* fifo = (void*)(uintptr_t)®s->FIFO; + uint32_t words = length / 4U; + dmdma_transfer_config_t cfg = { + .direction = st->data_read ? dmdma_direction_peripheral_to_memory + : dmdma_direction_memory_to_peripheral, + .request = desc->dma_channel, + .source_address = st->data_read ? fifo : data->buffer, + .destination_address = st->data_read ? data->buffer : fifo, + .source_width = dmdma_data_width_word, + .destination_width = dmdma_data_width_word, + .source_increment = !st->data_read, + .destination_increment = st->data_read, + .circular = false, + .priority = dmdma_priority_very_high, + /* Ignored by the hardware: the SDIO is the flow controller. */ + .element_count = (words > STM32_DMA_MAX_ELEMENTS) ? STM32_DMA_MAX_ELEMENTS : words, + .timeout_ms = 0, + }; + dmdma_stream_options_t options = { + .flow_controller = dmdma_flow_controller_peripheral, + .fifo_threshold = dmdma_fifo_full, + .source_burst = dmdma_burst_4, + .destination_burst = dmdma_burst_4, + }; + st->dma_buffer = data->buffer; + st->dma_length = length; + dcache_before_dma(st); + st->dma_busy = true; + int rc = dmdma_lease_start_ex(st->dma, &cfg, &options); + if (rc != 0) + { + st->dma_busy = false; + DMOD_LOG_ERROR("dmsdio_port: DMA start failed (%d)\n", rc); + } + return rc; +} + +/* Whole cache lines only, so maintenance never touches neighbouring data. */ +static bool dma_capable(const void* buffer, size_t length) +{ + return ((uintptr_t)buffer % STM32_SDIO_DMA_ALIGNMENT) == 0 && + (length % STM32_SDIO_DMA_ALIGNMENT) == 0 && + stm32_sdio_family_dma_reachable(buffer, length); +} + +/* + * FMC/FSMC external memory (banks 1-6, e.g. the SDRAM dmod-boot puts module + * memory in on STM32F746G-DISCO). A DMA stream cannot read it fast enough to + * feed the host FIFO at 48 MHz / 4-bit (~24 MB/s): writes from there end in + * TXUNDERR - hardware flow control is off (see stm32_common.c), so nothing + * stops the card clock while the FIFO runs dry. Reads into it keep up. + */ +#define STM32_EXTERNAL_MEMORY_START 0x60000000UL +#define STM32_EXTERNAL_MEMORY_END 0xE0000000UL + +static bool in_external_memory(const void* buffer, size_t length) +{ + uintptr_t start = (uintptr_t)buffer; + return start < STM32_EXTERNAL_MEMORY_END && start + length > STM32_EXTERNAL_MEMORY_START; +} + +dmod_dmsdio_port_api_declaration(1.0, bool, _buffer_is_direct, + ( dmsdio_instance_t instance, const void* buffer, size_t length, dmsdio_direction_t direction )) +{ + (void)instance; + return buffer != NULL && dma_capable(buffer, length) && + (direction == dmsdio_direction_read || !in_external_memory(buffer, length)); +} + +/* + * Bounce buffers come from dmod-boot's "dma" heap (internal SRAM - DTCM on + * STM32F7, which DMA2 reaches through the AHBS port and the D-cache never + * holds), like dmeth's descriptor rings. Targets without that heap use the + * regular one, which is internal SRAM there. + */ +#define STM32_DMA_HEAP_NAME "dma" + +dmod_dmsdio_port_api_declaration(1.0, void*, _buffer_alloc, ( dmsdio_instance_t instance, size_t size )) +{ + (void)instance; + dmheap_context_t* heap = dmheap_get_context_by_name(STM32_DMA_HEAP_NAME); + return (heap != NULL) ? dmheap_aligned_alloc(heap, STM32_SDIO_DMA_ALIGNMENT, size, "dmsdio_port") + : Dmod_AlignedMalloc(size, STM32_SDIO_DMA_ALIGNMENT); +} + +dmod_dmsdio_port_api_declaration(1.0, void, _buffer_free, ( dmsdio_instance_t instance, void* buffer )) +{ + (void)instance; + if (buffer == NULL) + { + return; + } + dmheap_context_t* heap = dmheap_get_context_by_name(STM32_DMA_HEAP_NAME); + if (heap != NULL) + { + dmheap_free(heap, buffer, true); + } + else + { + Dmod_Free(buffer); + } +} + +dmsdio_status_t stm32_sdio_data_arm(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, + const stm32_sdio_instance_desc_t* desc, const dmsdio_data_t* data) +{ + uint32_t code = 0; + dmsdio_status_t status = validate(data, &code); + uint32_t length = data->block_size * data->block_count; + st->data_read = (data->direction == dmsdio_direction_read); + st->data_dma = !(st->data_read && length <= STM32_SDIO_FIFO_BYTES); + if (status == dmsdio_status_ok && st->data_dma && !dma_capable(data->buffer, length)) + { + DMOD_LOG_ERROR("dmsdio_port: buffer %p not usable for DMA\n", data->buffer); + status = dmsdio_status_invalid; + } + if (status != dmsdio_status_ok) + { + return status; + } + st->cursor = (uint32_t*)data->buffer; + st->words_left = length / 4U; + st->dctrl = STM32_SDIO_DCTRL_DTEN | (code << STM32_SDIO_DCTRL_DBLOCKSIZE_Pos) | + (st->data_read ? STM32_SDIO_DCTRL_DTDIR_READ : 0U) | + (st->data_dma ? STM32_SDIO_DCTRL_DMAEN : 0U); + uint64_t ticks = (uint64_t)data->timeout_ms * st->clock_hz / 1000U; + regs->DTIMER = (ticks > UINT32_MAX) ? UINT32_MAX : (uint32_t)ticks; + regs->DLEN = length; + if (st->data_dma && start_dma(regs, st, desc, data, length) != 0) + { + return dmsdio_status_overrun; + } + st->data_busy = true; + if (st->data_read) + { + regs->DCTRL = st->dctrl; + stm32_sdio_mask_enable(regs, STM32_SDIO_STA_DATAEND | stm32_sdio_data_error_flags()); + } + return dmsdio_status_ok; +} + +static dmsdio_status_t data_result(uint32_t sta) +{ + if (sta & STM32_SDIO_STA_DTIMEOUT) + { + return dmsdio_status_data_timeout; + } + if (sta & (STM32_SDIO_STA_DCRCFAIL | stm32_sdio_family_error_flags)) + { + return dmsdio_status_data_crc; + } + if (sta & (STM32_SDIO_STA_TXUNDERR | STM32_SDIO_STA_RXOVERR)) + { + return dmsdio_status_overrun; + } + return dmsdio_status_ok; +} + +/* After DATAEND the DMA flushes its FIFO to memory and reports completion. */ +static dmsdio_status_t finish_dma(stm32_sdio_state_t* st) +{ + if (!stm32_sdio_wait(st, &st->dma_busy, STM32_DMA_FINISH_SLACK_MS)) + { + DMOD_LOG_ERROR("dmsdio_port: DMA did not complete after DATAEND\n"); + return dmsdio_status_overrun; + } + dcache_after_dma(st); + return (st->dma_event & (dmdma_event_error | dmdma_event_timeout)) ? dmsdio_status_overrun + : dmsdio_status_ok; +} + +dmsdio_status_t stm32_sdio_data_run(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, const dmsdio_data_t* data) +{ + if (!st->data_read) + { + regs->DCTRL = st->dctrl; + stm32_sdio_mask_enable(regs, STM32_SDIO_STA_DATAEND | stm32_sdio_data_error_flags()); + } + uint64_t timeout = (uint64_t)data->timeout_ms * data->block_count + STM32_DATA_TIMEOUT_SLACK_MS; + if (!stm32_sdio_wait(st, &st->data_busy, (timeout > UINT32_MAX) ? UINT32_MAX : (uint32_t)timeout)) + { + return dmsdio_status_data_timeout; + } + dmsdio_status_t status = data_result(st->data_status); + if (status == dmsdio_status_ok && st->data_dma) + { + status = finish_dma(st); + } + if (status == dmsdio_status_ok && !st->data_dma && st->words_left != 0) + { + status = dmsdio_status_overrun; /* DATAEND without every word read */ + } + return status; +} + +/* SDIO interrupt, data part: DATAEND or a data error ends the DPSM phase. */ +void stm32_sdio_data_irq(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st, uint32_t sta) +{ + if (!st->data_busy || (sta & (STM32_SDIO_STA_DATAEND | stm32_sdio_data_error_flags())) == 0) + { + return; + } + while (!st->data_dma && st->words_left > 0 && (regs->STA & STM32_SDIO_STA_RXDAVL)) + { + *st->cursor++ = regs->FIFO; + st->words_left--; + } + regs->MASK &= ~(STM32_SDIO_STA_DATAEND | stm32_sdio_data_error_flags()); + regs->ICR = STM32_SDIO_STA_DATAEND | STM32_SDIO_STA_DBCKEND | stm32_sdio_data_error_flags(); + st->data_status = sta; + st->data_busy = false; + dmosi_semaphore_post(st->done, 1); +} + +void stm32_sdio_data_abort(volatile stm32_sdio_t* regs, stm32_sdio_state_t* st) +{ + regs->DCTRL = 0; /* stop the DPSM */ + if (st->dma != NULL) + { + dmdma_lease_abort(st->dma); /* no-op when idle */ + } + for (uint32_t i = 0; i < STM32_SDIO_FIFO_WORDS && (regs->STA & STM32_SDIO_STA_RXDAVL); i++) + { + (void)regs->FIFO; + } + st->data_busy = false; + st->dma_busy = false; + st->words_left = 0; +} diff --git a/src/port/stm32f4/config.cmake b/src/port/stm32f4/config.cmake new file mode 100644 index 0000000..2d4a3a6 --- /dev/null +++ b/src/port/stm32f4/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/armv7/cortex-m4" CACHE STRING "Name of the tools configuration") diff --git a/src/port/stm32f4/port.c b/src/port/stm32f4/port.c new file mode 100644 index 0000000..08b3310 --- /dev/null +++ b/src/port/stm32f4/port.c @@ -0,0 +1,54 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmsdio_port.h" +#include "dmod.h" +#include "../stm32_common/stm32_common.h" + +/* + * STM32F4 SDIO (RM0090 section 31). The host logic is shared with STM32F7 + * in stm32_common; this file only describes the F4 instance and IRQ. + */ + +/* ---- Family description ---- */ + +const stm32_sdio_instance_desc_t stm32_sdio_instances[] = +{ + /* SDIO: APB2 0x40012C00, RCC_APB2ENR.SDIOEN (bit 11), SDIO_IRQn 49, + * DMA2 stream 3 or 6, channel 4 (RM0090 table 43) */ + { 0x40012C00UL, 11U, 49U, 1U, { 3U, 6U }, 4U }, +}; +const uint8_t stm32_sdio_instance_count = sizeof(stm32_sdio_instances) / sizeof(stm32_sdio_instances[0]); + +/* STBITERR (start bit not detected on all data lines in wide bus mode) exists on F4 only. */ +const uint32_t stm32_sdio_family_error_flags = STM32_SDIO_STA_STBITERR; + +/* Cortex-M4: no data cache, nothing to maintain around DMA. */ +const bool stm32_sdio_family_has_dcache = false; + +/* The DMA controllers cannot access the 64 KB CCM data RAM (RM0090 2.3.1). */ +#define STM32F4_CCM_START 0x10000000UL +#define STM32F4_CCM_END 0x10010000UL + +bool stm32_sdio_family_dma_reachable(const void* address, size_t length) +{ + uintptr_t start = (uintptr_t)address; + return (start + length <= STM32F4_CCM_START) || (start >= STM32F4_CCM_END); +} + +/* ---- DMOD lifecycle ---- */ + +int dmod_init(const Dmod_Config_t *Config) +{ + (void)Config; + stm32_sdio_common_init(); + return 0; +} + +int dmod_deinit(void) +{ + stm32_sdio_common_deinit(); + return 0; +} + +/* ---- IRQ handlers ---- */ + +DMOD_IRQ_HANDLER(49) { stm32_sdio_irq_handler(1); } /* SDIO */ diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index b7bcafb..9a9ba13 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -1,29 +1,63 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmsdio_port.h" #include "dmod.h" +#include "../stm32_common/stm32_common.h" + +/* + * STM32F7 SDMMC (RM0385 section 35 / RM0410 section 35). The host logic is + * shared with STM32F4 in stm32_common; this file only describes the F7 + * instance and IRQ. + */ + +/* ---- Family description ---- */ + +const stm32_sdio_instance_desc_t stm32_sdio_instances[] = +{ + /* SDMMC1: APB2 0x40012C00, RCC_APB2ENR.SDMMC1EN (bit 11), SDMMC1_IRQn 49, + * DMA2 stream 3 or 6, channel 4 (RM0385 table 27) */ + { 0x40012C00UL, 11U, 49U, 1U, { 3U, 6U }, 4U }, + /* + * SDMMC2 (0x40011C00, APB2ENR bit 7, IRQ 103) exists only on F76x/F77x. + * Packages are built per CPU family, so registering its IRQ would make + * the loader warn on every F74x/F75x (IRQ table of 98 entries). Not + * supported until the port can be specialized per MCU. + */ +}; +const uint8_t stm32_sdio_instance_count = sizeof(stm32_sdio_instances) / sizeof(stm32_sdio_instances[0]); + +/* SDMMC has no STBITERR (STA bit 9 is reserved on F7). */ +const uint32_t stm32_sdio_family_error_flags = 0U; + +/* Cortex-M7: 32-byte-line D-cache. Maintenance is done whenever it is + * enabled (SCB->CCR.DC), see stm32_data.c. */ +const bool stm32_sdio_family_has_dcache = true; + +/* DMA2 reaches SRAM1/2, DTCM (through the AHBS port) and external memory - + * only ITCM RAM (0x00000000-0x00003FFF) is out of reach, and no data buffer + * lives there. */ +#define STM32F7_ITCM_END 0x00004000UL + +bool stm32_sdio_family_dma_reachable(const void* address, size_t length) +{ + (void)length; + return (uintptr_t)address >= STM32F7_ITCM_END; +} /* ---- DMOD lifecycle ---- */ int dmod_init(const Dmod_Config_t *Config) { - Dmod_Printf("dmsdio port module initialized (stm32f7)\n"); + (void)Config; + stm32_sdio_common_init(); return 0; } int dmod_deinit(void) { - Dmod_Printf("dmsdio port module deinitialized (stm32f7)\n"); + stm32_sdio_common_deinit(); 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 - * } - */ +/* ---- IRQ handlers ---- */ + +DMOD_IRQ_HANDLER(49) { stm32_sdio_irq_handler(1); } /* SDMMC1 */ diff --git a/src/port/x86_64/config.cmake b/src/port/x86_64/config.cmake new file mode 100644 index 0000000..5151164 --- /dev/null +++ b/src/port/x86_64/config.cmake @@ -0,0 +1,9 @@ +# 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 +# +# The x86_64 port is a simulated SD card used by the automated tests; it +# never ships as a release package. +set(DMOD_TOOLS_NAME "arch/x86_64" CACHE STRING "Name of the tools configuration") diff --git a/src/port/x86_64/dmsdio_mock.h b/src/port/x86_64/dmsdio_mock.h new file mode 100644 index 0000000..682c1d8 --- /dev/null +++ b/src/port/x86_64/dmsdio_mock.h @@ -0,0 +1,96 @@ +#ifndef DMSDIO_MOCK_H +#define DMSDIO_MOCK_H + +#include "dmsdio_port.h" + +/** + * @file dmsdio_mock.h + * @brief Control interface of the x86_64 mock SD card (tests only). + * + * The x86_64 dmsdio_port does not drive hardware: it simulates an SD + * memory card at the command level (card state machine, OCR/CID/CSD/SCR/ + * SD Status registers, CMD6 switch status, sparse block storage) so the + * complete protocol in the core module can be exercised on a CI runner. + * These functions exist only in the x86_64 port. + */ + +/** Maximum number of blocks the mock stores (sparse, written blocks only). */ +#define DMSDIO_MOCK_MAX_STORED_BLOCKS 4096u + +/** Injectable transport faults. */ +typedef enum +{ + dmsdio_mock_fault_none = 0, + dmsdio_mock_fault_cmd_timeout, /**< Command gets no response */ + dmsdio_mock_fault_cmd_crc, /**< Response CRC error */ + dmsdio_mock_fault_data_crc, /**< Data block CRC error */ + dmsdio_mock_fault_data_timeout, /**< Data phase times out */ + dmsdio_mock_fault_bad_index, /**< Response carries a wrong command index */ +} dmsdio_mock_fault_t; + +/** Use as cmd_index to match any command (except APP_CMD). */ +#define DMSDIO_MOCK_ANY_COMMAND 0xFFu + +/** Use as count for a fault that never expires. */ +#define DMSDIO_MOCK_FOREVER 0xFFFFFFFFu + +/** Counters observed by the mock. */ +typedef struct +{ + uint32_t commands[64]; /**< Plain commands by index */ + uint32_t app_commands[64]; /**< Application commands by index */ + uint64_t blocks_read; /**< 512-byte blocks read */ + uint64_t blocks_written; /**< 512-byte blocks written */ + uint32_t aborts; /**< _abort() calls */ + uint32_t faults_injected; /**< Faults delivered */ + uint32_t clock_hz; /**< Current bus clock */ + uint32_t max_clock_hz; /**< Highest clock programmed */ + dmsdio_bus_width_t bus_width; /**< Current host bus width */ + bool card_bus_4bit; /**< Card switched to 4-bit (ACMD6) */ + bool card_high_speed; /**< Card switched to High Speed (CMD6) */ + bool powered; /**< Card power */ + uint64_t last_address; /**< Last read/write command argument */ + uint32_t indirect_writes; /**< Write data phases from a buffer _buffer_is_direct() rejects */ +} dmsdio_mock_stats_t; + +/** Insert a fresh card of the given type (storage cleared). */ +dmod_dmsdio_port_api(1.0, int, _mock_insert, ( dmsdio_instance_t instance, dmsdio_card_type_t type )); + +/** Remove the card: every later command times out. */ +dmod_dmsdio_port_api(1.0, int, _mock_remove, ( dmsdio_instance_t instance )); + +/** Remove the card after `blocks` more data blocks have been transferred. */ +dmod_dmsdio_port_api(1.0, int, _mock_remove_after_blocks, ( dmsdio_instance_t instance, uint32_t blocks )); + +/** Deliver `fault` to the next `count` matching commands (cmd_index or DMSDIO_MOCK_ANY_COMMAND). */ +dmod_dmsdio_port_api(1.0, int, _mock_inject_fault, + ( dmsdio_instance_t instance, dmsdio_mock_fault_t fault, uint8_t cmd_index, uint32_t count )); + +/** Set the CSD temporary write protection bit of the inserted card. */ +dmod_dmsdio_port_api(1.0, int, _mock_set_write_protect, ( dmsdio_instance_t instance, bool enabled )); + +/** Let the card refuse the CMD6 High Speed switch (switch status reports 0xF). */ +dmod_dmsdio_port_api(1.0, int, _mock_refuse_high_speed, ( dmsdio_instance_t instance, bool refuse )); + +/** + * Model memory the data path cannot write from at full speed (like external + * SDRAM on STM32F7): while enabled, only buffers from _buffer_alloc() are + * direct for writes, and write data phases from anything else are counted + * in indirect_writes. + */ +dmod_dmsdio_port_api(1.0, int, _mock_set_slow_writes, ( dmsdio_instance_t instance, bool enabled )); + +/** Read and optionally clear the counters. */ +dmod_dmsdio_port_api(1.0, int, _mock_get_stats, ( dmsdio_instance_t instance, dmsdio_mock_stats_t* stats, bool reset )); + +/** + * Content of a block that was never written. Deterministic in all 64 bits of + * the block number so reads far beyond 4 GiB prove no address truncation. + */ +static inline uint8_t dmsdio_mock_pattern(uint64_t lba, uint32_t offset) +{ + uint64_t mix = lba ^ (lba >> 13) ^ (lba >> 27) ^ (lba >> 41); + return (uint8_t)(mix * 31u + offset * 7u + (lba >> 32)); +} + +#endif /* DMSDIO_MOCK_H */ diff --git a/src/port/x86_64/mock_card.c b/src/port/x86_64/mock_card.c new file mode 100644 index 0000000..9cf3fdb --- /dev/null +++ b/src/port/x86_64/mock_card.c @@ -0,0 +1,435 @@ +/* + * Command processing of the mock SD card. + */ +#include "mock_card.h" +#include "dmod.h" +#include + +#define R1_OUT_OF_RANGE (1u << 31) +#define R1_ADDRESS_ERROR (1u << 30) +#define R1_BLOCK_LEN_ERROR (1u << 29) +#define R1_ERASE_SEQ_ERROR (1u << 28) +#define R1_WP_VIOLATION (1u << 26) +#define R1_ILLEGAL_COMMAND (1u << 22) +#define R1_READY_FOR_DATA (1u << 8) +#define R1_APP_CMD (1u << 5) + +enum { ST_IDLE, ST_READY, ST_IDENT, ST_STBY, ST_TRAN, ST_DATA, ST_RCV, ST_PRG }; + +#define OCR_WINDOW 0x00FF8000u +#define OCR_CCS 0x40000000u +#define OCR_BUSY 0x80000000u +#define ERASE_MAX_BLOCKS 256u + +static bool is_high_capacity(const mock_host_t* host) +{ + return host->type == dmsdio_card_type_sdhc || host->type == dmsdio_card_type_sdxc; +} + +void mock_card_reset(mock_host_t* host) +{ + host->state = ST_IDLE; + host->rca = 0; + host->app_cmd = false; + host->hcs_seen = false; + host->acmd41_polls = 0; + host->card_4bit = false; + host->high_speed = false; + host->busy_polls = 0; + host->status_errors = 0; + host->stats.card_bus_4bit = false; + host->stats.card_high_speed = false; +} + +static uint32_t r1(const mock_host_t* host, uint32_t state) +{ + bool ready = (state == ST_TRAN && host->busy_polls == 0); + uint32_t reported = (state == ST_TRAN && host->busy_polls != 0) ? ST_PRG : state; + return host->status_errors | (reported << 9) | (ready ? R1_READY_FOR_DATA : 0u) | + (host->app_cmd ? R1_APP_CMD : 0u); +} + +static bool addressed(const mock_host_t* host, uint32_t arg) +{ + return (arg >> 16) == host->rca; +} + +/* ---- data phase helpers ---- */ + +static dmsdio_status_t data_check(mock_host_t* host, const dmsdio_data_t* data, + uint32_t block_size, uint32_t min_count) +{ + if (data == NULL || data->buffer == NULL || data->block_size != block_size || + data->block_count < min_count || data->timeout_ms == 0) + { + return dmsdio_status_invalid; + } + if (host->data_fault != dmsdio_mock_fault_none) + { + dmsdio_mock_fault_t fault = host->data_fault; + host->data_fault = dmsdio_mock_fault_none; + return (fault == dmsdio_mock_fault_data_crc) ? dmsdio_status_data_crc : dmsdio_status_data_timeout; + } + bool card_wide = host->card_4bit; + bool host_wide = (host->host_width == dmsdio_bus_width_4bit); + if (card_wide != host_wide) + { + return dmsdio_status_data_crc; /* lanes disagree: garbage on the bus */ + } + if (host->clock_hz > MOCK_DEFAULT_SPEED_HZ && !host->high_speed) + { + return dmsdio_status_data_crc; /* overclocked default-speed card */ + } + return dmsdio_status_ok; +} + +static dmsdio_status_t send_register(mock_host_t* host, const dmsdio_data_t* data, + const uint8_t* reg, uint32_t size) +{ + dmsdio_status_t st = data_check(host, data, size, 1); + if (st == dmsdio_status_ok) + { + memcpy(data->buffer, reg, size); + } + return st; +} + +/* Count a transferred block; returns false once the card was pulled. */ +static bool consume_block(mock_host_t* host) +{ + if (host->remove_after == 0) + { + host->inserted = false; + host->remove_after = -1; + return false; + } + if (host->remove_after > 0) + { + host->remove_after--; + } + return true; +} + +static bool to_lba(mock_host_t* host, uint32_t arg, uint32_t count, uint64_t* lba, uint32_t* error) +{ + host->stats.last_address = arg; + if (!is_high_capacity(host) && (arg % MOCK_BLOCK_SIZE) != 0) + { + *error = R1_ADDRESS_ERROR; + return false; + } + *lba = is_high_capacity(host) ? arg : (uint64_t)arg / MOCK_BLOCK_SIZE; + if (*lba + count > mock_card_capacity_blocks(host->type)) + { + *error = R1_OUT_OF_RANGE; + return false; + } + return true; +} + +/* ---- block commands ---- */ + +static dmsdio_status_t cmd_read(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + bool multi = (cmd->index == 18); + uint64_t lba = 0; + uint32_t error = 0; + resp->words[0] = r1(host, host->state); + host->state = multi ? ST_DATA : ST_TRAN; + dmsdio_status_t st = data_check(host, data, MOCK_BLOCK_SIZE, 1); + if (st != dmsdio_status_ok || (!multi && data->block_count != 1)) + { + return (st != dmsdio_status_ok) ? st : dmsdio_status_invalid; + } + if (!to_lba(host, cmd->argument, data->block_count, &lba, &error)) + { + resp->words[0] |= error; + return dmsdio_status_data_timeout; + } + for (uint32_t i = 0; i < data->block_count; i++) + { + if (!consume_block(host)) + { + return dmsdio_status_data_timeout; + } + mock_store_read(host, lba + i, (uint8_t*)data->buffer + (size_t)i * MOCK_BLOCK_SIZE); + host->stats.blocks_read++; + } + return dmsdio_status_ok; +} + +static dmsdio_status_t cmd_write(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + bool multi = (cmd->index == 25); + uint64_t lba = 0; + uint32_t error = host->write_protect ? R1_WP_VIOLATION : 0u; + resp->words[0] = r1(host, host->state); + host->state = multi ? ST_RCV : ST_TRAN; + dmsdio_status_t st = data_check(host, data, MOCK_BLOCK_SIZE, 1); + if (st != dmsdio_status_ok || (!multi && data->block_count != 1)) + { + return (st != dmsdio_status_ok) ? st : dmsdio_status_invalid; + } + if (error != 0 || !to_lba(host, cmd->argument, data->block_count, &lba, &error)) + { + resp->words[0] |= error; + return dmsdio_status_data_timeout; + } + host->busy_polls = 1; + for (uint32_t i = 0; i < data->block_count; i++) + { + if (!consume_block(host)) + { + return dmsdio_status_data_timeout; + } + const uint8_t* src = (const uint8_t*)data->buffer + (size_t)i * MOCK_BLOCK_SIZE; + if (mock_store_write(host, lba + i, src) != 0) + { + return dmsdio_status_data_crc; + } + host->stats.blocks_written++; + } + return dmsdio_status_ok; +} + +static dmsdio_status_t cmd_stop(mock_host_t* host, dmsdio_response_t* resp) +{ + if (host->state != ST_DATA && host->state != ST_RCV) + { + host->status_errors |= R1_ILLEGAL_COMMAND; + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state); + host->busy_polls = (host->state == ST_RCV) ? 1u : 0u; + host->state = ST_TRAN; + return dmsdio_status_ok; +} + +static dmsdio_status_t cmd_erase(mock_host_t* host, const dmsdio_command_t* cmd, dmsdio_response_t* resp) +{ + uint64_t lba = 0; + uint32_t error = 0; + resp->words[0] = r1(host, host->state); + if (cmd->index != 38) + { + uint64_t* target = (cmd->index == 32) ? &host->erase_start : &host->erase_end; + if (!to_lba(host, cmd->argument, 1, &lba, &error)) + { + resp->words[0] |= error; + return dmsdio_status_ok; + } + *target = lba; + return dmsdio_status_ok; + } + if (host->erase_end < host->erase_start || host->erase_end - host->erase_start >= ERASE_MAX_BLOCKS) + { + resp->words[0] |= R1_ERASE_SEQ_ERROR; + return dmsdio_status_ok; + } + uint8_t zero[MOCK_BLOCK_SIZE]; + memset(zero, 0, sizeof(zero)); + for (lba = host->erase_start; lba <= host->erase_end; lba++) + { + mock_store_write(host, lba, zero); + } + host->busy_polls = 1; + return dmsdio_status_ok; +} + +/* ---- identification commands ---- */ + +static dmsdio_status_t cmd_if_cond(mock_host_t* host, uint32_t arg, dmsdio_response_t* resp) +{ + if (host->type == dmsdio_card_type_sdsc_v1 || host->state != ST_IDLE || ((arg >> 8) & 0xFu) != 1u) + { + return dmsdio_status_cmd_timeout; /* v1 cards ignore CMD8 */ + } + resp->words[0] = arg & 0xFFFu; + return dmsdio_status_ok; +} + +static dmsdio_status_t acmd_op_cond(mock_host_t* host, uint32_t arg, dmsdio_response_t* resp) +{ + if (host->state != ST_IDLE && host->state != ST_READY) + { + return dmsdio_status_cmd_timeout; + } + host->hcs_seen = host->hcs_seen || (arg & OCR_CCS) != 0; + bool hc = is_high_capacity(host); + bool done = (++host->acmd41_polls > MOCK_ACMD41_BUSY_POLLS) && (!hc || host->hcs_seen); + resp->index = 0x3F; + resp->words[0] = OCR_WINDOW; + if (done) + { + host->state = ST_READY; + resp->words[0] |= OCR_BUSY | ((hc && host->hcs_seen) ? OCR_CCS : 0u); + } + return dmsdio_status_ok; +} + +static dmsdio_status_t cmd_ident(mock_host_t* host, const dmsdio_command_t* cmd, dmsdio_response_t* resp) +{ + if (cmd->index == 2 && host->state == ST_READY) + { + host->state = ST_IDENT; + resp->index = 0x3F; + mock_card_cid(host, resp->words); + return dmsdio_status_ok; + } + if (cmd->index == 3 && (host->state == ST_IDENT || host->state == ST_STBY)) + { + host->state = ST_STBY; + host->rca = (uint16_t)(0x1234u + host->type); + resp->words[0] = ((uint32_t)host->rca << 16) | (ST_STBY << 9); + return dmsdio_status_ok; + } + if (cmd->index == 9 && host->state == ST_STBY && addressed(host, cmd->argument)) + { + resp->index = 0x3F; + mock_card_csd(host, resp->words); + return dmsdio_status_ok; + } + return dmsdio_status_cmd_timeout; +} + +static dmsdio_status_t cmd_select(mock_host_t* host, uint32_t arg, dmsdio_response_t* resp) +{ + if (!addressed(host, arg) || (host->state != ST_STBY && host->state != ST_TRAN)) + { + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state); + host->state = ST_TRAN; + return dmsdio_status_ok; +} + +static dmsdio_status_t cmd_status(mock_host_t* host, uint32_t arg, dmsdio_response_t* resp) +{ + if (!addressed(host, arg) || host->state < ST_STBY) + { + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state); + host->status_errors = 0; + if (host->busy_polls > 0) + { + host->busy_polls--; + } + return dmsdio_status_ok; +} + +/* ---- transfer-state configuration commands ---- */ + +static dmsdio_status_t cmd_switch(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + uint8_t status[64]; + if (host->state != ST_TRAN || host->type == dmsdio_card_type_sdsc_v1) + { + host->status_errors |= R1_ILLEGAL_COMMAND; + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state); + mock_card_switch_status(host, cmd->argument, status); + host->stats.card_high_speed = host->high_speed; + return send_register(host, data, status, sizeof(status)); +} + +static dmsdio_status_t cmd_blocklen(mock_host_t* host, uint32_t arg, dmsdio_response_t* resp) +{ + if (host->state != ST_TRAN) + { + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state) | ((arg != MOCK_BLOCK_SIZE) ? R1_BLOCK_LEN_ERROR : 0u); + return dmsdio_status_ok; +} + +static dmsdio_status_t app_command(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + uint8_t reg[64]; + if (cmd->index == 41) + { + return acmd_op_cond(host, cmd->argument, resp); + } + if (host->state != ST_TRAN) + { + return dmsdio_status_cmd_timeout; + } + resp->words[0] = r1(host, host->state) | R1_APP_CMD; + switch (cmd->index) + { + case 6: + host->card_4bit = (cmd->argument & 0x3u) == 2u; + host->stats.card_bus_4bit = host->card_4bit; + return dmsdio_status_ok; + case 13: + mock_card_ssr(host, reg); + return send_register(host, data, reg, 64); + case 42: + return dmsdio_status_ok; + case 51: + mock_card_scr(host, reg); + return send_register(host, data, reg, 8); + default: + return dmsdio_status_cmd_timeout; + } +} + +static dmsdio_status_t command(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + switch (cmd->index) + { + case 0: mock_card_reset(host); return dmsdio_status_ok; + case 2: case 3: case 9: return cmd_ident(host, cmd, resp); + case 6: return cmd_switch(host, cmd, data, resp); + case 7: return cmd_select(host, cmd->argument, resp); + case 8: return cmd_if_cond(host, cmd->argument, resp); + case 12: return cmd_stop(host, resp); + case 13: return cmd_status(host, cmd->argument, resp); + case 16: return cmd_blocklen(host, cmd->argument, resp); + case 17: case 18: return (host->state == ST_TRAN) ? cmd_read(host, cmd, data, resp) : dmsdio_status_cmd_timeout; + case 24: case 25: return (host->state == ST_TRAN) ? cmd_write(host, cmd, data, resp) : dmsdio_status_cmd_timeout; + case 32: case 33: case 38: return (host->state == ST_TRAN) ? cmd_erase(host, cmd, resp) : dmsdio_status_cmd_timeout; + case 55: + if (host->state >= ST_STBY && !addressed(host, cmd->argument)) + { + return dmsdio_status_cmd_timeout; + } + host->app_cmd = true; + resp->words[0] = r1(host, host->state); + return dmsdio_status_ok; + default: + host->status_errors |= R1_ILLEGAL_COMMAND; + return dmsdio_status_cmd_timeout; + } +} + +dmsdio_status_t mock_card_execute(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + memset(resp, 0, sizeof(*resp)); + resp->index = cmd->index; + if (!host->inserted || !host->powered) + { + /* Nothing answers - which only shows for commands expecting a response. */ + return (cmd->response == dmsdio_response_none) ? dmsdio_status_ok : dmsdio_status_cmd_timeout; + } + if (host->state <= ST_IDENT && host->clock_hz > MOCK_IDENT_CLOCK_HZ) + { + return dmsdio_status_cmd_crc; /* identification must run at <= 400 kHz */ + } + bool app = host->app_cmd; + host->app_cmd = false; + if (app) + { + host->stats.app_commands[cmd->index & 63u]++; + return app_command(host, cmd, data, resp); + } + host->stats.commands[cmd->index & 63u]++; + return command(host, cmd, data, resp); +} diff --git a/src/port/x86_64/mock_card.h b/src/port/x86_64/mock_card.h new file mode 100644 index 0000000..ea6877d --- /dev/null +++ b/src/port/x86_64/mock_card.h @@ -0,0 +1,86 @@ +#ifndef DMSDIO_MOCK_CARD_H +#define DMSDIO_MOCK_CARD_H + +#include "dmsdio_mock.h" + +/* + * Command-level model of an SD memory card used by the x86_64 port. It + * follows the SD Physical Layer Simplified Specification closely enough to + * catch protocol mistakes in the core module: wrong ordering, wrong + * argument encoding, raising the clock before High Speed is active, + * mismatched bus widths or byte-vs-block addressing errors all fail. + */ + +#define MOCK_SOURCE_CLOCK_HZ 48000000u +#define MOCK_IDENT_CLOCK_HZ 400000u +#define MOCK_DEFAULT_SPEED_HZ 25000000u +#define MOCK_BLOCK_SIZE 512u +#define MOCK_ACMD41_BUSY_POLLS 2u + +typedef struct +{ + uint64_t lba; + uint8_t data[MOCK_BLOCK_SIZE]; +} mock_block_t; + +typedef struct +{ + /* host side */ + bool initialized; + uint32_t clock_hz; + dmsdio_bus_width_t host_width; + bool powered; + bool slow_writes; /* only port buffers are direct for writes */ + uint8_t* port_buffer; /* last _buffer_alloc() result */ + size_t port_buffer_size; + + /* card side */ + bool inserted; + dmsdio_card_type_t type; + uint32_t state; + uint16_t rca; + bool app_cmd; + bool hcs_seen; + uint32_t acmd41_polls; + bool card_4bit; + bool high_speed; + bool refuse_high_speed; + bool write_protect; + uint32_t busy_polls; + uint32_t status_errors; + uint64_t erase_start; + uint64_t erase_end; + + /* faults */ + dmsdio_mock_fault_t fault; + uint8_t fault_cmd; + uint32_t fault_count; + int64_t remove_after; /* -1 = disabled */ + dmsdio_mock_fault_t data_fault; /* armed for the next data phase */ + + /* storage (sparse) */ + mock_block_t* blocks; + uint32_t block_used; + + dmsdio_mock_stats_t stats; +} mock_host_t; + +/* registers */ +uint64_t mock_card_capacity_blocks(dmsdio_card_type_t type); +void mock_card_cid(const mock_host_t* host, uint32_t out[4]); +void mock_card_csd(const mock_host_t* host, uint32_t out[4]); +void mock_card_scr(const mock_host_t* host, uint8_t out[8]); +void mock_card_ssr(const mock_host_t* host, uint8_t out[64]); +void mock_card_switch_status(mock_host_t* host, uint32_t arg, uint8_t out[64]); + +/* storage */ +void mock_store_clear(mock_host_t* host); +void mock_store_read(const mock_host_t* host, uint64_t lba, uint8_t* out); +int mock_store_write(mock_host_t* host, uint64_t lba, const uint8_t* in); + +/* command processing */ +dmsdio_status_t mock_card_execute(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp); +void mock_card_reset(mock_host_t* host); + +#endif /* DMSDIO_MOCK_CARD_H */ diff --git a/src/port/x86_64/mock_registers.c b/src/port/x86_64/mock_registers.c new file mode 100644 index 0000000..bf8664a --- /dev/null +++ b/src/port/x86_64/mock_registers.c @@ -0,0 +1,185 @@ +/* + * Card registers of the mock SD card (CID, CSD, SCR, SD Status, CMD6 + * switch status) and its sparse block storage. + */ +#include "mock_card.h" +#include "dmod.h" +#include + +/* Set `width` bits ending at `msb` in a 128-bit register (MSW first). */ +static void reg128_set(uint32_t reg[4], unsigned msb, unsigned width, uint64_t value) +{ + unsigned lsb = msb + 1u - width; + for (unsigned i = 0; i < width; i++) + { + unsigned bit = lsb + i; + uint32_t mask = 1u << (bit % 32u); + if ((value >> i) & 1u) + { + reg[3u - bit / 32u] |= mask; + } + else + { + reg[3u - bit / 32u] &= ~mask; + } + } +} + +uint64_t mock_card_capacity_blocks(dmsdio_card_type_t type) +{ + switch (type) + { + case dmsdio_card_type_sdsc_v1: return 1ull << 21; /* 1 GiB */ + case dmsdio_card_type_sdsc_v2: return 1ull << 22; /* 2 GiB */ + case dmsdio_card_type_sdhc: return 1ull << 24; /* 8 GiB */ + case dmsdio_card_type_sdxc: return 1ull << 27; /* 64 GiB */ + default: return 0; + } +} + +void mock_card_cid(const mock_host_t* host, uint32_t out[4]) +{ + static const char name[5] = { 'D', 'M', 'S', 'D', '0' }; + memset(out, 0, 4 * sizeof(uint32_t)); + reg128_set(out, 127, 8, 0x03); /* MID */ + reg128_set(out, 119, 8, 'S'); /* OID */ + reg128_set(out, 111, 8, 'D'); + for (unsigned i = 0; i < 5; i++) + { + reg128_set(out, 103 - 8 * i, 8, (uint64_t)(name[i] + (i == 4 ? host->type : 0))); + } + reg128_set(out, 63, 8, 0x10); /* PRV 1.0 */ + reg128_set(out, 55, 32, 0xC0DE0000u + host->type); /* PSN */ + reg128_set(out, 19, 8, 24); /* MDT year 2024 */ + reg128_set(out, 11, 4, 9); /* MDT month 9 */ + reg128_set(out, 0, 1, 1); +} + +static void csd_common(const mock_host_t* host, uint32_t out[4]) +{ + reg128_set(out, 103, 8, 0x32); /* TRAN_SPEED 25 MHz */ + reg128_set(out, 95, 12, 0x5B5); /* CCC incl. classes 5, 10 */ + reg128_set(out, 46, 1, 1); /* ERASE_BLK_EN */ + reg128_set(out, 45, 7, 0x7F); /* SECTOR_SIZE */ + reg128_set(out, 12, 1, host->write_protect ? 1 : 0);/* TMP_WRITE_PROTECT */ + reg128_set(out, 0, 1, 1); +} + +void mock_card_csd(const mock_host_t* host, uint32_t out[4]) +{ + memset(out, 0, 4 * sizeof(uint32_t)); + csd_common(host, out); + if (host->type == dmsdio_card_type_sdsc_v1 || host->type == dmsdio_card_type_sdsc_v2) + { + /* CSD v1: (C_SIZE + 1) * 2^(C_SIZE_MULT + 2) * 2^READ_BL_LEN */ + uint32_t bl_len = (host->type == dmsdio_card_type_sdsc_v1) ? 9u : 10u; + reg128_set(out, 127, 2, 0); + reg128_set(out, 83, 4, bl_len); + reg128_set(out, 73, 12, 4095); + reg128_set(out, 49, 3, 7); + reg128_set(out, 25, 4, bl_len); + return; + } + /* CSD v2: (C_SIZE + 1) * 512 KiB */ + uint64_t c_size = mock_card_capacity_blocks(host->type) / 1024u - 1u; + reg128_set(out, 127, 2, 1); + reg128_set(out, 83, 4, 9); + reg128_set(out, 69, 22, c_size); + reg128_set(out, 25, 4, 9); +} + +void mock_card_scr(const mock_host_t* host, uint8_t out[8]) +{ + bool v1 = (host->type == dmsdio_card_type_sdsc_v1); + bool xc = (host->type == dmsdio_card_type_sdxc); + memset(out, 0, 8); + out[0] = v1 ? 0x00 : 0x02; /* SCR_STRUCTURE 0, SD_SPEC */ + out[1] = (uint8_t)((v1 ? 0x00 : 0x20) | 0x05); /* SD_SECURITY, SD_BUS_WIDTHS 1+4 */ + out[2] = xc ? 0x80 : 0x00; /* SD_SPEC3 */ + out[3] = xc ? 0x02 : 0x00; /* CMD_SUPPORT: CMD23 */ +} + +void mock_card_ssr(const mock_host_t* host, uint8_t out[64]) +{ + memset(out, 0, 64); + out[0] = host->card_4bit ? 0x80 : 0x00; /* DAT_BUS_WIDTH */ + out[8] = 0x04; /* SPEED_CLASS 10 */ + out[10] = 0x90; /* AU_SIZE 4 MiB */ + out[11] = 0x00; + out[12] = 0x01; /* ERASE_SIZE 1 AU */ + out[13] = (uint8_t)((1u << 2) | 1u); /* ERASE_TIMEOUT 1 s, OFFSET 1 s */ + out[24] = (host->type == dmsdio_card_type_sdxc) ? 0x02 : 0x00; /* DISCARD_SUPPORT */ +} + +void mock_card_switch_status(mock_host_t* host, uint32_t arg, uint8_t out[64]) +{ + uint32_t requested = arg & 0xFu; + bool set = (arg & 0x80000000u) != 0; + bool ok = (requested == 0xFu || requested <= 1u) && !(requested == 1u && host->refuse_high_speed); + uint32_t result = (requested == 0xFu) ? (host->high_speed ? 1u : 0u) : (ok ? requested : 0xFu); + + memset(out, 0, 64); + out[1] = 0x64; /* max current 100 mA */ + out[12] = 0x80; /* group 1 support: 0, 1, 15 */ + out[13] = host->refuse_high_speed ? 0x01 : 0x03; + out[16] = (uint8_t)(result & 0xFu); /* group 1 result */ + if (set && ok && requested != 0xFu) + { + host->high_speed = (requested == 1u); + } +} + +/* ---- sparse storage ---- */ + +void mock_store_clear(mock_host_t* host) +{ + Dmod_Free(host->blocks); + host->blocks = NULL; + host->block_used = 0; +} + +static mock_block_t* find_block(const mock_host_t* host, uint64_t lba) +{ + for (uint32_t i = 0; i < host->block_used; i++) + { + if (host->blocks[i].lba == lba) + { + return &host->blocks[i]; + } + } + return NULL; +} + +void mock_store_read(const mock_host_t* host, uint64_t lba, uint8_t* out) +{ + const mock_block_t* block = find_block(host, lba); + if (block != NULL) + { + memcpy(out, block->data, MOCK_BLOCK_SIZE); + return; + } + for (uint32_t i = 0; i < MOCK_BLOCK_SIZE; i++) + { + out[i] = dmsdio_mock_pattern(lba, i); + } +} + +int mock_store_write(mock_host_t* host, uint64_t lba, const uint8_t* in) +{ + mock_block_t* block = find_block(host, lba); + if (block == NULL) + { + if (host->blocks == NULL) + { + host->blocks = Dmod_Malloc(sizeof(mock_block_t) * DMSDIO_MOCK_MAX_STORED_BLOCKS); + } + if (host->blocks == NULL || host->block_used >= DMSDIO_MOCK_MAX_STORED_BLOCKS) + { + return -1; + } + block = &host->blocks[host->block_used++]; + block->lba = lba; + } + memcpy(block->data, in, MOCK_BLOCK_SIZE); + return 0; +} diff --git a/src/port/x86_64/port.c b/src/port/x86_64/port.c new file mode 100644 index 0000000..1b73417 --- /dev/null +++ b/src/port/x86_64/port.c @@ -0,0 +1,373 @@ +/** + * @file port.c + * @brief dmsdio_port for the x86_64 host: a simulated SD card. + * + * There is no SD host controller on a CI runner, so this port answers the + * core module's commands with a command-level SD card model (see + * mock_card.c). It implements the regular port API plus the test-only + * control API from dmsdio_mock.h (card insertion/removal, fault injection, + * counters). Nothing here decides protocol behavior - it only reacts to the + * commands the core sends, the way a real card would. + */ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmsdio_mock.h" +#include "mock_card.h" +#include "dmod.h" +#include +#include + +#define MOCK_MAX_INSTANCES 2u + +static mock_host_t g_hosts[MOCK_MAX_INSTANCES]; + +static mock_host_t* get_host(dmsdio_instance_t instance) +{ + if (instance < 1 || instance > MOCK_MAX_INSTANCES) + { + return NULL; + } + return &g_hosts[instance - 1]; +} + +static mock_host_t* get_ready_host(dmsdio_instance_t instance) +{ + mock_host_t* host = get_host(instance); + return (host != NULL && host->initialized) ? host : NULL; +} + +int dmod_init(const Dmod_Config_t *Config) +{ + (void)Config; + memset(g_hosts, 0, sizeof(g_hosts)); + for (unsigned i = 0; i < MOCK_MAX_INSTANCES; i++) + { + g_hosts[i].remove_after = -1; + } + return 0; +} + +int dmod_deinit(void) +{ + for (unsigned i = 0; i < MOCK_MAX_INSTANCES; i++) + { + mock_store_clear(&g_hosts[i]); + } + return 0; +} + +/* ---- port API ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _host_init, ( dmsdio_instance_t instance )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -ENODEV; + } + if (host->initialized) + { + return -EBUSY; + } + host->initialized = true; + host->host_width = dmsdio_bus_width_1bit; + host->clock_hz = 0; + host->powered = false; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _host_deinit, ( dmsdio_instance_t instance )) +{ + mock_host_t* host = get_ready_host(instance); + if (host == NULL) + { + return -ENODEV; + } + host->initialized = false; + host->powered = false; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_power, ( dmsdio_instance_t instance, bool on )) +{ + mock_host_t* host = get_ready_host(instance); + if (host == NULL) + { + return -ENODEV; + } + if (!on || !host->powered) + { + mock_card_reset(host); /* power loss resets the card */ + } + host->powered = on; + host->stats.powered = on; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_clock, ( dmsdio_instance_t instance, uint32_t max_hz, uint32_t* actual_hz )) +{ + mock_host_t* host = get_ready_host(instance); + if (host == NULL || max_hz == 0) + { + return (host == NULL) ? -ENODEV : -EINVAL; + } + /* Integer divider from a 48 MHz source, like SDMMC with CLK48. */ + uint32_t divider = (MOCK_SOURCE_CLOCK_HZ + max_hz - 1u) / max_hz; + host->clock_hz = MOCK_SOURCE_CLOCK_HZ / divider; + host->stats.clock_hz = host->clock_hz; + if (host->clock_hz > host->stats.max_clock_hz) + { + host->stats.max_clock_hz = host->clock_hz; + } + if (actual_hz != NULL) + { + *actual_hz = host->clock_hz; + } + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _set_bus_width, ( dmsdio_instance_t instance, dmsdio_bus_width_t width )) +{ + mock_host_t* host = get_ready_host(instance); + if (host == NULL) + { + return -ENODEV; + } + if (width != dmsdio_bus_width_1bit && width != dmsdio_bus_width_4bit) + { + return -EINVAL; + } + host->host_width = width; + host->stats.bus_width = width; + return 0; +} + +static bool fault_matches(const mock_host_t* host, const dmsdio_command_t* cmd, const dmsdio_data_t* data) +{ + if (host->fault == dmsdio_mock_fault_none || host->fault_count == 0 || host->app_cmd) + { + return false; /* never split an APP_CMD pair from its command */ + } + bool index_ok = (host->fault_cmd == DMSDIO_MOCK_ANY_COMMAND) ? (cmd->index != 55) + : (host->fault_cmd == cmd->index); + bool data_fault = (host->fault == dmsdio_mock_fault_data_crc || + host->fault == dmsdio_mock_fault_data_timeout); + return index_ok && (!data_fault || data != NULL); +} + +static dmsdio_status_t execute_with_fault(mock_host_t* host, const dmsdio_command_t* cmd, + const dmsdio_data_t* data, dmsdio_response_t* resp) +{ + dmsdio_mock_fault_t fault = host->fault; + if (host->fault_count != DMSDIO_MOCK_FOREVER) + { + host->fault_count--; + } + host->stats.faults_injected++; + switch (fault) + { + case dmsdio_mock_fault_cmd_timeout: + memset(resp, 0, sizeof(*resp)); + return dmsdio_status_cmd_timeout; + case dmsdio_mock_fault_cmd_crc: + memset(resp, 0, sizeof(*resp)); + return dmsdio_status_cmd_crc; + case dmsdio_mock_fault_bad_index: + { + dmsdio_status_t st = mock_card_execute(host, cmd, data, resp); + resp->index = (uint8_t)((cmd->index + 1u) & 0x3Fu); + return st; + } + default: + host->data_fault = fault; + return mock_card_execute(host, cmd, data, resp); + } +} + +/* ---- buffers ---- */ + +static bool in_port_buffer(const mock_host_t* host, const void* buffer, size_t length) +{ + const uint8_t* start = buffer; + return host->port_buffer != NULL && start >= host->port_buffer && + length <= host->port_buffer_size - (size_t)(start - host->port_buffer); +} + +static bool buffer_is_direct(const mock_host_t* host, const void* buffer, size_t length, + dmsdio_direction_t direction) +{ + bool aligned = ((uintptr_t)buffer % DMSDIO_TRANSFER_ALIGNMENT) == 0; + bool fast = direction == dmsdio_direction_read || !host->slow_writes || + in_port_buffer(host, buffer, length); + return buffer != NULL && aligned && fast; +} + +dmod_dmsdio_port_api_declaration(1.0, bool, _buffer_is_direct, + ( dmsdio_instance_t instance, const void* buffer, size_t length, dmsdio_direction_t direction )) +{ + mock_host_t* host = get_host(instance); + return host != NULL && buffer_is_direct(host, buffer, length, direction); +} + +dmod_dmsdio_port_api_declaration(1.0, void*, _buffer_alloc, ( dmsdio_instance_t instance, size_t size )) +{ + mock_host_t* host = get_host(instance); + void* buffer = (host != NULL) ? Dmod_AlignedMalloc(size, DMSDIO_TRANSFER_ALIGNMENT) : NULL; + if (buffer != NULL) + { + host->port_buffer = buffer; + host->port_buffer_size = size; + } + return buffer; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _buffer_free, ( dmsdio_instance_t instance, void* buffer )) +{ + mock_host_t* host = get_host(instance); + if (host != NULL && buffer != NULL && buffer == (void*)host->port_buffer) + { + host->port_buffer = NULL; + host->port_buffer_size = 0; + } + Dmod_Free(buffer); +} + +dmod_dmsdio_port_api_declaration(1.0, dmsdio_status_t, _execute, + ( dmsdio_instance_t instance, const dmsdio_command_t* command, + const dmsdio_data_t* data, dmsdio_response_t* response )) +{ + dmsdio_response_t scratch; + mock_host_t* host = get_ready_host(instance); + if (host == NULL || command == NULL || command->index > 63) + { + return dmsdio_status_invalid; + } + response = (response != NULL) ? response : &scratch; + if (data != NULL && data->direction == dmsdio_direction_write && + !buffer_is_direct(host, data->buffer, (size_t)data->block_size * data->block_count, data->direction)) + { + host->stats.indirect_writes++; + } + dmsdio_status_t st = fault_matches(host, command, data) + ? execute_with_fault(host, command, data, response) + : mock_card_execute(host, command, data, response); + host->data_fault = dmsdio_mock_fault_none; + return st; +} + +dmod_dmsdio_port_api_declaration(1.0, void, _abort, ( dmsdio_instance_t instance )) +{ + mock_host_t* host = get_ready_host(instance); + if (host != NULL) + { + host->stats.aborts++; + } +} + +/* ---- mock control API ---- */ + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_insert, ( dmsdio_instance_t instance, dmsdio_card_type_t type )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL || mock_card_capacity_blocks(type) == 0) + { + return -EINVAL; + } + mock_store_clear(host); + host->type = type; + host->inserted = true; + host->write_protect = false; + host->refuse_high_speed = false; + host->remove_after = -1; + mock_card_reset(host); + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_remove, ( dmsdio_instance_t instance )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->inserted = false; + host->remove_after = -1; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_remove_after_blocks, ( dmsdio_instance_t instance, uint32_t blocks )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->remove_after = blocks; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_inject_fault, + ( dmsdio_instance_t instance, dmsdio_mock_fault_t fault, uint8_t cmd_index, uint32_t count )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->fault = fault; + host->fault_cmd = cmd_index; + host->fault_count = (fault == dmsdio_mock_fault_none) ? 0u : count; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_set_write_protect, ( dmsdio_instance_t instance, bool enabled )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->write_protect = enabled; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_refuse_high_speed, ( dmsdio_instance_t instance, bool refuse )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->refuse_high_speed = refuse; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_set_slow_writes, ( dmsdio_instance_t instance, bool enabled )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL) + { + return -EINVAL; + } + host->slow_writes = enabled; + return 0; +} + +dmod_dmsdio_port_api_declaration(1.0, int, _mock_get_stats, ( dmsdio_instance_t instance, dmsdio_mock_stats_t* stats, bool reset )) +{ + mock_host_t* host = get_host(instance); + if (host == NULL || stats == NULL) + { + return -EINVAL; + } + *stats = host->stats; + if (reset) + { + memset(&host->stats, 0, sizeof(host->stats)); + host->stats.clock_hz = host->clock_hz; + host->stats.bus_width = host->host_width; + host->stats.powered = host->powered; + host->stats.card_bus_4bit = host->card_4bit; + host->stats.card_high_speed = host->high_speed; + } + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36b6e18..b3dc435 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,15 +4,26 @@ set(DMOD_AUTHOR_NAME "Patryk Kubiak") set(DMOD_STACK_SIZE 1024) set(DMOD_PRIORITY 0) +# The test stands in for dmdevfs: it implements dmdrvi's MAL +# (dmdrvi_device_available/_unavailable) to observe card hot-plug. +set(DMOD_MAL_IMPLS dmdrvi) + dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} dmsdio_test.c ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include + # Control interface of the simulated card (x86_64 dmsdio_port only) + ${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} + dmdrvi@>=2.1 + 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 diff --git a/tests/dmsdio_test.c b/tests/dmsdio_test.c index 7c4f660..9ff84b2 100644 --- a/tests/dmsdio_test.c +++ b/tests/dmsdio_test.c @@ -1,32 +1,898 @@ #define DMOD_ENABLE_REGISTRATION ON +#define ENABLE_DIF_REGISTRATIONS ON #include "dmod_test.h" +#include "dmdrvi.h" +#include "dmini.h" #include "dmsdio.h" +#include "dmsdio_mock.h" +#include +#include +#include -static dmsdio_t g_handle = NULL; +/* + * Mocked protocol tests. + * + * The x86_64 dmsdio_port simulates an SD card at the command level. The + * driver is used exactly the way dmdevfs uses it: its dmdrvi DIF functions + * are looked up at runtime by module name, and this test module implements + * the dmdrvi MAL (dmdrvi_device_available/_unavailable) in place of dmdevfs + * to observe hot-plug notifications, and calls the monitor ioctls itself in + * place of dmdevmon. + */ + +#define HOST 1 +#define GIB (1024ull * 1024ull * 1024ull) +#define BLK 512u + +typedef struct +{ + dmod_dmdrvi_create_t create; + dmod_dmdrvi_free_t free; + dmod_dmdrvi_open_t open; + dmod_dmdrvi_close_t close; + dmod_dmdrvi_read_t read; + dmod_dmdrvi_write_t write; + dmod_dmdrvi_ioctl_t ioctl; + dmod_dmdrvi_flush_t flush; + dmod_dmdrvi_stat_t stat; + dmod_dmdrvi_path_ready_t path_ready; +} driver_t; + +static driver_t g_drv; +static dmdrvi_context_t g_ctx; +static dmdrvi_dev_num_t g_host_num; +static void* g_host; +static void* g_card; +static volatile int g_available; +static volatile int g_unavailable; +static dmdrvi_dev_num_t g_announced; +/* Aligned for direct transfers; g_buf + 1 is the misaligned case. */ +static uint8_t g_buf[16 * BLK + 8] __attribute__((aligned(DMSDIO_TRANSFER_ALIGNMENT))); +static uint8_t g_ref[16 * BLK] __attribute__((aligned(DMSDIO_TRANSFER_ALIGNMENT))); + +/* ---- dmdrvi MAL (normally implemented by dmdevfs) ---- */ + +void dmdrvi_device_available(dmdrvi_context_t context, const dmdrvi_dev_num_t* dev_num) +{ + (void)context; + g_announced = *dev_num; + g_available++; +} + +void dmdrvi_device_unavailable(dmdrvi_context_t context, const dmdrvi_dev_num_t* dev_num) +{ + (void)context; + (void)dev_num; + g_unavailable++; +} + +/* ---- harness ---- */ + +static bool load_driver(void) +{ + Dmod_Context_t* module = Dmod_GetModuleContext("dmsdio"); + if (module == NULL) + { + return false; + } + g_drv.create = Dmod_GetDifFunction(module, dmod_dmdrvi_create_sig); + g_drv.free = Dmod_GetDifFunction(module, dmod_dmdrvi_free_sig); + g_drv.open = Dmod_GetDifFunction(module, dmod_dmdrvi_open_sig); + g_drv.close = Dmod_GetDifFunction(module, dmod_dmdrvi_close_sig); + g_drv.read = Dmod_GetDifFunction(module, dmod_dmdrvi_read_sig); + g_drv.write = Dmod_GetDifFunction(module, dmod_dmdrvi_write_sig); + g_drv.ioctl = Dmod_GetDifFunction(module, dmod_dmdrvi_ioctl_sig); + g_drv.flush = Dmod_GetDifFunction(module, dmod_dmdrvi_flush_sig); + g_drv.stat = Dmod_GetDifFunction(module, dmod_dmdrvi_stat_sig); + g_drv.path_ready = Dmod_GetDifFunction(module, dmod_dmdrvi_path_ready_sig); + return g_drv.create && g_drv.free && g_drv.open && g_drv.close && g_drv.read && + g_drv.write && g_drv.ioctl && g_drv.flush && g_drv.stat && g_drv.path_ready; +} + +static dmdrvi_dev_num_t card_num(void) +{ + dmdrvi_dev_num_t num; + memset(&num, 0, sizeof(num)); + num.flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR; + num.major = g_host_num.major; + num.minor = 0; + return num; +} + +static dmsdio_host_info_t host_info(void) +{ + dmsdio_host_info_t info; + memset(&info, 0, sizeof(info)); + g_drv.ioctl(g_ctx, g_host, dmsdio_ioctl_cmd_get_host_info, &info); + return info; +} + +static dmsdio_card_info_t card_info(void) +{ + dmsdio_card_info_t info; + memset(&info, 0, sizeof(info)); + g_drv.ioctl(g_ctx, g_host, dmsdio_ioctl_cmd_get_card_info, &info); + return info; +} + +static void open_card(void) +{ + dmdrvi_dev_num_t num = card_num(); + if (g_card != NULL) + { + g_drv.close(g_ctx, g_card); + } + g_card = g_drv.open(g_ctx, DMDRVI_O_RDWR, &num); +} + +static void stop_driver(void) +{ + if (g_ctx == NULL) + { + return; + } + if (g_card != NULL) + { + g_drv.close(g_ctx, g_card); + } + if (g_host != NULL) + { + g_drv.close(g_ctx, g_host); + } + g_drv.free(g_ctx); + g_ctx = NULL; + g_card = NULL; + g_host = NULL; +} + +/* (Re)create the driver context with extra ini lines, card type already inserted. */ +static void start_driver(const char* extra) +{ + char ini_text[512]; + stop_driver(); + Dmod_SnPrintf(ini_text, sizeof(ini_text), + "[dmsdio]\ninstance=%d\nbus_width=4\npoll_interval_ms=0\nretries=2\n%s", + HOST, extra != NULL ? extra : ""); + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, ini_text); + g_ctx = g_drv.create(ini, &g_host_num); + dmini_destroy(ini); + if (g_ctx == NULL) + { + return; + } + g_host = g_drv.open(g_ctx, DMDRVI_O_RDWR, &g_host_num); + /* What dmdevfs does once the host node is registered and mounted... */ + g_drv.path_ready(g_ctx, &g_host_num, "/dev/dmsdio0"); + /* ...and what dmdevmon does first when dmdevfs reported it. */ + g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_REFRESH, NULL); + open_card(); +} + +static int refresh(void) +{ + return g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_REFRESH, NULL); +} + +static int monitor_event(void) +{ + return g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_EVENT, NULL); +} + +/* Swap the inserted card for a fresh one of another type. */ +static void use_card(dmsdio_card_type_t type, const char* extra) +{ + dmsdio_port_mock_insert(HOST, type); + start_driver(extra); +} + +static dmdrvi_ssize_t card_read(void* buf, size_t size, uint64_t offset) +{ + return g_drv.read(g_ctx, g_card, buf, size, (dmdrvi_offset_t)offset); +} + +static dmdrvi_ssize_t card_write(const void* buf, size_t size, uint64_t offset) +{ + return g_drv.write(g_ctx, g_card, buf, size, (dmdrvi_offset_t)offset); +} + +static bool matches_pattern(const uint8_t* data, uint64_t offset, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + uint64_t at = offset + i; + if (data[i] != dmsdio_mock_pattern(at / BLK, (uint32_t)(at % BLK))) + { + return false; + } + } + return true; +} + +static bool bytes_equal(const uint8_t* a, const uint8_t* b, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + if (a[i] != b[i]) + { + return false; + } + } + return true; +} + +static void fill(uint8_t* data, size_t size, uint8_t seed) +{ + for (size_t i = 0; i < size; i++) + { + data[i] = (uint8_t)(seed + i * 13u); + } +} + +static dmsdio_mock_stats_t stats(bool reset) +{ + dmsdio_mock_stats_t s; + memset(&s, 0, sizeof(s)); + dmsdio_port_mock_get_stats(HOST, &s, reset); + return s; +} void dmod_test_setup(void) { - g_handle = dmsdio_create(); + static bool loaded = false; + if (!loaded) + { + loaded = load_driver(); + } + g_available = 0; + g_unavailable = 0; + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_none, 0, 0); + dmsdio_port_mock_set_slow_writes(HOST, false); + use_card(dmsdio_card_type_sdhc, NULL); + stats(true); } void dmod_test_teardown(void) { - dmsdio_destroy(g_handle); - g_handle = NULL; + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_none, 0, 0); + stop_driver(); +} + +/* ====================================================================== + * Identification + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_driver_is_discoverable_through_dmdrvi) +{ + DMOD_TEST_EXPECT_NOT_NULL(g_drv.create); + DMOD_TEST_EXPECT_NOT_NULL(g_ctx); + DMOD_TEST_EXPECT_NOT_NULL(g_host); + DMOD_TEST_EXPECT_EQ(g_host_num.flags, DMDRVI_NUM_MAJOR); + DMOD_TEST_EXPECT_EQ(g_host_num.major, 0); +} + +DMOD_TEST_STEP(dmsdio_announces_card_node) +{ + DMOD_TEST_EXPECT_EQ(g_available, 1); + DMOD_TEST_EXPECT_EQ(g_announced.flags, DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR); + DMOD_TEST_EXPECT_EQ(g_announced.major, 0); + DMOD_TEST_EXPECT_EQ(g_announced.minor, 0); + DMOD_TEST_EXPECT_NOT_NULL(g_card); +} + +DMOD_TEST_STEP(dmsdio_card_node_waits_for_host_registration) +{ + stop_driver(); + g_available = 0; + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=1\npoll_interval_ms=0\n"); + g_ctx = g_drv.create(ini, &g_host_num); + dmini_destroy(ini); + g_host = g_drv.open(g_ctx, DMDRVI_O_RDWR, &g_host_num); + /* identified, but dmdevfs has not registered the host node yet */ + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_TRUE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(g_available, 0); + dmdrvi_dev_num_t num = card_num(); + g_drv.path_ready(g_ctx, &num, "/dev/dmsdio0/0"); /* card node: ignored */ + g_drv.path_ready(g_ctx, &g_host_num, "/dev/dmsdio0"); + DMOD_TEST_EXPECT_EQ(g_available, 0); /* only REFRESH announces */ + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(g_available, 1); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(g_available, 1); /* announced once */ +} + +DMOD_TEST_STEP(dmsdio_identification_retries_transient_errors) +{ + dmsdio_port_mock_remove(HOST); + start_driver(NULL); + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdhc); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_cmd_crc, 2, 1); /* CMD2 */ + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(stats(false).faults_injected, 1u); + DMOD_TEST_EXPECT_EQ(card_info().type, dmsdio_card_type_sdhc); +} + +DMOD_TEST_STEP(dmsdio_identifies_sdsc_v1) +{ + use_card(dmsdio_card_type_sdsc_v1, NULL); + dmsdio_card_info_t info = card_info(); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_EQ(info.type, dmsdio_card_type_sdsc_v1); + DMOD_TEST_EXPECT_FALSE(info.block_addressing); + DMOD_TEST_EXPECT_EQ(info.capacity_bytes, 1ull * GIB); + DMOD_TEST_EXPECT_EQ(info.csd.structure, 0); + DMOD_TEST_EXPECT_EQ(info.bus_width, dmsdio_bus_width_4bit); + DMOD_TEST_EXPECT_FALSE(info.high_speed); /* SD 1.x has no CMD6 */ + DMOD_TEST_EXPECT_EQ(info.clock_hz, 24000000u); + DMOD_TEST_EXPECT_EQ(s.commands[16], 1u); /* SET_BLOCKLEN for SDSC */ + DMOD_TEST_EXPECT_EQ(s.commands[6], 0u); +} + +DMOD_TEST_STEP(dmsdio_identifies_sdsc_v2) +{ + use_card(dmsdio_card_type_sdsc_v2, NULL); + dmsdio_card_info_t info = card_info(); + DMOD_TEST_EXPECT_EQ(info.type, dmsdio_card_type_sdsc_v2); + DMOD_TEST_EXPECT_FALSE(info.block_addressing); + DMOD_TEST_EXPECT_EQ(info.capacity_bytes, 2ull * GIB); + DMOD_TEST_EXPECT_EQ(info.csd.read_bl_len, 10); + DMOD_TEST_EXPECT_TRUE(info.high_speed); + DMOD_TEST_EXPECT_EQ(info.clock_hz, 48000000u); + DMOD_TEST_EXPECT_EQ(info.cid.manufacturer_id, 0x03); + DMOD_TEST_EXPECT_EQ(strcmp(info.cid.oem_id, "SD"), 0); + DMOD_TEST_EXPECT_EQ(info.cid.manufacture_year, 2024); +} + +DMOD_TEST_STEP(dmsdio_identifies_sdhc) +{ + dmsdio_card_info_t info = card_info(); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_EQ(info.type, dmsdio_card_type_sdhc); + DMOD_TEST_EXPECT_TRUE(info.block_addressing); + DMOD_TEST_EXPECT_EQ(info.capacity_bytes, 8ull * GIB); + DMOD_TEST_EXPECT_EQ(info.csd.structure, 1); + DMOD_TEST_EXPECT_TRUE(info.high_speed); + DMOD_TEST_EXPECT_TRUE(s.card_high_speed); + DMOD_TEST_EXPECT_TRUE(s.card_bus_4bit); + DMOD_TEST_EXPECT_EQ(s.bus_width, dmsdio_bus_width_4bit); + DMOD_TEST_EXPECT_EQ(info.ssr.au_size_bytes, 4u * 1024u * 1024u); + DMOD_TEST_EXPECT_EQ(info.generation, host_info().generation); +} + +DMOD_TEST_STEP(dmsdio_identifies_sdxc_with_full_64bit_capacity) +{ + use_card(dmsdio_card_type_sdxc, NULL); + dmsdio_card_info_t info = card_info(); + dmdrvi_block_info_t block; + dmdrvi_stat_t st; + DMOD_TEST_EXPECT_EQ(info.type, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(info.capacity_bytes, 64ull * GIB); + DMOD_TEST_EXPECT_TRUE(info.scr.sd_spec3); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_GET_INFO, &block), 0); + DMOD_TEST_EXPECT_EQ(block.logical_block_size, BLK); + DMOD_TEST_EXPECT_EQ(block.block_count, 64ull * GIB / BLK); + DMOD_TEST_EXPECT_TRUE((block.flags & DMDRVI_BLOCK_FLAG_REMOVABLE) != 0); + DMOD_TEST_EXPECT_TRUE((block.flags & DMDRVI_BLOCK_FLAG_DISCARD_SUPPORTED) != 0); + DMOD_TEST_EXPECT_EQ(g_drv.stat(g_ctx, "/dev/dmsdio0/0", &st), 0); + DMOD_TEST_EXPECT_EQ(st.size, 64ull * GIB); + DMOD_TEST_EXPECT_EQ(g_drv.stat(g_ctx, "/dev/dmsdio0", &st), 0); + DMOD_TEST_EXPECT_EQ(st.size, 0u); +} + +DMOD_TEST_STEP(dmsdio_one_bit_bus_when_configured) +{ + use_card(dmsdio_card_type_sdhc, "bus_width=1\n"); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_EQ(card_info().bus_width, dmsdio_bus_width_1bit); + DMOD_TEST_EXPECT_FALSE(s.card_bus_4bit); + DMOD_TEST_EXPECT_EQ(s.app_commands[6], 0u); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +DMOD_TEST_STEP(dmsdio_default_speed_when_high_speed_refused) +{ + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdhc); + dmsdio_port_mock_refuse_high_speed(HOST, true); + start_driver(NULL); + dmsdio_card_info_t info = card_info(); + DMOD_TEST_EXPECT_EQ(info.type, dmsdio_card_type_sdhc); + DMOD_TEST_EXPECT_FALSE(info.high_speed); + DMOD_TEST_EXPECT_EQ(info.clock_hz, 24000000u); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +DMOD_TEST_STEP(dmsdio_clock_limit_is_respected) +{ + use_card(dmsdio_card_type_sdhc, "max_clock_hz=12000000\n"); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_FALSE(card_info().high_speed); + DMOD_TEST_EXPECT_TRUE(s.max_clock_hz <= 12000000u); +} + +DMOD_TEST_STEP(dmsdio_no_card_is_reported) +{ + dmsdio_port_mock_remove(HOST); + start_driver(NULL); + g_available = 0; + dmdrvi_stat_t st; + DMOD_TEST_EXPECT_NOT_NULL(g_ctx); + DMOD_TEST_EXPECT_NULL(g_card); + DMOD_TEST_EXPECT_FALSE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_drv.stat(g_ctx, "/dev/dmsdio0/0", &st), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_available, 0); + DMOD_TEST_EXPECT_EQ(host_info().last_error, 0); /* an empty slot is not an error */ + DMOD_TEST_EXPECT_FALSE(stats(false).powered); +} + +DMOD_TEST_STEP(dmsdio_malformed_identification_response_is_eproto) +{ + dmsdio_port_mock_remove(HOST); + start_driver(NULL); + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdhc); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_bad_index, 8, 1); + DMOD_TEST_EXPECT_EQ(refresh(), -EPROTO); + DMOD_TEST_EXPECT_EQ(host_info().last_error, -EPROTO); + DMOD_TEST_EXPECT_EQ(refresh(), 0); +} + +DMOD_TEST_STEP(dmsdio_reads_active_board_section) +{ + stop_driver(); + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, "[sd_card_detect]\ninstance=9\nmode=input\n" + "[sd_card]\ninstance=1\nmajor=3\npoll_interval_ms=0\n"); + dmini_set_active_section(ini, "sd_card", 0); + g_ctx = g_drv.create(ini, &g_host_num); + dmini_destroy(ini); + DMOD_TEST_EXPECT_NOT_NULL(g_ctx); + DMOD_TEST_EXPECT_EQ(g_host_num.major, 3); + g_host = g_drv.open(g_ctx, DMDRVI_O_RDWR, &g_host_num); + g_drv.path_ready(g_ctx, &g_host_num, "/dev/dmsdio3"); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(host_info().instance, 1); + DMOD_TEST_EXPECT_TRUE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(g_announced.major, 3); +} + +DMOD_TEST_STEP(dmsdio_rejects_invalid_configuration) +{ + stop_driver(); + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=1\nbus_width=8\n"); + dmdrvi_dev_num_t num; + DMOD_TEST_EXPECT_NULL(g_drv.create(ini, &num)); + dmini_destroy(ini); + + ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=7\nbus_width=4\n"); + DMOD_TEST_EXPECT_NULL(g_drv.create(ini, &num)); /* host acquisition fails */ + dmini_destroy(ini); + + ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=1\n" + "monitor_event_handler=a_handler_name_longer_than_31_chars\n"); + DMOD_TEST_EXPECT_NULL(g_drv.create(ini, &num)); + dmini_destroy(ini); +} + +/* ====================================================================== + * Data path + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_full_sector_round_trip_uses_multi_block) +{ + fill(g_ref, 8 * BLK, 0x11); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, 8 * BLK, 4096), (dmdrvi_ssize_t)(8 * BLK)); + memset(g_buf, 0, sizeof(g_buf)); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 8 * BLK, 4096), (dmdrvi_ssize_t)(8 * BLK)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf, g_ref, 8 * BLK)); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_EQ(s.commands[25], 1u); + DMOD_TEST_EXPECT_EQ(s.commands[18], 1u); + DMOD_TEST_EXPECT_EQ(s.commands[12], 2u); + DMOD_TEST_EXPECT_EQ(s.blocks_written, 8u); + DMOD_TEST_EXPECT_EQ(g_drv.flush(g_ctx, g_card), 0); +} + +DMOD_TEST_STEP(dmsdio_large_request_is_split) +{ + use_card(dmsdio_card_type_sdhc, "max_blocks_per_transfer=4\n"); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 10 * BLK, 0), (dmdrvi_ssize_t)(10 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, 0, 10 * BLK)); + dmsdio_mock_stats_t s = stats(false); + DMOD_TEST_EXPECT_EQ(s.commands[18], 3u); /* 4 + 4 + 2 blocks */ +} + +DMOD_TEST_STEP(dmsdio_unaligned_write_preserves_neighbours) +{ + const uint64_t offset = 1000; /* spans blocks 1 and 2 */ + fill(g_ref, 100, 0x5A); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, 100, offset), 100); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 4 * BLK, 0), (dmdrvi_ssize_t)(4 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, 0, offset)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf + offset, g_ref, 100)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf + offset + 100, offset + 100, 4 * BLK - offset - 100)); +} + +DMOD_TEST_STEP(dmsdio_unaligned_read_spanning_blocks) +{ + const uint64_t offset = 3 * BLK + 17; + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 3 * BLK, offset), (dmdrvi_ssize_t)(3 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, offset, 3 * BLK)); + /* misaligned destination buffer takes the bounce path */ + DMOD_TEST_EXPECT_EQ(card_read(g_buf + 1, 2 * BLK, 0), (dmdrvi_ssize_t)(2 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf + 1, 0, 2 * BLK)); +} + +DMOD_TEST_STEP(dmsdio_misaligned_buffer_bounces_multi_block) +{ + /* 16 blocks through the 8-block bounce buffer: two commands, not 16. */ + DMOD_TEST_EXPECT_EQ(card_read(g_buf + 1, 16 * BLK, 0), (dmdrvi_ssize_t)(16 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf + 1, 0, 16 * BLK)); + dmsdio_mock_stats_t s = stats(true); + DMOD_TEST_EXPECT_EQ(s.commands[18], 2u); + DMOD_TEST_EXPECT_EQ(s.commands[17], 0u); + + fill(g_buf + 1, 16 * BLK, 0x37); + DMOD_TEST_EXPECT_EQ(card_write(g_buf + 1, 16 * BLK, 32 * BLK), (dmdrvi_ssize_t)(16 * BLK)); + s = stats(true); + DMOD_TEST_EXPECT_EQ(s.commands[25], 2u); + DMOD_TEST_EXPECT_EQ(s.commands[24], 0u); + DMOD_TEST_EXPECT_EQ(card_read(g_ref, 16 * BLK, 32 * BLK), (dmdrvi_ssize_t)(16 * BLK)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_ref, g_buf + 1, 16 * BLK)); +} + +DMOD_TEST_STEP(dmsdio_writes_from_slow_memory_go_through_the_port_buffer) +{ + /* Like STM32F7 with the buffer in external SDRAM: the port only takes + * its own buffer for writes, reads stay direct. */ + dmsdio_port_mock_set_slow_writes(HOST, true); + fill(g_buf, 16 * BLK, 0x21); + DMOD_TEST_EXPECT_EQ(card_write(g_buf, 16 * BLK, 64 * BLK), (dmdrvi_ssize_t)(16 * BLK)); + fill(g_ref, 100, 0x5A); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, 100, 64 * BLK + 100), 100); /* inside one block */ + dmsdio_mock_stats_t s = stats(true); + DMOD_TEST_EXPECT_EQ(s.indirect_writes, 0u); + DMOD_TEST_EXPECT_EQ(s.commands[25], 2u); /* 8 + 8 blocks */ + DMOD_TEST_EXPECT_EQ(s.blocks_written, 17u); /* + the partial block */ + + DMOD_TEST_EXPECT_EQ(card_read(g_buf + BLK, 8 * BLK, 64 * BLK), (dmdrvi_ssize_t)(8 * BLK)); + s = stats(true); + DMOD_TEST_EXPECT_EQ(s.commands[18], 1u); /* reads stay direct */ + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf + BLK + 100, g_ref, 100)); + dmsdio_port_mock_set_slow_writes(HOST, false); +} + +DMOD_TEST_STEP(dmsdio_bounce_blocks_is_configurable) +{ + use_card(dmsdio_card_type_sdhc, "bounce_blocks=4\n"); + DMOD_TEST_EXPECT_NOT_NULL(g_ctx); + stats(true); + DMOD_TEST_EXPECT_EQ(card_read(g_buf + 1, 16 * BLK, 0), (dmdrvi_ssize_t)(16 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf + 1, 0, 16 * BLK)); + DMOD_TEST_EXPECT_EQ(stats(false).commands[18], 4u); + + use_card(dmsdio_card_type_sdhc, "bounce_blocks=0\n"); + DMOD_TEST_EXPECT_NULL(g_ctx); +} + +DMOD_TEST_STEP(dmsdio_offsets_above_4gib_are_not_truncated) +{ + use_card(dmsdio_card_type_sdxc, NULL); + const uint64_t offset = 5ull * GIB + 3 * BLK + 123; + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 2 * BLK, offset), (dmdrvi_ssize_t)(2 * BLK)); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, offset, 2 * BLK)); + + const uint64_t last = 64ull * GIB - BLK; + fill(g_ref, BLK, 0x77); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, BLK, last), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_EQ(stats(false).last_address, last / BLK); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, last), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf, g_ref, BLK)); } -DMOD_TEST_STEP(dmsdio_create) +DMOD_TEST_STEP(dmsdio_sdsc_uses_byte_addresses) { - DMOD_TEST_EXPECT_NOT_NULL(g_handle); + use_card(dmsdio_card_type_sdsc_v2, NULL); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 1000ull * BLK), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_EQ(stats(false).last_address, 1000ull * BLK); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, 1000ull * BLK, BLK)); } -DMOD_TEST_STEP(dmsdio_is_valid) +DMOD_TEST_STEP(dmsdio_end_of_device) { - DMOD_TEST_EXPECT_TRUE(dmsdio_is_valid(g_handle)); + const uint64_t end = 8ull * GIB; + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, end), 0); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 2 * BLK, end - 100), 100); + DMOD_TEST_EXPECT_EQ(card_write(g_buf, BLK, end), -ENOSPC); + DMOD_TEST_EXPECT_EQ(card_write(g_buf, 2 * BLK, end - BLK), (dmdrvi_ssize_t)BLK); } -DMOD_TEST_STEP(dmsdio_destroy_null) +DMOD_TEST_STEP(dmsdio_argument_validation) +{ + dmdrvi_dev_num_t num = card_num(); + void* ro = g_drv.open(g_ctx, DMDRVI_O_RDONLY, &num); + DMOD_TEST_EXPECT_EQ(g_drv.read(g_ctx, g_card, g_buf, BLK, -1), -EINVAL); + DMOD_TEST_EXPECT_EQ(g_drv.read(g_ctx, g_card, g_buf, (size_t)INT64_MAX + 1u, 0), -EOVERFLOW); + DMOD_TEST_EXPECT_EQ(g_drv.read(g_ctx, g_card, g_buf, 0, 0), 0); + DMOD_TEST_EXPECT_EQ(g_drv.write(g_ctx, ro, g_buf, BLK, 0), -EBADF); + DMOD_TEST_EXPECT_EQ(g_drv.read(g_ctx, g_host, g_buf, BLK, 0), -ENOTSUP); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_MONITOR_REFRESH, NULL), -ENOTTY); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_BLOCK_GET_INFO, g_buf), -ENOTTY); + g_drv.close(g_ctx, ro); +} + +/* ====================================================================== + * Erase / write protection + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_erase_and_discard) +{ + dmdrvi_block_range_t range = { 20 * BLK, 2 * BLK }; + dmdrvi_block_range_t bad = { 20 * BLK + 1, BLK }; + fill(g_ref, 2 * BLK, 0x42); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, 2 * BLK, 20 * BLK), (dmdrvi_ssize_t)(2 * BLK)); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_ERASE, &range), 0); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 2 * BLK, 20 * BLK), (dmdrvi_ssize_t)(2 * BLK)); + memset(g_ref, 0, 2 * BLK); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf, g_ref, 2 * BLK)); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_ERASE, &bad), -EINVAL); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_DISCARD, &range), -ENOTSUP); + + use_card(dmsdio_card_type_sdxc, NULL); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_DISCARD, &range), 0); +} + +DMOD_TEST_STEP(dmsdio_write_protected_card) +{ + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdhc); + dmsdio_port_mock_set_write_protect(HOST, true); + start_driver(NULL); + dmdrvi_block_info_t block; + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_BLOCK_GET_INFO, &block), 0); + DMOD_TEST_EXPECT_TRUE((block.flags & DMDRVI_BLOCK_FLAG_READ_ONLY) != 0); + DMOD_TEST_EXPECT_EQ(card_write(g_buf, BLK, 0), -EROFS); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_EQ(stats(false).commands[24], 0u); +} + +/* ====================================================================== + * Errors, retries and recovery + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_transient_crc_is_recovered_by_retry) { - /* Destroying NULL must not crash. */ - dmsdio_destroy(NULL); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_data_crc, 17, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_TRUE(matches_pattern(g_buf, 0, BLK)); + DMOD_TEST_EXPECT_EQ(host_info().retry_count, 1u); + DMOD_TEST_EXPECT_EQ(host_info().last_error, -EBADMSG); + DMOD_TEST_EXPECT_EQ(stats(false).faults_injected, 1u); +} + +DMOD_TEST_STEP(dmsdio_multi_block_write_retry_restores_transfer_state) +{ + fill(g_ref, 4 * BLK, 0x21); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_data_timeout, 25, 1); + DMOD_TEST_EXPECT_EQ(card_write(g_ref, 4 * BLK, 0), (dmdrvi_ssize_t)(4 * BLK)); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 4 * BLK, 0), (dmdrvi_ssize_t)(4 * BLK)); + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_buf, g_ref, 4 * BLK)); + DMOD_TEST_EXPECT_TRUE(stats(false).aborts >= 1u); +} + +DMOD_TEST_STEP(dmsdio_retry_exhaustion_returns_eio) +{ + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_data_crc, 17, DMSDIO_MOCK_FOREVER); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -EIO); + DMOD_TEST_EXPECT_EQ(stats(false).faults_injected, 3u); /* 1 + retries=2 */ + DMOD_TEST_EXPECT_TRUE(host_info().card_attached); +} + +DMOD_TEST_STEP(dmsdio_precise_errors_without_retries) +{ + use_card(dmsdio_card_type_sdhc, "retries=0\n"); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_data_crc, 17, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -EBADMSG); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_cmd_crc, 24, 1); + DMOD_TEST_EXPECT_EQ(card_write(g_buf, BLK, 0), -EBADMSG); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_cmd_timeout, 17, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -ETIMEDOUT); + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_data_timeout, 18, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 2 * BLK, 0), -ETIMEDOUT); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +DMOD_TEST_STEP(dmsdio_malformed_response_is_not_retried) +{ + dmsdio_port_mock_inject_fault(HOST, dmsdio_mock_fault_bad_index, 17, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -EPROTO); + DMOD_TEST_EXPECT_EQ(stats(false).faults_injected, 1u); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +DMOD_TEST_STEP(dmsdio_removal_during_transfer) +{ + void* old = g_card; + dmsdio_port_mock_remove_after_blocks(HOST, 2); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 8 * BLK, 0), -ENODEV); + DMOD_TEST_EXPECT_FALSE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(host_info().last_error, -ENODEV); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_drv.flush(g_ctx, old), -ENODEV); + dmdrvi_dev_num_t num = card_num(); + DMOD_TEST_EXPECT_NULL(g_drv.open(g_ctx, DMDRVI_O_RDWR, &num)); + /* The node is withdrawn by the next REFRESH, not by the failing transfer. */ + DMOD_TEST_EXPECT_EQ(g_unavailable, 0); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); +} + +DMOD_TEST_STEP(dmsdio_card_swapped_after_removal_during_transfer) +{ + /* Pulled mid-transfer, another card in before the monitor refreshes: + * the old node is withdrawn before the new card is announced. */ + dmsdio_port_mock_remove_after_blocks(HOST, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, 4 * BLK, 0), -ENODEV); + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); + DMOD_TEST_EXPECT_EQ(g_available, 2); + DMOD_TEST_EXPECT_EQ(card_info().type, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -ESTALE); +} + +DMOD_TEST_STEP(dmsdio_stale_handle_after_card_swap) +{ + uint32_t generation = host_info().generation; + dmsdio_port_mock_remove(HOST); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); + DMOD_TEST_EXPECT_EQ(g_available, 2); + DMOD_TEST_EXPECT_EQ(host_info().generation, generation + 2u); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), -ESTALE); + dmsdio_card_info_t info; + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, dmsdio_ioctl_cmd_get_card_info, &info), -ESTALE); + open_card(); + DMOD_TEST_EXPECT_NOT_NULL(g_card); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +DMOD_TEST_STEP(dmsdio_refresh_keeps_present_card) +{ + uint32_t generation = host_info().generation; + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_EQ(host_info().generation, generation); + DMOD_TEST_EXPECT_EQ(g_available, 1); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); +} + +/* ====================================================================== + * dmdrvi monitor contract (driven by dmdevmon) + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_create_leaves_the_card_to_the_monitor) +{ + /* No thread, no service, no bus traffic: the first REFRESH identifies. */ + stop_driver(); + g_available = 0; + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=1\n"); + g_ctx = g_drv.create(ini, &g_host_num); + dmini_destroy(ini); + g_host = g_drv.open(g_ctx, DMDRVI_O_RDWR, &g_host_num); + DMOD_TEST_EXPECT_FALSE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(host_info().scan_count, 0u); + g_drv.path_ready(g_ctx, &g_host_num, "/dev/dmsdio0"); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_TRUE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(g_available, 1); + Dmod_ThreadSleep(50); + DMOD_TEST_EXPECT_EQ(host_info().scan_count, 1u); /* nothing polls behind our back */ +} + +DMOD_TEST_STEP(dmsdio_monitor_policy_is_handed_out) +{ + use_card(dmsdio_card_type_sdhc, "monitor_event_handler=sd0_card_detect\n" + "monitor_settle_ms=20\npoll_interval_ms=250\n"); + dmdrvi_monitor_policy_t policy; + memset(&policy, 0xFF, sizeof(policy)); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_GET_POLICY, &policy), 0); + DMOD_TEST_EXPECT_EQ(strcmp(policy.event_handler, "sd0_card_detect"), 0); + DMOD_TEST_EXPECT_EQ(policy.settle_ms, 20u); + DMOD_TEST_EXPECT_EQ(policy.poll_interval_ms, 250u); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_GET_POLICY, NULL), -EINVAL); + + /* Defaults: no event handler, 50 ms settle time, polling every second. */ + stop_driver(); + dmini_context_t ini = dmini_create(); + dmini_parse_string(ini, "[dmsdio]\ninstance=1\n"); + g_ctx = g_drv.create(ini, &g_host_num); + dmini_destroy(ini); + g_host = g_drv.open(g_ctx, DMDRVI_O_RDWR, &g_host_num); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_host, DMDRVI_IOCTL_MONITOR_GET_POLICY, &policy), 0); + DMOD_TEST_EXPECT_EQ(policy.event_handler[0], '\0'); + DMOD_TEST_EXPECT_EQ(policy.settle_ms, 50u); + DMOD_TEST_EXPECT_EQ(policy.poll_interval_ms, 1000u); +} + +DMOD_TEST_STEP(dmsdio_card_node_is_not_monitored) +{ + /* dmdevfs reports only the host node as a "monitor" device. */ + dmdrvi_monitor_policy_t policy; + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_MONITOR_GET_POLICY, &policy), -ENOTTY); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_MONITOR_EVENT, NULL), -ENOTTY); + DMOD_TEST_EXPECT_EQ(g_drv.ioctl(g_ctx, g_card, DMDRVI_IOCTL_MONITOR_REFRESH, NULL), -ENOTTY); +} + +DMOD_TEST_STEP(dmsdio_event_without_card_detect_pin) +{ + /* Nothing to sample - the event leaves everything to REFRESH. */ + DMOD_TEST_EXPECT_EQ(monitor_event(), 0); + DMOD_TEST_EXPECT_EQ(card_read(g_buf, BLK, 0), (dmdrvi_ssize_t)BLK); + DMOD_TEST_EXPECT_EQ(g_unavailable, 0); +} + +DMOD_TEST_STEP(dmsdio_refresh_follows_removal_and_insertion) +{ + /* What dmdevmon does on every poll interval without a card detect pin. */ + dmsdio_port_mock_remove(HOST); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + DMOD_TEST_EXPECT_FALSE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); + DMOD_TEST_EXPECT_EQ(refresh(), -ENODEV); + DMOD_TEST_EXPECT_EQ(g_unavailable, 1); + dmsdio_port_mock_insert(HOST, dmsdio_card_type_sdxc); + DMOD_TEST_EXPECT_EQ(refresh(), 0); + DMOD_TEST_EXPECT_TRUE(host_info().card_attached); + DMOD_TEST_EXPECT_EQ(g_available, 2); + DMOD_TEST_EXPECT_EQ(card_info().type, dmsdio_card_type_sdxc); +} + +/* ====================================================================== + * Register decoders (public API) + * ====================================================================== */ + +DMOD_TEST_STEP(dmsdio_decode_csd_versions) +{ + /* CSD v2, C_SIZE = 0x3FFF -> 8 GiB */ + uint32_t v2[4] = { 0x400E0032u, 0x5B590000u, 0x3FFF7F80u, 0x0A400001u }; + uint32_t v3[4] = { 0x800E0032u, 0x5B590000u, 0x3FFF7F80u, 0x0A400001u }; + dmsdio_csd_t csd; + DMOD_TEST_EXPECT_EQ(dmsdio_decode_csd(v2, &csd), 0); + DMOD_TEST_EXPECT_EQ(csd.structure, 1); + DMOD_TEST_EXPECT_EQ(csd.capacity_bytes, 8ull * GIB); + DMOD_TEST_EXPECT_EQ(csd.ccc, 0x5B5); + DMOD_TEST_EXPECT_EQ(dmsdio_decode_csd(v3, &csd), -ENOTSUP); + DMOD_TEST_EXPECT_EQ(dmsdio_decode_csd(NULL, &csd), -EINVAL); +} + +DMOD_TEST_STEP(dmsdio_decode_scr_and_ssr) +{ + const uint8_t scr[8] = { 0x02, 0x35, 0x80, 0x02, 0, 0, 0, 0 }; + const uint8_t bad_scr[8] = { 0x12, 0x35, 0, 0, 0, 0, 0, 0 }; + uint8_t ssr_raw[64]; + dmsdio_scr_t s; + dmsdio_ssr_t ssr; + DMOD_TEST_EXPECT_EQ(dmsdio_decode_scr(scr, &s), 0); + DMOD_TEST_EXPECT_EQ(s.sd_spec, 2); + DMOD_TEST_EXPECT_TRUE(s.sd_spec3); + DMOD_TEST_EXPECT_TRUE(s.bus_width_4bit); + DMOD_TEST_EXPECT_TRUE(s.cmd23_supported); + DMOD_TEST_EXPECT_EQ(dmsdio_decode_scr(bad_scr, &s), -EPROTO); + + memset(ssr_raw, 0, sizeof(ssr_raw)); + ssr_raw[0] = 0x80; + ssr_raw[10] = 0xA0; /* AU_SIZE 0xA = 8 MiB */ + ssr_raw[24] = 0x02; /* DISCARD_SUPPORT */ + DMOD_TEST_EXPECT_EQ(dmsdio_decode_ssr(ssr_raw, &ssr), 0); + DMOD_TEST_EXPECT_EQ(ssr.bus_width_code, 2); + DMOD_TEST_EXPECT_EQ(ssr.au_size_bytes, 8u * 1024u * 1024u); + DMOD_TEST_EXPECT_TRUE(ssr.discard_supported); }