Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GPIO three-wire SPI controller for Zephyr

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.

Supported transfers

  • 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.

Devicetree

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.

ZMK build test

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 build

The verifier checks the generated Kconfig, devicetree, linked controller API symbols, and UF2 output.

CDC diagnostics

Enable the controller's informational log level when validating hardware:

CONFIG_SPI_THREE_WIRE_GPIO_LOG_LEVEL_INF=y

Initialization 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.

License

Apache-2.0.

About

Generic GPIO-backed three-wire SPI controller for Zephyr and ZMK

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages