This Zephyr module provides a standard SPI controller for devices whose MOSI
and MISO signals are physically tied to one bidirectional SDIO pin. It exists
for drivers that use the normal Zephyr spi_transceive() API but need the data
pin to change from output to input without releasing chip select.
The initial use case is the unmodified
cormoran/zmk-driver-pmw3610-with-custom-studio-rpc
driver on ZMK 0.4 / Zephyr 4.1. The controller is not PMW3610-specific: the
turnaround and chip-select timing are devicetree properties.
- SPI controller/master mode, mode 3, 8-bit words, MSB first;
- TX-only and RX-only transfers;
- standard full-duplex buffer progression where RX bytes overlapping TX are
discard buffers (
buf = NULL); - sequential TX then RX when the caller sets
SPI_HALF_DUPLEX; - multiple controller instances and multiple GPIO chip selects;
- synchronous transfers. Async SPI is reported as unsupported.
A shared data wire cannot receive useful data while the controller is driving
it. Therefore a full-duplex call with a non-NULL RX buffer overlapping TX is
rejected with -ENOTSUP. This catches ambiguous transfers instead of silently
returning the transmitted bits as received data.
The cormoran PMW3610 register-read shape works without changing that driver:
TX: [address]
RX: [NULL discard, data ...]
The address clocks while the leading discard advances. When TX is exhausted,
the controller changes SDIO to input, waits turnaround-delay-us, and clocks
the remaining RX data with the same CS still asserted.
Add this repository as a Zephyr module, then use the controller as the parent SPI bus instead of a hardware SPIM node:
#include <zephyr/dt-bindings/input/input-event-codes.h>
/ {
spi_three_wire: spi-three-wire {
compatible = "te9no,spi-three-wire-gpio";
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
sck-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
sdio-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
cs-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
/* PMW3610-safe values, including the 30 us post-write recovery. */
turnaround-delay-us = <4>;
select-delay-us = <1>;
deselect-delay-us = <4>;
recovery-delay-us = <30>;
trackball: trackball@0 {
compatible = "cormoran,pmw3610";
reg = <0>;
spi-max-frequency = <1500000>;
irq-gpios = <&gpio0 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
evt-type = <INPUT_EV_REL>;
x-input-code = <INPUT_REL_X>;
y-input-code = <INPUT_REL_Y>;
disable-burst-read;
};
};
};
Use GPIO_ACTIVE_HIGH for SCK and SDIO. cs-gpios uses normal Zephyr SPI
semantics and may be active low or active high. Each child node's reg selects
the matching entry in the controller's cs-gpios array.
The bit-bang half-period is calculated from each child's
spi-max-frequency. Since k_busy_wait() has microsecond resolution, it is
rounded up and the actual clock never exceeds the requested maximum. Requests
above 500 kHz currently run at approximately 500 kHz.
recovery-delay-us is intentionally generic and defaults to zero in the
binding. PMW3610 integrations should set it to at least 30 microseconds so
successive register writes satisfy the sensor's post-write recovery timing.
SPI_HOLD_ON_CS and SPI_LOCK_ON are rejected. Every successful synchronous
call owns the bus for its entire duration and releases CS before returning.
tests/zmk-config contains a ZMK 0.4 fixture with an unchanged
cormoran,pmw3610 child on this controller. In a west workspace that already
contains ZMK and the cormoran PMW3610 module:
west build -p always -s zmk/app -b xiao_ble//zmk \
-S spi-three-wire-pmw3610 -- \
-DZMK_CONFIG="$PWD/tests/zmk-config/config" \
-DZMK_EXTRA_MODULES="$PWD;$PWD/tests/zmk-config" \
-DSHIELD=tester_xiao
python3 tests/verify_build.py buildThe verifier checks the generated Kconfig, devicetree, linked controller API symbols, and UF2 output.
Enable the controller's informational log level when validating hardware:
CONFIG_SPI_THREE_WIRE_GPIO_LOG_LEVEL_INF=yInitialization prints the selected SCK/SDIO pins and turnaround delay. The first actual output-to-input turnaround is logged once. At informational log level, each controller instance also summarizes only its first 16 non-empty transfers: sequence number, the first two transmitted bytes, the first four actual received bytes (excluding NULL/discard buffers), and the result code. After transfer 16 this startup trace stops permanently. Transfer failures are logged for the first three occurrences and then at power-of-two counts, which keeps a persistent motion-read failure visible without flooding CDC during normal polling. Unsupported transfer shapes are logged as errors immediately.
For Polaris integration, retain the right-side zmk-usb-logging and
cdc-debug-boot snippets. Also retain the PMW3610 driver's own diagnostic log
level so CDC captures PMW initialization/self-test, Product ID, Revision,
performance register, and motion-read failures. This module does not replace
or disable either CDC snippet.
Apache-2.0.