diff --git a/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml new file mode 100644 index 0000000000000..b35a575c07cbe --- /dev/null +++ b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: GPL-2.0 +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/spi/multitracks,spi-transport.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: MultiTracks SPI transport protocol (Host role) + +maintainers: + - Michael Loh + +description: | + A framed, multi-channel, ack/retry link between an i.MX SoC (Host) and an + STM32-class client MCU (Client) over a raw SPI bus, plus a companion NRDY + GPIO handshake line in addition to the physical SPI SCK/MOSI/MISO lines. + See the firmware repo's firmware-common/spi-transport/docs/ProtocolSpec.md + for the full wire protocol. + + NSS and NRDY are bound here as driver-private properties, not the + standard "cs-gpios" -- the protocol holds NSS low continuously across a + multi-step request/ack/clock/complete cycle, not just one SPI transfer, + so the driver owns and drives it directly rather than letting the SPI + core auto-toggle it per-message. + +allOf: + - $ref: /schemas/spi/spi-peripheral-props.yaml# + +properties: + compatible: + const: multitracks,spi-transport + + reg: + maxItems: 1 + + multitracks,nss-gpios: + maxItems: 1 + description: + Host-driven NSS (transfer request) line, held low across the whole + request/ack/clock/complete cycle. Always GPIO_ACTIVE_HIGH regardless + of the physical wire's true active sense, so the driver's logical + value always equals the literal pin level (the protocol's own + convention is "true = pin HIGH"). + + multitracks,nrdy-gpios: + maxItems: 1 + description: + Client-driven NRDY (ready/ack) line, read by the Host. Same + GPIO_ACTIVE_HIGH convention as multitracks,nss-gpios. + +required: + - compatible + - reg + - spi-max-frequency + - multitracks,nss-gpios + - multitracks,nrdy-gpios + +unevaluatedProperties: false + +examples: + - | + #include + spi { + #address-cells = <1>; + #size-cells = <0>; + + spi@0 { + compatible = "multitracks,spi-transport"; + reg = <0>; + spi-max-frequency = <500000>; + + multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; + }; + }; + +... diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml index 7bf6e3d5d8b6b..c98d8091102d8 100644 --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml @@ -912,6 +912,8 @@ patternProperties: description: Imagination Technologies Ltd. (formerly MIPS Technologies Inc.) "^multi-inno,.*": description: Multi-Inno Technology Co.,Ltd + "^multitracks,.*": + description: MultiTracks.com, LLC "^mundoreader,.*": description: Mundo Reader S.L. "^murata,.*": diff --git a/arch/arm64/boot/dts/freescale/Makefile b/arch/arm64/boot/dts/freescale/Makefile index 6959e1e016b2b..9408152e2bcb9 100644 --- a/arch/arm64/boot/dts/freescale/Makefile +++ b/arch/arm64/boot/dts/freescale/Makefile @@ -75,6 +75,7 @@ dtb-$(CONFIG_ARCH_MXC) += imx8mm-ab2.dtb imx8mm-ab2-m4.dtb imx8mm-ddr4-ab2.dtb i dtb-$(CONFIG_ARCH_MXC) += imx8mm-evk.dtb imx8mm-evk-rpmsg.dtb imx8mm-evk-rm67191.dtb \ imx8mm-evk-root.dtb imx8mm-evk-inmate.dtb imx8mm-evk-revb-qca-wifi.dtb \ imx8mm-evk-ecspi-slave.dtb \ + imx8mm-evk-spi-transport.dtb \ imx8mm-evk-pcie-ep.dtb \ imx8mm-evk-usd-wifi.dtb \ imx8mm-evk-qca-wifi.dtb \ diff --git a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts new file mode 100644 index 0000000000000..271cc35835278 --- /dev/null +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright 2026 MultiTracks.com, LLC. +// +// MT-158113: binds the MultiTracks SPI transport driver (Host role) onto +// ecspi2, mirroring the pins already used for this same purpose on the +// mt-connect production board (mt-connect.dts) -- see +// firmware/firmware-common/spi-transport/docs/ProtocolSpec.md for the wire +// protocol and NSS/NRDY handshake this binds to real hardware. +// +// NSS/NRDY are bound here as driver-private "multitracks,nss-gpios"/ +// "multitracks,nrdy-gpios" properties, NOT the standard "cs-gpios" -- the +// transport protocol holds +// NSS low continuously across a multi-step request/ack/clock/complete +// cycle, not just one SPI transfer, so the driver must own and drive it +// directly rather than let the SPI core auto-toggle it per-message. Both +// are declared GPIO_ACTIVE_HIGH regardless of the physical wire's true +// active sense, so the driver's gpiod_get/set_value_cansleep() calls +// always see the literal pin level, matching the protocol's own +// "true = pin HIGH" convention (see drivers/spi/spi-mt-transport/ +// spi_transport_hw_linux.c). + +#include "imx8mm-evk.dts" + +/delete-node/ &spidev0; + +&ecspi2 { + /delete-property/ cs-gpios; + pinctrl-names = "default"; + pinctrl-0 = <&pinctrl_ecspi2 &pinctrl_ecspi2_cs &pinctrl_ecspi2_nrdy>; + + mt_transport0: spi@0 { + compatible = "multitracks,spi-transport"; + reg = <0>; + spi-max-frequency = <4000000>; /* matches mt-connect.dts -- stepping down from 10MHz to isolate a clock-rate-dependent stall, MT-159369 */ + + multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; + }; +}; + +&iomuxc { + pinctrl_ecspi2_nrdy: ecspi2nrdygrp { + fsl,pins = < + MX8MM_IOMUXC_SAI3_RXC_GPIO4_IO29 0x82 + >; + }; +}; diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index c56236f14e9f2..77053bf7b90a2 100644 --- a/arch/arm64/boot/dts/freescale/mt-connect.dts +++ b/arch/arm64/boot/dts/freescale/mt-connect.dts @@ -508,16 +508,33 @@ status = "okay"; }; +/* MT-159369: binds the MultiTracks SPI transport driver (Host role), + * porting the binding already hardware-validated on imx8mm-evk-spi-transport.dts + * (MT-158113) back to this production board -- same NSS/NRDY pins + * (GPIO5_IO13 / GPIO4_IO29, this board's own SPI2_NSS / SPI2_INT pin + * names, per the gpio5/gpio4 gpio-line-names above), same reasoning for + * multitracks,nss-gpios replacing cs-gpios (the transport protocol holds + * NSS low across a multi-step request/ack/clock/complete cycle, not just + * one SPI transfer, so the driver must own and drive it directly rather + * than let the SPI core auto-toggle it per-message) and for declaring + * both driver-private GPIOs GPIO_ACTIVE_HIGH regardless of physical wire + * polarity (see drivers/spi/spi-mt-transport/spi_transport_hw_linux.c). + * pinctrl_ecspi2 below already covers both the NSS and INT pin muxes on + * this board (unlike the EVK, which needed separate overlay-added + * groups), so no pinctrl changes are needed here. + */ &ecspi2 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi2>; - cs-gpios = <&gpio5 13 GPIO_ACTIVE_LOW>; - spidev1: spi@0 { + mt_transport0: spi@0 { + compatible = "multitracks,spi-transport"; reg = <0>; - compatible = "rohm,dh2228fv"; - spi-max-frequency = <500000>; + spi-max-frequency = <4000000>; /* MT-159369: stepping down from 10MHz to isolate a clock-rate-dependent DMA-arm-failure/CRC stall -- 500kHz soak-tested clean, 10MHz stalls within seconds */ + + multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; }; }; diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 3ce0fd5df8e9c..8710a56cc1306 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1211,6 +1211,27 @@ config SPI_LOOPBACK_TEST primarily used for development of spi_master drivers and to detect regressions +config SPI_MT_TRANSPORT + tristate "MultiTracks SPI transport protocol driver (Host role)" + # No COMPILE_TEST: this module's core/ subdirectory only exists once + # Yocto's meta-mt-transport-evk stages it in at build time (see this + # directory's Makefile) -- allmodconfig/allyesconfig CI builds outside + # that pipeline would hard-error on core/ being missing. Found by + # Copilot's PR #46 review. + depends on ARCH_MXC + depends on GPIOLIB + # Driver unconditionally registers dev_groups/DEVICE_ATTR_RO sysfs + # attributes and calls sysfs_emit() -- non-functional without SYSFS. + # Found by Copilot's PR #46 review. + depends on SYSFS + help + Kernel Host-role port of the MultiTracks SPI transport protocol + (see firmware repo firmware-common/spi-transport/) -- a framed, + multi-channel, ack/retry link between an i.MX SoC (Host) and an + STM32-class client MCU (Client) over a raw SPI bus, plus a + companion NRDY GPIO handshake line in addition to the physical + SPI SCK/MOSI/MISO/NSS lines. + config SPI_TLE62X0 tristate "Infineon TLE62X0 (for power switching)" depends on SYSFS diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile index 6af54842b9fa4..657b9a6f099ea 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -140,6 +140,7 @@ obj-$(CONFIG_SPI_TEGRA210_QUAD) += spi-tegra210-quad.o obj-$(CONFIG_SPI_TEGRA114) += spi-tegra114.o obj-$(CONFIG_SPI_TEGRA20_SFLASH) += spi-tegra20-sflash.o obj-$(CONFIG_SPI_TEGRA20_SLINK) += spi-tegra20-slink.o +obj-$(CONFIG_SPI_MT_TRANSPORT) += spi-mt-transport/ obj-$(CONFIG_SPI_TLE62X0) += spi-tle62x0.o spi-thunderx-objs := spi-cavium.o spi-cavium-thunderx.o obj-$(CONFIG_SPI_THUNDERX) += spi-thunderx.o diff --git a/drivers/spi/spi-mt-transport/.gitignore b/drivers/spi/spi-mt-transport/.gitignore new file mode 100644 index 0000000000000..7edc66e7a5611 --- /dev/null +++ b/drivers/spi/spi-mt-transport/.gitignore @@ -0,0 +1,5 @@ +# core/ is the portable SPI transport protocol core -- single source of +# truth lives in the firmware repo (firmware-common/spi-transport/), staged +# here at Yocto build time (see imx8mmini-bb-evk's meta-mt-transport-evk +# linux-imx_%.bbappend), never committed into this repo. +core/ diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile new file mode 100644 index 0000000000000..f867f1e30b3c0 --- /dev/null +++ b/drivers/spi/spi-mt-transport/Makefile @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# IMPORTANT -- this module only builds inside the imx8mmini-bb-evk Yocto BSP +# pipeline, not from a plain checkout of this kernel tree built directly +# (e.g. a bare `make -C . M=drivers/spi/spi-mt-transport modules`). The +# core/ referenced below is deliberately NOT committed to this repo (see +# core/'s absence and the .gitignore in this directory) -- it's the +# portable protocol core, whose single source of truth is the firmware +# repo's firmware-common/spi-transport/{src,inc}/. Yocto's +# meta-mt-transport-evk/recipes-kernel/linux-imx/linux-imx_%.bbappend +# fetches it separately and stages it into core/ via a do_patch[postfuncs] +# hook *before* this Makefile ever runs. This Makefile is only ever +# evaluated when CONFIG_SPI_MT_TRANSPORT is already enabled (Kbuild only +# descends here via drivers/spi/Makefile's obj-$(CONFIG_SPI_MT_TRANSPORT) +# line), so the check below fires exactly when it matters and fails fast +# with a clear message instead of the opaque "No rule to make target +# core/spi_transport.o" a missing core/ would otherwise produce deep in +# the build. +# $(src) alone resolves against $(objtree) under Yocto's out-of-tree (O=) +# kernel builds, same reason the ccflags-y line below needs the +# $(srctree)/ prefix -- this check needs it too, or it always reports +# core/ missing even when it's genuinely staged (confirmed via a real +# false-positive build failure without this prefix). +ifeq ($(wildcard $(srctree)/$(src)/core/spi_transport.c),) +$(error spi-mt-transport: core/ is not staged. This module only builds inside \ + the imx8mmini-bb-evk Yocto BSP pipeline, which fetches the portable \ + protocol core from the firmware repo and stages it into core/ before \ + this Makefile runs -- build via bitbake, not a plain kernel checkout) +endif + +obj-$(CONFIG_SPI_MT_TRANSPORT) += spi-mt-transport.o +spi-mt-transport-y := \ + core/spi_transport.o \ + core/spi_transport_channel.o \ + core/spi_transport_frame.o \ + core/spi_transport_crc16.o \ + core/spi_transport_hw.o \ + spi_transport_os_linux.o \ + spi_transport_hw_linux.o \ + spi_mt_transport_drv.o + +# $(src) alone resolves against $(objtree) under Yocto's out-of-tree (O=) +# kernel builds; these headers only exist in the source tree, so the +# $(srctree)/ prefix is required or the compile can't find them. +ccflags-y += -I$(srctree)/$(src)/kernel-compat -I$(srctree)/$(src)/core/include diff --git a/drivers/spi/spi-mt-transport/kernel-compat/stdarg.h b/drivers/spi/spi-mt-transport/kernel-compat/stdarg.h new file mode 100644 index 0000000000000..494aa34430cbf --- /dev/null +++ b/drivers/spi/spi-mt-transport/kernel-compat/stdarg.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the portable core (../../../.. relative to here). + * + * The portable core targets hosted C11, so it includes the standard + * . Under the kernel's -nostdinc build this cross-compiler's own + * freestanding headers aren't on the search path either, so + * doesn't resolve at all -- linux/stdarg.h is the kernel's own sanctioned + * replacement for exactly this case. Only reached via this module's own + * Makefile (ccflags-y += -I$(src)/kernel-compat), so it + * cannot shadow anywhere else in the kernel tree. + */ +#include diff --git a/drivers/spi/spi-mt-transport/kernel-compat/stdbool.h b/drivers/spi/spi-mt-transport/kernel-compat/stdbool.h new file mode 100644 index 0000000000000..0ae939efdccfe --- /dev/null +++ b/drivers/spi/spi-mt-transport/kernel-compat/stdbool.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the portable core (../../../.. relative to here). + * + * The portable core targets hosted C11, so it includes the standard + * for bool/true/false. Under the kernel's -nostdinc build this + * cross-compiler's own freestanding headers aren't on the search path + * either, so doesn't resolve at all. linux/types.h (bool) and + * linux/stddef.h (true/false) are the kernel's equivalents. Only reached via + * this module's own Makefile (ccflags-y += -I$(src)/kernel-compat), + * so it cannot shadow anywhere else in the kernel tree. + */ +#include +#include diff --git a/drivers/spi/spi-mt-transport/kernel-compat/stdint.h b/drivers/spi/spi-mt-transport/kernel-compat/stdint.h new file mode 100644 index 0000000000000..8f8772dbd2414 --- /dev/null +++ b/drivers/spi/spi-mt-transport/kernel-compat/stdint.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the portable core (../../../.. relative to here). + * + * The portable core targets hosted C11, so it includes the standard + * for the fixed-width int types. Under the kernel's -nostdinc + * build this cross-compiler's own freestanding headers aren't on the search + * path either, so doesn't resolve at all. linux/types.h already + * provides int8_t/uint8_t/.../int64_t/uint64_t (via asm-generic/int-ll64.h) + * with identical signedness/width. Only reached via this module's own + * Makefile (ccflags-y += -I$(src)/kernel-compat), so it + * cannot shadow anywhere else in the kernel tree. + */ +#include diff --git a/drivers/spi/spi-mt-transport/kernel-compat/string.h b/drivers/spi/spi-mt-transport/kernel-compat/string.h new file mode 100644 index 0000000000000..75e46a5c7d80a --- /dev/null +++ b/drivers/spi/spi-mt-transport/kernel-compat/string.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the portable core (../../../.. relative to here). + * + * The portable core targets hosted C11 (its STM32/host-native builds use a + * real libc), so it includes the standard for memcpy/memset/ + * memcmp. The kernel build has no hosted libc and doesn't provide a bare + * -- linux/string.h is the kernel's equivalent, with compatible + * signatures for the functions the core actually uses. This shim lets + * resolve to it without hand-editing the portable core's .c files + * themselves. Only reached via this module's own Makefile + * (ccflags-y += -I$(src)/kernel-compat), so it cannot shadow + * anywhere else in the kernel tree. + */ +#include diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c new file mode 100644 index 0000000000000..a58dcc7efd485 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -0,0 +1,1018 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * spi_mt_transport_drv.c - Linux kernel Host-role driver for the MultiTracks + * SPI transport protocol. Builds in-tree (see drivers/spi/Kconfig's + * CONFIG_SPI_MT_TRANSPORT and this directory's Makefile) against the + * portable protocol core, which is NOT committed in this repo -- it's the + * firmware repo's firmware-common/spi-transport/{src,inc}/ single source + * of truth, staged into this directory's gitignored core/ subdirectory at + * Yocto build time (see imx8mmini-bb-evk's meta-mt-transport-evk + * linux-imx_%.bbappend's do_patch postfunc). + * + * Talks Host role to an STM32-class Client peer over a raw SPI bus plus a + * companion NRDY GPIO handshake line. See the firmware repo's + * firmware-common/spi-transport/docs/ProtocolSpec.md for the wire protocol + * and handshake state machine this ports into the kernel. + * + * Scope note (MT-158113): this is the driver only. The EVK-side test + * framework (MT-158682) is a separate ticket -- the userspace interface + * below (one misc device per raw channel + a small per-channel TX ring, see + * mt_transport_tx_service()) is still a placeholder ahead of a real config + * surface: single in-flight RX message per channel, no ioctl/config + * surface. Channel *count* is generic, though (MT-159369): every raw + * channel the core supports (SPI_TRANSPORT_CHANNELS_MAX - 1, currently 19) + * gets a misc device, registered unconditionally at probe() time, whether + * or not anything uses it yet. What a given channel number actually + * carries (Transport Services on 1, Property Model on 2, the legacy IPC + * tunnel on 3, future MIDI/debug/vendor channels on 4+) is a + * userspace-level fact only (firmware-common/mt_ipc_fbs/docs/ChannelMapping.md) + * -- this driver has no notion of channel purpose, and adding a new one + * never requires a kernel change here again. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "spi_transport/spi_transport.h" +#include "spi_transport/spi_transport_types.h" +#include "spi_transport_os_linux.h" +#include "spi_transport_hw_linux.h" + +#define DRIVER_NAME "spi-mt-transport" +#define MT_TRANSPORT_TX_QUEUE_DEPTH 10 + +/* Every raw channel the spi-transport core supports + * (SPI_TRANSPORT_CHANNELS_MAX) except channel 0 (the transport's own + * control/handshake channel, never userspace-visible) gets a misc device -- + * see this file's header. Channel 1 keeps the original single-channel + * driver's unprefixed misc device name ("mt_spi_transport") for backward + * compatibility with existing tools/scripts; every other channel is + * "mt_spi_transport_ch" (see MT_TRANSPORT_MISC_NAME_MAX). + */ +#define MT_TRANSPORT_NUM_CHANNELS (SPI_TRANSPORT_CHANNELS_MAX - 1u) +/* "mt_spi_transport_ch" (20) + up to 3 digits + NUL fits SPI_TRANSPORT_CHANNELS_MAX + * values up to 999 -- reassess if that constant ever grows past 3 digits. + */ +#define MT_TRANSPORT_MISC_NAME_MAX 24 + +struct mt_transport_tx_slot { + uint8_t buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + uint16_t len; +}; + +struct mt_transport_priv; + +/* Everything that used to be single-instance state directly in struct + * mt_transport_priv but is actually per-channel -- split out here so a + * second channel (Property Model, MT-148208) can be added without + * duplicating the whole driver. See struct mt_transport_priv's own comment + * for what stays link-level instead of moving here. + */ +struct mt_transport_channel { + struct mt_transport_priv *priv; /* back-pointer -- needed for the + * shared removed/refcount/htransport + * checks in the open/release/read/ + * write/poll functions below, which + * used to reach priv directly via + * container_of() on a single shared + * misc device. + */ + uint8_t channel_num; + char misc_name[MT_TRANSPORT_MISC_NAME_MAX]; /* backing storage for + * misc.name -- must + * outlive + * misc_register(), so + * can't be a probe()- + * local buffer. + */ + + /* Minimal placeholder userspace interface -- MT-158682 owns the real + * design. Single hardcoded channel per device, single in-flight RX + * message, blocking read()/write(), best-effort poll(). + */ + struct miscdevice misc; + wait_queue_head_t rx_wq; + spinlock_t rx_lock; + uint8_t rx_buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + uint16_t rx_len; + bool rx_valid; + + /* TX ring (see mt_transport_misc_write()/mt_transport_tx_service()). + * spiTransportSend() borrows pBuffer -- per + * spi_transport_channel.h's trSpiTransportChannelSlot comment, the + * core keeps using it until the whole message finishes sending, + * which can span multiple ticks -- so each slot needs + * driver-instance lifetime, not a write()-local stack array (the + * latter is a use-after-return once the syscall returns and its + * frame is torn down). + * + * A slot is only ever reused once a *later* spiTransportSend() call + * succeeds: the core enforces a single in-flight message per channel + * (txPending only clears when the previous message's last chunk is + * confirmed sent), so that later success is itself proof the + * previous slot is done -- not a timing guess. This lets write() + * enqueue and return immediately instead of blocking on a flat + * drain wait; tx_lock protects only the ring's head/tail/count + * bookkeeping (no sleeping calls under it). + */ + spinlock_t tx_lock; + struct mt_transport_tx_slot tx_slots[MT_TRANSPORT_TX_QUEUE_DEPTH]; + unsigned int tx_head; /* next slot index to submit */ + unsigned int tx_tail; /* next free slot index to fill */ + /* Slots written by write() but not yet handed to spiTransportSend() -- + * deliberately NOT counting the in-flight slot too (see + * mt_transport_tx_service()'s comment on why conflating the two was a + * real bug). The room check callers need is + * tx_queued_count + (tx_in_flight_idx >= 0 ? 1 : 0) < DEPTH. + */ + unsigned int tx_queued_count; + int tx_in_flight_idx; /* Set on every successful spiTransportSend(), + * but never cleared back to -1 -- the core's + * public API has no TX-completion signal to + * clear it on (tracked as MT-158925 item 1), + * so this permanently costs one slot of room + * in mt_transport_tx_room_locked() from the + * first successful send onward. Not a + * correctness bug (no data loss, no crash -- + * just DEPTH-1 usable slots instead of DEPTH), + * and not fixable here without the core API + * change MT-158925 requests; a driver-side + * guess (e.g. a timeout) would reintroduce + * the exact anti-pattern that ticket already + * flags as having cost ~200x throughput + * previously. Found by Copilot's PR #46 + * review. + */ + wait_queue_head_t tx_free_wq; + + atomic_t available; /* 1 = no fd currently open on this channel's + * device, 0 = one is -- enforces single-open + * semantics per channel, see + * mt_transport_misc_open(). This also closes + * the concurrent-reader race Copilot's review + * separately flagged in + * mt_transport_misc_read(): with at most one + * open fd per channel, there is only ever one + * reader on that channel. + */ +}; + +struct mt_transport_priv { + struct spi_device *spi; + struct device *dev; + + struct gpio_desc *nss_gpiod; + struct gpio_desc *nrdy_gpiod; + int nrdy_irq; + bool nrdy_irq_requested; /* only true once devm_request_threaded_irq() + * actually succeeded -- see mt_transport_remove(). + */ + + trSpiTransportOs os; + trSpiTransportHw hw; + struct mt_transport_os_ctx os_ctx; + struct mt_transport_hw_ctx hw_ctx; + thSpiTransport htransport; + + struct task_struct *tick_thread; + + /* One entry per raw channel (index 0 = channel 1, the original + * single-channel device's unchanged name/number, through index + * MT_TRANSPORT_NUM_CHANNELS - 1 = channel SPI_TRANSPORT_CHANNELS_MAX - 1) + * -- see MT_TRANSPORT_NUM_CHANNELS's comment and struct + * mt_transport_channel. + */ + struct mt_transport_channel channels[MT_TRANSPORT_NUM_CHANNELS]; + + /* Link-wide event counters -- mirrors the STM32 Client harness's + * [DBG] conn=/disc=/hdrCrc=/payCrc=/seq=/dmaFail=/dmaTo= naming + * (firmware-common/spi-transport/test/stm32-disco/app/, in the + * firmware repo) so a fault-injection run's peer-side verdict can + * actually be read off this Host, not just inferred from the absence + * of a crash. Before this, mt_transport_event_callback() only logged + * via dev_dbg(), invisible in dmesg without dynamic debug explicitly + * enabled -- confirmed live: zero log output across ~50 real + * connect/disconnect cycles and dozens of DMA-failure injections + * during hardware bring-up (MT-158113). atomic_t: incremented from + * the tick thread (mt_transport_event_callback(), single-threaded), + * read from arbitrary userspace context via sysfs. Link-level, not + * per-channel -- connect/disconnect/CRC/DMA events happen to the + * physical link, not to one channel of it. + */ + atomic_t evt_connected; + atomic_t evt_disconnected; + atomic_t evt_hdr_crc; + atomic_t evt_payload_crc; + atomic_t evt_seq_gap; + atomic_t evt_dma_failure; + atomic_t evt_dma_timeout; + + /* priv is kzalloc'd, not devm_kzalloc'd (see mt_transport_probe()): + * the embedded miscdevices can outlive the SPI device across an + * unbind if userspace still holds a channel's device open, so + * something other than devm has to own freeing this memory -- + * kref does, with one reference held by the driver instance itself + * (dropped in mt_transport_remove()) and one more per open fd, on + * either channel (mt_transport_misc_open()/_release()). Found by + * Copilot's PR #46 review. + */ + struct kref refcount; + bool removed; /* true once mt_transport_remove() has torn down the + * transport core/tick thread -- read()/write() must + * bail out with -ENODEV rather than touching + * now-invalid os/hw state via an fd that outlived + * unbind. + */ +}; + +/* Wakes the tick kthread -- shared by the SPI-completion path and the + * (optional) NRDY-IRQ path, both of which only ever need to say "something + * happened, re-run spiTransportTick() soon" rather than touch core state + * directly from interrupt context. + */ +static void mt_transport_tick_notify(void *pNotifyCtx) +{ + struct mt_transport_priv *priv = pNotifyCtx; + + priv->os.pTaskNotifyGive(priv->os.pContext); +} + +static void mt_transport_rx_callback(void *pContext, uint8_t channel, const uint8_t *pBuffer, + uint16_t length, uint8_t flags) +{ + struct mt_transport_channel *chan = pContext; + unsigned long irqflags; + + (void)flags; + /* pContext already identifies which channel this is -- each channel + * is registered with its own struct mt_transport_channel* as + * context (see mt_transport_probe()), so no channel-number branch + * is needed here. + */ + (void)channel; + if (length > sizeof(chan->rx_buf)) + length = sizeof(chan->rx_buf); + + spin_lock_irqsave(&chan->rx_lock, irqflags); + memcpy(chan->rx_buf, pBuffer, length); + chan->rx_len = length; + /* WRITE_ONCE() pairs with the unlocked READ_ONCE() reads of rx_valid + * in mt_transport_misc_poll() and the wait_event_interruptible() + * condition below -- rx_buf/rx_len are only ever touched under + * rx_lock, but rx_valid itself is also read lock-free in those two + * spots (both are the standard Linux poll_wait()/wait_event idiom, + * where the wait/wake primitives themselves provide the needed + * ordering -- this is about being explicit for readers/tooling like + * KCSAN, not fixing an actual race). + */ + WRITE_ONCE(chan->rx_valid, true); + spin_unlock_irqrestore(&chan->rx_lock, irqflags); + + wake_up_interruptible(&chan->rx_wq); +} + +static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) +{ + /* pContext is the same struct mt_transport_channel* passed to + * spiTransportRegisterChannel() for both callbacks on this channel + * (see mt_transport_probe()) -- events are link-level, not + * per-channel, so derive priv from chan rather than assuming + * pContext already IS priv. Passing chan straight through as + * priv here (both are the first-member-is-a-pointer shape) used to + * silently reinterpret chan's own fields as priv's, corrupting + * priv->dev into a garbage pointer -- confirmed on hardware as a + * NULL-deref Oops in __dev_printk() the first time this callback + * fired. + */ + struct mt_transport_channel *chan = pContext; + struct mt_transport_priv *priv = chan->priv; + + switch (eEvent) { + case eSpiTransportEventConnected: + atomic_inc(&priv->evt_connected); + dev_info(priv->dev, "link event: connected\n"); + break; + case eSpiTransportEventDisconnected: + atomic_inc(&priv->evt_disconnected); + dev_info(priv->dev, "link event: disconnected\n"); + break; + case eSpiTransportEventErrorHeaderCrc: + atomic_inc(&priv->evt_hdr_crc); + dev_warn(priv->dev, "link event: header CRC error\n"); + break; + case eSpiTransportEventErrorPayloadCrc: + atomic_inc(&priv->evt_payload_crc); + dev_warn(priv->dev, "link event: payload CRC error\n"); + break; + case eSpiTransportEventErrorSequenceGap: + atomic_inc(&priv->evt_seq_gap); + dev_warn(priv->dev, "link event: sequence gap\n"); + break; + case eSpiTransportEventErrorDmaFailure: + atomic_inc(&priv->evt_dma_failure); + dev_warn(priv->dev, "link event: DMA arm failure\n"); + break; + case eSpiTransportEventErrorDmaTimeout: + atomic_inc(&priv->evt_dma_timeout); + dev_warn(priv->dev, "link event: DMA timeout\n"); + break; + default: + dev_warn(priv->dev, "link event: unknown (%d)\n", (int)eEvent); + break; + } +} + +/* + * Submit the oldest queued TX slot (if any) via spiTransportSend(). A + * success return proves the *previous* in-flight slot (if any) is now + * done -- the core only accepts a new send once the last one's final + * chunk is confirmed -- so that previous slot is freed right here, not + * after a guessed timeout. Called once per channel per tick thread + * iteration; a Busy return just means retry next tick, no state changes. + * + * Gates on tx_queued_count, not "is anything occupied at all": an earlier + * version checked the combined queued+in-flight total, which let tx_head + * advance onto a slot write() had never actually filled whenever exactly + * one message was in flight and nothing new had been queued behind it -- + * tx_service() would then resend whatever stale bytes happened to be + * sitting in that slot (found via Copilot PR review). Tracking queued + * count separately from "is one slot in flight" makes "is there anything + * NEW to submit" the only thing this check needs to answer. + */ +static void mt_transport_tx_service(struct mt_transport_channel *chan) +{ + unsigned long flags; + unsigned int idx; + uint16_t len; + teSpiTransportError err; + + spin_lock_irqsave(&chan->tx_lock, flags); + if (chan->tx_queued_count == 0) { + spin_unlock_irqrestore(&chan->tx_lock, flags); + return; + } + idx = chan->tx_head; + len = chan->tx_slots[idx].len; + spin_unlock_irqrestore(&chan->tx_lock, flags); + + err = spiTransportSend(chan->priv->htransport, chan->channel_num, chan->tx_slots[idx].buf, + len, true); + if (err != eSpiTransportErrorNone) + return; + + spin_lock_irqsave(&chan->tx_lock, flags); + chan->tx_queued_count--; + chan->tx_in_flight_idx = idx; + chan->tx_head = (chan->tx_head + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; + spin_unlock_irqrestore(&chan->tx_lock, flags); + wake_up_interruptible(&chan->tx_free_wq); +} + +static int mt_transport_tick_thread_fn(void *data) +{ + struct mt_transport_priv *priv = data; + int i; + + while (!kthread_should_stop()) { + priv->os.pTaskNotifyWait(priv->os.pContext, 2); + spiTransportTick(priv->htransport); + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) + mt_transport_tx_service(&priv->channels[i]); + } + return 0; +} + +/* --- Minimal userspace interface (placeholder ahead of MT-158682) --- */ + +static void mt_transport_priv_release(struct kref *kref) +{ + struct mt_transport_priv *priv = container_of(kref, struct mt_transport_priv, refcount); + + kfree(priv); +} + +static int mt_transport_misc_open(struct inode *inode, struct file *filp) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; + + (void)inode; + + if (READ_ONCE(priv->removed)) + return -ENODEV; + + if (!atomic_dec_and_test(&chan->available)) { + atomic_inc(&chan->available); + return -EBUSY; + } + + kref_get(&priv->refcount); + return 0; +} + +static int mt_transport_misc_release(struct inode *inode, struct file *filp) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; + + (void)inode; + + atomic_inc(&chan->available); + kref_put(&priv->refcount, mt_transport_priv_release); + return 0; +} + +static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_t count, + loff_t *ppos) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; + uint8_t scratch[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + unsigned long irqflags; + uint16_t len; + int ret; + + (void)ppos; + + /* POSIX: a count of 0 must return 0 with no other effect -- must not + * block, and must not consume a pending message. + */ + if (count == 0) + return 0; + + if (READ_ONCE(priv->removed)) + return -ENODEV; + + /* The single-open check in mt_transport_misc_open() only prevents a + * second fd -- it does nothing to stop two threads issuing read() on + * the *same* fd concurrently, which POSIX permits. Both could + * observe rx_valid true (via the O_NONBLOCK check below, or both + * waking from the same wait_event_interruptible()) and race into the + * consume block; without rechecking under the lock immediately before + * consuming, the loser would copy the winner's already-cleared + * rx_buf/rx_len as if it were a fresh message. Looping back to + * wait/retry instead of falling through fixes this: only the thread + * that actually observes rx_valid true *under rx_lock* consumes it, + * and rx_valid is cleared in that same critical section. Found by + * Copilot's PR #46 review. + */ + for (;;) { + spin_lock_irqsave(&chan->rx_lock, irqflags); + if (chan->rx_valid) { + /* Snapshot into a local buffer under the lock, then + * copy_to_user() outside it -- copy_to_user() can + * fault/sleep, which is illegal while holding a + * spinlock. rx_valid is cleared here too (not after + * the copy) since it's rx_lock-protected state, same + * as rx_buf -- a failing copy_to_user (a broken + * caller's bad pointer) now consumes the buffered + * message rather than leaving it for retry, a minor, + * acceptable behavior change for this placeholder + * interface. + */ + len = chan->rx_len; + if (len > count) + len = count; + memcpy(scratch, chan->rx_buf, len); + WRITE_ONCE(chan->rx_valid, false); + spin_unlock_irqrestore(&chan->rx_lock, irqflags); + break; + } + spin_unlock_irqrestore(&chan->rx_lock, irqflags); + + if (filp->f_flags & O_NONBLOCK) + return -EAGAIN; + + /* removed is included in the wait condition (not just checked + * up front) so a reader already blocked here when + * mt_transport_remove() runs actually wakes up instead of + * sleeping forever -- remove() wakes rx_wq right after + * setting removed for exactly this reason. Found by + * Copilot's PR #46 review. + */ + ret = wait_event_interruptible(chan->rx_wq, + READ_ONCE(chan->rx_valid) || READ_ONCE(priv->removed)); + if (ret) + return ret; + if (READ_ONCE(priv->removed)) + return -ENODEV; + } + + if (copy_to_user(buf, scratch, len)) + return -EFAULT; + + return len; +} + +/* + * True if a new slot can be enqueued. Caller must already hold tx_lock -- + * occupied total is tx_queued_count (not-yet-submitted slots) plus one + * more if a slot is currently in flight (tx_in_flight_idx >= 0), since + * that slot is still reserved even though it doesn't count toward + * tx_queued_count. + */ +static inline bool mt_transport_tx_room_locked(struct mt_transport_channel *chan) +{ + unsigned int occupied = chan->tx_queued_count + (chan->tx_in_flight_idx >= 0 ? 1 : 0); + + return occupied < MT_TRANSPORT_TX_QUEUE_DEPTH; +} + +/* + * wait_event_interruptible()'s condition check only -- takes and releases + * tx_lock itself since it must be callable without already holding it. + * mt_transport_misc_write()'s own room check below calls + * mt_transport_tx_room_locked() directly instead (already holding the + * lock at that point) rather than this wrapper, and deliberately so: that + * check has to stay under the *same* lock acquisition that immediately + * follows (the enqueue), otherwise a second writer could take the + * now-free slot in the gap between checking and re-locking. + */ +static bool mt_transport_tx_has_room(struct mt_transport_channel *chan) +{ + unsigned long flags; + bool room; + + spin_lock_irqsave(&chan->tx_lock, flags); + room = mt_transport_tx_room_locked(chan); + spin_unlock_irqrestore(&chan->tx_lock, flags); + return room; +} + +static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf, size_t count, + loff_t *ppos) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; + uint8_t scratch[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + unsigned long flags; + unsigned int idx; + size_t len = count; + int ret; + + (void)ppos; + + /* POSIX: a count of 0 must return 0 with no other effect -- must not + * enqueue a 0-length transport message. + */ + if (count == 0) + return 0; + + if (READ_ONCE(priv->removed)) + return -ENODEV; + + if (len > sizeof(scratch)) + len = sizeof(scratch); + if (copy_from_user(scratch, buf, len)) + return -EFAULT; + + for (;;) { + spin_lock_irqsave(&chan->tx_lock, flags); + if (mt_transport_tx_room_locked(chan)) + break; + spin_unlock_irqrestore(&chan->tx_lock, flags); + + if (filp->f_flags & O_NONBLOCK) + return -EAGAIN; + /* removed is included in the wait condition for the same + * reason as mt_transport_misc_read()'s rx_wq wait -- a + * writer already blocked here on unbind must not sleep + * forever; remove() wakes tx_free_wq right after setting + * removed. Found by Copilot's PR #46 review. + */ + ret = wait_event_interruptible(chan->tx_free_wq, + mt_transport_tx_has_room(chan) || READ_ONCE(priv->removed)); + if (ret) + return ret; + if (READ_ONCE(priv->removed)) + return -ENODEV; + } + + idx = chan->tx_tail; + memcpy(chan->tx_slots[idx].buf, scratch, len); + chan->tx_slots[idx].len = len; + chan->tx_tail = (chan->tx_tail + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; + chan->tx_queued_count++; + spin_unlock_irqrestore(&chan->tx_lock, flags); + + /* Kick the tick thread so mt_transport_tx_service() attempts this + * send right away instead of waiting up to its ~2ms poll interval. + */ + priv->os.pTaskNotifyGive(priv->os.pContext); + + return len; +} + +static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; + __poll_t mask = 0; + + poll_wait(filp, &chan->rx_wq, wait); + /* Reported so an fd that outlives unbind can detect teardown via + * poll() instead of only finding out on its next read()/write() -- + * rx_wq is woken on removal (see mt_transport_remove()), so this + * check is reachable rather than only ever seen on a fresh poll(). + * Found by Copilot's PR #46 review. + */ + if (READ_ONCE(priv->removed)) + return EPOLLHUP | EPOLLERR; + if (READ_ONCE(chan->rx_valid)) + mask |= EPOLLIN | EPOLLRDNORM; + return mask; +} + +static const struct file_operations mt_transport_misc_fops = { + .owner = THIS_MODULE, + .open = mt_transport_misc_open, + .release = mt_transport_misc_release, + .read = mt_transport_misc_read, + .write = mt_transport_misc_write, + .poll = mt_transport_misc_poll, + /* This device doesn't support offsets -- no_llseek is the in-tree + * convention for that (e.g. drivers/spi/spidev.c), preventing + * unexpected seek behavior on the default llseek. Found by Copilot's + * PR #46 review. + */ + .llseek = no_llseek, +}; + +static ssize_t link_state_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct spi_device *spi = to_spi_device(dev); + struct mt_transport_priv *priv = spi_get_drvdata(spi); + const char *state; + + (void)attr; + switch (spiTransportGetLinkState(priv->htransport)) { + case eSpiTransportLinkConnected: + state = "connected"; + break; + case eSpiTransportLinkHandshaking: + state = "handshaking"; + break; + default: + state = "disconnected"; + break; + } + return sysfs_emit(buf, "%s\n", state); +} +static DEVICE_ATTR_RO(link_state); + +static ssize_t event_counters_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct spi_device *spi = to_spi_device(dev); + struct mt_transport_priv *priv = spi_get_drvdata(spi); + + (void)attr; + /* Field names match the STM32 Client harness's [DBG] line + * (conn=/disc=/hdrCrc=/payCrc=/seq=/dmaFail=/dmaTo=) so a + * fault-injection run's peer-side verdict can be read off this file + * directly against that harness's docs/TestPlan.md. + */ + return sysfs_emit(buf, "conn=%d disc=%d hdrCrc=%d payCrc=%d seq=%d dmaFail=%d dmaTo=%d\n", + atomic_read(&priv->evt_connected), atomic_read(&priv->evt_disconnected), + atomic_read(&priv->evt_hdr_crc), atomic_read(&priv->evt_payload_crc), + atomic_read(&priv->evt_seq_gap), atomic_read(&priv->evt_dma_failure), + atomic_read(&priv->evt_dma_timeout)); +} +static DEVICE_ATTR_RO(event_counters); + +static struct attribute *mt_transport_attrs[] = { + &dev_attr_link_state.attr, + &dev_attr_event_counters.attr, + NULL, +}; +ATTRIBUTE_GROUPS(mt_transport); + +static int mt_transport_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct mt_transport_priv *priv; + trSpiTransportConfig config; + int ret; + int i; + int started_misc = 0; + + /* Plain kzalloc, not devm_kzalloc: see the refcount/available/removed + * comment on struct mt_transport_priv -- this memory must be able to + * outlive the SPI device's unbind if userspace still holds a + * channel's device open. + */ + priv = kzalloc(sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + kref_init(&priv->refcount); + + priv->spi = spi; + priv->dev = dev; + spi_set_drvdata(spi, priv); + + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) { + struct mt_transport_channel *chan = &priv->channels[i]; + + chan->priv = priv; + chan->channel_num = (uint8_t)(i + 1); + if (chan->channel_num == 1) + strscpy(chan->misc_name, "mt_spi_transport", sizeof(chan->misc_name)); + else + scnprintf(chan->misc_name, sizeof(chan->misc_name), "mt_spi_transport_ch%u", + chan->channel_num); + atomic_set(&chan->available, 1); + init_waitqueue_head(&chan->rx_wq); + spin_lock_init(&chan->rx_lock); + spin_lock_init(&chan->tx_lock); + init_waitqueue_head(&chan->tx_free_wq); + chan->tx_in_flight_idx = -1; + } + + /* Custom "multitracks,nss"/"multitracks,nrdy" bindings, not the + * standard "cs-gpios" -- see spi_transport_hw_linux.c's file comment + * for why these must stay outside the SPI core's own chip-select + * handling. Vendor-prefixed per the DT binding's own + * multitracks,spi-transport.yaml (and the "multitracks" registry + * entry in vendor-prefixes.yaml) -- devm_gpiod_get()'s con_id here + * has the "-gpios" suffix implicitly appended to form the property + * name it looks up, so "multitracks,nss" resolves to the + * "multitracks,nss-gpios" DT property. Found by Copilot's PR #46 + * review. + */ + priv->nss_gpiod = devm_gpiod_get(dev, "multitracks,nss", GPIOD_OUT_HIGH); + if (IS_ERR(priv->nss_gpiod)) { + ret = PTR_ERR(priv->nss_gpiod); + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, ret, "failed to get multitracks,nss-gpios\n"); + } + + priv->nrdy_gpiod = devm_gpiod_get(dev, "multitracks,nrdy", GPIOD_IN); + if (IS_ERR(priv->nrdy_gpiod)) { + ret = PTR_ERR(priv->nrdy_gpiod); + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, ret, "failed to get multitracks,nrdy-gpios\n"); + } + + ret = mt_transport_os_linux_init(&priv->os_ctx, dev, &priv->os); + if (ret) { + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, ret, "mt_transport_os_linux_init failed\n"); + } + mt_transport_hw_linux_init(&priv->hw_ctx, spi, priv->nss_gpiod, priv->nrdy_gpiod, &priv->hw); + mt_transport_hw_linux_set_notify(&priv->hw_ctx, mt_transport_tick_notify, priv); + + config.role = eSpiTransportRoleHost; + config.prOs = &priv->os; + config.prHw = &priv->hw; + + if (spiTransportInit(&config, &priv->htransport) != eSpiTransportErrorNone) { + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); + } + + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) { + struct mt_transport_channel *chan = &priv->channels[i]; + + if (spiTransportRegisterChannel(priv->htransport, chan->channel_num, + mt_transport_rx_callback, mt_transport_event_callback, + chan) + != eSpiTransportErrorNone) { + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, -EINVAL, + "spiTransportRegisterChannel(%u) failed\n", + chan->channel_num); + } + } + + /* Optional latency optimization -- if the NRDY line has no usable + * IRQ, tick-driven pReadyRead() polling (nominally every 2ms, see the + * tick thread above -- actually whatever HZ rounds msecs_to_jiffies(2) + * up to, e.g. 4ms or 10ms depending on kernel config) is a fully + * sufficient fallback per the core's own contract, so a failure here + * is not fatal. + */ + priv->nrdy_irq = gpiod_to_irq(priv->nrdy_gpiod); + /* -EPROBE_DEFER means the IRQ chip backing this GPIO isn't ready yet, + * not "this GPIO has no IRQ" -- must propagate it so the kernel + * retries this whole probe() later, or the IRQ optimization gets + * silently and permanently disabled by a boot-time ordering race + * instead of the actual IRQ becoming available a bit later. + */ + if (priv->nrdy_irq == -EPROBE_DEFER) { + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, -EPROBE_DEFER, "NRDY IRQ not ready yet\n"); + } + /* >= 0, not > 0: IRQ 0 is a legally valid IRQ number on some + * platforms/irqdomains -- only negative values mean "no IRQ" per + * gpiod_to_irq()'s own contract. Treating 0 as "no IRQ" would skip + * the threaded handler for a GPIO that genuinely maps to IRQ 0. + */ + if (priv->nrdy_irq >= 0) { + ret = devm_request_threaded_irq(dev, priv->nrdy_irq, NULL, + mt_transport_hw_linux_nrdy_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING + | IRQF_ONESHOT, + DRIVER_NAME "-nrdy", &priv->hw_ctx); + if (ret) + dev_dbg(dev, "no NRDY IRQ (%d) -- falling back to tick-poll only\n", ret); + else + priv->nrdy_irq_requested = true; + } else { + dev_dbg(dev, "NRDY line has no IRQ -- tick-poll only\n"); + } + + priv->tick_thread = kthread_run(mt_transport_tick_thread_fn, priv, "%s-tick", DRIVER_NAME); + if (IS_ERR(priv->tick_thread)) { + ret = PTR_ERR(priv->tick_thread); + /* The NRDY IRQ may already be live at this point (it's + * requested earlier in this function) -- unlike devm_kzalloc, + * kref_put() below can free priv's memory (including the + * embedded hw_ctx the IRQ handler dereferences) synchronously, + * right now, not deferred to the same devm unwind pass that + * would otherwise free the IRQ first. Must free it explicitly + * before dropping the last reference, same as + * mt_transport_remove() does. Found by Copilot's PR #46 + * review (on the two later error paths below; this one has + * the identical bug since the IRQ is requested even earlier). + */ + if (priv->nrdy_irq_requested) + devm_free_irq(dev, priv->nrdy_irq, &priv->hw_ctx); + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, ret, "failed to start tick thread\n"); + } + + if (spiTransportStart(priv->htransport) != eSpiTransportErrorNone) { + /* Free the IRQ before stopping the tick thread -- see + * mt_transport_remove()'s comment: the threaded handler can + * still fire and call pOnReadyEvent() into a transport that's + * mid-teardown otherwise. Found by Copilot's PR #46 review + * (flagged on the misc_register() failure path below; this + * path has the identical ordering bug). + */ + if (priv->nrdy_irq_requested) + devm_free_irq(dev, priv->nrdy_irq, &priv->hw_ctx); + kthread_stop(priv->tick_thread); + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); + } + + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) { + struct mt_transport_channel *chan = &priv->channels[i]; + + chan->misc.minor = MISC_DYNAMIC_MINOR; + chan->misc.name = chan->misc_name; + chan->misc.fops = &mt_transport_misc_fops; + ret = misc_register(&chan->misc); + if (ret) { + int j; + + /* Free the IRQ first, matching mt_transport_remove()'s + * ordering -- the threaded handler can still fire and + * call pOnReadyEvent() into a transport that's + * mid-teardown otherwise. Found by Copilot's PR #46 + * review. + */ + if (priv->nrdy_irq_requested) + devm_free_irq(dev, priv->nrdy_irq, &priv->hw_ctx); + /* An earlier channel's misc_register() already + * succeeded, so userspace can already hold an fd on + * it by the time a later channel's registration fails + * here -- set removed and wake its wait queues first, + * same as mt_transport_remove(), or a read()/write() + * already blocked on that fd would sleep forever past + * the misc_deregister() below. Found by Copilot's PR + * #46 review. + */ + WRITE_ONCE(priv->removed, true); + for (j = 0; j < started_misc; j++) { + wake_up_interruptible_all(&priv->channels[j].rx_wq); + wake_up_interruptible_all(&priv->channels[j].tx_free_wq); + } + /* Roll back any channel's misc device already + * registered before this one failed -- otherwise a + * second channel's registration failure would leak + * the first channel's /dev node across this probe + * failure. + */ + for (j = 0; j < started_misc; j++) + misc_deregister(&priv->channels[j].misc); + kthread_stop(priv->tick_thread); + spiTransportStop(priv->htransport); + kref_put(&priv->refcount, mt_transport_priv_release); + return dev_err_probe(dev, ret, "misc_register(%s) failed\n", + chan->misc_name); + } + started_misc++; + } + + dev_info(dev, "MultiTracks SPI transport driver probed (Host role, %d channels)\n", + MT_TRANSPORT_NUM_CHANNELS); + return 0; +} + +static void mt_transport_remove(struct spi_device *spi) +{ + struct mt_transport_priv *priv = spi_get_drvdata(spi); + int i; + + /* Set first, before anything else is torn down: any read()/write() + * on an fd that outlived this unbind must bail out with -ENODEV + * instead of touching the os/hw state this function is about to + * stop -- e.g. write()'s priv->os.pTaskNotifyGive() call would + * otherwise reach into a tick thread/os_ctx that's mid-teardown or + * already gone. This is a fail-fast measure, not the memory-safety + * fix itself -- priv's memory itself stays valid for as long as any + * such fd remains open via the kref below (see struct + * mt_transport_priv's refcount/removed comment). Found by + * Copilot's PR #46 review. + */ + WRITE_ONCE(priv->removed, true); + /* Wake any reader/writer already blocked in wait_event_interruptible() + * on either channel's rx_wq/tx_free_wq -- both now include removed in + * their wait condition, but a waiter sleeping before this + * WRITE_ONCE() would otherwise never re-check it and could hang + * forever across unbind. Found by Copilot's PR #46 review. + */ + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) { + wake_up_interruptible_all(&priv->channels[i].rx_wq); + wake_up_interruptible_all(&priv->channels[i].tx_free_wq); + } + + /* Free the NRDY IRQ (if one was ever successfully requested) before + * anything else: it's devm-managed, so it would otherwise only get + * freed by the driver core *after* this function returns, leaving it + * live through misc_deregister()/kthread_stop()/spiTransportStop() + * below -- if the line toggles during that window (a real STM32 + * Client peer doesn't stop just because this side is unbinding), + * mt_transport_hw_linux_nrdy_irq() could fire and call + * priv->hw.pOnReadyEvent() into a core that's mid-teardown. + * devm_free_irq() blocks until any already-running (threaded) + * handler invocation finishes, so this also can't race an + * in-flight callback the way a bare disable_irq() wouldn't. + */ + if (priv->nrdy_irq_requested) + devm_free_irq(priv->dev, priv->nrdy_irq, &priv->hw_ctx); + + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) + misc_deregister(&priv->channels[i].misc); + /* kthread_stop() blocks until the tick thread's loop actually exits, + * guaranteeing no thread is still calling spiTransportTick()/ + * mt_transport_tx_service() by the time spiTransportStop() runs. + * The reverse order (stop-then-kthread_stop, the original ordering + * here) left a window where the still-running tick thread could call + * into the core after it was already torn down. + */ + kthread_stop(priv->tick_thread); + spiTransportStop(priv->htransport); + + /* Drops the driver instance's own reference (taken via kref_init() + * in probe()). If no fd is currently open on either channel, this is + * the last reference and priv is freed right here -- same timing as + * the devm-based lifetime this replaced. If userspace still holds + * either channel's device open, priv stays alive (kept by that fd's + * own reference from mt_transport_misc_open()) until it's closed. + */ + kref_put(&priv->refcount, mt_transport_priv_release); +} + +static const struct of_device_id mt_transport_of_match[] = { + { .compatible = "multitracks,spi-transport", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, mt_transport_of_match); + +static struct spi_driver mt_transport_driver = { + .driver = { + .name = DRIVER_NAME, + .of_match_table = mt_transport_of_match, + .dev_groups = mt_transport_groups, + }, + .probe = mt_transport_probe, + .remove = mt_transport_remove, +}; +module_spi_driver(mt_transport_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("MultiTracks.com, LLC."); +MODULE_DESCRIPTION("MultiTracks SPI transport protocol driver (Host role)"); diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c new file mode 100644 index 0000000000000..a18ce42ed7e46 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * spi_transport_hw_linux.c - Linux kernel HW-adapter for the MultiTracks SPI + * transport core, Host role only. See spi_transport_hw_linux.h. + */ + +#include +#include +#include +#include + +#include "spi_transport_hw_linux.h" + +/* + * Bound for mt_hw_abort()'s wait on an in-flight transfer's completion. + * spi_imx_dma_transfer()'s own internal timeout (spi_imx_calculate_timeout() + * in drivers/spi/spi-imx.c) is unconditionally >= 2000ms (a flat "+1 second, + * doubled" floor, regardless of this driver's small fixed frame size), and + * spi_imx_transfer_one() calls it exactly once with no internal retry -- + * confirmed by reading both. 3000ms gives that floor comfortable scheduling + * margin without the abort path itself becoming an unbounded stall. + */ +#define MT_HW_ABORT_TIMEOUT_MS 3000 + +/* + * NSS/NRDY are driven/read as plain manually-owned GPIOs, not the SPI + * subsystem's automatic per-message chip-select. The core holds NSS low + * continuously across a whole request->ack->clock->complete cycle (which may + * span more than one spi_async() call in the Client-initiated case), not + * just the duration of one transfer -- the SPI core's built-in cs-gpios + * handling only ever asserts CS for a single spi_message. Both GPIOs are + * requested via a driver-private "multitracks,nss-gpios"/ + * "multitracks,nrdy-gpios" devicetree binding (see spi_mt_transport_drv.c) + * rather than the standard "cs-gpios" + * property, specifically so the SPI core never learns about them and never + * tries to toggle them itself. Both are declared GPIO_ACTIVE_HIGH in the + * devicetree regardless of the physical wire's true active sense, so that + * gpiod_set_value_cansleep()'s logical value always equals the literal pin + * level -- matching this whole contract's "true = pin HIGH" convention + * exactly. + * + * All three accessors below use the _cansleep variants: none of these + * calls happen from atomic/IRQ context -- mt_hw_select_assert() and + * mt_hw_ready_read() run from the tick kthread, and + * mt_transport_hw_linux_nrdy_irq() is registered as a threaded IRQ (NULL + * primary handler), which by definition runs in a context where sleeping + * is allowed. Using the plain (non-cansleep) accessors would be unsafe if + * this GPIO ever ends up backed by a sleep-capable provider (e.g. an + * I2C/SPI GPIO expander) instead of the native SoC GPIO controller this + * board happens to use. + */ + +static void mt_hw_spi_complete(void *context) +{ + struct mt_transport_hw_ctx *ctx = context; + uint16_t length = ctx->msg.status == 0 ? ctx->xfer.len : 0; + + /* pOnTransferComplete() must run before transferComplete is signaled: + * mt_hw_abort() waits on this same completion, and it runs on the + * tick thread -- a different context than this SPI completion + * callback. Signaling first would let an aborting/woken tick thread + * proceed (and potentially call back into the core) while this + * context is still inside pOnTransferComplete() mutating core state, + * a concurrent unsynchronized access. Found by Copilot's PR #46 + * review. + */ + if (ctx->pHw->pOnTransferComplete) + ctx->pHw->pOnTransferComplete(ctx->pHw->pCoreCtx, length); + + /* Signal "msg/xfer no longer referenced by the SPI core, and the + * core has already been notified" only now -- pNotify may wake the + * tick thread straight into a new pTransferStart(), which gates on + * this same completion, so it's still correctly ordered after. + */ + complete(&ctx->transferComplete); + + if (ctx->pNotify) + ctx->pNotify(ctx->pNotifyCtx); +} + +static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *pTx, uint8_t *pRx, + uint16_t length) +{ + struct mt_transport_hw_ctx *ctx = pContext; + int ret; + + /* msg/xfer are shared across every transfer (see the struct comment) + * -- reinitializing them while the SPI core still has the previous + * submission queued/in-flight corrupts its internal message-queue and + * scatterlist state. mt_hw_abort() is supposed to guarantee this is + * clear before the core ever calls back in here again, so hitting + * this is itself a bug elsewhere; refuse rather than corrupt state. + */ + if (!completion_done(&ctx->transferComplete)) { + dev_err(&ctx->spi->dev, + "pTransferStart() called with a previous transfer still in flight -- refusing to reinitialize shared msg/xfer state\n"); + return eSpiTransportErrorHardwareFailure; + } + reinit_completion(&ctx->transferComplete); + + spi_message_init(&ctx->msg); + memset(&ctx->xfer, 0, sizeof(ctx->xfer)); + ctx->xfer.tx_buf = pTx; + ctx->xfer.rx_buf = pRx; + ctx->xfer.len = length; + spi_message_add_tail(&ctx->xfer, &ctx->msg); + ctx->msg.complete = mt_hw_spi_complete; + ctx->msg.context = ctx; + + ret = spi_async(ctx->spi, &ctx->msg); + if (ret) { + dev_dbg(&ctx->spi->dev, "spi_async failed: %d\n", ret); + /* No async completion will ever fire for this failed + * submission -- release the in-flight guard ourselves. + */ + complete(&ctx->transferComplete); + return eSpiTransportErrorHardwareFailure; + } + + return eSpiTransportErrorNone; +} + +/* Host only: drive NSS. No-op on Client, but this adapter only ever runs + * Host role, so unconditionally drive the line. + */ +static void mt_hw_select_assert(void *pContext, bool high) +{ + struct mt_transport_hw_ctx *ctx = pContext; + + gpiod_set_value_cansleep(ctx->nss_gpiod, high ? 1 : 0); +} + +/* Client only -- Host never calls this; left wired to a harmless stub so a + * stray call (there should never be one) doesn't crash rather than silently + * doing nothing unexpected. + */ +static void mt_hw_ready_assert(void *pContext, bool high) +{ + struct mt_transport_hw_ctx *ctx = pContext; + + (void)high; + dev_warn_once(&ctx->spi->dev, "pReadyAssert called on Host role adapter (unexpected)\n"); +} + +static bool mt_hw_ready_read(void *pContext) +{ + struct mt_transport_hw_ctx *ctx = pContext; + int val = gpiod_get_value_cansleep(ctx->nrdy_gpiod); + + /* A negative errno (GPIO provider failure) must not fall through the + * old bare ternary, which mapped any nonzero result -- errno included + * -- to true. Fail closed instead: report not-ready rather than + * risk clocking the peer on an invalid handshake. Found by Copilot's + * PR #46 review. + */ + if (val < 0) { + dev_err_ratelimited(&ctx->spi->dev, "NRDY GPIO read failed: %d\n", val); + return false; + } + + return val ? true : false; +} + +/* + * Force the transfer engine back to idle after a wedged pTransferStart(). + * Unlike STM32 HAL (which needs a manual RCC-level peripheral reset), the + * i.MX8MM's spi-imx controller driver already runs its own + * completion-timeout + dmaengine_terminate_all() + reset recovery internally + * on a stuck DMA transfer (drivers/spi/spi-imx.c transfer_one()). The Linux + * SPI core also has no public master-mode equivalent of HAL_SPI_Abort() -- + * spi_slave_abort() is slave-mode only. + * + * Confirmed live on the EVK (MT-158682): spi-imx's internal recovery is NOT + * sufficient on its own, because it isn't synchronous with this call. The + * core's own disconnect watchdog (SPI_TRANSPORT_DISCONNECT_MS, 1500ms) fires + * before spi_imx_calculate_timeout()'s unconditional >=2000ms floor can + * possibly have elapsed, so a log-only pAbort() let the retry that follows + * reinitialize msg/xfer (see mt_hw_transfer_start()) while spi_imx was still + * blocked inside its own wait_for_completion_timeout() referencing that same + * memory -- corrupting the SPI core's message queue/scatterlist state and + * crashing with a NULL deref in spi_imx_dma_transfer()'s sg_last(). This + * contract has no return value (must be safe to call whether or not + * anything is armed, and the core proceeds regardless of what happens here), + * so the only correct fix available is to actually block until spi-imx's own + * bounded recovery has had time to finish before returning. + */ +static void mt_hw_abort(void *pContext) +{ + struct mt_transport_hw_ctx *ctx = pContext; + + if (completion_done(&ctx->transferComplete)) + return; + + if (!wait_for_completion_timeout(&ctx->transferComplete, + msecs_to_jiffies(MT_HW_ABORT_TIMEOUT_MS))) { + dev_err(&ctx->spi->dev, + "pAbort(): transfer still in flight %ums after spi-imx's own DMA-timeout recovery should have finished -- proceeding anyway, next transfer may still race\n", + MT_HW_ABORT_TIMEOUT_MS); + } + + /* Restore the "idle, no transfer in flight" resting state for the + * next mt_hw_transfer_start(), whether we got here via a genuine + * completion or the timeout fallback above -- wait_for_completion_* + * consumes the completion on success, and the timeout path never + * signaled it in the first place. + */ + complete(&ctx->transferComplete); +} + +void mt_transport_hw_linux_init(struct mt_transport_hw_ctx *ctx, struct spi_device *spi, + struct gpio_desc *nss_gpiod, struct gpio_desc *nrdy_gpiod, + trSpiTransportHw *pHw) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->spi = spi; + ctx->nss_gpiod = nss_gpiod; + ctx->nrdy_gpiod = nrdy_gpiod; + + /* Starts "done" -- idle, no transfer in flight yet. */ + init_completion(&ctx->transferComplete); + complete(&ctx->transferComplete); + + ctx->pHw = pHw; + + memset(pHw, 0, sizeof(*pHw)); + pHw->pTransferStart = mt_hw_transfer_start; + pHw->pSelectAssert = mt_hw_select_assert; + pHw->pReadyAssert = mt_hw_ready_assert; + pHw->pReadyRead = mt_hw_ready_read; + pHw->pAbort = mt_hw_abort; + pHw->pContext = ctx; + + /* pOnSelectEvent/pOnClockStart deliberately left NULL -- Client-only + * concepts the core's Host code paths never invoke. pOnReadyEvent is + * wired later if an NRDY IRQ is available (see + * mt_transport_hw_linux_nrdy_irq()); polling pReadyRead() via the tick + * loop always works as the fallback. + */ +} + +void mt_transport_hw_linux_set_notify(struct mt_transport_hw_ctx *ctx, + void (*pNotify)(void *pNotifyCtx), void *pNotifyCtx) +{ + ctx->pNotify = pNotify; + ctx->pNotifyCtx = pNotifyCtx; +} + +irqreturn_t mt_transport_hw_linux_nrdy_irq(int irq, void *dev_id) +{ + struct mt_transport_hw_ctx *ctx = dev_id; + int val = gpiod_get_value_cansleep(ctx->nrdy_gpiod); + + /* Same errno-to-true bug as mt_hw_ready_read(), but here a + * misreported level would advance the Host state machine on a bad + * handshake -- skip the event entirely on error instead of guessing + * a level; the tick thread's own pReadyRead() polling remains + * available as a fallback. Found by Copilot's PR #46 review. + */ + if (val < 0) { + dev_err_ratelimited(&ctx->spi->dev, "NRDY GPIO read failed in IRQ handler: %d\n", + val); + return IRQ_HANDLED; + } + + if (ctx->pHw->pOnReadyEvent) + ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, val ? true : false); + + if (ctx->pNotify) + ctx->pNotify(ctx->pNotifyCtx); + + return IRQ_HANDLED; +} diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h new file mode 100644 index 0000000000000..e346b1dcba0b1 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * spi_transport_hw_linux.h - Linux kernel HW-adapter for the MultiTracks SPI + * transport core, Host role only (see + * ../../spi_transport_hw.h for the contract). + * + * Host role does not need, and must not wire, pReadyAssert (Client-only, + * drives NRDY), pOnSelectEvent (Client-only, watches for an edge on a pin + * only Host itself drives) or pOnClockStart (Client-only "clocking started" + * latch) -- see spi_mt_transport_drv.c's probe() for where this is asserted. + */ + +#ifndef SPI_TRANSPORT_HW_LINUX_H +#define SPI_TRANSPORT_HW_LINUX_H + +#include +#include +#include +#include + +#include "spi_transport/spi_transport_hw.h" + +struct mt_transport_hw_ctx { + struct spi_device *spi; + struct gpio_desc *nss_gpiod; /* Host-driven request/select line */ + struct gpio_desc *nrdy_gpiod; /* Host reads only; Client drives it */ + + /* Back-pointer to the trSpiTransportHw instance this ctx is + * pContext for -- spiTransportHwSetCallbacks() (called by the core + * during spiTransportInit()) fills in pOnTransferComplete/ + * pOnReadyEvent/pCoreCtx directly on *this* struct, not on ctx, so + * the completion/IRQ paths reach them through here. + */ + trSpiTransportHw *pHw; + + /* Reused across every transfer -- the core's Host state machine only + * ever has one transfer in flight at a time. That invariant is + * enforced (not just assumed) via transferComplete: "done" means no + * spi_async() is outstanding against msg/xfer, so it's safe to + * reinitialize them. Without this, mt_hw_abort() being a no-op could + * let a retry reinitialize msg/xfer while the SPI core still had the + * previous submission queued/in-flight, corrupting its internal + * message-queue and scatterlist state -- see the NULL-deref crash in + * spi_imx_dma_transfer()'s sg_last() this was written to fix. + */ + struct spi_message msg; + struct spi_transfer xfer; + struct completion transferComplete; + + /* Completion notify to wake the driver's tick kthread after a + * transfer completes -- set by spi_mt_transport_drv.c via + * mt_transport_hw_linux_set_notify(). + */ + void (*pNotify)(void *pNotifyCtx); + void *pNotifyCtx; +}; + +void mt_transport_hw_linux_init(struct mt_transport_hw_ctx *ctx, struct spi_device *spi, + struct gpio_desc *nss_gpiod, struct gpio_desc *nrdy_gpiod, + trSpiTransportHw *pHw); + +void mt_transport_hw_linux_set_notify(struct mt_transport_hw_ctx *ctx, + void (*pNotify)(void *pNotifyCtx), void *pNotifyCtx); + +/* NRDY GPIO IRQ handler (both-edges), wired by spi_mt_transport_drv.c's + * probe() if the chosen NRDY line has usable IRQ support. Latency + * optimization only -- tick-driven pReadyRead() polling always works too. + */ +irqreturn_t mt_transport_hw_linux_nrdy_irq(int irq, void *dev_id); + +#endif /* SPI_TRANSPORT_HW_LINUX_H */ diff --git a/drivers/spi/spi-mt-transport/spi_transport_os_linux.c b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c new file mode 100644 index 0000000000000..945874e3d0f48 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * spi_transport_os_linux.c - Linux kernel OS-adapter for the MultiTracks + * SPI transport core. See spi_transport_os_linux.h. + */ + +#include +#include +#include +#include +#include + +#include "spi_transport_os_linux.h" + +/* + * pTaskNotifyWait/pTaskNotifyGive: a single-waiter completion used as a + * repeating notify, not a one-shot. Deliberately never reinit_completion()'d: + * struct completion's own counter already handles repeated notify/wait + * cycles correctly on its own (a pTaskNotifyGive() that lands while not + * waiting just leaves the counter at 1, so the next wait returns + * immediately instead of blocking). An earlier version called + * reinit_completion() right after the wait, which reopened a race in the + * other direction -- a pTaskNotifyGive() landing in the (small but real) + * window between wait_for_completion_timeout() returning and + * reinit_completion() running would get silently discarded, delaying the + * tick thread until the next timeout. There is exactly one waiter (the + * driver's tick kthread), so no reinit is ever needed here. + */ +static void mt_os_task_notify_wait(void *pContext, uint32_t timeoutMs) +{ + struct mt_transport_os_ctx *ctx = pContext; + + wait_for_completion_timeout(&ctx->notify, msecs_to_jiffies(timeoutMs)); +} + +/* Must be IRQ-safe -- complete() is documented safe from interrupt context. */ +static void mt_os_task_notify_give(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + + complete(&ctx->notify); +} + +static uint32_t mt_os_tick_get(void *pContext) +{ + /* Truncating a monotonic ns count to u32 ms is fine: the core only + * ever compares ticks via wraparound-tolerant unsigned subtraction, + * same as the FreeRTOS/STM32 adapter's own 32-bit millis(). + */ + return (uint32_t)(ktime_get_ns() / NSEC_PER_MSEC); +} + +/* Registration-table lock -- never taken from interrupt context. */ +static void mt_os_mutex_lock(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + + mutex_lock(&ctx->reg_lock); +} + +static void mt_os_mutex_unlock(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + + mutex_unlock(&ctx->reg_lock); +} + +/* + * Guards the one Host check-then-claim race between the tick kthread's poll + * and the NRDY-IRQ path (see spi_transport_os.h). Must be IRQ-safe both + * directions, and the core promises this region is only ever a few field + * reads/writes -- never a call that could block. The core's own contract + * never nests these calls *on a single caller's own path*, but that does + * not mean crit_lock is never contended -- the tick kthread and the NRDY + * IRQ handler are two different execution contexts that can genuinely run + * on two different CPUs at once. spin_lock_irqsave()'s saved flags must + * therefore be per-CPU (ctx->crit_flags), not a single shared field: + * once the lock is held, preemption/local IRQs stay disabled on this CPU + * until the matching exit, so this_cpu_ptr() is stable across the whole + * enter/exit pair without needing get_cpu()/put_cpu(). + */ +static void mt_os_critical_enter(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + unsigned long flags; + + spin_lock_irqsave(&ctx->crit_lock, flags); + *this_cpu_ptr(ctx->crit_flags) = flags; +} + +static void mt_os_critical_exit(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + unsigned long flags = *this_cpu_ptr(ctx->crit_flags); + + spin_unlock_irqrestore(&ctx->crit_lock, flags); +} + +static void mt_os_log(void *pContext, const char *pFormat, va_list args) +{ + struct mt_transport_os_ctx *ctx = pContext; + va_list args_copy; + struct va_format vaf; + + /* On architectures where va_list is an array type, &args here would + * point at the local (already pointer-decayed) parameter rather than + * a real va_list object, breaking %pV's va_arg()-based consumption -- + * va_copy() into a genuinely local va_list is the portable way to + * get something &-able regardless of the platform's va_list + * representation. Found by Copilot's PR #46 review. + */ + va_copy(args_copy, args); + vaf.fmt = pFormat; + vaf.va = &args_copy; + + dev_dbg(ctx->dev, "%pV", &vaf); + + va_end(args_copy); +} + +int mt_transport_os_linux_init(struct mt_transport_os_ctx *ctx, struct device *dev, + trSpiTransportOs *pOs) +{ + ctx->dev = dev; + init_completion(&ctx->notify); + mutex_init(&ctx->reg_lock); + spin_lock_init(&ctx->crit_lock); + + ctx->crit_flags = devm_alloc_percpu(dev, unsigned long); + if (!ctx->crit_flags) + return -ENOMEM; + + pOs->pTaskNotifyWait = mt_os_task_notify_wait; + pOs->pTaskNotifyGive = mt_os_task_notify_give; + pOs->pTickGet = mt_os_tick_get; + pOs->pMutexLock = mt_os_mutex_lock; + pOs->pMutexUnlock = mt_os_mutex_unlock; + pOs->pCriticalEnter = mt_os_critical_enter; + pOs->pCriticalExit = mt_os_critical_exit; + pOs->pLog = mt_os_log; + pOs->pContext = ctx; + + return 0; +} diff --git a/drivers/spi/spi-mt-transport/spi_transport_os_linux.h b/drivers/spi/spi-mt-transport/spi_transport_os_linux.h new file mode 100644 index 0000000000000..9a1215241df4d --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * spi_transport_os_linux.h - Linux kernel OS-adapter for the MultiTracks + * SPI transport core (see ../../spi_transport_os.h for the contract this + * implements). + * + * Host-role only. Maps the core's OS-adapter contract onto kernel primitives: + * a struct completion for ISR-to-kthread handoff, a mutex for the core's + * registration-table critical section, and a spinlock for the one + * tick-poll-vs-IRQ race the core documents as needing IRQ-safe protection. + */ + +#ifndef SPI_TRANSPORT_OS_LINUX_H +#define SPI_TRANSPORT_OS_LINUX_H + +#include +#include +#include + +#include "spi_transport/spi_transport_os.h" + +struct device; + +struct mt_transport_os_ctx { + struct device *dev; + struct completion notify; + struct mutex reg_lock; + spinlock_t crit_lock; + /* Per-CPU, not a single shared field: spin_lock_irqsave()'s saved + * flags must be per-caller. crit_lock can be contended from two + * different CPUs at once (the tick kthread vs. the NRDY IRQ path -- + * see mt_os_critical_enter()'s comment), so a single ctx-wide field + * would let the losing CPU's spin overwrite the value the winning + * CPU needs to restore on unlock. + */ + unsigned long __percpu *crit_flags; +}; + +/* + * Initialize ctx and fill in *pOs with function pointers bound to ctx. + * ctx must outlive the transport instance (embed it in the driver's private + * struct). Allocates a devm-managed per-CPU flags slot (auto-freed on + * driver detach, same lifetime model as the driver's other devm_* state) -- + * returns 0 on success, -ENOMEM if that allocation fails. + */ +int mt_transport_os_linux_init(struct mt_transport_os_ctx *ctx, struct device *dev, + trSpiTransportOs *pOs); + +#endif /* SPI_TRANSPORT_OS_LINUX_H */