From 6fa1950b10c6b163fd48366d006a80d3fa4633c2 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Mon, 21 Sep 2026 18:58:36 +0200 Subject: [PATCH 01/10] first implementation --- CMakeLists.txt | 21 +- Makefile | 2 +- README.md | 112 ++- configs/README.md | 192 +++++ configs/board/nucleo-f401re/spi1.ini | 68 ++ configs/board/nucleo-f411re/spi1.ini | 69 ++ configs/board/nucleo-f446re/spi1.ini | 69 ++ configs/board/nucleo-f767zi/spi1.ini | 79 +++ configs/board/stm32f4-discovery/spi1.ini | 77 ++ configs/board/stm32f429i-discovery/spi5.ini | 68 ++ configs/board/stm32f746g-disco/spi2.ini | 74 ++ configs/board/stm32f769i-discovery/spi2.ini | 74 ++ configs/mcu/stm32f401re.ini | 8 + configs/mcu/stm32f405rg.ini | 8 + configs/mcu/stm32f407vg.ini | 8 + configs/mcu/stm32f411re.ini | 8 + configs/mcu/stm32f429zi.ini | 8 + configs/mcu/stm32f439zi.ini | 8 + configs/mcu/stm32f446re.ini | 8 + configs/mcu/stm32f469ni.ini | 8 + configs/mcu/stm32f722re.ini | 8 + configs/mcu/stm32f746zg.ini | 8 + configs/mcu/stm32f767zi.ini | 8 + configs/mcu/stm32f769ni.ini | 8 + dmspi.dmr | 4 + docs/README.md | 3 + docs/api-reference.md | 50 +- docs/configuration.md | 197 ++++++ docs/port-implementation.md | 58 +- examples/config.ini | 8 + include/dmspi.h | 58 +- include/dmspi_port.h | 89 ++- include/dmspi_types.h | 154 ++++ src/dmspi.c | 743 ++++++++++++++++++-- src/port/CMakeLists.txt | 19 +- src/port/stm32_common/stm32_common.c | 514 ++++++++++++++ src/port/stm32_common/stm32_common.h | 161 +++++ src/port/stm32f4/config.cmake | 6 + src/port/stm32f4/port.c | 40 ++ src/port/stm32f7/port.c | 33 +- tests/dmspi_test.c | 37 +- tools/spitest/CMakeLists.txt | 39 + tools/spitest/README.md | 72 ++ tools/spitest/main.c | 246 +++++++ tools/spitest/spitest.dmr | 30 + 45 files changed, 3383 insertions(+), 179 deletions(-) create mode 100644 configs/README.md create mode 100644 configs/board/nucleo-f401re/spi1.ini create mode 100644 configs/board/nucleo-f411re/spi1.ini create mode 100644 configs/board/nucleo-f446re/spi1.ini create mode 100644 configs/board/nucleo-f767zi/spi1.ini create mode 100644 configs/board/stm32f4-discovery/spi1.ini create mode 100644 configs/board/stm32f429i-discovery/spi5.ini create mode 100644 configs/board/stm32f746g-disco/spi2.ini create mode 100644 configs/board/stm32f769i-discovery/spi2.ini create mode 100644 configs/mcu/stm32f401re.ini create mode 100644 configs/mcu/stm32f405rg.ini create mode 100644 configs/mcu/stm32f407vg.ini create mode 100644 configs/mcu/stm32f411re.ini create mode 100644 configs/mcu/stm32f429zi.ini create mode 100644 configs/mcu/stm32f439zi.ini create mode 100644 configs/mcu/stm32f446re.ini create mode 100644 configs/mcu/stm32f469ni.ini create mode 100644 configs/mcu/stm32f722re.ini create mode 100644 configs/mcu/stm32f746zg.ini create mode 100644 configs/mcu/stm32f767zi.ini create mode 100644 configs/mcu/stm32f769ni.ini create mode 100644 docs/configuration.md create mode 100644 examples/config.ini create mode 100644 include/dmspi_types.h create mode 100644 src/port/stm32_common/stm32_common.c create mode 100644 src/port/stm32_common/stm32_common.h create mode 100644 src/port/stm32f4/config.cmake create mode 100644 src/port/stm32f4/port.c create mode 100644 tools/spitest/CMakeLists.txt create mode 100644 tools/spitest/README.md create mode 100644 tools/spitest/main.c create mode 100644 tools/spitest/spitest.dmr diff --git a/CMakeLists.txt b/CMakeLists.txt index fb3195b..03886cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,13 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} src/dmspi.c ) +dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi + dmini + dmhaman + dm_sw_ring +) + target_include_directories(${DMOD_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ) @@ -75,12 +82,16 @@ 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() add_subdirectory(tests) + +# ====================================================================== +# spitest Tool +# ====================================================================== +# Manual device test tool - flashed onto a real board to exercise a real +# master<->slave SPI link against an already-dmdevfs-configured device (see +# tools/spitest/README.md). Not part of the ctest suite: it needs real +# hardware, a dmdevfs config, and a human to wire it up. +add_subdirectory(tools/spitest) diff --git a/Makefile b/Makefile index b018683..1abce8f 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ DMOD_CXXSOURCES= DMOD_INC_DIRS=include # The list of libraries to link -DMOD_LIBS= +DMOD_LIBS=dmdrvi dmini dmhaman dm_sw_ring dmspi_port # The list of definitions DMOD_DEFINITIONS= diff --git a/README.md b/README.md index d082fde..a4c3e4a 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,32 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/choco-technologies/dmspi/actions/workflows/ci.yml/badge.svg)](https://github.com/choco-technologies/dmspi/actions/workflows/ci.yml) -dmspi DMOD library module. +SPI driver module for the DMOD ecosystem, supporting both master and slave +roles. ## Description -TODO: describe what this module does. +dmspi is a hardware driver (like `dmuart`/`dmgpio`): it registers with +`dmdrvi` and is used through the generic device API (create/open/read/ +write/ioctl/close), configured from an INI file via `dmini`. It supports: + +- Master and slave roles +- Configurable clock polarity/phase (CPOL/CPHA, or a `mode=0..3` shorthand) +- MSB-first or LSB-first bit order +- Software or hardware NSS (chip-select) management +- Polling or interrupt-driven operation, with an optional RX ring buffer + (useful for slave mode, where reception isn't driven by the local CPU) +- True full-duplex transfers via `dmspi_ioctl_cmd_transfer` + +Like `dmuart`/`dmgpio`, dmspi only configures the SPI peripheral itself - +pin muxing (SCK/MOSI/MISO/NSS alternate functions) is done externally via +`dmgpio`, and in `nss_mode=soft` chip-select is a plain GPIO the caller +toggles around each transaction. + +See [docs/configuration.md](docs/configuration.md) for the full +configuration reference and a usage example, and +[configs/](configs/README.md) for ready-made per-board/per-MCU INI files +(the same `dmdevfs`-loadable format `dmuart`/`dmgpio`/`dmclk` use). ## Building @@ -48,54 +69,76 @@ dmf-get install -d ${DMOD_DMF_DIR}/test_dmspi-local.dmd -y dmod_loader build/dmf/test_dmspi.dmf ``` -## Usage - - - -This library module provides functions that can be used by other modules: - -```c -#include "dmspi.h" -``` - -## API +This exercises hardware-independent logic only (safe to run on the host). +For a real master<->slave SPI link on actual hardware, see +[tools/spitest/](tools/spitest/) - a small CLI tool, flashed onto a board +and run from the `dmell` shell, that opens an already-`dmdevfs`-configured +device by path (see [configs/](configs/README.md)) and exercises it. It +supports both a single-board loopback (two devices wired together) and a +two-board test (one board master, one board slave). -| Function | Description | -|----------|-------------| -| `dmspi_create()` | Create a new `dmspi_t` instance. | -| `dmspi_destroy()` | Destroy an instance created by `_create()`. | -| `dmspi_is_valid()` | Check whether a handle is a valid instance. | +## Usage -See [include/dmspi.h](include/dmspi.h) for the full -declarations and [docs/api-reference.md](docs/api-reference.md) for the -complete reference. +The normal way to use a dmspi device is through `dmdevfs`, like any other +file: open the path `dmdevfs` created for it +(`Dmod_FileOpen("/dev/dmspi1", "r+")`), then `Dmod_Ioctl`/`Dmod_FileClose` +(portable SAL wrappers declared in `dmod.h`, no extra module to link) - see +[tools/spitest/main.c](tools/spitest/main.c) for a complete, build-tested +example. + +Writing your own driver-adjacent code that talks to dmspi *without* going +through a mounted path is also possible, but lower-level: dmspi is a +separate loadable module, so its `dmdrvi` entry points +(`dmdrvi_dmspi_create`, `_open`, `_ioctl`, ...) must be resolved dynamically +via `Dmod_GetDifFunction()` rather than called by name - see +[docs/configuration.md](docs/configuration.md) for why and how. See +[docs/api-reference.md](docs/api-reference.md) for the complete API +reference. ## Documentation See the `docs/` directory: - **[api-reference.md](docs/api-reference.md)** - Complete API documentation +- **[configuration.md](docs/configuration.md)** - INI configuration reference and usage example +- **[port-implementation.md](docs/port-implementation.md)** - How the port is structured and how to add another architecture View documentation using `dmf-man dmspi`. +## Configuration Files + +[configs/](configs/README.md) has ready-made, `dmdevfs`-loadable INI files +per board (SPI pins muxed via `dmgpio` + the `dmspi` section itself) and per +MCU (pin-agnostic defaults) - the same layout and mechanism `dmuart`, +`dmgpio`, and `dmclk` use. + ## Hardware Port This module ships two DMOD modules: the architecture-independent `dmspi` and `dmspi_port`, which contains the -architecture-specific implementation. The active architecture is selected via +architecture-specific implementation. `stm32f4` and `stm32f7` are supported +today, sharing a common SPI register implementation (both families use the +same SPI IP block). The active architecture is selected via `DMOD_CPU_FAMILY` (default: `stm32f7`): ```bash -cmake .. -DDMOD_CPU_FAMILY=stm32f7 +cmake .. -DDMOD_CPU_FAMILY=stm32f7 # or stm32f4 ``` -See [docs/port-implementation.md](docs/port-implementation.md) for how to add -another architecture. Port-specific files: +See [docs/port-implementation.md](docs/port-implementation.md) for how the +common/per-family split works and how to add another architecture. +Port-specific files: ``` ├── include/dmspi_port.h ├── src/port/ │ ├── CMakeLists.txt +│ ├── stm32_common/ # shared STM32 SPI register implementation +│ │ ├── stm32_common.h +│ │ └── stm32_common.c +│ ├── stm32f4/ +│ │ ├── config.cmake +│ │ └── port.c │ └── stm32f7/ │ ├── config.cmake │ └── port.c @@ -105,17 +148,32 @@ another architecture. Port-specific files: ``` dmspi/ +├── configs/ # per-board/per-MCU INI files - see "Configuration Files" above +│ ├── board/ +│ └── mcu/ ├── docs/ # Documentation (markdown format) +├── examples/ +│ └── config.ini # minimal, board-agnostic sample ├── include/ # Public headers -│ └── dmspi.h +│ ├── dmspi.h +│ ├── dmspi_port.h +│ └── dmspi_types.h ├── src/ -│ └── dmspi.c +│ ├── dmspi.c +│ └── port/ # see "Hardware Port" above ├── tests/ │ ├── CMakeLists.txt │ └── dmspi_test.c +├── tools/ +│ └── spitest/ # hardware self-test - see "Testing" above +│ ├── CMakeLists.txt +│ ├── main.c +│ ├── README.md +│ └── spitest.dmr ├── CMakeLists.txt ├── Makefile ├── dmspi.dmr +├── dmspi_port.dmr └── manifest.dmm ``` diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..08ae07f --- /dev/null +++ b/configs/README.md @@ -0,0 +1,192 @@ +# DMSPI Configuration Files + +This directory contains pre-configured SPI settings for various development +boards and MCUs, following the same layout as `dmuart`/`dmgpio`/`dmclk`. +These are what `dmdevfs` (or `dmf-get --config-dir`/`--config-map`) actually +loads to bring the driver up dynamically - not throwaway samples. + +**Every pin assignment below is sourced from that board's own ST user +manual, schematic, or a working third-party board config (Zephyr's device +tree) - never assumed by analogy with another board.** Two boards +(F746G-DISCO, F769I-DISCOVERY) turned out to route their Arduino SPI header +through **SPI2** on completely different pins than the "obvious" SPI1 +PA5/PA6/PA7 that works on the Nucleo-64 boards, because their larger pin +count means PA5-PA7 are already claimed by USB HS ULPI/DCMI/Ethernet - a +concrete example of why this isn't guessable per-family. Each file's +comments cite the specific source and, for the accelerometer/gyroscope +boards, flag which detail (usually just the exact CS pin) came from a +secondary source rather than the primary schematic. Always cross-check +against your own board revision before flashing. + +## Directory Structure + +``` +configs/ +├── board/ # Board-specific configurations +│ ├── nucleo-f401re/ +│ │ └── spi1.ini +│ ├── nucleo-f411re/ +│ │ └── spi1.ini +│ ├── nucleo-f446re/ +│ │ └── spi1.ini +│ ├── nucleo-f767zi/ +│ │ └── spi1.ini +│ ├── stm32f4-discovery/ +│ │ └── spi1.ini +│ ├── stm32f429i-discovery/ +│ │ └── spi5.ini +│ ├── stm32f746g-disco/ +│ │ └── spi2.ini +│ └── stm32f769i-discovery/ +│ └── spi2.ini +└── mcu/ # MCU-specific (pin-agnostic) configurations + ├── stm32f401re.ini + ├── stm32f405rg.ini + ├── stm32f407vg.ini + ├── stm32f411re.ini + ├── stm32f429zi.ini + ├── stm32f439zi.ini + ├── stm32f446re.ini + ├── stm32f469ni.ini + ├── stm32f722re.ini + ├── stm32f746zg.ini + ├── stm32f767zi.ini + └── stm32f769ni.ini +``` + +## Configuration Format + +Each board configuration file contains: +- `[section]` entries with `driver_name=dmgpio` that mux the SCK/MISO/MOSI + pins to the SPI peripheral's alternate function. +- One `[section]` with `driver_name=dmspi` for the peripheral itself. + +`dmdevfs` scans a config file (or a whole directory of them) for sections +declaring `driver_name`, dynamically resolves that module's `dmdrvi` +interface (`Dmod_LoadModuleByName()` + `Dmod_GetDifFunction()` - see +[docs/configuration.md](../docs/configuration.md)), and creates one device +per section, ordered by `driver_order` (GPIO pin sections must land before +the SPI section that depends on them, hence both use the same order here - +declaration order within a `driver_order` group already puts the pins +first). + +The SPI peripheral itself never drives chip-select (`nss_mode` stays +`soft` throughout this directory) - dmspi still manages it, just as a +plain `dmgpio` pin rather than a native NSS signal. Every file below +configures the relevant CS pin as its own standalone `dmgpio` device +(`[..._cs]`, declared *before* the `dmspi` section that needs it) and +references it from the `dmspi` section's `cs_path`, so dmspi opens that +device and asserts/deasserts it automatically around each transfer - see +[docs/configuration.md](../docs/configuration.md)'s "Chip select" section +for exactly how. On the four generic Arduino-header boards this assumes a +single shield device wired to D10 (the Arduino-standard CS pin); on the +accelerometer/gyroscope boards it's the one fixed onboard device. If your +setup doesn't match either (multiple slaves, a shield that manages its own +CS, ...), drop `cs_path` from the `dmspi` section and manage CS yourself. + +### Example (nucleo-f401re/spi1.ini) + +```ini +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low +``` + +## Board Configurations + +| Board | Folder | SPI Instance | Pins | CS (`cs_path` wired) | Source | Notes | +|-------|--------|---------------|------|------------------------|--------|-------| +| NUCLEO-F401RE | `board/nucleo-f401re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | +| NUCLEO-F411RE | `board/nucleo-f411re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | +| NUCLEO-F446RE | `board/nucleo-f446re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | +| NUCLEO-F767ZI | `board/nucleo-f767zi/` | SPI1 | PA5/PA6/PA7 | D10=PD14 **(unverified)** | Zephyr `nucleo_f767zi.dts` | Arduino header; **PA7 conflicts with onboard Ethernet** - check solder bridges | +| STM32F4-DISCOVERY | `board/stm32f4-discovery/` | SPI1 | PA5/PA6/PA7 | PE3 | ST community + stm32f4-discovery.net | Onboard MEMS motion sensor (LIS3DSH/LIS302DL) | +| STM32F429I-DISCOVERY | `board/stm32f429i-discovery/` | SPI5 | PF7/PF8/PF9 | PC1 | UM1670 + schematic pack | Onboard L3GD20 gyroscope | +| STM32F746G-DISCO | `board/stm32f746g-disco/` | **SPI2** | PI1/PB14/PB15 | D10=PI0 | UM1907 Table 3 | Arduino header - **not** SPI1 (PA5-7 used by USB HS ULPI/DCMI/ETH) | +| STM32F769I-DISCOVERY | `board/stm32f769i-discovery/` | **SPI2** | PH6/PJ4/PJ3 | D10=PF7 | UM2033 | Arduino header - **not** SPI1, same reason as F746G-DISCO | + +Every row above was verified against the cited source before being written +here; see each `.ini` file's own header comment for the exact quote/detail +and any remaining caveat (the `alternate_function=5` value is inferred from +the general STM32 AF-per-peripheral convention, not independently checked +per-pin, on every board here; NUCLEO-F767ZI's D10=PD14 pairing specifically +was not independently cross-checked either, unlike its SCK/MISO/MOSI - see +each file for specifics). + +## MCU Configurations + +Pin-agnostic default (`[dmspi]` only, no GPIO sections - the same shape as +`dmuart`'s `mcu/` configs) for each MCU covered by `dmgpio`/`dmclk`'s own +`configs/mcu/`: + +| MCU | File | +|-----|------| +| STM32F401RE | `mcu/stm32f401re.ini` | +| STM32F405RG | `mcu/stm32f405rg.ini` | +| STM32F407VG | `mcu/stm32f407vg.ini` | +| STM32F411RE | `mcu/stm32f411re.ini` | +| STM32F429ZI | `mcu/stm32f429zi.ini` | +| STM32F439ZI | `mcu/stm32f439zi.ini` | +| STM32F446RE | `mcu/stm32f446re.ini` | +| STM32F469NI | `mcu/stm32f469ni.ini` | +| STM32F722RE | `mcu/stm32f722re.ini` | +| STM32F746ZG | `mcu/stm32f746zg.ini` | +| STM32F767ZI | `mcu/stm32f767zi.ini` | +| STM32F769NI | `mcu/stm32f769ni.ini` | + +## Usage + +Install a config alongside the module with `dmf-get`: + +```bash +dmf-get dmspi@1.0 --config board/nucleo-f401re/spi1.ini --config-dir ./config +``` + +or point `dmdevfs` at the whole `board//` directory (together with +that board's `dmgpio`/`dmclk` config directories) to bring up every +peripheral it declares in one pass. diff --git a/configs/board/nucleo-f401re/spi1.ini b/configs/board/nucleo-f401re/spi1.ini new file mode 100644 index 0000000..17af78c --- /dev/null +++ b/configs/board/nucleo-f401re/spi1.ini @@ -0,0 +1,68 @@ +; NUCLEO-F401RE SPI1 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against UM1724 "STM32 Nucleo-64 boards" (Arduino connector +; table): D13(SCK)=PA5, D12(MISO)=PA6, D11(MOSI)=PA7, D10(CS)=PB6. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI1 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port A alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below +; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path +; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) +; so dmspi drives it low/high around every transfer automatically. This +; assumes a single shield device on D10, the common case - for a shield +; that uses a different CS pin, or manages multiple devices itself, +; remove cs_path from [arduino_spi] and drive CS yourself instead. +; [arduino_spi_cs] must stay declared before [arduino_spi] in this file +; for cs_path to resolve to an existing device. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low diff --git a/configs/board/nucleo-f411re/spi1.ini b/configs/board/nucleo-f411re/spi1.ini new file mode 100644 index 0000000..69253d0 --- /dev/null +++ b/configs/board/nucleo-f411re/spi1.ini @@ -0,0 +1,69 @@ +; NUCLEO-F411RE SPI1 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against UM1724 "STM32 Nucleo-64 boards" (Arduino connector +; table): D13(SCK)=PA5, D12(MISO)=PA6, D11(MOSI)=PA7, D10(CS)=PB6. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI1 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port A alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below +; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path +; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) +; so dmspi drives it low/high around every transfer automatically. This +; assumes a single shield device on D10, the common case - for a shield +; that uses a different CS pin, or manages multiple devices itself, +; remove cs_path from [arduino_spi] and drive CS yourself instead. +; [arduino_spi_cs] must stay declared before [arduino_spi] in this file +; for cs_path to resolve to an existing device. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low + diff --git a/configs/board/nucleo-f446re/spi1.ini b/configs/board/nucleo-f446re/spi1.ini new file mode 100644 index 0000000..7f70737 --- /dev/null +++ b/configs/board/nucleo-f446re/spi1.ini @@ -0,0 +1,69 @@ +; NUCLEO-F446RE SPI1 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against UM1724 "STM32 Nucleo-64 boards" (Arduino connector +; table): D13(SCK)=PA5, D12(MISO)=PA6, D11(MOSI)=PA7, D10(CS)=PB6. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI1 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port A alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below +; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path +; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) +; so dmspi drives it low/high around every transfer automatically. This +; assumes a single shield device on D10, the common case - for a shield +; that uses a different CS pin, or manages multiple devices itself, +; remove cs_path from [arduino_spi] and drive CS yourself instead. +; [arduino_spi_cs] must stay declared before [arduino_spi] in this file +; for cs_path to resolve to an existing device. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low + diff --git a/configs/board/nucleo-f767zi/spi1.ini b/configs/board/nucleo-f767zi/spi1.ini new file mode 100644 index 0000000..3e6c169 --- /dev/null +++ b/configs/board/nucleo-f767zi/spi1.ini @@ -0,0 +1,79 @@ +; NUCLEO-F767ZI SPI1 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against Zephyr's nucleo_f767zi.dts (real, working board config - +; pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>): D13(SCK)=PA5, +; D12(MISO)=PA6, D11(MOSI)=PA7. D10(CS) is PD14 per the same source +; (listed alongside the SPI pins) but that specific pin/signal pairing was +; not independently cross-checked, unlike SCK/MISO/MOSI above - confirm +; PD14 against UM1974 before trusting [arduino_spi_cs] below on real +; hardware. +; +; IMPORTANT: the same Zephyr doc explicitly warns "The Arduino Uno v3 +; specified SPI device conflicts with the on-board ETH device on pin PA7" +; (shared with ETH_CRS_DV) - whether PA7 is actually free for SPI depends +; on this board's solder-bridge configuration (see UM1974's solder bridge +; table). Confirm PA7 isn't routed to the Ethernet PHY on your board +; before relying on this. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI1 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port A alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] +; below configures D10 (PD14) as a plain dmgpio output, and +; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted +; under /dev/arduino_spi_cs) so dmspi drives it low/high around every +; transfer automatically - assuming a single shield device on D10 and +; that the PD14 guess above is right. For a shield that uses a different +; CS pin, remove cs_path from [arduino_spi] and drive CS yourself instead. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PD14 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low + diff --git a/configs/board/stm32f4-discovery/spi1.ini b/configs/board/stm32f4-discovery/spi1.ini new file mode 100644 index 0000000..6259fe0 --- /dev/null +++ b/configs/board/stm32f4-discovery/spi1.ini @@ -0,0 +1,77 @@ +; STM32F4-DISCOVERY SPI1 - onboard MEMS motion sensor (LIS3DSH on later +; board revisions, LIS302DL on early ones - both wired the same way) +; +; Pins: PA5 (SCK), PA6 (MISO), PA7 (MOSI), PE3 (CS) - cross-checked against +; multiple independent sources (ST community threads and a stm32f4-discovery.net +; writeup) rather than the UM1472 schematic directly (couldn't fetch the PDF +; itself in this session) - the CS=PE3 value in particular is corroborated +; by working example code (HAL_GPIO_WritePin(GPIOE, GPIO_PIN_3, ...)) in +; more than one place, but verify against your board's schematic +; (MB997-Cxx) before relying on it. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI1 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port A alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; mode=3 (CPOL=1, CPHA=1) matches the LIS3DSH datasheet (clock idles high) +; and is confirmed by ST's own community answers for the same MEMS family. +; +; CS is not part of the SPI peripheral (nss_mode=soft) - [mems_cs] below +; configures PE3 as a plain dmgpio output, and [mems_spi]'s cs_path points +; dmspi at the resulting device (mounted under /dev/mems_cs - the section +; name, since it isn't literally "dmgpio") so dmspi drives it low/high +; around every transfer automatically. driver_order=3 on both sections +; only guarantees they run before/with other order-3 drivers on this +; board; [mems_cs] must still be declared (and therefore created) before +; [mems_spi] within this file for cs_path to resolve to an existing +; device - keep that ordering if you edit this file. + +[mems_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[mems_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[mems_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[mems_cs] +driver_name=dmgpio +driver_order=3 +pin=PE3 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[mems_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=1000000 +mode=3 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/mems_cs +cs_active_level=low diff --git a/configs/board/stm32f429i-discovery/spi5.ini b/configs/board/stm32f429i-discovery/spi5.ini new file mode 100644 index 0000000..3209775 --- /dev/null +++ b/configs/board/stm32f429i-discovery/spi5.ini @@ -0,0 +1,68 @@ +; STM32F429I-DISCOVERY SPI5 - onboard L3GD20 gyroscope +; +; Pins verified against UM1670 "Discovery kit with STM32F429ZI MCU" and its +; schematic pack (MB1075-F429I-C01): SCK=PF7, MISO=PF8, MOSI=PF9, CS=PC1. +; +; alternate_function=5 follows the STM32F4/F7 convention that SPI5 uses +; the same AF number on every pin it can be routed to; not independently +; checked against the Port F alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; mode=3 (CPOL=1, CPHA=1) matches the L3GD20 datasheet (clock idles high, +; data sampled on the falling edge). +; +; CS is not part of the SPI peripheral (nss_mode=soft) - [gyro_cs] below +; configures PC1 as a plain dmgpio output, and [gyro_spi]'s cs_path points +; dmspi at the resulting device (mounted under /dev/gyro_cs) so dmspi +; drives it low/high around every transfer automatically. [gyro_cs] must +; stay declared before [gyro_spi] in this file for cs_path to resolve to +; an existing device. + +[gyro_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PF7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[gyro_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PF8 +mode=alternate +alternate_function=5 +pull=none + +[gyro_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PF9 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[gyro_cs] +driver_name=dmgpio +driver_order=3 +pin=PC1 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[gyro_spi] +driver_name=dmspi +driver_order=3 +instance=5 +role=master +baudrate=1000000 +mode=3 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/gyro_cs +cs_active_level=low diff --git a/configs/board/stm32f746g-disco/spi2.ini b/configs/board/stm32f746g-disco/spi2.ini new file mode 100644 index 0000000..4d236cc --- /dev/null +++ b/configs/board/stm32f746g-disco/spi2.ini @@ -0,0 +1,74 @@ +; STM32F746G-DISCO SPI2 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against UM1907 "Discovery kit for STM32F7 series with STM32F746NG +; MCU", Table 3 "Arduino Connectors (CN4, CN5, CN6, CN7)": +; D13 (SCK) -> PI1 +; D12 (MISO) -> PB14 +; D11 (MOSI) -> PB15 +; D10 (NSS) -> PI0 +; This is SPI2, not SPI1 - PA5/PA6/PA7 are used on this board for +; USB_OTG_HS_ULPI/DCMI/Ethernet instead, so the Arduino SPI header is +; rerouted to SPI2 on different pins entirely. +; +; alternate_function=5 follows the STM32F4/F7 convention that a given SPI +; instance uses the same AF number on every pin it can be routed to (SPI1/ +; SPI2/SPI4/SPI5/SPI6 = AF5, SPI3 = AF6); it was not independently checked +; against the Port I/Port B alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] +; below configures D10 (PI0) as a plain dmgpio output, and +; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted +; under /dev/arduino_spi_cs) so dmspi drives it low/high around every +; transfer automatically - assuming a single shield device on D10. For a +; shield that uses a different CS pin, remove cs_path from [arduino_spi] +; and drive CS yourself instead. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PI1 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PB14 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PB15 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PI0 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=2 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low diff --git a/configs/board/stm32f769i-discovery/spi2.ini b/configs/board/stm32f769i-discovery/spi2.ini new file mode 100644 index 0000000..3abec6a --- /dev/null +++ b/configs/board/stm32f769i-discovery/spi2.ini @@ -0,0 +1,74 @@ +; STM32F769I-DISCOVERY SPI2 (Arduino Uno V3 connector D13/D12/D11) +; +; Verified against UM2033 "Discovery kit with STM32F769NI MCU" (Arduino +; connector pinout table): +; D13 (SCK) -> PH6 +; D12 (MISO) -> PJ4 +; D11 (MOSI) -> PJ3 +; D10 (NSS) -> PF7 +; This is SPI2, not SPI1 - as on STM32F746G-DISCO, PA5/PA6/PA7 are already +; used elsewhere on this board, so the Arduino SPI header is rerouted to +; SPI2 on different pins entirely. +; +; alternate_function=5 follows the STM32F4/F7 convention that a given SPI +; instance uses the same AF number on every pin it can be routed to (SPI1/ +; SPI2/SPI4/SPI5/SPI6 = AF5, SPI3 = AF6); it was not independently checked +; against the Port H/Port J alternate-function table for this specific +; part - verify against the datasheet before relying on it. +; +; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] +; below configures D10 (PF7) as a plain dmgpio output, and +; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted +; under /dev/arduino_spi_cs) so dmspi drives it low/high around every +; transfer automatically - assuming a single shield device on D10. For a +; shield that uses a different CS pin, remove cs_path from [arduino_spi] +; and drive CS yourself instead. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=3 +pin=PH6 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=3 +pin=PJ4 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=3 +pin=PJ3 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=3 +pin=PF7 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +[arduino_spi] +driver_name=dmspi +driver_order=3 +instance=2 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +cs_path=/dev/arduino_spi_cs +cs_active_level=low diff --git a/configs/mcu/stm32f401re.ini b/configs/mcu/stm32f401re.ini new file mode 100644 index 0000000..ca96909 --- /dev/null +++ b/configs/mcu/stm32f401re.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F401RE microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f405rg.ini b/configs/mcu/stm32f405rg.ini new file mode 100644 index 0000000..7a03ed2 --- /dev/null +++ b/configs/mcu/stm32f405rg.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F405RG microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f407vg.ini b/configs/mcu/stm32f407vg.ini new file mode 100644 index 0000000..9f1608e --- /dev/null +++ b/configs/mcu/stm32f407vg.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F407VG microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f411re.ini b/configs/mcu/stm32f411re.ini new file mode 100644 index 0000000..b024607 --- /dev/null +++ b/configs/mcu/stm32f411re.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F411RE microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f429zi.ini b/configs/mcu/stm32f429zi.ini new file mode 100644 index 0000000..e6fcf2b --- /dev/null +++ b/configs/mcu/stm32f429zi.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F429ZI microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f439zi.ini b/configs/mcu/stm32f439zi.ini new file mode 100644 index 0000000..4b85665 --- /dev/null +++ b/configs/mcu/stm32f439zi.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F439ZI microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f446re.ini b/configs/mcu/stm32f446re.ini new file mode 100644 index 0000000..7bf8385 --- /dev/null +++ b/configs/mcu/stm32f446re.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F446RE microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f469ni.ini b/configs/mcu/stm32f469ni.ini new file mode 100644 index 0000000..505b15f --- /dev/null +++ b/configs/mcu/stm32f469ni.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F469NI microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f722re.ini b/configs/mcu/stm32f722re.ini new file mode 100644 index 0000000..d942c9b --- /dev/null +++ b/configs/mcu/stm32f722re.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F722RE microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f746zg.ini b/configs/mcu/stm32f746zg.ini new file mode 100644 index 0000000..0b31fda --- /dev/null +++ b/configs/mcu/stm32f746zg.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F746ZG microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f767zi.ini b/configs/mcu/stm32f767zi.ini new file mode 100644 index 0000000..50777a9 --- /dev/null +++ b/configs/mcu/stm32f767zi.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F767ZI microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/configs/mcu/stm32f769ni.ini b/configs/mcu/stm32f769ni.ini new file mode 100644 index 0000000..df77437 --- /dev/null +++ b/configs/mcu/stm32f769ni.ini @@ -0,0 +1,8 @@ +; DMSPI default configuration for STM32F769NI microcontroller +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/dmspi.dmr b/dmspi.dmr index f820816..8010441 100644 --- a/dmspi.dmr +++ b/dmspi.dmr @@ -25,6 +25,10 @@ readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/REA # Include directory with all headers (public header + generated _defs.h) inc=./include => ${destination}/${module}/include [origin=${repo_dir}/include] [origin=${build_dir}/${module}_defs.h] +# === Configuration Files === +# Configuration directory with board and MCU specific configs +configs=./configs => ${destination}/${module}/configs [origin=${repo_dir}/configs] + # === License === # License file license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] diff --git a/docs/README.md b/docs/README.md index 1500a83..0d23b40 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,9 @@ Welcome to the dmspi module documentation. ## Contents - **[api-reference.md](api-reference.md)** - Complete API documentation +- **[configuration.md](configuration.md)** - INI configuration reference and usage example +- **[port-implementation.md](port-implementation.md)** - How the stm32f4/stm32f7 port is structured and how to add another architecture +- **[../configs/README.md](../configs/README.md)** - Ready-made per-board/per-MCU INI files ## Quick Reference diff --git a/docs/api-reference.md b/docs/api-reference.md index 41ce3e8..9de4bd2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,12 +1,54 @@ # dmspi API Reference -Document the module's public types and functions here as you add them to -`include/dmspi.h`. +dmspi is a DMDRVI hardware driver: applications interact with it through the +generic `dmdrvi` DIF (create/open/read/write/ioctl/close/free), not through a +bespoke per-module API. Because dmspi is a separate loadable module, these +entry points (named `dmdrvi_dmspi_*`) must be resolved dynamically via +`Dmod_GetDifFunction()` rather than called by name - see +[configuration.md](configuration.md) for why and a full usage example. ## Types -_(none yet)_ +See [include/dmspi_types.h](../include/dmspi_types.h) for the complete +definitions. + +| Type | Description | +|------|-------------| +| `dmspi_instance_t` | SPI instance number (1-based, e.g. 1 = SPI1) | +| `dmspi_baudrate_t` | Requested SCK frequency in Hz (master mode only) | +| `dmspi_role_t` | `dmspi_role_master` / `dmspi_role_slave` | +| `dmspi_clock_polarity_t` | `dmspi_clock_polarity_low` / `_high` (CPOL) | +| `dmspi_clock_phase_t` | `dmspi_clock_phase_1_edge` / `_2_edge` (CPHA) | +| `dmspi_bit_order_t` | `dmspi_bit_order_msb_first` / `_lsb_first` | +| `dmspi_nss_mode_t` | `dmspi_nss_mode_soft` / `_hard` - see [configuration.md](configuration.md) | +| `dmspi_int_trigger_t` | Interrupt trigger bitmask: `rx_not_empty`, `tx_empty`, `error` | +| `dmspi_transfer_t` | Full-duplex transfer descriptor (`tx`, `rx`, `size`) used with `dmspi_ioctl_cmd_transfer` | +| `dmspi_config_t` | Full driver configuration (see [include/dmspi.h](../include/dmspi.h)) | + +Data frames are always 8 bits wide; 16-bit frame support is not implemented. ## Functions -_(none yet)_ +`dmspi_validate_config` is a plain, statically-callable function (a normal +Built-in API, linked via `dmod_link_modules(dmspi)`). Every other row is a +DIF entry point - resolve it via `Dmod_GetDifFunction()`, it is **not** +callable by name from another module (see [configuration.md](configuration.md)). + +| Function | Description | +|----------|-------------| +| `dmspi_validate_config(const dmspi_config_t*)` | Validate a config struct without touching hardware. Safe to call even when `dmspi_port` isn't loaded. | +| `dmdrvi_dmspi_create(dmini_context_t, dmdrvi_dev_num_t*)` | Create a driver instance from an INI configuration. | +| `dmdrvi_dmspi_free(dmdrvi_context_t)` | Destroy an instance created by `_create`. | +| `dmdrvi_dmspi_open` / `_close` | Open/close a handle on the device. | +| `dmdrvi_dmspi_read` | Receive bytes - drains the RX ring if configured, otherwise blocks clocking dummy bytes (master) or waiting for the external master (slave). | +| `dmdrvi_dmspi_write` | Transmit bytes (full duplex at the wire level; received bytes are discarded). | +| `dmdrvi_dmspi_ioctl` | Get/set configuration parameters, register an interrupt handler, run a full-duplex transfer, or reconfigure - see `dmspi_ioctl_cmd_t` in [include/dmspi_types.h](../include/dmspi_types.h). | +| `dmdrvi_dmspi_flush` | Block until any in-progress transfer completes. | +| `dmdrvi_dmspi_stat` | Report device metadata. | + +## Port API + +The architecture-specific implementation lives in the separate `dmspi_port` +module (see [include/dmspi_port.h](../include/dmspi_port.h) and +[port-implementation.md](port-implementation.md)). `dmspi.c` never touches +hardware directly - every register access goes through `dmspi_port_*`. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..3acf299 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,197 @@ +# DMSPI Configuration Guide + +## Configuration File Format + +DMSPI uses INI-format configuration files parsed by the DMINI module. + +## Configuration Parameters + +All parameters are in the `[dmspi]` section (or a board/device-named section, +e.g. `[flash_spi]` - the section is auto-detected the same way DMGPIO/DMUART +do it): + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `instance` | integer | 1 | SPI instance number (1-6, depending on family/board) | +| `role` | string | "master" | `"master"` or `"slave"` | +| `baudrate` | integer | (required for master) | Requested SCK frequency in Hz. Ignored for `role=slave` | +| `mode` | integer | - | SPI mode shorthand 0-3 (sets both `clock_polarity`/`clock_phase`); overridden by explicit keys below when present | +| `clock_polarity` | string | "low" | `"low"` or `"high"` (CPOL) | +| `clock_phase` | string | "1edge" | `"1edge"` or `"2edge"` (CPHA) | +| `bit_order` | string | "msb_first" | `"msb_first"` or `"lsb_first"` | +| `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS - see `cs_path` below) or `"hard"` (peripheral drives/reads the physical NSS pin) | +| `cs_path` | string | (none) | Path to a separately dmdevfs-configured `dmgpio` output device (e.g. `/dev/flash_cs`) - if set, dmspi asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave`. See "Chip select" below | +| `cs_active_level` | string | "low" | `"low"` or `"high"` - which level `cs_path` is driven to when asserting (selecting) the device | +| `interrupt_trigger` | string | "off" | `"off"`, `"rx_not_empty"`, `"tx_empty"`, or `"error"` | +| `interrupt_handler` | string | (none) | Name of a dmhaman-registered handler to call on interrupt | +| `rx_ring_size` | integer | 0 | Capacity of the RX ring buffer, in bytes (0 disables it; useful mainly for `role=slave`, where reception isn't driven by the local CPU) | +| `rx_ring_wait_mode` | string | "some_data" | RX ring read-wait behavior: `"none"`, `"some_data"`, `"all_data"` | + +Data frames are always 8 bits; there is no `data_size` parameter. + +## Chip select + +The SPI peripheral itself only ever drives clock/data lines and, in +`nss_mode=hard`, a single NSS pin. Real deployments almost always need +more flexibility than that: one bus master talking to several slaves, each +with its own CS line, or a CS pin the SPI peripheral doesn't own directly. +`cs_path` covers the common case - **one** fixed slave device - by pointing +dmspi at a `dmgpio` output pin configured as its own separate `dmdevfs` +device; dmspi opens it once (`Dmod_FileOpen`) and asserts/deasserts it via +`Dmod_Ioctl` around every `write`/`read`/`dmspi_ioctl_cmd_transfer` call: + +```ini +; The GPIO pin itself, as a normal standalone dmgpio device +[flash_cs] +driver_name=dmgpio +driver_order=3 +pin=PA4 +mode=output +pull=up +speed=maximum +output_circuit=push_pull + +; The SPI device - cs_path references the device above by its dmdevfs path +[flash_spi] +driver_name=dmspi +driver_order=3 +instance=1 +role=master +baudrate=4000000 +mode=0 +cs_path=/dev/flash_cs +cs_active_level=low +``` + +`[flash_cs]` must be declared (and therefore created by `dmdevfs`) before +`[flash_spi]` for `cs_path` to resolve to an existing device - see +`configs/README.md` for the full ordering rules. If you have more than one +slave on the bus, or your CS line needs different timing/handling than +"assert before, deassert after every call", leave `cs_path` unset and +manage CS yourself around whatever calls you make into dmspi - see +[`tools/spitest/main.c`](../tools/spitest/main.c) for the shape of that +(it doesn't use `cs_path` since it talks to a peer board/loopback pair, +not a fixed slave device). + +## Examples + +### Master, mode 0, 1 MHz, CS managed via cs_path + +```ini +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +nss_mode=soft +cs_path=/dev/flash_cs +``` + +### Master talking to a device that needs mode 3 + +```ini +[dmspi] +instance=2 +role=master +baudrate=4000000 +mode=3 +bit_order=msb_first +``` + +### Slave with an interrupt-driven RX ring + +```ini +[dmspi] +instance=3 +role=slave +nss_mode=hard +interrupt_trigger=rx_not_empty +rx_ring_size=256 +rx_ring_wait_mode=some_data +``` + +## Pre-configured Boards + +See [`../configs/README.md`](../configs/README.md) for ready-made, +`dmdevfs`-loadable per-board (SPI pins + the section above bundled together) +and per-MCU configurations. + +## Usage via dmdevfs (normal case) + +Once a config like the ones above has been applied, `dmdevfs` creates a +device node for it under `/dev` (path/name depend on the config's +`driver_name`/section - see `configs/README.md`). From there, use it like +any other file, through the portable `Dmod_File*`/`Dmod_Ioctl` SAL wrappers +(declared in `dmod.h` itself - no extra module to link, they forward to +`dmvfs` under the hood): + +```c +#include "dmod.h" +#include "dmspi_types.h" + +void* fp = Dmod_FileOpen("/dev/dmspi1", "r+"); + +// True full-duplex transfer (e.g. reading a register while sending its address) +uint8_t tx[4] = { 0x03, 0x00, 0x00, 0x00 }; +uint8_t rx[4]; +dmspi_transfer_t xfer = { .tx = tx, .rx = rx, .size = sizeof(tx) }; +Dmod_Ioctl(fp, dmspi_ioctl_cmd_transfer, &xfer); + +// Read back which role this device was configured with +dmspi_role_t role; +Dmod_Ioctl(fp, dmspi_ioctl_cmd_get_role, &role); + +Dmod_FileClose(fp); +``` + +See [`../tools/spitest/main.c`](../tools/spitest/main.c) for a complete, +build-tested tool built this way (it takes a device path, reads its role +back, and runs the appropriate side of a loopback test - no config or pins +guessed or hardcoded). + +Chip-select is not managed by dmspi in `nss_mode=soft` (the common case for +multi-slave master setups) - toggle a `dmgpio` pin around each transaction +instead. + +## Usage without dmdevfs (advanced) + +Talking to dmspi directly - without a `dmdevfs`-mounted path - is possible +but lower-level: dmspi is a separate loadable module, so its `dmdrvi` entry +points (`dmdrvi_dmspi_create`, `_open`, `_ioctl`, ...) live in a different +binary than the caller's and cannot be called by name. Resolve them +dynamically instead, the same way `dmdevfs` itself does (see +`prepare_driver_module()`/`configure_driver()` in `dmdevfs.c`): load/enable +the module by name, then look up each entry point with +`Dmod_GetDifFunction()`. + +```c +#include "dmspi.h" +#include "dmdrvi.h" +#include "dmini.h" + +#define ENABLE_DIF_REGISTRATIONS ON /* before including dmdrvi.h */ +#include "dmdrvi.h" + +Dmod_Context_t *dmspi_module = Dmod_LoadModuleByName("dmspi"); +Dmod_EnableModule("dmspi", true, NULL); + +dmod_dmdrvi_create_t dmdrvi_create = (dmod_dmdrvi_create_t) + Dmod_GetDifFunction(dmspi_module, dmod_dmdrvi_create_sig); +dmod_dmdrvi_open_t dmdrvi_open = (dmod_dmdrvi_open_t) + Dmod_GetDifFunction(dmspi_module, dmod_dmdrvi_open_sig); +dmod_dmdrvi_ioctl_t dmdrvi_ioctl = (dmod_dmdrvi_ioctl_t) + Dmod_GetDifFunction(dmspi_module, dmod_dmdrvi_ioctl_sig); +/* ...dmod_dmdrvi_close_sig / dmod_dmdrvi_free_sig the same way */ + +dmini_context_t config = dmini_create(); +dmini_set_int(config, "dmspi", "instance", 1); +dmini_set_string(config, "dmspi", "role", "master"); +dmini_set_int(config, "dmspi", "baudrate", 1000000); + +dmdrvi_dev_num_t dev_num = {0}; +dmdrvi_context_t spi_ctx = dmdrvi_create(config, &dev_num); +void* handle = dmdrvi_open(spi_ctx, DMDRVI_O_RDWR, &dev_num); +``` + +Prefer the `dmdevfs`/`dmvfs` path above unless you have a specific reason +not to go through it (e.g. writing `dmdevfs` itself). diff --git a/docs/port-implementation.md b/docs/port-implementation.md index 1d70bfd..fd16d77 100644 --- a/docs/port-implementation.md +++ b/docs/port-implementation.md @@ -1,24 +1,56 @@ # Adding a New MCU Port to dmspi -`dmspi_port` currently supports `stm32f7`. The port is split from -the core `dmspi` module so a new architecture can be added without +`dmspi_port` currently supports `stm32f4` and `stm32f7`. The port is split +from the core `dmspi` module so a new architecture can be added without touching architecture-independent logic. +## Current layout + +``` +src/port/ +├── CMakeLists.txt # selects DMOD_CPU_FAMILY's port.c + shared sources +├── stm32_common/ +│ ├── stm32_common.h # shared SPI register layout, instance table, helpers +│ └── stm32_common.c # every dmod_dmspi_port_api_declaration(...) lives here +├── stm32f4/ +│ ├── config.cmake +│ └── port.c # dmod_init/dmod_deinit, IRQ handlers, one register hook +└── stm32f7/ + ├── config.cmake + └── port.c # same shape as stm32f4/port.c +``` + +STM32F4 and STM32F7 implement the same SPI IP block (identical CR1/CR2/SR/DR +register layout, RCC enable bits, and NVIC IRQ numbers for SPI1-SPI6), so +`stm32_common.c` owns the entire driver: register-level init, transfers, +clock/CPOL/CPHA/bit-order/NSS/baud-rate configuration, interrupt handling, +and the RX ring integration. Each family's `port.c` is a thin wrapper: +DMOD lifecycle hooks, `DMOD_IRQ_HANDLER(...)` entries that forward into the +shared `stm32_spi_irq_handler()`, and exactly one function - +`stm32_spi_family_configure_frame_format()` - which is the one place the two +SPI IP revisions genuinely diverge (STM32F7 needs `CR2.FRXTH` set so RXNE +asserts per-byte instead of per 16-bit FIFO level; STM32F4's classic IP +needs nothing beyond clearing `CR1.DFF`). See `stm32f4/port.c` and +`stm32f7/port.c` for the two (very short) implementations. + ## 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_dmspi_port_api_declaration(...)` functions declared in - `include/dmspi_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 +2. Create `src/port//port.c` implementing `dmod_init`/`dmod_deinit` + and any `DMOD_IRQ_HANDLER(...)` needed, following + `src/port/stm32f7/port.c` as a template. +3. If the underlying peripheral IP is identical (or nearly identical) to an + existing family, reuse that family's `src/port/_common/` instead + of writing a new implementation - only add the register-level quirks that + genuinely differ, following the `stm32_spi_family_configure_frame_format()` + pattern above. Only introduce a brand new `_common/` folder for a + genuinely different peripheral IP (e.g. a non-STM32 vendor). +4. Update `src/port/CMakeLists.txt`'s family-detection `if()` if the new + family needs different shared sources than the `^stm32` case already + picks up. +5. Build by selecting the new family: `cmake .. -DDMOD_CPU_FAMILY=`. +6. 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. diff --git a/examples/config.ini b/examples/config.ini new file mode 100644 index 0000000..cec19bb --- /dev/null +++ b/examples/config.ini @@ -0,0 +1,8 @@ +; Configuration file for DMSPI example +[dmspi] +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft diff --git a/include/dmspi.h b/include/dmspi.h index df70e61..478282b 100644 --- a/include/dmspi.h +++ b/include/dmspi.h @@ -1,51 +1,35 @@ #ifndef DMSPI_H #define DMSPI_H -#include -#include -#include -#include "dmod_types.h" #include "dmspi_defs.h" +#include "dmspi_types.h" /** - * Public API for the dmspi module. - * - * Functions are declared with the dmod_dmspi_api(...) macro - dmod's - * standard pattern for functions callable from other modules (or from this - * module's own tests/), resolved dynamically by the loader rather than - * through normal static linkage. See dm_sw_ring/include/dm_sw_ring.h for a - * fully worked real-world example of the same shape. - * - * Definitions in src/dmspi.c use the matching - * dmod_dmspi_api_declaration(...) macro - a plain C function - * definition here will NOT satisfy these declarations at link time. - * - * This is an example interface using the usual "opaque handle" pattern - - * replace the handle, functions, and struct definition in - * src/dmspi.c with your module's real API. + * @brief SPI driver configuration structure */ - -/* Opaque handle - the real struct is defined in src/dmspi.c */ -typedef struct dmspi* dmspi_t; +typedef struct +{ + dmspi_instance_t instance; /**< SPI instance number (1-based) */ + dmspi_role_t role; /**< Master or slave */ + dmspi_baudrate_t baudrate; /**< Requested SCK frequency, Hz (master only) */ + dmspi_clock_polarity_t clock_polarity; /**< Clock polarity (CPOL) */ + dmspi_clock_phase_t clock_phase; /**< Clock phase (CPHA) */ + dmspi_bit_order_t bit_order; /**< Bit order (MSB/LSB first) */ + dmspi_nss_mode_t nss_mode; /**< NSS (chip select) management mode */ + dmspi_int_trigger_t interrupt_trigger; /**< Interrupt trigger source */ + dmspi_interrupt_handler_t interrupt_handler; /**< Interrupt handler (NULL = not used) */ +} dmspi_config_t; /** - * Create a new dmspi instance. + * @brief Validate a configuration structure without touching hardware. * - * @return A valid handle on success, or NULL on allocation failure. - */ -dmod_dmspi_api(1.0, dmspi_t, _create, ( void )); - -/** - * Destroy an instance created by dmspi_create(). Safe to call with - * NULL. - */ -dmod_dmspi_api(1.0, void, _destroy, ( dmspi_t handle )); - -/** - * Example accessor - replace with your module's real API. + * Checks that the combination of parameters is self-consistent (e.g. a + * master requires a non-zero baud rate). Does not require the dmspi_port + * module to be loaded, so it is safe to call from any context, including + * off-target unit tests. * - * @return true if handle is a valid, non-NULL instance. + * @return true if the configuration is valid, false otherwise. */ -dmod_dmspi_api(1.0, bool, _is_valid, ( dmspi_t handle )); +dmod_dmspi_api(1.0, bool, _validate_config, ( const dmspi_config_t *config )); #endif // DMSPI_H diff --git a/include/dmspi_port.h b/include/dmspi_port.h index 300e4f6..83934d8 100644 --- a/include/dmspi_port.h +++ b/include/dmspi_port.h @@ -3,31 +3,74 @@ #include "dmod_types.h" #include "dmspi_port_defs.h" +#include "dmspi_types.h" +#include "dm_sw_ring.h" -/* - * Declare architecture-independent port API functions here, following the - * dmod_dmspi_port_api(version, return_type, _suffix, (args)) pattern, - * e.g.: - * - * dmod_dmspi_port_api(1.0, int, _configure, ( int some_arg ) ); - * - * Each declaration here must have a matching *definition* in - * src/port//port.c (or a shared src/port/_common/ file), written - * with the dmod_dmspi_port_api_declaration(...) macro instead: - * - * dmod_dmspi_port_api_declaration(1.0, int, _configure, ( int some_arg ) ) - * { - * ... - * } - * - * 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_dmspi_port_api_declaration(...) - * definition to avoid an undefined-reference link error. +/* --- Lifecycle --- */ + +dmod_dmspi_port_api(1.0, int, _init, ( dmspi_instance_t instance, dmspi_role_t role ) ); +dmod_dmspi_port_api(1.0, int, _deinit, ( dmspi_instance_t instance ) ); + +/* --- Data transfer --- * - * See dmfmc/include/dmfmc_port.h and dmfmc/src/port/stm32_common/stm32_common.c - * for a fully worked example. + * All transfers are full-duplex at the hardware level: _transmit discards + * the bytes shifted in while sending, _receive shifts out a dummy byte + * (0xFF) for every byte it reads, and _transfer exposes both directions at + * once. In slave mode these calls block waiting for the external master to + * clock each byte - configure a RX ring (see _set_rx_ring) and an interrupt + * trigger for non-blocking slave reception instead. */ +dmod_dmspi_port_api(1.0, int, _transmit, ( dmspi_instance_t instance, const uint8_t* data, size_t size ) ); +dmod_dmspi_port_api(1.0, int, _receive, ( dmspi_instance_t instance, uint8_t* data, size_t size, size_t* received ) ); +dmod_dmspi_port_api(1.0, int, _transfer, ( dmspi_instance_t instance, const uint8_t* tx, uint8_t* rx, size_t size ) ); + +/* --- Status --- */ + +dmod_dmspi_port_api(1.0, bool, _is_busy, ( dmspi_instance_t instance ) ); + +/* --- Role --- */ + +dmod_dmspi_port_api(1.0, int, _set_role, ( dmspi_instance_t instance, dmspi_role_t role ) ); +dmod_dmspi_port_api(1.0, dmspi_role_t, _get_role, ( dmspi_instance_t instance ) ); + +/* --- Baud rate (master only) --- */ + +dmod_dmspi_port_api(1.0, int, _set_baudrate, ( dmspi_instance_t instance, dmspi_baudrate_t baudrate ) ); +dmod_dmspi_port_api(1.0, dmspi_baudrate_t, _get_baudrate, ( dmspi_instance_t instance ) ); + +/* --- Clock polarity / phase --- */ + +dmod_dmspi_port_api(1.0, int, _set_clock_polarity, ( dmspi_instance_t instance, dmspi_clock_polarity_t polarity ) ); +dmod_dmspi_port_api(1.0, dmspi_clock_polarity_t, _get_clock_polarity, ( dmspi_instance_t instance ) ); +dmod_dmspi_port_api(1.0, int, _set_clock_phase, ( dmspi_instance_t instance, dmspi_clock_phase_t phase ) ); +dmod_dmspi_port_api(1.0, dmspi_clock_phase_t, _get_clock_phase, ( dmspi_instance_t instance ) ); + +/* --- Bit order --- */ + +dmod_dmspi_port_api(1.0, int, _set_bit_order, ( dmspi_instance_t instance, dmspi_bit_order_t bit_order ) ); +dmod_dmspi_port_api(1.0, dmspi_bit_order_t, _get_bit_order, ( dmspi_instance_t instance ) ); + +/* --- NSS management --- */ + +dmod_dmspi_port_api(1.0, int, _set_nss_mode, ( dmspi_instance_t instance, dmspi_nss_mode_t nss_mode ) ); +dmod_dmspi_port_api(1.0, dmspi_nss_mode_t, _get_nss_mode, ( dmspi_instance_t instance ) ); + +/* --- Interrupt trigger --- */ + +dmod_dmspi_port_api(1.0, int, _set_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t trigger ) ); +dmod_dmspi_port_api(1.0, int, _read_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t *out_trigger ) ); + +/* --- Interrupt handler registration --- */ + +dmod_dmspi_port_api(1.0, int, _add_interrupt_handler, + ( dmspi_instance_t instance, dmspi_port_interrupt_handler_t handler, void *user_ptr ) ); +dmod_dmspi_port_api(1.0, int, _remove_interrupt_handler, + ( dmspi_instance_t instance, void *user_ptr ) ); + +/* --- SW ring buffer (RX only - see _add_interrupt_handler for TX events) --- */ + +dmod_dmspi_port_api(1.0, int, _set_rx_ring, + ( dmspi_instance_t instance, dm_sw_ring_t ring ) ); + #endif // DMSPI_PORT_H diff --git a/include/dmspi_types.h b/include/dmspi_types.h new file mode 100644 index 0000000..abdc5f3 --- /dev/null +++ b/include/dmspi_types.h @@ -0,0 +1,154 @@ +#ifndef DMSPI_TYPES_H +#define DMSPI_TYPES_H + +#include +#include +#include + +/** + * @brief SPI instance type (1-based, e.g. 1 = SPI1) + */ +typedef uint8_t dmspi_instance_t; + +/** + * @brief SPI requested clock frequency, in Hz (master mode only) + */ +typedef uint32_t dmspi_baudrate_t; + +/** + * @brief SPI role - master drives SCK/NSS, slave is clocked externally + */ +typedef enum +{ + dmspi_role_master = 0, /**< This instance drives the bus */ + dmspi_role_slave, /**< This instance is clocked by an external master */ +} dmspi_role_t; + +/** + * @brief SPI clock polarity (CPOL) + */ +typedef enum +{ + dmspi_clock_polarity_low = 0, /**< Clock idles low */ + dmspi_clock_polarity_high, /**< Clock idles high */ +} dmspi_clock_polarity_t; + +/** + * @brief SPI clock phase (CPHA) + */ +typedef enum +{ + dmspi_clock_phase_1_edge = 0, /**< Data sampled on the first clock edge */ + dmspi_clock_phase_2_edge, /**< Data sampled on the second clock edge */ +} dmspi_clock_phase_t; + +/** + * @brief SPI bit order + */ +typedef enum +{ + dmspi_bit_order_msb_first = 0, /**< MSB transmitted first (default) */ + dmspi_bit_order_lsb_first, /**< LSB transmitted first */ +} dmspi_bit_order_t; + +/** + * @brief NSS (chip select) management mode + */ +typedef enum +{ + dmspi_nss_mode_soft = 0, /**< Software managed - master: CS handled externally + * (e.g. via dmgpio); slave: instance always selected */ + dmspi_nss_mode_hard, /**< Hardware managed - master: NSS pin driven by the + * peripheral (single-slave only); slave: physical + * NSS pin gates the peripheral */ +} dmspi_nss_mode_t; + +/** + * @brief SPI interrupt trigger source + */ +typedef enum +{ + dmspi_int_trigger_off = 0, /**< Interrupts disabled */ + dmspi_int_trigger_rx_not_empty = (1 << 0), /**< RX buffer not empty */ + dmspi_int_trigger_tx_empty = (1 << 1), /**< TX buffer empty */ + dmspi_int_trigger_error = (1 << 2), /**< Mode fault / overrun / CRC error */ +} dmspi_int_trigger_t; + +/** + * @brief IOCTL commands for DMSPI device + */ +typedef enum +{ + dmspi_ioctl_cmd_get_role = 1, /**< Get role (master/slave) */ + dmspi_ioctl_cmd_set_role, /**< Set role (master/slave) */ + dmspi_ioctl_cmd_get_baudrate, /**< Get requested baud rate */ + dmspi_ioctl_cmd_set_baudrate, /**< Set requested baud rate */ + dmspi_ioctl_cmd_get_clock_polarity, /**< Get clock polarity (CPOL) */ + dmspi_ioctl_cmd_set_clock_polarity, /**< Set clock polarity (CPOL) */ + dmspi_ioctl_cmd_get_clock_phase, /**< Get clock phase (CPHA) */ + dmspi_ioctl_cmd_set_clock_phase, /**< Set clock phase (CPHA) */ + dmspi_ioctl_cmd_get_bit_order, /**< Get bit order */ + dmspi_ioctl_cmd_set_bit_order, /**< Set bit order */ + dmspi_ioctl_cmd_get_nss_mode, /**< Get NSS management mode */ + dmspi_ioctl_cmd_set_nss_mode, /**< Set NSS management mode */ + dmspi_ioctl_cmd_set_interrupt_handler, /**< Set interrupt handler; arg = dmspi_interrupt_handler_t* */ + dmspi_ioctl_cmd_reconfigure, /**< Reconfigure SPI with current settings */ + dmspi_ioctl_cmd_transfer, /**< Full-duplex transfer; arg = dmspi_transfer_t* */ + + dmspi_ioctl_cmd_max +} dmspi_ioctl_cmd_t; + +/** + * @brief Opaque driver context type (forward declaration) + * + * The concrete definition is private to the dmspi driver. + */ +struct dmdrvi_context; +typedef struct dmdrvi_context *dmdrvi_context_t; + +/** + * @brief Full-duplex transfer descriptor, used with dmspi_ioctl_cmd_transfer. + * + * Either @p tx or @p rx may be NULL: a NULL @p tx shifts out a dummy byte + * (0xFF) for every position, a NULL @p rx discards received bytes. + */ +typedef struct +{ + const uint8_t *tx; /**< Bytes to transmit (may be NULL) */ + uint8_t *rx; /**< Buffer to receive into (may be NULL) */ + size_t size; /**< Number of bytes to exchange */ +} dmspi_transfer_t; + +/** + * @brief Parameters passed to a dmhaman-registered interrupt handler. + */ +typedef struct +{ + dmspi_instance_t instance; /**< SPI instance that generated the interrupt */ + dmspi_int_trigger_t trigger; /**< Which trigger fired */ + uint8_t data; /**< Received data byte (valid for rx_not_empty trigger) */ +} dmspi_interrupt_params_t; + +/** + * @brief SPI interrupt handler function type + * + * @param context Context of the driver instance + * @param instance SPI instance that generated the interrupt + * @param trigger Which interrupt trigger fired + * @param data Received data byte (valid when trigger includes rx_not_empty) + */ +typedef void (*dmspi_interrupt_handler_t)(dmdrvi_context_t context, dmspi_instance_t instance, dmspi_int_trigger_t trigger, uint8_t data); + +/** + * @brief SPI port interrupt handler function type + * + * Called by the port layer when an interrupt occurs on a SPI instance. + * + * @param user_ptr User pointer supplied at registration time (e.g. driver context) + * @param instance SPI instance that generated the interrupt + * @param trigger Which interrupt trigger fired + * @param data Received data byte (valid when trigger includes rx_not_empty) + */ +typedef void (*dmspi_port_interrupt_handler_t)(void *user_ptr, dmspi_instance_t instance, dmspi_int_trigger_t trigger, uint8_t data); + +#endif /* DMSPI_TYPES_H */ diff --git a/src/dmspi.c b/src/dmspi.c index 34cc957..a03f3cb 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -1,84 +1,739 @@ -#define DMOD_ENABLE_REGISTRATION ON +#define DMOD_ENABLE_REGISTRATION ON #include "dmod.h" #include "dmspi.h" +#include "dmspi_port.h" +#include "dmdrvi.h" +#include "dmhaman.h" +#include "dmini.h" +#include "dm_sw_ring.h" +#include +#include -/* Example internal state - replace with your module's real fields. */ -struct dmspi +/* Magic set to DSPI */ +#define DMSPI_CONTEXT_MAGIC 0x44535049 + +/* ---- Chip-select (CS/NSS) management ---- + * + * nss_mode=soft leaves NSS entirely unmanaged by the SPI peripheral - by + * design, since a single soft-NSS bus can have many slaves, each with its + * own CS line, and only the caller knows which one applies to a given + * transaction. For the common case of ONE fixed slave device, this driver + * can still drive that CS line automatically: point the config's cs_path + * at a separately dmdevfs-configured dmgpio output pin (see + * configs/README.md) and this driver asserts it around every + * _write/_read/transfer-ioctl call in master mode, deasserting it again + * once the transfer completes. It is ignored for role=slave - a slave + * never drives its own CS. + * + * This goes through dmgpio's own dmdrvi device (Dmod_FileOpen on its + * dmdevfs path + Dmod_Ioctl), not a direct dependency on the dmgpio + * module: every published dmgpio release (v0.6 through v0.8.0, checked by + * hand) is missing its .dmr resource file, so dmf-get cannot fetch + * dmgpio_types.h through the normal dmod_link_modules path here. + * DMGPIO_IOCTL_CMD_SET_PINS_STATE and DMGPIO_PINS_STATE_ALL_{LOW,HIGH} + * below are copied from that header's dmgpio_ioctl_cmd_t/ + * dmgpio_pins_state_t enums - read directly from the sibling dmgpio + * checkout (../dmgpio/include/dmgpio_types.h) rather than assumed, and + * stable/small ordinal values dmgpio can't change without breaking its + * own ABI. Replace this with a real #include "dmgpio_types.h" plus + * dmod_link_modules(dmspi dmgpio) once dmgpio's packaging is fixed + * upstream. */ +#define DMGPIO_IOCTL_CMD_SET_PINS_STATE 1 +#define DMGPIO_PINS_STATE_ALL_LOW 0 +#define DMGPIO_PINS_STATE_ALL_HIGH 1 + +/** + * @brief DMDRVI context structure + */ +struct dmdrvi_context { - bool valid; + uint32_t magic; /**< Magic number for validation */ + dmspi_config_t config; /**< Configuration parameters */ + char *interrupt_handler_name; /**< dmhaman handler name (NULL = not used) */ + dm_sw_ring_t rx_ring; /**< Software ring buffer for received bytes */ + uint32_t rx_ring_size; /**< Capacity of the RX ring buffer (from config) */ + dm_sw_ring_flags_t rx_ring_wait_flags; /**< RX ring read-wait behavior (from config) */ + void *cs_fp; /**< dmgpio device handle for the CS pin (NULL = unmanaged) */ + bool cs_active_high; /**< true = assert by driving the pin high instead of low */ }; -dmod_dmspi_api_declaration(1.0, dmspi_t, _create, ( void )) +static void cs_assert(dmdrvi_context_t context) { - /* 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 dmspi *instance = Dmod_Malloc(sizeof(*instance)); - if (instance == NULL) + if (context->cs_fp == NULL) + return; + int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_HIGH : DMGPIO_PINS_STATE_ALL_LOW; + Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); +} + +static void cs_deassert(dmdrvi_context_t context) +{ + if (context->cs_fp == NULL) + return; + int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_LOW : DMGPIO_PINS_STATE_ALL_HIGH; + Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); +} + +static int is_valid_context(dmdrvi_context_t context) +{ + return (context != NULL && context->magic == DMSPI_CONTEXT_MAGIC); +} + +/* ---- Interrupt dispatch ---- */ + +/* Dispatches port interrupt events to a dmhaman-registered handler. */ +static void internal_interrupt_handler(void *user_ptr, dmspi_instance_t instance, + dmspi_int_trigger_t trigger, uint8_t data) +{ + dmdrvi_context_t ctx = (dmdrvi_context_t)user_ptr; + + if (ctx->interrupt_handler_name != NULL) { - return NULL; + dmspi_interrupt_params_t params; + params.instance = instance; + params.trigger = trigger; + params.data = data; + dmhaman_call_handler(ctx->interrupt_handler_name, ¶ms); + } +} + +/* ---- String conversion helpers ---- */ + +static int string_to_role(const char *s, dmspi_role_t *out_role) +{ + if (s != NULL) + { + if (strcmp(s, "master") == 0) { *out_role = dmspi_role_master; return 0; } + if (strcmp(s, "slave") == 0) { *out_role = dmspi_role_slave; return 0; } } + return -1; +} + +static dmspi_clock_polarity_t string_to_clock_polarity(const char *s) +{ + if (s != NULL && strcmp(s, "high") == 0) return dmspi_clock_polarity_high; + return dmspi_clock_polarity_low; +} + +static dmspi_clock_phase_t string_to_clock_phase(const char *s) +{ + if (s != NULL && strcmp(s, "2edge") == 0) return dmspi_clock_phase_2_edge; + return dmspi_clock_phase_1_edge; +} - instance->valid = true; - return instance; +static dmspi_bit_order_t string_to_bit_order(const char *s) +{ + if (s != NULL && strcmp(s, "lsb_first") == 0) return dmspi_bit_order_lsb_first; + return dmspi_bit_order_msb_first; } -dmod_dmspi_api_declaration(1.0, void, _destroy, ( dmspi_t handle )) +static dmspi_nss_mode_t string_to_nss_mode(const char *s) { - Dmod_Free(handle); + if (s != NULL && strcmp(s, "hard") == 0) return dmspi_nss_mode_hard; + return dmspi_nss_mode_soft; } -dmod_dmspi_api_declaration(1.0, bool, _is_valid, ( dmspi_t handle )) +static dmspi_int_trigger_t string_to_interrupt_trigger(const char *s) { - return handle != NULL && handle->valid; + if (s != NULL) + { + if (strcmp(s, "rx_not_empty") == 0) return dmspi_int_trigger_rx_not_empty; + if (strcmp(s, "tx_empty") == 0) return dmspi_int_trigger_tx_empty; + if (strcmp(s, "error") == 0) return dmspi_int_trigger_error; + } + return dmspi_int_trigger_off; } +static dm_sw_ring_flags_t string_to_rx_ring_wait_flags(const char *s) +{ + if (s != NULL) + { + if (strcmp(s, "none") == 0) return 0; + if (strcmp(s, "all_data") == 0) return dm_sw_ring_flags_wait_for_all_data; + } + return dm_sw_ring_flags_wait_for_some_data; +} + +/* ---- Configuration ---- */ + /** - * @brief Pre-initialization function for the module. - * - * @note This function is optional. You can remove it if you don't need it. + * @brief Detect which INI section holds the SPI configuration. * - * This function is called when the module enabling is in progress. + * The driver supports two config file layouts: + * 1. Standard: a [dmspi] section. + * 2. Board / device-named: a section whose name is the device label + * (e.g. [flash_spi] in board config files). * - * 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. + * Detection strategy: + * a) If [dmspi] contains an 'instance' or 'role' key -> use "dmspi". + * b) Otherwise serialise the INI to a temporary string and scan for the + * first named section (skipping [main]) that contains 'instance' or + * 'role'. Copy its name into section_buf and return it. + * c) Fall back to "dmspi" so the caller produces a meaningful error. */ -void dmod_preinit(void) +static const char *detect_config_section(dmini_context_t ini, + char *section_buf, size_t section_buf_sz) { - if(Dmod_IsFunctionConnected( Dmod_Printf )) + /* Fast path - standard [dmspi] section present */ + if (dmini_has_key(ini, "dmspi", "instance") || dmini_has_key(ini, "dmspi", "role")) + return "dmspi"; + + /* Slow path - serialise the INI and scan for the first usable section */ + int needed = dmini_generate_string(ini, NULL, 0); + if (needed <= 1) + return "dmspi"; + + /* dmini_generate_string follows snprintf convention: the first call returns + * the number of characters that would be written, NOT including the null + * terminator. Allocate one extra byte so the null terminator always fits + * and the scanning loop below is guaranteed to find it within the buffer. */ + char *ini_str = (char *)Dmod_Malloc((size_t)needed + 1); + if (ini_str == NULL) + return "dmspi"; + + if (dmini_generate_string(ini, ini_str, (size_t)needed + 1) <= 0) { - Dmod_Printf("API is connected!\n"); + Dmod_Free(ini_str); + return "dmspi"; } + ini_str[needed] = '\0'; + + const char *result = "dmspi"; + char *p = ini_str; + while (*p != '\0') + { + if (*p == '[') + { + char *name_start = p + 1; + char *name_end = name_start; + while (*name_end != '\0' && *name_end != ']' && + *name_end != '\n' && *name_end != '\r') + name_end++; + + if (*name_end == ']') + { + size_t name_len = (size_t)(name_end - name_start); + if (name_len > 0 && name_len < section_buf_sz) + { + memcpy(section_buf, name_start, name_len); + section_buf[name_len] = '\0'; + + if (strcmp(section_buf, "main") != 0 && + (dmini_has_key(ini, section_buf, "instance") || + dmini_has_key(ini, section_buf, "role"))) + { + result = section_buf; + break; + } + } + } + } + p++; + } + + Dmod_Free(ini_str); + return result; +} + +static int check_config_parameters(const dmspi_config_t *cfg) +{ + if (cfg->instance == 0) + { + DMOD_LOG_ERROR("Invalid SPI instance (must be >= 1)\n"); + return -EINVAL; + } + if (cfg->role == dmspi_role_master && cfg->baudrate == 0) + { + DMOD_LOG_ERROR("Baud rate not set in configuration (required for master role)\n"); + return -EINVAL; + } + return 0; +} + +dmod_dmspi_api_declaration(1.0, bool, _validate_config, ( const dmspi_config_t *config )) +{ + return (config != NULL) && (check_config_parameters(config) == 0); +} + +static int read_config_parameters(dmdrvi_context_t context, dmini_context_t config) +{ + char section_buf[64]; + const char *section = detect_config_section(config, section_buf, sizeof(section_buf)); + + context->config.instance = (dmspi_instance_t)dmini_get_int(config, section, "instance", 1); + + if (string_to_role(dmini_get_string(config, section, "role", "master"), &context->config.role) != 0) + { + DMOD_LOG_ERROR("Invalid 'role' in [%s] config (expected master/slave)\n", section); + return -EINVAL; + } + + /* Optional "mode=0..3" shorthand (SPI mode 0-3), overridden by explicit + * clock_polarity/clock_phase keys when both are present. */ + int spi_mode = dmini_get_int(config, section, "mode", -1); + if (spi_mode >= 0 && spi_mode <= 3) + { + context->config.clock_polarity = (spi_mode & 0x2) ? dmspi_clock_polarity_high : dmspi_clock_polarity_low; + context->config.clock_phase = (spi_mode & 0x1) ? dmspi_clock_phase_2_edge : dmspi_clock_phase_1_edge; + } + else + { + context->config.clock_polarity = dmspi_clock_polarity_low; + context->config.clock_phase = dmspi_clock_phase_1_edge; + } + if (dmini_has_key(config, section, "clock_polarity")) + context->config.clock_polarity = string_to_clock_polarity(dmini_get_string(config, section, "clock_polarity", "low")); + if (dmini_has_key(config, section, "clock_phase")) + context->config.clock_phase = string_to_clock_phase(dmini_get_string(config, section, "clock_phase", "1edge")); + + context->config.baudrate = (dmspi_baudrate_t)dmini_get_int(config, section, "baudrate", 0); + context->config.bit_order = string_to_bit_order(dmini_get_string(config, section, "bit_order", "msb_first")); + context->config.nss_mode = string_to_nss_mode(dmini_get_string(config, section, "nss_mode", "soft")); + context->config.interrupt_trigger = string_to_interrupt_trigger(dmini_get_string(config, section, "interrupt_trigger", "off")); + context->config.interrupt_handler = NULL; + + const char *handler_name = dmini_get_string(config, section, "interrupt_handler", NULL); + context->interrupt_handler_name = (handler_name != NULL) ? Dmod_StrDup(handler_name) : NULL; + + context->rx_ring_size = (uint32_t)dmini_get_int(config, section, "rx_ring_size", 0); + context->rx_ring_wait_flags = string_to_rx_ring_wait_flags( + dmini_get_string(config, section, "rx_ring_wait_mode", "some_data")); + + return check_config_parameters(&context->config); } /** - * @brief Initialization function for the module. + * @brief Open the CS pin device named by 'cs_path', if configured. * - * 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 + * Requires context->config.role to already be set (read_config_parameters() + * must run first). A no-op (returns 0) when cs_path is absent, or when + * role=slave (a slave never drives its own CS - see the file header + * comment on CS management). */ -int dmod_init(const Dmod_Config_t *Config) +static int setup_cs_pin(dmdrvi_context_t context, dmini_context_t config) { - Dmod_Printf("Hello, World!\n"); + char section_buf[64]; + const char *section = detect_config_section(config, section_buf, sizeof(section_buf)); + + const char *cs_path = dmini_get_string(config, section, "cs_path", NULL); + if (cs_path == NULL) + return 0; + + if (context->config.role != dmspi_role_master) + { + DMOD_LOG_ERROR("'cs_path' is ignored for role=slave (a slave never drives its own CS)\n"); + return 0; + } + + context->cs_active_high = (strcmp( + dmini_get_string(config, section, "cs_active_level", "low"), "high") == 0); + + context->cs_fp = Dmod_FileOpen(cs_path, "r+"); + if (context->cs_fp == NULL) + { + DMOD_LOG_ERROR("Failed to open CS pin device '%s'\n", cs_path); + return -EINVAL; + } + + cs_deassert(context); /* start deselected */ 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 + * @brief Apply configuration to the port layer */ +static int configure(dmdrvi_context_t context) +{ + dmspi_config_t *c = &context->config; + int ret; + + ret = dmspi_port_init(c->instance, c->role); + if (ret != 0) + { + DMOD_LOG_ERROR("Failed to initialize SPI%u\n", c->instance); + return ret; + } + + ret = dmspi_port_set_clock_polarity(c->instance, c->clock_polarity); + if (ret != 0) goto err; + + ret = dmspi_port_set_clock_phase(c->instance, c->clock_phase); + if (ret != 0) goto err; + + ret = dmspi_port_set_bit_order(c->instance, c->bit_order); + if (ret != 0) goto err; + + ret = dmspi_port_set_nss_mode(c->instance, c->nss_mode); + if (ret != 0) goto err; + + if (c->role == dmspi_role_master) + { + ret = dmspi_port_set_baudrate(c->instance, c->baudrate); + if (ret != 0) goto err; + } + + dmspi_int_trigger_t trigger = c->interrupt_trigger; + if (context->rx_ring_size > 0) + trigger |= dmspi_int_trigger_rx_not_empty; + + if (trigger != dmspi_int_trigger_off) + { + ret = dmspi_port_set_interrupt_trigger(c->instance, trigger); + if (ret != 0) goto err; + } + + DMOD_LOG_INFO("SPI%u configured: role=%s, %u Hz, CPOL=%u, CPHA=%u\n", + c->instance, (c->role == dmspi_role_master) ? "master" : "slave", + c->baudrate, (unsigned)c->clock_polarity, (unsigned)c->clock_phase); + + return 0; + +err: + DMOD_LOG_ERROR("Failed to configure SPI%u\n", c->instance); + dmspi_port_deinit(c->instance); + return ret; +} + +/* ---- IOCTL helpers ---- */ + +static int update_configuration(dmspi_config_t *cfg, int command, void *arg) +{ + switch (command) + { + case dmspi_ioctl_cmd_set_role: + cfg->role = *(dmspi_role_t *)arg; + break; + case dmspi_ioctl_cmd_set_baudrate: + cfg->baudrate = *(dmspi_baudrate_t *)arg; + break; + case dmspi_ioctl_cmd_set_clock_polarity: + cfg->clock_polarity = *(dmspi_clock_polarity_t *)arg; + break; + case dmspi_ioctl_cmd_set_clock_phase: + cfg->clock_phase = *(dmspi_clock_phase_t *)arg; + break; + case dmspi_ioctl_cmd_set_bit_order: + cfg->bit_order = *(dmspi_bit_order_t *)arg; + break; + case dmspi_ioctl_cmd_set_nss_mode: + cfg->nss_mode = *(dmspi_nss_mode_t *)arg; + break; + default: + return -EINVAL; + } + return check_config_parameters(cfg); +} + +static int read_configuration(dmdrvi_context_t context, int command, void *arg) +{ + dmspi_config_t *c = &context->config; + switch (command) + { + case dmspi_ioctl_cmd_get_role: + *(dmspi_role_t *)arg = c->role; + break; + case dmspi_ioctl_cmd_get_baudrate: + *(dmspi_baudrate_t *)arg = c->baudrate; + break; + case dmspi_ioctl_cmd_get_clock_polarity: + *(dmspi_clock_polarity_t *)arg = c->clock_polarity; + break; + case dmspi_ioctl_cmd_get_clock_phase: + *(dmspi_clock_phase_t *)arg = c->clock_phase; + break; + case dmspi_ioctl_cmd_get_bit_order: + *(dmspi_bit_order_t *)arg = c->bit_order; + break; + case dmspi_ioctl_cmd_get_nss_mode: + *(dmspi_nss_mode_t *)arg = c->nss_mode; + break; + default: + return -EINVAL; + } + return 0; +} + +/* ---- DMOD lifecycle ---- */ + +int dmod_init(const Dmod_Config_t *Config) +{ + DMOD_LOG_INFO("DMSPI interface module initialized\n"); + return 0; +} + int dmod_deinit(void) { - Dmod_Printf("Goodbye, World!\n"); + DMOD_LOG_INFO("DMSPI interface module deinitialized\n"); + return 0; +} + +/* ---- DMDRVI interface ---- */ + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_context_t config, dmdrvi_dev_num_t* dev_num )) +{ + if (config == NULL || dev_num == NULL) + { + DMOD_LOG_ERROR("Invalid parameters to dmspi_dmdrvi_create\n"); + return NULL; + } + + dmdrvi_context_t context = Dmod_Malloc(sizeof(struct dmdrvi_context)); + if (context == NULL) + return NULL; + + memset(context, 0, sizeof(*context)); + context->magic = DMSPI_CONTEXT_MAGIC; + + if (read_config_parameters(context, config) != 0 || + setup_cs_pin(context, config) != 0 || + configure(context) != 0) + { + DMOD_LOG_ERROR("Failed to create DMDRVI context with provided configuration\n"); + if (context->cs_fp != NULL) + Dmod_FileClose(context->cs_fp); + Dmod_Free(context->interrupt_handler_name); + Dmod_Free(context); + return NULL; + } + + /* Create RX ring buffer */ + if (context->rx_ring_size > 0) + { + /* No dm_sw_ring_flags_mutex_sync: this ring is written to directly from + * the SPI RX interrupt handler, and taking a FreeRTOS mutex from ISR + * context is invalid. The ring's lock-free critical-section path (used + * when no mutex is configured) is safe for this single-ISR-producer / + * task-consumer pattern. */ + context->rx_ring = dm_sw_ring_create(context->rx_ring_size, + dm_sw_ring_flags_drop_old_data | context->rx_ring_wait_flags); + if (context->rx_ring == NULL) + { + DMOD_LOG_ERROR("Failed to create RX ring buffer (size=%u)\n", context->rx_ring_size); + } + else + { + dmspi_port_set_rx_ring(context->config.instance, context->rx_ring); + } + } + + /* Register interrupt handler if ring buffer or dmhaman handler is active */ + if (context->rx_ring != NULL || context->interrupt_handler_name != NULL) + { + if (dmspi_port_add_interrupt_handler(context->config.instance, + internal_interrupt_handler, context) != 0) + { + DMOD_LOG_ERROR("Failed to register interrupt handler\n"); + } + } + + DMOD_LOG_INFO("SPI device created for instance %u\n", context->config.instance); + + /* Populate dev_num: minor = SPI instance number (1-based). */ + dev_num->flags = DMDRVI_NUM_MINOR; + dev_num->major = 0; + dev_num->minor = (dmdrvi_dev_id_t)context->config.instance; + + /* If the config uses a named section (e.g. [flash_spi]) populate alt_name + * so the device filesystem registers the device under that human-friendly + * name instead of a numeric path. */ + char section_buf[DMDRVI_ALT_NAME_MAX_LEN + 1]; + const char *section = detect_config_section(config, section_buf, sizeof(section_buf)); + if (strcmp(section, "dmspi") != 0) + { + size_t name_len = strlen(section); + if (name_len <= DMDRVI_ALT_NAME_MAX_LEN) + { + dev_num->flags |= DMDRVI_NUM_ALT_NAME; + memcpy(dev_num->alt_name, section, name_len + 1); + } + } + + return context; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t context )) +{ + if (is_valid_context(context)) + { + if (context->rx_ring != NULL || context->interrupt_handler_name != NULL) + dmspi_port_remove_interrupt_handler(context->config.instance, context); + + if (context->rx_ring != NULL) + { + dmspi_port_set_rx_ring(context->config.instance, NULL); + dm_sw_ring_destroy(context->rx_ring); + context->rx_ring = NULL; + } + + if (context->cs_fp != NULL) + { + cs_deassert(context); + Dmod_FileClose(context->cs_fp); + context->cs_fp = NULL; + } + + Dmod_Free(context->interrupt_handler_name); + dmspi_port_deinit(context->config.instance); + context->magic = 0; + Dmod_Free(context); + } +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void*, _open, ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t *dev_num )) +{ + if (!is_valid_context(context)) + { + DMOD_LOG_ERROR("Invalid DMDRVI context in dmspi_dmdrvi_open\n"); + return NULL; + } + return context; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _close, ( dmdrvi_context_t context, void* handle )) +{ + /* No specific action needed to close the SPI device handle */ +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _read, ( dmdrvi_context_t context, void* handle, void* buffer, size_t size, uint32_t offset )) +{ + if (!is_valid_context(context) || buffer == NULL || size == 0) + return 0; + + if (context->rx_ring != NULL) + return (size_t)dm_sw_ring_read(context->rx_ring, buffer, (dm_sw_ring_capacity_t)size); + + if (context->config.role == dmspi_role_master) + cs_assert(context); + + size_t received = 0; + int ret = dmspi_port_receive(context->config.instance, (uint8_t *)buffer, size, &received); + + if (context->config.role == dmspi_role_master) + cs_deassert(context); + + if (ret != 0) + return 0; + return received; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _write, ( dmdrvi_context_t context, void* handle, const void* buffer, size_t size, uint32_t offset )) +{ + if (!is_valid_context(context) || buffer == NULL || size == 0) + return 0; + + if (context->config.role == dmspi_role_master) + cs_assert(context); + + int ret = dmspi_port_transmit(context->config.instance, (const uint8_t *)buffer, size); + + if (context->config.role == dmspi_role_master) + cs_deassert(context); + + if (ret != 0) + return 0; + return size; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _ioctl, ( dmdrvi_context_t context, void* handle, int command, void* arg )) +{ + int ret = 0; + if (!is_valid_context(context)) + { + DMOD_LOG_ERROR("Invalid DMDRVI context in dmspi_dmdrvi_ioctl\n"); + return -EINVAL; + } + + if (command >= dmspi_ioctl_cmd_max) + { + DMOD_LOG_ERROR("Invalid ioctl command %d\n", command); + return -EINVAL; + } + + if (command == dmspi_ioctl_cmd_reconfigure) + { + dmspi_port_deinit(context->config.instance); + return configure(context); + } + + if (command == dmspi_ioctl_cmd_transfer) + { + if (arg == NULL) return -EINVAL; + dmspi_transfer_t *xfer = (dmspi_transfer_t *)arg; + + if (context->config.role == dmspi_role_master) + cs_assert(context); + + ret = dmspi_port_transfer(context->config.instance, xfer->tx, xfer->rx, xfer->size); + + if (context->config.role == dmspi_role_master) + cs_deassert(context); + + return ret; + } + + if (command == dmspi_ioctl_cmd_set_interrupt_handler) + { + if (arg == NULL) + { + dmspi_port_remove_interrupt_handler(context->config.instance, context); + return 0; + } + return dmspi_port_add_interrupt_handler( + context->config.instance, + (dmspi_port_interrupt_handler_t)*(dmspi_interrupt_handler_t *)arg, + context); + } + + if (arg == NULL) + { + DMOD_LOG_ERROR("Null argument for ioctl command %d\n", command); + return -EINVAL; + } + + /* Try read first */ + ret = read_configuration(context, command, arg); + if (ret != 0) + { + /* Not a read command - try write */ + dmspi_config_t new_config; + memcpy(&new_config, &context->config, sizeof(dmspi_config_t)); + + ret = update_configuration(&new_config, command, arg); + if (ret == 0) + { + memcpy(&context->config, &new_config, sizeof(dmspi_config_t)); + dmspi_port_deinit(context->config.instance); + ret = configure(context); + } + } + + return ret; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _flush, ( dmdrvi_context_t context, void* handle )) +{ + if (!is_valid_context(context)) + { + DMOD_LOG_ERROR("Invalid DMDRVI context in dmspi_dmdrvi_flush\n"); + return -EINVAL; + } + + /* Wait for any in-progress transfer to complete */ + while (dmspi_port_is_busy(context->config.instance)) + { + /* busy-wait: SPI transfers are short, no yield primitive needed here */ + } + return 0; +} + +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _stat, ( dmdrvi_context_t context, const char* path, dmdrvi_stat_t* stat )) +{ + if (!is_valid_context(context) || stat == NULL) + { + DMOD_LOG_ERROR("Invalid parameters in dmspi_dmdrvi_stat\n"); + return -EINVAL; + } + + stat->size = 0; /* Stream/message device, no fixed size */ + stat->mode = 0666; return 0; } diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index e041637..1dde73b 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -16,6 +16,15 @@ set(DMOD_STACK_SIZE 1024) # Path to the DMR file for this module set(DMOD_DMR_PATH ${CMAKE_SOURCE_DIR}/dmspi_port.dmr) +# Determine common source files based on CPU family. Every STM32 family +# shares the same SPI register layout (see src/port/stm32_common), so this +# is where a new stm32-family port plugs into the shared implementation +# instead of duplicating it. +set(COMMON_SOURCES "") +if(DMOD_CPU_FAMILY MATCHES "^stm32") + set(COMMON_SOURCES stm32_common/stm32_common.c) +endif() + # # dmod_add_library - create a library module # it has the same signature as add_library @@ -25,12 +34,14 @@ set(DMOD_DMR_PATH ${CMAKE_SOURCE_DIR}/dmspi_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 + ${COMMON_SOURCES} ) -# To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) +dmod_link_modules(${DMOD_MODULE_NAME} + dm_sw_ring + dmclk_port + dmosi +) target_include_directories(${DMOD_MODULE_NAME} BEFORE PRIVATE ${CMAKE_SOURCE_DIR}/include diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c new file mode 100644 index 0000000..5502903 --- /dev/null +++ b/src/port/stm32_common/stm32_common.c @@ -0,0 +1,514 @@ +/* DMOD_ENABLE_REGISTRATION is intentionally NOT set here: each family's + * port.c (which includes dmspi_port.h with the flag on) is the single + * translation unit that emits this module's API registration table. + * Defining it here too would duplicate every registration entry at link + * time, since this file provides the actual function bodies that table + * points to. See dmgpio/src/port/stm32_common/stm32_common.c for the same + * split. */ +#include "dmod.h" +#include "dmosi.h" +#include "dmclk_port.h" +#include "dm_sw_ring.h" +#include "stm32_common.h" + +#define STM32_APB1PERIPH_BASE 0x40000000UL +#define STM32_APB2PERIPH_BASE 0x40010000UL + +/* Instance descriptor table - identical across STM32F4/F7 for every + * instance the two families share (base address, RCC bus/bit and IRQ + * number assignment are the same on both). Boards that don't wire up the + * higher-numbered instances simply never configure them. */ +const stm32_spi_instance_desc_t stm32_spi_instances[STM32_SPI_MAX_INSTANCES] = +{ + /* SPI1 */ { STM32_APB2PERIPH_BASE + 0x3000UL, &STM32_RCC->APB2ENR, 12U, STM32_RCC_CFGR_PPRE2_Pos, 35U }, + /* SPI2 */ { STM32_APB1PERIPH_BASE + 0x3800UL, &STM32_RCC->APB1ENR, 14U, STM32_RCC_CFGR_PPRE1_Pos, 36U }, + /* SPI3 */ { STM32_APB1PERIPH_BASE + 0x3C00UL, &STM32_RCC->APB1ENR, 15U, STM32_RCC_CFGR_PPRE1_Pos, 51U }, + /* SPI4 */ { STM32_APB2PERIPH_BASE + 0x3400UL, &STM32_RCC->APB2ENR, 13U, STM32_RCC_CFGR_PPRE2_Pos, 84U }, + /* SPI5 */ { STM32_APB2PERIPH_BASE + 0x5000UL, &STM32_RCC->APB2ENR, 20U, STM32_RCC_CFGR_PPRE2_Pos, 85U }, + /* SPI6 */ { STM32_APB2PERIPH_BASE + 0x5400UL, &STM32_RCC->APB2ENR, 21U, STM32_RCC_CFGR_PPRE2_Pos, 86U }, +}; + +/* Per-instance interrupt handler registration */ +static dmspi_port_interrupt_handler_t irq_handlers[STM32_SPI_MAX_INSTANCES]; +static void *irq_user_ptrs[STM32_SPI_MAX_INSTANCES]; + +/* Per-instance SW ring buffers - set via dmspi_port_set_rx_ring() */ +static dm_sw_ring_t rx_rings[STM32_SPI_MAX_INSTANCES]; + +void stm32_spi_common_init(void) +{ + for (unsigned i = 0; i < STM32_SPI_MAX_INSTANCES; i++) + { + irq_handlers[i] = NULL; + irq_user_ptrs[i] = NULL; + rx_rings[i] = NULL; + } +} + +void stm32_spi_common_deinit(void) +{ + /* Nothing to release: rings are owned/destroyed by the core dmspi driver */ +} + +void stm32_spi_nvic_enable_irq(uint32_t irqn) +{ + /* NVIC priority defaults to 0 (highest, non-maskable by FreeRTOS critical + * sections) after reset. Any interrupt that may call dmosi/FreeRTOS + * ISR-safe API (as this one does, via dm_sw_ring signaling a semaphore + * from within the IRQ handler) must be configured at or below the + * priority dmosi_get_min_interrupt_priority() reports, or the RTOS + * cannot safely mask it during its own critical sections. */ + 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); +} + +void stm32_spi_nvic_disable_irq(uint32_t irqn) +{ + volatile uint32_t *nvic_icer = (volatile uint32_t *)0xE000E180UL; + nvic_icer[irqn >> 5U] = 1U << (irqn & 0x1FU); +} + +static int validate_instance(dmspi_instance_t instance) +{ + return (instance >= 1 && instance <= STM32_SPI_MAX_INSTANCES) ? 0 : -1; +} + +static volatile stm32_spi_t *get_spi(dmspi_instance_t instance) +{ + return (volatile stm32_spi_t *)stm32_spi_instances[instance - 1].base; +} + +static void enable_spi_clock(dmspi_instance_t instance) +{ + const stm32_spi_instance_desc_t *desc = &stm32_spi_instances[instance - 1]; + *desc->enr |= (1U << desc->enable_bit); +} + +/* Convert a 3-bit RCC_CFGR PPREx field to its division factor. + * Encoding (same on STM32F4/F7): MSB=0 -> not divided (/1); + * 100->/2, 101->/4, 110->/8, 111->/16. */ +static uint32_t apb_prescaler_div(uint32_t ppre_bits) +{ + if ((ppre_bits & 0x4U) == 0U) return 1U; + return 2U << (ppre_bits & 0x3U); +} + +static uint32_t get_spi_clock(dmspi_instance_t instance) +{ + const stm32_spi_instance_desc_t *desc = &stm32_spi_instances[instance - 1]; + + /* Query the real, currently-configured core clock from dmclk instead of + * assuming the reset-default HSI - dmclk may have switched to a PLL/HSE + * derived frequency before this module is configured. */ + uint32_t sysclk = (uint32_t)dmclk_port_get_current_frequency(); + if (sysclk == 0U) + sysclk = STM32_HSI_VALUE; /* defensive fallback, should not normally happen */ + + uint32_t ppre = (STM32_RCC->CFGR >> desc->ppre_shift) & 0x7U; + return sysclk / apb_prescaler_div(ppre); +} + +/* ---- Port API implementation ---- */ + +dmod_dmspi_port_api_declaration(1.0, int, _init, ( dmspi_instance_t instance, dmspi_role_t role )) +{ + if (validate_instance(instance) != 0) return -1; + + enable_spi_clock(instance); + + volatile stm32_spi_t *SPI = get_spi(instance); + + SPI->CR1 = 0; + SPI->CR2 = 0; + stm32_spi_family_configure_frame_format(SPI); + + /* Default to software NSS management until dmspi_port_set_nss_mode() + * runs; SSI defaults to "selected" for a slave and "deselected" for a + * master so neither role raises a spurious mode fault before the rest + * of the configuration sequence completes. */ + SPI->CR1 |= STM32_SPI_CR1_SSM; + if (role == dmspi_role_master) + SPI->CR1 |= STM32_SPI_CR1_MSTR | STM32_SPI_CR1_SSI; + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _deinit, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + SPI->CR2 = 0; + + stm32_spi_nvic_disable_irq(stm32_spi_instances[instance - 1].irqn); + + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _transfer, ( dmspi_instance_t instance, const uint8_t* tx, uint8_t* rx, size_t size )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + + for (size_t i = 0; i < size; i++) + { + uint32_t timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_TXE)) + { + if (--timeout == 0) return -1; + } + SPI->DR = tx ? tx[i] : 0xFFU; + + timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_RXNE)) + { + if (--timeout == 0) return -1; + } + + uint32_t sr = SPI->SR; + uint8_t byte = (uint8_t)(SPI->DR & 0xFFU); /* reading DR completes the OVR clear sequence too */ + + if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) + { + /* MODF clear sequence is: read SR (done above), then write CR1 - + * re-assert SPE so the peripheral keeps running afterwards. */ + SPI->CR1 |= STM32_SPI_CR1_SPE; + return -1; + } + + if (rx != NULL) rx[i] = byte; + } + + /* Wait for the last bit to finish shifting out before returning, so a + * caller that immediately toggles chip-select does not cut it short. */ + uint32_t timeout = STM32_SPI_TIMEOUT_VALUE; + while (SPI->SR & STM32_SPI_SR_BSY) + { + if (--timeout == 0) return -1; + } + + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _transmit, ( dmspi_instance_t instance, const uint8_t* data, size_t size )) +{ + if (data == NULL) return -1; + return dmspi_port_transfer(instance, data, NULL, size); +} + +dmod_dmspi_port_api_declaration(1.0, int, _receive, ( dmspi_instance_t instance, uint8_t* data, size_t size, size_t* received )) +{ + if (validate_instance(instance) != 0 || data == NULL || received == NULL) return -1; + + dm_sw_ring_t ring = rx_rings[instance - 1]; + if (ring != NULL) + { + *received = (size_t)dm_sw_ring_read(ring, data, (dm_sw_ring_capacity_t)size); + return 0; + } + + volatile stm32_spi_t *SPI = get_spi(instance); + *received = 0; + + for (size_t i = 0; i < size; i++) + { + uint32_t timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_TXE)) + { + if (--timeout == 0) return 0; + } + SPI->DR = 0xFFU; /* dummy byte - keeps the clock/shift register moving */ + + timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_RXNE)) + { + if (--timeout == 0) return 0; + } + + uint32_t sr = SPI->SR; + data[i] = (uint8_t)(SPI->DR & 0xFFU); + + if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) + { + SPI->CR1 |= STM32_SPI_CR1_SPE; + return -1; + } + (*received)++; + } + + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, bool, _is_busy, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return false; + return (get_spi(instance)->SR & STM32_SPI_SR_BSY) != 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_role, ( dmspi_instance_t instance, dmspi_role_t role )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + + if (role == dmspi_role_master) + SPI->CR1 |= STM32_SPI_CR1_MSTR; + else + SPI->CR1 &= ~STM32_SPI_CR1_MSTR; + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_role_t, _get_role, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_role_slave; + return (get_spi(instance)->CR1 & STM32_SPI_CR1_MSTR) ? dmspi_role_master : dmspi_role_slave; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_baudrate, ( dmspi_instance_t instance, dmspi_baudrate_t baudrate )) +{ + if (validate_instance(instance) != 0 || baudrate == 0) return -1; + + uint32_t pclk = get_spi_clock(instance); + + /* BR[2:0] selects a /2../256 divisor; pick the smallest divisor whose + * resulting SCK does not exceed the requested rate. */ + uint32_t br = 0; + while (br < 7U && (pclk / (2U << br)) > baudrate) + br++; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + SPI->CR1 = (SPI->CR1 & ~STM32_SPI_CR1_BR_Msk) | (br << STM32_SPI_CR1_BR_Pos); + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_baudrate_t, _get_baudrate, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return 0; + + uint32_t pclk = get_spi_clock(instance); + uint32_t br = (get_spi(instance)->CR1 & STM32_SPI_CR1_BR_Msk) >> STM32_SPI_CR1_BR_Pos; + return (dmspi_baudrate_t)(pclk / (2U << br)); +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_clock_polarity, ( dmspi_instance_t instance, dmspi_clock_polarity_t polarity )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + + if (polarity == dmspi_clock_polarity_high) + SPI->CR1 |= STM32_SPI_CR1_CPOL; + else + SPI->CR1 &= ~STM32_SPI_CR1_CPOL; + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_clock_polarity_t, _get_clock_polarity, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_clock_polarity_low; + return (get_spi(instance)->CR1 & STM32_SPI_CR1_CPOL) ? dmspi_clock_polarity_high : dmspi_clock_polarity_low; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_clock_phase, ( dmspi_instance_t instance, dmspi_clock_phase_t phase )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + + if (phase == dmspi_clock_phase_2_edge) + SPI->CR1 |= STM32_SPI_CR1_CPHA; + else + SPI->CR1 &= ~STM32_SPI_CR1_CPHA; + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_clock_phase_t, _get_clock_phase, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_clock_phase_1_edge; + return (get_spi(instance)->CR1 & STM32_SPI_CR1_CPHA) ? dmspi_clock_phase_2_edge : dmspi_clock_phase_1_edge; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_bit_order, ( dmspi_instance_t instance, dmspi_bit_order_t bit_order )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + + if (bit_order == dmspi_bit_order_lsb_first) + SPI->CR1 |= STM32_SPI_CR1_LSBFIRST; + else + SPI->CR1 &= ~STM32_SPI_CR1_LSBFIRST; + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_bit_order_t, _get_bit_order, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_bit_order_msb_first; + return (get_spi(instance)->CR1 & STM32_SPI_CR1_LSBFIRST) ? dmspi_bit_order_lsb_first : dmspi_bit_order_msb_first; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_nss_mode, ( dmspi_instance_t instance, dmspi_nss_mode_t nss_mode )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + bool is_master = (SPI->CR1 & STM32_SPI_CR1_MSTR) != 0; + + SPI->CR1 &= ~STM32_SPI_CR1_SPE; + + if (nss_mode == dmspi_nss_mode_soft) + { + SPI->CR1 |= STM32_SPI_CR1_SSM; + /* SSI feeds the internal NSS signal when SSM=1: a master must see it + * high (deselected - CS is managed externally, e.g. via dmgpio) to + * avoid a spurious mode fault; a slave must see it low (permanently + * selected) since there is no external NSS pin to gate it. */ + if (is_master) + SPI->CR1 |= STM32_SPI_CR1_SSI; + else + SPI->CR1 &= ~STM32_SPI_CR1_SSI; + SPI->CR2 &= ~STM32_SPI_CR2_SSOE; + } + else + { + SPI->CR1 &= ~(STM32_SPI_CR1_SSM | STM32_SPI_CR1_SSI); + /* SSOE lets a master drive the physical NSS pin automatically + * (single-slave systems only); a slave always relies on the + * physical pin once SSM=0, so SSOE is left clear for it. */ + if (is_master) + SPI->CR2 |= STM32_SPI_CR2_SSOE; + else + SPI->CR2 &= ~STM32_SPI_CR2_SSOE; + } + + SPI->CR1 |= STM32_SPI_CR1_SPE; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_nss_mode_t, _get_nss_mode, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_nss_mode_soft; + return (get_spi(instance)->CR1 & STM32_SPI_CR1_SSM) ? dmspi_nss_mode_soft : dmspi_nss_mode_hard; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t trigger )) +{ + if (validate_instance(instance) != 0) return -1; + + volatile stm32_spi_t *SPI = get_spi(instance); + + SPI->CR2 &= ~(STM32_SPI_CR2_RXNEIE | STM32_SPI_CR2_TXEIE | STM32_SPI_CR2_ERRIE); + + if (trigger & dmspi_int_trigger_rx_not_empty) SPI->CR2 |= STM32_SPI_CR2_RXNEIE; + if (trigger & dmspi_int_trigger_tx_empty) SPI->CR2 |= STM32_SPI_CR2_TXEIE; + if (trigger & dmspi_int_trigger_error) SPI->CR2 |= STM32_SPI_CR2_ERRIE; + + uint32_t irqn = stm32_spi_instances[instance - 1].irqn; + if (trigger != dmspi_int_trigger_off) + stm32_spi_nvic_enable_irq(irqn); + else + stm32_spi_nvic_disable_irq(irqn); + + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _read_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t *out_trigger )) +{ + if (validate_instance(instance) != 0 || out_trigger == NULL) return -1; + + uint32_t cr2 = get_spi(instance)->CR2; + *out_trigger = dmspi_int_trigger_off; + + if (cr2 & STM32_SPI_CR2_RXNEIE) *out_trigger = (dmspi_int_trigger_t)(*out_trigger | dmspi_int_trigger_rx_not_empty); + if (cr2 & STM32_SPI_CR2_TXEIE) *out_trigger = (dmspi_int_trigger_t)(*out_trigger | dmspi_int_trigger_tx_empty); + if (cr2 & STM32_SPI_CR2_ERRIE) *out_trigger = (dmspi_int_trigger_t)(*out_trigger | dmspi_int_trigger_error); + + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _add_interrupt_handler, + ( dmspi_instance_t instance, dmspi_port_interrupt_handler_t handler, void *user_ptr )) +{ + if (validate_instance(instance) != 0 || handler == NULL) return -1; + + irq_handlers[instance - 1] = handler; + irq_user_ptrs[instance - 1] = user_ptr; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _remove_interrupt_handler, + ( dmspi_instance_t instance, void *user_ptr )) +{ + if (validate_instance(instance) != 0) return -1; + + if (irq_user_ptrs[instance - 1] == user_ptr) + { + irq_handlers[instance - 1] = NULL; + irq_user_ptrs[instance - 1] = NULL; + } + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_rx_ring, + ( dmspi_instance_t instance, dm_sw_ring_t ring )) +{ + if (validate_instance(instance) != 0) return -1; + rx_rings[instance - 1] = ring; + return 0; +} + +/* ---- ISR handler, shared by every family's DMOD_IRQ_HANDLER ---- */ + +void stm32_spi_irq_handler(dmspi_instance_t instance) +{ + if (validate_instance(instance) != 0) return; + + volatile stm32_spi_t *SPI = get_spi(instance); + uint32_t idx = (uint32_t)(instance - 1); + + uint8_t data = 0; + dmspi_int_trigger_t trigger = dmspi_int_trigger_off; + uint32_t sr = SPI->SR; + + if (sr & STM32_SPI_SR_RXNE) + { + data = (uint8_t)(SPI->DR & 0xFFU); /* reading DR clears RXNE (and completes an OVR clear) */ + trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_rx_not_empty); + + if (rx_rings[idx] != NULL) + dm_sw_ring_write(rx_rings[idx], &data, 1); + + sr = SPI->SR; + } + if (sr & STM32_SPI_SR_TXE) + trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_tx_empty); + if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) + { + (void)SPI->DR; /* completes the OVR clear sequence */ + SPI->CR1 |= STM32_SPI_CR1_SPE; /* completes the MODF clear sequence */ + trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_error); + } + + dmspi_port_interrupt_handler_t handler = irq_handlers[idx]; + if (handler != NULL && trigger != dmspi_int_trigger_off) + handler(irq_user_ptrs[idx], instance, trigger, data); +} diff --git a/src/port/stm32_common/stm32_common.h b/src/port/stm32_common/stm32_common.h new file mode 100644 index 0000000..c25cd95 --- /dev/null +++ b/src/port/stm32_common/stm32_common.h @@ -0,0 +1,161 @@ +#ifndef STM32_COMMON_H +#define STM32_COMMON_H + +#include +#include +#include "dmspi_port.h" + +/** + * @brief STM32 SPI register layout. + * + * Identical field layout on F4 and F7 (both implement the "SPI v1" IP + * block - no FIFO). The only per-family differences are a handful of + * CR1/CR2 bits, handled by stm32_spi_family_configure_frame_format(). + */ +typedef struct +{ + volatile uint32_t CR1; /**< Control register 1 */ + volatile uint32_t CR2; /**< Control register 2 */ + volatile uint32_t SR; /**< Status register */ + volatile uint32_t DR; /**< Data register */ + volatile uint32_t CRCPR; /**< CRC polynomial register */ + volatile uint32_t RXCRCR; /**< RX CRC register */ + volatile uint32_t TXCRCR; /**< TX CRC register */ + volatile uint32_t I2SCFGR; /**< I2S configuration register */ + volatile uint32_t I2SPR; /**< I2S prescaler register */ +} stm32_spi_t; + +/* SPI_CR1 bits (identical position on F4/F7, except bit 11: DFF on F4, + * CRCL on F7 - unused by this driver, CRC support is not implemented) */ +#define STM32_SPI_CR1_CPHA (1U << 0) +#define STM32_SPI_CR1_CPOL (1U << 1) +#define STM32_SPI_CR1_MSTR (1U << 2) +#define STM32_SPI_CR1_BR_Pos 3U +#define STM32_SPI_CR1_BR_Msk (0x7U << STM32_SPI_CR1_BR_Pos) +#define STM32_SPI_CR1_SPE (1U << 6) +#define STM32_SPI_CR1_LSBFIRST (1U << 7) +#define STM32_SPI_CR1_SSI (1U << 8) +#define STM32_SPI_CR1_SSM (1U << 9) +#define STM32_SPI_CR1_RXONLY (1U << 10) +#define STM32_SPI_CR1_DFF (1U << 11) /**< F4 only */ + +/* SPI_CR2 bits (bits 8-14 - DS/FRXTH/LDMA - are F7 only) */ +#define STM32_SPI_CR2_RXDMAEN (1U << 0) +#define STM32_SPI_CR2_TXDMAEN (1U << 1) +#define STM32_SPI_CR2_SSOE (1U << 2) +#define STM32_SPI_CR2_ERRIE (1U << 5) +#define STM32_SPI_CR2_RXNEIE (1U << 6) +#define STM32_SPI_CR2_TXEIE (1U << 7) +#define STM32_SPI_CR2_FRXTH (1U << 12) /**< F7 only */ +#define STM32_SPI_CR2_DS_Pos 8U /**< F7 only */ +#define STM32_SPI_CR2_DS_Msk (0xFU << STM32_SPI_CR2_DS_Pos) +#define STM32_SPI_CR2_DS_8BIT (0x7U << STM32_SPI_CR2_DS_Pos) + +/* SPI_SR bits */ +#define STM32_SPI_SR_RXNE (1U << 0) +#define STM32_SPI_SR_TXE (1U << 1) +#define STM32_SPI_SR_CRCERR (1U << 4) +#define STM32_SPI_SR_MODF (1U << 5) +#define STM32_SPI_SR_OVR (1U << 6) +#define STM32_SPI_SR_BSY (1U << 7) + +/** RCC register structure (subset shared by F4/F7 for peripheral clock enable) */ +typedef struct +{ + volatile uint32_t CR; + volatile uint32_t PLLCFGR; + volatile uint32_t CFGR; + volatile uint32_t CIR; + volatile uint32_t AHB1RSTR; + volatile uint32_t AHB2RSTR; + volatile uint32_t AHB3RSTR; + volatile uint32_t RESERVED0; + volatile uint32_t APB1RSTR; + volatile uint32_t APB2RSTR; + volatile uint32_t RESERVED1[2]; + volatile uint32_t AHB1ENR; + volatile uint32_t AHB2ENR; + volatile uint32_t AHB3ENR; + volatile uint32_t RESERVED2; + volatile uint32_t APB1ENR; + volatile uint32_t APB2ENR; +} stm32_rcc_t; + +#define STM32_RCC_BASE 0x40023800UL +#define STM32_RCC ((volatile stm32_rcc_t *)STM32_RCC_BASE) + +/* RCC_CFGR APB prescaler fields (same layout on STM32F4/F7) */ +#define STM32_RCC_CFGR_PPRE1_Pos 10U +#define STM32_RCC_CFGR_PPRE1_Msk (0x7U << STM32_RCC_CFGR_PPRE1_Pos) +#define STM32_RCC_CFGR_PPRE2_Pos 13U +#define STM32_RCC_CFGR_PPRE2_Msk (0x7U << STM32_RCC_CFGR_PPRE2_Pos) + +#define STM32_HSI_VALUE 16000000U /**< HSI clock value in Hz, used as a + * defensive fallback only */ + +/** Highest SPI instance number supported by this port (SPI1-SPI6) */ +#define STM32_SPI_MAX_INSTANCES 6U + +/** Default timeout (busy-wait iterations) for blocking SPI operations */ +#define STM32_SPI_TIMEOUT_VALUE 100000U + +/** + * @brief Per-instance descriptor: base address, enabling RCC register/bit, + * and NVIC IRQ number. Identical across F4 and F7 for every instance + * that exists on both families (SPI1-SPI6 share base address, RCC + * bus/bit and IRQ number assignment on both). + */ +typedef struct +{ + uint32_t base; /**< Peripheral base address */ + volatile uint32_t *enr; /**< RCC enable register (APB1ENR or APB2ENR) */ + uint32_t enable_bit; /**< Bit within *enr that enables this instance's clock */ + uint32_t ppre_shift; /**< PPRE field position in RCC_CFGR for this instance's bus */ + uint32_t irqn; /**< NVIC IRQ number */ +} stm32_spi_instance_desc_t; + +/** Instance descriptor table, indexed by (instance - 1). Defined in stm32_common.c. */ +extern const stm32_spi_instance_desc_t stm32_spi_instances[STM32_SPI_MAX_INSTANCES]; + +/** + * @brief Apply the family-specific frame format bits. + * + * This driver always uses 8-bit data frames. On F4 that only requires + * clearing DFF (CR1 bit 11); on F7 the newer FIFO-capable IP additionally + * needs DS[3:0]=8bit and FRXTH=1 so RXNE asserts after a single byte + * instead of waiting for a 16-bit FIFO level. Implemented once per family + * in that family's port.c, since it is the one place the two SPI IP + * revisions genuinely diverge. + * + * @param SPI Register block of the instance being configured. + */ +void stm32_spi_family_configure_frame_format(volatile stm32_spi_t *SPI); + +/** + * @brief Common EXTI-style NVIC helpers, shared by both families' port.c + * (declared here so stm32_common.c and the per-family port.c files + * agree on the same priority policy). + */ +void stm32_spi_nvic_enable_irq(uint32_t irqn); +void stm32_spi_nvic_disable_irq(uint32_t irqn); + +/** + * @brief Reset the shared interrupt-handler/RX-ring bookkeeping. + * + * Called once from each family's dmod_init()/dmod_deinit(), since those + * lifecycle hooks live in the per-family port.c, not in this shared file. + */ +void stm32_spi_common_init(void); +void stm32_spi_common_deinit(void); + +/** + * @brief Shared ISR body, called from each family's DMOD_IRQ_HANDLER. + * + * Drains RXNE into the configured RX ring (if any) and/or dispatches to a + * registered interrupt handler, and reports/clears error flags. + * + * @param instance SPI instance the firing IRQ belongs to. + */ +void stm32_spi_irq_handler(dmspi_instance_t instance); + +#endif // STM32_COMMON_H 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..2ec3de6 --- /dev/null +++ b/src/port/stm32f4/port.c @@ -0,0 +1,40 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmspi_port.h" +#include "dmod.h" +#include "../stm32_common/stm32_common.h" + +/* ---- DMOD lifecycle ---- */ + +int dmod_init(const Dmod_Config_t *Config) +{ + Dmod_Printf("dmspi port module initialized (stm32f4)\n"); + stm32_spi_common_init(); + return 0; +} + +int dmod_deinit(void) +{ + Dmod_Printf("dmspi port module deinitialized (stm32f4)\n"); + stm32_spi_common_deinit(); + return 0; +} + +/* ---- Family-specific register quirk ---- + * + * STM32F4 implements the classic SPI IP: data size is selected via + * CR1.DFF (0 = 8-bit, 1 = 16-bit) and RXNE always asserts per-byte, so + * this driver's 8-bit-only operation just needs DFF defensively cleared - + * there is no FIFO/threshold to configure like on F7. */ +void stm32_spi_family_configure_frame_format(volatile stm32_spi_t *SPI) +{ + SPI->CR1 &= ~STM32_SPI_CR1_DFF; +} + +/* ---- IRQ handlers ---- */ + +DMOD_IRQ_HANDLER(35) { stm32_spi_irq_handler(1); } /* SPI1 */ +DMOD_IRQ_HANDLER(36) { stm32_spi_irq_handler(2); } /* SPI2 */ +DMOD_IRQ_HANDLER(51) { stm32_spi_irq_handler(3); } /* SPI3 */ +DMOD_IRQ_HANDLER(84) { stm32_spi_irq_handler(4); } /* SPI4 */ +DMOD_IRQ_HANDLER(85) { stm32_spi_irq_handler(5); } /* SPI5 */ +DMOD_IRQ_HANDLER(86) { stm32_spi_irq_handler(6); } /* SPI6 */ diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index 25c87c5..b1a68df 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -1,29 +1,42 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmspi_port.h" #include "dmod.h" +#include "../stm32_common/stm32_common.h" /* ---- DMOD lifecycle ---- */ int dmod_init(const Dmod_Config_t *Config) { Dmod_Printf("dmspi port module initialized (stm32f7)\n"); + stm32_spi_common_init(); return 0; } int dmod_deinit(void) { Dmod_Printf("dmspi port module deinitialized (stm32f7)\n"); + stm32_spi_common_deinit(); return 0; } -/* ---- API implementation ---- +/* ---- Family-specific register quirk ---- * - * Implement the dmod_dmspi_port_api_declaration(...) functions - * declared in include/dmspi_port.h here. Register an interrupt - * handler if needed, e.g.: - * - * DMOD_IRQ_HANDLER(SOME_IRQn) - * { - * // handle interrupt - * } - */ + * STM32F7 implements the newer FIFO-capable SPI IP: data size is selected + * via CR2.DS[3:0] instead of CR1.DFF, and RXNE only asserts once the RX + * FIFO reaches the threshold configured by CR2.FRXTH (default: 16-bit / + * half-full). This driver only ever moves 8-bit frames, so FRXTH must be + * set to get a per-byte RXNE like STM32F4's classic SPI IP already gives + * for free. */ +void stm32_spi_family_configure_frame_format(volatile stm32_spi_t *SPI) +{ + SPI->CR2 = (SPI->CR2 & ~STM32_SPI_CR2_DS_Msk) | STM32_SPI_CR2_DS_8BIT | STM32_SPI_CR2_FRXTH; +} + +/* ---- IRQ handlers ---- */ + +DMOD_IRQ_HANDLER(35) { stm32_spi_irq_handler(1); } /* SPI1 */ +DMOD_IRQ_HANDLER(36) { stm32_spi_irq_handler(2); } /* SPI2 */ +DMOD_IRQ_HANDLER(51) { stm32_spi_irq_handler(3); } /* SPI3 */ +DMOD_IRQ_HANDLER(84) { stm32_spi_irq_handler(4); } /* SPI4 */ +DMOD_IRQ_HANDLER(85) { stm32_spi_irq_handler(5); } /* SPI5 */ +DMOD_IRQ_HANDLER(86) { stm32_spi_irq_handler(6); } /* SPI6 */ diff --git a/tests/dmspi_test.c b/tests/dmspi_test.c index 0983c00..946afca 100644 --- a/tests/dmspi_test.c +++ b/tests/dmspi_test.c @@ -1,32 +1,47 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod_test.h" #include "dmspi.h" +#include -static dmspi_t g_handle = NULL; +static dmspi_config_t g_config; void dmod_test_setup(void) { - g_handle = dmspi_create(); + memset(&g_config, 0, sizeof(g_config)); + g_config.instance = 1; + g_config.role = dmspi_role_master; + g_config.baudrate = 1000000; } void dmod_test_teardown(void) { - dmspi_destroy(g_handle); - g_handle = NULL; } -DMOD_TEST_STEP(dmspi_create) +DMOD_TEST_STEP(dmspi_validate_config_accepts_valid_master) { - DMOD_TEST_EXPECT_NOT_NULL(g_handle); + DMOD_TEST_EXPECT_TRUE(dmspi_validate_config(&g_config)); } -DMOD_TEST_STEP(dmspi_is_valid) +DMOD_TEST_STEP(dmspi_validate_config_rejects_zero_instance) { - DMOD_TEST_EXPECT_TRUE(dmspi_is_valid(g_handle)); + g_config.instance = 0; + DMOD_TEST_EXPECT_FALSE(dmspi_validate_config(&g_config)); } -DMOD_TEST_STEP(dmspi_destroy_null) +DMOD_TEST_STEP(dmspi_validate_config_rejects_master_without_baudrate) { - /* Destroying NULL must not crash. */ - dmspi_destroy(NULL); + g_config.baudrate = 0; + DMOD_TEST_EXPECT_FALSE(dmspi_validate_config(&g_config)); +} + +DMOD_TEST_STEP(dmspi_validate_config_accepts_slave_without_baudrate) +{ + g_config.role = dmspi_role_slave; + g_config.baudrate = 0; + DMOD_TEST_EXPECT_TRUE(dmspi_validate_config(&g_config)); +} + +DMOD_TEST_STEP(dmspi_validate_config_rejects_null) +{ + DMOD_TEST_EXPECT_FALSE(dmspi_validate_config(NULL)); } diff --git a/tools/spitest/CMakeLists.txt b/tools/spitest/CMakeLists.txt new file mode 100644 index 0000000..baa6a5f --- /dev/null +++ b/tools/spitest/CMakeLists.txt @@ -0,0 +1,39 @@ +# ===================================================================== +# spitest - Manual dmspi Device Test Tool +# ===================================================================== +# Added via add_subdirectory() from the top-level CMakeLists.txt, so it +# shares dmspi's own dmod fetch/configuration (DMOD_MODULE_NAME, DMOD_DIR, +# DMOD_MODULE_VERSION, ...) instead of setting up a second, separate DMOD +# project - see dmtty/tools/tty for the same pattern elsewhere in the DMOD +# ecosystem. + +# Path to the DMR file for this module - setting this before dmod_add_executable +# makes the build automatically produce a ready-to-publish release package +# (via mkdmrpkg) under build/packages/spitest.zip, right alongside dmspi.zip +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/spitest.dmr) + +dmod_add_executable(spitest ${DMOD_MODULE_VERSION} + main.c +) + +# Device access goes through Dmod_FileOpen/_Ioctl/_FileClose - the portable +# DMOD SAL wrappers around dmvfs, declared in dmod's own core headers +# (dmod_sal.h, pulled in by dmod.h). dmvfs itself is a DMOD_SYSTEM +# component built into dmod-boot, not a separately loadable module, so +# there is nothing to dmod_link_modules() for it. +# +# dmosi is a separate module - fetch its headers for the worker thread +# used by the loopback test. The real implementation is resolved at +# runtime against the system's own instance, same as dmtty/tools/tty's +# dmtty_attach() - no static linkage. +dmod_link_modules(spitest + dmosi +) + +# dmspi_types.h lives in this same repo (no dmf-get fetch needed) - it only +# pulls in dmspi_ioctl_cmd_t/dmspi_transfer_t/dmspi_role_t, not the full +# driver API, since every device access here goes through Dmod_FileOpen by +# path. +target_include_directories(spitest PRIVATE + ${CMAKE_SOURCE_DIR}/include +) diff --git a/tools/spitest/README.md b/tools/spitest/README.md new file mode 100644 index 0000000..b081678 --- /dev/null +++ b/tools/spitest/README.md @@ -0,0 +1,72 @@ +# spitest - Manual dmspi Device Test Tool + +A small DMOD application module that exercises a real SPI transfer against +an already-configured `dmspi` device and reports PASS/FAIL. It does not +configure, create, or know anything about pins/instances/roles up front - +it only takes a device path (e.g. `/dev/dmspi1`, produced by `dmdevfs` from +a config.ini - see [`../../configs/README.md`](../../configs/README.md)) +and reads that device's role (master/slave) back from it via `ioctl`. + +It sends two distinct, known byte patterns - one MOSI-bound +(master->slave), one MISO-bound (slave->master) - and checks each side +received exactly what the other side sent, in both directions. + +## Building + +Built together with dmspi itself (added via `add_subdirectory()` from the +top-level `CMakeLists.txt`): + +```bash +cmake -B build +cmake --build build +``` + +The resulting module file is produced at `build/dmf/spitest.dmf`, alongside +`build/dmf/dmspi.dmf`. + +## Usage + +From `dmell`, once the device(s) you want to test have already been +configured by `dmdevfs` (see [`../../configs/README.md`](../../configs/README.md) +for ready-made per-board configs, or write your own): + +``` +spitest +spitest +``` + +### Single device (two-board test) + +Run this on each board separately - one board's device must be configured +`role=master`, the other's `role=slave`, wired together per +[`../../configs/README.md`](../../configs/README.md)'s wiring notes: + +``` +spitest /dev/dmspi1 +``` + +Prints which role the device reports, runs the appropriate side of the +transfer, and reports PASS/FAIL. + +### Two devices (single-board loopback) + +Give it both device paths and it opens both, confirms one is master and the +other is slave (in either order), and runs the transfer between them - the +slave side in a worker thread so both sides are active at the same time: + +``` +spitest /dev/dmspi1 /dev/dmspi2 +``` + +## Exit codes + +- `0` - PASS +- `1` - communication/data mismatch, or `-h`/`--help` with no other args +- non-zero (from `main`, no specific code) - setup failure: device path + couldn't be opened, or it doesn't answer `dmspi_ioctl_cmd_get_role` + (i.e. it isn't a dmspi device) + +On a data mismatch, each mismatching byte is logged individually +(`byte N: expected 0x.., got 0x..`) to help pinpoint a wiring or timing +issue - e.g. a MOSI/MISO swap will show every byte mismatching against the +*other* direction's pattern. diff --git a/tools/spitest/main.c b/tools/spitest/main.c new file mode 100644 index 0000000..7a9de48 --- /dev/null +++ b/tools/spitest/main.c @@ -0,0 +1,246 @@ +#include "dmod.h" +#include "dmspi_types.h" +#include "dmosi.h" +#include + +/** + * @brief Manual dmspi device test tool. + * + * Exercises a real SPI transfer against an already-configured dmspi device + * node - it does not configure or create anything itself. The device is + * whatever `dmdevfs` set up from a dmspi config.ini (see + * ../../configs/README.md); this tool only needs the resulting path + * (e.g. /dev/dmspi1) and reads the device's role (master/slave) back from + * it via ioctl. + * + * Two distinct, deterministic byte patterns are used - one MOSI-bound + * (master->slave), one MISO-bound (slave->master) - so a mismatch (or a + * MOSI/MISO swap) is caught, not just "some bytes moved". + */ + +#define TEST_PATTERN_LEN 16U +#define SLAVE_READY_DELAY_MS 20U + +static void build_mosi_pattern(uint8_t *buf) +{ + for (unsigned i = 0; i < TEST_PATTERN_LEN; i++) + buf[i] = (uint8_t)i; +} + +static void build_miso_pattern(uint8_t *buf) +{ + for (unsigned i = 0; i < TEST_PATTERN_LEN; i++) + buf[i] = (uint8_t)(0xFFU - i); +} + +static bool verify_pattern(const char *label, const uint8_t *actual, const uint8_t *expected, size_t len) +{ + bool ok = true; + for (size_t i = 0; i < len; i++) + { + if (actual[i] != expected[i]) + { + if (ok) + DMOD_LOG_ERROR("%s: mismatch\n", label); + DMOD_LOG_ERROR(" byte %u: expected 0x%02X, got 0x%02X\n", + (unsigned)i, expected[i], actual[i]); + ok = false; + } + } + if (ok) + Dmod_Printf("%s: OK (%u bytes matched)\n", label, (unsigned)len); + return ok; +} + +static void *open_device(const char *path) +{ + /* Dmod_FileOpen/_Ioctl/_FileClose are the portable DMOD SAL wrappers + * around dmvfs - they work the same way from any module without + * fetching dmvfs itself (a DMOD_SYSTEM component built into dmod-boot, + * not a separately loadable module). */ + void *fp = Dmod_FileOpen(path, "r+"); + if (fp == NULL) + DMOD_LOG_ERROR("spitest: failed to open '%s'\n", path); + return fp; +} + +static bool query_role(void *fp, const char *path, dmspi_role_t *out_role) +{ + int ret = Dmod_Ioctl(fp, dmspi_ioctl_cmd_get_role, out_role); + if (ret != 0) + { + DMOD_LOG_ERROR("spitest: failed to query role of '%s' (error %d) - is it really a " + "dmspi device?\n", path, ret); + return false; + } + return true; +} + +static void run_transfer(void *fp, bool as_master, bool *ok) +{ + uint8_t tx[TEST_PATTERN_LEN]; + uint8_t rx[TEST_PATTERN_LEN] = {0}; + if (as_master) + build_mosi_pattern(tx); + else + build_miso_pattern(tx); + + dmspi_transfer_t xfer = { .tx = tx, .rx = rx, .size = TEST_PATTERN_LEN }; + int ret = Dmod_Ioctl(fp, dmspi_ioctl_cmd_transfer, &xfer); + if (ret != 0) + { + DMOD_LOG_ERROR("spitest: transfer failed (error %d)%s\n", ret, + as_master ? " - check wiring" : " - is the master running and wired correctly?"); + *ok = false; + return; + } + + uint8_t expected[TEST_PATTERN_LEN]; + if (as_master) + build_miso_pattern(expected); + else + build_mosi_pattern(expected); + + *ok = verify_pattern(as_master ? "MISO (slave->master)" : "MOSI (master->slave)", + rx, expected, TEST_PATTERN_LEN); +} + +/* ---- single device: this board holds only one side of the link, the + * other side is a peer board wired to it ---- */ + +static int run_single(const char *path) +{ + void *fp = open_device(path); + if (fp == NULL) + return -1; + + dmspi_role_t role; + if (!query_role(fp, path, &role)) + { + Dmod_FileClose(fp); + return -1; + } + + bool as_master = (role == dmspi_role_master); + Dmod_Printf("spitest: '%s' is configured as %s\n", path, as_master ? "master" : "slave"); + if (!as_master) + Dmod_Printf("spitest: waiting for the peer board's master to clock data in...\n"); + + bool ok = false; + run_transfer(fp, as_master, &ok); + Dmod_FileClose(fp); + + Dmod_Printf("\n=== %s test ('%s'): %s ===\n", + as_master ? "Master" : "Slave", path, ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} + +/* ---- two devices on this board: one worker thread per side, so both + * run their (blocking) transfer at the same time ---- */ + +typedef struct +{ + void *fp; + bool as_master; + bool ok; +} worker_ctx_t; + +static void worker_thread_entry(void *arg) +{ + worker_ctx_t *ctx = (worker_ctx_t *)arg; + run_transfer(ctx->fp, ctx->as_master, &ctx->ok); +} + +static int run_loopback(const char *path_a, const char *path_b) +{ + void *fp_a = open_device(path_a); + void *fp_b = open_device(path_b); + if (fp_a == NULL || fp_b == NULL) + { + if (fp_a) Dmod_FileClose(fp_a); + if (fp_b) Dmod_FileClose(fp_b); + return -1; + } + + dmspi_role_t role_a, role_b; + if (!query_role(fp_a, path_a, &role_a) || !query_role(fp_b, path_b, &role_b)) + { + Dmod_FileClose(fp_a); + Dmod_FileClose(fp_b); + return -1; + } + if (role_a == role_b) + { + DMOD_LOG_ERROR("spitest: '%s' and '%s' are both configured as %s - one must be " + "master and the other slave\n", path_a, path_b, + (role_a == dmspi_role_master) ? "master" : "slave"); + Dmod_FileClose(fp_a); + Dmod_FileClose(fp_b); + return -1; + } + + void *master_fp = (role_a == dmspi_role_master) ? fp_a : fp_b; + void *slave_fp = (role_a == dmspi_role_master) ? fp_b : fp_a; + const char *master_path = (role_a == dmspi_role_master) ? path_a : path_b; + const char *slave_path = (role_a == dmspi_role_master) ? path_b : path_a; + Dmod_Printf("spitest: '%s' is master, '%s' is slave\n", master_path, slave_path); + + worker_ctx_t slave_ctx = { .fp = slave_fp, .as_master = false, .ok = false }; + dmosi_thread_t slave_thread = dmosi_thread_create(worker_thread_entry, &slave_ctx, + 1, 2048U + DMOSI_THREAD_STACK_OVERHEAD, "spitest_slave", dmosi_process_current()); + if (slave_thread == NULL) + { + DMOD_LOG_ERROR("spitest: failed to start slave worker thread\n"); + Dmod_FileClose(fp_a); + Dmod_FileClose(fp_b); + return -1; + } + + /* Give the slave task a moment to reach its blocking transfer before the + * master starts clocking, so it doesn't miss the first byte. */ + dmosi_thread_sleep(SLAVE_READY_DELAY_MS); + + bool master_ok = false; + run_transfer(master_fp, true, &master_ok); + + dmosi_thread_join(slave_thread); + + Dmod_FileClose(fp_a); + Dmod_FileClose(fp_b); + + bool ok = master_ok && slave_ctx.ok; + Dmod_Printf("\n=== Loopback test ('%s' master <-> '%s' slave): %s ===\n", + master_path, slave_path, ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} + +static void print_usage(const char *prog) +{ + Dmod_Printf("Usage: %s [peer_device_path]\n", prog); + Dmod_Printf("\n"); + Dmod_Printf("Exercises a real SPI transfer on an already-configured dmspi device -\n"); + Dmod_Printf("see ../../configs/README.md for how to get one via dmdevfs. The\n"); + Dmod_Printf("device's role (master/slave) is read back from it via ioctl, not\n"); + Dmod_Printf("passed on the command line.\n"); + Dmod_Printf("\n"); + Dmod_Printf(" Run against a single device (use on each board\n"); + Dmod_Printf(" separately for a two-board master<->slave test).\n"); + Dmod_Printf(" [peer_device_path] Also open a second device on THIS board and run a\n"); + Dmod_Printf(" loopback test between the two - one must already be\n"); + Dmod_Printf(" configured as master, the other as slave, and wired\n"); + Dmod_Printf(" together with a cable.\n"); +} + +int main(int argc, char *argv[]) +{ + if (argc < 2 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) + { + print_usage(argv[0]); + return (argc < 2) ? 1 : 0; + } + + if (argc >= 3) + return (run_loopback(argv[1], argv[2]) == 0) ? 0 : 1; + + return (run_single(argv[1]) == 0) ? 0 : 1; +} diff --git a/tools/spitest/spitest.dmr b/tools/spitest/spitest.dmr new file mode 100644 index 0000000..9161984 --- /dev/null +++ b/tools/spitest/spitest.dmr @@ -0,0 +1,30 @@ +# DMOD Resource File for spitest +# This file specifies where resources should be installed + +# === Core Module === +# Main module file - always installed +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] + +# Compressed module +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] + +# Dependencies file (if exists) +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] + +# Version information +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# README file +# NOTE: ${repo_dir} is the top-level repo root (CMAKE_SOURCE_DIR), since +# spitest is built via add_subdirectory() from dmspi's own CMakeLists.txt - +# so paths here are given relative to the repo root, not to tools/spitest +# itself. +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/tools/spitest/README.md] + +# === License === +# License file (shared with the rest of the dmspi repository) +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/tools/spitest/${module}.dmr] From 815ff080ff618d629d13b2a8e1b414cd2c06cf94 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Mon, 21 Sep 2026 19:33:28 +0200 Subject: [PATCH 02/10] Fix cs_path: lazy-open CS pin instead of opening at create time Testing on real STM32F746G-DISCO hardware found two bugs: - The cs_path device path format was wrong: dmdevfs mounts alt-named devices under /dev/dmgpio/, not a flat /dev/. Fixed in all 8 board configs. - More fundamentally, dmspi_dmdrvi_create() was opening cs_path eagerly, which fails whenever dmspi's own config is part of dmdevfs's initial boot-time scan (the normal case): dmdevfs's /dev mount isn't marked "ready" until that whole scan finishes, so Dmod_FileOpen() on any other /dev path fails during that window even when the path is correct. Fix the path alone wasn't enough - confirmed the same path opens fine from an interactive shell after boot, but not from within the boot scan itself. Fixed by opening cs_path lazily on first transfer instead of at create time, which sidesteps the ordering problem entirely. Verified end-to-end on hardware: dmspi now creates successfully, the device appears under /dev, spitest opens it and queries its role, and a transfer correctly asserts/deasserts the CS pin (confirmed via readback) before timing out cleanly (bounded, no hang) with nothing wired to MISO. Co-Authored-By: Claude Sonnet 5 --- configs/README.md | 19 +++--- configs/board/nucleo-f401re/spi1.ini | 20 +++--- configs/board/nucleo-f411re/spi1.ini | 20 +++--- configs/board/nucleo-f446re/spi1.ini | 20 +++--- configs/board/nucleo-f767zi/spi1.ini | 15 +++-- configs/board/stm32f4-discovery/spi1.ini | 17 +++--- configs/board/stm32f429i-discovery/spi5.ini | 11 ++-- configs/board/stm32f746g-disco/spi2.ini | 21 ++++--- configs/board/stm32f769i-discovery/spi2.ini | 15 +++-- docs/configuration.md | 32 +++++++--- src/dmspi.c | 68 +++++++++++++++------ 11 files changed, 170 insertions(+), 88 deletions(-) diff --git a/configs/README.md b/configs/README.md index 08ae07f..fffcb33 100644 --- a/configs/README.md +++ b/configs/README.md @@ -75,14 +75,17 @@ The SPI peripheral itself never drives chip-select (`nss_mode` stays plain `dmgpio` pin rather than a native NSS signal. Every file below configures the relevant CS pin as its own standalone `dmgpio` device (`[..._cs]`, declared *before* the `dmspi` section that needs it) and -references it from the `dmspi` section's `cs_path`, so dmspi opens that -device and asserts/deasserts it automatically around each transfer - see +references it from the `dmspi` section's `cs_path`, so dmspi +asserts/deasserts it automatically around each transfer - see [docs/configuration.md](../docs/configuration.md)'s "Chip select" section -for exactly how. On the four generic Arduino-header boards this assumes a -single shield device wired to D10 (the Arduino-standard CS pin); on the -accelerometer/gyroscope boards it's the one fixed onboard device. If your -setup doesn't match either (multiple slaves, a shield that manages its own -CS, ...), drop `cs_path` from the `dmspi` section and manage CS yourself. +for exactly how (including the `/dev/dmgpio/` path +format - confirmed on real hardware, **not** a flat `/dev/`) and why +it's opened lazily rather than at boot. On the four generic Arduino-header +boards this assumes a single shield device wired to D10 (the +Arduino-standard CS pin); on the accelerometer/gyroscope boards it's the +one fixed onboard device. If your setup doesn't match either (multiple +slaves, a shield that manages its own CS, ...), drop `cs_path` from the +`dmspi` section and manage CS yourself. ### Example (nucleo-f401re/spi1.ini) @@ -133,7 +136,7 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio1/arduino_spi_cs cs_active_level=low ``` diff --git a/configs/board/nucleo-f401re/spi1.ini b/configs/board/nucleo-f401re/spi1.ini index 17af78c..5a52ef6 100644 --- a/configs/board/nucleo-f401re/spi1.ini +++ b/configs/board/nucleo-f401re/spi1.ini @@ -10,13 +10,17 @@ ; ; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below ; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) -; so dmspi drives it low/high around every transfer automatically. This -; assumes a single shield device on D10, the common case - for a shield -; that uses a different CS pin, or manages multiple devices itself, -; remove cs_path from [arduino_spi] and drive CS yourself instead. -; [arduino_spi_cs] must stay declared before [arduino_spi] in this file -; for cs_path to resolve to an existing device. +; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - +; port index 1 = port B, confirmed on real hardware; see +; docs/configuration.md's "Chip select" section for the path format and +; why it's opened lazily, not at boot) so dmspi drives it low/high around +; every transfer automatically. This assumes a single shield device on +; D10, the common case - for a shield that uses a different CS pin, or +; manages multiple devices itself, remove cs_path from [arduino_spi] and +; drive CS yourself instead. [arduino_spi_cs] must stay declared before +; [arduino_spi] in this file so it exists before [arduino_spi] tries to +; open it (though since the open is lazy, on first transfer, this mainly +; just keeps the file readable in the order things happen). [arduino_spi_sck] driver_name=dmgpio @@ -64,5 +68,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio1/arduino_spi_cs cs_active_level=low diff --git a/configs/board/nucleo-f411re/spi1.ini b/configs/board/nucleo-f411re/spi1.ini index 69253d0..ee01930 100644 --- a/configs/board/nucleo-f411re/spi1.ini +++ b/configs/board/nucleo-f411re/spi1.ini @@ -10,13 +10,17 @@ ; ; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below ; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) -; so dmspi drives it low/high around every transfer automatically. This -; assumes a single shield device on D10, the common case - for a shield -; that uses a different CS pin, or manages multiple devices itself, -; remove cs_path from [arduino_spi] and drive CS yourself instead. -; [arduino_spi_cs] must stay declared before [arduino_spi] in this file -; for cs_path to resolve to an existing device. +; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - +; port index 1 = port B, confirmed on real hardware; see +; docs/configuration.md's "Chip select" section for the path format and +; why it's opened lazily, not at boot) so dmspi drives it low/high around +; every transfer automatically. This assumes a single shield device on +; D10, the common case - for a shield that uses a different CS pin, or +; manages multiple devices itself, remove cs_path from [arduino_spi] and +; drive CS yourself instead. [arduino_spi_cs] must stay declared before +; [arduino_spi] in this file so it exists before [arduino_spi] tries to +; open it (though since the open is lazy, on first transfer, this mainly +; just keeps the file readable in the order things happen). [arduino_spi_sck] driver_name=dmgpio @@ -64,6 +68,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio1/arduino_spi_cs cs_active_level=low diff --git a/configs/board/nucleo-f446re/spi1.ini b/configs/board/nucleo-f446re/spi1.ini index 7f70737..23bc596 100644 --- a/configs/board/nucleo-f446re/spi1.ini +++ b/configs/board/nucleo-f446re/spi1.ini @@ -10,13 +10,17 @@ ; ; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below ; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (mounted under /dev/arduino_spi_cs) -; so dmspi drives it low/high around every transfer automatically. This -; assumes a single shield device on D10, the common case - for a shield -; that uses a different CS pin, or manages multiple devices itself, -; remove cs_path from [arduino_spi] and drive CS yourself instead. -; [arduino_spi_cs] must stay declared before [arduino_spi] in this file -; for cs_path to resolve to an existing device. +; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - +; port index 1 = port B, confirmed on real hardware; see +; docs/configuration.md's "Chip select" section for the path format and +; why it's opened lazily, not at boot) so dmspi drives it low/high around +; every transfer automatically. This assumes a single shield device on +; D10, the common case - for a shield that uses a different CS pin, or +; manages multiple devices itself, remove cs_path from [arduino_spi] and +; drive CS yourself instead. [arduino_spi_cs] must stay declared before +; [arduino_spi] in this file so it exists before [arduino_spi] tries to +; open it (though since the open is lazy, on first transfer, this mainly +; just keeps the file readable in the order things happen). [arduino_spi_sck] driver_name=dmgpio @@ -64,6 +68,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio1/arduino_spi_cs cs_active_level=low diff --git a/configs/board/nucleo-f767zi/spi1.ini b/configs/board/nucleo-f767zi/spi1.ini index 3e6c169..0da5ce4 100644 --- a/configs/board/nucleo-f767zi/spi1.ini +++ b/configs/board/nucleo-f767zi/spi1.ini @@ -22,11 +22,14 @@ ; ; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] ; below configures D10 (PD14) as a plain dmgpio output, and -; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted -; under /dev/arduino_spi_cs) so dmspi drives it low/high around every -; transfer automatically - assuming a single shield device on D10 and -; that the PD14 guess above is right. For a shield that uses a different -; CS pin, remove cs_path from [arduino_spi] and drive CS yourself instead. +; [arduino_spi]'s cs_path points dmspi at the resulting device +; (/dev/dmgpio3/arduino_spi_cs - port index 3 = port D; see +; docs/configuration.md's "Chip select" section for the path format, +; confirmed on real hardware for a different board/port) so dmspi drives +; it low/high around every transfer automatically - assuming a single +; shield device on D10 and that the PD14 guess above is right. For a +; shield that uses a different CS pin, remove cs_path from [arduino_spi] +; and drive CS yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -74,6 +77,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio3/arduino_spi_cs cs_active_level=low diff --git a/configs/board/stm32f4-discovery/spi1.ini b/configs/board/stm32f4-discovery/spi1.ini index 6259fe0..8482f50 100644 --- a/configs/board/stm32f4-discovery/spi1.ini +++ b/configs/board/stm32f4-discovery/spi1.ini @@ -19,13 +19,14 @@ ; ; CS is not part of the SPI peripheral (nss_mode=soft) - [mems_cs] below ; configures PE3 as a plain dmgpio output, and [mems_spi]'s cs_path points -; dmspi at the resulting device (mounted under /dev/mems_cs - the section -; name, since it isn't literally "dmgpio") so dmspi drives it low/high -; around every transfer automatically. driver_order=3 on both sections -; only guarantees they run before/with other order-3 drivers on this -; board; [mems_cs] must still be declared (and therefore created) before -; [mems_spi] within this file for cs_path to resolve to an existing -; device - keep that ordering if you edit this file. +; dmspi at the resulting device (/dev/dmgpio4/mems_cs - port index 4 = +; port E, confirmed on real hardware for a different board/port; see +; docs/configuration.md's "Chip select" section for the path format and +; why the open happens lazily, on first transfer, not at boot) so dmspi +; drives it low/high around every transfer automatically. [mems_cs] must +; still be declared before [mems_spi] within this file, so it exists +; first even though the actual open is lazy - keep that ordering if you +; edit this file. [mems_spi_sck] driver_name=dmgpio @@ -73,5 +74,5 @@ baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_path=/dev/mems_cs +cs_path=/dev/dmgpio4/mems_cs cs_active_level=low diff --git a/configs/board/stm32f429i-discovery/spi5.ini b/configs/board/stm32f429i-discovery/spi5.ini index 3209775..6f03621 100644 --- a/configs/board/stm32f429i-discovery/spi5.ini +++ b/configs/board/stm32f429i-discovery/spi5.ini @@ -13,10 +13,13 @@ ; ; CS is not part of the SPI peripheral (nss_mode=soft) - [gyro_cs] below ; configures PC1 as a plain dmgpio output, and [gyro_spi]'s cs_path points -; dmspi at the resulting device (mounted under /dev/gyro_cs) so dmspi +; dmspi at the resulting device (/dev/dmgpio2/gyro_cs - port index 2 = +; port C, confirmed on real hardware for a different board/port; see +; docs/configuration.md's "Chip select" section for the path format and +; why the open happens lazily, on first transfer, not at boot) so dmspi ; drives it low/high around every transfer automatically. [gyro_cs] must -; stay declared before [gyro_spi] in this file for cs_path to resolve to -; an existing device. +; stay declared before [gyro_spi] in this file, so it exists first even +; though the actual open is lazy. [gyro_spi_sck] driver_name=dmgpio @@ -64,5 +67,5 @@ baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_path=/dev/gyro_cs +cs_path=/dev/dmgpio2/gyro_cs cs_active_level=low diff --git a/configs/board/stm32f746g-disco/spi2.ini b/configs/board/stm32f746g-disco/spi2.ini index 4d236cc..d0e90e0 100644 --- a/configs/board/stm32f746g-disco/spi2.ini +++ b/configs/board/stm32f746g-disco/spi2.ini @@ -17,12 +17,19 @@ ; part - verify against the datasheet before relying on it. ; ; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] -; below configures D10 (PI0) as a plain dmgpio output, and -; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted -; under /dev/arduino_spi_cs) so dmspi drives it low/high around every -; transfer automatically - assuming a single shield device on D10. For a -; shield that uses a different CS pin, remove cs_path from [arduino_spi] -; and drive CS yourself instead. +; below configures D10 (PI0) as a plain dmgpio output, and [arduino_spi]'s +; cs_path points dmspi at the resulting device so dmspi drives it low/high +; around every transfer automatically - assuming a single shield device on +; D10. For a shield that uses a different CS pin, remove cs_path from +; [arduino_spi] and drive CS yourself instead. +; +; The path is /dev/dmgpio/, NOT a flat +; /dev/ - confirmed on real STM32F746G-DISCO hardware: dmgpio +; mounts each alt-named device under a per-port class directory +; (port_index: A=0, B=1, ... I=8, so PI0 -> dmgpio8). Getting this wrong +; makes dmspi's Dmod_FileOpen(cs_path) fail at create time (logged, not a +; hang - dmdevfs just reports "Failed to configure driver: dmspi" and boot +; continues). [arduino_spi_sck] driver_name=dmgpio @@ -70,5 +77,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio8/arduino_spi_cs cs_active_level=low diff --git a/configs/board/stm32f769i-discovery/spi2.ini b/configs/board/stm32f769i-discovery/spi2.ini index 3abec6a..78356c2 100644 --- a/configs/board/stm32f769i-discovery/spi2.ini +++ b/configs/board/stm32f769i-discovery/spi2.ini @@ -18,11 +18,14 @@ ; ; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] ; below configures D10 (PF7) as a plain dmgpio output, and -; [arduino_spi]'s cs_path points dmspi at the resulting device (mounted -; under /dev/arduino_spi_cs) so dmspi drives it low/high around every -; transfer automatically - assuming a single shield device on D10. For a -; shield that uses a different CS pin, remove cs_path from [arduino_spi] -; and drive CS yourself instead. +; [arduino_spi]'s cs_path points dmspi at the resulting device +; (/dev/dmgpio5/arduino_spi_cs - port index 5 = port F; see +; docs/configuration.md's "Chip select" section for the path format, +; confirmed on real hardware for a different board/port, and why the open +; happens lazily, on first transfer, not at boot) so dmspi drives it +; low/high around every transfer automatically - assuming a single shield +; device on D10. For a shield that uses a different CS pin, remove +; cs_path from [arduino_spi] and drive CS yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -70,5 +73,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/arduino_spi_cs +cs_path=/dev/dmgpio5/arduino_spi_cs cs_active_level=low diff --git a/docs/configuration.md b/docs/configuration.md index 3acf299..ee236c7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,7 +20,7 @@ do it): | `clock_phase` | string | "1edge" | `"1edge"` or `"2edge"` (CPHA) | | `bit_order` | string | "msb_first" | `"msb_first"` or `"lsb_first"` | | `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS - see `cs_path` below) or `"hard"` (peripheral drives/reads the physical NSS pin) | -| `cs_path` | string | (none) | Path to a separately dmdevfs-configured `dmgpio` output device (e.g. `/dev/flash_cs`) - if set, dmspi asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave`. See "Chip select" below | +| `cs_path` | string | (none) | Path to a separately dmdevfs-configured `dmgpio` output device (e.g. `/dev/dmgpio0/flash_cs` - see "Chip select" below for the path format) - if set, dmspi asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave` | | `cs_active_level` | string | "low" | `"low"` or `"high"` - which level `cs_path` is driven to when asserting (selecting) the device | | `interrupt_trigger` | string | "off" | `"off"`, `"rx_not_empty"`, `"tx_empty"`, or `"error"` | | `interrupt_handler` | string | (none) | Name of a dmhaman-registered handler to call on interrupt | @@ -37,8 +37,24 @@ more flexibility than that: one bus master talking to several slaves, each with its own CS line, or a CS pin the SPI peripheral doesn't own directly. `cs_path` covers the common case - **one** fixed slave device - by pointing dmspi at a `dmgpio` output pin configured as its own separate `dmdevfs` -device; dmspi opens it once (`Dmod_FileOpen`) and asserts/deasserts it via -`Dmod_Ioctl` around every `write`/`read`/`dmspi_ioctl_cmd_transfer` call: +device; dmspi asserts/deasserts it via `Dmod_Ioctl` around every +`write`/`read`/`dmspi_ioctl_cmd_transfer` call. + +The path must be the pin's real `dmdevfs` path, confirmed on real +hardware to be `/dev/dmgpio/` (`port_index`: +A=0, B=1, ... - e.g. a pin on PA4 named `[flash_cs]` ends up at +`/dev/dmgpio0/flash_cs`), **not** a flat `/dev/`. + +dmspi does **not** open `cs_path` at device-creation time - only lazily, on +the first actual transfer. This matters if your `dmspi` section is itself +part of `dmdevfs`'s initial boot-time config scan (the normal case, e.g. +via `configs/board/.../*.ini`): at that point in boot, `dmdevfs`'s own +`/dev` mount isn't marked ready yet, so `Dmod_FileOpen()` on *any* other +`/dev` path - including a perfectly correct `cs_path` - fails during that +window (confirmed on real STM32F746G-DISCO hardware: opening the exact +same path from an interactive shell afterwards works fine). Opening +lazily instead, on first use, sidesteps this entirely, since by the time +anything actually transfers data, boot has long finished. ```ini ; The GPIO pin itself, as a normal standalone dmgpio device @@ -59,7 +75,7 @@ instance=1 role=master baudrate=4000000 mode=0 -cs_path=/dev/flash_cs +cs_path=/dev/dmgpio0/flash_cs cs_active_level=low ``` @@ -84,7 +100,7 @@ role=master baudrate=1000000 mode=0 nss_mode=soft -cs_path=/dev/flash_cs +cs_path=/dev/dmgpio0/flash_cs ``` ### Master talking to a device that needs mode 3 @@ -149,9 +165,9 @@ build-tested tool built this way (it takes a device path, reads its role back, and runs the appropriate side of a loopback test - no config or pins guessed or hardcoded). -Chip-select is not managed by dmspi in `nss_mode=soft` (the common case for -multi-slave master setups) - toggle a `dmgpio` pin around each transaction -instead. +`spitest` itself doesn't set `cs_path` (it talks to a peer board or a +loopback pair, not a fixed slave device), so it doesn't demonstrate CS +management - see "Chip select" above for that. ## Usage without dmdevfs (advanced) diff --git a/src/dmspi.c b/src/dmspi.c index a03f3cb..f216355 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -53,23 +53,57 @@ struct dmdrvi_context dm_sw_ring_t rx_ring; /**< Software ring buffer for received bytes */ uint32_t rx_ring_size; /**< Capacity of the RX ring buffer (from config) */ dm_sw_ring_flags_t rx_ring_wait_flags; /**< RX ring read-wait behavior (from config) */ - void *cs_fp; /**< dmgpio device handle for the CS pin (NULL = unmanaged) */ + char *cs_path; /**< CS pin device path (NULL = unmanaged); see cs_ensure_open() */ + void *cs_fp; /**< dmgpio device handle for the CS pin, opened lazily */ bool cs_active_high; /**< true = assert by driving the pin high instead of low */ + bool cs_open_failed; /**< true once a failed lazy-open has been logged (avoid log spam) */ }; -static void cs_assert(dmdrvi_context_t context) +static void cs_deassert(dmdrvi_context_t context) { if (context->cs_fp == NULL) return; - int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_HIGH : DMGPIO_PINS_STATE_ALL_LOW; + int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_LOW : DMGPIO_PINS_STATE_ALL_HIGH; Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); } -static void cs_deassert(dmdrvi_context_t context) +/** + * @brief Open context->cs_path on first use, caching the handle. + * + * Deliberately NOT opened at _create() time: _create() can run as part of + * dmdevfs's initial boot-time config scan, before dmdevfs's own /dev mount + * is marked "ready" (see dmfsi_dmdevfs_mounted() in dmdevfs.c) - + * Dmod_FileOpen() on another /dev path fails during that window even for a + * perfectly valid path (confirmed on real STM32F746G-DISCO hardware: the + * exact same path that fails during boot opens fine once queried + * afterwards, e.g. from an interactive shell). By the time a caller + * actually transfers data, boot has long finished, so opening here instead + * of at create time sidesteps the ordering problem entirely. + */ +static bool cs_ensure_open(dmdrvi_context_t context) { + if (context->cs_fp != NULL) + return true; + if (context->cs_path == NULL || context->cs_open_failed) + return false; + + context->cs_fp = Dmod_FileOpen(context->cs_path, "r+"); if (context->cs_fp == NULL) + { + DMOD_LOG_ERROR("Failed to open CS pin device '%s'\n", context->cs_path); + context->cs_open_failed = true; + return false; + } + + cs_deassert(context); /* start deselected */ + return true; +} + +static void cs_assert(dmdrvi_context_t context) +{ + if (!cs_ensure_open(context)) return; - int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_LOW : DMGPIO_PINS_STATE_ALL_HIGH; + int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_HIGH : DMGPIO_PINS_STATE_ALL_LOW; Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); } @@ -302,12 +336,13 @@ static int read_config_parameters(dmdrvi_context_t context, dmini_context_t conf } /** - * @brief Open the CS pin device named by 'cs_path', if configured. + * @brief Record the CS pin device path named by 'cs_path', if configured. * - * Requires context->config.role to already be set (read_config_parameters() - * must run first). A no-op (returns 0) when cs_path is absent, or when - * role=slave (a slave never drives its own CS - see the file header - * comment on CS management). + * Does not open it - see cs_ensure_open()'s comment for why that has to + * wait until first use. Requires context->config.role to already be set + * (read_config_parameters() must run first). A no-op (returns 0) when + * cs_path is absent, or when role=slave (a slave never drives its own CS - + * see the file header comment on CS management). */ static int setup_cs_pin(dmdrvi_context_t context, dmini_context_t config) { @@ -327,14 +362,13 @@ static int setup_cs_pin(dmdrvi_context_t context, dmini_context_t config) context->cs_active_high = (strcmp( dmini_get_string(config, section, "cs_active_level", "low"), "high") == 0); - context->cs_fp = Dmod_FileOpen(cs_path, "r+"); - if (context->cs_fp == NULL) + context->cs_path = Dmod_StrDup(cs_path); + if (context->cs_path == NULL) { - DMOD_LOG_ERROR("Failed to open CS pin device '%s'\n", cs_path); - return -EINVAL; + DMOD_LOG_ERROR("Failed to allocate CS pin path\n"); + return -ENOMEM; } - cs_deassert(context); /* start deselected */ return 0; } @@ -488,8 +522,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_c configure(context) != 0) { DMOD_LOG_ERROR("Failed to create DMDRVI context with provided configuration\n"); - if (context->cs_fp != NULL) - Dmod_FileClose(context->cs_fp); + Dmod_Free(context->cs_path); Dmod_Free(context->interrupt_handler_name); Dmod_Free(context); return NULL; @@ -570,6 +603,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t cont Dmod_FileClose(context->cs_fp); context->cs_fp = NULL; } + Dmod_Free(context->cs_path); Dmod_Free(context->interrupt_handler_name); dmspi_port_deinit(context->config.instance); From 8ceac7db09c95c130251a9787cacf469d2607b66 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Mon, 21 Sep 2026 20:00:31 +0200 Subject: [PATCH 03/10] Replace cs_path (dmdevfs VFS path) with cs_pin (dmgpio pin-lease API) The cs_path design - Dmod_FileOpen() on a dmdevfs /dev path, opened lazily, driven via hardcoded dmgpio ioctl constants - had three real problems: the /dev/dmgpio/ path format is an implementation detail the caller had to compute by hand (got it wrong twice while writing the board configs); the ioctl constants were a manually-copied guess at dmgpio's ABI; and the lazy-open only existed to work around dmdevfs's /dev mount not being "ready" during its own boot-time config scan, so a broken CS pin only surfaced on first transfer, not at config time. dmgpio now exposes a pin-lease API (dmgpio_lease.h) built for exactly this: dmgpio_pin_acquire/_release/_write, a direct Built-in API dependency resolved by the loader at module-load time, not a runtime /dev lookup, with real collision detection (dmgpio_port_claim_pins). cs_pin names the pin directly ("PE3", the same syntax dmgpio's own pin= already uses) and dmspi claims it eagerly in _create() - a bad or already-claimed pin now fails at config time instead of silently on first transfer. The lease also configures the pin itself, so the separate [xxx_cs] dmgpio board-config section is gone entirely. Requires a dmgpio release with the pin-lease API and the packaging fix in choco-technologies/dmgpio (pushed to master there); dmod_link_modules here will not resolve dmgpio's headers until that release exists. Verified locally in the meantime by building against the fixed dmgpio checkout directly (stm32f4, stm32f7, and the x86_64 "core" build all compile and link cleanly). Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + configs/README.md | 36 ++--- configs/board/nucleo-f401re/spi1.ini | 31 +--- configs/board/nucleo-f411re/spi1.ini | 31 +--- configs/board/nucleo-f446re/spi1.ini | 31 +--- configs/board/nucleo-f767zi/spi1.ini | 28 +--- configs/board/stm32f4-discovery/spi1.ini | 25 +-- configs/board/stm32f429i-discovery/spi5.ini | 24 +-- configs/board/stm32f746g-disco/spi2.ini | 31 +--- configs/board/stm32f769i-discovery/spi2.ini | 27 +--- docs/configuration.md | 74 ++++----- src/dmspi.c | 171 ++++++++++---------- 12 files changed, 182 insertions(+), 328 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 03886cc..d1cea21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ dmod_link_modules(${DMOD_MODULE_NAME} dmini dmhaman dm_sw_ring + dmgpio ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE diff --git a/configs/README.md b/configs/README.md index fffcb33..0a3a38a 100644 --- a/configs/README.md +++ b/configs/README.md @@ -72,20 +72,17 @@ first). The SPI peripheral itself never drives chip-select (`nss_mode` stays `soft` throughout this directory) - dmspi still manages it, just as a -plain `dmgpio` pin rather than a native NSS signal. Every file below -configures the relevant CS pin as its own standalone `dmgpio` device -(`[..._cs]`, declared *before* the `dmspi` section that needs it) and -references it from the `dmspi` section's `cs_path`, so dmspi -asserts/deasserts it automatically around each transfer - see +plain GPIO pin rather than a native NSS signal. Every `dmspi` section +below names its CS pin directly via `cs_pin` (e.g. `cs_pin=PB6`); dmspi +claims and configures that pin itself through dmgpio's pin-lease API, so +there's no separate `dmgpio` section to declare for it - see [docs/configuration.md](../docs/configuration.md)'s "Chip select" section -for exactly how (including the `/dev/dmgpio/` path -format - confirmed on real hardware, **not** a flat `/dev/`) and why -it's opened lazily rather than at boot. On the four generic Arduino-header -boards this assumes a single shield device wired to D10 (the -Arduino-standard CS pin); on the accelerometer/gyroscope boards it's the -one fixed onboard device. If your setup doesn't match either (multiple -slaves, a shield that manages its own CS, ...), drop `cs_path` from the -`dmspi` section and manage CS yourself. +for exactly how. On the four generic Arduino-header boards this assumes a +single shield device wired to D10 (the Arduino-standard CS pin); on the +accelerometer/gyroscope boards it's the one fixed onboard device. If your +setup doesn't match either (multiple slaves, a shield that manages its +own CS, ...), drop `cs_pin` from the `dmspi` section and manage CS +yourself. ### Example (nucleo-f401re/spi1.ini) @@ -118,15 +115,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PB6 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -136,13 +124,13 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio1/arduino_spi_cs +cs_pin=PB6 cs_active_level=low ``` ## Board Configurations -| Board | Folder | SPI Instance | Pins | CS (`cs_path` wired) | Source | Notes | +| Board | Folder | SPI Instance | Pins | CS (`cs_pin`) | Source | Notes | |-------|--------|---------------|------|------------------------|--------|-------| | NUCLEO-F401RE | `board/nucleo-f401re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | | NUCLEO-F411RE | `board/nucleo-f411re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | diff --git a/configs/board/nucleo-f401re/spi1.ini b/configs/board/nucleo-f401re/spi1.ini index 5a52ef6..7f75cae 100644 --- a/configs/board/nucleo-f401re/spi1.ini +++ b/configs/board/nucleo-f401re/spi1.ini @@ -8,19 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below -; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - -; port index 1 = port B, confirmed on real hardware; see -; docs/configuration.md's "Chip select" section for the path format and -; why it's opened lazily, not at boot) so dmspi drives it low/high around -; every transfer automatically. This assumes a single shield device on -; D10, the common case - for a shield that uses a different CS pin, or -; manages multiple devices itself, remove cs_path from [arduino_spi] and -; drive CS yourself instead. [arduino_spi_cs] must stay declared before -; [arduino_spi] in this file so it exists before [arduino_spi] tries to -; open it (though since the open is lazy, on first transfer, this mainly -; just keeps the file readable in the order things happen). +; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it +; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see +; docs/configuration.md's "Chip select" section), so no separate dmgpio +; section is needed for it. This assumes a single shield device on D10, +; the common case - for a shield that uses a different CS pin, or manages +; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS +; yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -50,15 +44,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PB6 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -68,5 +53,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio1/arduino_spi_cs +cs_pin=PB6 cs_active_level=low diff --git a/configs/board/nucleo-f411re/spi1.ini b/configs/board/nucleo-f411re/spi1.ini index ee01930..0ed1d84 100644 --- a/configs/board/nucleo-f411re/spi1.ini +++ b/configs/board/nucleo-f411re/spi1.ini @@ -8,19 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below -; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - -; port index 1 = port B, confirmed on real hardware; see -; docs/configuration.md's "Chip select" section for the path format and -; why it's opened lazily, not at boot) so dmspi drives it low/high around -; every transfer automatically. This assumes a single shield device on -; D10, the common case - for a shield that uses a different CS pin, or -; manages multiple devices itself, remove cs_path from [arduino_spi] and -; drive CS yourself instead. [arduino_spi_cs] must stay declared before -; [arduino_spi] in this file so it exists before [arduino_spi] tries to -; open it (though since the open is lazy, on first transfer, this mainly -; just keeps the file readable in the order things happen). +; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it +; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see +; docs/configuration.md's "Chip select" section), so no separate dmgpio +; section is needed for it. This assumes a single shield device on D10, +; the common case - for a shield that uses a different CS pin, or manages +; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS +; yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -50,15 +44,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PB6 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -68,6 +53,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio1/arduino_spi_cs +cs_pin=PB6 cs_active_level=low diff --git a/configs/board/nucleo-f446re/spi1.ini b/configs/board/nucleo-f446re/spi1.ini index 23bc596..e07793d 100644 --- a/configs/board/nucleo-f446re/spi1.ini +++ b/configs/board/nucleo-f446re/spi1.ini @@ -8,19 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - [arduino_spi_cs] below -; configures it as a plain dmgpio output, and [arduino_spi]'s cs_path -; points dmspi at the resulting device (/dev/dmgpio1/arduino_spi_cs - -; port index 1 = port B, confirmed on real hardware; see -; docs/configuration.md's "Chip select" section for the path format and -; why it's opened lazily, not at boot) so dmspi drives it low/high around -; every transfer automatically. This assumes a single shield device on -; D10, the common case - for a shield that uses a different CS pin, or -; manages multiple devices itself, remove cs_path from [arduino_spi] and -; drive CS yourself instead. [arduino_spi_cs] must stay declared before -; [arduino_spi] in this file so it exists before [arduino_spi] tries to -; open it (though since the open is lazy, on first transfer, this mainly -; just keeps the file readable in the order things happen). +; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it +; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see +; docs/configuration.md's "Chip select" section), so no separate dmgpio +; section is needed for it. This assumes a single shield device on D10, +; the common case - for a shield that uses a different CS pin, or manages +; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS +; yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -50,15 +44,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PB6 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -68,6 +53,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio1/arduino_spi_cs +cs_pin=PB6 cs_active_level=low diff --git a/configs/board/nucleo-f767zi/spi1.ini b/configs/board/nucleo-f767zi/spi1.ini index 0da5ce4..b5b7902 100644 --- a/configs/board/nucleo-f767zi/spi1.ini +++ b/configs/board/nucleo-f767zi/spi1.ini @@ -5,8 +5,7 @@ ; D12(MISO)=PA6, D11(MOSI)=PA7. D10(CS) is PD14 per the same source ; (listed alongside the SPI pins) but that specific pin/signal pairing was ; not independently cross-checked, unlike SCK/MISO/MOSI above - confirm -; PD14 against UM1974 before trusting [arduino_spi_cs] below on real -; hardware. +; PD14 against UM1974 before trusting cs_pin=PD14 below on real hardware. ; ; IMPORTANT: the same Zephyr doc explicitly warns "The Arduino Uno v3 ; specified SPI device conflicts with the on-board ETH device on pin PA7" @@ -20,15 +19,11 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] -; below configures D10 (PD14) as a plain dmgpio output, and -; [arduino_spi]'s cs_path points dmspi at the resulting device -; (/dev/dmgpio3/arduino_spi_cs - port index 3 = port D; see -; docs/configuration.md's "Chip select" section for the path format, -; confirmed on real hardware for a different board/port) so dmspi drives -; it low/high around every transfer automatically - assuming a single -; shield device on D10 and that the PD14 guess above is right. For a -; shield that uses a different CS pin, remove cs_path from [arduino_spi] +; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and +; drives D10 (PD14) directly via cs_pin=PD14 below (dmgpio's pin-lease +; API, see docs/configuration.md's "Chip select" section), assuming a +; single shield device on D10 and that the PD14 guess above is right. For +; a shield that uses a different CS pin, remove cs_pin from [arduino_spi] ; and drive CS yourself instead. [arduino_spi_sck] @@ -59,15 +54,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PD14 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -77,6 +63,6 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio3/arduino_spi_cs +cs_pin=PD14 cs_active_level=low diff --git a/configs/board/stm32f4-discovery/spi1.ini b/configs/board/stm32f4-discovery/spi1.ini index 8482f50..2246295 100644 --- a/configs/board/stm32f4-discovery/spi1.ini +++ b/configs/board/stm32f4-discovery/spi1.ini @@ -17,16 +17,10 @@ ; mode=3 (CPOL=1, CPHA=1) matches the LIS3DSH datasheet (clock idles high) ; and is confirmed by ST's own community answers for the same MEMS family. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - [mems_cs] below -; configures PE3 as a plain dmgpio output, and [mems_spi]'s cs_path points -; dmspi at the resulting device (/dev/dmgpio4/mems_cs - port index 4 = -; port E, confirmed on real hardware for a different board/port; see -; docs/configuration.md's "Chip select" section for the path format and -; why the open happens lazily, on first transfer, not at boot) so dmspi -; drives it low/high around every transfer automatically. [mems_cs] must -; still be declared before [mems_spi] within this file, so it exists -; first even though the actual open is lazy - keep that ordering if you -; edit this file. +; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and +; drives PE3 directly via cs_pin=PE3 below (dmgpio's pin-lease API, see +; docs/configuration.md's "Chip select" section), so no separate dmgpio +; section is needed for it. [mems_spi_sck] driver_name=dmgpio @@ -56,15 +50,6 @@ speed=maximum output_circuit=push_pull pull=none -[mems_cs] -driver_name=dmgpio -driver_order=3 -pin=PE3 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [mems_spi] driver_name=dmspi driver_order=3 @@ -74,5 +59,5 @@ baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio4/mems_cs +cs_pin=PE3 cs_active_level=low diff --git a/configs/board/stm32f429i-discovery/spi5.ini b/configs/board/stm32f429i-discovery/spi5.ini index 6f03621..3acb0a9 100644 --- a/configs/board/stm32f429i-discovery/spi5.ini +++ b/configs/board/stm32f429i-discovery/spi5.ini @@ -11,15 +11,10 @@ ; mode=3 (CPOL=1, CPHA=1) matches the L3GD20 datasheet (clock idles high, ; data sampled on the falling edge). ; -; CS is not part of the SPI peripheral (nss_mode=soft) - [gyro_cs] below -; configures PC1 as a plain dmgpio output, and [gyro_spi]'s cs_path points -; dmspi at the resulting device (/dev/dmgpio2/gyro_cs - port index 2 = -; port C, confirmed on real hardware for a different board/port; see -; docs/configuration.md's "Chip select" section for the path format and -; why the open happens lazily, on first transfer, not at boot) so dmspi -; drives it low/high around every transfer automatically. [gyro_cs] must -; stay declared before [gyro_spi] in this file, so it exists first even -; though the actual open is lazy. +; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and +; drives PC1 directly via cs_pin=PC1 below (dmgpio's pin-lease API, see +; docs/configuration.md's "Chip select" section), so no separate dmgpio +; section is needed for it. [gyro_spi_sck] driver_name=dmgpio @@ -49,15 +44,6 @@ speed=maximum output_circuit=push_pull pull=none -[gyro_cs] -driver_name=dmgpio -driver_order=3 -pin=PC1 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [gyro_spi] driver_name=dmspi driver_order=3 @@ -67,5 +53,5 @@ baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio2/gyro_cs +cs_pin=PC1 cs_active_level=low diff --git a/configs/board/stm32f746g-disco/spi2.ini b/configs/board/stm32f746g-disco/spi2.ini index d0e90e0..a4d2336 100644 --- a/configs/board/stm32f746g-disco/spi2.ini +++ b/configs/board/stm32f746g-disco/spi2.ini @@ -16,20 +16,12 @@ ; against the Port I/Port B alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] -; below configures D10 (PI0) as a plain dmgpio output, and [arduino_spi]'s -; cs_path points dmspi at the resulting device so dmspi drives it low/high -; around every transfer automatically - assuming a single shield device on -; D10. For a shield that uses a different CS pin, remove cs_path from -; [arduino_spi] and drive CS yourself instead. -; -; The path is /dev/dmgpio/, NOT a flat -; /dev/ - confirmed on real STM32F746G-DISCO hardware: dmgpio -; mounts each alt-named device under a per-port class directory -; (port_index: A=0, B=1, ... I=8, so PI0 -> dmgpio8). Getting this wrong -; makes dmspi's Dmod_FileOpen(cs_path) fail at create time (logged, not a -; hang - dmdevfs just reports "Failed to configure driver: dmspi" and boot -; continues). +; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and +; drives D10 (PI0) directly via cs_pin=PI0 below (dmgpio's pin-lease API, +; see docs/configuration.md's "Chip select" section), so no separate +; dmgpio section is needed for it. Verified end-to-end on real +; STM32F746G-DISCO hardware. For a shield that uses a different CS pin, +; remove cs_pin from [arduino_spi] and drive CS yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -59,15 +51,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PI0 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -77,5 +60,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio8/arduino_spi_cs +cs_pin=PI0 cs_active_level=low diff --git a/configs/board/stm32f769i-discovery/spi2.ini b/configs/board/stm32f769i-discovery/spi2.ini index 78356c2..e11ca2a 100644 --- a/configs/board/stm32f769i-discovery/spi2.ini +++ b/configs/board/stm32f769i-discovery/spi2.ini @@ -16,16 +16,12 @@ ; against the Port H/Port J alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - [arduino_spi_cs] -; below configures D10 (PF7) as a plain dmgpio output, and -; [arduino_spi]'s cs_path points dmspi at the resulting device -; (/dev/dmgpio5/arduino_spi_cs - port index 5 = port F; see -; docs/configuration.md's "Chip select" section for the path format, -; confirmed on real hardware for a different board/port, and why the open -; happens lazily, on first transfer, not at boot) so dmspi drives it -; low/high around every transfer automatically - assuming a single shield -; device on D10. For a shield that uses a different CS pin, remove -; cs_path from [arduino_spi] and drive CS yourself instead. +; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and +; drives D10 (PF7) directly via cs_pin=PF7 below (dmgpio's pin-lease API, +; see docs/configuration.md's "Chip select" section), so no separate +; dmgpio section is needed for it. This assumes a single shield device on +; D10 - for a shield that uses a different CS pin, remove cs_pin from +; [arduino_spi] and drive CS yourself instead. [arduino_spi_sck] driver_name=dmgpio @@ -55,15 +51,6 @@ speed=maximum output_circuit=push_pull pull=none -[arduino_spi_cs] -driver_name=dmgpio -driver_order=3 -pin=PF7 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - [arduino_spi] driver_name=dmspi driver_order=3 @@ -73,5 +60,5 @@ baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_path=/dev/dmgpio5/arduino_spi_cs +cs_pin=PF7 cs_active_level=low diff --git a/docs/configuration.md b/docs/configuration.md index ee236c7..c50b0ca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,9 +19,9 @@ do it): | `clock_polarity` | string | "low" | `"low"` or `"high"` (CPOL) | | `clock_phase` | string | "1edge" | `"1edge"` or `"2edge"` (CPHA) | | `bit_order` | string | "msb_first" | `"msb_first"` or `"lsb_first"` | -| `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS - see `cs_path` below) or `"hard"` (peripheral drives/reads the physical NSS pin) | -| `cs_path` | string | (none) | Path to a separately dmdevfs-configured `dmgpio` output device (e.g. `/dev/dmgpio0/flash_cs` - see "Chip select" below for the path format) - if set, dmspi asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave` | -| `cs_active_level` | string | "low" | `"low"` or `"high"` - which level `cs_path` is driven to when asserting (selecting) the device | +| `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS - see `cs_pin` below) or `"hard"` (peripheral drives/reads the physical NSS pin) | +| `cs_pin` | string | (none) | A GPIO pin name, e.g. `"PA4"` (see "Chip select" below) - if set, dmspi claims it via dmgpio's pin-lease API and asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave` | +| `cs_active_level` | string | "low" | `"low"` or `"high"` - which level `cs_pin` is driven to when asserting (selecting) the device | | `interrupt_trigger` | string | "off" | `"off"`, `"rx_not_empty"`, `"tx_empty"`, or `"error"` | | `interrupt_handler` | string | (none) | Name of a dmhaman-registered handler to call on interrupt | | `rx_ring_size` | integer | 0 | Capacity of the RX ring buffer, in bytes (0 disables it; useful mainly for `role=slave`, where reception isn't driven by the local CPU) | @@ -35,39 +35,23 @@ The SPI peripheral itself only ever drives clock/data lines and, in `nss_mode=hard`, a single NSS pin. Real deployments almost always need more flexibility than that: one bus master talking to several slaves, each with its own CS line, or a CS pin the SPI peripheral doesn't own directly. -`cs_path` covers the common case - **one** fixed slave device - by pointing -dmspi at a `dmgpio` output pin configured as its own separate `dmdevfs` -device; dmspi asserts/deasserts it via `Dmod_Ioctl` around every -`write`/`read`/`dmspi_ioctl_cmd_transfer` call. - -The path must be the pin's real `dmdevfs` path, confirmed on real -hardware to be `/dev/dmgpio/` (`port_index`: -A=0, B=1, ... - e.g. a pin on PA4 named `[flash_cs]` ends up at -`/dev/dmgpio0/flash_cs`), **not** a flat `/dev/`. - -dmspi does **not** open `cs_path` at device-creation time - only lazily, on -the first actual transfer. This matters if your `dmspi` section is itself -part of `dmdevfs`'s initial boot-time config scan (the normal case, e.g. -via `configs/board/.../*.ini`): at that point in boot, `dmdevfs`'s own -`/dev` mount isn't marked ready yet, so `Dmod_FileOpen()` on *any* other -`/dev` path - including a perfectly correct `cs_path` - fails during that -window (confirmed on real STM32F746G-DISCO hardware: opening the exact -same path from an interactive shell afterwards works fine). Opening -lazily instead, on first use, sidesteps this entirely, since by the time -anything actually transfers data, boot has long finished. +`cs_pin` covers the common case - **one** fixed slave device - by naming a +GPIO pin (the same `"P"` syntax dmgpio's own `pin=` uses, e.g. +`"PA4"`); dmspi claims it directly through dmgpio's pin-lease API +(`dmgpio_pin_acquire()`/`dmgpio_lease.h`) and asserts/deasserts it via +`dmgpio_pin_write()` around every `write`/`read`/`dmspi_ioctl_cmd_transfer` +call. + +This is a direct, linked dependency on `dmgpio` (`dmod_link_modules(dmspi +dmgpio)`), not a `/dev` path - so the pin is claimed straight from +`_create()`, and a failure (malformed `cs_pin`, or the pin already leased +to another driver) is reported immediately at config time instead of +silently on first transfer. The lease also owns the pin's own +configuration (mode/speed/output type/initial level), so there is no +separate `dmgpio` config section to declare for it - `cs_pin` is +everything you need: ```ini -; The GPIO pin itself, as a normal standalone dmgpio device -[flash_cs] -driver_name=dmgpio -driver_order=3 -pin=PA4 -mode=output -pull=up -speed=maximum -output_circuit=push_pull - -; The SPI device - cs_path references the device above by its dmdevfs path [flash_spi] driver_name=dmspi driver_order=3 @@ -75,23 +59,21 @@ instance=1 role=master baudrate=4000000 mode=0 -cs_path=/dev/dmgpio0/flash_cs +cs_pin=PA4 cs_active_level=low ``` -`[flash_cs]` must be declared (and therefore created by `dmdevfs`) before -`[flash_spi]` for `cs_path` to resolve to an existing device - see -`configs/README.md` for the full ordering rules. If you have more than one -slave on the bus, or your CS line needs different timing/handling than -"assert before, deassert after every call", leave `cs_path` unset and -manage CS yourself around whatever calls you make into dmspi - see +If you have more than one slave on the bus, or your CS line needs +different timing/handling than "assert before, deassert after every +call", leave `cs_pin` unset and manage CS yourself around whatever calls +you make into dmspi - see [`tools/spitest/main.c`](../tools/spitest/main.c) for the shape of that -(it doesn't use `cs_path` since it talks to a peer board/loopback pair, -not a fixed slave device). +(it doesn't use `cs_pin` since it talks to a peer board/loopback pair, not +a fixed slave device). ## Examples -### Master, mode 0, 1 MHz, CS managed via cs_path +### Master, mode 0, 1 MHz, CS managed via cs_pin ```ini [dmspi] @@ -100,7 +82,7 @@ role=master baudrate=1000000 mode=0 nss_mode=soft -cs_path=/dev/dmgpio0/flash_cs +cs_pin=PA4 ``` ### Master talking to a device that needs mode 3 @@ -165,7 +147,7 @@ build-tested tool built this way (it takes a device path, reads its role back, and runs the appropriate side of a loopback test - no config or pins guessed or hardcoded). -`spitest` itself doesn't set `cs_path` (it talks to a peer board or a +`spitest` itself doesn't set `cs_pin` (it talks to a peer board or a loopback pair, not a fixed slave device), so it doesn't demonstrate CS management - see "Chip select" above for that. diff --git a/src/dmspi.c b/src/dmspi.c index f216355..5da9431 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -6,6 +6,7 @@ #include "dmhaman.h" #include "dmini.h" #include "dm_sw_ring.h" +#include "dmgpio_lease.h" #include #include @@ -18,29 +19,33 @@ * design, since a single soft-NSS bus can have many slaves, each with its * own CS line, and only the caller knows which one applies to a given * transaction. For the common case of ONE fixed slave device, this driver - * can still drive that CS line automatically: point the config's cs_path - * at a separately dmdevfs-configured dmgpio output pin (see - * configs/README.md) and this driver asserts it around every - * _write/_read/transfer-ioctl call in master mode, deasserting it again - * once the transfer completes. It is ignored for role=slave - a slave - * never drives its own CS. + * can still drive that CS line automatically: cs_pin names a GPIO pin + * (e.g. "PE3" - same syntax as pin= in a dmgpio board-config section) and + * this driver asserts it around every _write/_read/transfer-ioctl call in + * master mode, deasserting it again once the transfer completes. Ignored + * for role=slave - a slave never drives its own CS. * - * This goes through dmgpio's own dmdrvi device (Dmod_FileOpen on its - * dmdevfs path + Dmod_Ioctl), not a direct dependency on the dmgpio - * module: every published dmgpio release (v0.6 through v0.8.0, checked by - * hand) is missing its .dmr resource file, so dmf-get cannot fetch - * dmgpio_types.h through the normal dmod_link_modules path here. - * DMGPIO_IOCTL_CMD_SET_PINS_STATE and DMGPIO_PINS_STATE_ALL_{LOW,HIGH} - * below are copied from that header's dmgpio_ioctl_cmd_t/ - * dmgpio_pins_state_t enums - read directly from the sibling dmgpio - * checkout (../dmgpio/include/dmgpio_types.h) rather than assumed, and - * stable/small ordinal values dmgpio can't change without breaking its - * own ABI. Replace this with a real #include "dmgpio_types.h" plus - * dmod_link_modules(dmspi dmgpio) once dmgpio's packaging is fixed - * upstream. */ -#define DMGPIO_IOCTL_CMD_SET_PINS_STATE 1 -#define DMGPIO_PINS_STATE_ALL_LOW 0 -#define DMGPIO_PINS_STATE_ALL_HIGH 1 + * The pin is claimed through dmgpio's lease API (dmgpio_lease.h, + * dmod_dmgpio_api _pin_acquire/_release/_write) - a direct Built-in API + * dependency (dmod_link_modules(dmspi dmgpio), see CMakeLists.txt), + * resolved by the loader at module-load time. That replaces an earlier + * cs_path design (a dmdevfs /dev path, opened lazily via Dmod_FileOpen/ + * Dmod_Ioctl against hardcoded dmgpio ioctl constants) and fixes what was + * wrong with it: + * - no dmdevfs path format to get right (port-index-in-path etc) - + * cs_pin reuses the exact "PA5" syntax every other pin= in these + * config files already uses; + * - no hardcoded copy of dmgpio's ioctl ABI - the lease functions are + * real, typed, linked declarations; + * - no boot-ordering hazard - dmgpio_pin_acquire() is a declared module + * dependency, not a runtime /dev lookup, so it can run straight from + * _create() and a failure (e.g. -EBUSY, the pin already leased to + * another driver) surfaces immediately at config time instead of + * silently on first transfer. + * The lease also owns pin configuration itself (mode/speed/output type/ + * initial level), so cs_pin replaces the separate dmgpio [xxx_cs] board- + * config section entirely - see configs/README.md. + */ /** * @brief DMDRVI context structure @@ -53,58 +58,20 @@ struct dmdrvi_context dm_sw_ring_t rx_ring; /**< Software ring buffer for received bytes */ uint32_t rx_ring_size; /**< Capacity of the RX ring buffer (from config) */ dm_sw_ring_flags_t rx_ring_wait_flags; /**< RX ring read-wait behavior (from config) */ - char *cs_path; /**< CS pin device path (NULL = unmanaged); see cs_ensure_open() */ - void *cs_fp; /**< dmgpio device handle for the CS pin, opened lazily */ + dmgpio_lease_t cs_lease; /**< CS pin lease (NULL = unmanaged) */ bool cs_active_high; /**< true = assert by driving the pin high instead of low */ - bool cs_open_failed; /**< true once a failed lazy-open has been logged (avoid log spam) */ }; -static void cs_deassert(dmdrvi_context_t context) -{ - if (context->cs_fp == NULL) - return; - int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_LOW : DMGPIO_PINS_STATE_ALL_HIGH; - Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); -} - -/** - * @brief Open context->cs_path on first use, caching the handle. - * - * Deliberately NOT opened at _create() time: _create() can run as part of - * dmdevfs's initial boot-time config scan, before dmdevfs's own /dev mount - * is marked "ready" (see dmfsi_dmdevfs_mounted() in dmdevfs.c) - - * Dmod_FileOpen() on another /dev path fails during that window even for a - * perfectly valid path (confirmed on real STM32F746G-DISCO hardware: the - * exact same path that fails during boot opens fine once queried - * afterwards, e.g. from an interactive shell). By the time a caller - * actually transfers data, boot has long finished, so opening here instead - * of at create time sidesteps the ordering problem entirely. - */ -static bool cs_ensure_open(dmdrvi_context_t context) +static void cs_assert(dmdrvi_context_t context) { - if (context->cs_fp != NULL) - return true; - if (context->cs_path == NULL || context->cs_open_failed) - return false; - - context->cs_fp = Dmod_FileOpen(context->cs_path, "r+"); - if (context->cs_fp == NULL) - { - DMOD_LOG_ERROR("Failed to open CS pin device '%s'\n", context->cs_path); - context->cs_open_failed = true; - return false; - } - - cs_deassert(context); /* start deselected */ - return true; + if (context->cs_lease != NULL) + dmgpio_pin_write(context->cs_lease, context->cs_active_high); } -static void cs_assert(dmdrvi_context_t context) +static void cs_deassert(dmdrvi_context_t context) { - if (!cs_ensure_open(context)) - return; - int state = context->cs_active_high ? DMGPIO_PINS_STATE_ALL_HIGH : DMGPIO_PINS_STATE_ALL_LOW; - Dmod_Ioctl(context->cs_fp, DMGPIO_IOCTL_CMD_SET_PINS_STATE, &state); + if (context->cs_lease != NULL) + dmgpio_pin_write(context->cs_lease, !context->cs_active_high); } static int is_valid_context(dmdrvi_context_t context) @@ -187,6 +154,29 @@ static dm_sw_ring_flags_t string_to_rx_ring_wait_flags(const char *s) return dm_sw_ring_flags_wait_for_some_data; } +/** + * @brief Parse "P" (e.g. "PE3") into dmgpio's pin encoding + * (port*16 + index, see dmgpio_lease.h) - the same syntax already used by + * pin= in a dmgpio board-config section. Returns -1 on a malformed string. + */ +static int string_to_gpio_pin(const char *s) +{ + if (s == NULL || s[0] != 'P' || s[1] < 'A' || s[1] > 'K' || s[2] == '\0') + return -1; + + int index = 0; + for (const char *p = s + 2; *p != '\0'; p++) + { + if (*p < '0' || *p > '9') + return -1; + index = index * 10 + (*p - '0'); + if (index > 15) + return -1; + } + + return (int)(s[1] - 'A') * 16 + index; +} + /* ---- Configuration ---- */ /** @@ -336,37 +326,48 @@ static int read_config_parameters(dmdrvi_context_t context, dmini_context_t conf } /** - * @brief Record the CS pin device path named by 'cs_path', if configured. + * @brief Claim and configure the CS pin named by 'cs_pin', if configured. * - * Does not open it - see cs_ensure_open()'s comment for why that has to - * wait until first use. Requires context->config.role to already be set - * (read_config_parameters() must run first). A no-op (returns 0) when - * cs_path is absent, or when role=slave (a slave never drives its own CS - - * see the file header comment on CS management). + * Requires context->config.role to already be set (read_config_parameters() + * must run first). A no-op (returns 0) when cs_pin is absent, or when + * role=slave (a slave never drives its own CS - see the file header + * comment on CS management). Fails outright - and so fails _create() - if + * cs_pin is present but malformed, or if the pin can't be claimed (e.g. + * already leased to another driver). */ static int setup_cs_pin(dmdrvi_context_t context, dmini_context_t config) { char section_buf[64]; const char *section = detect_config_section(config, section_buf, sizeof(section_buf)); - const char *cs_path = dmini_get_string(config, section, "cs_path", NULL); - if (cs_path == NULL) + const char *cs_pin_str = dmini_get_string(config, section, "cs_pin", NULL); + if (cs_pin_str == NULL) return 0; if (context->config.role != dmspi_role_master) { - DMOD_LOG_ERROR("'cs_path' is ignored for role=slave (a slave never drives its own CS)\n"); + DMOD_LOG_ERROR("'cs_pin' is ignored for role=slave (a slave never drives its own CS)\n"); return 0; } + int pin = string_to_gpio_pin(cs_pin_str); + if (pin < 0) + { + DMOD_LOG_ERROR("Invalid 'cs_pin' value '%s' (expected e.g. \"PE3\")\n", cs_pin_str); + return -EINVAL; + } + context->cs_active_high = (strcmp( dmini_get_string(config, section, "cs_active_level", "low"), "high") == 0); - context->cs_path = Dmod_StrDup(cs_path); - if (context->cs_path == NULL) + /* Preload deasserted; mode/af are the only settings this plain output + * pin needs, edge/user are unused (no interrupt). */ + int ret = dmgpio_pin_acquire((int16_t)pin, dmgpio_mode_output, 0, + !context->cs_active_high, NULL, NULL, &context->cs_lease); + if (ret != 0) { - DMOD_LOG_ERROR("Failed to allocate CS pin path\n"); - return -ENOMEM; + DMOD_LOG_ERROR("Failed to claim CS pin '%s' (%d) - already in use?\n", cs_pin_str, ret); + return ret; } return 0; @@ -522,7 +523,8 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_c configure(context) != 0) { DMOD_LOG_ERROR("Failed to create DMDRVI context with provided configuration\n"); - Dmod_Free(context->cs_path); + if (context->cs_lease != NULL) + dmgpio_pin_release(context->cs_lease); Dmod_Free(context->interrupt_handler_name); Dmod_Free(context); return NULL; @@ -597,13 +599,12 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t cont context->rx_ring = NULL; } - if (context->cs_fp != NULL) + if (context->cs_lease != NULL) { cs_deassert(context); - Dmod_FileClose(context->cs_fp); - context->cs_fp = NULL; + dmgpio_pin_release(context->cs_lease); + context->cs_lease = NULL; } - Dmod_Free(context->cs_path); Dmod_Free(context->interrupt_handler_name); dmspi_port_deinit(context->config.instance); From 16c993be804f433c2dd3ea6ad804c49c9f98ffc5 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Tue, 22 Sep 2026 18:27:49 +0200 Subject: [PATCH 04/10] Updated the implementation to use the friends mechanism for the chip select --- README.md | 9 +- configs/README.md | 61 ++++--- configs/board/nucleo-f401re/spi1.ini | 33 ++-- configs/board/nucleo-f411re/spi1.ini | 32 ++-- configs/board/nucleo-f446re/spi1.ini | 32 ++-- configs/board/nucleo-f767zi/spi1.ini | 33 ++-- configs/board/stm32f4-discovery/spi1.ini | 30 +++- configs/board/stm32f429i-discovery/spi5.ini | 30 +++- configs/board/stm32f746g-disco/spi2.ini | 33 ++-- configs/board/stm32f769i-discovery/spi2.ini | 32 ++-- docs/api-reference.md | 1 + docs/configuration.md | 88 +++++---- src/dmspi.c | 187 +++++++------------- 13 files changed, 336 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index a4c3e4a..6e8bf31 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ write/ioctl/close), configured from an INI file via `dmini`. It supports: (useful for slave mode, where reception isn't driven by the local CPU) - True full-duplex transfers via `dmspi_ioctl_cmd_transfer` -Like `dmuart`/`dmgpio`, dmspi only configures the SPI peripheral itself - -pin muxing (SCK/MOSI/MISO/NSS alternate functions) is done externally via -`dmgpio`, and in `nss_mode=soft` chip-select is a plain GPIO the caller -toggles around each transaction. +Like `dmuart`/`dmgpio`, dmspi only configures the SPI peripheral itself. +Pin muxing is done externally via `dmgpio`; in `nss_mode=soft`, an optional +GPIO in the same `friends_group` with `friend_role=chip_select` is discovered +through `dmdevfs` and toggled with the public GPIO ioctl through its device +path around each transaction. See [docs/configuration.md](docs/configuration.md) for the full configuration reference and a usage example, and diff --git a/configs/README.md b/configs/README.md index 0a3a38a..80b8f9c 100644 --- a/configs/README.md +++ b/configs/README.md @@ -60,36 +60,36 @@ Each board configuration file contains: - `[section]` entries with `driver_name=dmgpio` that mux the SCK/MISO/MOSI pins to the SPI peripheral's alternate function. - One `[section]` with `driver_name=dmspi` for the peripheral itself. +- One `[section]` with `driver_name=dmgpio` and + `friend_role=chip_select` for software-controlled CS. `dmdevfs` scans a config file (or a whole directory of them) for sections declaring `driver_name`, dynamically resolves that module's `dmdrvi` interface (`Dmod_LoadModuleByName()` + `Dmod_GetDifFunction()` - see [docs/configuration.md](../docs/configuration.md)), and creates one device -per section, ordered by `driver_order` (GPIO pin sections must land before -the SPI section that depends on them, hence both use the same order here - -declaration order within a `driver_order` group already puts the pins -first). - -The SPI peripheral itself never drives chip-select (`nss_mode` stays -`soft` throughout this directory) - dmspi still manages it, just as a -plain GPIO pin rather than a native NSS signal. Every `dmspi` section -below names its CS pin directly via `cs_pin` (e.g. `cs_pin=PB6`); dmspi -claims and configures that pin itself through dmgpio's pin-lease API, so -there's no separate `dmgpio` section to declare for it - see -[docs/configuration.md](../docs/configuration.md)'s "Chip select" section -for exactly how. On the four generic Arduino-header boards this assumes a -single shield device wired to D10 (the Arduino-standard CS pin); on the -accelerometer/gyroscope boards it's the one fixed onboard device. If your -setup doesn't match either (multiple slaves, a shield that manages its -own CS, ...), drop `cs_pin` from the `dmspi` section and manage CS -yourself. +per section. These sections use `driver_order=2`, so they are configured after +external RAM (`dmfmc` uses order 1). DMA may use the same order because dmspi +does not currently depend on it. `dmdevfs` keeps declaration order stable +within the group: signal GPIOs are configured first, then SPI, and finally its +CS GPIO. + +The SPI peripheral itself never drives chip-select (`nss_mode` stays `soft` +throughout this directory). Every related section shares a `friends_group`. +The separate CS GPIO carries `friend_role=chip_select`, allowing `dmdevfs` to +report its generated path to dmspi; dmspi drives it through the filesystem +using `dmgpio_ioctl_cmd_set_pins_state`. +See [docs/configuration.md](../docs/configuration.md)'s "Chip select" section. +On Arduino-header boards this assumes a single shield on D10; on the sensor +boards it is the fixed onboard device. For multiple slaves or custom CS +timing, omit the `chip_select` friend and manage CS externally. ### Example (nucleo-f401re/spi1.ini) ```ini [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA5 mode=alternate alternate_function=5 @@ -99,7 +99,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA6 mode=alternate alternate_function=5 @@ -107,7 +108,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA7 mode=alternate alternate_function=5 @@ -117,20 +119,31 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=1 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PB6 cs_active_level=low + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull ``` ## Board Configurations -| Board | Folder | SPI Instance | Pins | CS (`cs_pin`) | Source | Notes | +| Board | Folder | SPI Instance | Pins | CS GPIO friend | Source | Notes | |-------|--------|---------------|------|------------------------|--------|-------| | NUCLEO-F401RE | `board/nucleo-f401re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | | NUCLEO-F411RE | `board/nucleo-f411re/` | SPI1 | PA5/PA6/PA7 | D10=PB6 | UM1724 | Arduino Uno V3 header (D13/D12/D11) | diff --git a/configs/board/nucleo-f401re/spi1.ini b/configs/board/nucleo-f401re/spi1.ini index 7f75cae..c6e9967 100644 --- a/configs/board/nucleo-f401re/spi1.ini +++ b/configs/board/nucleo-f401re/spi1.ini @@ -8,17 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it -; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see -; docs/configuration.md's "Chip select" section), so no separate dmgpio -; section is needed for it. This assumes a single shield device on D10, -; the common case - for a shield that uses a different CS pin, or manages -; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS -; yourself instead. +; D10 (PB6) is the Arduino-standard CS/SS pin. It is exposed as a separate +; dmgpio friend below and dmspi drives it through its dmdevfs path. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA5 mode=alternate alternate_function=5 @@ -28,7 +24,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA6 mode=alternate alternate_function=5 @@ -36,7 +33,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA7 mode=alternate alternate_function=5 @@ -46,12 +44,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=1 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PB6 cs_active_level=low + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/nucleo-f411re/spi1.ini b/configs/board/nucleo-f411re/spi1.ini index 0ed1d84..750b30d 100644 --- a/configs/board/nucleo-f411re/spi1.ini +++ b/configs/board/nucleo-f411re/spi1.ini @@ -8,17 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it -; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see -; docs/configuration.md's "Chip select" section), so no separate dmgpio -; section is needed for it. This assumes a single shield device on D10, -; the common case - for a shield that uses a different CS pin, or manages -; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS -; yourself instead. +; D10 (PB6) is the Arduino-standard CS/SS pin. It is exposed as a separate +; dmgpio friend below and dmspi drives it through its dmdevfs path. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA5 mode=alternate alternate_function=5 @@ -28,7 +24,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA6 mode=alternate alternate_function=5 @@ -36,7 +33,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA7 mode=alternate alternate_function=5 @@ -46,13 +44,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=1 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PB6 cs_active_level=low +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/nucleo-f446re/spi1.ini b/configs/board/nucleo-f446re/spi1.ini index e07793d..5752ce3 100644 --- a/configs/board/nucleo-f446re/spi1.ini +++ b/configs/board/nucleo-f446re/spi1.ini @@ -8,17 +8,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; D10 (PB6) is the Arduino-standard CS/SS pin - dmspi claims and drives it -; directly via cs_pin=PB6 below (dmgpio's pin-lease API, see -; docs/configuration.md's "Chip select" section), so no separate dmgpio -; section is needed for it. This assumes a single shield device on D10, -; the common case - for a shield that uses a different CS pin, or manages -; multiple devices itself, remove cs_pin from [arduino_spi] and drive CS -; yourself instead. +; D10 (PB6) is the Arduino-standard CS/SS pin. It is exposed as a separate +; dmgpio friend below and dmspi drives it through its dmdevfs path. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA5 mode=alternate alternate_function=5 @@ -28,7 +24,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA6 mode=alternate alternate_function=5 @@ -36,7 +33,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA7 mode=alternate alternate_function=5 @@ -46,13 +44,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=1 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PB6 cs_active_level=low +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PB6 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/nucleo-f767zi/spi1.ini b/configs/board/nucleo-f767zi/spi1.ini index b5b7902..ff8171b 100644 --- a/configs/board/nucleo-f767zi/spi1.ini +++ b/configs/board/nucleo-f767zi/spi1.ini @@ -5,7 +5,7 @@ ; D12(MISO)=PA6, D11(MOSI)=PA7. D10(CS) is PD14 per the same source ; (listed alongside the SPI pins) but that specific pin/signal pairing was ; not independently cross-checked, unlike SCK/MISO/MOSI above - confirm -; PD14 against UM1974 before trusting cs_pin=PD14 below on real hardware. +; PD14 against UM1974 before trusting it on real hardware. ; ; IMPORTANT: the same Zephyr doc explicitly warns "The Arduino Uno v3 ; specified SPI device conflicts with the on-board ETH device on pin PA7" @@ -19,16 +19,13 @@ ; checked against the Port A alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and -; drives D10 (PD14) directly via cs_pin=PD14 below (dmgpio's pin-lease -; API, see docs/configuration.md's "Chip select" section), assuming a -; single shield device on D10 and that the PD14 guess above is right. For -; a shield that uses a different CS pin, remove cs_pin from [arduino_spi] -; and drive CS yourself instead. +; CS is not part of the SPI peripheral (nss_mode=soft). D10 (PD14) is a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA5 mode=alternate alternate_function=5 @@ -38,7 +35,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA6 mode=alternate alternate_function=5 @@ -46,7 +44,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PA7 mode=alternate alternate_function=5 @@ -56,13 +55,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=1 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PD14 cs_active_level=low +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PD14 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/stm32f4-discovery/spi1.ini b/configs/board/stm32f4-discovery/spi1.ini index 2246295..c3c355c 100644 --- a/configs/board/stm32f4-discovery/spi1.ini +++ b/configs/board/stm32f4-discovery/spi1.ini @@ -17,14 +17,13 @@ ; mode=3 (CPOL=1, CPHA=1) matches the LIS3DSH datasheet (clock idles high) ; and is confirmed by ST's own community answers for the same MEMS family. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and -; drives PE3 directly via cs_pin=PE3 below (dmgpio's pin-lease API, see -; docs/configuration.md's "Chip select" section), so no separate dmgpio -; section is needed for it. +; CS is not part of the SPI peripheral (nss_mode=soft). PE3 is exposed as a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. [mems_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=mems_spi pin=PA5 mode=alternate alternate_function=5 @@ -34,7 +33,8 @@ pull=none [mems_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=mems_spi pin=PA6 mode=alternate alternate_function=5 @@ -42,7 +42,8 @@ pull=none [mems_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=mems_spi pin=PA7 mode=alternate alternate_function=5 @@ -52,12 +53,23 @@ pull=none [mems_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=mems_spi instance=1 role=master baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_pin=PE3 cs_active_level=low + +[mems_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=mems_spi +friend_role=chip_select +pin=PE3 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/stm32f429i-discovery/spi5.ini b/configs/board/stm32f429i-discovery/spi5.ini index 3acb0a9..87f32f0 100644 --- a/configs/board/stm32f429i-discovery/spi5.ini +++ b/configs/board/stm32f429i-discovery/spi5.ini @@ -11,14 +11,13 @@ ; mode=3 (CPOL=1, CPHA=1) matches the L3GD20 datasheet (clock idles high, ; data sampled on the falling edge). ; -; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and -; drives PC1 directly via cs_pin=PC1 below (dmgpio's pin-lease API, see -; docs/configuration.md's "Chip select" section), so no separate dmgpio -; section is needed for it. +; CS is not part of the SPI peripheral (nss_mode=soft). PC1 is exposed as a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. [gyro_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=gyro_spi pin=PF7 mode=alternate alternate_function=5 @@ -28,7 +27,8 @@ pull=none [gyro_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=gyro_spi pin=PF8 mode=alternate alternate_function=5 @@ -36,7 +36,8 @@ pull=none [gyro_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=gyro_spi pin=PF9 mode=alternate alternate_function=5 @@ -46,12 +47,23 @@ pull=none [gyro_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=gyro_spi instance=5 role=master baudrate=1000000 mode=3 bit_order=msb_first nss_mode=soft -cs_pin=PC1 cs_active_level=low + +[gyro_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=gyro_spi +friend_role=chip_select +pin=PC1 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/stm32f746g-disco/spi2.ini b/configs/board/stm32f746g-disco/spi2.ini index a4d2336..7a48730 100644 --- a/configs/board/stm32f746g-disco/spi2.ini +++ b/configs/board/stm32f746g-disco/spi2.ini @@ -16,16 +16,14 @@ ; against the Port I/Port B alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and -; drives D10 (PI0) directly via cs_pin=PI0 below (dmgpio's pin-lease API, -; see docs/configuration.md's "Chip select" section), so no separate -; dmgpio section is needed for it. Verified end-to-end on real -; STM32F746G-DISCO hardware. For a shield that uses a different CS pin, -; remove cs_pin from [arduino_spi] and drive CS yourself instead. +; CS is not part of the SPI peripheral (nss_mode=soft). D10 (PI0) is a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. +; The pin assignment was verified end-to-end on real hardware. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PI1 mode=alternate alternate_function=5 @@ -35,7 +33,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PB14 mode=alternate alternate_function=5 @@ -43,7 +42,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PB15 mode=alternate alternate_function=5 @@ -53,12 +53,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=2 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PI0 cs_active_level=low + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PI0 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/configs/board/stm32f769i-discovery/spi2.ini b/configs/board/stm32f769i-discovery/spi2.ini index e11ca2a..7a97b0c 100644 --- a/configs/board/stm32f769i-discovery/spi2.ini +++ b/configs/board/stm32f769i-discovery/spi2.ini @@ -16,16 +16,13 @@ ; against the Port H/Port J alternate-function table for this specific ; part - verify against the datasheet before relying on it. ; -; CS is not part of the SPI peripheral (nss_mode=soft) - dmspi claims and -; drives D10 (PF7) directly via cs_pin=PF7 below (dmgpio's pin-lease API, -; see docs/configuration.md's "Chip select" section), so no separate -; dmgpio section is needed for it. This assumes a single shield device on -; D10 - for a shield that uses a different CS pin, remove cs_pin from -; [arduino_spi] and drive CS yourself instead. +; CS is not part of the SPI peripheral (nss_mode=soft). D10 (PF7) is a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. [arduino_spi_sck] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PH6 mode=alternate alternate_function=5 @@ -35,7 +32,8 @@ pull=none [arduino_spi_miso] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PJ4 mode=alternate alternate_function=5 @@ -43,7 +41,8 @@ pull=none [arduino_spi_mosi] driver_name=dmgpio -driver_order=3 +driver_order=2 +friends_group=arduino_spi pin=PJ3 mode=alternate alternate_function=5 @@ -53,12 +52,23 @@ pull=none [arduino_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=arduino_spi instance=2 role=master baudrate=1000000 mode=0 bit_order=msb_first nss_mode=soft -cs_pin=PF7 cs_active_level=low + +[arduino_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +friend_role=chip_select +pin=PF7 +mode=output +pull=up +speed=maximum +output_circuit=push_pull diff --git a/docs/api-reference.md b/docs/api-reference.md index 9de4bd2..38c50ab 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -39,6 +39,7 @@ callable by name from another module (see [configuration.md](configuration.md)). | `dmspi_validate_config(const dmspi_config_t*)` | Validate a config struct without touching hardware. Safe to call even when `dmspi_port` isn't loaded. | | `dmdrvi_dmspi_create(dmini_context_t, dmdrvi_dev_num_t*)` | Create a driver instance from an INI configuration. | | `dmdrvi_dmspi_free(dmdrvi_context_t)` | Destroy an instance created by `_create`. | +| `dmdrvi_dmspi_friend_changed` | Track the dmdevfs path of a ready friend with `friend_role=chip_select`; clears it when the friend is no longer ready. | | `dmdrvi_dmspi_open` / `_close` | Open/close a handle on the device. | | `dmdrvi_dmspi_read` | Receive bytes - drains the RX ring if configured, otherwise blocks clocking dummy bytes (master) or waiting for the external master (slave). | | `dmdrvi_dmspi_write` | Transmit bytes (full duplex at the wire level; received bytes are discarded). | diff --git a/docs/configuration.md b/docs/configuration.md index c50b0ca..fd74af6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,9 +19,8 @@ do it): | `clock_polarity` | string | "low" | `"low"` or `"high"` (CPOL) | | `clock_phase` | string | "1edge" | `"1edge"` or `"2edge"` (CPHA) | | `bit_order` | string | "msb_first" | `"msb_first"` or `"lsb_first"` | -| `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS - see `cs_pin` below) or `"hard"` (peripheral drives/reads the physical NSS pin) | -| `cs_pin` | string | (none) | A GPIO pin name, e.g. `"PA4"` (see "Chip select" below) - if set, dmspi claims it via dmgpio's pin-lease API and asserts/deasserts it automatically around every transfer in `role=master`. Ignored for `role=slave` | -| `cs_active_level` | string | "low" | `"low"` or `"high"` - which level `cs_pin` is driven to when asserting (selecting) the device | +| `nss_mode` | string | "soft" | `"soft"` (peripheral ignores NSS; an optional GPIO friend can provide CS) or `"hard"` (peripheral drives/reads the physical NSS pin) | +| `cs_active_level` | string | "low" | `"low"` or `"high"` - which level the `chip_select` GPIO friend is driven to when selecting the device | | `interrupt_trigger` | string | "off" | `"off"`, `"rx_not_empty"`, `"tx_empty"`, or `"error"` | | `interrupt_handler` | string | (none) | Name of a dmhaman-registered handler to call on interrupt | | `rx_ring_size` | integer | 0 | Capacity of the RX ring buffer, in bytes (0 disables it; useful mainly for `role=slave`, where reception isn't driven by the local CPU) | @@ -35,54 +34,83 @@ The SPI peripheral itself only ever drives clock/data lines and, in `nss_mode=hard`, a single NSS pin. Real deployments almost always need more flexibility than that: one bus master talking to several slaves, each with its own CS line, or a CS pin the SPI peripheral doesn't own directly. -`cs_pin` covers the common case - **one** fixed slave device - by naming a -GPIO pin (the same `"P"` syntax dmgpio's own `pin=` uses, e.g. -`"PA4"`); dmspi claims it directly through dmgpio's pin-lease API -(`dmgpio_pin_acquire()`/`dmgpio_lease.h`) and asserts/deasserts it via -`dmgpio_pin_write()` around every `write`/`read`/`dmspi_ioctl_cmd_transfer` -call. - -This is a direct, linked dependency on `dmgpio` (`dmod_link_modules(dmspi -dmgpio)`), not a `/dev` path - so the pin is claimed straight from -`_create()`, and a failure (malformed `cs_pin`, or the pin already leased -to another driver) is reported immediately at config time instead of -silently on first transfer. The lease also owns the pin's own -configuration (mode/speed/output type/initial level), so there is no -separate `dmgpio` config section to declare for it - `cs_pin` is -everything you need: +For the common case of **one** fixed slave, configure CS as a separate +`dmgpio` section. Give the GPIO and SPI sections the same `friends_group`, +and mark the GPIO with `friend_role=chip_select`. `dmdevfs` reports that +GPIO's device path to dmspi through `dmdrvi_friend_changed()`. dmspi then +opens the path and calls `dmgpio_ioctl_cmd_set_pins_state` around every +`write`/`read`/`dmspi_ioctl_cmd_transfer` call. + +The signal pins are declared first, the SPI controller second, and CS last. +They use `driver_order=2`, after external RAM initialization at order 1. +dmspi does not currently use DMA, so it does not need a later ordering group +than DMA. `dmdevfs` uses stable insertion within the group, ensuring dmspi +exists before a CS friend can be announced, including when configuration is +added to an already mounted `dmdevfs` instance: ```ini +[flash_spi_sck] +driver_name=dmgpio +driver_order=2 +friends_group=flash_spi +pin=PA5 +mode=alternate +alternate_function=5 + [flash_spi] driver_name=dmspi -driver_order=3 +driver_order=2 +friends_group=flash_spi instance=1 role=master baudrate=4000000 mode=0 -cs_pin=PA4 +nss_mode=soft cs_active_level=low + +[flash_spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=flash_spi +friend_role=chip_select +pin=PA4 +mode=output +pull=up +speed=maximum +output_circuit=push_pull ``` If you have more than one slave on the bus, or your CS line needs -different timing/handling than "assert before, deassert after every -call", leave `cs_pin` unset and manage CS yourself around whatever calls -you make into dmspi - see +different timing/handling than "assert before, deassert after every call", +do not add a `chip_select` friend and manage CS yourself around whatever +calls you make into dmspi - see [`tools/spitest/main.c`](../tools/spitest/main.c) for the shape of that -(it doesn't use `cs_pin` since it talks to a peer board/loopback pair, not +(it doesn't use a CS friend since it talks to a peer board/loopback pair, not a fixed slave device). ## Examples -### Master, mode 0, 1 MHz, CS managed via cs_pin +### Master, mode 0, 1 MHz, CS managed via a GPIO friend ```ini -[dmspi] +[spi] +driver_name=dmspi +driver_order=2 +friends_group=example_spi instance=1 role=master baudrate=1000000 mode=0 nss_mode=soft -cs_pin=PA4 + +[spi_cs] +driver_name=dmgpio +driver_order=2 +friends_group=example_spi +friend_role=chip_select +pin=PA4 +mode=output +pull=up ``` ### Master talking to a device that needs mode 3 @@ -147,9 +175,9 @@ build-tested tool built this way (it takes a device path, reads its role back, and runs the appropriate side of a loopback test - no config or pins guessed or hardcoded). -`spitest` itself doesn't set `cs_pin` (it talks to a peer board or a -loopback pair, not a fixed slave device), so it doesn't demonstrate CS -management - see "Chip select" above for that. +`spitest` itself doesn't configure a `chip_select` friend (it talks to a peer +board or a loopback pair, not a fixed slave device), so it doesn't demonstrate +CS management - see "Chip select" above for that. ## Usage without dmdevfs (advanced) diff --git a/src/dmspi.c b/src/dmspi.c index 5da9431..b1c9249 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -6,7 +6,7 @@ #include "dmhaman.h" #include "dmini.h" #include "dm_sw_ring.h" -#include "dmgpio_lease.h" +#include "dmgpio_types.h" #include #include @@ -15,36 +15,11 @@ /* ---- Chip-select (CS/NSS) management ---- * - * nss_mode=soft leaves NSS entirely unmanaged by the SPI peripheral - by - * design, since a single soft-NSS bus can have many slaves, each with its - * own CS line, and only the caller knows which one applies to a given - * transaction. For the common case of ONE fixed slave device, this driver - * can still drive that CS line automatically: cs_pin names a GPIO pin - * (e.g. "PE3" - same syntax as pin= in a dmgpio board-config section) and - * this driver asserts it around every _write/_read/transfer-ioctl call in - * master mode, deasserting it again once the transfer completes. Ignored - * for role=slave - a slave never drives its own CS. - * - * The pin is claimed through dmgpio's lease API (dmgpio_lease.h, - * dmod_dmgpio_api _pin_acquire/_release/_write) - a direct Built-in API - * dependency (dmod_link_modules(dmspi dmgpio), see CMakeLists.txt), - * resolved by the loader at module-load time. That replaces an earlier - * cs_path design (a dmdevfs /dev path, opened lazily via Dmod_FileOpen/ - * Dmod_Ioctl against hardcoded dmgpio ioctl constants) and fixes what was - * wrong with it: - * - no dmdevfs path format to get right (port-index-in-path etc) - - * cs_pin reuses the exact "PA5" syntax every other pin= in these - * config files already uses; - * - no hardcoded copy of dmgpio's ioctl ABI - the lease functions are - * real, typed, linked declarations; - * - no boot-ordering hazard - dmgpio_pin_acquire() is a declared module - * dependency, not a runtime /dev lookup, so it can run straight from - * _create() and a failure (e.g. -EBUSY, the pin already leased to - * another driver) surfaces immediately at config time instead of - * silently on first transfer. - * The lease also owns pin configuration itself (mode/speed/output type/ - * initial level), so cs_pin replaces the separate dmgpio [xxx_cs] board- - * config section entirely - see configs/README.md. + * A software-controlled CS is a separate dmgpio device in the same + * friends_group as this SPI device, with friend_role=chip_select. dmdevfs + * reports the GPIO's node path through dmdrvi_friend_changed(). We retain a + * copy of that path and drive the pin through the filesystem around each + * master transfer. dmdevfs owns GPIO creation, configuration, and lifetime. */ /** @@ -58,20 +33,40 @@ struct dmdrvi_context dm_sw_ring_t rx_ring; /**< Software ring buffer for received bytes */ uint32_t rx_ring_size; /**< Capacity of the RX ring buffer (from config) */ dm_sw_ring_flags_t rx_ring_wait_flags; /**< RX ring read-wait behavior (from config) */ - dmgpio_lease_t cs_lease; /**< CS pin lease (NULL = unmanaged) */ + char *cs_path; /**< Friend GPIO path (NULL = unmanaged) */ bool cs_active_high; /**< true = assert by driving the pin high instead of low */ }; +static void cs_set_active(dmdrvi_context_t context, bool active) +{ + if (context->cs_path == NULL || context->config.nss_mode != dmspi_nss_mode_soft) + return; + + bool high = (active == context->cs_active_high); + dmgpio_pins_state_t state = high + ? dmgpio_pins_state_all_high + : dmgpio_pins_state_all_low; + + void *file = Dmod_FileOpen(context->cs_path, "r+"); + if (file == NULL) + { + DMOD_LOG_ERROR("Failed to open chip-select GPIO: %s\n", context->cs_path); + return; + } + + if (Dmod_Ioctl(file, dmgpio_ioctl_cmd_set_pins_state, &state) != 0) + DMOD_LOG_ERROR("Failed to update chip-select GPIO: %s\n", context->cs_path); + Dmod_FileClose(file); +} + static void cs_assert(dmdrvi_context_t context) { - if (context->cs_lease != NULL) - dmgpio_pin_write(context->cs_lease, context->cs_active_high); + cs_set_active(context, true); } static void cs_deassert(dmdrvi_context_t context) { - if (context->cs_lease != NULL) - dmgpio_pin_write(context->cs_lease, !context->cs_active_high); + cs_set_active(context, false); } static int is_valid_context(dmdrvi_context_t context) @@ -154,29 +149,6 @@ static dm_sw_ring_flags_t string_to_rx_ring_wait_flags(const char *s) return dm_sw_ring_flags_wait_for_some_data; } -/** - * @brief Parse "P" (e.g. "PE3") into dmgpio's pin encoding - * (port*16 + index, see dmgpio_lease.h) - the same syntax already used by - * pin= in a dmgpio board-config section. Returns -1 on a malformed string. - */ -static int string_to_gpio_pin(const char *s) -{ - if (s == NULL || s[0] != 'P' || s[1] < 'A' || s[1] > 'K' || s[2] == '\0') - return -1; - - int index = 0; - for (const char *p = s + 2; *p != '\0'; p++) - { - if (*p < '0' || *p > '9') - return -1; - index = index * 10 + (*p - '0'); - if (index > 15) - return -1; - } - - return (int)(s[1] - 'A') * 16 + index; -} - /* ---- Configuration ---- */ /** @@ -314,6 +286,8 @@ static int read_config_parameters(dmdrvi_context_t context, dmini_context_t conf context->config.nss_mode = string_to_nss_mode(dmini_get_string(config, section, "nss_mode", "soft")); context->config.interrupt_trigger = string_to_interrupt_trigger(dmini_get_string(config, section, "interrupt_trigger", "off")); context->config.interrupt_handler = NULL; + context->cs_active_high = (strcmp( + dmini_get_string(config, section, "cs_active_level", "low"), "high") == 0); const char *handler_name = dmini_get_string(config, section, "interrupt_handler", NULL); context->interrupt_handler_name = (handler_name != NULL) ? Dmod_StrDup(handler_name) : NULL; @@ -325,54 +299,6 @@ static int read_config_parameters(dmdrvi_context_t context, dmini_context_t conf return check_config_parameters(&context->config); } -/** - * @brief Claim and configure the CS pin named by 'cs_pin', if configured. - * - * Requires context->config.role to already be set (read_config_parameters() - * must run first). A no-op (returns 0) when cs_pin is absent, or when - * role=slave (a slave never drives its own CS - see the file header - * comment on CS management). Fails outright - and so fails _create() - if - * cs_pin is present but malformed, or if the pin can't be claimed (e.g. - * already leased to another driver). - */ -static int setup_cs_pin(dmdrvi_context_t context, dmini_context_t config) -{ - char section_buf[64]; - const char *section = detect_config_section(config, section_buf, sizeof(section_buf)); - - const char *cs_pin_str = dmini_get_string(config, section, "cs_pin", NULL); - if (cs_pin_str == NULL) - return 0; - - if (context->config.role != dmspi_role_master) - { - DMOD_LOG_ERROR("'cs_pin' is ignored for role=slave (a slave never drives its own CS)\n"); - return 0; - } - - int pin = string_to_gpio_pin(cs_pin_str); - if (pin < 0) - { - DMOD_LOG_ERROR("Invalid 'cs_pin' value '%s' (expected e.g. \"PE3\")\n", cs_pin_str); - return -EINVAL; - } - - context->cs_active_high = (strcmp( - dmini_get_string(config, section, "cs_active_level", "low"), "high") == 0); - - /* Preload deasserted; mode/af are the only settings this plain output - * pin needs, edge/user are unused (no interrupt). */ - int ret = dmgpio_pin_acquire((int16_t)pin, dmgpio_mode_output, 0, - !context->cs_active_high, NULL, NULL, &context->cs_lease); - if (ret != 0) - { - DMOD_LOG_ERROR("Failed to claim CS pin '%s' (%d) - already in use?\n", cs_pin_str, ret); - return ret; - } - - return 0; -} - /** * @brief Apply configuration to the port layer */ @@ -518,13 +444,9 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_c memset(context, 0, sizeof(*context)); context->magic = DMSPI_CONTEXT_MAGIC; - if (read_config_parameters(context, config) != 0 || - setup_cs_pin(context, config) != 0 || - configure(context) != 0) + if (read_config_parameters(context, config) != 0 || configure(context) != 0) { DMOD_LOG_ERROR("Failed to create DMDRVI context with provided configuration\n"); - if (context->cs_lease != NULL) - dmgpio_pin_release(context->cs_lease); Dmod_Free(context->interrupt_handler_name); Dmod_Free(context); return NULL; @@ -599,13 +521,10 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t cont context->rx_ring = NULL; } - if (context->cs_lease != NULL) - { - cs_deassert(context); - dmgpio_pin_release(context->cs_lease); - context->cs_lease = NULL; - } - + /* Do not access the CS node here. Its section is declared after dmspi, + * so dmdevfs tears the GPIO down first (reverse order). Every completed + * transfer has already left CS deasserted. */ + Dmod_Free(context->cs_path); Dmod_Free(context->interrupt_handler_name); dmspi_port_deinit(context->config.instance); context->magic = 0; @@ -613,6 +532,36 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t cont } } +dmod_dmdrvi_dif_api_declaration(1.0, dmspi, 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, "chip_select") != 0 || + context->config.role != dmspi_role_master || + context->config.nss_mode != dmspi_nss_mode_soft) + { + return; + } + + if (info->state != dmdrvi_dev_state_ready || info->node_path == NULL) + { + Dmod_Free(context->cs_path); + context->cs_path = NULL; + return; + } + + char *path = Dmod_StrDup(info->node_path); + if (path == NULL) + { + DMOD_LOG_ERROR("Failed to retain chip-select GPIO path\n"); + return; + } + + Dmod_Free(context->cs_path); + context->cs_path = path; + cs_deassert(context); +} + dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void*, _open, ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t *dev_num )) { if (!is_valid_context(context)) From 7bf35a3e21e99c39e53e2ffc3a67e0e8b32505c9 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Wed, 23 Sep 2026 11:55:49 +0100 Subject: [PATCH 05/10] Fix STM32F7 8-bit SPI transfers --- src/port/stm32_common/stm32_common.c | 36 ++++++++++++++++++++++------ src/port/stm32_common/stm32_common.h | 6 ++--- tools/spitest/main.c | 33 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c index 5502903..9ecfa48 100644 --- a/src/port/stm32_common/stm32_common.c +++ b/src/port/stm32_common/stm32_common.c @@ -80,6 +80,26 @@ static volatile stm32_spi_t *get_spi(dmspi_instance_t instance) return (volatile stm32_spi_t *)stm32_spi_instances[instance - 1].base; } +/* The access width to DR must match the configured frame size. In 8-bit + * mode a 32-bit store on FIFO-capable STM32 SPI blocks is interpreted as + * several packed frames, which fills RX FIFO and raises OVR immediately. */ +static void write_data_byte(volatile stm32_spi_t *SPI, uint8_t data) +{ + *(volatile uint8_t *)&SPI->DR = data; +} + +static uint8_t read_data_byte(volatile stm32_spi_t *SPI) +{ + return *(volatile uint8_t *)&SPI->DR; +} + +static void clear_receive_state(volatile stm32_spi_t *SPI) +{ + while (SPI->SR & STM32_SPI_SR_RXNE) + (void)read_data_byte(SPI); + (void)SPI->SR; /* DR then SR clears OVR. */ +} + static void enable_spi_clock(dmspi_instance_t instance) { const stm32_spi_instance_desc_t *desc = &stm32_spi_instances[instance - 1]; @@ -154,6 +174,7 @@ dmod_dmspi_port_api_declaration(1.0, int, _transfer, ( dmspi_instance_t instance if (validate_instance(instance) != 0) return -1; volatile stm32_spi_t *SPI = get_spi(instance); + clear_receive_state(SPI); for (size_t i = 0; i < size; i++) { @@ -162,7 +183,7 @@ dmod_dmspi_port_api_declaration(1.0, int, _transfer, ( dmspi_instance_t instance { if (--timeout == 0) return -1; } - SPI->DR = tx ? tx[i] : 0xFFU; + write_data_byte(SPI, tx ? tx[i] : 0xFFU); timeout = STM32_SPI_TIMEOUT_VALUE; while (!(SPI->SR & STM32_SPI_SR_RXNE)) @@ -170,8 +191,8 @@ dmod_dmspi_port_api_declaration(1.0, int, _transfer, ( dmspi_instance_t instance if (--timeout == 0) return -1; } - uint32_t sr = SPI->SR; - uint8_t byte = (uint8_t)(SPI->DR & 0xFFU); /* reading DR completes the OVR clear sequence too */ + uint8_t byte = read_data_byte(SPI); + uint32_t sr = SPI->SR; /* DR then SR also clears a pending OVR. */ if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) { @@ -222,7 +243,7 @@ dmod_dmspi_port_api_declaration(1.0, int, _receive, ( dmspi_instance_t instance, { if (--timeout == 0) return 0; } - SPI->DR = 0xFFU; /* dummy byte - keeps the clock/shift register moving */ + write_data_byte(SPI, 0xFFU); /* dummy byte keeps the clock moving */ timeout = STM32_SPI_TIMEOUT_VALUE; while (!(SPI->SR & STM32_SPI_SR_RXNE)) @@ -230,8 +251,8 @@ dmod_dmspi_port_api_declaration(1.0, int, _receive, ( dmspi_instance_t instance, if (--timeout == 0) return 0; } + data[i] = read_data_byte(SPI); uint32_t sr = SPI->SR; - data[i] = (uint8_t)(SPI->DR & 0xFFU); if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) { @@ -491,7 +512,7 @@ void stm32_spi_irq_handler(dmspi_instance_t instance) if (sr & STM32_SPI_SR_RXNE) { - data = (uint8_t)(SPI->DR & 0xFFU); /* reading DR clears RXNE (and completes an OVR clear) */ + data = read_data_byte(SPI); trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_rx_not_empty); if (rx_rings[idx] != NULL) @@ -503,7 +524,8 @@ void stm32_spi_irq_handler(dmspi_instance_t instance) trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_tx_empty); if (sr & (STM32_SPI_SR_OVR | STM32_SPI_SR_MODF)) { - (void)SPI->DR; /* completes the OVR clear sequence */ + (void)read_data_byte(SPI); + (void)SPI->SR; /* DR then SR clears OVR */ SPI->CR1 |= STM32_SPI_CR1_SPE; /* completes the MODF clear sequence */ trigger = (dmspi_int_trigger_t)(trigger | dmspi_int_trigger_error); } diff --git a/src/port/stm32_common/stm32_common.h b/src/port/stm32_common/stm32_common.h index c25cd95..9719673 100644 --- a/src/port/stm32_common/stm32_common.h +++ b/src/port/stm32_common/stm32_common.h @@ -8,9 +8,9 @@ /** * @brief STM32 SPI register layout. * - * Identical field layout on F4 and F7 (both implement the "SPI v1" IP - * block - no FIFO). The only per-family differences are a handful of - * CR1/CR2 bits, handled by stm32_spi_family_configure_frame_format(). + * Identical register offsets on F4 and F7. STM32F7 adds FIFO/data-size + * behavior behind the same layout; those family-specific CR1/CR2 bits are + * handled by stm32_spi_family_configure_frame_format(). */ typedef struct { diff --git a/tools/spitest/main.c b/tools/spitest/main.c index 7a9de48..d82659a 100644 --- a/tools/spitest/main.c +++ b/tools/spitest/main.c @@ -135,6 +135,34 @@ static int run_single(const char *path) return ok ? 0 : 1; } +static int run_echo_loopback(const char *path) +{ + void *fp = open_device(path); + if (fp == NULL) + return -1; + + dmspi_role_t role; + if (!query_role(fp, path, &role) || role != dmspi_role_master) + { + DMOD_LOG_ERROR("spitest: echo loopback requires a master device\n"); + Dmod_FileClose(fp); + return -1; + } + + uint8_t tx[TEST_PATTERN_LEN]; + uint8_t rx[TEST_PATTERN_LEN] = {0}; + build_mosi_pattern(tx); + dmspi_transfer_t xfer = { .tx = tx, .rx = rx, .size = TEST_PATTERN_LEN }; + int ret = Dmod_Ioctl(fp, dmspi_ioctl_cmd_transfer, &xfer); + bool ok = ret == 0 && verify_pattern("MOSI->MISO echo", rx, tx, TEST_PATTERN_LEN); + if (ret != 0) + DMOD_LOG_ERROR("spitest: echo transfer failed (error %d)\n", ret); + + Dmod_FileClose(fp); + Dmod_Printf("\n=== Echo loopback test ('%s'): %s ===\n", path, ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} + /* ---- two devices on this board: one worker thread per side, so both * run their (blocking) transfer at the same time ---- */ @@ -217,6 +245,7 @@ static int run_loopback(const char *path_a, const char *path_b) static void print_usage(const char *prog) { Dmod_Printf("Usage: %s [peer_device_path]\n", prog); + Dmod_Printf(" %s --echo \n", prog); Dmod_Printf("\n"); Dmod_Printf("Exercises a real SPI transfer on an already-configured dmspi device -\n"); Dmod_Printf("see ../../configs/README.md for how to get one via dmdevfs. The\n"); @@ -229,6 +258,7 @@ static void print_usage(const char *prog) Dmod_Printf(" loopback test between the two - one must already be\n"); Dmod_Printf(" configured as master, the other as slave, and wired\n"); Dmod_Printf(" together with a cable.\n"); + Dmod_Printf(" --echo Test one master with MOSI physically connected to MISO.\n"); } int main(int argc, char *argv[]) @@ -239,6 +269,9 @@ int main(int argc, char *argv[]) return (argc < 2) ? 1 : 0; } + if (strcmp(argv[1], "--echo") == 0) + return (argc >= 3 && run_echo_loopback(argv[2]) == 0) ? 0 : 1; + if (argc >= 3) return (run_loopback(argv[1], argv[2]) == 0) ? 0 : 1; From 920cc5ada82a96bbaaf34c7dc29838821af00957 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 26 Sep 2026 13:00:44 +0100 Subject: [PATCH 06/10] Migrate to dmdrvi 2.0 (64-bit I/O contract) Bumps every DIF registration to the 2.0 interface version dmdrvi.h now requires, including _friend_changed (used here for the GPIO chip-select friend relationship). The functional changes are in _read/_write (offset -> dmdrvi_offset_t, return type -> dmdrvi_ssize_t, with explicit -EOVERFLOW/-EINVAL guards) and _stat (size -> dmdrvi_size_t). dmspi is transaction-like (SPI transfers), not a seekable/addressable device, so offset is validated (must be non-negative) but otherwise ignored, same treatment dmclk gives its own non-seekable device. The RX ring path clamps (rather than silently truncates) a request larger than dm_sw_ring's own uint32_t capacity type before handing it to that separate module's interface. _open already took the full (context, flags, dev_num) parameter list required by the contract, so no signature-drift fix was needed here. Part of choco-technologies/dmod-ecosystem#8. --- src/dmspi.c | 84 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/src/dmspi.c b/src/dmspi.c index b1c9249..fa6c256 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -429,7 +429,7 @@ int dmod_deinit(void) /* ---- DMDRVI interface ---- */ -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_context_t config, dmdrvi_dev_num_t* dev_num )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, dmdrvi_context_t, _create, ( dmini_context_t config, dmdrvi_dev_num_t* dev_num )) { if (config == NULL || dev_num == NULL) { @@ -507,7 +507,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, dmdrvi_context_t, _create, ( dmini_c return context; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t context )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, void, _free, ( dmdrvi_context_t context )) { if (is_valid_context(context)) { @@ -532,7 +532,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _free, ( dmdrvi_context_t cont } } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _friend_changed, +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, void, _friend_changed, ( dmdrvi_context_t context, const dmdrvi_friend_info_t* info )) { if (!is_valid_context(context) || info == NULL || info->friend_role == NULL || @@ -562,7 +562,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _friend_changed, cs_deassert(context); } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void*, _open, ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t *dev_num )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, void*, _open, ( dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t *dev_num )) { if (!is_valid_context(context)) { @@ -572,18 +572,49 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void*, _open, ( dmdrvi_context_t con return context; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, void, _close, ( dmdrvi_context_t context, void* handle )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, void, _close, ( dmdrvi_context_t context, void* handle )) { /* No specific action needed to close the SPI device handle */ } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _read, ( dmdrvi_context_t context, void* handle, void* buffer, size_t size, uint32_t offset )) +/** + * @brief Read (receive) bytes over the SPI bus + * + * dmspi is transaction-like, not a seekable/addressable device - there is no + * byte offset into anything, so offset is only validated (must be + * non-negative) and otherwise ignored, matching how dmclk treats offset for + * its own non-seekable device. + * + * @param context DMDRVI context + * @param handle Device handle (unused - dmspi has a single implicit device per context) + * @param buffer Buffer to read data into + * @param size Number of bytes to read; values greater than INT64_MAX fail + * with -EOVERFLOW because they cannot be represented by dmdrvi_ssize_t + * @param offset Unused beyond validation (dmspi is not seekable); must be non-negative + * + * @return dmdrvi_ssize_t Number of bytes received, or a negative errno-compatible error + */ +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, dmdrvi_ssize_t, _read, ( dmdrvi_context_t context, void* handle, void* buffer, size_t size, dmdrvi_offset_t offset )) { + if (offset < 0) + { + return -EINVAL; + } + if (size > (size_t)INT64_MAX) + { + return -EOVERFLOW; + } if (!is_valid_context(context) || buffer == NULL || size == 0) return 0; if (context->rx_ring != NULL) - return (size_t)dm_sw_ring_read(context->rx_ring, buffer, (dm_sw_ring_capacity_t)size); + { + /* dm_sw_ring's own interface caps a single transfer at + * dm_sw_ring_capacity_t (uint32_t) - clamp rather than truncate so a + * request larger than that does not silently wrap to a small value. */ + dm_sw_ring_capacity_t ring_size = (size > (size_t)UINT32_MAX) ? UINT32_MAX : (dm_sw_ring_capacity_t)size; + return (dmdrvi_ssize_t)dm_sw_ring_read(context->rx_ring, buffer, ring_size); + } if (context->config.role == dmspi_role_master) cs_assert(context); @@ -596,11 +627,36 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _read, ( dmdrvi_context_t co if (ret != 0) return 0; - return received; + return (dmdrvi_ssize_t)received; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _write, ( dmdrvi_context_t context, void* handle, const void* buffer, size_t size, uint32_t offset )) +/** + * @brief Write (transmit) bytes over the SPI bus + * + * dmspi is transaction-like, not a seekable/addressable device - there is no + * byte offset into anything, so offset is only validated (must be + * non-negative) and otherwise ignored, matching how dmclk treats offset for + * its own non-seekable device. + * + * @param context DMDRVI context + * @param handle Device handle (unused - dmspi has a single implicit device per context) + * @param buffer Buffer with data to write + * @param size Number of bytes to write; values greater than INT64_MAX fail + * with -EOVERFLOW because they cannot be represented by dmdrvi_ssize_t + * @param offset Unused beyond validation (dmspi is not seekable); must be non-negative + * + * @return dmdrvi_ssize_t Number of bytes transmitted, or a negative errno-compatible error + */ +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, dmdrvi_ssize_t, _write, ( dmdrvi_context_t context, void* handle, const void* buffer, size_t size, dmdrvi_offset_t offset )) { + if (offset < 0) + { + return -EINVAL; + } + if (size > (size_t)INT64_MAX) + { + return -EOVERFLOW; + } if (!is_valid_context(context) || buffer == NULL || size == 0) return 0; @@ -614,10 +670,10 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, size_t, _write, ( dmdrvi_context_t c if (ret != 0) return 0; - return size; + return (dmdrvi_ssize_t)size; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _ioctl, ( dmdrvi_context_t context, void* handle, int command, void* arg )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, int, _ioctl, ( dmdrvi_context_t context, void* handle, int command, void* arg )) { int ret = 0; if (!is_valid_context(context)) @@ -693,7 +749,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _ioctl, ( dmdrvi_context_t cont return ret; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _flush, ( dmdrvi_context_t context, void* handle )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, int, _flush, ( dmdrvi_context_t context, void* handle )) { if (!is_valid_context(context)) { @@ -709,7 +765,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _flush, ( dmdrvi_context_t cont return 0; } -dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _stat, ( dmdrvi_context_t context, const char* path, dmdrvi_stat_t* stat )) +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, int, _stat, ( dmdrvi_context_t context, const char* path, dmdrvi_stat_t* stat )) { if (!is_valid_context(context) || stat == NULL) { @@ -717,7 +773,7 @@ dmod_dmdrvi_dif_api_declaration(1.0, dmspi, int, _stat, ( dmdrvi_context_t conte return -EINVAL; } - stat->size = 0; /* Stream/message device, no fixed size */ + stat->size = (dmdrvi_size_t)0; /* Stream/message device, no fixed size */ stat->mode = 0666; return 0; } From a7c7f81680be0da984ba95cd748327bcfe89aa72 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 26 Sep 2026 17:10:16 +0100 Subject: [PATCH 07/10] CI: fetch dmspi's runtime dependencies before running dmod_loader dmod_link_modules only downloads headers at configure time and records runtime dependencies into the generated dmspi.dmd - it doesn't stage the actual .dmf blobs into build/dmf. The "Run tests with dmod_loader" step called dmod_loader directly without that fetch step, so dmhaman (a runtime dependency of dmspi) was never present, failing every run with "Cannot load module by name - module not found: dmhaman". The ctest target in tests/CMakeLists.txt already does this correctly (dmf-get install -d test_dmspi-local.dmd before dmod_loader) - this just brings the CI workflow's manual step in line with it, fetching dmspi's own dependencies via dmf-get -d build/dmf/dmspi.dmd first. Confirmed locally: dmhaman.dmf/dmini.dmf/dm_sw_ring.dmf are absent from build/dmf before this step and present after it. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f67a188..faab018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,11 @@ jobs: - name: Run tests with dmod_loader run: | export DMOD_DMF_DIR=$(pwd)/build/dmf + # dmod_link_modules only fetches headers at configure time; dmspi's + # own runtime dependencies (e.g. dmhaman) still need to be staged + # into build/dmf before dmod_loader can resolve them, same as the + # ctest target defined in tests/CMakeLists.txt already does. + dmf-get install -d build/dmf/dmspi.dmd -y dmod_loader build/dmf/test_dmspi.dmf discover-cpu-families: From efbe877aee65d8a6210cd26e052f0cbdc0944069 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 26 Sep 2026 17:18:11 +0100 Subject: [PATCH 08/10] CI: don't let dmspi_port's missing x86_64 build abort dmf-get install dmspi.dmd lists dmspi_port as a dependency, but this job never needs the real port (it only builds against dmspi_port_if headers, see the comment on the build step), and dmspi_port has no published x86_64 package anyway. Under `sh -e`, dmf-get returning non-zero because of that one irrelevant, expected-to-fail lookup aborted the script before dmod_loader ever ran - so the previous fix (fetching dmhaman/dmini/ dm_sw_ring) never actually got exercised. `|| true` on the dmf-get line lets that fail without masking a real problem: if dmhaman/dmini/dm_sw_ring genuinely failed to fetch, dmod_loader would still fail loudly and clearly on the missing module. --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faab018..77caf30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,12 @@ jobs: # own runtime dependencies (e.g. dmhaman) still need to be staged # into build/dmf before dmod_loader can resolve them, same as the # ctest target defined in tests/CMakeLists.txt already does. - dmf-get install -d build/dmf/dmspi.dmd -y + # dmspi.dmd also lists dmspi_port, which has no published x86_64 + # build and isn't needed here anyway (this job only builds against + # dmspi_port_if headers, never the real port - see the comment on + # the build step above) - `|| true` lets that one lookup fail + # without masking a real failure to fetch dmhaman/dmini/dm_sw_ring. + dmf-get install -d build/dmf/dmspi.dmd -y || true dmod_loader build/dmf/test_dmspi.dmf discover-cpu-families: From 95dfd82b7edde7565f8cc1aef59d17fa02e28ca4 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 26 Sep 2026 18:04:05 +0100 Subject: [PATCH 09/10] Add an x86_64 software-loopback dmspi_port so CI can enable dmspi test_dmspi's dmod_loader-based test needs the full dmspi module enabled, which pulls in its declared dmspi_port dependency - but the real port is STM32 hardware register access with no host build, and no x86_64 release has ever been published for it. Every CI run failed with "Cannot load required module 'dmspi_port'" once the earlier missing-dmhaman/dmspi_port-lookup issues were fixed. Added src/port/x86_64/port.c: a self-contained software loopback (bytes written via _transmit/_transfer's tx side are queued into the same instance's FIFO or RX ring, and drained by _receive/_transfer's rx side) that doesn't touch real hardware, dmclk_port, or dmosi, so it has no further unpublished dependencies of its own - confirmed its generated .dmd only requires dm_sw_ring, which does have an x86_64 release. Wired into the CI workflow: the core build step now also builds dmspi_port for DMOD_CPU_FAMILY=x86_64 so the .dmf is already staged in build/dmf before dmod_loader runs, and the discover-cpu-families / build-dmspi matrix job will now also produce a real x86_64 release artifact for it going forward. --- .github/workflows/ci.yml | 25 +-- src/port/x86_64/config.cmake | 6 + src/port/x86_64/port.c | 294 +++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 src/port/x86_64/config.cmake create mode 100644 src/port/x86_64/port.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77caf30..4e367f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,12 +23,14 @@ jobs: run: | mkdir -p build cd build - # dmspi links against dmspi_port_if (headers only), - # not dmspi_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 - cmake --build . --target dmspi test_dmspi + # dmspi links against dmspi_port_if (headers only), so building it + # never touches src/port/*/port.c. dmspi_port itself is also built + # here (against src/port/x86_64's software-loopback stub, selected + # via DMOD_CPU_FAMILY) purely so dmod_loader has something to + # satisfy dmspi's runtime dmspi_port dependency below - there is + # no published x86_64 release of the real hardware port. + cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 -DDMOD_CPU_FAMILY=x86_64 + cmake --build . --target dmspi dmspi_port test_dmspi - name: Verify module files run: | @@ -37,6 +39,7 @@ jobs: test -f build/dmf/dmspi.dmf test -f build/dmf/dmspi_version.txt test -f build/dmf/test_dmspi.dmf + test -f build/dmf/dmspi_port.dmf echo "Module files present" - name: Run tests with dmod_loader @@ -47,10 +50,12 @@ jobs: # into build/dmf before dmod_loader can resolve them, same as the # ctest target defined in tests/CMakeLists.txt already does. # dmspi.dmd also lists dmspi_port, which has no published x86_64 - # build and isn't needed here anyway (this job only builds against - # dmspi_port_if headers, never the real port - see the comment on - # the build step above) - `|| true` lets that one lookup fail - # without masking a real failure to fetch dmhaman/dmini/dm_sw_ring. + # release (only the real stm32f4/stm32f7 hardware ports are + # published) - the build step above already placed our own + # x86_64 dmspi_port.dmf (the loopback stub) in build/dmf, so + # `|| true` just lets dmf-get's redundant remote lookup for it + # fail without masking a real failure to fetch dmhaman/dmini/ + # dm_sw_ring. dmf-get install -d build/dmf/dmspi.dmd -y || true dmod_loader build/dmf/test_dmspi.dmf diff --git a/src/port/x86_64/config.cmake b/src/port/x86_64/config.cmake new file mode 100644 index 0000000..e360bee --- /dev/null +++ b/src/port/x86_64/config.cmake @@ -0,0 +1,6 @@ +# DMOD_TOOLS_NAME must match a directory under dmod/configs/arch/... +# Known mappings used elsewhere in the ecosystem: +# stm32f7 -> arch/armv7/cortex-m7 +# stm32f4 -> arch/armv7/cortex-m4 +# x86_64 -> arch/x86_64 +set(DMOD_TOOLS_NAME "arch/x86_64" CACHE STRING "Name of the tools configuration") diff --git a/src/port/x86_64/port.c b/src/port/x86_64/port.c new file mode 100644 index 0000000..327d8b6 --- /dev/null +++ b/src/port/x86_64/port.c @@ -0,0 +1,294 @@ +/** + * @file port.c + * @brief dmspi_port implementation for the x86_64 host. + * + * There is no real SPI peripheral to drive on a regular CI runner, so this + * port loops every instance back on itself in software: bytes handed to + * _transmit/_transfer's tx side are queued into that instance's own FIFO, + * and _receive/_transfer's rx side drain the same FIFO - exactly what a + * master would see if its MOSI pin were wired straight to its MISO pin. + * This exists purely so dmspi's dmod_loader-based tests (see ../../tests) + * can enable the full dmspi module - with its declared dmspi_port + * dependency - on a host that will never see real hardware. + */ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmspi_port.h" +#include "dmod.h" +#include + +#define DMSPI_PORT_MAX_INSTANCES 4 +#define DMSPI_PORT_FIFO_SIZE 256 + +typedef struct +{ + bool in_use; + dmspi_role_t role; + dmspi_baudrate_t baudrate; + dmspi_clock_polarity_t clock_polarity; + dmspi_clock_phase_t clock_phase; + dmspi_bit_order_t bit_order; + dmspi_nss_mode_t nss_mode; + dmspi_int_trigger_t interrupt_trigger; + dmspi_port_interrupt_handler_t irq_handler; + void* irq_user_ptr; + dm_sw_ring_t rx_ring; + + uint8_t fifo[DMSPI_PORT_FIFO_SIZE]; + size_t fifo_head; + size_t fifo_count; +} instance_state_t; + +static instance_state_t g_instances[DMSPI_PORT_MAX_INSTANCES]; + +static int validate_instance(dmspi_instance_t instance) +{ + return (instance >= 1 && instance <= DMSPI_PORT_MAX_INSTANCES) ? 0 : -1; +} + +static instance_state_t* get_instance(dmspi_instance_t instance) +{ + return &g_instances[instance - 1]; +} + +/* Queues one byte for loopback and fans it out to whichever consumer is + * listening: a registered RX ring (drained by _receive), an interrupt + * handler (if rx_not_empty is armed), or this instance's own FIFO (drained + * directly by _receive/_transfer when no ring is registered). */ +static void loopback_byte(dmspi_instance_t instance, instance_state_t* s, uint8_t byte) +{ + if (s->rx_ring != NULL) + { + dm_sw_ring_write(s->rx_ring, &byte, 1); + } + else + { + if (s->fifo_count >= DMSPI_PORT_FIFO_SIZE) + { + /* Overrun: drop the oldest queued byte rather than block, same + * as a real peripheral's OVR would lose data instead of stall. */ + s->fifo_head = (s->fifo_head + 1) % DMSPI_PORT_FIFO_SIZE; + s->fifo_count--; + } + s->fifo[(s->fifo_head + s->fifo_count) % DMSPI_PORT_FIFO_SIZE] = byte; + s->fifo_count++; + } + + if (s->irq_handler != NULL && (s->interrupt_trigger & dmspi_int_trigger_rx_not_empty) != 0) + { + s->irq_handler(s->irq_user_ptr, instance, dmspi_int_trigger_rx_not_empty, byte); + } +} + +static size_t drain_fifo(instance_state_t* s, uint8_t* data, size_t size) +{ + size_t n = (s->fifo_count < size) ? s->fifo_count : size; + for (size_t i = 0; i < n; i++) + { + data[i] = s->fifo[(s->fifo_head + i) % DMSPI_PORT_FIFO_SIZE]; + } + s->fifo_head = (s->fifo_head + n) % DMSPI_PORT_FIFO_SIZE; + s->fifo_count -= n; + return n; +} + +int dmod_init(const Dmod_Config_t *Config) +{ + memset(g_instances, 0, sizeof(g_instances)); + Dmod_Printf("dmspi port module initialized (x86_64 loopback stub)\n"); + return 0; +} + +int dmod_deinit(void) +{ + Dmod_Printf("dmspi port module deinitialized (x86_64 loopback stub)\n"); + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _init, ( dmspi_instance_t instance, dmspi_role_t role )) +{ + if (validate_instance(instance) != 0) return -1; + + instance_state_t* s = get_instance(instance); + memset(s, 0, sizeof(*s)); + s->in_use = true; + s->role = role; + s->bit_order = dmspi_bit_order_msb_first; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _deinit, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return -1; + + instance_state_t* s = get_instance(instance); + memset(s, 0, sizeof(*s)); + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _transfer, ( dmspi_instance_t instance, const uint8_t* tx, uint8_t* rx, size_t size )) +{ + if (validate_instance(instance) != 0) return -1; + + instance_state_t* s = get_instance(instance); + for (size_t i = 0; i < size; i++) + { + uint8_t tx_byte = tx ? tx[i] : 0xFFU; + loopback_byte(instance, s, tx_byte); + + uint8_t rx_byte = 0xFFU; + drain_fifo(s, &rx_byte, 1); + if (rx != NULL) rx[i] = rx_byte; + } + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _transmit, ( dmspi_instance_t instance, const uint8_t* data, size_t size )) +{ + if (data == NULL) return -1; + return dmspi_port_transfer(instance, data, NULL, size); +} + +dmod_dmspi_port_api_declaration(1.0, int, _receive, ( dmspi_instance_t instance, uint8_t* data, size_t size, size_t* received )) +{ + if (validate_instance(instance) != 0 || data == NULL || received == NULL) return -1; + + instance_state_t* s = get_instance(instance); + if (s->rx_ring != NULL) + { + *received = (size_t)dm_sw_ring_read(s->rx_ring, data, (dm_sw_ring_capacity_t)size); + } + else + { + *received = drain_fifo(s, data, size); + } + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, bool, _is_busy, ( dmspi_instance_t instance )) +{ + return false; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_role, ( dmspi_instance_t instance, dmspi_role_t role )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->role = role; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_role_t, _get_role, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_role_master; + return get_instance(instance)->role; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_baudrate, ( dmspi_instance_t instance, dmspi_baudrate_t baudrate )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->baudrate = baudrate; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_baudrate_t, _get_baudrate, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return 0; + return get_instance(instance)->baudrate; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_clock_polarity, ( dmspi_instance_t instance, dmspi_clock_polarity_t polarity )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->clock_polarity = polarity; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_clock_polarity_t, _get_clock_polarity, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_clock_polarity_low; + return get_instance(instance)->clock_polarity; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_clock_phase, ( dmspi_instance_t instance, dmspi_clock_phase_t phase )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->clock_phase = phase; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_clock_phase_t, _get_clock_phase, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_clock_phase_1_edge; + return get_instance(instance)->clock_phase; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_bit_order, ( dmspi_instance_t instance, dmspi_bit_order_t bit_order )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->bit_order = bit_order; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_bit_order_t, _get_bit_order, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_bit_order_msb_first; + return get_instance(instance)->bit_order; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_nss_mode, ( dmspi_instance_t instance, dmspi_nss_mode_t nss_mode )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->nss_mode = nss_mode; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, dmspi_nss_mode_t, _get_nss_mode, ( dmspi_instance_t instance )) +{ + if (validate_instance(instance) != 0) return dmspi_nss_mode_soft; + return get_instance(instance)->nss_mode; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t trigger )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->interrupt_trigger = trigger; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _read_interrupt_trigger, ( dmspi_instance_t instance, dmspi_int_trigger_t *out_trigger )) +{ + if (validate_instance(instance) != 0 || out_trigger == NULL) return -1; + *out_trigger = get_instance(instance)->interrupt_trigger; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _add_interrupt_handler, + ( dmspi_instance_t instance, dmspi_port_interrupt_handler_t handler, void *user_ptr )) +{ + if (validate_instance(instance) != 0 || handler == NULL) return -1; + + instance_state_t* s = get_instance(instance); + s->irq_handler = handler; + s->irq_user_ptr = user_ptr; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _remove_interrupt_handler, + ( dmspi_instance_t instance, void *user_ptr )) +{ + if (validate_instance(instance) != 0) return -1; + + instance_state_t* s = get_instance(instance); + if (s->irq_user_ptr != user_ptr) return -1; + + s->irq_handler = NULL; + s->irq_user_ptr = NULL; + return 0; +} + +dmod_dmspi_port_api_declaration(1.0, int, _set_rx_ring, + ( dmspi_instance_t instance, dm_sw_ring_t ring )) +{ + if (validate_instance(instance) != 0) return -1; + get_instance(instance)->rx_ring = ring; + return 0; +} From f869762f993e365496aeaaf6e1861635e3400352 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sat, 26 Sep 2026 18:20:26 +0100 Subject: [PATCH 10/10] CI: build the x86_64 dmspi_port stub in a separate configure Setting DMOD_CPU_FAMILY=x86_64 on the same configure that builds dmspi itself broke dmgpio's header fetch: dmod_link_modules keys dmgpio's header download off DMOD_CPU_FAMILY too (not just DMOD_TOOLS_NAME), and dmgpio has never published an x86_64 package - only stm32f4/stm32f7, which is also all its own release pipeline ever builds. Previously DMOD_CPU_FAMILY defaulted to stm32f7 and that fetch worked by accident; overriding it to x86_64 made it request the (nonexistent) dmgpio-v1.0-x86_64.zip and 404, breaking the dmspi/test_dmspi build that already worked in the previous run. Split it into its own build_dmspi_port_x86_64/ configure that only builds the dmspi_port target, then copy just its .dmf/.dmd into the main build/dmf/ before the dmf-get + dmod_loader step. Confirmed locally: dmspi/test_dmspi build cleanly with the default CPU family, dmspi_port builds cleanly in the separate x86_64 configure, and the copied dmspi_port.dmf survives dmf-get's (expected, `|| true`'d) failed remote lookup for it. Also fixes the same DMOD_CPU_FAMILY=x86_64 problem in the discover-cpu-families/build-dmspi matrix job: it now builds only the dmspi_port target for the x86_64 entry instead of the default "all", which was hitting the identical dmgpio 404 and getting the whole matrix cancelled via fail-fast. --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e367f6..7c20006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,14 +23,32 @@ jobs: run: | mkdir -p build cd build - # dmspi links against dmspi_port_if (headers only), so building it - # never touches src/port/*/port.c. dmspi_port itself is also built - # here (against src/port/x86_64's software-loopback stub, selected - # via DMOD_CPU_FAMILY) purely so dmod_loader has something to - # satisfy dmspi's runtime dmspi_port dependency below - there is - # no published x86_64 release of the real hardware port. + # dmspi links against dmspi_port_if (headers only), + # not dmspi_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. Leave + # DMOD_CPU_FAMILY at its default (stm32f7): dmod_link_modules' + # header fetch for dmgpio keys off of it too, and dmgpio has never + # published an x86_64 package - only stm32f4/stm32f7, which is + # also all its release pipeline ever builds. Overriding it to + # x86_64 here 404s that fetch, since dmgpio's headers don't come + # from a family-independent package. + cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 + cmake --build . --target dmspi test_dmspi + + - name: Build x86_64 dmspi_port loopback stub + run: | + # Separate build dir/configure so DMOD_CPU_FAMILY=x86_64 here can't + # affect the dmgpio header fetch above. dmspi_port itself has no + # x86_64 release (it's real STM32 register access) - this builds + # src/port/x86_64/port.c, a software loopback stub that exists + # purely so dmod_loader has something to satisfy dmspi's runtime + # dmspi_port dependency with below. + mkdir -p build_dmspi_port_x86_64 + cd build_dmspi_port_x86_64 cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_TOOLS_NAME=arch/x86_64 -DDMOD_CPU_FAMILY=x86_64 - cmake --build . --target dmspi dmspi_port test_dmspi + cmake --build . --target dmspi_port + cp dmf/dmspi_port.dmf dmf/dmspi_port.dmd ../build/dmf/ - name: Verify module files run: | @@ -107,7 +125,17 @@ jobs: cd build_$CPU_FAMILY cmake .. -DDMOD_CPU_FAMILY="${CPU_FAMILY}" - cmake --build . + if [ "$CPU_FAMILY" = "x86_64" ]; then + # x86_64 only exists as a software-loopback stand-in for the + # real (hardware-only) dmspi_port - it has no dmspi release of + # its own to build, and building the full "all" target here + # would also re-fetch dmgpio's headers keyed off this same + # DMOD_CPU_FAMILY, which 404s since dmgpio never published an + # x86_64 package (see the core job's comment on this). + cmake --build . --target dmspi_port + else + cmake --build . + fi echo "Build completed for CPU family: ${CPU_FAMILY}" ls -lh dmf/