diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f67a188..7c20006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,30 @@ jobs: # 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. + # 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_port + cp dmf/dmspi_port.dmf dmf/dmspi_port.dmd ../build/dmf/ + - name: Verify module files run: | echo "Checking for module files..." @@ -37,11 +57,24 @@ 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 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. + # dmspi.dmd also lists dmspi_port, which has no published x86_64 + # 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 discover-cpu-families: @@ -92,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/ diff --git a/CMakeLists.txt b/CMakeLists.txt index fb3195b..d1cea21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,14 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} src/dmspi.c ) +dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi + dmini + dmhaman + dm_sw_ring + dmgpio +) + target_include_directories(${DMOD_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ) @@ -75,12 +83,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..6e8bf31 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,33 @@ [![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 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 +[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 +70,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" -``` +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). -## API - -| 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 +149,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..80b8f9c --- /dev/null +++ b/configs/README.md @@ -0,0 +1,196 @@ +# 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. +- 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. 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=2 +friends_group=arduino_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 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) | +| 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..c6e9967 --- /dev/null +++ b/configs/board/nucleo-f401re/spi1.ini @@ -0,0 +1,66 @@ +; 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. 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=2 +friends_group=arduino_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..750b30d --- /dev/null +++ b/configs/board/nucleo-f411re/spi1.ini @@ -0,0 +1,66 @@ +; 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. 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=2 +friends_group=arduino_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..5752ce3 --- /dev/null +++ b/configs/board/nucleo-f446re/spi1.ini @@ -0,0 +1,66 @@ +; 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. 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=2 +friends_group=arduino_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..ff8171b --- /dev/null +++ b/configs/board/nucleo-f767zi/spi1.ini @@ -0,0 +1,77 @@ +; 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 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" +; (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). D10 (PD14) is a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..c3c355c --- /dev/null +++ b/configs/board/stm32f4-discovery/spi1.ini @@ -0,0 +1,75 @@ +; 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). 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=2 +friends_group=mems_spi +pin=PA5 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[mems_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=mems_spi +pin=PA6 +mode=alternate +alternate_function=5 +pull=none + +[mems_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=mems_spi +pin=PA7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[mems_spi] +driver_name=dmspi +driver_order=2 +friends_group=mems_spi +instance=1 +role=master +baudrate=1000000 +mode=3 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..87f32f0 --- /dev/null +++ b/configs/board/stm32f429i-discovery/spi5.ini @@ -0,0 +1,69 @@ +; 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). 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=2 +friends_group=gyro_spi +pin=PF7 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[gyro_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=gyro_spi +pin=PF8 +mode=alternate +alternate_function=5 +pull=none + +[gyro_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=gyro_spi +pin=PF9 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[gyro_spi] +driver_name=dmspi +driver_order=2 +friends_group=gyro_spi +instance=5 +role=master +baudrate=1000000 +mode=3 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..7a48730 --- /dev/null +++ b/configs/board/stm32f746g-disco/spi2.ini @@ -0,0 +1,75 @@ +; 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). 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=2 +friends_group=arduino_spi +pin=PI1 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PB14 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PB15 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=2 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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 new file mode 100644 index 0000000..7a97b0c --- /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). D10 (PF7) is a +; separate dmgpio friend below and dmspi drives it through its dmdevfs path. + +[arduino_spi_sck] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PH6 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi_miso] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PJ4 +mode=alternate +alternate_function=5 +pull=none + +[arduino_spi_mosi] +driver_name=dmgpio +driver_order=2 +friends_group=arduino_spi +pin=PJ3 +mode=alternate +alternate_function=5 +speed=maximum +output_circuit=push_pull +pull=none + +[arduino_spi] +driver_name=dmspi +driver_order=2 +friends_group=arduino_spi +instance=2 +role=master +baudrate=1000000 +mode=0 +bit_order=msb_first +nss_mode=soft +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/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..38c50ab 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,12 +1,55 @@ # 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_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). | +| `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..fd74af6 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,223 @@ +# 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; 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) | +| `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. +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=2 +friends_group=flash_spi +instance=1 +role=master +baudrate=4000000 +mode=0 +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", +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 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 a GPIO friend + +```ini +[spi] +driver_name=dmspi +driver_order=2 +friends_group=example_spi +instance=1 +role=master +baudrate=1000000 +mode=0 +nss_mode=soft + +[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 + +```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). + +`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) + +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..fa6c256 100644 --- a/src/dmspi.c +++ b/src/dmspi.c @@ -1,84 +1,779 @@ -#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 "dmgpio_types.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 ---- + * + * 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. + */ + +/** + * @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) */ + char *cs_path; /**< Friend GPIO path (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_set_active(dmdrvi_context_t context, bool active) { - /* 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_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) { - return 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) +{ + cs_set_active(context, true); +} + +static void cs_deassert(dmdrvi_context_t context) +{ + cs_set_active(context, false); +} + +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) + { + 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; +} + +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; +} - instance->valid = true; - return instance; +static dmspi_nss_mode_t string_to_nss_mode(const char *s) +{ + if (s != NULL && strcmp(s, "hard") == 0) return dmspi_nss_mode_hard; + return dmspi_nss_mode_soft; } -dmod_dmspi_api_declaration(1.0, void, _destroy, ( dmspi_t handle )) +static dmspi_int_trigger_t string_to_interrupt_trigger(const char *s) { - Dmod_Free(handle); + 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; } -dmod_dmspi_api_declaration(1.0, bool, _is_valid, ( dmspi_t handle )) +static dm_sw_ring_flags_t string_to_rx_ring_wait_flags(const char *s) { - return handle != NULL && handle->valid; + 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. + * @brief Detect which INI section holds the SPI configuration. * - * @note This function is optional. You can remove it if you don't need it. + * 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). * - * This function is called when the module enabling is in progress. - * - * You can use this function to load the required dependencies, such as - * other modules. Please be aware that the module is not fully initialized, - * so not all the API functions are available - you can check if the API - * is connected by calling the Dmod_IsFunctionConnected() function. + * 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; + 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; + + 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. - * - * 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 + * @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_Printf("Hello, World!\n"); + DMOD_LOG_INFO("DMSPI interface module initialized\n"); return 0; } +int dmod_deinit(void) +{ + DMOD_LOG_INFO("DMSPI interface module deinitialized\n"); + return 0; +} + +/* ---- DMDRVI interface ---- */ + +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) + { + 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 || configure(context) != 0) + { + DMOD_LOG_ERROR("Failed to create DMDRVI context with provided configuration\n"); + 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(2.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; + } + + /* 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; + Dmod_Free(context); + } +} + +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 || + 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(2.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(2.0, dmspi, void, _close, ( dmdrvi_context_t context, void* handle )) +{ + /* No specific action needed to close the SPI device handle */ +} + /** - * @brief De-initialization function for the module. + * @brief Read (receive) bytes over the SPI bus * - * 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 + * 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 */ -int dmod_deinit(void) +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) + { + /* 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); + + 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 (dmdrvi_ssize_t)received; +} + +/** + * @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; + + 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 (dmdrvi_ssize_t)size; +} + +dmod_dmdrvi_dif_api_declaration(2.0, dmspi, int, _ioctl, ( dmdrvi_context_t context, void* handle, int command, void* arg )) { - Dmod_Printf("Goodbye, World!\n"); + 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(2.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(2.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 = (dmdrvi_size_t)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..9ecfa48 --- /dev/null +++ b/src/port/stm32_common/stm32_common.c @@ -0,0 +1,536 @@ +/* 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; +} + +/* 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]; + *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); + clear_receive_state(SPI); + + 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; + } + write_data_byte(SPI, tx ? tx[i] : 0xFFU); + + timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_RXNE)) + { + if (--timeout == 0) return -1; + } + + 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)) + { + /* 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; + } + write_data_byte(SPI, 0xFFU); /* dummy byte keeps the clock moving */ + + timeout = STM32_SPI_TIMEOUT_VALUE; + while (!(SPI->SR & STM32_SPI_SR_RXNE)) + { + if (--timeout == 0) return 0; + } + + data[i] = read_data_byte(SPI); + uint32_t sr = SPI->SR; + + 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 = read_data_byte(SPI); + 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)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); + } + + 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..9719673 --- /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 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 +{ + 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/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; +} 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..d82659a --- /dev/null +++ b/tools/spitest/main.c @@ -0,0 +1,279 @@ +#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; +} + +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 ---- */ + +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(" %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"); + 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"); + Dmod_Printf(" --echo Test one master with MOSI physically connected to MISO.\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 (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; + + 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]