From b357ec67d630d8f2d28100a59e757610a90f395f Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Sun, 16 Aug 2026 16:16:20 -0600 Subject: [PATCH 01/35] MT-158113: add MultiTracks SPI transport driver (Host role) for imx8mm EVK Kernel port of the SPI transport protocol (firmware-common/spi-transport, vendored from MultiTracksDotCom/firmware @ 9923f343) as a Host-role SPI platform driver, for kernel<->kernel SPI transport testing between the iMX8MM-BB EVK and an STM32F723-DISCO client. Binds to ecspi2 via a new imx8mm-evk-spi-transport.dts overlay, with NSS/NRDY handshake lines owned directly by the driver (see dts comment) rather than SPI-core cs-gpios. --- arch/arm64/boot/dts/freescale/Makefile | 1 + .../freescale/imx8mm-evk-spi-transport.dts | 46 + arch/arm64/configs/imx_v8_defconfig | 1 + drivers/spi/Kconfig | 12 + drivers/spi/Makefile | 1 + drivers/spi/spi-mt-transport/Makefile | 13 + .../spi/spi-mt-transport/core/PROVENANCE.md | 48 + .../include/spi_transport/spi_transport.h | 213 +++ .../spi_transport/spi_transport_channel.h | 139 ++ .../spi_transport/spi_transport_frame.h | 77 + .../include/spi_transport/spi_transport_hw.h | 121 ++ .../include/spi_transport/spi_transport_os.h | 77 + .../spi_transport/spi_transport_types.h | 133 ++ .../core/kernel-compat/string.h | 15 + .../spi/spi-mt-transport/core/spi_transport.c | 1466 +++++++++++++++++ .../core/spi_transport_channel.c | 262 +++ .../core/spi_transport_crc16.c | 67 + .../core/spi_transport_frame.c | 151 ++ .../spi-mt-transport/core/spi_transport_hw.c | 31 + .../spi-mt-transport/spi_mt_transport_drv.c | 351 ++++ .../spi-mt-transport/spi_transport_hw_linux.c | 158 ++ .../spi-mt-transport/spi_transport_hw_linux.h | 62 + .../spi-mt-transport/spi_transport_os_linux.c | 109 ++ .../spi-mt-transport/spi_transport_os_linux.h | 40 + 24 files changed, 3594 insertions(+) create mode 100644 arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts create mode 100644 drivers/spi/spi-mt-transport/Makefile create mode 100644 drivers/spi/spi-mt-transport/core/PROVENANCE.md create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h create mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h create mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/string.h create mode 100644 drivers/spi/spi-mt-transport/core/spi_transport.c create mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_channel.c create mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_crc16.c create mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_frame.c create mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_hw.c create mode 100644 drivers/spi/spi-mt-transport/spi_mt_transport_drv.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.h create mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.h 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..95790e7a1dd8a --- /dev/null +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -0,0 +1,46 @@ +// 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 "mt-nss-gpios"/"mt-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() calls always see the +// literal pin level, matching the protocol's own "true = pin HIGH" +// convention (see spi_transport_hw.h in the vendored core). + +#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 = <500000>; /* matches mt-connect.dts; revisit once link timing is measured */ + + mt-nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + mt-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/configs/imx_v8_defconfig b/arch/arm64/configs/imx_v8_defconfig index 6e3754f5be47a..1a0933ecd39c8 100644 --- a/arch/arm64/configs/imx_v8_defconfig +++ b/arch/arm64/configs/imx_v8_defconfig @@ -454,6 +454,7 @@ CONFIG_SPI_FSL_DSPI=y CONFIG_SPI_PL022=y CONFIG_SPI_ROCKCHIP=y CONFIG_SPI_SPIDEV=y +CONFIG_SPI_MT_TRANSPORT=m CONFIG_SPI_SLAVE=y CONFIG_SPI_SLAVE_TIME=y CONFIG_SPI_SLAVE_SYSTEM_CONTROL=y diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 3ce0fd5df8e9c..c685bb5b05642 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1211,6 +1211,18 @@ 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)" + depends on ARCH_MXC || COMPILE_TEST + depends on GPIOLIB + 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..f01e36d13c69a 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -12,6 +12,7 @@ obj-$(CONFIG_SPI_MEM) += spi-mem.o obj-$(CONFIG_SPI_MUX) += spi-mux.o obj-$(CONFIG_SPI_SPIDEV) += spidev.o obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o +obj-$(CONFIG_SPI_MT_TRANSPORT) += spi-mt-transport/ # SPI master controller drivers (bus) obj-$(CONFIG_SPI_ALTERA) += spi-altera-platform.o diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile new file mode 100644 index 0000000000000..4279935b9f6f8 --- /dev/null +++ b/drivers/spi/spi-mt-transport/Makefile @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0 +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 + +ccflags-y += -I$(src)/core/kernel-compat -I$(src)/core/include diff --git a/drivers/spi/spi-mt-transport/core/PROVENANCE.md b/drivers/spi/spi-mt-transport/core/PROVENANCE.md new file mode 100644 index 0000000000000..93275ffa82400 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/PROVENANCE.md @@ -0,0 +1,48 @@ +# Provenance + +Vendored verbatim from `MultiTracksDotCom/firmware` @ +`9923f343acb6abbdc1a34e1f95803d313c52741c`, path +`firmware-common/spi-transport/{src,inc/spi_transport}`. + +Files vendored (portable core only — the `platform/host` and `platform/stm32` +adapter subdirectories from the source tree are NOT vendored; this port +provides its own `platform/linux-kernel`-equivalent adapter as sibling files +one directory up, see `spi_transport_os_linux.c`/`spi_transport_hw_linux.c`): + +- `spi_transport.c` +- `spi_transport_channel.c` +- `spi_transport_crc16.c` +- `spi_transport_frame.c` +- `spi_transport_hw.c` +- `include/spi_transport/spi_transport.h` +- `include/spi_transport/spi_transport_channel.h` +- `include/spi_transport/spi_transport_frame.h` +- `include/spi_transport/spi_transport_hw.h` +- `include/spi_transport/spi_transport_os.h` +- `include/spi_transport/spi_transport_types.h` + +Do not hand-edit these files. If a change is needed, make it in the firmware +repo's copy first, then re-vendor by re-copying and updating the commit SHA +above. + +The one exception: the vendored `.c` files' `#include ` (hosted +libc, used by their STM32/host-native builds) doesn't resolve under the +kernel's freestanding build -- ``/``/`` +already work directly (GCC provides these regardless of `-nostdinc`, and +other in-tree drivers in this repo already rely on that), but `` +is hosted-only. Rather than hand-edit the vendored files, `../Makefile` +adds `core/kernel-compat/` to the include path ahead of the vendored +headers -- it contains only a `string.h` shim that `#include +`. This is a build-time-only addition, not a change to any +vendored file; keep it that way on re-vendor. + +To check for drift against the firmware repo: + +``` +diff -rq linux-imx/drivers/spi/spi-mt-transport/core/ \ + <(cd firmware && git show 9923f343acb6abbdc1a34e1f95803d313c52741c:firmware-common/spi-transport) +``` + +(or, more practically, re-clone the firmware repo at the pinned SHA and diff +directory-to-directory, matching `src/*.c` -> `core/*.c` and +`inc/spi_transport/*.h` -> `core/include/spi_transport/*.h`.) diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h new file mode 100644 index 0000000000000..d2b7d8ca1e7ef --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h @@ -0,0 +1,213 @@ +//****************************************************************************** +// @file : spi_transport.h +// @brief : Public API for the SPI transport -- init/start/stop, per-channel +// registration, send, and link-wide connect/disconnect/error +// events. See docs/ProtocolSpec.md and docs/ChannelApi.md. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_H +#define SPI_TRANSPORT_H + +#include +#include + +#include "spi_transport/spi_transport_hw.h" +#include "spi_transport/spi_transport_os.h" +#include "spi_transport/spi_transport_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef void *thSpiTransport; + +typedef struct _trSpiTransportConfig +{ + teSpiTransportRole role; + const trSpiTransportOs *prOs; + trSpiTransportHw *prHw; /* non-const: spiTransportInit() wires its own + reverse callbacks into this same instance */ +} trSpiTransportConfig; + +/// @brief RX data callback: fired in task context (never from an ISR) when a +/// complete, reassembled message arrives on `channel`. `pBuffer` is +/// only valid for the duration of the callback. +typedef void (*tpSpiTransportRxCallback) (void *pContext, uint8_t channel, const uint8_t *pBuffer, + uint16_t length, uint8_t flags); + +/// @brief Link-wide event callback (connect/disconnect + transport errors, +/// see teSpiTransportEvent). Every registered channel receives the +/// same event sequence -- events are not per-channel. +typedef void (*tpSpiTransportEventCallback) (void *pContext, teSpiTransportEvent eEvent); + +/// @brief One-time global init. Not thread-safe against a concurrent second +/// call; call once at startup before spiTransportStart(). +teSpiTransportError spiTransportInit (const trSpiTransportConfig *prConfig, + thSpiTransport *phTransport); + +/// @brief Begin operation: Host starts issuing requests from IDLE; Client +/// starts waiting from IDLE. See docs/ProtocolSpec.md NSS/NRDY section. +teSpiTransportError spiTransportStart (thSpiTransport hTransport); + +/// @brief Stop operation. Safe to call at any point, including mid-transfer +/// (see the fault-injection "reset during transfer" mode in +/// docs/TestPlan.md) -- must leave no stuck DMA/GPIO state behind. +void spiTransportStop (thSpiTransport hTransport); + +/// @brief Register a channel for RX delivery and/or link events. Channel 0 is +/// transport-internal and always returns eSpiTransportErrorInvalidChannel. +/// pEventCallback may be NULL if this channel only cares about RX data. +teSpiTransportError spiTransportRegisterChannel (thSpiTransport hTransport, uint8_t channel, + tpSpiTransportRxCallback pRxCallback, + tpSpiTransportEventCallback pEventCallback, + void *pContext); + +teSpiTransportError spiTransportDeregisterChannel (thSpiTransport hTransport, uint8_t channel); + +/// @brief Queue `length` bytes for delivery on `channel`, segmenting across +/// multiple frames as needed (see SPI_TRANSPORT_FRAME_PAYLOAD_SIZE). +/// Channel 0 is transport-internal and always returns +/// eSpiTransportErrorInvalidChannel (same restriction as +/// spiTransportRegisterChannel()). Returns eSpiTransportErrorBusy if +/// the channel's previous message hasn't finished sending yet -- +/// there is no hidden queue depth beyond the single in-flight +/// message per channel. +teSpiTransportError spiTransportSend (thSpiTransport hTransport, uint8_t channel, + const uint8_t *pBuffer, uint16_t length, bool ackRequired); + +/// @brief Current link state, for polling use (most callers should prefer the +/// event callback instead). +teSpiTransportLinkState spiTransportGetLinkState (thSpiTransport hTransport); + +/// @brief Client only: enable/disable Client-initiated transfers (Client +/// arms its own DMA and drops NRDY while NSS is still high, to +/// solicit Host rather than waiting for Host's own request cycle -- +/// see docs/ProtocolSpec.md "Client-initiated transfers"). No-op on +/// Host. **Defaults to disabled** at spiTransportInit() -- today's +/// validated Host-only-initiates behavior is the standing default; +/// this must be explicitly opted into. Intended to be toggled at +/// runtime (e.g. a console command mirroring the existing +/// fault-injection commands) so a hardware regression can be backed +/// out instantly without a reflash, per the staged hardware rollout +/// plan in project memory. Safe to call at any time, including +/// mid-cycle -- takes effect on the next idle opportunity to +/// self-initiate; never interrupts an already-armed cycle. +void spiTransportSetClientSelfInitEnabled (thSpiTransport hTransport, bool enabled); + +/// @brief Current state of the flag set by +/// spiTransportSetClientSelfInitEnabled() -- for a console command's +/// own echo/status line, not required for the feature itself. +bool spiTransportIsClientSelfInitEnabled (thSpiTransport hTransport); + +/// @brief Drive the timer-based parts of the state machine (Host's +/// effectively-every-tick heartbeat/request-issue check, both +/// roles' 1.5s disconnect check). +/// The platform integration is responsible for calling this +/// periodically (STM32: from the transport task's own loop, e.g. +/// every 1-5ms; host-native tests: called directly by the test/loopback +/// driver). Safe to call more often than needed. +void spiTransportTick (thSpiTransport hTransport); + +/* Bring-up diagnostics -- temporary, see docs/TestPlan.md. Counts times the + * deferred-RX ring (see spi_transport.c) was still full when the ISR tried + * to write the next frame into it, i.e. the consuming task fell behind by + * more than SPI_TRANSPORT_RX_RING_DEPTH transfers and a frame was dropped. */ +uint32_t spiTransportDebugRxOverwriteCount (thSpiTransport hTransport); + +/// @brief Bring-up diagnostic -- temporary. pBuiltCount: how many times +/// buildOutgoingFrame() has run (every arm attempt, success or +/// failure). pAdvanceCount: how many times txSeq actually advanced +/// (onTransferComplete confirmed a physical transfer). pCurrentTxSeq: +/// the live txSeq value. Added to directly verify on real hardware +/// that the seq-peek/commit split holds -- built and advance should +/// only ever diverge by the count of abandoned/failed arm attempts +/// (the existing dmaFail counter), never more, and never less. +void spiTransportDebugTxSeqCounts (thSpiTransport hTransport, uint32_t *pBuiltCount, + uint32_t *pAdvanceCount, uint16_t *pCurrentTxSeq); + +/// @brief Bring-up diagnostic -- temporary. Client only: how many times +/// onSelectEvent()/clientArmSelfInitiateIfIdle() declined to ack or +/// self-arm specifically because the deferred-RX ring +/// (SPI_TRANSPORT_RX_RING_DEPTH) had no free slot -- real +/// backpressure (Host's existing ack-wait timeout retries later) +/// rather than accepting the transfer and dropping it at the ring. +/// Climbing steadily indicates sustained, not just bursty, load. +uint32_t spiTransportDebugRxRingFullRejectedCount (thSpiTransport hTransport); + +/// @brief Bring-up diagnostic -- temporary. Client only: how many real +/// NSS-falling edges onSelectEvent() has seen since boot, +/// split by outcome -- pRejectedCount (clientState wasn't Idle, so +/// the edge was ignored and NRDY was NOT touched) vs. pArmedCount +/// (clientState was Idle, pTransferStart() succeeded, and NRDY WAS +/// asserted low). Added for the DMA-wedge investigation, to +/// correlate a logic-analyzer-observed "NRDY never goes low" against +/// which path the core actually took. Either output param may be +/// NULL. Not per-instance (there's only ever one Client role active +/// per process) -- hTransport is accepted for API symmetry with the +/// other spiTransportDebugXxx calls but otherwise unused. +void spiTransportDebugClientArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, + uint32_t *pArmedCount); + +/// @brief Bring-up diagnostic -- temporary. Host-side mirror of +/// spiTransportDebugClientArmCounts(): does hostArmTransferIfAcked() +/// ever actually see hostState==eHostWaitingAck with NRDY low +/// (pArmedCount, proceeds to a real pTransferStart()) vs. everything +/// else (pRejectedCount -- wrong hostState, or NRDY still high). Not +/// per-instance; hTransport accepted for API symmetry only. +void spiTransportDebugHostArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, + uint32_t *pArmedCount); + +/// @brief Bring-up diagnostic -- temporary. Client-initiated-transfers +/// counters, landed alongside the feature itself (per the DMA-wedge +/// investigation's explicit lesson: visibility before the first +/// hardware test, not after). pAttemptCount: clientArmSelfInitiateIfIdle() +/// proceeded to a real pTransferStart(). pTimeoutCount: the self-arm +/// watchdog actually fired (pAbort() was called) -- climbing steadily +/// rather than rarely, especially alongside spiTransportDebugRxOverwriteCount()'s +/// sibling dmaCplt/[DIAG] counter going flat, is the direct +/// fingerprint of a stuck-peripheral regression. pHostArmedCount: +/// hostArmClientInitiatedIfIdle() succeeded (Host's side of the same +/// cycle). pRejectedSelfArmedCount: a split of +/// spiTransportDebugClientArmCounts()'s pRejectedCount, isolating +/// specifically "onSelectEvent rejected because clientState was +/// already eClientSelfArmed" (expected/healthy under self-init) from +/// everything else (still counted in that other call's +/// pRejectedCount, potentially a real problem). Any output param may +/// be NULL. Not per-instance; hTransport accepted for API symmetry +/// only. +void spiTransportDebugClientSelfInitCounts (thSpiTransport hTransport, uint32_t *pAttemptCount, + uint32_t *pTimeoutCount, uint32_t *pHostArmedCount, + uint32_t *pRejectedSelfArmedCount); + +/// @brief Bring-up diagnostic -- temporary. Details of the most recent +/// sequence-gap event and running totals by category: exact repeat +/// of the last-seen seq (pDuplicateCount), seq skipped forward +/// (pLossCount), or anything else, e.g. behind by more than one +/// (pOtherCount). Any output param may be NULL. +void spiTransportDebugLastGap (thSpiTransport hTransport, uint16_t *pExpected, uint16_t *pActual, + uint32_t *pDuplicateCount, uint32_t *pLossCount, + uint32_t *pOtherCount); + +/// @brief Bring-up diagnostic -- temporary. Our own epoch (generated once at +/// spiTransportInit()) and the peer's last-known epoch, to check +/// whether the epoch value is actually varying across reboots as +/// intended. Any output param may be NULL. +void spiTransportDebugEpoch (thSpiTransport hTransport, uint32_t *pOwnEpoch, uint32_t *pPeerEpoch, + bool *pPeerEpochKnown); + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h new file mode 100644 index 0000000000000..1698a26337e50 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h @@ -0,0 +1,139 @@ +//****************************************************************************** +// @file : spi_transport_channel.h +// @brief : Internal channel registration table, RX reassembly, and +// per-channel single-slot TX queue. Used by spi_transport.c +// only -- not part of the public API (see spi_transport.h). +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_CHANNEL_H +#define SPI_TRANSPORT_CHANNEL_H + +#include +#include + +#include "spi_transport/spi_transport.h" +#include "spi_transport/spi_transport_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef struct _trSpiTransportChannelTable trSpiTransportChannelTable; + +void spiTransportChannelTableInit (trSpiTransportChannelTable *prTable); + +teSpiTransportError spiTransportChannelRegister (trSpiTransportChannelTable *prTable, + uint8_t channel, + tpSpiTransportRxCallback pRxCallback, + tpSpiTransportEventCallback pEventCallback, + void *pContext); +teSpiTransportError spiTransportChannelDeregister (trSpiTransportChannelTable *prTable, + uint8_t channel); + +/// @brief Queue `length` bytes for `channel`. Buffer is borrowed (not +/// copied) -- caller must keep it valid until fully sent; returns +/// eSpiTransportErrorInvalidChannel for channel 0 (transport-internal, +/// see SPI_TRANSPORT_CHANNEL_CONTROL) or channel >= +/// SPI_TRANSPORT_CHANNELS_MAX, eSpiTransportErrorBusy if a previous +/// message on this channel hasn't finished, eSpiTransportErrorInvalidParam +/// if length exceeds SPI_TRANSPORT_CHANNEL_MESSAGE_MAX or pBuffer is +/// NULL with a nonzero length. +teSpiTransportError spiTransportChannelQueueTx (trSpiTransportChannelTable *prTable, + uint8_t channel, const uint8_t *pBuffer, + uint16_t length, bool ackRequired); + +/// @brief Round-robin pick of the next channel with TX data queued, starting +/// the search just after `startAfterChannel` (caller passes the last +/// channel serviced, or SPI_TRANSPORT_CHANNELS_MAX to start at 0). +/// On a hit, copies up to SPI_TRANSPORT_FRAME_PAYLOAD_SIZE bytes into +/// pOutPayload and sets *pOutChannel/*pOutFlags/*pOutLength. Returns +/// false if no channel has anything queued. +/// +/// Deliberately does NOT advance the channel's send offset or clear +/// txPending -- this only PEEKS the next chunk to build into a +/// frame. The caller must call spiTransportChannelCommitTx() once +/// the physical transfer carrying that chunk is confirmed to have +/// actually completed, not before. Committing at peek time (the +/// original design) meant any transfer that failed to arm or never +/// completed silently and permanently lost that chunk -- confirmed +/// live on hardware as a real, if hard to isolate, contributor to +/// the peer's sequence-gap counter (txSeq was already consumed for +/// a frame that never actually reached the peer). +bool spiTransportChannelNextTx (trSpiTransportChannelTable *prTable, uint8_t startAfterChannel, + uint8_t *pOutChannel, uint8_t *pOutPayload, uint16_t *pOutLength, + uint8_t *pOutFlags); + +/// @brief Confirm the chunk most recently returned by spiTransportChannelNextTx +/// for `channel` actually went out -- advances that channel's send +/// offset by `chunkLen` and, if `wasLastChunk`, clears txPending +/// (frees the single in-flight slot for a new spiTransportChannelQueueTx +/// call). Never call this for a transfer that failed to arm or never +/// completed -- simply not calling it is the correct "abandon this +/// attempt, retry the same unconsumed chunk next time" behavior. +void spiTransportChannelCommitTx (trSpiTransportChannelTable *prTable, uint8_t channel, + uint16_t chunkLen, bool wasLastChunk); + +/// @brief True if any channel has a TX message queued (used by the Host's +/// heartbeat check to decide whether it needs to issue a request +/// before the (effectively every-tick) backstop timer fires). +bool spiTransportChannelHasPending (const trSpiTransportChannelTable *prTable); + +/// @brief Reassemble an inbound chunk (per the START/END flags) and, once a +/// full message is complete, invoke the channel's RX callback in the +/// caller's context (task context -- never call from an ISR). +/// Silently drops+counts data for an unregistered channel. +void spiTransportChannelDispatchRx (trSpiTransportChannelTable *prTable, uint8_t channel, + const uint8_t *pPayload, uint16_t length, uint8_t flags); + +/// @brief Discard any in-progress RX reassembly and TX-in-flight state on +/// every channel -- called on every reconnect (epoch change), per +/// docs/ProtocolSpec.md's reconnect-baseline rule. +void spiTransportChannelResetAll (trSpiTransportChannelTable *prTable); + +/// @brief Notify every registered channel's event callback (may be NULL) of +/// a link-wide event. Not channel-specific -- every subscriber sees +/// the same sequence. +void spiTransportChannelNotifyEvent (trSpiTransportChannelTable *prTable, + teSpiTransportEvent eEvent); + +typedef struct _trSpiTransportChannelSlot +{ + bool registered; + tpSpiTransportRxCallback pRxCallback; + tpSpiTransportEventCallback pEventCallback; + void *pContext; + + /* TX (single in-flight message, borrowed buffer). */ + bool txPending; + const uint8_t *pTxBuffer; + uint16_t txLength; + uint16_t txOffset; + bool txAckRequired; + + /* RX reassembly (static buffer, no malloc). */ + bool rxInProgress; + uint8_t rxBuffer[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + uint16_t rxOffset; +} trSpiTransportChannelSlot; + +struct _trSpiTransportChannelTable +{ + trSpiTransportChannelSlot slots[SPI_TRANSPORT_CHANNELS_MAX]; +}; + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_CHANNEL_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h new file mode 100644 index 0000000000000..a0bb0ef059bf0 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h @@ -0,0 +1,77 @@ +//****************************************************************************** +// @file : spi_transport_frame.h +// @brief : Wire-frame encode/decode and CRC-16/CCITT-FALSE declarations. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_FRAME_H +#define SPI_TRANSPORT_FRAME_H + +#include +#include + +#include "spi_transport/spi_transport_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// @brief Decoded view of a frame -- never overlaid directly on the DMA +/// buffer (see docs/ProtocolSpec.md "Frame layout" for why: explicit +/// codec functions avoid padding/aliasing hazards across compilers). +typedef struct _trSpiTransportFrame +{ + uint16_t magic; + uint8_t version; + uint8_t channel; + uint16_t seq; + uint16_t ack; + uint8_t flags; + uint16_t payloadLength; + const uint8_t *pPayload; /* points into the caller-owned decode buffer, valid only + until the next spiTransportFrameDecode() call on that buffer */ +} trSpiTransportFrame; + +/// @brief CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect). +/// crc16Init() must be called once before any other CRC/frame call. +void crc16Init (void); +uint16_t crc16 (const uint8_t *pBuffer, uint16_t length); + +/// @brief Encode prFrame plus payload bytes [0, payloadLength) into pOutBuffer +/// (must be exactly SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes). Fills in +/// headerCrc/payloadCrc; prFrame->payloadLength bytes are copied from +/// pPayload, the rest of the payload region is zero-filled. +teSpiTransportError spiTransportFrameEncode (const trSpiTransportFrame *prFrame, + const uint8_t *pPayload, uint8_t *pOutBuffer); + +/// @brief Decode pInBuffer (exactly SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes) into +/// *prFrame. prFrame->pPayload is set to point inside pInBuffer. +/// Returns eSpiTransportErrorInvalidParam on a magic mismatch, +/// eSpiTransportErrorHardwareFailure on a header or payload CRC +/// mismatch (caller distinguishes via the two CRC-check functions +/// below if it needs to tell header-CRC apart from payload-CRC +/// failures for event reporting). +teSpiTransportError spiTransportFrameDecode (const uint8_t *pInBuffer, + trSpiTransportFrame *prFrame); + +/// @brief Standalone checks, used by spiTransportFrameDecode() internally and +/// exposed so callers (and unit tests) can distinguish which CRC +/// failed without re-decoding. +bool spiTransportFrameHeaderCrcOk (const uint8_t *pInBuffer); +bool spiTransportFramePayloadCrcOk (const uint8_t *pInBuffer); + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_FRAME_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h new file mode 100644 index 0000000000000..acfc78f2262aa --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h @@ -0,0 +1,121 @@ +//****************************************************************************** +// @file : spi_transport_hw.h +// @brief : Hardware-adapter contract the portable transport core depends +// on -- implemented once per platform (STM32 HAL SPI/DMA/GPIO, +// host-native loopback, future Linux kernel spi_sync()). +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_HW_H +#define SPI_TRANSPORT_HW_H + +#include +#include + +#include "spi_transport/spi_transport_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// @brief Hardware-adapter function pointers. See docs/ProtocolSpec.md +/// "NSS/NRDY handshake state machine" for exactly when the core calls +/// each of these and what each signal means for Host vs. Client. +typedef struct _trSpiTransportHw +{ + /// @brief Begin a full-duplex DMA transfer of exactly + /// SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes. Must return immediately; + /// completion is reported via the pOnTransferComplete callback + /// (see spiTransportHwSetCallbacks below). + /// On Client, this may now be called in two distinct contexts, + /// both of which the adapter must handle identically: after NSS + /// has already fallen (Host-initiated, the original contract -- + /// see pOnSelectEvent below) or *before* NSS falls, while + /// soliciting Host (Client-initiated -- see docs/ProtocolSpec.md + /// "Client-initiated transfers"). pAbort() below must be equally + /// effective recovering from either case -- this is the exact + /// contract an earlier, reverted attempt at Client-initiated + /// transfers got wrong (no bounded recovery existed for the + /// latter case at all, so a peer that never noticed/clocked the + /// solicitation left the peripheral stuck indefinitely). + teSpiTransportError (*pTransferStart) (void *pContext, const uint8_t *pTx, uint8_t *pRx, + uint16_t length); + + /// @brief Host only: drive the NSS line. Literal level convention used by + /// every function in this struct: true = pin driven/read HIGH, + /// false = LOW. NSS HIGH = idle, NSS LOW = request. No-op on + /// Client. + void (*pSelectAssert) (void *pContext, bool high); + + /// @brief Client only: drive the NRDY line. NRDY HIGH = idle/committed + /// (rest state), NRDY LOW = ack (see docs/ProtocolSpec.md + /// "NSS/NRDY handshake state machine"). No-op on Host. + void (*pReadyAssert) (void *pContext, bool high); + + /// @brief Host only: read the current NRDY input level (true = HIGH = + /// Client idle/available). Unused on Client. + bool (*pReadyRead) (void *pContext); + + /// @brief Force the underlying transfer engine back to idle/ready when + /// the core detects a pTransferStart() it armed never completed + /// (peer never clocked it, or vanished mid-transfer) -- see + /// doDisconnect() in spi_transport.c. Resetting the core's own + /// hostState/clientState bookkeeping isn't sufficient on real + /// hardware: a DMA transfer genuinely armed via + /// HAL_SPI_TransmitReceive_DMA and never clocked to completion + /// leaves the SPI peripheral itself latched busy, so every + /// subsequent real pTransferStart() call fails forever until this + /// is called. Must be safe to call even when nothing is armed. + void (*pAbort) (void *pContext); + + void *pContext; + + /* Core-side callbacks, filled in by spiTransportHwSetCallbacks() below. + * An adapter implementation invokes these (via its own held pointer to + * this same trSpiTransportHw instance) when the corresponding hardware + * event actually happens -- e.g. from a DMA-complete ISR, the NSS-EXTI + * ISR, or (in the null/loopback adapter) a direct synchronous call. */ + void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length); + void (*pOnSelectEvent) (void *pCoreCtx, bool asserted); + void (*pOnReadyEvent) (void *pCoreCtx, bool asserted); + void (*pOnClockStart) (void *pCoreCtx); + void *pCoreCtx; +} trSpiTransportHw; + +/// @brief Register the core's callbacks with a HW adapter instance. Called +/// once during spiTransportInit(), before spiTransportStart(). +/// @param pOnTransferComplete Fired when a pTransferStart() DMA transfer +/// finishes (success or hardware failure -- see length==0 convention +/// in the adapter implementation notes). +/// @param pOnSelectEvent Client only: fired on the NSS-EXTI edge (external to +/// HAL, per the board's requirement) -- asserted=true is the falling +/// edge (Host's request). +/// @param pOnReadyEvent Host only: fired on an NRDY level change, if the +/// adapter implements it via EXTI rather than pure polling. May be +/// left unused (NULL-checked by the caller) if the adapter only polls. +/// @param pOnClockStart Client only: fired the instant real clocking begins +/// (RX FIFO start indicator), distinct from and earlier than +/// pOnTransferComplete -- this is what the core uses to decide when +/// to re-assert NRDY high (the "committed, in-flight" latch), per +/// docs/ProtocolSpec.md. +void spiTransportHwSetCallbacks (trSpiTransportHw *prHw, + void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length), + void (*pOnSelectEvent) (void *pCoreCtx, bool asserted), + void (*pOnReadyEvent) (void *pCoreCtx, bool asserted), + void (*pOnClockStart) (void *pCoreCtx), void *pCoreCtx); + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_HW_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h new file mode 100644 index 0000000000000..fe5673ac56fba --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h @@ -0,0 +1,77 @@ +//****************************************************************************** +// @file : spi_transport_os.h +// @brief : OS-adapter contract the portable transport core depends on -- +// implemented once per platform (FreeRTOS, host-native stub, +// future Linux kernel). +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_OS_H +#define SPI_TRANSPORT_OS_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// @brief OS-adapter function pointers. The core never calls an RTOS/kernel +/// primitive directly -- every such call goes through this struct, so +/// the same core builds against FreeRTOS, a host-native stub, or (in +/// the future) Linux kernel primitives without core changes. +typedef struct _trSpiTransportOs +{ + /// @brief Block the calling task until the core's completion notification + /// is given (pTaskNotifyGive) or timeoutMs elapses. + void (*pTaskNotifyWait) (void *pContext, uint32_t timeoutMs); + + /// @brief Wake the task blocked in pTaskNotifyWait. Must be safe to call + /// from interrupt context. + void (*pTaskNotifyGive) (void *pContext); + + /// @brief Monotonic milliseconds since boot. + uint32_t (*pTickGet) (void *pContext); + + /// @brief Enter/leave the core's internal critical section (registration + /// table, per-channel TX slots). Not used from interrupt context. + void (*pMutexLock) (void *pContext); + void (*pMutexUnlock) (void *pContext); + + /// @brief Enter/leave a short task-vs-interrupt critical section -- + /// unlike pMutexLock/Unlock, this MUST be safe to call from (and + /// against) interrupt context, since it guards Host's + /// check-then-claim state transitions (hostArmTransferIfAcked, + /// hostIssueRequestIfReady) shared between spiTransportTick()'s + /// tick-driven poll and the NRDY-EXTI interrupt-driven path + /// (spiTransportHwStm32OnNrdyExti). Confirmed live on hardware: + /// without this, the two paths can race on the same + /// check-then-act, both proceed, and Host silently double-sends + /// (txSeq advances twice for one physical transfer), which the + /// Client sees as a spurious sequence-gap. Keep the guarded + /// region tiny -- a few field reads/writes only, never a HAL + /// DMA/GPIO call that could block. + void (*pCriticalEnter) (void *pContext); + void (*pCriticalExit) (void *pContext); + + /// @brief Optional diagnostic log sink. May be NULL. + void (*pLog) (void *pContext, const char *pFormat, va_list args); + + void *pContext; +} trSpiTransportOs; + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_OS_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h new file mode 100644 index 0000000000000..f47365a2ac1dc --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h @@ -0,0 +1,133 @@ +//****************************************************************************** +// @file : spi_transport_types.h +// @brief : Wire-format constants, flags, error codes and role/state enums +// for the SPI transport. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#ifndef SPI_TRANSPORT_TYPES_H +#define SPI_TRANSPORT_TYPES_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Frame layout (see docs/ProtocolSpec.md "Frame layout"): + * offset size field + * 0 2 magic (0x5AA5, little-endian) + * 2 1 version + * 3 1 channel + * 4 2 seq + * 6 2 ack + * 8 1 flags + * 9 1 reserved + * 10 2 payloadLength + * 12 2 headerCrc (CRC-16/CCITT-FALSE over bytes 0-11) + * 14 112 payload + * 126 2 payloadCrc (CRC-16/CCITT-FALSE over first payloadLength payload bytes) + */ +#define SPI_TRANSPORT_FRAME_MAGIC (0x5AA5u) +#define SPI_TRANSPORT_FRAME_VERSION (1u) + +#define SPI_TRANSPORT_FRAME_HEADER_SIZE (14u) +#define SPI_TRANSPORT_FRAME_PAYLOAD_SIZE (112u) +#define SPI_TRANSPORT_FRAME_CRC_SIZE (2u) +#define SPI_TRANSPORT_FRAME_TOTAL_SIZE \ + (SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE \ + + SPI_TRANSPORT_FRAME_CRC_SIZE) + +#define SPI_TRANSPORT_CHANNEL_CONTROL (0u) +#define SPI_TRANSPORT_CHANNELS_MAX (8u) + +/* Max reassembled message size per channel (static buffer, no malloc). A + * message larger than this cannot be sent/received -- spiTransportSend() + * returns eSpiTransportErrorInvalidParam and an oversized inbound + * (START-without-END-by-this-size) is dropped and counted, not delivered. */ +#define SPI_TRANSPORT_CHANNEL_MESSAGE_MAX (512u) + +/* Flags bitfield (offset 8). Bits 6-7 reserved, must be 0 on send. */ +#define SPI_TRANSPORT_FLAG_START (0x01u) +#define SPI_TRANSPORT_FLAG_END (0x02u) +#define SPI_TRANSPORT_FLAG_ACK_REQUIRED (0x04u) +#define SPI_TRANSPORT_FLAG_ERROR (0x08u) +#define SPI_TRANSPORT_FLAG_RESET (0x10u) +#define SPI_TRANSPORT_FLAG_FILLER (0x20u) + +/* Channel-0 control message types. */ +#define SPI_TRANSPORT_CTRL_HELLO (1u) +#define SPI_TRANSPORT_CTRL_HELLO_ACK (2u) + +/* Timing model (docs/ProtocolSpec.md "Connected/disconnected timing model"). + * Host re-issues a request itself, without waiting on this backstop, + * whenever it has anything queued -- this heartbeat only governs how long + * Host will go with NOTHING queued before issuing a request anyway (a + * FILLER if still nothing to send by then), so real traffic is never + * paced by this value. Set to effectively "every tick" (1ms) rather than + * 500ms: Host must not sit idle for any noticeable stretch, even with + * zero application traffic -- a fast, continuous heartbeat is cheap (a + * FILLER frame) and keeps the link's actual round-trip latency close to + * the physical transfer time instead of up to half a second. + * SPI_TRANSPORT_DISCONNECT_MS is intentionally NOT scaled down to match -- + * it's a coarse "peer is genuinely gone" bound, not a heartbeat multiple, + * and 1.5s remains the right tolerance for that regardless of how often + * Host pings within it. */ +#define SPI_TRANSPORT_HEARTBEAT_MS (1u) +#define SPI_TRANSPORT_DISCONNECT_MS (1500u) + +typedef enum +{ + eSpiTransportErrorNone = 0, + eSpiTransportErrorInvalidParam, + eSpiTransportErrorInvalidChannel, + eSpiTransportErrorAlreadyRegistered, + eSpiTransportErrorNotRegistered, + eSpiTransportErrorBusy, + eSpiTransportErrorNotConnected, + eSpiTransportErrorHardwareFailure, + eSpiTransportErrorTimeout, +} teSpiTransportError; + +typedef enum +{ + eSpiTransportRoleHost = 0, + eSpiTransportRoleClient = 1, +} teSpiTransportRole; + +typedef enum +{ + eSpiTransportLinkDisconnected = 0, + eSpiTransportLinkHandshaking, + eSpiTransportLinkConnected, +} teSpiTransportLinkState; + +/* Link-wide events, delivered to every registered channel's event callback + * (see spi_transport.h) -- connect/disconnect state changes and transport + * errors are visible to every subscriber, not just RX data. */ +typedef enum +{ + eSpiTransportEventConnected = 0, + eSpiTransportEventDisconnected, + eSpiTransportEventErrorHeaderCrc, + eSpiTransportEventErrorPayloadCrc, + eSpiTransportEventErrorSequenceGap, + eSpiTransportEventErrorDmaFailure, + eSpiTransportEventErrorDmaTimeout, +} teSpiTransportEvent; + +#ifdef __cplusplus +} +#endif + +#endif /* SPI_TRANSPORT_TYPES_H */ diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/string.h b/drivers/spi/spi-mt-transport/core/kernel-compat/string.h new file mode 100644 index 0000000000000..41b3a49d3f0f9 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/kernel-compat/string.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). + * + * The vendored 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 vendored .c files + * themselves. Only reached via this module's own Makefile + * (ccflags-y += -I$(src)/core/kernel-compat), so it cannot shadow + * anywhere else in the kernel tree. + */ +#include diff --git a/drivers/spi/spi-mt-transport/core/spi_transport.c b/drivers/spi/spi-mt-transport/core/spi_transport.c new file mode 100644 index 0000000000000..36a7cf05bcf34 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/spi_transport.c @@ -0,0 +1,1466 @@ +//****************************************************************************** +// @file : spi_transport.c +// @brief : Portable transport core -- Host/Client NSS/NRDY state machine, +// channel-0 handshake/reconnect-baseline protocol, ~1ms +// heartbeat / 1.5s disconnect timers. See docs/ProtocolSpec.md. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#include "spi_transport/spi_transport.h" + +#include + +#include "spi_transport/spi_transport_channel.h" +#include "spi_transport/spi_transport_frame.h" + +/* Internal watchdog: how long the Host waits for the Client's NRDY ack + * before giving up on this one request attempt. Not part of the wire + * protocol -- purely a local recovery bound, well under the 1.5s disconnect + * timer so a single wedged attempt doesn't have to wait that long to retry. */ +#define SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS (100u) + +/* Client-side mirror of the above, for self-initiated transfers (see + * eClientSelfArmed): how long Client will sit with its own DMA armed and + * NRDY low, NSS still high, before concluding Host isn't going to notice + * this solicitation. Must stay comfortably above + * SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS + one typical transfer (so a Host + * that's genuinely just running its own request cycle isn't mistaken for + * "not noticing"), and comfortably below SPI_TRANSPORT_DISCONNECT_MS (this + * is a per-attempt recovery, not a disconnect) -- see docs/ProtocolSpec.md + * "Client-initiated transfers" for the full reasoning. */ +#define SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS (250u) + +/* Depth of the deferred-RX ring (see trSpiTransportInstance's rxRingBuffer + * comment) -- how many completed-but-not-yet-task-processed frames can + * queue up before the ISR starts dropping new arrivals. */ +#define SPI_TRANSPORT_RX_RING_DEPTH (3u) + +/* Bring-up diagnostics -- temporary, see docs/TestPlan.md. Direct evidence + * for the DMA-wedge investigation: distinguishes onSelectEvent() rejecting a + * real NSS-falling edge because clientState wasn't Idle from it actually + * proceeding to assert NRDY low, so a logic-analyzer-observed "NRDY never + * goes low" can be correlated against which path the core actually took. */ +volatile uint32_t gDiagOnSelectRejectedCount = 0; +volatile uint32_t gDiagOnSelectArmedCount = 0; + +/* Mirrors the pair above, Host side: does hostArmTransferIfAcked() ever + * actually see hostState==eHostWaitingAck && NRDY low (pRejectedCount is + * everything else -- wrong hostState, or NRDY still high) and proceed to a + * real pTransferStart() attempt (pArmedCount)? A failed attempt is already + * visible via the existing dmaFail/[DBG] counter, so not duplicated here. */ +volatile uint32_t gDiagHostArmRejectedCount = 0; +volatile uint32_t gDiagHostArmArmedCount = 0; + +/* Client self-initiation diagnostics -- added alongside the feature itself + * (not retrofitted after a hardware failure like the pairs above), per the + * explicit lesson from the DMA-wedge investigation: land the visibility + * BEFORE the first hardware test, not after. gDiagClientSelfArmTimeoutCount + * climbing steadily (not just occasionally) alongside the existing + * gDiagDmaCompleteCount/dmaCplt counter going flat is the direct + * fingerprint of a stuck-peripheral regression -- visible within one + * [DIAG] print interval instead of requiring a fresh logic-analyzer + * capture to diagnose. */ +volatile uint32_t gDiagClientSelfArmAttemptCount = 0; /* clientArmSelfInitiateIfIdle attempted */ +volatile uint32_t gDiagClientSelfArmTimeoutCount = 0; /* watchdog actually fired (pAbort called) */ +volatile uint32_t gDiagHostClientInitArmedCount = 0; /* hostArmClientInitiatedIfIdle succeeded */ +volatile uint32_t gDiagOnSelectRejectedSelfArmedCount = 0; /* split of + gDiagOnSelectRejectedCount: specifically "rejected because clientState + was eClientSelfArmed" (expected/healthy under self-init) vs. anything + else (still counted in gDiagOnSelectRejectedCount, potentially a real + problem). */ + +/* TX-side seq-consumption diagnostics -- added to directly verify the + * buildOutgoingFrame()/onTransferComplete() seq-peek/commit split (see + * buildOutgoingFrame's own comment) actually holds on real hardware, not + * just in native tests: gDiagTxFrameBuiltCount increments once per + * buildOutgoingFrame() call (every arm attempt, success or failure); + * gDiagTxSeqAdvanceCount increments once per confirmed txSeq++ in + * onTransferComplete(). The two are expected to diverge by exactly the + * count of abandoned/failed arm attempts (dmaFail) -- built-advance should + * never exceed that, and advance should never exceed built. See + * spiTransportDebugTxSeqCounts(). */ +volatile uint32_t gDiagTxFrameBuiltCount = 0; +volatile uint32_t gDiagTxSeqAdvanceCount = 0; + +/* RX-ring backpressure diagnostic -- Client only. Counts how many times + * onSelectEvent()/clientArmSelfInitiateIfIdle() declined to ack/self-arm + * specifically because the deferred-RX ring had no free slot (see + * SPI_TRANSPORT_RX_RING_DEPTH), as opposed to being busy for some other + * reason. Climbing steadily means the ring is genuinely undersized for the + * sustained load, not just absorbing rare bursts -- see + * spiTransportDebugRxRingFullRejectedCount(). */ +volatile uint32_t gDiagRxRingFullRejectedCount = 0; + +/* Control-channel (channel 0) message body: {type, role, epoch(LE32), startSeq(LE16)}. */ +#define SPI_TRANSPORT_CTRL_BODY_SIZE (8u) + +typedef enum +{ + eHostIdle = 0, + eHostWaitingAck, + eHostTransferring, +} teHostState; + +typedef enum +{ + eClientIdle = 0, + /* Self-initiated: Client armed its own DMA and dropped NRDY while NSS is + * still HIGH, soliciting Host -- distinct from eClientArmed (which is + * always a reaction to Host's own NSS-falling edge). Bounded by + * SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS (see clientServiceTick()); + * onClockStart() moves this on to eClientTransferring exactly like + * eClientArmed, since once real clocking starts it no longer matters + * which side asked for it. */ + eClientSelfArmed, + eClientArmed, + eClientTransferring, +} teClientState; + +typedef struct _trSpiTransportInstance +{ + bool inUse; + teSpiTransportRole role; + const trSpiTransportOs *prOs; + trSpiTransportHw *prHw; + + trSpiTransportChannelTable channels; + + teSpiTransportLinkState linkState; + uint32_t epoch; + uint32_t peerEpoch; + bool peerEpochKnown; + bool helloSentThisEpoch; + bool pendingHello; + bool pendingHelloAck; + uint16_t startSeqFromPeer; + + uint16_t txSeq; + uint16_t rxLastSeq; + bool rxSeqBaselineArmed; + + uint8_t lastTxChannel; + + uint32_t lastSendTickMs; + uint32_t lastRecvTickMs; + + teHostState hostState; + uint32_t hostWaitStartMs; + + teClientState clientState; + uint32_t clientSelfArmStartMs; /* set when clientState becomes eClientSelfArmed */ + bool clientSelfInitEnabled; /* see spiTransportSetClientSelfInitEnabled() -- defaults false */ + + /* Deferred TX-commit bookkeeping: buildOutgoingFrame() only PEEKS a + * channel's next chunk (see spiTransportChannelNextTx()'s updated + * contract) -- these record which chunk was peeked into the + * currently-in-flight frame, so the transfer-complete confirmation + * (onTransferComplete) can commit it, or a failed/abandoned attempt can + * simply leave it uncommitted for a clean retry. */ + bool txCommitPending; + uint8_t txCommitChannel; + uint16_t txCommitChunkLen; + bool txCommitWasLastChunk; + + uint8_t txBuffer[SPI_TRANSPORT_FRAME_TOTAL_SIZE]; + uint8_t rxBuffer[SPI_TRANSPORT_FRAME_TOTAL_SIZE]; + + /* Deferred-processing handoff: the ISR-context transfer-complete handler + * (see onTransferComplete) only snapshots rxBuffer here and notifies the + * task -- it must NOT call processIncomingFrame() itself, since that + * invokes user RX/event callbacks, which docs/ProtocolSpec.md requires + * to run in task context only. spiTransportTick() drains this. + * + * Single-producer (ISR)/single-consumer (task) ring, depth + * SPI_TRANSPORT_RX_RING_DEPTH: rxRingHead is written only by the + * producer, rxRingTail only by the consumer, so each side can read the + * other's counter without a lock. Index into rxRingBuffer is + * (counter % SPI_TRANSPORT_RX_RING_DEPTH); both counters are + * free-running (never reduced mod the depth themselves), so unsigned + * wraparound subtraction (head - tail) always gives the correct + * occupied-slot count. Replaces an earlier single-slot design (a + * depth-2 ring was tried once before to close this same starved-task + * window, reordered frames on real hardware for a reason never + * isolated, and was reverted back to the single slot rather than ship + * that). This version avoids the most likely cause of that class of + * bug by construction: on a full ring, the ISR drops the *new* arrival + * (see the ring-full branch in onTransferComplete) rather than + * overwriting the oldest slot in place, which would race the task + * mid-read of that exact slot -- the ISR can preempt the task at any + * point, so slot ownership must never be ambiguous. See + * test/host/test_rx_ring.c for the back-to-back-completion/FIFO-order + * coverage this earlier attempt apparently didn't have. */ + uint8_t rxRingBuffer[SPI_TRANSPORT_RX_RING_DEPTH][SPI_TRANSPORT_FRAME_TOTAL_SIZE]; + volatile uint32_t rxRingHead; + volatile uint32_t rxRingTail; + + /* Bring-up diagnostic -- temporary, see spiTransportDebugRxOverwriteCount(). + * Now counts "ring was full, newest arrival dropped" rather than + * "existing slot overwritten", but is exactly as rare/meaningful a + * backpressure signal as before. */ + volatile uint32_t rxOverwriteCount; + + /* Bring-up diagnostic -- temporary, see spiTransportDebugLastGap(). Only + * ever written from task context (processIncomingFrame), so no + * volatile/critical-section needed here. */ + uint16_t lastGapExpected; + uint16_t lastGapActual; + uint32_t gapDuplicateCount; /* frame.seq == rxLastSeq (exact repeat) */ + uint32_t gapLossCount; /* frame.seq ahead of expected (skipped) */ + uint32_t gapOtherCount; /* anything else (behind by >1, wrap, etc.) */ +} trSpiTransportInstance; + +/* Static pool, no malloc anywhere (see docs/ProtocolSpec.md "Channel + * model"). Real firmware only ever uses one slot (one physical link per + * board); a size of 2 is what lets host-native tests run a Host instance + * and a Client instance simultaneously in the same process against the + * null-loopback HW adapter (see platform/host/spi_transport_hw_null.c). */ +#define SPI_TRANSPORT_INSTANCES_MAX (2u) +static trSpiTransportInstance gInstances[SPI_TRANSPORT_INSTANCES_MAX]; + +static void onTransferComplete (void *pCoreCtx, uint16_t length); +static void onSelectEvent (void *pCoreCtx, bool asserted); +static void onReadyEvent (void *pCoreCtx, bool asserted); +static void onClockStart (void *pCoreCtx); + +static void +putU16 (uint8_t *pOut, uint16_t value) +{ + pOut[0] = (uint8_t)(value & 0xFFu); + pOut[1] = (uint8_t)((value >> 8) & 0xFFu); +} +static uint16_t +getU16 (const uint8_t *pIn) +{ + return (uint16_t)((uint16_t)pIn[0] | ((uint16_t)pIn[1] << 8)); +} +static void +putU32 (uint8_t *pOut, uint32_t value) +{ + pOut[0] = (uint8_t)(value & 0xFFu); + pOut[1] = (uint8_t)((value >> 8) & 0xFFu); + pOut[2] = (uint8_t)((value >> 16) & 0xFFu); + pOut[3] = (uint8_t)((value >> 24) & 0xFFu); +} +static uint32_t +getU32 (const uint8_t *pIn) +{ + return (uint32_t)pIn[0] | ((uint32_t)pIn[1] << 8) | ((uint32_t)pIn[2] << 16) + | ((uint32_t)pIn[3] << 24); +} + +static void +updateLinkState (trSpiTransportInstance *pInst) +{ + bool shouldBeConnected = pInst->peerEpochKnown && pInst->helloSentThisEpoch; + + if (shouldBeConnected && (pInst->linkState != eSpiTransportLinkConnected)) + { + pInst->linkState = eSpiTransportLinkConnected; + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventConnected); + } +} + +static void +resetForHandshake (trSpiTransportInstance *pInst) +{ + pInst->linkState = eSpiTransportLinkHandshaking; + pInst->helloSentThisEpoch = false; + pInst->pendingHello = true; + pInst->rxSeqBaselineArmed = false; + spiTransportChannelResetAll (&pInst->channels); + + /* Any commit still deferred against the (now-wiped) channel table would + * be meaningless -- drop it rather than risk a later onTransferComplete + * committing stale channel/offset values into a freshly reset slot. */ + pInst->txCommitPending = false; +} + +static void +handleHandshake (trSpiTransportInstance *pInst, uint8_t msgType, uint32_t peerEpoch, + uint16_t peerStartSeq) +{ + bool epochChanged = (!pInst->peerEpochKnown) || (peerEpoch != pInst->peerEpoch); + + /* A peer only ever sends a fresh HELLO (as opposed to a HELLO_ACK) + * right after its own Start()/reboot or its own detected disconnect -- + * see buildOutgoingFrame's pendingHello handling, which is never + * re-armed spontaneously mid-connection. So an incoming HELLO while we + * still think we're Connected is itself sufficient proof the peer + * restarted, and must be treated as a reconnect even when epochChanged + * is false. Confirmed live on hardware that epoch alone is NOT reliable + * enough on its own to catch this: epoch is a boot-time RTOS tick + * snapshot taken in spiTransportInit(), and on this harness's Client + * role that call consistently lands before the scheduler's first tick + * -- every single Client reboot produces epoch=0, a guaranteed + * collision with whatever Host already had stored, not a rare + * probabilistic one. Without this OR clause, Host silently never + * re-baselines against a rebooted Client: no Disconnected event, no + * rxSeqBaselineArmed reset (each such miss produces one real spurious + * sequence-gap event where the peer's seq legitimately restarted from + * ~0), and recovery depends entirely on the far coarser, timing- + * dependent 1.5s silence timeout instead. */ + bool peerRestarted = epochChanged + || ((msgType == SPI_TRANSPORT_CTRL_HELLO) + && (pInst->linkState == eSpiTransportLinkConnected)); + + if (peerRestarted) + { + bool wasConnected = (pInst->linkState == eSpiTransportLinkConnected); + + pInst->peerEpoch = peerEpoch; + pInst->peerEpochKnown = true; + pInst->startSeqFromPeer = peerStartSeq; + resetForHandshake (pInst); + + if (wasConnected) + { + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventDisconnected); + } + } + + if (msgType == SPI_TRANSPORT_CTRL_HELLO) + { + pInst->pendingHelloAck = true; + } + + updateLinkState (pInst); +} + +static void +doDisconnect (trSpiTransportInstance *pInst, uint32_t now) +{ + bool wasConnected = (pInst->linkState == eSpiTransportLinkConnected); + + pInst->peerEpochKnown = false; + pInst->epoch = pInst->prOs->pTickGet (pInst->prOs->pContext); + resetForHandshake (pInst); + pInst->lastRecvTickMs = now; + + /* Un-wedge the NSS/NRDY handshake, not just the protocol state above: + * confirmed live on hardware that if the peer vanishes (e.g. reboots) + * mid-transfer, the surviving side's hostState/clientState is left + * stuck in a Waiting/Armed/Transferring state forever -- its DMA is + * waiting on clock edges or an ack that will never arrive, and nothing + * else ever moves it back to Idle. That left NRDY (Client) or NSS + * (Host) permanently latched, wedging the physical link even though + * the 1.5s disconnect timer above correctly recovered the protocol + * layer. Force both back to Idle and release the line here as the + * disconnect timer's backstop -- Host also has its own tighter 100ms + * per-attempt watchdog (see hostServiceTick), this is what actually + * covers Client, which has no per-attempt watchdog of its own. */ + if (pInst->role == eSpiTransportRoleHost) + { + /* Only eHostTransferring means a physical transfer was actually + * armed and never confirmed complete -- eHostWaitingAck gets + * its own 100ms watchdog well before this 1.5s path could ever + * see it stuck. That distinction is what makes this a DMA + * timeout specifically, not just "the link is down" (which + * eSpiTransportEventDisconnected below already covers). */ + bool wasArmed = (pInst->hostState == eHostTransferring); + if (wasArmed) + { + /* Abort the real transfer BEFORE flipping hostState back + * to Idle below -- hostIssueRequestIfReady/ + * hostArmTransferIfAcked gate a new real + * pTransferStart() on hostState, so as long as it still + * reads non-Idle here, a concurrent tick/EXTI can't slip + * a new attempt in against a peripheral that's still + * mid-abort. Confirmed live on hardware that resetting + * hostState first (the original order) opens exactly + * that window -- the SPI peripheral's own HAL state + * (HAL_SPI_STATE_BUSY_TX_RX) is what's actually latched, + * not just this instance's bookkeeping, so a transfer + * armed on top of an in-progress abort just fails again + * with HAL_BUSY forever. */ + pInst->prHw->pAbort (pInst->prHw->pContext); + } + if (pInst->hostState != eHostIdle) + { + pInst->hostState = eHostIdle; + pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); + } + if (wasArmed) + { + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorDmaTimeout); + } + } + else + { + bool wasArmed = (pInst->clientState != eClientIdle); + if (wasArmed) + { + /* See the matching Host-side comment above -- same + * ordering requirement: onSelectEvent's own guard + * (clientState != eClientIdle) is what keeps a real + * NSS-falling edge from re-arming the Client's slave DMA + * while this abort is still in flight, so it must run + * before clientState is reset below. */ + pInst->prHw->pAbort (pInst->prHw->pContext); + } + if (pInst->clientState != eClientIdle) + { + pInst->clientState = eClientIdle; + pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); + } + if (wasArmed) + { + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorDmaTimeout); + } + } + + if (wasConnected) + { + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventDisconnected); + } +} + +static uint16_t +buildControlBody (uint8_t *pOut, uint8_t type, teSpiTransportRole role, uint32_t epoch, + uint16_t startSeq) +{ + pOut[0] = type; + pOut[1] = (uint8_t)role; + putU32 (&pOut[2], epoch); + putU16 (&pOut[6], startSeq); + return SPI_TRANSPORT_CTRL_BODY_SIZE; +} + +/// @brief Choose and encode the next outbound frame: pending HELLO/HELLO_ACK +/// first, else a channel with queued data (round-robin), else a +/// FILLER -- see docs/ProtocolSpec.md "Channel-0 handshake" and +/// "Connected/disconnected timing model". +static void +buildOutgoingFrame (trSpiTransportInstance *pInst, uint8_t *pOutBuffer) +{ + uint8_t payload[SPI_TRANSPORT_FRAME_PAYLOAD_SIZE]; + uint8_t channel = SPI_TRANSPORT_CHANNEL_CONTROL; + uint8_t flags = 0; + uint16_t length = 0; + + gDiagTxFrameBuiltCount++; + pInst->txCommitPending = false; + + if (pInst->pendingHello) + { + length = buildControlBody (payload, SPI_TRANSPORT_CTRL_HELLO, pInst->role, pInst->epoch, + pInst->txSeq); + flags = SPI_TRANSPORT_FLAG_START | SPI_TRANSPORT_FLAG_END | SPI_TRANSPORT_FLAG_RESET; + pInst->pendingHello = false; + pInst->helloSentThisEpoch = true; + } + else if (pInst->pendingHelloAck) + { + length = buildControlBody (payload, SPI_TRANSPORT_CTRL_HELLO_ACK, pInst->role, + pInst->epoch, pInst->txSeq); + flags = SPI_TRANSPORT_FLAG_START | SPI_TRANSPORT_FLAG_END; + if (pInst->linkState != eSpiTransportLinkConnected) + { + flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_RESET); + } + pInst->pendingHelloAck = false; + } + else + { + uint8_t dataChannel; + uint16_t dataLength; + uint8_t dataFlags; + + if (spiTransportChannelNextTx (&pInst->channels, pInst->lastTxChannel, &dataChannel, + payload, &dataLength, &dataFlags)) + { + channel = dataChannel; + length = dataLength; + flags = dataFlags; + pInst->lastTxChannel = dataChannel; + + /* Peeked only -- not committed until the physical + * transfer carrying this frame is confirmed complete + * (see onTransferComplete). Left uncommitted, an + * abandoned/failed attempt naturally retries this exact + * chunk next time, since spiTransportChannelNextTx() + * didn't advance anything either. */ + pInst->txCommitPending = true; + pInst->txCommitChannel = dataChannel; + pInst->txCommitChunkLen = dataLength; + pInst->txCommitWasLastChunk = (dataFlags & SPI_TRANSPORT_FLAG_END) != 0; + } + else + { + flags = SPI_TRANSPORT_FLAG_FILLER; + if (pInst->linkState != eSpiTransportLinkConnected) + { + flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_RESET); + } + } + } + + trSpiTransportFrame frame; + frame.version = SPI_TRANSPORT_FRAME_VERSION; + frame.channel = channel; + frame.seq = pInst->txSeq; + frame.ack = pInst->rxLastSeq; + frame.flags = flags; + frame.payloadLength = length; + + (void)spiTransportFrameEncode (&frame, payload, pOutBuffer); + + /* txSeq is peeked here, not consumed -- mirrors the channel-data + * peek/commit split above. It only advances in onTransferComplete(), + * once the physical transfer this frame belongs to is confirmed to + * have actually gone out. An attempt that never completes (DMA-arm + * failure, watchdog-timeout abort, disconnect mid-transfer) leaves + * txSeq unchanged, so the next call re-peeks and re-sends this exact + * seq value instead of skipping it -- skipping it here previously + * meant the peer never saw that seq number at all, surfacing as a + * phantom sequence-gap error on every failed-then-retried attempt even + * though the actual channel data was correctly retried underneath. */ + + pInst->lastSendTickMs = pInst->prOs->pTickGet (pInst->prOs->pContext); +} + +/// @brief Decode and process one inbound frame -- handshake/epoch tracking, +/// sequence-gap detection, and channel dispatch. +static void +processIncomingFrame (trSpiTransportInstance *pInst, const uint8_t *pInBuffer) +{ + trSpiTransportFrame frame; + teSpiTransportError err = spiTransportFrameDecode (pInBuffer, &frame); + + if (err == eSpiTransportErrorHardwareFailure) + { + if (!spiTransportFrameHeaderCrcOk (pInBuffer)) + { + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorHeaderCrc); + } + else + { + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorPayloadCrc); + } + return; + } + if (err != eSpiTransportErrorNone) + { + return; /* bad magic / malformed -- dropped silently */ + } + + pInst->lastRecvTickMs = pInst->prOs->pTickGet (pInst->prOs->pContext); + + if ((frame.channel == SPI_TRANSPORT_CHANNEL_CONTROL) + && (frame.payloadLength >= SPI_TRANSPORT_CTRL_BODY_SIZE)) + { + uint8_t msgType = frame.pPayload[0]; + if ((msgType == SPI_TRANSPORT_CTRL_HELLO) || (msgType == SPI_TRANSPORT_CTRL_HELLO_ACK)) + { + uint32_t peerEpoch = getU32 (&frame.pPayload[2]); + uint16_t peerStartSeq = getU16 (&frame.pPayload[6]); + handleHandshake (pInst, msgType, peerEpoch, peerStartSeq); + } + } + + if (pInst->linkState == eSpiTransportLinkConnected) + { + if (pInst->rxSeqBaselineArmed) + { + uint16_t expected = (uint16_t)(pInst->rxLastSeq + 1u); + if (frame.seq != expected) + { + pInst->lastGapExpected = expected; + pInst->lastGapActual = frame.seq; + if (frame.seq == pInst->rxLastSeq) + { + pInst->gapDuplicateCount++; + } + else if ((uint16_t)(frame.seq - expected) < 0x8000u) + { + pInst->gapLossCount++; + } + else + { + pInst->gapOtherCount++; + } + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorSequenceGap); + } + } + else + { + pInst->rxSeqBaselineArmed = true; + } + pInst->rxLastSeq = frame.seq; + + if (frame.channel != SPI_TRANSPORT_CHANNEL_CONTROL) + { + spiTransportChannelDispatchRx (&pInst->channels, frame.channel, frame.pPayload, + frame.payloadLength, frame.flags); + } + } +} + +/* Host only. If waiting on the Client's ack and NRDY currently reads low, + * start the real clocked transfer. Shared between the tick-driven poll + * (hostServiceTick) and the interrupt-driven path (onReadyEvent) -- see + * docs/ProtocolSpec.md section 6; the tick poll remains correct on its own, the + * EXTI path just cuts the latency of noticing the edge. + * + * The check-then-claim on hostState is wrapped in pCriticalEnter/Exit + * because both callers (tick-driven and EXTI-driven) can genuinely race: + * confirmed live on hardware that without this guard, the task can read + * hostState==eHostWaitingAck, get preempted by the NRDY-falling EXTI before + * writing eHostTransferring, and the ISR's own call also passes the same + * (still-stale) check -- both then call buildOutgoingFrame(), advancing + * txSeq twice for what becomes only one physical transfer, which the Client + * observes as a spurious sequence-gap. */ +static void +hostArmTransferIfAcked (trSpiTransportInstance *pInst) +{ + pInst->prOs->pCriticalEnter (pInst->prOs->pContext); + bool shouldArm = (pInst->hostState == eHostWaitingAck) + && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == false); + if (shouldArm) + { + pInst->hostState = eHostTransferring; + } + pInst->prOs->pCriticalExit (pInst->prOs->pContext); + + if (!shouldArm) + { + gDiagHostArmRejectedCount++; + return; + } + gDiagHostArmArmedCount++; + buildOutgoingFrame (pInst, pInst->txBuffer); + teSpiTransportError startErr = pInst->prHw->pTransferStart ( + pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); + + if (startErr != eSpiTransportErrorNone) + { + /* DMA never actually armed -- nothing was clocked out, so + * onTransferComplete will never fire for this attempt. Abandon + * the peeked chunk (leave it uncommitted -- the same channel + * offset/txPending state as before this call, so the next + * successful attempt naturally retries it) and release NSS + * immediately rather than waiting out the 100ms ack-wait + * timeout or the 1.5s disconnect backstop for something we + * already know failed right now. */ + pInst->txCommitPending = false; + pInst->hostState = eHostIdle; + pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); + } +} + +/* Host only. Disambiguates a NRDY-falling edge/observation seen while + * hostState==eHostIdle: Host never drives NRDY itself and Client only ever + * lowers it either responding to Host's own request (which implies + * hostState==eHostWaitingAck, handled by hostArmTransferIfAcked() above, not + * this function) or self-initiating (clientState==eClientSelfArmed, see + * clientArmSelfInitiateIfIdle()). So hostState==eHostIdle observing NRDY low + * can only mean the latter -- no new wire signal needed, Host's own two-state + * distinction already disambiguates this for free. Skips straight to + * eHostTransferring (no separate ack step -- the ack already happened, it's + * what triggered this call). Same check-then-claim-under-critical-section + * shape as hostArmTransferIfAcked(), for the same double-arm-race reason. */ +static void +hostArmClientInitiatedIfIdle (trSpiTransportInstance *pInst) +{ + pInst->prOs->pCriticalEnter (pInst->prOs->pContext); + bool shouldArm = (pInst->hostState == eHostIdle) + && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == false); + if (shouldArm) + { + pInst->hostState = eHostTransferring; + } + pInst->prOs->pCriticalExit (pInst->prOs->pContext); + + if (!shouldArm) + { + return; + } + gDiagHostClientInitArmedCount++; + /* NSS low: lets the hardware-NSS-managed Client peripheral (already + * self-armed, waiting) actually begin shifting, and gives Client's own + * NSS-EXTI a real edge -- onSelectEvent's existing clientState!=eClientIdle + * guard correctly ignores it there (Client is eClientSelfArmed, not + * Idle), so no change needed on that side. */ + pInst->prHw->pSelectAssert (pInst->prHw->pContext, false); + buildOutgoingFrame (pInst, pInst->txBuffer); + teSpiTransportError startErr = pInst->prHw->pTransferStart ( + pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); + + if (startErr != eSpiTransportErrorNone) + { + pInst->txCommitPending = false; + pInst->hostState = eHostIdle; + pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); + } +} + +/* Host only. If idle and something is queued/due, and NRDY currently reads + * high (Client available), issue the next request. Shared the same way as + * hostArmTransferIfAcked() above -- same race, same critical-section fix. */ +static void +hostIssueRequestIfReady (trSpiTransportInstance *pInst, uint32_t now) +{ + bool haveSomethingQueued = pInst->pendingHello || pInst->pendingHelloAck + || spiTransportChannelHasPending (&pInst->channels); + bool heartbeatDue = (now - pInst->lastSendTickMs) >= SPI_TRANSPORT_HEARTBEAT_MS; + + pInst->prOs->pCriticalEnter (pInst->prOs->pContext); + bool shouldIssue = (pInst->hostState == eHostIdle) && (haveSomethingQueued || heartbeatDue) + && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == true); + if (shouldIssue) + { + pInst->hostState = eHostWaitingAck; + pInst->hostWaitStartMs = now; + } + pInst->prOs->pCriticalExit (pInst->prOs->pContext); + + if (!shouldIssue) + { + return; /* not idle, nothing to send, or Client not currently available */ + } + + pInst->prHw->pSelectAssert (pInst->prHw->pContext, false); /* LOW = issue the request */ +} + +static void +hostServiceTick (trSpiTransportInstance *pInst, uint32_t now) +{ + if (pInst->hostState == eHostWaitingAck) + { + hostArmTransferIfAcked (pInst); + if ((pInst->hostState == eHostWaitingAck) + && ((now - pInst->hostWaitStartMs) >= SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS)) + { + /* Client never acked this attempt -- give up on it, let the next + * heartbeat/data-queued opportunity retry (the 1.5s disconnect + * timer is the backstop if the link is genuinely down). */ + pInst->prHw->pSelectAssert (pInst->prHw->pContext, + true); /* HIGH = idle, give up this attempt */ + pInst->hostState = eHostIdle; + } + return; + } + + if (pInst->hostState != eHostIdle) + { + return; /* eHostTransferring: waiting on onTransferComplete */ + } + + /* Poll-path fallback for platforms without a NRDY EXTI (onReadyEvent's + * falling-edge branch is the latency-optimized path for those that have + * one): a Client self-initiation could have dropped NRDY between ticks + * with nothing to interrupt on. Re-check hostState afterward -- a + * successful claim here moves it to eHostTransferring. */ + hostArmClientInitiatedIfIdle (pInst); + if (pInst->hostState != eHostIdle) + { + return; + } + + hostIssueRequestIfReady (pInst, now); +} + +static void +onTransferComplete (void *pCoreCtx, uint16_t length) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; + (void)length; + + if (pInst->role == eSpiTransportRoleHost) + { + pInst->prHw->pSelectAssert (pInst->prHw->pContext, + true); /* HIGH = deassert, transfer done */ + pInst->hostState = eHostIdle; + } + else + { + pInst->clientState = eClientIdle; + } + + /* This callback firing at all is the confirmation the just-armed + * transfer's outgoing bytes were actually clocked out -- only now is + * it safe to commit whatever channel chunk buildOutgoingFrame() peeked + * into this frame, and to consume the seq value it peeked alongside + * it (see buildOutgoingFrame's comment). Every completed transfer + * consumes exactly one seq value, regardless of whether it carried + * real channel data or was a HELLO/FILLER frame. */ + pInst->txSeq++; + gDiagTxSeqAdvanceCount++; + if (pInst->txCommitPending) + { + spiTransportChannelCommitTx (&pInst->channels, pInst->txCommitChannel, + pInst->txCommitChunkLen, pInst->txCommitWasLastChunk); + pInst->txCommitPending = false; + } + + /* ISR context on real hardware -- must not call processIncomingFrame() + * here (it invokes user callbacks). Snapshot into the ring and defer to + * spiTransportTick(), which runs in task context. Producer side only: + * reads rxRingTail (written only by the consumer) to compute occupancy, + * writes rxRingHead. If the ring is full, drop this newest arrival + * (rare in practice, see spiTransportDebugRxOverwriteCount) rather than + * evicting an existing slot the task might be mid-read of -- this ISR + * can preempt the task at any point, so an in-place slot overwrite here + * would race that read. */ + uint32_t head = pInst->rxRingHead; + uint32_t tail = pInst->rxRingTail; + if ((head - tail) >= SPI_TRANSPORT_RX_RING_DEPTH) + { + pInst->rxOverwriteCount++; + } + else + { + memcpy (pInst->rxRingBuffer[head % SPI_TRANSPORT_RX_RING_DEPTH], pInst->rxBuffer, + SPI_TRANSPORT_FRAME_TOTAL_SIZE); + pInst->rxRingHead = head + 1u; + pInst->prOs->pTaskNotifyGive (pInst->prOs->pContext); + } +} + +static void +onSelectEvent (void *pCoreCtx, bool asserted) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; + + /* Backpressure: if the deferred-RX ring has no free slot, do NOT ack + * this request at all -- leave NRDY high, exactly as if Client weren't + * ready. Host's existing 100ms ack-wait timeout (hostServiceTick) is + * already the correct recovery path for "Client didn't ack this + * attempt", so this just reuses it instead of acking a transfer whose + * received frame would only get dropped at the ring anyway (see + * onTransferComplete's ring-full branch). Deferring the attempt this + * way, rather than accepting and silently losing it, is the actual fix + * for the RX-ring-overflow investigation -- not a replacement for it, + * a second layer: the loop-timing fix (see harnessMain) closes the + * dominant cause (blocking UART stalls), this closes the residual gap + * for whatever legitimate backlog remains. Checked before the + * clientState guard below since it's a distinct, worth-tracking- + * separately reason to decline. */ + bool ringFull = (pInst->rxRingHead - pInst->rxRingTail) >= SPI_TRANSPORT_RX_RING_DEPTH; + + if ((pInst->role != eSpiTransportRoleClient) || !asserted || (pInst->clientState != eClientIdle) + || ringFull) + { + if ((pInst->role == eSpiTransportRoleClient) && asserted) + { + if (ringFull && (pInst->clientState == eClientIdle)) + { + gDiagRxRingFullRejectedCount++; + return; + } + gDiagOnSelectRejectedCount++; + /* Real NSS falling while already eClientSelfArmed is the + * EXPECTED shape of a Client-initiated cycle (Host is + * about to catch up to the solicitation already in + * flight) -- split out from the general rejection count + * so it isn't confused with a genuine problem (e.g. + * clientState stuck eClientArmed/eClientTransferring for + * some other reason). */ + if (pInst->clientState == eClientSelfArmed) + { + gDiagOnSelectRejectedSelfArmedCount++; + } + } + return; + } + + /* Claim eClientArmed before triggering pTransferStart/pReadyAssert + * below, not after: on the host-native null-loopback HW adapter (and, + * on real hardware, if the physical transfer completes and its ISRs + * run before this function returns), the whole transfer -- including + * onClockStart/onTransferComplete moving clientState on to + * eClientTransferring/eClientIdle -- can happen synchronously inside + * that pReadyAssert call. Assigning eClientArmed afterwards would + * stomp that already-correct later state back to a stale one. */ + pInst->clientState = eClientArmed; + buildOutgoingFrame (pInst, pInst->txBuffer); + teSpiTransportError startErr = pInst->prHw->pTransferStart ( + pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); + + if (startErr != eSpiTransportErrorNone) + { + /* Slave DMA never actually armed. Abandon the peeked chunk + * (leave it uncommitted for a clean retry) and go back to + * Idle without ever asserting NRDY low -- Host's existing + * 100ms ack-wait timeout already handles "Client never acked + * this attempt" correctly, so this degrades to that same, + * already-working retry path instead of needing a new one. */ + pInst->txCommitPending = false; + pInst->clientState = eClientIdle; + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); + return; + } + gDiagOnSelectArmedCount++; + pInst->prHw->pReadyAssert (pInst->prHw->pContext, false); +} + +static void +onClockStart (void *pCoreCtx) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; + + if (pInst->role != eSpiTransportRoleClient) + { + return; + } + + pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); + pInst->clientState = eClientTransferring; +} + +/* Client only. If idle and something is queued, arm the Client's own DMA + * and drop NRDY while NSS is still HIGH, soliciting Host -- see + * hostArmClientInitiatedIfIdle() for how Host disambiguates this from its + * own request cycle (no new wire signal needed). Task-context only (called + * from clientServiceTick()), so the critical section here is what keeps + * onSelectEvent()'s ISR-context clientState!=eClientIdle check race-free + * against this claim -- unlike Host's symmetric pair, only this one writer + * needs the section, since onSelectEvent is the sole other writer and it + * only ever runs with this masked out while the section is held. */ +static void +clientArmSelfInitiateIfIdle (trSpiTransportInstance *pInst, uint32_t now) +{ + if (!pInst->clientSelfInitEnabled) + { + return; /* disabled (the default) -- see spiTransportSetClientSelfInitEnabled() */ + } + + bool haveSomethingQueued = pInst->pendingHello || pInst->pendingHelloAck + || spiTransportChannelHasPending (&pInst->channels); + /* Same backpressure reasoning as onSelectEvent's ring-full check: don't + * solicit Host for a reply this Client has no room to receive. */ + bool ringFull = (pInst->rxRingHead - pInst->rxRingTail) >= SPI_TRANSPORT_RX_RING_DEPTH; + + pInst->prOs->pCriticalEnter (pInst->prOs->pContext); + bool shouldArm = (pInst->clientState == eClientIdle) && haveSomethingQueued && !ringFull; + if (shouldArm) + { + pInst->clientState = eClientSelfArmed; + pInst->clientSelfArmStartMs = now; + } + pInst->prOs->pCriticalExit (pInst->prOs->pContext); + + if (!shouldArm) + { + if (ringFull && (pInst->clientState == eClientIdle) && haveSomethingQueued) + { + gDiagRxRingFullRejectedCount++; + } + return; + } + gDiagClientSelfArmAttemptCount++; + buildOutgoingFrame (pInst, pInst->txBuffer); + teSpiTransportError startErr = pInst->prHw->pTransferStart ( + pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); + + if (startErr != eSpiTransportErrorNone) + { + /* Same degrade-to-existing-retry-path reasoning as + * onSelectEvent's own failure branch: abandon the peeked chunk, + * go back to Idle without ever asserting NRDY low, and let the + * next tick's opportunity retry. */ + pInst->txCommitPending = false; + pInst->clientState = eClientIdle; + spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); + return; + } + pInst->prHw->pReadyAssert (pInst->prHw->pContext, + false); /* solicit: NRDY low, NSS still high */ +} + +/* Client only. Mirrors hostServiceTick(): services the self-init watchdog + * (eClientSelfArmed timeout) and otherwise attempts a new self-initiation + * when idle. Called from spiTransportTick() for the Client role. */ +static void +clientServiceTick (trSpiTransportInstance *pInst, uint32_t now) +{ + if (pInst->clientState == eClientSelfArmed) + { + if ((now - pInst->clientSelfArmStartMs) >= SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS) + { + /* Host never noticed/clocked this solicitation -- self-heal + * now rather than waiting out the far coarser 1.5s + * disconnect backstop (doDisconnect()'s own generic + * clientState!=eClientIdle handling there remains a + * second-layer defense if this ever doesn't fire, e.g. if + * the platform starves this tick badly enough). pAbort() + * BEFORE resetting clientState/NRDY -- same ordering rule + * as doDisconnect()'s Client branch, for the same reason: + * closes the window where a concurrent + * onSelectEvent/self-init claim could re-arm against + * hardware still mid-abort. */ + gDiagClientSelfArmTimeoutCount++; + pInst->prHw->pAbort (pInst->prHw->pContext); + pInst->clientState = eClientIdle; + pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); + spiTransportChannelNotifyEvent (&pInst->channels, + eSpiTransportEventErrorDmaTimeout); + } + return; /* still self-armed (not yet timed out): don't also retry */ + } + + if (pInst->clientState != eClientIdle) + { + return; /* eClientArmed/eClientTransferring: reacting to a real Host request */ + } + + clientArmSelfInitiateIfIdle (pInst, now); +} + +static void +onReadyEvent (void *pCoreCtx, bool high) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; + + if (pInst->role != eSpiTransportRoleHost) + { + return; + } + + /* Interrupt-driven NRDY watch: acts immediately on the edge rather than + * waiting for the next spiTransportTick() poll -- the tick-driven path + * (hostServiceTick) remains correct on its own and is what a platform + * with no NRDY EXTI (or an adapter that leaves pOnReadyEvent unfired) + * relies on exclusively; this is a pure latency optimization. */ + if (!high) + { + /* Falling edge: either Client's ack for Host's own pending + * request (hostState==eHostWaitingAck), or Client self- + * initiating while Host was idle (hostState==eHostIdle) -- + * mutually exclusive by construction (see + * hostArmClientInitiatedIfIdle()'s doc comment), so branch on + * which rather than trying both (calling the wrong one is a + * harmless no-op either way, but would pollute that path's own + * [DIAG] rejection counter with an unrelated cause). */ + if (pInst->hostState == eHostWaitingAck) + { + hostArmTransferIfAcked (pInst); + } + else + { + hostArmClientInitiatedIfIdle (pInst); + } + } + else + { + /* Rising edge: Client became available again. */ + hostIssueRequestIfReady (pInst, pInst->prOs->pTickGet (pInst->prOs->pContext)); + } +} + +teSpiTransportError +spiTransportInit (const trSpiTransportConfig *prConfig, thSpiTransport *phTransport) +{ + if ((prConfig == NULL) || (prConfig->prOs == NULL) || (prConfig->prHw == NULL) + || (phTransport == NULL)) + { + return eSpiTransportErrorInvalidParam; + } + + crc16Init (); + + trSpiTransportInstance *pInst = NULL; + for (uint8_t i = 0; i < SPI_TRANSPORT_INSTANCES_MAX; i++) + { + if (!gInstances[i].inUse) + { + pInst = &gInstances[i]; + break; + } + } + if (pInst == NULL) + { + return eSpiTransportErrorBusy; /* pool exhausted */ + } + + memset (pInst, 0, sizeof (*pInst)); + + pInst->inUse = true; + pInst->role = prConfig->role; + pInst->prOs = prConfig->prOs; + pInst->prHw = prConfig->prHw; + + spiTransportChannelTableInit (&pInst->channels); + pInst->linkState = eSpiTransportLinkDisconnected; + pInst->lastTxChannel = SPI_TRANSPORT_CHANNELS_MAX - 1u; /* round-robin starts at 0 */ + pInst->epoch = pInst->prOs->pTickGet (pInst->prOs->pContext); + + spiTransportHwSetCallbacks (pInst->prHw, onTransferComplete, onSelectEvent, onReadyEvent, + onClockStart, pInst); + + *phTransport = pInst; + return eSpiTransportErrorNone; +} + +teSpiTransportError +spiTransportStart (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return eSpiTransportErrorInvalidParam; + } + + uint32_t now = pInst->prOs->pTickGet (pInst->prOs->pContext); + + pInst->peerEpochKnown = false; + resetForHandshake (pInst); + pInst->txSeq = 0; + pInst->rxLastSeq = 0; + pInst->lastSendTickMs = now; + pInst->lastRecvTickMs = now; + pInst->hostState = eHostIdle; + pInst->clientState = eClientIdle; + + pInst->rxRingHead = 0u; + pInst->rxRingTail = 0u; + + /* Force the physical line back to idle-high regardless of whatever + * level it was left at (e.g. a prior spiTransportStop() call, or a + * board's power-on GPIO default) -- otherwise a Start() that follows a + * mid-transfer Stop() (or the very first Start() before any GPIO init + * guarantee) can begin operation with the peer seeing a stale low + * NSS/NRDY that this session's state machine never actually asserted. */ + if (pInst->role == eSpiTransportRoleHost) + { + pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); + } + else + { + pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); + } + + return eSpiTransportErrorNone; +} + +void +spiTransportStop (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return; + } + + /* Same un-wedge requirement as doDisconnect()'s backstop -- Stop() can + * be called mid-transfer (see the fault-injection "reset during + * transfer" mode in docs/TestPlan.md), and simply resetting + * hostState/clientState below without releasing the physical line or + * aborting a genuinely-armed peripheral would leave NSS/NRDY latched + * and the SPI/DMA peripheral still busy for whatever runs next. Abort + * before flipping state, same ordering reason as doDisconnect(). */ + if (pInst->role == eSpiTransportRoleHost) + { + if (pInst->hostState == eHostTransferring) + { + pInst->prHw->pAbort (pInst->prHw->pContext); + } + pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); + } + else + { + if (pInst->clientState != eClientIdle) + { + pInst->prHw->pAbort (pInst->prHw->pContext); + } + pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); + } + + pInst->linkState = eSpiTransportLinkDisconnected; + pInst->hostState = eHostIdle; + pInst->clientState = eClientIdle; + spiTransportChannelResetAll (&pInst->channels); +} + +teSpiTransportError +spiTransportRegisterChannel (thSpiTransport hTransport, uint8_t channel, + tpSpiTransportRxCallback pRxCallback, + tpSpiTransportEventCallback pEventCallback, void *pContext) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return eSpiTransportErrorInvalidParam; + } + return spiTransportChannelRegister (&pInst->channels, channel, pRxCallback, pEventCallback, + pContext); +} + +teSpiTransportError +spiTransportDeregisterChannel (thSpiTransport hTransport, uint8_t channel) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return eSpiTransportErrorInvalidParam; + } + return spiTransportChannelDeregister (&pInst->channels, channel); +} + +teSpiTransportError +spiTransportSend (thSpiTransport hTransport, uint8_t channel, const uint8_t *pBuffer, + uint16_t length, bool ackRequired) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return eSpiTransportErrorInvalidParam; + } + return spiTransportChannelQueueTx (&pInst->channels, channel, pBuffer, length, ackRequired); +} + +teSpiTransportLinkState +spiTransportGetLinkState (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return eSpiTransportLinkDisconnected; + } + return pInst->linkState; +} + +void +spiTransportSetClientSelfInitEnabled (thSpiTransport hTransport, bool enabled) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse || (pInst->role != eSpiTransportRoleClient)) + { + return; + } + pInst->clientSelfInitEnabled = enabled; +} + +bool +spiTransportIsClientSelfInitEnabled (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return false; + } + return pInst->clientSelfInitEnabled; +} + +void +spiTransportTick (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return; + } + + /* Consumer side of the ring: drain everything queued, not just one + * frame, so a burst of back-to-back completions (see onTransferComplete) + * doesn't leave later ones waiting for a whole extra tick. Reads + * rxRingHead (written only by the producer/ISR) fresh each iteration -- + * safe without a lock for the same single-writer-per-counter reason as + * the producer side, and picks up anything that arrived mid-drain for + * free. Strict FIFO: tail only ever advances by exactly one slot at a + * time, in arrival order. */ + while (pInst->rxRingTail != pInst->rxRingHead) + { + uint32_t tail = pInst->rxRingTail; + processIncomingFrame (pInst, pInst->rxRingBuffer[tail % SPI_TRANSPORT_RX_RING_DEPTH]); + pInst->rxRingTail = tail + 1u; + } + + uint32_t now = pInst->prOs->pTickGet (pInst->prOs->pContext); + + if ((pInst->linkState != eSpiTransportLinkDisconnected) + && ((now - pInst->lastRecvTickMs) >= SPI_TRANSPORT_DISCONNECT_MS)) + { + doDisconnect (pInst, now); + } + + if (pInst->role == eSpiTransportRoleHost) + { + hostServiceTick (pInst, now); + } + else + { + clientServiceTick (pInst, now); + } +} + +uint32_t +spiTransportDebugRxOverwriteCount (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return 0u; + } + return pInst->rxOverwriteCount; +} + +void +spiTransportDebugTxSeqCounts (thSpiTransport hTransport, uint32_t *pBuiltCount, + uint32_t *pAdvanceCount, uint16_t *pCurrentTxSeq) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + if (pBuiltCount != NULL) + { + *pBuiltCount = 0u; + } + if (pAdvanceCount != NULL) + { + *pAdvanceCount = 0u; + } + if (pCurrentTxSeq != NULL) + { + *pCurrentTxSeq = 0u; + } + return; + } + if (pBuiltCount != NULL) + { + *pBuiltCount = gDiagTxFrameBuiltCount; + } + if (pAdvanceCount != NULL) + { + *pAdvanceCount = gDiagTxSeqAdvanceCount; + } + if (pCurrentTxSeq != NULL) + { + *pCurrentTxSeq = pInst->txSeq; + } +} + +uint32_t +spiTransportDebugRxRingFullRejectedCount (thSpiTransport hTransport) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return 0u; + } + return gDiagRxRingFullRejectedCount; +} + +void +spiTransportDebugClientArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, + uint32_t *pArmedCount) +{ + (void)hTransport; + if (pRejectedCount != NULL) + { + *pRejectedCount = gDiagOnSelectRejectedCount; + } + if (pArmedCount != NULL) + { + *pArmedCount = gDiagOnSelectArmedCount; + } +} + +void +spiTransportDebugHostArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, + uint32_t *pArmedCount) +{ + (void)hTransport; + if (pRejectedCount != NULL) + { + *pRejectedCount = gDiagHostArmRejectedCount; + } + if (pArmedCount != NULL) + { + *pArmedCount = gDiagHostArmArmedCount; + } +} + +void +spiTransportDebugClientSelfInitCounts (thSpiTransport hTransport, uint32_t *pAttemptCount, + uint32_t *pTimeoutCount, uint32_t *pHostArmedCount, + uint32_t *pRejectedSelfArmedCount) +{ + (void)hTransport; + if (pAttemptCount != NULL) + { + *pAttemptCount = gDiagClientSelfArmAttemptCount; + } + if (pTimeoutCount != NULL) + { + *pTimeoutCount = gDiagClientSelfArmTimeoutCount; + } + if (pHostArmedCount != NULL) + { + *pHostArmedCount = gDiagHostClientInitArmedCount; + } + if (pRejectedSelfArmedCount != NULL) + { + *pRejectedSelfArmedCount = gDiagOnSelectRejectedSelfArmedCount; + } +} + +void +spiTransportDebugLastGap (thSpiTransport hTransport, uint16_t *pExpected, uint16_t *pActual, + uint32_t *pDuplicateCount, uint32_t *pLossCount, uint32_t *pOtherCount) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return; + } + if (pExpected != NULL) + { + *pExpected = pInst->lastGapExpected; + } + if (pActual != NULL) + { + *pActual = pInst->lastGapActual; + } + if (pDuplicateCount != NULL) + { + *pDuplicateCount = pInst->gapDuplicateCount; + } + if (pLossCount != NULL) + { + *pLossCount = pInst->gapLossCount; + } + if (pOtherCount != NULL) + { + *pOtherCount = pInst->gapOtherCount; + } +} + +void +spiTransportDebugEpoch (thSpiTransport hTransport, uint32_t *pOwnEpoch, uint32_t *pPeerEpoch, + bool *pPeerEpochKnown) +{ + trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; + if ((pInst == NULL) || !pInst->inUse) + { + return; + } + if (pOwnEpoch != NULL) + { + *pOwnEpoch = pInst->epoch; + } + if (pPeerEpoch != NULL) + { + *pPeerEpoch = pInst->peerEpoch; + } + if (pPeerEpochKnown != NULL) + { + *pPeerEpochKnown = pInst->peerEpochKnown; + } +} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_channel.c b/drivers/spi/spi-mt-transport/core/spi_transport_channel.c new file mode 100644 index 0000000000000..cbaa7b5a936c5 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/spi_transport_channel.c @@ -0,0 +1,262 @@ +//****************************************************************************** +// @file : spi_transport_channel.c +// @brief : Channel registration table, RX reassembly, per-channel +// single-slot TX queue. See spi_transport_channel.h. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#include "spi_transport/spi_transport_channel.h" + +#include + +void +spiTransportChannelTableInit (trSpiTransportChannelTable *prTable) +{ + memset (prTable, 0, sizeof (*prTable)); +} + +teSpiTransportError +spiTransportChannelRegister (trSpiTransportChannelTable *prTable, uint8_t channel, + tpSpiTransportRxCallback pRxCallback, + tpSpiTransportEventCallback pEventCallback, void *pContext) +{ + if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) + { + return eSpiTransportErrorInvalidChannel; + } + if (prTable->slots[channel].registered) + { + return eSpiTransportErrorAlreadyRegistered; + } + + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + memset (pSlot, 0, sizeof (*pSlot)); + pSlot->registered = true; + pSlot->pRxCallback = pRxCallback; + pSlot->pEventCallback = pEventCallback; + pSlot->pContext = pContext; + + return eSpiTransportErrorNone; +} + +teSpiTransportError +spiTransportChannelDeregister (trSpiTransportChannelTable *prTable, uint8_t channel) +{ + if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) + { + return eSpiTransportErrorInvalidChannel; + } + if (!prTable->slots[channel].registered) + { + return eSpiTransportErrorNotRegistered; + } + + memset (&prTable->slots[channel], 0, sizeof (prTable->slots[channel])); + return eSpiTransportErrorNone; +} + +teSpiTransportError +spiTransportChannelQueueTx (trSpiTransportChannelTable *prTable, uint8_t channel, + const uint8_t *pBuffer, uint16_t length, bool ackRequired) +{ + /* Channel 0 is transport-internal (HELLO/HELLO_ACK/FILLER) -- matches + * spiTransportChannelRegister()'s own guard. Without this, application + * data queued on channel 0 would collide with control-frame traffic and + * corrupt the handshake/state machine (spiTransportChannelNextTx() has + * no way to distinguish the two once queued). */ + if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) + { + return eSpiTransportErrorInvalidChannel; + } + if (length > SPI_TRANSPORT_CHANNEL_MESSAGE_MAX) + { + return eSpiTransportErrorInvalidParam; + } + if ((pBuffer == NULL) && (length > 0u)) + { + return eSpiTransportErrorInvalidParam; + } + + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + if (pSlot->txPending) + { + return eSpiTransportErrorBusy; + } + + pSlot->txPending = true; + pSlot->pTxBuffer = pBuffer; + pSlot->txLength = length; + pSlot->txOffset = 0; + pSlot->txAckRequired = ackRequired; + + return eSpiTransportErrorNone; +} + +bool +spiTransportChannelNextTx (trSpiTransportChannelTable *prTable, uint8_t startAfterChannel, + uint8_t *pOutChannel, uint8_t *pOutPayload, uint16_t *pOutLength, + uint8_t *pOutFlags) +{ + uint8_t start = (uint8_t)((startAfterChannel + 1u) % SPI_TRANSPORT_CHANNELS_MAX); + + for (uint8_t i = 0; i < SPI_TRANSPORT_CHANNELS_MAX; i++) + { + uint8_t channel = (uint8_t)((start + i) % SPI_TRANSPORT_CHANNELS_MAX); + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + + if (!pSlot->txPending) + { + continue; + } + + uint16_t remaining = (uint16_t)(pSlot->txLength - pSlot->txOffset); + uint16_t chunk = (remaining < SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) + ? remaining + : SPI_TRANSPORT_FRAME_PAYLOAD_SIZE; + + uint8_t flags = 0; + if (pSlot->txOffset == 0u) + { + flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_START); + } + bool isLastChunk = (uint16_t)(pSlot->txOffset + chunk) >= pSlot->txLength; + if (isLastChunk) + { + flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_END); + } + if (pSlot->txAckRequired) + { + flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_ACK_REQUIRED); + } + + if (chunk > 0u) + { + memcpy (pOutPayload, &pSlot->pTxBuffer[pSlot->txOffset], chunk); + } + + *pOutChannel = channel; + *pOutLength = chunk; + *pOutFlags = flags; + return true; + } + + return false; +} + +void +spiTransportChannelCommitTx (trSpiTransportChannelTable *prTable, uint8_t channel, + uint16_t chunkLen, bool wasLastChunk) +{ + if (channel >= SPI_TRANSPORT_CHANNELS_MAX) + { + return; + } + + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + pSlot->txOffset = (uint16_t)(pSlot->txOffset + chunkLen); + + if (wasLastChunk) + { + pSlot->txPending = false; + pSlot->pTxBuffer = NULL; + } +} + +bool +spiTransportChannelHasPending (const trSpiTransportChannelTable *prTable) +{ + for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) + { + if (prTable->slots[channel].txPending) + { + return true; + } + } + + return false; +} + +void +spiTransportChannelDispatchRx (trSpiTransportChannelTable *prTable, uint8_t channel, + const uint8_t *pPayload, uint16_t length, uint8_t flags) +{ + if (channel >= SPI_TRANSPORT_CHANNELS_MAX) + { + return; + } + + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + if (!pSlot->registered) + { + return; /* unregistered channel: silently dropped (counted by the caller, if desired) */ + } + + if (flags & SPI_TRANSPORT_FLAG_START) + { + pSlot->rxInProgress = true; + pSlot->rxOffset = 0; + } + + if (!pSlot->rxInProgress) + { + return; /* END/middle chunk arrived with no START seen yet (e.g. post-reconnect) */ + } + + if ((uint32_t)pSlot->rxOffset + length > SPI_TRANSPORT_CHANNEL_MESSAGE_MAX) + { + pSlot->rxInProgress = false; /* oversized message: abandon and drop */ + return; + } + + if (length > 0u) + { + memcpy (&pSlot->rxBuffer[pSlot->rxOffset], pPayload, length); + pSlot->rxOffset = (uint16_t)(pSlot->rxOffset + length); + } + + if (flags & SPI_TRANSPORT_FLAG_END) + { + pSlot->rxInProgress = false; + if (pSlot->pRxCallback != NULL) + { + pSlot->pRxCallback (pSlot->pContext, channel, pSlot->rxBuffer, pSlot->rxOffset, + flags); + } + } +} + +void +spiTransportChannelResetAll (trSpiTransportChannelTable *prTable) +{ + for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) + { + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + pSlot->txPending = false; + pSlot->pTxBuffer = NULL; + pSlot->txOffset = 0; + pSlot->rxInProgress = false; + pSlot->rxOffset = 0; + } +} + +void +spiTransportChannelNotifyEvent (trSpiTransportChannelTable *prTable, teSpiTransportEvent eEvent) +{ + for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) + { + trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; + if (pSlot->registered && (pSlot->pEventCallback != NULL)) + { + pSlot->pEventCallback (pSlot->pContext, eEvent); + } + } +} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c b/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c new file mode 100644 index 0000000000000..6f81bc296f590 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c @@ -0,0 +1,67 @@ +//****************************************************************************** +// @file : spi_transport_crc16.c +// @brief : CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect), +// table-driven. New algorithm for this transport -- deliberately +// not the shared CRC32 used elsewhere in this repo, see +// docs/ProtocolSpec.md "CRC choice & duplication tradeoff". +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#include "spi_transport/spi_transport_frame.h" + +static uint16_t crc16Table[256]; +static bool crc16TableBuilt = false; + +static void +buildCrc16Table (void) +{ + for (uint32_t i = 0; i < 256u; i++) + { + uint16_t crc = (uint16_t)(i << 8); + for (uint32_t bit = 0; bit < 8u; bit++) + { + if (crc & 0x8000u) + { + crc = (uint16_t)((crc << 1) ^ 0x1021u); + } + else + { + crc = (uint16_t)(crc << 1); + } + } + crc16Table[i] = crc; + } +} + +void +crc16Init (void) +{ + if (!crc16TableBuilt) + { + buildCrc16Table (); + crc16TableBuilt = true; + } +} + +uint16_t +crc16 (const uint8_t *pBuffer, uint16_t length) +{ + uint16_t crc = 0xFFFFu; + + for (uint16_t i = 0; i < length; i++) + { + uint8_t index = (uint8_t)((crc >> 8) ^ pBuffer[i]); + crc = (uint16_t)((crc << 8) ^ crc16Table[index]); + } + + return crc; +} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_frame.c b/drivers/spi/spi-mt-transport/core/spi_transport_frame.c new file mode 100644 index 0000000000000..7af53edc8524b --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/spi_transport_frame.c @@ -0,0 +1,151 @@ +//****************************************************************************** +// @file : spi_transport_frame.c +// @brief : Wire-frame encode/decode. Explicit little-endian codec, not a +// packed struct overlay -- see spi_transport_frame.h. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#include "spi_transport/spi_transport_frame.h" + +#include + +static void +putU16 (uint8_t *pOut, uint16_t value) +{ + pOut[0] = (uint8_t)(value & 0xFFu); + pOut[1] = (uint8_t)((value >> 8) & 0xFFu); +} + +static uint16_t +getU16 (const uint8_t *pIn) +{ + return (uint16_t)((uint16_t)pIn[0] | ((uint16_t)pIn[1] << 8)); +} + +teSpiTransportError +spiTransportFrameEncode (const trSpiTransportFrame *prFrame, const uint8_t *pPayload, + uint8_t *pOutBuffer) +{ + if ((prFrame == NULL) || (pOutBuffer == NULL)) + { + return eSpiTransportErrorInvalidParam; + } + if (prFrame->payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) + { + return eSpiTransportErrorInvalidParam; + } + if ((pPayload == NULL) && (prFrame->payloadLength > 0u)) + { + return eSpiTransportErrorInvalidParam; + } + + putU16 (&pOutBuffer[0], SPI_TRANSPORT_FRAME_MAGIC); + pOutBuffer[2] = prFrame->version; + pOutBuffer[3] = prFrame->channel; + putU16 (&pOutBuffer[4], prFrame->seq); + putU16 (&pOutBuffer[6], prFrame->ack); + pOutBuffer[8] = prFrame->flags; + pOutBuffer[9] = 0; /* reserved */ + putU16 (&pOutBuffer[10], prFrame->payloadLength); + putU16 (&pOutBuffer[12], crc16 (pOutBuffer, SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u)); + + uint8_t *pPayloadRegion = &pOutBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; + if (prFrame->payloadLength > 0u) + { + memcpy (pPayloadRegion, pPayload, prFrame->payloadLength); + } + if (prFrame->payloadLength < SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) + { + memset (&pPayloadRegion[prFrame->payloadLength], 0, + (size_t)(SPI_TRANSPORT_FRAME_PAYLOAD_SIZE - prFrame->payloadLength)); + } + + uint16_t payloadCrc = crc16 (pPayloadRegion, prFrame->payloadLength); + putU16 (&pOutBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE], + payloadCrc); + + return eSpiTransportErrorNone; +} + +bool +spiTransportFrameHeaderCrcOk (const uint8_t *pInBuffer) +{ + if (pInBuffer == NULL) + { + return false; + } + uint16_t expected = getU16 (&pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u]); + uint16_t actual = crc16 (pInBuffer, SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u); + return expected == actual; +} + +bool +spiTransportFramePayloadCrcOk (const uint8_t *pInBuffer) +{ + if (pInBuffer == NULL) + { + return false; + } + uint16_t payloadLength = getU16 (&pInBuffer[10]); + if (payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) + { + return false; + } + + const uint8_t *pPayloadRegion = &pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; + uint16_t expected + = getU16 (&pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE]); + uint16_t actual = crc16 (pPayloadRegion, payloadLength); + return expected == actual; +} + +teSpiTransportError +spiTransportFrameDecode (const uint8_t *pInBuffer, trSpiTransportFrame *prFrame) +{ + if ((pInBuffer == NULL) || (prFrame == NULL)) + { + return eSpiTransportErrorInvalidParam; + } + + uint16_t magic = getU16 (&pInBuffer[0]); + if (magic != SPI_TRANSPORT_FRAME_MAGIC) + { + return eSpiTransportErrorInvalidParam; + } + + if (!spiTransportFrameHeaderCrcOk (pInBuffer)) + { + return eSpiTransportErrorHardwareFailure; + } + + uint16_t payloadLength = getU16 (&pInBuffer[10]); + if (payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) + { + return eSpiTransportErrorInvalidParam; + } + + if (!spiTransportFramePayloadCrcOk (pInBuffer)) + { + return eSpiTransportErrorHardwareFailure; + } + + prFrame->magic = magic; + prFrame->version = pInBuffer[2]; + prFrame->channel = pInBuffer[3]; + prFrame->seq = getU16 (&pInBuffer[4]); + prFrame->ack = getU16 (&pInBuffer[6]); + prFrame->flags = pInBuffer[8]; + prFrame->payloadLength = payloadLength; + prFrame->pPayload = &pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; + + return eSpiTransportErrorNone; +} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_hw.c b/drivers/spi/spi-mt-transport/core/spi_transport_hw.c new file mode 100644 index 0000000000000..fdf19498d64c9 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/spi_transport_hw.c @@ -0,0 +1,31 @@ +//****************************************************************************** +// @file : spi_transport_hw.c +// @brief : Generic (adapter-independent) setter for the HW-adapter's +// core-side callback fields. See spi_transport_hw.h. +// @date : 2026-08-10 +//****************************************************************************** +// @attention +// +// Copyright (c) 2026 MultiTracks.com, LLC. +// All rights reserved. +// +// For internal MultiTracks use only. Unauthorized reproduction, distribution, +// or disclosure is prohibited. +// +//****************************************************************************** + +#include "spi_transport/spi_transport_hw.h" + +void +spiTransportHwSetCallbacks (trSpiTransportHw *prHw, + void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length), + void (*pOnSelectEvent) (void *pCoreCtx, bool asserted), + void (*pOnReadyEvent) (void *pCoreCtx, bool asserted), + void (*pOnClockStart) (void *pCoreCtx), void *pCoreCtx) +{ + prHw->pOnTransferComplete = pOnTransferComplete; + prHw->pOnSelectEvent = pOnSelectEvent; + prHw->pOnReadyEvent = pOnReadyEvent; + prHw->pOnClockStart = pOnClockStart; + prHw->pCoreCtx = pCoreCtx; +} 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..581823d9d0372 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * spi_mt_transport_drv.c - Linux kernel Host-role driver for the MultiTracks + * SPI transport protocol (see core/ for the portable protocol core, vendored + * from the firmware repo -- see core/PROVENANCE.md). + * + * 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 + * (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 is a deliberately minimal placeholder, just enough to prove the + * module loads, probes, and can move a byte. + */ + +#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_CHANNEL 1 + +struct mt_transport_priv { + struct spi_device *spi; + struct device *dev; + + struct gpio_desc *nss_gpiod; + struct gpio_desc *nrdy_gpiod; + int nrdy_irq; + + 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; + + /* Minimal placeholder userspace interface -- MT-158682 owns the real + * design. Single hardcoded channel, 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; +}; + +/* 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_priv *priv = pContext; + unsigned long irqflags; + + (void)flags; + if (channel != MT_TRANSPORT_CHANNEL) + return; + if (length > sizeof(priv->rx_buf)) + length = sizeof(priv->rx_buf); + + spin_lock_irqsave(&priv->rx_lock, irqflags); + memcpy(priv->rx_buf, pBuffer, length); + priv->rx_len = length; + priv->rx_valid = true; + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + + wake_up_interruptible(&priv->rx_wq); +} + +static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) +{ + struct mt_transport_priv *priv = pContext; + + dev_dbg(priv->dev, "link event: %d\n", (int)eEvent); +} + +static int mt_transport_tick_thread_fn(void *data) +{ + struct mt_transport_priv *priv = data; + + while (!kthread_should_stop()) { + priv->os.pTaskNotifyWait(priv->os.pContext, 2); + spiTransportTick(priv->htransport); + } + return 0; +} + +/* --- Minimal userspace interface (placeholder, see plan sec 7) --- */ + +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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + unsigned long irqflags; + uint16_t len; + int ret; + + (void)ppos; + + if (filp->f_flags & O_NONBLOCK) { + spin_lock_irqsave(&priv->rx_lock, irqflags); + if (!priv->rx_valid) { + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + return -EAGAIN; + } + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + } else { + ret = wait_event_interruptible(priv->rx_wq, priv->rx_valid); + if (ret) + return ret; + } + + spin_lock_irqsave(&priv->rx_lock, irqflags); + len = priv->rx_len; + if (len > count) + len = count; + if (copy_to_user(buf, priv->rx_buf, len)) { + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + return -EFAULT; + } + priv->rx_valid = false; + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + + return len; +} + +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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + uint8_t txBuf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + teSpiTransportError err; + size_t len = count; + + (void)ppos; + + if (len > sizeof(txBuf)) + len = sizeof(txBuf); + if (copy_from_user(txBuf, buf, len)) + return -EFAULT; + + err = spiTransportSend(priv->htransport, MT_TRANSPORT_CHANNEL, txBuf, len, true); + if (err != eSpiTransportErrorNone) + return -EBUSY; + + return len; +} + +static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) +{ + struct miscdevice *misc = filp->private_data; + struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + __poll_t mask = 0; + + poll_wait(filp, &priv->rx_wq, wait); + if (priv->rx_valid) + mask |= EPOLLIN | EPOLLRDNORM; + return mask; +} + +static const struct file_operations mt_transport_misc_fops = { + .owner = THIS_MODULE, + .read = mt_transport_misc_read, + .write = mt_transport_misc_write, + .poll = mt_transport_misc_poll, +}; + +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 struct attribute *mt_transport_attrs[] = { + &dev_attr_link_state.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; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->spi = spi; + priv->dev = dev; + spi_set_drvdata(spi, priv); + + init_waitqueue_head(&priv->rx_wq); + spin_lock_init(&priv->rx_lock); + + /* Custom "mt-nss"/"mt-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. + */ + priv->nss_gpiod = devm_gpiod_get(dev, "mt-nss", GPIOD_OUT_HIGH); + if (IS_ERR(priv->nss_gpiod)) + return dev_err_probe(dev, PTR_ERR(priv->nss_gpiod), + "failed to get mt-nss-gpios\n"); + + priv->nrdy_gpiod = devm_gpiod_get(dev, "mt-nrdy", GPIOD_IN); + if (IS_ERR(priv->nrdy_gpiod)) + return dev_err_probe(dev, PTR_ERR(priv->nrdy_gpiod), + "failed to get mt-nrdy-gpios\n"); + + mt_transport_os_linux_init(&priv->os_ctx, dev, &priv->os); + 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) + return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); + + if (spiTransportRegisterChannel(priv->htransport, MT_TRANSPORT_CHANNEL, + mt_transport_rx_callback, mt_transport_event_callback, + priv) + != eSpiTransportErrorNone) + return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel failed\n"); + + /* Optional latency optimization -- if the NRDY line has no usable + * IRQ, tick-driven pReadyRead() polling (2ms cadence, see the tick + * thread above) 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); + 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 { + 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)) + return dev_err_probe(dev, PTR_ERR(priv->tick_thread), + "failed to start tick thread\n"); + + if (spiTransportStart(priv->htransport) != eSpiTransportErrorNone) { + kthread_stop(priv->tick_thread); + return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); + } + + priv->misc.minor = MISC_DYNAMIC_MINOR; + priv->misc.name = "mt_spi_transport"; + priv->misc.fops = &mt_transport_misc_fops; + ret = misc_register(&priv->misc); + if (ret) { + spiTransportStop(priv->htransport); + kthread_stop(priv->tick_thread); + return dev_err_probe(dev, ret, "misc_register failed\n"); + } + + dev_info(dev, "MultiTracks SPI transport driver probed (Host role)\n"); + return 0; +} + +static void mt_transport_remove(struct spi_device *spi) +{ + struct mt_transport_priv *priv = spi_get_drvdata(spi); + + misc_deregister(&priv->misc); + spiTransportStop(priv->htransport); + kthread_stop(priv->tick_thread); +} + +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..f07b610311df7 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -0,0 +1,158 @@ +// 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 "spi_transport_hw_linux.h" + +/* + * 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 "mt-nss-gpios"/"mt-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()'s logical value always equals the literal pin level -- + * matching this whole contract's "true = pin HIGH" convention exactly. + */ + +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; + + if (ctx->pHw->pOnTransferComplete) + ctx->pHw->pOnTransferComplete(ctx->pHw->pCoreCtx, length); + + 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; + + 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); + 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(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; + + return gpiod_get_value(ctx->nrdy_gpiod) ? 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. First cut: log-only stub. Revisit + * once real EVK<->Disco hardware (MT-158682) shows whether spi-imx's + * internal recovery is sufficient on its own. + */ +static void mt_hw_abort(void *pContext) +{ + struct mt_transport_hw_ctx *ctx = pContext; + + dev_dbg(&ctx->spi->dev, "pAbort() called (stub -- see comment)\n"); +} + +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; + + 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; + bool high = gpiod_get_value(ctx->nrdy_gpiod) ? true : false; + + if (ctx->pHw->pOnReadyEvent) + ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, high); + + 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..22a7304eb8c3d --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h @@ -0,0 +1,62 @@ +/* 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 + * core/include/spi_transport/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 "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. + */ + struct spi_message msg; + struct spi_transfer xfer; + + /* 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..b7f09a31d27fe --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c @@ -0,0 +1,109 @@ +// 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 "spi_transport_os_linux.h" + +/* + * pTaskNotifyWait/pTaskNotifyGive: a single-waiter completion used as a + * repeating notify, not a one-shot. reinit_completion() runs *after* + * consuming the wait, not before -- reinit-before-wait would race a + * pTaskNotifyGive() landing between the previous tick() and this wait, + * silently swallowing the wakeup until the next timeout. There is exactly + * one waiter (the driver's tick kthread), so this ordering is safe. + */ +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)); + reinit_completion(&ctx->notify); +} + +/* 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, so a single saved-flags field in ctx is enough; + * this is not a general-purpose reentrant lock. + */ +static void mt_os_critical_enter(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + + spin_lock_irqsave(&ctx->crit_lock, ctx->crit_flags); +} + +static void mt_os_critical_exit(void *pContext) +{ + struct mt_transport_os_ctx *ctx = pContext; + + spin_unlock_irqrestore(&ctx->crit_lock, ctx->crit_flags); +} + +static void mt_os_log(void *pContext, const char *pFormat, va_list args) +{ + struct mt_transport_os_ctx *ctx = pContext; + struct va_format vaf = { .fmt = pFormat, .va = &args }; + + dev_dbg(ctx->dev, "%pV", &vaf); +} + +void 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); + + 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; +} 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..40fdd9dd06edb --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * spi_transport_os_linux.h - Linux kernel OS-adapter for the MultiTracks + * SPI transport core (see core/include/spi_transport/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; + unsigned long 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; no allocation here). + */ +void mt_transport_os_linux_init(struct mt_transport_os_ctx *ctx, struct device *dev, + trSpiTransportOs *pOs); + +#endif /* SPI_TRANSPORT_OS_LINUX_H */ From d3091e24bde33e809935f02198b4d9c4549365bf Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Sun, 16 Aug 2026 17:03:36 -0600 Subject: [PATCH 02/35] MT-158113: fix spi-mt-transport header lookup under out-of-tree kernel builds ccflags-y used -I\$(src)/core/include, which resolves against \$(objtree) under Yocto's O= kernel builds. These vendored core/ headers only exist in the source tree, so every TU failed with "spi_transport/spi_transport.h: No such file or directory". Prefix with $(srctree) to point at the actual source location. --- drivers/spi/spi-mt-transport/Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile index 4279935b9f6f8..71249013d4ce8 100644 --- a/drivers/spi/spi-mt-transport/Makefile +++ b/drivers/spi/spi-mt-transport/Makefile @@ -10,4 +10,7 @@ spi-mt-transport-y := \ spi_transport_hw_linux.o \ spi_mt_transport_drv.o -ccflags-y += -I$(src)/core/kernel-compat -I$(src)/core/include +# $(src) alone resolves against $(objtree) under Yocto's out-of-tree (O=) +# kernel builds; these headers live only in the source tree, so the +# $(srctree)/ prefix is required or the compile can't find them. +ccflags-y += -I$(srctree)/$(src)/core/kernel-compat -I$(srctree)/$(src)/core/include From 4797fd2ea9f352d79ac616fc6fe85fdea3414cf7 Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Sun, 16 Aug 2026 17:20:31 -0600 Subject: [PATCH 03/35] MT-158113: add stdbool.h/stdint.h/stdarg.h kernel-compat shims The vendored core also includes these standard hosted-C11 headers, none of which resolve under -nostdinc (this cross-compiler's own freestanding headers aren't on the search path either). Same pattern as the existing string.h shim: redirect to the kernel's own equivalents. --- .../spi/spi-mt-transport/core/PROVENANCE.md | 24 +++++++++++-------- .../core/kernel-compat/stdarg.h | 13 ++++++++++ .../core/kernel-compat/stdbool.h | 14 +++++++++++ .../core/kernel-compat/stdint.h | 14 +++++++++++ 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h create mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h create mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h diff --git a/drivers/spi/spi-mt-transport/core/PROVENANCE.md b/drivers/spi/spi-mt-transport/core/PROVENANCE.md index 93275ffa82400..5db9f25895c50 100644 --- a/drivers/spi/spi-mt-transport/core/PROVENANCE.md +++ b/drivers/spi/spi-mt-transport/core/PROVENANCE.md @@ -25,16 +25,20 @@ Do not hand-edit these files. If a change is needed, make it in the firmware repo's copy first, then re-vendor by re-copying and updating the commit SHA above. -The one exception: the vendored `.c` files' `#include ` (hosted -libc, used by their STM32/host-native builds) doesn't resolve under the -kernel's freestanding build -- ``/``/`` -already work directly (GCC provides these regardless of `-nostdinc`, and -other in-tree drivers in this repo already rely on that), but `` -is hosted-only. Rather than hand-edit the vendored files, `../Makefile` -adds `core/kernel-compat/` to the include path ahead of the vendored -headers -- it contains only a `string.h` shim that `#include -`. This is a build-time-only addition, not a change to any -vendored file; keep it that way on re-vendor. +The exception: the vendored files include standard hosted-C11 headers +(``, ``, ``, ``) for their +STM32/host-native builds. None of these resolve under the kernel's +`-nostdinc` build -- this cross-compiler's own freestanding headers aren't +on the search path either, and the kernel provides its own equivalents +instead (`linux/string.h`, `linux/types.h` + `linux/stddef.h`, +`linux/types.h`, `linux/stdarg.h` respectively). Rather than hand-edit the +vendored files, `../Makefile` adds `core/kernel-compat/` to the include path +ahead of the vendored headers (via `-I$(srctree)/$(src)/core/kernel-compat` +-- the `$(srctree)/` prefix is required since `$(src)` alone resolves +against `$(objtree)` under Yocto's out-of-tree kernel builds) -- it contains +one shim per standard header, each `#include`-ing the kernel equivalent. +This is a build-time-only addition, not a change to any vendored file; keep +it that way on re-vendor. To check for drift against the firmware repo: diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h new file mode 100644 index 0000000000000..2ed3d371624a2 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). + * + * The vendored 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$(srctree)/$(src)/core/kernel-compat), so it + * cannot shadow anywhere else in the kernel tree. + */ +#include diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h new file mode 100644 index 0000000000000..c38567c50adf5 --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). + * + * The vendored 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$(srctree)/$(src)/core/kernel-compat), + * so it cannot shadow anywhere else in the kernel tree. + */ +#include +#include diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h new file mode 100644 index 0000000000000..6d7a537f77a7c --- /dev/null +++ b/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). + * + * The vendored 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$(srctree)/$(src)/core/kernel-compat), so it + * cannot shadow anywhere else in the kernel tree. + */ +#include From bb0003d1e13876725f1fac25e950d7016ef3c6c7 Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Mon, 17 Aug 2026 11:04:27 -0600 Subject: [PATCH 04/35] MT-158113: fix pAbort() no-op race corrupting SPI core state on DMA timeout mt_hw_abort() was a log-only stub, but the core's disconnect watchdog (SPI_TRANSPORT_DISCONNECT_MS, 1500ms) fires before spi_imx_calculate_timeout()'s unconditional >=2000ms floor can possibly elapse. That let a retry reinitialize the shared ctx->msg/ctx->xfer via 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 and scatterlist state. Reproduced live on the EVK as a NULL deref in spi_imx_dma_transfer()'s sg_last(), triggered by repeated DMA TX timeouts with no peer wired up yet. pAbort() has no return value (must be safe to call whether or not anything is armed, and the core proceeds regardless), so the only correct fix is to make it actually block until the in-flight transfer's completion has fired (or spi-imx's own bounded recovery should have finished), via a completion tracked across mt_hw_transfer_start()/mt_hw_spi_complete()/mt_hw_abort(). --- .../spi-mt-transport/spi_transport_hw_linux.c | 76 ++++++++++++++++++- .../spi-mt-transport/spi_transport_hw_linux.h | 11 ++- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index f07b610311df7..bb4a6e5ee54cd 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -7,9 +7,21 @@ #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 @@ -31,6 +43,12 @@ 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; + /* Signal "msg/xfer no longer referenced by the SPI core" before + * notifying the core -- pNotify may wake the tick thread straight + * into a new pTransferStart(), which gates on this same completion. + */ + complete(&ctx->transferComplete); + if (ctx->pHw->pOnTransferComplete) ctx->pHw->pOnTransferComplete(ctx->pHw->pCoreCtx, length); @@ -44,6 +62,20 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p 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; @@ -56,6 +88,10 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p 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; } @@ -98,15 +134,43 @@ static bool mt_hw_ready_read(void *pContext) * 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. First cut: log-only stub. Revisit - * once real EVK<->Disco hardware (MT-158682) shows whether spi-imx's - * internal recovery is sufficient on its own. + * 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; - dev_dbg(&ctx->spi->dev, "pAbort() called (stub -- see comment)\n"); + 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, @@ -118,6 +182,10 @@ void mt_transport_hw_linux_init(struct mt_transport_hw_ctx *ctx, struct spi_devi 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)); diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h index 22a7304eb8c3d..91af58db8688b 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "spi_transport/spi_transport_hw.h" @@ -33,10 +34,18 @@ struct mt_transport_hw_ctx { trSpiTransportHw *pHw; /* Reused across every transfer -- the core's Host state machine only - * ever has one transfer in flight at a time. + * 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 From 650eefeb44c9e61047c8d90c27cc3feec19e2fcb Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Tue, 18 Aug 2026 11:19:58 -0600 Subject: [PATCH 05/35] MT-158113: log/count link events, add event_counters sysfs attribute mt_transport_event_callback() only logged via dev_dbg(), invisible in dmesg without dynamic debug explicitly enabled -- confirmed live during hardware fault-injection testing against a real STM32F723-DISCO Client: zero log output across ~50 real connect/disconnect cycles and dozens of DMA-failure/timeout injections, even though the callback was firing correctly the whole time. Add per-event atomic counters and a new /sys/.../event_counters attribute, field-named to match the STM32 Client harness's own [DBG] conn=/disc=/hdrCrc=/payCrc=/seq=/dmaFail=/dmaTo= counters (see the firmware repo's test/stm32-disco/app/), so a fault-injection run's peer-side verdict (per that harness's docs/TestPlan.md) can actually be read off this Host instead of only inferred from the absence of a crash. --- .../spi-mt-transport/spi_mt_transport_drv.c | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 581823d9d0372..119ef801fba12 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -31,6 +31,7 @@ #include #include #include +#include #include "spi_transport/spi_transport.h" #include "spi_transport/spi_transport_types.h" @@ -66,6 +67,27 @@ struct mt_transport_priv { uint8_t rx_buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; uint16_t rx_len; bool rx_valid; + + /* 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. + */ + 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; }; /* Wakes the tick kthread -- shared by the SPI-completion path and the @@ -105,7 +127,39 @@ static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEve { struct mt_transport_priv *priv = pContext; - dev_dbg(priv->dev, "link event: %d\n", (int)eEvent); + 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; + } } static int mt_transport_tick_thread_fn(void *data) @@ -223,8 +277,28 @@ static ssize_t link_state_show(struct device *dev, struct device_attribute *attr } 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); From 2b603614814a360263c7f36c55637149bbf785fa Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Tue, 18 Aug 2026 14:52:35 -0600 Subject: [PATCH 06/35] MT-158113: remove spi-mt-transport driver -- moved to an out-of-tree module Deletes drivers/spi/spi-mt-transport/ (the driver plus its vendored copy of the SPI transport core library) and the in-tree Kbuild/Kconfig hooks (drivers/spi/Makefile's obj-y line, drivers/spi/Kconfig's CONFIG_SPI_MT_TRANSPORT entry, imx_v8_defconfig's =m line). The driver now builds as an out-of-tree Yocto kernel module, sourced directly from the firmware repo's firmware-common/spi-transport/ (see imx8mmini-bb-evk's meta-mt-transport-evk/recipes-kernel/spi-mt-transport/ spi-mt-transport_git.bb). This removes the real protocol implementation from this more public-facing repo entirely, and removes the vendored-copy duplication (previously tracked via core/PROVENANCE.md) -- the core is now referenced in place from a single source of truth. This branch's only remaining unique content vs. develop is the one devicetree overlay (imx8mm-evk-spi-transport.dts) -- everything else here is the pre-existing, driver-independent v6.6.36-vs-v6.18 kernel-version retarget. --- arch/arm64/configs/imx_v8_defconfig | 1 - drivers/spi/Kconfig | 12 - drivers/spi/Makefile | 1 - drivers/spi/spi-mt-transport/Makefile | 16 - .../spi/spi-mt-transport/core/PROVENANCE.md | 52 - .../include/spi_transport/spi_transport.h | 213 --- .../spi_transport/spi_transport_channel.h | 139 -- .../spi_transport/spi_transport_frame.h | 77 - .../include/spi_transport/spi_transport_hw.h | 121 -- .../include/spi_transport/spi_transport_os.h | 77 - .../spi_transport/spi_transport_types.h | 133 -- .../core/kernel-compat/stdarg.h | 13 - .../core/kernel-compat/stdbool.h | 14 - .../core/kernel-compat/stdint.h | 14 - .../core/kernel-compat/string.h | 15 - .../spi/spi-mt-transport/core/spi_transport.c | 1466 ----------------- .../core/spi_transport_channel.c | 262 --- .../core/spi_transport_crc16.c | 67 - .../core/spi_transport_frame.c | 151 -- .../spi-mt-transport/core/spi_transport_hw.c | 31 - .../spi-mt-transport/spi_mt_transport_drv.c | 425 ----- .../spi-mt-transport/spi_transport_hw_linux.c | 226 --- .../spi-mt-transport/spi_transport_hw_linux.h | 71 - .../spi-mt-transport/spi_transport_os_linux.c | 109 -- .../spi-mt-transport/spi_transport_os_linux.h | 40 - 25 files changed, 3746 deletions(-) delete mode 100644 drivers/spi/spi-mt-transport/Makefile delete mode 100644 drivers/spi/spi-mt-transport/core/PROVENANCE.md delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h delete mode 100644 drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h delete mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h delete mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h delete mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h delete mode 100644 drivers/spi/spi-mt-transport/core/kernel-compat/string.h delete mode 100644 drivers/spi/spi-mt-transport/core/spi_transport.c delete mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_channel.c delete mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_crc16.c delete mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_frame.c delete mode 100644 drivers/spi/spi-mt-transport/core/spi_transport_hw.c delete mode 100644 drivers/spi/spi-mt-transport/spi_mt_transport_drv.c delete mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.c delete mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.h delete mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.c delete mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.h diff --git a/arch/arm64/configs/imx_v8_defconfig b/arch/arm64/configs/imx_v8_defconfig index 1a0933ecd39c8..6e3754f5be47a 100644 --- a/arch/arm64/configs/imx_v8_defconfig +++ b/arch/arm64/configs/imx_v8_defconfig @@ -454,7 +454,6 @@ CONFIG_SPI_FSL_DSPI=y CONFIG_SPI_PL022=y CONFIG_SPI_ROCKCHIP=y CONFIG_SPI_SPIDEV=y -CONFIG_SPI_MT_TRANSPORT=m CONFIG_SPI_SLAVE=y CONFIG_SPI_SLAVE_TIME=y CONFIG_SPI_SLAVE_SYSTEM_CONTROL=y diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index c685bb5b05642..3ce0fd5df8e9c 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1211,18 +1211,6 @@ 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)" - depends on ARCH_MXC || COMPILE_TEST - depends on GPIOLIB - 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 f01e36d13c69a..6af54842b9fa4 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -12,7 +12,6 @@ obj-$(CONFIG_SPI_MEM) += spi-mem.o obj-$(CONFIG_SPI_MUX) += spi-mux.o obj-$(CONFIG_SPI_SPIDEV) += spidev.o obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o -obj-$(CONFIG_SPI_MT_TRANSPORT) += spi-mt-transport/ # SPI master controller drivers (bus) obj-$(CONFIG_SPI_ALTERA) += spi-altera-platform.o diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile deleted file mode 100644 index 71249013d4ce8..0000000000000 --- a/drivers/spi/spi-mt-transport/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -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 live only in the source tree, so the -# $(srctree)/ prefix is required or the compile can't find them. -ccflags-y += -I$(srctree)/$(src)/core/kernel-compat -I$(srctree)/$(src)/core/include diff --git a/drivers/spi/spi-mt-transport/core/PROVENANCE.md b/drivers/spi/spi-mt-transport/core/PROVENANCE.md deleted file mode 100644 index 5db9f25895c50..0000000000000 --- a/drivers/spi/spi-mt-transport/core/PROVENANCE.md +++ /dev/null @@ -1,52 +0,0 @@ -# Provenance - -Vendored verbatim from `MultiTracksDotCom/firmware` @ -`9923f343acb6abbdc1a34e1f95803d313c52741c`, path -`firmware-common/spi-transport/{src,inc/spi_transport}`. - -Files vendored (portable core only — the `platform/host` and `platform/stm32` -adapter subdirectories from the source tree are NOT vendored; this port -provides its own `platform/linux-kernel`-equivalent adapter as sibling files -one directory up, see `spi_transport_os_linux.c`/`spi_transport_hw_linux.c`): - -- `spi_transport.c` -- `spi_transport_channel.c` -- `spi_transport_crc16.c` -- `spi_transport_frame.c` -- `spi_transport_hw.c` -- `include/spi_transport/spi_transport.h` -- `include/spi_transport/spi_transport_channel.h` -- `include/spi_transport/spi_transport_frame.h` -- `include/spi_transport/spi_transport_hw.h` -- `include/spi_transport/spi_transport_os.h` -- `include/spi_transport/spi_transport_types.h` - -Do not hand-edit these files. If a change is needed, make it in the firmware -repo's copy first, then re-vendor by re-copying and updating the commit SHA -above. - -The exception: the vendored files include standard hosted-C11 headers -(``, ``, ``, ``) for their -STM32/host-native builds. None of these resolve under the kernel's -`-nostdinc` build -- this cross-compiler's own freestanding headers aren't -on the search path either, and the kernel provides its own equivalents -instead (`linux/string.h`, `linux/types.h` + `linux/stddef.h`, -`linux/types.h`, `linux/stdarg.h` respectively). Rather than hand-edit the -vendored files, `../Makefile` adds `core/kernel-compat/` to the include path -ahead of the vendored headers (via `-I$(srctree)/$(src)/core/kernel-compat` --- the `$(srctree)/` prefix is required since `$(src)` alone resolves -against `$(objtree)` under Yocto's out-of-tree kernel builds) -- it contains -one shim per standard header, each `#include`-ing the kernel equivalent. -This is a build-time-only addition, not a change to any vendored file; keep -it that way on re-vendor. - -To check for drift against the firmware repo: - -``` -diff -rq linux-imx/drivers/spi/spi-mt-transport/core/ \ - <(cd firmware && git show 9923f343acb6abbdc1a34e1f95803d313c52741c:firmware-common/spi-transport) -``` - -(or, more practically, re-clone the firmware repo at the pinned SHA and diff -directory-to-directory, matching `src/*.c` -> `core/*.c` and -`inc/spi_transport/*.h` -> `core/include/spi_transport/*.h`.) diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h deleted file mode 100644 index d2b7d8ca1e7ef..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport.h +++ /dev/null @@ -1,213 +0,0 @@ -//****************************************************************************** -// @file : spi_transport.h -// @brief : Public API for the SPI transport -- init/start/stop, per-channel -// registration, send, and link-wide connect/disconnect/error -// events. See docs/ProtocolSpec.md and docs/ChannelApi.md. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_H -#define SPI_TRANSPORT_H - -#include -#include - -#include "spi_transport/spi_transport_hw.h" -#include "spi_transport/spi_transport_os.h" -#include "spi_transport/spi_transport_types.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -typedef void *thSpiTransport; - -typedef struct _trSpiTransportConfig -{ - teSpiTransportRole role; - const trSpiTransportOs *prOs; - trSpiTransportHw *prHw; /* non-const: spiTransportInit() wires its own - reverse callbacks into this same instance */ -} trSpiTransportConfig; - -/// @brief RX data callback: fired in task context (never from an ISR) when a -/// complete, reassembled message arrives on `channel`. `pBuffer` is -/// only valid for the duration of the callback. -typedef void (*tpSpiTransportRxCallback) (void *pContext, uint8_t channel, const uint8_t *pBuffer, - uint16_t length, uint8_t flags); - -/// @brief Link-wide event callback (connect/disconnect + transport errors, -/// see teSpiTransportEvent). Every registered channel receives the -/// same event sequence -- events are not per-channel. -typedef void (*tpSpiTransportEventCallback) (void *pContext, teSpiTransportEvent eEvent); - -/// @brief One-time global init. Not thread-safe against a concurrent second -/// call; call once at startup before spiTransportStart(). -teSpiTransportError spiTransportInit (const trSpiTransportConfig *prConfig, - thSpiTransport *phTransport); - -/// @brief Begin operation: Host starts issuing requests from IDLE; Client -/// starts waiting from IDLE. See docs/ProtocolSpec.md NSS/NRDY section. -teSpiTransportError spiTransportStart (thSpiTransport hTransport); - -/// @brief Stop operation. Safe to call at any point, including mid-transfer -/// (see the fault-injection "reset during transfer" mode in -/// docs/TestPlan.md) -- must leave no stuck DMA/GPIO state behind. -void spiTransportStop (thSpiTransport hTransport); - -/// @brief Register a channel for RX delivery and/or link events. Channel 0 is -/// transport-internal and always returns eSpiTransportErrorInvalidChannel. -/// pEventCallback may be NULL if this channel only cares about RX data. -teSpiTransportError spiTransportRegisterChannel (thSpiTransport hTransport, uint8_t channel, - tpSpiTransportRxCallback pRxCallback, - tpSpiTransportEventCallback pEventCallback, - void *pContext); - -teSpiTransportError spiTransportDeregisterChannel (thSpiTransport hTransport, uint8_t channel); - -/// @brief Queue `length` bytes for delivery on `channel`, segmenting across -/// multiple frames as needed (see SPI_TRANSPORT_FRAME_PAYLOAD_SIZE). -/// Channel 0 is transport-internal and always returns -/// eSpiTransportErrorInvalidChannel (same restriction as -/// spiTransportRegisterChannel()). Returns eSpiTransportErrorBusy if -/// the channel's previous message hasn't finished sending yet -- -/// there is no hidden queue depth beyond the single in-flight -/// message per channel. -teSpiTransportError spiTransportSend (thSpiTransport hTransport, uint8_t channel, - const uint8_t *pBuffer, uint16_t length, bool ackRequired); - -/// @brief Current link state, for polling use (most callers should prefer the -/// event callback instead). -teSpiTransportLinkState spiTransportGetLinkState (thSpiTransport hTransport); - -/// @brief Client only: enable/disable Client-initiated transfers (Client -/// arms its own DMA and drops NRDY while NSS is still high, to -/// solicit Host rather than waiting for Host's own request cycle -- -/// see docs/ProtocolSpec.md "Client-initiated transfers"). No-op on -/// Host. **Defaults to disabled** at spiTransportInit() -- today's -/// validated Host-only-initiates behavior is the standing default; -/// this must be explicitly opted into. Intended to be toggled at -/// runtime (e.g. a console command mirroring the existing -/// fault-injection commands) so a hardware regression can be backed -/// out instantly without a reflash, per the staged hardware rollout -/// plan in project memory. Safe to call at any time, including -/// mid-cycle -- takes effect on the next idle opportunity to -/// self-initiate; never interrupts an already-armed cycle. -void spiTransportSetClientSelfInitEnabled (thSpiTransport hTransport, bool enabled); - -/// @brief Current state of the flag set by -/// spiTransportSetClientSelfInitEnabled() -- for a console command's -/// own echo/status line, not required for the feature itself. -bool spiTransportIsClientSelfInitEnabled (thSpiTransport hTransport); - -/// @brief Drive the timer-based parts of the state machine (Host's -/// effectively-every-tick heartbeat/request-issue check, both -/// roles' 1.5s disconnect check). -/// The platform integration is responsible for calling this -/// periodically (STM32: from the transport task's own loop, e.g. -/// every 1-5ms; host-native tests: called directly by the test/loopback -/// driver). Safe to call more often than needed. -void spiTransportTick (thSpiTransport hTransport); - -/* Bring-up diagnostics -- temporary, see docs/TestPlan.md. Counts times the - * deferred-RX ring (see spi_transport.c) was still full when the ISR tried - * to write the next frame into it, i.e. the consuming task fell behind by - * more than SPI_TRANSPORT_RX_RING_DEPTH transfers and a frame was dropped. */ -uint32_t spiTransportDebugRxOverwriteCount (thSpiTransport hTransport); - -/// @brief Bring-up diagnostic -- temporary. pBuiltCount: how many times -/// buildOutgoingFrame() has run (every arm attempt, success or -/// failure). pAdvanceCount: how many times txSeq actually advanced -/// (onTransferComplete confirmed a physical transfer). pCurrentTxSeq: -/// the live txSeq value. Added to directly verify on real hardware -/// that the seq-peek/commit split holds -- built and advance should -/// only ever diverge by the count of abandoned/failed arm attempts -/// (the existing dmaFail counter), never more, and never less. -void spiTransportDebugTxSeqCounts (thSpiTransport hTransport, uint32_t *pBuiltCount, - uint32_t *pAdvanceCount, uint16_t *pCurrentTxSeq); - -/// @brief Bring-up diagnostic -- temporary. Client only: how many times -/// onSelectEvent()/clientArmSelfInitiateIfIdle() declined to ack or -/// self-arm specifically because the deferred-RX ring -/// (SPI_TRANSPORT_RX_RING_DEPTH) had no free slot -- real -/// backpressure (Host's existing ack-wait timeout retries later) -/// rather than accepting the transfer and dropping it at the ring. -/// Climbing steadily indicates sustained, not just bursty, load. -uint32_t spiTransportDebugRxRingFullRejectedCount (thSpiTransport hTransport); - -/// @brief Bring-up diagnostic -- temporary. Client only: how many real -/// NSS-falling edges onSelectEvent() has seen since boot, -/// split by outcome -- pRejectedCount (clientState wasn't Idle, so -/// the edge was ignored and NRDY was NOT touched) vs. pArmedCount -/// (clientState was Idle, pTransferStart() succeeded, and NRDY WAS -/// asserted low). Added for the DMA-wedge investigation, to -/// correlate a logic-analyzer-observed "NRDY never goes low" against -/// which path the core actually took. Either output param may be -/// NULL. Not per-instance (there's only ever one Client role active -/// per process) -- hTransport is accepted for API symmetry with the -/// other spiTransportDebugXxx calls but otherwise unused. -void spiTransportDebugClientArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, - uint32_t *pArmedCount); - -/// @brief Bring-up diagnostic -- temporary. Host-side mirror of -/// spiTransportDebugClientArmCounts(): does hostArmTransferIfAcked() -/// ever actually see hostState==eHostWaitingAck with NRDY low -/// (pArmedCount, proceeds to a real pTransferStart()) vs. everything -/// else (pRejectedCount -- wrong hostState, or NRDY still high). Not -/// per-instance; hTransport accepted for API symmetry only. -void spiTransportDebugHostArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, - uint32_t *pArmedCount); - -/// @brief Bring-up diagnostic -- temporary. Client-initiated-transfers -/// counters, landed alongside the feature itself (per the DMA-wedge -/// investigation's explicit lesson: visibility before the first -/// hardware test, not after). pAttemptCount: clientArmSelfInitiateIfIdle() -/// proceeded to a real pTransferStart(). pTimeoutCount: the self-arm -/// watchdog actually fired (pAbort() was called) -- climbing steadily -/// rather than rarely, especially alongside spiTransportDebugRxOverwriteCount()'s -/// sibling dmaCplt/[DIAG] counter going flat, is the direct -/// fingerprint of a stuck-peripheral regression. pHostArmedCount: -/// hostArmClientInitiatedIfIdle() succeeded (Host's side of the same -/// cycle). pRejectedSelfArmedCount: a split of -/// spiTransportDebugClientArmCounts()'s pRejectedCount, isolating -/// specifically "onSelectEvent rejected because clientState was -/// already eClientSelfArmed" (expected/healthy under self-init) from -/// everything else (still counted in that other call's -/// pRejectedCount, potentially a real problem). Any output param may -/// be NULL. Not per-instance; hTransport accepted for API symmetry -/// only. -void spiTransportDebugClientSelfInitCounts (thSpiTransport hTransport, uint32_t *pAttemptCount, - uint32_t *pTimeoutCount, uint32_t *pHostArmedCount, - uint32_t *pRejectedSelfArmedCount); - -/// @brief Bring-up diagnostic -- temporary. Details of the most recent -/// sequence-gap event and running totals by category: exact repeat -/// of the last-seen seq (pDuplicateCount), seq skipped forward -/// (pLossCount), or anything else, e.g. behind by more than one -/// (pOtherCount). Any output param may be NULL. -void spiTransportDebugLastGap (thSpiTransport hTransport, uint16_t *pExpected, uint16_t *pActual, - uint32_t *pDuplicateCount, uint32_t *pLossCount, - uint32_t *pOtherCount); - -/// @brief Bring-up diagnostic -- temporary. Our own epoch (generated once at -/// spiTransportInit()) and the peer's last-known epoch, to check -/// whether the epoch value is actually varying across reboots as -/// intended. Any output param may be NULL. -void spiTransportDebugEpoch (thSpiTransport hTransport, uint32_t *pOwnEpoch, uint32_t *pPeerEpoch, - bool *pPeerEpochKnown); - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h deleted file mode 100644 index 1698a26337e50..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_channel.h +++ /dev/null @@ -1,139 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_channel.h -// @brief : Internal channel registration table, RX reassembly, and -// per-channel single-slot TX queue. Used by spi_transport.c -// only -- not part of the public API (see spi_transport.h). -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_CHANNEL_H -#define SPI_TRANSPORT_CHANNEL_H - -#include -#include - -#include "spi_transport/spi_transport.h" -#include "spi_transport/spi_transport_types.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -typedef struct _trSpiTransportChannelTable trSpiTransportChannelTable; - -void spiTransportChannelTableInit (trSpiTransportChannelTable *prTable); - -teSpiTransportError spiTransportChannelRegister (trSpiTransportChannelTable *prTable, - uint8_t channel, - tpSpiTransportRxCallback pRxCallback, - tpSpiTransportEventCallback pEventCallback, - void *pContext); -teSpiTransportError spiTransportChannelDeregister (trSpiTransportChannelTable *prTable, - uint8_t channel); - -/// @brief Queue `length` bytes for `channel`. Buffer is borrowed (not -/// copied) -- caller must keep it valid until fully sent; returns -/// eSpiTransportErrorInvalidChannel for channel 0 (transport-internal, -/// see SPI_TRANSPORT_CHANNEL_CONTROL) or channel >= -/// SPI_TRANSPORT_CHANNELS_MAX, eSpiTransportErrorBusy if a previous -/// message on this channel hasn't finished, eSpiTransportErrorInvalidParam -/// if length exceeds SPI_TRANSPORT_CHANNEL_MESSAGE_MAX or pBuffer is -/// NULL with a nonzero length. -teSpiTransportError spiTransportChannelQueueTx (trSpiTransportChannelTable *prTable, - uint8_t channel, const uint8_t *pBuffer, - uint16_t length, bool ackRequired); - -/// @brief Round-robin pick of the next channel with TX data queued, starting -/// the search just after `startAfterChannel` (caller passes the last -/// channel serviced, or SPI_TRANSPORT_CHANNELS_MAX to start at 0). -/// On a hit, copies up to SPI_TRANSPORT_FRAME_PAYLOAD_SIZE bytes into -/// pOutPayload and sets *pOutChannel/*pOutFlags/*pOutLength. Returns -/// false if no channel has anything queued. -/// -/// Deliberately does NOT advance the channel's send offset or clear -/// txPending -- this only PEEKS the next chunk to build into a -/// frame. The caller must call spiTransportChannelCommitTx() once -/// the physical transfer carrying that chunk is confirmed to have -/// actually completed, not before. Committing at peek time (the -/// original design) meant any transfer that failed to arm or never -/// completed silently and permanently lost that chunk -- confirmed -/// live on hardware as a real, if hard to isolate, contributor to -/// the peer's sequence-gap counter (txSeq was already consumed for -/// a frame that never actually reached the peer). -bool spiTransportChannelNextTx (trSpiTransportChannelTable *prTable, uint8_t startAfterChannel, - uint8_t *pOutChannel, uint8_t *pOutPayload, uint16_t *pOutLength, - uint8_t *pOutFlags); - -/// @brief Confirm the chunk most recently returned by spiTransportChannelNextTx -/// for `channel` actually went out -- advances that channel's send -/// offset by `chunkLen` and, if `wasLastChunk`, clears txPending -/// (frees the single in-flight slot for a new spiTransportChannelQueueTx -/// call). Never call this for a transfer that failed to arm or never -/// completed -- simply not calling it is the correct "abandon this -/// attempt, retry the same unconsumed chunk next time" behavior. -void spiTransportChannelCommitTx (trSpiTransportChannelTable *prTable, uint8_t channel, - uint16_t chunkLen, bool wasLastChunk); - -/// @brief True if any channel has a TX message queued (used by the Host's -/// heartbeat check to decide whether it needs to issue a request -/// before the (effectively every-tick) backstop timer fires). -bool spiTransportChannelHasPending (const trSpiTransportChannelTable *prTable); - -/// @brief Reassemble an inbound chunk (per the START/END flags) and, once a -/// full message is complete, invoke the channel's RX callback in the -/// caller's context (task context -- never call from an ISR). -/// Silently drops+counts data for an unregistered channel. -void spiTransportChannelDispatchRx (trSpiTransportChannelTable *prTable, uint8_t channel, - const uint8_t *pPayload, uint16_t length, uint8_t flags); - -/// @brief Discard any in-progress RX reassembly and TX-in-flight state on -/// every channel -- called on every reconnect (epoch change), per -/// docs/ProtocolSpec.md's reconnect-baseline rule. -void spiTransportChannelResetAll (trSpiTransportChannelTable *prTable); - -/// @brief Notify every registered channel's event callback (may be NULL) of -/// a link-wide event. Not channel-specific -- every subscriber sees -/// the same sequence. -void spiTransportChannelNotifyEvent (trSpiTransportChannelTable *prTable, - teSpiTransportEvent eEvent); - -typedef struct _trSpiTransportChannelSlot -{ - bool registered; - tpSpiTransportRxCallback pRxCallback; - tpSpiTransportEventCallback pEventCallback; - void *pContext; - - /* TX (single in-flight message, borrowed buffer). */ - bool txPending; - const uint8_t *pTxBuffer; - uint16_t txLength; - uint16_t txOffset; - bool txAckRequired; - - /* RX reassembly (static buffer, no malloc). */ - bool rxInProgress; - uint8_t rxBuffer[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; - uint16_t rxOffset; -} trSpiTransportChannelSlot; - -struct _trSpiTransportChannelTable -{ - trSpiTransportChannelSlot slots[SPI_TRANSPORT_CHANNELS_MAX]; -}; - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_CHANNEL_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h deleted file mode 100644 index a0bb0ef059bf0..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_frame.h +++ /dev/null @@ -1,77 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_frame.h -// @brief : Wire-frame encode/decode and CRC-16/CCITT-FALSE declarations. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_FRAME_H -#define SPI_TRANSPORT_FRAME_H - -#include -#include - -#include "spi_transport/spi_transport_types.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/// @brief Decoded view of a frame -- never overlaid directly on the DMA -/// buffer (see docs/ProtocolSpec.md "Frame layout" for why: explicit -/// codec functions avoid padding/aliasing hazards across compilers). -typedef struct _trSpiTransportFrame -{ - uint16_t magic; - uint8_t version; - uint8_t channel; - uint16_t seq; - uint16_t ack; - uint8_t flags; - uint16_t payloadLength; - const uint8_t *pPayload; /* points into the caller-owned decode buffer, valid only - until the next spiTransportFrameDecode() call on that buffer */ -} trSpiTransportFrame; - -/// @brief CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect). -/// crc16Init() must be called once before any other CRC/frame call. -void crc16Init (void); -uint16_t crc16 (const uint8_t *pBuffer, uint16_t length); - -/// @brief Encode prFrame plus payload bytes [0, payloadLength) into pOutBuffer -/// (must be exactly SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes). Fills in -/// headerCrc/payloadCrc; prFrame->payloadLength bytes are copied from -/// pPayload, the rest of the payload region is zero-filled. -teSpiTransportError spiTransportFrameEncode (const trSpiTransportFrame *prFrame, - const uint8_t *pPayload, uint8_t *pOutBuffer); - -/// @brief Decode pInBuffer (exactly SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes) into -/// *prFrame. prFrame->pPayload is set to point inside pInBuffer. -/// Returns eSpiTransportErrorInvalidParam on a magic mismatch, -/// eSpiTransportErrorHardwareFailure on a header or payload CRC -/// mismatch (caller distinguishes via the two CRC-check functions -/// below if it needs to tell header-CRC apart from payload-CRC -/// failures for event reporting). -teSpiTransportError spiTransportFrameDecode (const uint8_t *pInBuffer, - trSpiTransportFrame *prFrame); - -/// @brief Standalone checks, used by spiTransportFrameDecode() internally and -/// exposed so callers (and unit tests) can distinguish which CRC -/// failed without re-decoding. -bool spiTransportFrameHeaderCrcOk (const uint8_t *pInBuffer); -bool spiTransportFramePayloadCrcOk (const uint8_t *pInBuffer); - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_FRAME_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h deleted file mode 100644 index acfc78f2262aa..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_hw.h +++ /dev/null @@ -1,121 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_hw.h -// @brief : Hardware-adapter contract the portable transport core depends -// on -- implemented once per platform (STM32 HAL SPI/DMA/GPIO, -// host-native loopback, future Linux kernel spi_sync()). -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_HW_H -#define SPI_TRANSPORT_HW_H - -#include -#include - -#include "spi_transport/spi_transport_types.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/// @brief Hardware-adapter function pointers. See docs/ProtocolSpec.md -/// "NSS/NRDY handshake state machine" for exactly when the core calls -/// each of these and what each signal means for Host vs. Client. -typedef struct _trSpiTransportHw -{ - /// @brief Begin a full-duplex DMA transfer of exactly - /// SPI_TRANSPORT_FRAME_TOTAL_SIZE bytes. Must return immediately; - /// completion is reported via the pOnTransferComplete callback - /// (see spiTransportHwSetCallbacks below). - /// On Client, this may now be called in two distinct contexts, - /// both of which the adapter must handle identically: after NSS - /// has already fallen (Host-initiated, the original contract -- - /// see pOnSelectEvent below) or *before* NSS falls, while - /// soliciting Host (Client-initiated -- see docs/ProtocolSpec.md - /// "Client-initiated transfers"). pAbort() below must be equally - /// effective recovering from either case -- this is the exact - /// contract an earlier, reverted attempt at Client-initiated - /// transfers got wrong (no bounded recovery existed for the - /// latter case at all, so a peer that never noticed/clocked the - /// solicitation left the peripheral stuck indefinitely). - teSpiTransportError (*pTransferStart) (void *pContext, const uint8_t *pTx, uint8_t *pRx, - uint16_t length); - - /// @brief Host only: drive the NSS line. Literal level convention used by - /// every function in this struct: true = pin driven/read HIGH, - /// false = LOW. NSS HIGH = idle, NSS LOW = request. No-op on - /// Client. - void (*pSelectAssert) (void *pContext, bool high); - - /// @brief Client only: drive the NRDY line. NRDY HIGH = idle/committed - /// (rest state), NRDY LOW = ack (see docs/ProtocolSpec.md - /// "NSS/NRDY handshake state machine"). No-op on Host. - void (*pReadyAssert) (void *pContext, bool high); - - /// @brief Host only: read the current NRDY input level (true = HIGH = - /// Client idle/available). Unused on Client. - bool (*pReadyRead) (void *pContext); - - /// @brief Force the underlying transfer engine back to idle/ready when - /// the core detects a pTransferStart() it armed never completed - /// (peer never clocked it, or vanished mid-transfer) -- see - /// doDisconnect() in spi_transport.c. Resetting the core's own - /// hostState/clientState bookkeeping isn't sufficient on real - /// hardware: a DMA transfer genuinely armed via - /// HAL_SPI_TransmitReceive_DMA and never clocked to completion - /// leaves the SPI peripheral itself latched busy, so every - /// subsequent real pTransferStart() call fails forever until this - /// is called. Must be safe to call even when nothing is armed. - void (*pAbort) (void *pContext); - - void *pContext; - - /* Core-side callbacks, filled in by spiTransportHwSetCallbacks() below. - * An adapter implementation invokes these (via its own held pointer to - * this same trSpiTransportHw instance) when the corresponding hardware - * event actually happens -- e.g. from a DMA-complete ISR, the NSS-EXTI - * ISR, or (in the null/loopback adapter) a direct synchronous call. */ - void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length); - void (*pOnSelectEvent) (void *pCoreCtx, bool asserted); - void (*pOnReadyEvent) (void *pCoreCtx, bool asserted); - void (*pOnClockStart) (void *pCoreCtx); - void *pCoreCtx; -} trSpiTransportHw; - -/// @brief Register the core's callbacks with a HW adapter instance. Called -/// once during spiTransportInit(), before spiTransportStart(). -/// @param pOnTransferComplete Fired when a pTransferStart() DMA transfer -/// finishes (success or hardware failure -- see length==0 convention -/// in the adapter implementation notes). -/// @param pOnSelectEvent Client only: fired on the NSS-EXTI edge (external to -/// HAL, per the board's requirement) -- asserted=true is the falling -/// edge (Host's request). -/// @param pOnReadyEvent Host only: fired on an NRDY level change, if the -/// adapter implements it via EXTI rather than pure polling. May be -/// left unused (NULL-checked by the caller) if the adapter only polls. -/// @param pOnClockStart Client only: fired the instant real clocking begins -/// (RX FIFO start indicator), distinct from and earlier than -/// pOnTransferComplete -- this is what the core uses to decide when -/// to re-assert NRDY high (the "committed, in-flight" latch), per -/// docs/ProtocolSpec.md. -void spiTransportHwSetCallbacks (trSpiTransportHw *prHw, - void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length), - void (*pOnSelectEvent) (void *pCoreCtx, bool asserted), - void (*pOnReadyEvent) (void *pCoreCtx, bool asserted), - void (*pOnClockStart) (void *pCoreCtx), void *pCoreCtx); - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_HW_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h deleted file mode 100644 index fe5673ac56fba..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_os.h +++ /dev/null @@ -1,77 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_os.h -// @brief : OS-adapter contract the portable transport core depends on -- -// implemented once per platform (FreeRTOS, host-native stub, -// future Linux kernel). -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_OS_H -#define SPI_TRANSPORT_OS_H - -#include -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - -/// @brief OS-adapter function pointers. The core never calls an RTOS/kernel -/// primitive directly -- every such call goes through this struct, so -/// the same core builds against FreeRTOS, a host-native stub, or (in -/// the future) Linux kernel primitives without core changes. -typedef struct _trSpiTransportOs -{ - /// @brief Block the calling task until the core's completion notification - /// is given (pTaskNotifyGive) or timeoutMs elapses. - void (*pTaskNotifyWait) (void *pContext, uint32_t timeoutMs); - - /// @brief Wake the task blocked in pTaskNotifyWait. Must be safe to call - /// from interrupt context. - void (*pTaskNotifyGive) (void *pContext); - - /// @brief Monotonic milliseconds since boot. - uint32_t (*pTickGet) (void *pContext); - - /// @brief Enter/leave the core's internal critical section (registration - /// table, per-channel TX slots). Not used from interrupt context. - void (*pMutexLock) (void *pContext); - void (*pMutexUnlock) (void *pContext); - - /// @brief Enter/leave a short task-vs-interrupt critical section -- - /// unlike pMutexLock/Unlock, this MUST be safe to call from (and - /// against) interrupt context, since it guards Host's - /// check-then-claim state transitions (hostArmTransferIfAcked, - /// hostIssueRequestIfReady) shared between spiTransportTick()'s - /// tick-driven poll and the NRDY-EXTI interrupt-driven path - /// (spiTransportHwStm32OnNrdyExti). Confirmed live on hardware: - /// without this, the two paths can race on the same - /// check-then-act, both proceed, and Host silently double-sends - /// (txSeq advances twice for one physical transfer), which the - /// Client sees as a spurious sequence-gap. Keep the guarded - /// region tiny -- a few field reads/writes only, never a HAL - /// DMA/GPIO call that could block. - void (*pCriticalEnter) (void *pContext); - void (*pCriticalExit) (void *pContext); - - /// @brief Optional diagnostic log sink. May be NULL. - void (*pLog) (void *pContext, const char *pFormat, va_list args); - - void *pContext; -} trSpiTransportOs; - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_OS_H */ diff --git a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h b/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h deleted file mode 100644 index f47365a2ac1dc..0000000000000 --- a/drivers/spi/spi-mt-transport/core/include/spi_transport/spi_transport_types.h +++ /dev/null @@ -1,133 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_types.h -// @brief : Wire-format constants, flags, error codes and role/state enums -// for the SPI transport. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#ifndef SPI_TRANSPORT_TYPES_H -#define SPI_TRANSPORT_TYPES_H - -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - -/* Frame layout (see docs/ProtocolSpec.md "Frame layout"): - * offset size field - * 0 2 magic (0x5AA5, little-endian) - * 2 1 version - * 3 1 channel - * 4 2 seq - * 6 2 ack - * 8 1 flags - * 9 1 reserved - * 10 2 payloadLength - * 12 2 headerCrc (CRC-16/CCITT-FALSE over bytes 0-11) - * 14 112 payload - * 126 2 payloadCrc (CRC-16/CCITT-FALSE over first payloadLength payload bytes) - */ -#define SPI_TRANSPORT_FRAME_MAGIC (0x5AA5u) -#define SPI_TRANSPORT_FRAME_VERSION (1u) - -#define SPI_TRANSPORT_FRAME_HEADER_SIZE (14u) -#define SPI_TRANSPORT_FRAME_PAYLOAD_SIZE (112u) -#define SPI_TRANSPORT_FRAME_CRC_SIZE (2u) -#define SPI_TRANSPORT_FRAME_TOTAL_SIZE \ - (SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE \ - + SPI_TRANSPORT_FRAME_CRC_SIZE) - -#define SPI_TRANSPORT_CHANNEL_CONTROL (0u) -#define SPI_TRANSPORT_CHANNELS_MAX (8u) - -/* Max reassembled message size per channel (static buffer, no malloc). A - * message larger than this cannot be sent/received -- spiTransportSend() - * returns eSpiTransportErrorInvalidParam and an oversized inbound - * (START-without-END-by-this-size) is dropped and counted, not delivered. */ -#define SPI_TRANSPORT_CHANNEL_MESSAGE_MAX (512u) - -/* Flags bitfield (offset 8). Bits 6-7 reserved, must be 0 on send. */ -#define SPI_TRANSPORT_FLAG_START (0x01u) -#define SPI_TRANSPORT_FLAG_END (0x02u) -#define SPI_TRANSPORT_FLAG_ACK_REQUIRED (0x04u) -#define SPI_TRANSPORT_FLAG_ERROR (0x08u) -#define SPI_TRANSPORT_FLAG_RESET (0x10u) -#define SPI_TRANSPORT_FLAG_FILLER (0x20u) - -/* Channel-0 control message types. */ -#define SPI_TRANSPORT_CTRL_HELLO (1u) -#define SPI_TRANSPORT_CTRL_HELLO_ACK (2u) - -/* Timing model (docs/ProtocolSpec.md "Connected/disconnected timing model"). - * Host re-issues a request itself, without waiting on this backstop, - * whenever it has anything queued -- this heartbeat only governs how long - * Host will go with NOTHING queued before issuing a request anyway (a - * FILLER if still nothing to send by then), so real traffic is never - * paced by this value. Set to effectively "every tick" (1ms) rather than - * 500ms: Host must not sit idle for any noticeable stretch, even with - * zero application traffic -- a fast, continuous heartbeat is cheap (a - * FILLER frame) and keeps the link's actual round-trip latency close to - * the physical transfer time instead of up to half a second. - * SPI_TRANSPORT_DISCONNECT_MS is intentionally NOT scaled down to match -- - * it's a coarse "peer is genuinely gone" bound, not a heartbeat multiple, - * and 1.5s remains the right tolerance for that regardless of how often - * Host pings within it. */ -#define SPI_TRANSPORT_HEARTBEAT_MS (1u) -#define SPI_TRANSPORT_DISCONNECT_MS (1500u) - -typedef enum -{ - eSpiTransportErrorNone = 0, - eSpiTransportErrorInvalidParam, - eSpiTransportErrorInvalidChannel, - eSpiTransportErrorAlreadyRegistered, - eSpiTransportErrorNotRegistered, - eSpiTransportErrorBusy, - eSpiTransportErrorNotConnected, - eSpiTransportErrorHardwareFailure, - eSpiTransportErrorTimeout, -} teSpiTransportError; - -typedef enum -{ - eSpiTransportRoleHost = 0, - eSpiTransportRoleClient = 1, -} teSpiTransportRole; - -typedef enum -{ - eSpiTransportLinkDisconnected = 0, - eSpiTransportLinkHandshaking, - eSpiTransportLinkConnected, -} teSpiTransportLinkState; - -/* Link-wide events, delivered to every registered channel's event callback - * (see spi_transport.h) -- connect/disconnect state changes and transport - * errors are visible to every subscriber, not just RX data. */ -typedef enum -{ - eSpiTransportEventConnected = 0, - eSpiTransportEventDisconnected, - eSpiTransportEventErrorHeaderCrc, - eSpiTransportEventErrorPayloadCrc, - eSpiTransportEventErrorSequenceGap, - eSpiTransportEventErrorDmaFailure, - eSpiTransportEventErrorDmaTimeout, -} teSpiTransportEvent; - -#ifdef __cplusplus -} -#endif - -#endif /* SPI_TRANSPORT_TYPES_H */ diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h deleted file mode 100644 index 2ed3d371624a2..0000000000000 --- a/drivers/spi/spi-mt-transport/core/kernel-compat/stdarg.h +++ /dev/null @@ -1,13 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -/* - * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). - * - * The vendored 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$(srctree)/$(src)/core/kernel-compat), so it - * cannot shadow anywhere else in the kernel tree. - */ -#include diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h deleted file mode 100644 index c38567c50adf5..0000000000000 --- a/drivers/spi/spi-mt-transport/core/kernel-compat/stdbool.h +++ /dev/null @@ -1,14 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -/* - * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). - * - * The vendored 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$(srctree)/$(src)/core/kernel-compat), - * so it cannot shadow anywhere else in the kernel tree. - */ -#include -#include diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h b/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h deleted file mode 100644 index 6d7a537f77a7c..0000000000000 --- a/drivers/spi/spi-mt-transport/core/kernel-compat/stdint.h +++ /dev/null @@ -1,14 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -/* - * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). - * - * The vendored 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$(srctree)/$(src)/core/kernel-compat), so it - * cannot shadow anywhere else in the kernel tree. - */ -#include diff --git a/drivers/spi/spi-mt-transport/core/kernel-compat/string.h b/drivers/spi/spi-mt-transport/core/kernel-compat/string.h deleted file mode 100644 index 41b3a49d3f0f9..0000000000000 --- a/drivers/spi/spi-mt-transport/core/kernel-compat/string.h +++ /dev/null @@ -1,15 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -/* - * Compat shim only -- NOT part of the vendored core (see ../PROVENANCE.md). - * - * The vendored 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 vendored .c files - * themselves. Only reached via this module's own Makefile - * (ccflags-y += -I$(src)/core/kernel-compat), so it cannot shadow - * anywhere else in the kernel tree. - */ -#include diff --git a/drivers/spi/spi-mt-transport/core/spi_transport.c b/drivers/spi/spi-mt-transport/core/spi_transport.c deleted file mode 100644 index 36a7cf05bcf34..0000000000000 --- a/drivers/spi/spi-mt-transport/core/spi_transport.c +++ /dev/null @@ -1,1466 +0,0 @@ -//****************************************************************************** -// @file : spi_transport.c -// @brief : Portable transport core -- Host/Client NSS/NRDY state machine, -// channel-0 handshake/reconnect-baseline protocol, ~1ms -// heartbeat / 1.5s disconnect timers. See docs/ProtocolSpec.md. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#include "spi_transport/spi_transport.h" - -#include - -#include "spi_transport/spi_transport_channel.h" -#include "spi_transport/spi_transport_frame.h" - -/* Internal watchdog: how long the Host waits for the Client's NRDY ack - * before giving up on this one request attempt. Not part of the wire - * protocol -- purely a local recovery bound, well under the 1.5s disconnect - * timer so a single wedged attempt doesn't have to wait that long to retry. */ -#define SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS (100u) - -/* Client-side mirror of the above, for self-initiated transfers (see - * eClientSelfArmed): how long Client will sit with its own DMA armed and - * NRDY low, NSS still high, before concluding Host isn't going to notice - * this solicitation. Must stay comfortably above - * SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS + one typical transfer (so a Host - * that's genuinely just running its own request cycle isn't mistaken for - * "not noticing"), and comfortably below SPI_TRANSPORT_DISCONNECT_MS (this - * is a per-attempt recovery, not a disconnect) -- see docs/ProtocolSpec.md - * "Client-initiated transfers" for the full reasoning. */ -#define SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS (250u) - -/* Depth of the deferred-RX ring (see trSpiTransportInstance's rxRingBuffer - * comment) -- how many completed-but-not-yet-task-processed frames can - * queue up before the ISR starts dropping new arrivals. */ -#define SPI_TRANSPORT_RX_RING_DEPTH (3u) - -/* Bring-up diagnostics -- temporary, see docs/TestPlan.md. Direct evidence - * for the DMA-wedge investigation: distinguishes onSelectEvent() rejecting a - * real NSS-falling edge because clientState wasn't Idle from it actually - * proceeding to assert NRDY low, so a logic-analyzer-observed "NRDY never - * goes low" can be correlated against which path the core actually took. */ -volatile uint32_t gDiagOnSelectRejectedCount = 0; -volatile uint32_t gDiagOnSelectArmedCount = 0; - -/* Mirrors the pair above, Host side: does hostArmTransferIfAcked() ever - * actually see hostState==eHostWaitingAck && NRDY low (pRejectedCount is - * everything else -- wrong hostState, or NRDY still high) and proceed to a - * real pTransferStart() attempt (pArmedCount)? A failed attempt is already - * visible via the existing dmaFail/[DBG] counter, so not duplicated here. */ -volatile uint32_t gDiagHostArmRejectedCount = 0; -volatile uint32_t gDiagHostArmArmedCount = 0; - -/* Client self-initiation diagnostics -- added alongside the feature itself - * (not retrofitted after a hardware failure like the pairs above), per the - * explicit lesson from the DMA-wedge investigation: land the visibility - * BEFORE the first hardware test, not after. gDiagClientSelfArmTimeoutCount - * climbing steadily (not just occasionally) alongside the existing - * gDiagDmaCompleteCount/dmaCplt counter going flat is the direct - * fingerprint of a stuck-peripheral regression -- visible within one - * [DIAG] print interval instead of requiring a fresh logic-analyzer - * capture to diagnose. */ -volatile uint32_t gDiagClientSelfArmAttemptCount = 0; /* clientArmSelfInitiateIfIdle attempted */ -volatile uint32_t gDiagClientSelfArmTimeoutCount = 0; /* watchdog actually fired (pAbort called) */ -volatile uint32_t gDiagHostClientInitArmedCount = 0; /* hostArmClientInitiatedIfIdle succeeded */ -volatile uint32_t gDiagOnSelectRejectedSelfArmedCount = 0; /* split of - gDiagOnSelectRejectedCount: specifically "rejected because clientState - was eClientSelfArmed" (expected/healthy under self-init) vs. anything - else (still counted in gDiagOnSelectRejectedCount, potentially a real - problem). */ - -/* TX-side seq-consumption diagnostics -- added to directly verify the - * buildOutgoingFrame()/onTransferComplete() seq-peek/commit split (see - * buildOutgoingFrame's own comment) actually holds on real hardware, not - * just in native tests: gDiagTxFrameBuiltCount increments once per - * buildOutgoingFrame() call (every arm attempt, success or failure); - * gDiagTxSeqAdvanceCount increments once per confirmed txSeq++ in - * onTransferComplete(). The two are expected to diverge by exactly the - * count of abandoned/failed arm attempts (dmaFail) -- built-advance should - * never exceed that, and advance should never exceed built. See - * spiTransportDebugTxSeqCounts(). */ -volatile uint32_t gDiagTxFrameBuiltCount = 0; -volatile uint32_t gDiagTxSeqAdvanceCount = 0; - -/* RX-ring backpressure diagnostic -- Client only. Counts how many times - * onSelectEvent()/clientArmSelfInitiateIfIdle() declined to ack/self-arm - * specifically because the deferred-RX ring had no free slot (see - * SPI_TRANSPORT_RX_RING_DEPTH), as opposed to being busy for some other - * reason. Climbing steadily means the ring is genuinely undersized for the - * sustained load, not just absorbing rare bursts -- see - * spiTransportDebugRxRingFullRejectedCount(). */ -volatile uint32_t gDiagRxRingFullRejectedCount = 0; - -/* Control-channel (channel 0) message body: {type, role, epoch(LE32), startSeq(LE16)}. */ -#define SPI_TRANSPORT_CTRL_BODY_SIZE (8u) - -typedef enum -{ - eHostIdle = 0, - eHostWaitingAck, - eHostTransferring, -} teHostState; - -typedef enum -{ - eClientIdle = 0, - /* Self-initiated: Client armed its own DMA and dropped NRDY while NSS is - * still HIGH, soliciting Host -- distinct from eClientArmed (which is - * always a reaction to Host's own NSS-falling edge). Bounded by - * SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS (see clientServiceTick()); - * onClockStart() moves this on to eClientTransferring exactly like - * eClientArmed, since once real clocking starts it no longer matters - * which side asked for it. */ - eClientSelfArmed, - eClientArmed, - eClientTransferring, -} teClientState; - -typedef struct _trSpiTransportInstance -{ - bool inUse; - teSpiTransportRole role; - const trSpiTransportOs *prOs; - trSpiTransportHw *prHw; - - trSpiTransportChannelTable channels; - - teSpiTransportLinkState linkState; - uint32_t epoch; - uint32_t peerEpoch; - bool peerEpochKnown; - bool helloSentThisEpoch; - bool pendingHello; - bool pendingHelloAck; - uint16_t startSeqFromPeer; - - uint16_t txSeq; - uint16_t rxLastSeq; - bool rxSeqBaselineArmed; - - uint8_t lastTxChannel; - - uint32_t lastSendTickMs; - uint32_t lastRecvTickMs; - - teHostState hostState; - uint32_t hostWaitStartMs; - - teClientState clientState; - uint32_t clientSelfArmStartMs; /* set when clientState becomes eClientSelfArmed */ - bool clientSelfInitEnabled; /* see spiTransportSetClientSelfInitEnabled() -- defaults false */ - - /* Deferred TX-commit bookkeeping: buildOutgoingFrame() only PEEKS a - * channel's next chunk (see spiTransportChannelNextTx()'s updated - * contract) -- these record which chunk was peeked into the - * currently-in-flight frame, so the transfer-complete confirmation - * (onTransferComplete) can commit it, or a failed/abandoned attempt can - * simply leave it uncommitted for a clean retry. */ - bool txCommitPending; - uint8_t txCommitChannel; - uint16_t txCommitChunkLen; - bool txCommitWasLastChunk; - - uint8_t txBuffer[SPI_TRANSPORT_FRAME_TOTAL_SIZE]; - uint8_t rxBuffer[SPI_TRANSPORT_FRAME_TOTAL_SIZE]; - - /* Deferred-processing handoff: the ISR-context transfer-complete handler - * (see onTransferComplete) only snapshots rxBuffer here and notifies the - * task -- it must NOT call processIncomingFrame() itself, since that - * invokes user RX/event callbacks, which docs/ProtocolSpec.md requires - * to run in task context only. spiTransportTick() drains this. - * - * Single-producer (ISR)/single-consumer (task) ring, depth - * SPI_TRANSPORT_RX_RING_DEPTH: rxRingHead is written only by the - * producer, rxRingTail only by the consumer, so each side can read the - * other's counter without a lock. Index into rxRingBuffer is - * (counter % SPI_TRANSPORT_RX_RING_DEPTH); both counters are - * free-running (never reduced mod the depth themselves), so unsigned - * wraparound subtraction (head - tail) always gives the correct - * occupied-slot count. Replaces an earlier single-slot design (a - * depth-2 ring was tried once before to close this same starved-task - * window, reordered frames on real hardware for a reason never - * isolated, and was reverted back to the single slot rather than ship - * that). This version avoids the most likely cause of that class of - * bug by construction: on a full ring, the ISR drops the *new* arrival - * (see the ring-full branch in onTransferComplete) rather than - * overwriting the oldest slot in place, which would race the task - * mid-read of that exact slot -- the ISR can preempt the task at any - * point, so slot ownership must never be ambiguous. See - * test/host/test_rx_ring.c for the back-to-back-completion/FIFO-order - * coverage this earlier attempt apparently didn't have. */ - uint8_t rxRingBuffer[SPI_TRANSPORT_RX_RING_DEPTH][SPI_TRANSPORT_FRAME_TOTAL_SIZE]; - volatile uint32_t rxRingHead; - volatile uint32_t rxRingTail; - - /* Bring-up diagnostic -- temporary, see spiTransportDebugRxOverwriteCount(). - * Now counts "ring was full, newest arrival dropped" rather than - * "existing slot overwritten", but is exactly as rare/meaningful a - * backpressure signal as before. */ - volatile uint32_t rxOverwriteCount; - - /* Bring-up diagnostic -- temporary, see spiTransportDebugLastGap(). Only - * ever written from task context (processIncomingFrame), so no - * volatile/critical-section needed here. */ - uint16_t lastGapExpected; - uint16_t lastGapActual; - uint32_t gapDuplicateCount; /* frame.seq == rxLastSeq (exact repeat) */ - uint32_t gapLossCount; /* frame.seq ahead of expected (skipped) */ - uint32_t gapOtherCount; /* anything else (behind by >1, wrap, etc.) */ -} trSpiTransportInstance; - -/* Static pool, no malloc anywhere (see docs/ProtocolSpec.md "Channel - * model"). Real firmware only ever uses one slot (one physical link per - * board); a size of 2 is what lets host-native tests run a Host instance - * and a Client instance simultaneously in the same process against the - * null-loopback HW adapter (see platform/host/spi_transport_hw_null.c). */ -#define SPI_TRANSPORT_INSTANCES_MAX (2u) -static trSpiTransportInstance gInstances[SPI_TRANSPORT_INSTANCES_MAX]; - -static void onTransferComplete (void *pCoreCtx, uint16_t length); -static void onSelectEvent (void *pCoreCtx, bool asserted); -static void onReadyEvent (void *pCoreCtx, bool asserted); -static void onClockStart (void *pCoreCtx); - -static void -putU16 (uint8_t *pOut, uint16_t value) -{ - pOut[0] = (uint8_t)(value & 0xFFu); - pOut[1] = (uint8_t)((value >> 8) & 0xFFu); -} -static uint16_t -getU16 (const uint8_t *pIn) -{ - return (uint16_t)((uint16_t)pIn[0] | ((uint16_t)pIn[1] << 8)); -} -static void -putU32 (uint8_t *pOut, uint32_t value) -{ - pOut[0] = (uint8_t)(value & 0xFFu); - pOut[1] = (uint8_t)((value >> 8) & 0xFFu); - pOut[2] = (uint8_t)((value >> 16) & 0xFFu); - pOut[3] = (uint8_t)((value >> 24) & 0xFFu); -} -static uint32_t -getU32 (const uint8_t *pIn) -{ - return (uint32_t)pIn[0] | ((uint32_t)pIn[1] << 8) | ((uint32_t)pIn[2] << 16) - | ((uint32_t)pIn[3] << 24); -} - -static void -updateLinkState (trSpiTransportInstance *pInst) -{ - bool shouldBeConnected = pInst->peerEpochKnown && pInst->helloSentThisEpoch; - - if (shouldBeConnected && (pInst->linkState != eSpiTransportLinkConnected)) - { - pInst->linkState = eSpiTransportLinkConnected; - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventConnected); - } -} - -static void -resetForHandshake (trSpiTransportInstance *pInst) -{ - pInst->linkState = eSpiTransportLinkHandshaking; - pInst->helloSentThisEpoch = false; - pInst->pendingHello = true; - pInst->rxSeqBaselineArmed = false; - spiTransportChannelResetAll (&pInst->channels); - - /* Any commit still deferred against the (now-wiped) channel table would - * be meaningless -- drop it rather than risk a later onTransferComplete - * committing stale channel/offset values into a freshly reset slot. */ - pInst->txCommitPending = false; -} - -static void -handleHandshake (trSpiTransportInstance *pInst, uint8_t msgType, uint32_t peerEpoch, - uint16_t peerStartSeq) -{ - bool epochChanged = (!pInst->peerEpochKnown) || (peerEpoch != pInst->peerEpoch); - - /* A peer only ever sends a fresh HELLO (as opposed to a HELLO_ACK) - * right after its own Start()/reboot or its own detected disconnect -- - * see buildOutgoingFrame's pendingHello handling, which is never - * re-armed spontaneously mid-connection. So an incoming HELLO while we - * still think we're Connected is itself sufficient proof the peer - * restarted, and must be treated as a reconnect even when epochChanged - * is false. Confirmed live on hardware that epoch alone is NOT reliable - * enough on its own to catch this: epoch is a boot-time RTOS tick - * snapshot taken in spiTransportInit(), and on this harness's Client - * role that call consistently lands before the scheduler's first tick - * -- every single Client reboot produces epoch=0, a guaranteed - * collision with whatever Host already had stored, not a rare - * probabilistic one. Without this OR clause, Host silently never - * re-baselines against a rebooted Client: no Disconnected event, no - * rxSeqBaselineArmed reset (each such miss produces one real spurious - * sequence-gap event where the peer's seq legitimately restarted from - * ~0), and recovery depends entirely on the far coarser, timing- - * dependent 1.5s silence timeout instead. */ - bool peerRestarted = epochChanged - || ((msgType == SPI_TRANSPORT_CTRL_HELLO) - && (pInst->linkState == eSpiTransportLinkConnected)); - - if (peerRestarted) - { - bool wasConnected = (pInst->linkState == eSpiTransportLinkConnected); - - pInst->peerEpoch = peerEpoch; - pInst->peerEpochKnown = true; - pInst->startSeqFromPeer = peerStartSeq; - resetForHandshake (pInst); - - if (wasConnected) - { - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventDisconnected); - } - } - - if (msgType == SPI_TRANSPORT_CTRL_HELLO) - { - pInst->pendingHelloAck = true; - } - - updateLinkState (pInst); -} - -static void -doDisconnect (trSpiTransportInstance *pInst, uint32_t now) -{ - bool wasConnected = (pInst->linkState == eSpiTransportLinkConnected); - - pInst->peerEpochKnown = false; - pInst->epoch = pInst->prOs->pTickGet (pInst->prOs->pContext); - resetForHandshake (pInst); - pInst->lastRecvTickMs = now; - - /* Un-wedge the NSS/NRDY handshake, not just the protocol state above: - * confirmed live on hardware that if the peer vanishes (e.g. reboots) - * mid-transfer, the surviving side's hostState/clientState is left - * stuck in a Waiting/Armed/Transferring state forever -- its DMA is - * waiting on clock edges or an ack that will never arrive, and nothing - * else ever moves it back to Idle. That left NRDY (Client) or NSS - * (Host) permanently latched, wedging the physical link even though - * the 1.5s disconnect timer above correctly recovered the protocol - * layer. Force both back to Idle and release the line here as the - * disconnect timer's backstop -- Host also has its own tighter 100ms - * per-attempt watchdog (see hostServiceTick), this is what actually - * covers Client, which has no per-attempt watchdog of its own. */ - if (pInst->role == eSpiTransportRoleHost) - { - /* Only eHostTransferring means a physical transfer was actually - * armed and never confirmed complete -- eHostWaitingAck gets - * its own 100ms watchdog well before this 1.5s path could ever - * see it stuck. That distinction is what makes this a DMA - * timeout specifically, not just "the link is down" (which - * eSpiTransportEventDisconnected below already covers). */ - bool wasArmed = (pInst->hostState == eHostTransferring); - if (wasArmed) - { - /* Abort the real transfer BEFORE flipping hostState back - * to Idle below -- hostIssueRequestIfReady/ - * hostArmTransferIfAcked gate a new real - * pTransferStart() on hostState, so as long as it still - * reads non-Idle here, a concurrent tick/EXTI can't slip - * a new attempt in against a peripheral that's still - * mid-abort. Confirmed live on hardware that resetting - * hostState first (the original order) opens exactly - * that window -- the SPI peripheral's own HAL state - * (HAL_SPI_STATE_BUSY_TX_RX) is what's actually latched, - * not just this instance's bookkeeping, so a transfer - * armed on top of an in-progress abort just fails again - * with HAL_BUSY forever. */ - pInst->prHw->pAbort (pInst->prHw->pContext); - } - if (pInst->hostState != eHostIdle) - { - pInst->hostState = eHostIdle; - pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); - } - if (wasArmed) - { - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorDmaTimeout); - } - } - else - { - bool wasArmed = (pInst->clientState != eClientIdle); - if (wasArmed) - { - /* See the matching Host-side comment above -- same - * ordering requirement: onSelectEvent's own guard - * (clientState != eClientIdle) is what keeps a real - * NSS-falling edge from re-arming the Client's slave DMA - * while this abort is still in flight, so it must run - * before clientState is reset below. */ - pInst->prHw->pAbort (pInst->prHw->pContext); - } - if (pInst->clientState != eClientIdle) - { - pInst->clientState = eClientIdle; - pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); - } - if (wasArmed) - { - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorDmaTimeout); - } - } - - if (wasConnected) - { - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventDisconnected); - } -} - -static uint16_t -buildControlBody (uint8_t *pOut, uint8_t type, teSpiTransportRole role, uint32_t epoch, - uint16_t startSeq) -{ - pOut[0] = type; - pOut[1] = (uint8_t)role; - putU32 (&pOut[2], epoch); - putU16 (&pOut[6], startSeq); - return SPI_TRANSPORT_CTRL_BODY_SIZE; -} - -/// @brief Choose and encode the next outbound frame: pending HELLO/HELLO_ACK -/// first, else a channel with queued data (round-robin), else a -/// FILLER -- see docs/ProtocolSpec.md "Channel-0 handshake" and -/// "Connected/disconnected timing model". -static void -buildOutgoingFrame (trSpiTransportInstance *pInst, uint8_t *pOutBuffer) -{ - uint8_t payload[SPI_TRANSPORT_FRAME_PAYLOAD_SIZE]; - uint8_t channel = SPI_TRANSPORT_CHANNEL_CONTROL; - uint8_t flags = 0; - uint16_t length = 0; - - gDiagTxFrameBuiltCount++; - pInst->txCommitPending = false; - - if (pInst->pendingHello) - { - length = buildControlBody (payload, SPI_TRANSPORT_CTRL_HELLO, pInst->role, pInst->epoch, - pInst->txSeq); - flags = SPI_TRANSPORT_FLAG_START | SPI_TRANSPORT_FLAG_END | SPI_TRANSPORT_FLAG_RESET; - pInst->pendingHello = false; - pInst->helloSentThisEpoch = true; - } - else if (pInst->pendingHelloAck) - { - length = buildControlBody (payload, SPI_TRANSPORT_CTRL_HELLO_ACK, pInst->role, - pInst->epoch, pInst->txSeq); - flags = SPI_TRANSPORT_FLAG_START | SPI_TRANSPORT_FLAG_END; - if (pInst->linkState != eSpiTransportLinkConnected) - { - flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_RESET); - } - pInst->pendingHelloAck = false; - } - else - { - uint8_t dataChannel; - uint16_t dataLength; - uint8_t dataFlags; - - if (spiTransportChannelNextTx (&pInst->channels, pInst->lastTxChannel, &dataChannel, - payload, &dataLength, &dataFlags)) - { - channel = dataChannel; - length = dataLength; - flags = dataFlags; - pInst->lastTxChannel = dataChannel; - - /* Peeked only -- not committed until the physical - * transfer carrying this frame is confirmed complete - * (see onTransferComplete). Left uncommitted, an - * abandoned/failed attempt naturally retries this exact - * chunk next time, since spiTransportChannelNextTx() - * didn't advance anything either. */ - pInst->txCommitPending = true; - pInst->txCommitChannel = dataChannel; - pInst->txCommitChunkLen = dataLength; - pInst->txCommitWasLastChunk = (dataFlags & SPI_TRANSPORT_FLAG_END) != 0; - } - else - { - flags = SPI_TRANSPORT_FLAG_FILLER; - if (pInst->linkState != eSpiTransportLinkConnected) - { - flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_RESET); - } - } - } - - trSpiTransportFrame frame; - frame.version = SPI_TRANSPORT_FRAME_VERSION; - frame.channel = channel; - frame.seq = pInst->txSeq; - frame.ack = pInst->rxLastSeq; - frame.flags = flags; - frame.payloadLength = length; - - (void)spiTransportFrameEncode (&frame, payload, pOutBuffer); - - /* txSeq is peeked here, not consumed -- mirrors the channel-data - * peek/commit split above. It only advances in onTransferComplete(), - * once the physical transfer this frame belongs to is confirmed to - * have actually gone out. An attempt that never completes (DMA-arm - * failure, watchdog-timeout abort, disconnect mid-transfer) leaves - * txSeq unchanged, so the next call re-peeks and re-sends this exact - * seq value instead of skipping it -- skipping it here previously - * meant the peer never saw that seq number at all, surfacing as a - * phantom sequence-gap error on every failed-then-retried attempt even - * though the actual channel data was correctly retried underneath. */ - - pInst->lastSendTickMs = pInst->prOs->pTickGet (pInst->prOs->pContext); -} - -/// @brief Decode and process one inbound frame -- handshake/epoch tracking, -/// sequence-gap detection, and channel dispatch. -static void -processIncomingFrame (trSpiTransportInstance *pInst, const uint8_t *pInBuffer) -{ - trSpiTransportFrame frame; - teSpiTransportError err = spiTransportFrameDecode (pInBuffer, &frame); - - if (err == eSpiTransportErrorHardwareFailure) - { - if (!spiTransportFrameHeaderCrcOk (pInBuffer)) - { - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorHeaderCrc); - } - else - { - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorPayloadCrc); - } - return; - } - if (err != eSpiTransportErrorNone) - { - return; /* bad magic / malformed -- dropped silently */ - } - - pInst->lastRecvTickMs = pInst->prOs->pTickGet (pInst->prOs->pContext); - - if ((frame.channel == SPI_TRANSPORT_CHANNEL_CONTROL) - && (frame.payloadLength >= SPI_TRANSPORT_CTRL_BODY_SIZE)) - { - uint8_t msgType = frame.pPayload[0]; - if ((msgType == SPI_TRANSPORT_CTRL_HELLO) || (msgType == SPI_TRANSPORT_CTRL_HELLO_ACK)) - { - uint32_t peerEpoch = getU32 (&frame.pPayload[2]); - uint16_t peerStartSeq = getU16 (&frame.pPayload[6]); - handleHandshake (pInst, msgType, peerEpoch, peerStartSeq); - } - } - - if (pInst->linkState == eSpiTransportLinkConnected) - { - if (pInst->rxSeqBaselineArmed) - { - uint16_t expected = (uint16_t)(pInst->rxLastSeq + 1u); - if (frame.seq != expected) - { - pInst->lastGapExpected = expected; - pInst->lastGapActual = frame.seq; - if (frame.seq == pInst->rxLastSeq) - { - pInst->gapDuplicateCount++; - } - else if ((uint16_t)(frame.seq - expected) < 0x8000u) - { - pInst->gapLossCount++; - } - else - { - pInst->gapOtherCount++; - } - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorSequenceGap); - } - } - else - { - pInst->rxSeqBaselineArmed = true; - } - pInst->rxLastSeq = frame.seq; - - if (frame.channel != SPI_TRANSPORT_CHANNEL_CONTROL) - { - spiTransportChannelDispatchRx (&pInst->channels, frame.channel, frame.pPayload, - frame.payloadLength, frame.flags); - } - } -} - -/* Host only. If waiting on the Client's ack and NRDY currently reads low, - * start the real clocked transfer. Shared between the tick-driven poll - * (hostServiceTick) and the interrupt-driven path (onReadyEvent) -- see - * docs/ProtocolSpec.md section 6; the tick poll remains correct on its own, the - * EXTI path just cuts the latency of noticing the edge. - * - * The check-then-claim on hostState is wrapped in pCriticalEnter/Exit - * because both callers (tick-driven and EXTI-driven) can genuinely race: - * confirmed live on hardware that without this guard, the task can read - * hostState==eHostWaitingAck, get preempted by the NRDY-falling EXTI before - * writing eHostTransferring, and the ISR's own call also passes the same - * (still-stale) check -- both then call buildOutgoingFrame(), advancing - * txSeq twice for what becomes only one physical transfer, which the Client - * observes as a spurious sequence-gap. */ -static void -hostArmTransferIfAcked (trSpiTransportInstance *pInst) -{ - pInst->prOs->pCriticalEnter (pInst->prOs->pContext); - bool shouldArm = (pInst->hostState == eHostWaitingAck) - && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == false); - if (shouldArm) - { - pInst->hostState = eHostTransferring; - } - pInst->prOs->pCriticalExit (pInst->prOs->pContext); - - if (!shouldArm) - { - gDiagHostArmRejectedCount++; - return; - } - gDiagHostArmArmedCount++; - buildOutgoingFrame (pInst, pInst->txBuffer); - teSpiTransportError startErr = pInst->prHw->pTransferStart ( - pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); - - if (startErr != eSpiTransportErrorNone) - { - /* DMA never actually armed -- nothing was clocked out, so - * onTransferComplete will never fire for this attempt. Abandon - * the peeked chunk (leave it uncommitted -- the same channel - * offset/txPending state as before this call, so the next - * successful attempt naturally retries it) and release NSS - * immediately rather than waiting out the 100ms ack-wait - * timeout or the 1.5s disconnect backstop for something we - * already know failed right now. */ - pInst->txCommitPending = false; - pInst->hostState = eHostIdle; - pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); - } -} - -/* Host only. Disambiguates a NRDY-falling edge/observation seen while - * hostState==eHostIdle: Host never drives NRDY itself and Client only ever - * lowers it either responding to Host's own request (which implies - * hostState==eHostWaitingAck, handled by hostArmTransferIfAcked() above, not - * this function) or self-initiating (clientState==eClientSelfArmed, see - * clientArmSelfInitiateIfIdle()). So hostState==eHostIdle observing NRDY low - * can only mean the latter -- no new wire signal needed, Host's own two-state - * distinction already disambiguates this for free. Skips straight to - * eHostTransferring (no separate ack step -- the ack already happened, it's - * what triggered this call). Same check-then-claim-under-critical-section - * shape as hostArmTransferIfAcked(), for the same double-arm-race reason. */ -static void -hostArmClientInitiatedIfIdle (trSpiTransportInstance *pInst) -{ - pInst->prOs->pCriticalEnter (pInst->prOs->pContext); - bool shouldArm = (pInst->hostState == eHostIdle) - && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == false); - if (shouldArm) - { - pInst->hostState = eHostTransferring; - } - pInst->prOs->pCriticalExit (pInst->prOs->pContext); - - if (!shouldArm) - { - return; - } - gDiagHostClientInitArmedCount++; - /* NSS low: lets the hardware-NSS-managed Client peripheral (already - * self-armed, waiting) actually begin shifting, and gives Client's own - * NSS-EXTI a real edge -- onSelectEvent's existing clientState!=eClientIdle - * guard correctly ignores it there (Client is eClientSelfArmed, not - * Idle), so no change needed on that side. */ - pInst->prHw->pSelectAssert (pInst->prHw->pContext, false); - buildOutgoingFrame (pInst, pInst->txBuffer); - teSpiTransportError startErr = pInst->prHw->pTransferStart ( - pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); - - if (startErr != eSpiTransportErrorNone) - { - pInst->txCommitPending = false; - pInst->hostState = eHostIdle; - pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); - } -} - -/* Host only. If idle and something is queued/due, and NRDY currently reads - * high (Client available), issue the next request. Shared the same way as - * hostArmTransferIfAcked() above -- same race, same critical-section fix. */ -static void -hostIssueRequestIfReady (trSpiTransportInstance *pInst, uint32_t now) -{ - bool haveSomethingQueued = pInst->pendingHello || pInst->pendingHelloAck - || spiTransportChannelHasPending (&pInst->channels); - bool heartbeatDue = (now - pInst->lastSendTickMs) >= SPI_TRANSPORT_HEARTBEAT_MS; - - pInst->prOs->pCriticalEnter (pInst->prOs->pContext); - bool shouldIssue = (pInst->hostState == eHostIdle) && (haveSomethingQueued || heartbeatDue) - && (pInst->prHw->pReadyRead (pInst->prHw->pContext) == true); - if (shouldIssue) - { - pInst->hostState = eHostWaitingAck; - pInst->hostWaitStartMs = now; - } - pInst->prOs->pCriticalExit (pInst->prOs->pContext); - - if (!shouldIssue) - { - return; /* not idle, nothing to send, or Client not currently available */ - } - - pInst->prHw->pSelectAssert (pInst->prHw->pContext, false); /* LOW = issue the request */ -} - -static void -hostServiceTick (trSpiTransportInstance *pInst, uint32_t now) -{ - if (pInst->hostState == eHostWaitingAck) - { - hostArmTransferIfAcked (pInst); - if ((pInst->hostState == eHostWaitingAck) - && ((now - pInst->hostWaitStartMs) >= SPI_TRANSPORT_HOST_ACK_WAIT_TIMEOUT_MS)) - { - /* Client never acked this attempt -- give up on it, let the next - * heartbeat/data-queued opportunity retry (the 1.5s disconnect - * timer is the backstop if the link is genuinely down). */ - pInst->prHw->pSelectAssert (pInst->prHw->pContext, - true); /* HIGH = idle, give up this attempt */ - pInst->hostState = eHostIdle; - } - return; - } - - if (pInst->hostState != eHostIdle) - { - return; /* eHostTransferring: waiting on onTransferComplete */ - } - - /* Poll-path fallback for platforms without a NRDY EXTI (onReadyEvent's - * falling-edge branch is the latency-optimized path for those that have - * one): a Client self-initiation could have dropped NRDY between ticks - * with nothing to interrupt on. Re-check hostState afterward -- a - * successful claim here moves it to eHostTransferring. */ - hostArmClientInitiatedIfIdle (pInst); - if (pInst->hostState != eHostIdle) - { - return; - } - - hostIssueRequestIfReady (pInst, now); -} - -static void -onTransferComplete (void *pCoreCtx, uint16_t length) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; - (void)length; - - if (pInst->role == eSpiTransportRoleHost) - { - pInst->prHw->pSelectAssert (pInst->prHw->pContext, - true); /* HIGH = deassert, transfer done */ - pInst->hostState = eHostIdle; - } - else - { - pInst->clientState = eClientIdle; - } - - /* This callback firing at all is the confirmation the just-armed - * transfer's outgoing bytes were actually clocked out -- only now is - * it safe to commit whatever channel chunk buildOutgoingFrame() peeked - * into this frame, and to consume the seq value it peeked alongside - * it (see buildOutgoingFrame's comment). Every completed transfer - * consumes exactly one seq value, regardless of whether it carried - * real channel data or was a HELLO/FILLER frame. */ - pInst->txSeq++; - gDiagTxSeqAdvanceCount++; - if (pInst->txCommitPending) - { - spiTransportChannelCommitTx (&pInst->channels, pInst->txCommitChannel, - pInst->txCommitChunkLen, pInst->txCommitWasLastChunk); - pInst->txCommitPending = false; - } - - /* ISR context on real hardware -- must not call processIncomingFrame() - * here (it invokes user callbacks). Snapshot into the ring and defer to - * spiTransportTick(), which runs in task context. Producer side only: - * reads rxRingTail (written only by the consumer) to compute occupancy, - * writes rxRingHead. If the ring is full, drop this newest arrival - * (rare in practice, see spiTransportDebugRxOverwriteCount) rather than - * evicting an existing slot the task might be mid-read of -- this ISR - * can preempt the task at any point, so an in-place slot overwrite here - * would race that read. */ - uint32_t head = pInst->rxRingHead; - uint32_t tail = pInst->rxRingTail; - if ((head - tail) >= SPI_TRANSPORT_RX_RING_DEPTH) - { - pInst->rxOverwriteCount++; - } - else - { - memcpy (pInst->rxRingBuffer[head % SPI_TRANSPORT_RX_RING_DEPTH], pInst->rxBuffer, - SPI_TRANSPORT_FRAME_TOTAL_SIZE); - pInst->rxRingHead = head + 1u; - pInst->prOs->pTaskNotifyGive (pInst->prOs->pContext); - } -} - -static void -onSelectEvent (void *pCoreCtx, bool asserted) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; - - /* Backpressure: if the deferred-RX ring has no free slot, do NOT ack - * this request at all -- leave NRDY high, exactly as if Client weren't - * ready. Host's existing 100ms ack-wait timeout (hostServiceTick) is - * already the correct recovery path for "Client didn't ack this - * attempt", so this just reuses it instead of acking a transfer whose - * received frame would only get dropped at the ring anyway (see - * onTransferComplete's ring-full branch). Deferring the attempt this - * way, rather than accepting and silently losing it, is the actual fix - * for the RX-ring-overflow investigation -- not a replacement for it, - * a second layer: the loop-timing fix (see harnessMain) closes the - * dominant cause (blocking UART stalls), this closes the residual gap - * for whatever legitimate backlog remains. Checked before the - * clientState guard below since it's a distinct, worth-tracking- - * separately reason to decline. */ - bool ringFull = (pInst->rxRingHead - pInst->rxRingTail) >= SPI_TRANSPORT_RX_RING_DEPTH; - - if ((pInst->role != eSpiTransportRoleClient) || !asserted || (pInst->clientState != eClientIdle) - || ringFull) - { - if ((pInst->role == eSpiTransportRoleClient) && asserted) - { - if (ringFull && (pInst->clientState == eClientIdle)) - { - gDiagRxRingFullRejectedCount++; - return; - } - gDiagOnSelectRejectedCount++; - /* Real NSS falling while already eClientSelfArmed is the - * EXPECTED shape of a Client-initiated cycle (Host is - * about to catch up to the solicitation already in - * flight) -- split out from the general rejection count - * so it isn't confused with a genuine problem (e.g. - * clientState stuck eClientArmed/eClientTransferring for - * some other reason). */ - if (pInst->clientState == eClientSelfArmed) - { - gDiagOnSelectRejectedSelfArmedCount++; - } - } - return; - } - - /* Claim eClientArmed before triggering pTransferStart/pReadyAssert - * below, not after: on the host-native null-loopback HW adapter (and, - * on real hardware, if the physical transfer completes and its ISRs - * run before this function returns), the whole transfer -- including - * onClockStart/onTransferComplete moving clientState on to - * eClientTransferring/eClientIdle -- can happen synchronously inside - * that pReadyAssert call. Assigning eClientArmed afterwards would - * stomp that already-correct later state back to a stale one. */ - pInst->clientState = eClientArmed; - buildOutgoingFrame (pInst, pInst->txBuffer); - teSpiTransportError startErr = pInst->prHw->pTransferStart ( - pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); - - if (startErr != eSpiTransportErrorNone) - { - /* Slave DMA never actually armed. Abandon the peeked chunk - * (leave it uncommitted for a clean retry) and go back to - * Idle without ever asserting NRDY low -- Host's existing - * 100ms ack-wait timeout already handles "Client never acked - * this attempt" correctly, so this degrades to that same, - * already-working retry path instead of needing a new one. */ - pInst->txCommitPending = false; - pInst->clientState = eClientIdle; - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); - return; - } - gDiagOnSelectArmedCount++; - pInst->prHw->pReadyAssert (pInst->prHw->pContext, false); -} - -static void -onClockStart (void *pCoreCtx) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; - - if (pInst->role != eSpiTransportRoleClient) - { - return; - } - - pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); - pInst->clientState = eClientTransferring; -} - -/* Client only. If idle and something is queued, arm the Client's own DMA - * and drop NRDY while NSS is still HIGH, soliciting Host -- see - * hostArmClientInitiatedIfIdle() for how Host disambiguates this from its - * own request cycle (no new wire signal needed). Task-context only (called - * from clientServiceTick()), so the critical section here is what keeps - * onSelectEvent()'s ISR-context clientState!=eClientIdle check race-free - * against this claim -- unlike Host's symmetric pair, only this one writer - * needs the section, since onSelectEvent is the sole other writer and it - * only ever runs with this masked out while the section is held. */ -static void -clientArmSelfInitiateIfIdle (trSpiTransportInstance *pInst, uint32_t now) -{ - if (!pInst->clientSelfInitEnabled) - { - return; /* disabled (the default) -- see spiTransportSetClientSelfInitEnabled() */ - } - - bool haveSomethingQueued = pInst->pendingHello || pInst->pendingHelloAck - || spiTransportChannelHasPending (&pInst->channels); - /* Same backpressure reasoning as onSelectEvent's ring-full check: don't - * solicit Host for a reply this Client has no room to receive. */ - bool ringFull = (pInst->rxRingHead - pInst->rxRingTail) >= SPI_TRANSPORT_RX_RING_DEPTH; - - pInst->prOs->pCriticalEnter (pInst->prOs->pContext); - bool shouldArm = (pInst->clientState == eClientIdle) && haveSomethingQueued && !ringFull; - if (shouldArm) - { - pInst->clientState = eClientSelfArmed; - pInst->clientSelfArmStartMs = now; - } - pInst->prOs->pCriticalExit (pInst->prOs->pContext); - - if (!shouldArm) - { - if (ringFull && (pInst->clientState == eClientIdle) && haveSomethingQueued) - { - gDiagRxRingFullRejectedCount++; - } - return; - } - gDiagClientSelfArmAttemptCount++; - buildOutgoingFrame (pInst, pInst->txBuffer); - teSpiTransportError startErr = pInst->prHw->pTransferStart ( - pInst->prHw->pContext, pInst->txBuffer, pInst->rxBuffer, SPI_TRANSPORT_FRAME_TOTAL_SIZE); - - if (startErr != eSpiTransportErrorNone) - { - /* Same degrade-to-existing-retry-path reasoning as - * onSelectEvent's own failure branch: abandon the peeked chunk, - * go back to Idle without ever asserting NRDY low, and let the - * next tick's opportunity retry. */ - pInst->txCommitPending = false; - pInst->clientState = eClientIdle; - spiTransportChannelNotifyEvent (&pInst->channels, eSpiTransportEventErrorDmaFailure); - return; - } - pInst->prHw->pReadyAssert (pInst->prHw->pContext, - false); /* solicit: NRDY low, NSS still high */ -} - -/* Client only. Mirrors hostServiceTick(): services the self-init watchdog - * (eClientSelfArmed timeout) and otherwise attempts a new self-initiation - * when idle. Called from spiTransportTick() for the Client role. */ -static void -clientServiceTick (trSpiTransportInstance *pInst, uint32_t now) -{ - if (pInst->clientState == eClientSelfArmed) - { - if ((now - pInst->clientSelfArmStartMs) >= SPI_TRANSPORT_CLIENT_SELF_ARM_TIMEOUT_MS) - { - /* Host never noticed/clocked this solicitation -- self-heal - * now rather than waiting out the far coarser 1.5s - * disconnect backstop (doDisconnect()'s own generic - * clientState!=eClientIdle handling there remains a - * second-layer defense if this ever doesn't fire, e.g. if - * the platform starves this tick badly enough). pAbort() - * BEFORE resetting clientState/NRDY -- same ordering rule - * as doDisconnect()'s Client branch, for the same reason: - * closes the window where a concurrent - * onSelectEvent/self-init claim could re-arm against - * hardware still mid-abort. */ - gDiagClientSelfArmTimeoutCount++; - pInst->prHw->pAbort (pInst->prHw->pContext); - pInst->clientState = eClientIdle; - pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); - spiTransportChannelNotifyEvent (&pInst->channels, - eSpiTransportEventErrorDmaTimeout); - } - return; /* still self-armed (not yet timed out): don't also retry */ - } - - if (pInst->clientState != eClientIdle) - { - return; /* eClientArmed/eClientTransferring: reacting to a real Host request */ - } - - clientArmSelfInitiateIfIdle (pInst, now); -} - -static void -onReadyEvent (void *pCoreCtx, bool high) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)pCoreCtx; - - if (pInst->role != eSpiTransportRoleHost) - { - return; - } - - /* Interrupt-driven NRDY watch: acts immediately on the edge rather than - * waiting for the next spiTransportTick() poll -- the tick-driven path - * (hostServiceTick) remains correct on its own and is what a platform - * with no NRDY EXTI (or an adapter that leaves pOnReadyEvent unfired) - * relies on exclusively; this is a pure latency optimization. */ - if (!high) - { - /* Falling edge: either Client's ack for Host's own pending - * request (hostState==eHostWaitingAck), or Client self- - * initiating while Host was idle (hostState==eHostIdle) -- - * mutually exclusive by construction (see - * hostArmClientInitiatedIfIdle()'s doc comment), so branch on - * which rather than trying both (calling the wrong one is a - * harmless no-op either way, but would pollute that path's own - * [DIAG] rejection counter with an unrelated cause). */ - if (pInst->hostState == eHostWaitingAck) - { - hostArmTransferIfAcked (pInst); - } - else - { - hostArmClientInitiatedIfIdle (pInst); - } - } - else - { - /* Rising edge: Client became available again. */ - hostIssueRequestIfReady (pInst, pInst->prOs->pTickGet (pInst->prOs->pContext)); - } -} - -teSpiTransportError -spiTransportInit (const trSpiTransportConfig *prConfig, thSpiTransport *phTransport) -{ - if ((prConfig == NULL) || (prConfig->prOs == NULL) || (prConfig->prHw == NULL) - || (phTransport == NULL)) - { - return eSpiTransportErrorInvalidParam; - } - - crc16Init (); - - trSpiTransportInstance *pInst = NULL; - for (uint8_t i = 0; i < SPI_TRANSPORT_INSTANCES_MAX; i++) - { - if (!gInstances[i].inUse) - { - pInst = &gInstances[i]; - break; - } - } - if (pInst == NULL) - { - return eSpiTransportErrorBusy; /* pool exhausted */ - } - - memset (pInst, 0, sizeof (*pInst)); - - pInst->inUse = true; - pInst->role = prConfig->role; - pInst->prOs = prConfig->prOs; - pInst->prHw = prConfig->prHw; - - spiTransportChannelTableInit (&pInst->channels); - pInst->linkState = eSpiTransportLinkDisconnected; - pInst->lastTxChannel = SPI_TRANSPORT_CHANNELS_MAX - 1u; /* round-robin starts at 0 */ - pInst->epoch = pInst->prOs->pTickGet (pInst->prOs->pContext); - - spiTransportHwSetCallbacks (pInst->prHw, onTransferComplete, onSelectEvent, onReadyEvent, - onClockStart, pInst); - - *phTransport = pInst; - return eSpiTransportErrorNone; -} - -teSpiTransportError -spiTransportStart (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return eSpiTransportErrorInvalidParam; - } - - uint32_t now = pInst->prOs->pTickGet (pInst->prOs->pContext); - - pInst->peerEpochKnown = false; - resetForHandshake (pInst); - pInst->txSeq = 0; - pInst->rxLastSeq = 0; - pInst->lastSendTickMs = now; - pInst->lastRecvTickMs = now; - pInst->hostState = eHostIdle; - pInst->clientState = eClientIdle; - - pInst->rxRingHead = 0u; - pInst->rxRingTail = 0u; - - /* Force the physical line back to idle-high regardless of whatever - * level it was left at (e.g. a prior spiTransportStop() call, or a - * board's power-on GPIO default) -- otherwise a Start() that follows a - * mid-transfer Stop() (or the very first Start() before any GPIO init - * guarantee) can begin operation with the peer seeing a stale low - * NSS/NRDY that this session's state machine never actually asserted. */ - if (pInst->role == eSpiTransportRoleHost) - { - pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); - } - else - { - pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); - } - - return eSpiTransportErrorNone; -} - -void -spiTransportStop (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return; - } - - /* Same un-wedge requirement as doDisconnect()'s backstop -- Stop() can - * be called mid-transfer (see the fault-injection "reset during - * transfer" mode in docs/TestPlan.md), and simply resetting - * hostState/clientState below without releasing the physical line or - * aborting a genuinely-armed peripheral would leave NSS/NRDY latched - * and the SPI/DMA peripheral still busy for whatever runs next. Abort - * before flipping state, same ordering reason as doDisconnect(). */ - if (pInst->role == eSpiTransportRoleHost) - { - if (pInst->hostState == eHostTransferring) - { - pInst->prHw->pAbort (pInst->prHw->pContext); - } - pInst->prHw->pSelectAssert (pInst->prHw->pContext, true); - } - else - { - if (pInst->clientState != eClientIdle) - { - pInst->prHw->pAbort (pInst->prHw->pContext); - } - pInst->prHw->pReadyAssert (pInst->prHw->pContext, true); - } - - pInst->linkState = eSpiTransportLinkDisconnected; - pInst->hostState = eHostIdle; - pInst->clientState = eClientIdle; - spiTransportChannelResetAll (&pInst->channels); -} - -teSpiTransportError -spiTransportRegisterChannel (thSpiTransport hTransport, uint8_t channel, - tpSpiTransportRxCallback pRxCallback, - tpSpiTransportEventCallback pEventCallback, void *pContext) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return eSpiTransportErrorInvalidParam; - } - return spiTransportChannelRegister (&pInst->channels, channel, pRxCallback, pEventCallback, - pContext); -} - -teSpiTransportError -spiTransportDeregisterChannel (thSpiTransport hTransport, uint8_t channel) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return eSpiTransportErrorInvalidParam; - } - return spiTransportChannelDeregister (&pInst->channels, channel); -} - -teSpiTransportError -spiTransportSend (thSpiTransport hTransport, uint8_t channel, const uint8_t *pBuffer, - uint16_t length, bool ackRequired) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return eSpiTransportErrorInvalidParam; - } - return spiTransportChannelQueueTx (&pInst->channels, channel, pBuffer, length, ackRequired); -} - -teSpiTransportLinkState -spiTransportGetLinkState (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return eSpiTransportLinkDisconnected; - } - return pInst->linkState; -} - -void -spiTransportSetClientSelfInitEnabled (thSpiTransport hTransport, bool enabled) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse || (pInst->role != eSpiTransportRoleClient)) - { - return; - } - pInst->clientSelfInitEnabled = enabled; -} - -bool -spiTransportIsClientSelfInitEnabled (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return false; - } - return pInst->clientSelfInitEnabled; -} - -void -spiTransportTick (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return; - } - - /* Consumer side of the ring: drain everything queued, not just one - * frame, so a burst of back-to-back completions (see onTransferComplete) - * doesn't leave later ones waiting for a whole extra tick. Reads - * rxRingHead (written only by the producer/ISR) fresh each iteration -- - * safe without a lock for the same single-writer-per-counter reason as - * the producer side, and picks up anything that arrived mid-drain for - * free. Strict FIFO: tail only ever advances by exactly one slot at a - * time, in arrival order. */ - while (pInst->rxRingTail != pInst->rxRingHead) - { - uint32_t tail = pInst->rxRingTail; - processIncomingFrame (pInst, pInst->rxRingBuffer[tail % SPI_TRANSPORT_RX_RING_DEPTH]); - pInst->rxRingTail = tail + 1u; - } - - uint32_t now = pInst->prOs->pTickGet (pInst->prOs->pContext); - - if ((pInst->linkState != eSpiTransportLinkDisconnected) - && ((now - pInst->lastRecvTickMs) >= SPI_TRANSPORT_DISCONNECT_MS)) - { - doDisconnect (pInst, now); - } - - if (pInst->role == eSpiTransportRoleHost) - { - hostServiceTick (pInst, now); - } - else - { - clientServiceTick (pInst, now); - } -} - -uint32_t -spiTransportDebugRxOverwriteCount (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return 0u; - } - return pInst->rxOverwriteCount; -} - -void -spiTransportDebugTxSeqCounts (thSpiTransport hTransport, uint32_t *pBuiltCount, - uint32_t *pAdvanceCount, uint16_t *pCurrentTxSeq) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - if (pBuiltCount != NULL) - { - *pBuiltCount = 0u; - } - if (pAdvanceCount != NULL) - { - *pAdvanceCount = 0u; - } - if (pCurrentTxSeq != NULL) - { - *pCurrentTxSeq = 0u; - } - return; - } - if (pBuiltCount != NULL) - { - *pBuiltCount = gDiagTxFrameBuiltCount; - } - if (pAdvanceCount != NULL) - { - *pAdvanceCount = gDiagTxSeqAdvanceCount; - } - if (pCurrentTxSeq != NULL) - { - *pCurrentTxSeq = pInst->txSeq; - } -} - -uint32_t -spiTransportDebugRxRingFullRejectedCount (thSpiTransport hTransport) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return 0u; - } - return gDiagRxRingFullRejectedCount; -} - -void -spiTransportDebugClientArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, - uint32_t *pArmedCount) -{ - (void)hTransport; - if (pRejectedCount != NULL) - { - *pRejectedCount = gDiagOnSelectRejectedCount; - } - if (pArmedCount != NULL) - { - *pArmedCount = gDiagOnSelectArmedCount; - } -} - -void -spiTransportDebugHostArmCounts (thSpiTransport hTransport, uint32_t *pRejectedCount, - uint32_t *pArmedCount) -{ - (void)hTransport; - if (pRejectedCount != NULL) - { - *pRejectedCount = gDiagHostArmRejectedCount; - } - if (pArmedCount != NULL) - { - *pArmedCount = gDiagHostArmArmedCount; - } -} - -void -spiTransportDebugClientSelfInitCounts (thSpiTransport hTransport, uint32_t *pAttemptCount, - uint32_t *pTimeoutCount, uint32_t *pHostArmedCount, - uint32_t *pRejectedSelfArmedCount) -{ - (void)hTransport; - if (pAttemptCount != NULL) - { - *pAttemptCount = gDiagClientSelfArmAttemptCount; - } - if (pTimeoutCount != NULL) - { - *pTimeoutCount = gDiagClientSelfArmTimeoutCount; - } - if (pHostArmedCount != NULL) - { - *pHostArmedCount = gDiagHostClientInitArmedCount; - } - if (pRejectedSelfArmedCount != NULL) - { - *pRejectedSelfArmedCount = gDiagOnSelectRejectedSelfArmedCount; - } -} - -void -spiTransportDebugLastGap (thSpiTransport hTransport, uint16_t *pExpected, uint16_t *pActual, - uint32_t *pDuplicateCount, uint32_t *pLossCount, uint32_t *pOtherCount) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return; - } - if (pExpected != NULL) - { - *pExpected = pInst->lastGapExpected; - } - if (pActual != NULL) - { - *pActual = pInst->lastGapActual; - } - if (pDuplicateCount != NULL) - { - *pDuplicateCount = pInst->gapDuplicateCount; - } - if (pLossCount != NULL) - { - *pLossCount = pInst->gapLossCount; - } - if (pOtherCount != NULL) - { - *pOtherCount = pInst->gapOtherCount; - } -} - -void -spiTransportDebugEpoch (thSpiTransport hTransport, uint32_t *pOwnEpoch, uint32_t *pPeerEpoch, - bool *pPeerEpochKnown) -{ - trSpiTransportInstance *pInst = (trSpiTransportInstance *)hTransport; - if ((pInst == NULL) || !pInst->inUse) - { - return; - } - if (pOwnEpoch != NULL) - { - *pOwnEpoch = pInst->epoch; - } - if (pPeerEpoch != NULL) - { - *pPeerEpoch = pInst->peerEpoch; - } - if (pPeerEpochKnown != NULL) - { - *pPeerEpochKnown = pInst->peerEpochKnown; - } -} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_channel.c b/drivers/spi/spi-mt-transport/core/spi_transport_channel.c deleted file mode 100644 index cbaa7b5a936c5..0000000000000 --- a/drivers/spi/spi-mt-transport/core/spi_transport_channel.c +++ /dev/null @@ -1,262 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_channel.c -// @brief : Channel registration table, RX reassembly, per-channel -// single-slot TX queue. See spi_transport_channel.h. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#include "spi_transport/spi_transport_channel.h" - -#include - -void -spiTransportChannelTableInit (trSpiTransportChannelTable *prTable) -{ - memset (prTable, 0, sizeof (*prTable)); -} - -teSpiTransportError -spiTransportChannelRegister (trSpiTransportChannelTable *prTable, uint8_t channel, - tpSpiTransportRxCallback pRxCallback, - tpSpiTransportEventCallback pEventCallback, void *pContext) -{ - if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) - { - return eSpiTransportErrorInvalidChannel; - } - if (prTable->slots[channel].registered) - { - return eSpiTransportErrorAlreadyRegistered; - } - - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - memset (pSlot, 0, sizeof (*pSlot)); - pSlot->registered = true; - pSlot->pRxCallback = pRxCallback; - pSlot->pEventCallback = pEventCallback; - pSlot->pContext = pContext; - - return eSpiTransportErrorNone; -} - -teSpiTransportError -spiTransportChannelDeregister (trSpiTransportChannelTable *prTable, uint8_t channel) -{ - if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) - { - return eSpiTransportErrorInvalidChannel; - } - if (!prTable->slots[channel].registered) - { - return eSpiTransportErrorNotRegistered; - } - - memset (&prTable->slots[channel], 0, sizeof (prTable->slots[channel])); - return eSpiTransportErrorNone; -} - -teSpiTransportError -spiTransportChannelQueueTx (trSpiTransportChannelTable *prTable, uint8_t channel, - const uint8_t *pBuffer, uint16_t length, bool ackRequired) -{ - /* Channel 0 is transport-internal (HELLO/HELLO_ACK/FILLER) -- matches - * spiTransportChannelRegister()'s own guard. Without this, application - * data queued on channel 0 would collide with control-frame traffic and - * corrupt the handshake/state machine (spiTransportChannelNextTx() has - * no way to distinguish the two once queued). */ - if ((channel == SPI_TRANSPORT_CHANNEL_CONTROL) || (channel >= SPI_TRANSPORT_CHANNELS_MAX)) - { - return eSpiTransportErrorInvalidChannel; - } - if (length > SPI_TRANSPORT_CHANNEL_MESSAGE_MAX) - { - return eSpiTransportErrorInvalidParam; - } - if ((pBuffer == NULL) && (length > 0u)) - { - return eSpiTransportErrorInvalidParam; - } - - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - if (pSlot->txPending) - { - return eSpiTransportErrorBusy; - } - - pSlot->txPending = true; - pSlot->pTxBuffer = pBuffer; - pSlot->txLength = length; - pSlot->txOffset = 0; - pSlot->txAckRequired = ackRequired; - - return eSpiTransportErrorNone; -} - -bool -spiTransportChannelNextTx (trSpiTransportChannelTable *prTable, uint8_t startAfterChannel, - uint8_t *pOutChannel, uint8_t *pOutPayload, uint16_t *pOutLength, - uint8_t *pOutFlags) -{ - uint8_t start = (uint8_t)((startAfterChannel + 1u) % SPI_TRANSPORT_CHANNELS_MAX); - - for (uint8_t i = 0; i < SPI_TRANSPORT_CHANNELS_MAX; i++) - { - uint8_t channel = (uint8_t)((start + i) % SPI_TRANSPORT_CHANNELS_MAX); - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - - if (!pSlot->txPending) - { - continue; - } - - uint16_t remaining = (uint16_t)(pSlot->txLength - pSlot->txOffset); - uint16_t chunk = (remaining < SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) - ? remaining - : SPI_TRANSPORT_FRAME_PAYLOAD_SIZE; - - uint8_t flags = 0; - if (pSlot->txOffset == 0u) - { - flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_START); - } - bool isLastChunk = (uint16_t)(pSlot->txOffset + chunk) >= pSlot->txLength; - if (isLastChunk) - { - flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_END); - } - if (pSlot->txAckRequired) - { - flags = (uint8_t)(flags | SPI_TRANSPORT_FLAG_ACK_REQUIRED); - } - - if (chunk > 0u) - { - memcpy (pOutPayload, &pSlot->pTxBuffer[pSlot->txOffset], chunk); - } - - *pOutChannel = channel; - *pOutLength = chunk; - *pOutFlags = flags; - return true; - } - - return false; -} - -void -spiTransportChannelCommitTx (trSpiTransportChannelTable *prTable, uint8_t channel, - uint16_t chunkLen, bool wasLastChunk) -{ - if (channel >= SPI_TRANSPORT_CHANNELS_MAX) - { - return; - } - - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - pSlot->txOffset = (uint16_t)(pSlot->txOffset + chunkLen); - - if (wasLastChunk) - { - pSlot->txPending = false; - pSlot->pTxBuffer = NULL; - } -} - -bool -spiTransportChannelHasPending (const trSpiTransportChannelTable *prTable) -{ - for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) - { - if (prTable->slots[channel].txPending) - { - return true; - } - } - - return false; -} - -void -spiTransportChannelDispatchRx (trSpiTransportChannelTable *prTable, uint8_t channel, - const uint8_t *pPayload, uint16_t length, uint8_t flags) -{ - if (channel >= SPI_TRANSPORT_CHANNELS_MAX) - { - return; - } - - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - if (!pSlot->registered) - { - return; /* unregistered channel: silently dropped (counted by the caller, if desired) */ - } - - if (flags & SPI_TRANSPORT_FLAG_START) - { - pSlot->rxInProgress = true; - pSlot->rxOffset = 0; - } - - if (!pSlot->rxInProgress) - { - return; /* END/middle chunk arrived with no START seen yet (e.g. post-reconnect) */ - } - - if ((uint32_t)pSlot->rxOffset + length > SPI_TRANSPORT_CHANNEL_MESSAGE_MAX) - { - pSlot->rxInProgress = false; /* oversized message: abandon and drop */ - return; - } - - if (length > 0u) - { - memcpy (&pSlot->rxBuffer[pSlot->rxOffset], pPayload, length); - pSlot->rxOffset = (uint16_t)(pSlot->rxOffset + length); - } - - if (flags & SPI_TRANSPORT_FLAG_END) - { - pSlot->rxInProgress = false; - if (pSlot->pRxCallback != NULL) - { - pSlot->pRxCallback (pSlot->pContext, channel, pSlot->rxBuffer, pSlot->rxOffset, - flags); - } - } -} - -void -spiTransportChannelResetAll (trSpiTransportChannelTable *prTable) -{ - for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) - { - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - pSlot->txPending = false; - pSlot->pTxBuffer = NULL; - pSlot->txOffset = 0; - pSlot->rxInProgress = false; - pSlot->rxOffset = 0; - } -} - -void -spiTransportChannelNotifyEvent (trSpiTransportChannelTable *prTable, teSpiTransportEvent eEvent) -{ - for (uint8_t channel = 0; channel < SPI_TRANSPORT_CHANNELS_MAX; channel++) - { - trSpiTransportChannelSlot *pSlot = &prTable->slots[channel]; - if (pSlot->registered && (pSlot->pEventCallback != NULL)) - { - pSlot->pEventCallback (pSlot->pContext, eEvent); - } - } -} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c b/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c deleted file mode 100644 index 6f81bc296f590..0000000000000 --- a/drivers/spi/spi-mt-transport/core/spi_transport_crc16.c +++ /dev/null @@ -1,67 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_crc16.c -// @brief : CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect), -// table-driven. New algorithm for this transport -- deliberately -// not the shared CRC32 used elsewhere in this repo, see -// docs/ProtocolSpec.md "CRC choice & duplication tradeoff". -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#include "spi_transport/spi_transport_frame.h" - -static uint16_t crc16Table[256]; -static bool crc16TableBuilt = false; - -static void -buildCrc16Table (void) -{ - for (uint32_t i = 0; i < 256u; i++) - { - uint16_t crc = (uint16_t)(i << 8); - for (uint32_t bit = 0; bit < 8u; bit++) - { - if (crc & 0x8000u) - { - crc = (uint16_t)((crc << 1) ^ 0x1021u); - } - else - { - crc = (uint16_t)(crc << 1); - } - } - crc16Table[i] = crc; - } -} - -void -crc16Init (void) -{ - if (!crc16TableBuilt) - { - buildCrc16Table (); - crc16TableBuilt = true; - } -} - -uint16_t -crc16 (const uint8_t *pBuffer, uint16_t length) -{ - uint16_t crc = 0xFFFFu; - - for (uint16_t i = 0; i < length; i++) - { - uint8_t index = (uint8_t)((crc >> 8) ^ pBuffer[i]); - crc = (uint16_t)((crc << 8) ^ crc16Table[index]); - } - - return crc; -} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_frame.c b/drivers/spi/spi-mt-transport/core/spi_transport_frame.c deleted file mode 100644 index 7af53edc8524b..0000000000000 --- a/drivers/spi/spi-mt-transport/core/spi_transport_frame.c +++ /dev/null @@ -1,151 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_frame.c -// @brief : Wire-frame encode/decode. Explicit little-endian codec, not a -// packed struct overlay -- see spi_transport_frame.h. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#include "spi_transport/spi_transport_frame.h" - -#include - -static void -putU16 (uint8_t *pOut, uint16_t value) -{ - pOut[0] = (uint8_t)(value & 0xFFu); - pOut[1] = (uint8_t)((value >> 8) & 0xFFu); -} - -static uint16_t -getU16 (const uint8_t *pIn) -{ - return (uint16_t)((uint16_t)pIn[0] | ((uint16_t)pIn[1] << 8)); -} - -teSpiTransportError -spiTransportFrameEncode (const trSpiTransportFrame *prFrame, const uint8_t *pPayload, - uint8_t *pOutBuffer) -{ - if ((prFrame == NULL) || (pOutBuffer == NULL)) - { - return eSpiTransportErrorInvalidParam; - } - if (prFrame->payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) - { - return eSpiTransportErrorInvalidParam; - } - if ((pPayload == NULL) && (prFrame->payloadLength > 0u)) - { - return eSpiTransportErrorInvalidParam; - } - - putU16 (&pOutBuffer[0], SPI_TRANSPORT_FRAME_MAGIC); - pOutBuffer[2] = prFrame->version; - pOutBuffer[3] = prFrame->channel; - putU16 (&pOutBuffer[4], prFrame->seq); - putU16 (&pOutBuffer[6], prFrame->ack); - pOutBuffer[8] = prFrame->flags; - pOutBuffer[9] = 0; /* reserved */ - putU16 (&pOutBuffer[10], prFrame->payloadLength); - putU16 (&pOutBuffer[12], crc16 (pOutBuffer, SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u)); - - uint8_t *pPayloadRegion = &pOutBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; - if (prFrame->payloadLength > 0u) - { - memcpy (pPayloadRegion, pPayload, prFrame->payloadLength); - } - if (prFrame->payloadLength < SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) - { - memset (&pPayloadRegion[prFrame->payloadLength], 0, - (size_t)(SPI_TRANSPORT_FRAME_PAYLOAD_SIZE - prFrame->payloadLength)); - } - - uint16_t payloadCrc = crc16 (pPayloadRegion, prFrame->payloadLength); - putU16 (&pOutBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE], - payloadCrc); - - return eSpiTransportErrorNone; -} - -bool -spiTransportFrameHeaderCrcOk (const uint8_t *pInBuffer) -{ - if (pInBuffer == NULL) - { - return false; - } - uint16_t expected = getU16 (&pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u]); - uint16_t actual = crc16 (pInBuffer, SPI_TRANSPORT_FRAME_HEADER_SIZE - 2u); - return expected == actual; -} - -bool -spiTransportFramePayloadCrcOk (const uint8_t *pInBuffer) -{ - if (pInBuffer == NULL) - { - return false; - } - uint16_t payloadLength = getU16 (&pInBuffer[10]); - if (payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) - { - return false; - } - - const uint8_t *pPayloadRegion = &pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; - uint16_t expected - = getU16 (&pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE + SPI_TRANSPORT_FRAME_PAYLOAD_SIZE]); - uint16_t actual = crc16 (pPayloadRegion, payloadLength); - return expected == actual; -} - -teSpiTransportError -spiTransportFrameDecode (const uint8_t *pInBuffer, trSpiTransportFrame *prFrame) -{ - if ((pInBuffer == NULL) || (prFrame == NULL)) - { - return eSpiTransportErrorInvalidParam; - } - - uint16_t magic = getU16 (&pInBuffer[0]); - if (magic != SPI_TRANSPORT_FRAME_MAGIC) - { - return eSpiTransportErrorInvalidParam; - } - - if (!spiTransportFrameHeaderCrcOk (pInBuffer)) - { - return eSpiTransportErrorHardwareFailure; - } - - uint16_t payloadLength = getU16 (&pInBuffer[10]); - if (payloadLength > SPI_TRANSPORT_FRAME_PAYLOAD_SIZE) - { - return eSpiTransportErrorInvalidParam; - } - - if (!spiTransportFramePayloadCrcOk (pInBuffer)) - { - return eSpiTransportErrorHardwareFailure; - } - - prFrame->magic = magic; - prFrame->version = pInBuffer[2]; - prFrame->channel = pInBuffer[3]; - prFrame->seq = getU16 (&pInBuffer[4]); - prFrame->ack = getU16 (&pInBuffer[6]); - prFrame->flags = pInBuffer[8]; - prFrame->payloadLength = payloadLength; - prFrame->pPayload = &pInBuffer[SPI_TRANSPORT_FRAME_HEADER_SIZE]; - - return eSpiTransportErrorNone; -} diff --git a/drivers/spi/spi-mt-transport/core/spi_transport_hw.c b/drivers/spi/spi-mt-transport/core/spi_transport_hw.c deleted file mode 100644 index fdf19498d64c9..0000000000000 --- a/drivers/spi/spi-mt-transport/core/spi_transport_hw.c +++ /dev/null @@ -1,31 +0,0 @@ -//****************************************************************************** -// @file : spi_transport_hw.c -// @brief : Generic (adapter-independent) setter for the HW-adapter's -// core-side callback fields. See spi_transport_hw.h. -// @date : 2026-08-10 -//****************************************************************************** -// @attention -// -// Copyright (c) 2026 MultiTracks.com, LLC. -// All rights reserved. -// -// For internal MultiTracks use only. Unauthorized reproduction, distribution, -// or disclosure is prohibited. -// -//****************************************************************************** - -#include "spi_transport/spi_transport_hw.h" - -void -spiTransportHwSetCallbacks (trSpiTransportHw *prHw, - void (*pOnTransferComplete) (void *pCoreCtx, uint16_t length), - void (*pOnSelectEvent) (void *pCoreCtx, bool asserted), - void (*pOnReadyEvent) (void *pCoreCtx, bool asserted), - void (*pOnClockStart) (void *pCoreCtx), void *pCoreCtx) -{ - prHw->pOnTransferComplete = pOnTransferComplete; - prHw->pOnSelectEvent = pOnSelectEvent; - prHw->pOnReadyEvent = pOnReadyEvent; - prHw->pOnClockStart = pOnClockStart; - prHw->pCoreCtx = pCoreCtx; -} diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c deleted file mode 100644 index 119ef801fba12..0000000000000 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ /dev/null @@ -1,425 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * spi_mt_transport_drv.c - Linux kernel Host-role driver for the MultiTracks - * SPI transport protocol (see core/ for the portable protocol core, vendored - * from the firmware repo -- see core/PROVENANCE.md). - * - * 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 - * (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 is a deliberately minimal placeholder, just enough to prove the - * module loads, probes, and can move a byte. - */ - -#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_CHANNEL 1 - -struct mt_transport_priv { - struct spi_device *spi; - struct device *dev; - - struct gpio_desc *nss_gpiod; - struct gpio_desc *nrdy_gpiod; - int nrdy_irq; - - 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; - - /* Minimal placeholder userspace interface -- MT-158682 owns the real - * design. Single hardcoded channel, 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; - - /* 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. - */ - 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; -}; - -/* 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_priv *priv = pContext; - unsigned long irqflags; - - (void)flags; - if (channel != MT_TRANSPORT_CHANNEL) - return; - if (length > sizeof(priv->rx_buf)) - length = sizeof(priv->rx_buf); - - spin_lock_irqsave(&priv->rx_lock, irqflags); - memcpy(priv->rx_buf, pBuffer, length); - priv->rx_len = length; - priv->rx_valid = true; - spin_unlock_irqrestore(&priv->rx_lock, irqflags); - - wake_up_interruptible(&priv->rx_wq); -} - -static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) -{ - struct mt_transport_priv *priv = pContext; - - 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; - } -} - -static int mt_transport_tick_thread_fn(void *data) -{ - struct mt_transport_priv *priv = data; - - while (!kthread_should_stop()) { - priv->os.pTaskNotifyWait(priv->os.pContext, 2); - spiTransportTick(priv->htransport); - } - return 0; -} - -/* --- Minimal userspace interface (placeholder, see plan sec 7) --- */ - -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_priv *priv = container_of(misc, struct mt_transport_priv, misc); - unsigned long irqflags; - uint16_t len; - int ret; - - (void)ppos; - - if (filp->f_flags & O_NONBLOCK) { - spin_lock_irqsave(&priv->rx_lock, irqflags); - if (!priv->rx_valid) { - spin_unlock_irqrestore(&priv->rx_lock, irqflags); - return -EAGAIN; - } - spin_unlock_irqrestore(&priv->rx_lock, irqflags); - } else { - ret = wait_event_interruptible(priv->rx_wq, priv->rx_valid); - if (ret) - return ret; - } - - spin_lock_irqsave(&priv->rx_lock, irqflags); - len = priv->rx_len; - if (len > count) - len = count; - if (copy_to_user(buf, priv->rx_buf, len)) { - spin_unlock_irqrestore(&priv->rx_lock, irqflags); - return -EFAULT; - } - priv->rx_valid = false; - spin_unlock_irqrestore(&priv->rx_lock, irqflags); - - return len; -} - -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_priv *priv = container_of(misc, struct mt_transport_priv, misc); - uint8_t txBuf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; - teSpiTransportError err; - size_t len = count; - - (void)ppos; - - if (len > sizeof(txBuf)) - len = sizeof(txBuf); - if (copy_from_user(txBuf, buf, len)) - return -EFAULT; - - err = spiTransportSend(priv->htransport, MT_TRANSPORT_CHANNEL, txBuf, len, true); - if (err != eSpiTransportErrorNone) - return -EBUSY; - - return len; -} - -static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) -{ - struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); - __poll_t mask = 0; - - poll_wait(filp, &priv->rx_wq, wait); - if (priv->rx_valid) - mask |= EPOLLIN | EPOLLRDNORM; - return mask; -} - -static const struct file_operations mt_transport_misc_fops = { - .owner = THIS_MODULE, - .read = mt_transport_misc_read, - .write = mt_transport_misc_write, - .poll = mt_transport_misc_poll, -}; - -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; - - priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - priv->spi = spi; - priv->dev = dev; - spi_set_drvdata(spi, priv); - - init_waitqueue_head(&priv->rx_wq); - spin_lock_init(&priv->rx_lock); - - /* Custom "mt-nss"/"mt-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. - */ - priv->nss_gpiod = devm_gpiod_get(dev, "mt-nss", GPIOD_OUT_HIGH); - if (IS_ERR(priv->nss_gpiod)) - return dev_err_probe(dev, PTR_ERR(priv->nss_gpiod), - "failed to get mt-nss-gpios\n"); - - priv->nrdy_gpiod = devm_gpiod_get(dev, "mt-nrdy", GPIOD_IN); - if (IS_ERR(priv->nrdy_gpiod)) - return dev_err_probe(dev, PTR_ERR(priv->nrdy_gpiod), - "failed to get mt-nrdy-gpios\n"); - - mt_transport_os_linux_init(&priv->os_ctx, dev, &priv->os); - 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) - return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); - - if (spiTransportRegisterChannel(priv->htransport, MT_TRANSPORT_CHANNEL, - mt_transport_rx_callback, mt_transport_event_callback, - priv) - != eSpiTransportErrorNone) - return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel failed\n"); - - /* Optional latency optimization -- if the NRDY line has no usable - * IRQ, tick-driven pReadyRead() polling (2ms cadence, see the tick - * thread above) 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); - 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 { - 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)) - return dev_err_probe(dev, PTR_ERR(priv->tick_thread), - "failed to start tick thread\n"); - - if (spiTransportStart(priv->htransport) != eSpiTransportErrorNone) { - kthread_stop(priv->tick_thread); - return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); - } - - priv->misc.minor = MISC_DYNAMIC_MINOR; - priv->misc.name = "mt_spi_transport"; - priv->misc.fops = &mt_transport_misc_fops; - ret = misc_register(&priv->misc); - if (ret) { - spiTransportStop(priv->htransport); - kthread_stop(priv->tick_thread); - return dev_err_probe(dev, ret, "misc_register failed\n"); - } - - dev_info(dev, "MultiTracks SPI transport driver probed (Host role)\n"); - return 0; -} - -static void mt_transport_remove(struct spi_device *spi) -{ - struct mt_transport_priv *priv = spi_get_drvdata(spi); - - misc_deregister(&priv->misc); - spiTransportStop(priv->htransport); - kthread_stop(priv->tick_thread); -} - -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 deleted file mode 100644 index bb4a6e5ee54cd..0000000000000 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ /dev/null @@ -1,226 +0,0 @@ -// 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 "mt-nss-gpios"/"mt-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()'s logical value always equals the literal pin level -- - * matching this whole contract's "true = pin HIGH" convention exactly. - */ - -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; - - /* Signal "msg/xfer no longer referenced by the SPI core" before - * notifying the core -- pNotify may wake the tick thread straight - * into a new pTransferStart(), which gates on this same completion. - */ - complete(&ctx->transferComplete); - - if (ctx->pHw->pOnTransferComplete) - ctx->pHw->pOnTransferComplete(ctx->pHw->pCoreCtx, length); - - 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(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; - - return gpiod_get_value(ctx->nrdy_gpiod) ? 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; - bool high = gpiod_get_value(ctx->nrdy_gpiod) ? true : false; - - if (ctx->pHw->pOnReadyEvent) - ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, high); - - 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 deleted file mode 100644 index 91af58db8688b..0000000000000 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h +++ /dev/null @@ -1,71 +0,0 @@ -/* 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 - * core/include/spi_transport/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 deleted file mode 100644 index b7f09a31d27fe..0000000000000 --- a/drivers/spi/spi-mt-transport/spi_transport_os_linux.c +++ /dev/null @@ -1,109 +0,0 @@ -// 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 "spi_transport_os_linux.h" - -/* - * pTaskNotifyWait/pTaskNotifyGive: a single-waiter completion used as a - * repeating notify, not a one-shot. reinit_completion() runs *after* - * consuming the wait, not before -- reinit-before-wait would race a - * pTaskNotifyGive() landing between the previous tick() and this wait, - * silently swallowing the wakeup until the next timeout. There is exactly - * one waiter (the driver's tick kthread), so this ordering is safe. - */ -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)); - reinit_completion(&ctx->notify); -} - -/* 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, so a single saved-flags field in ctx is enough; - * this is not a general-purpose reentrant lock. - */ -static void mt_os_critical_enter(void *pContext) -{ - struct mt_transport_os_ctx *ctx = pContext; - - spin_lock_irqsave(&ctx->crit_lock, ctx->crit_flags); -} - -static void mt_os_critical_exit(void *pContext) -{ - struct mt_transport_os_ctx *ctx = pContext; - - spin_unlock_irqrestore(&ctx->crit_lock, ctx->crit_flags); -} - -static void mt_os_log(void *pContext, const char *pFormat, va_list args) -{ - struct mt_transport_os_ctx *ctx = pContext; - struct va_format vaf = { .fmt = pFormat, .va = &args }; - - dev_dbg(ctx->dev, "%pV", &vaf); -} - -void 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); - - 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; -} diff --git a/drivers/spi/spi-mt-transport/spi_transport_os_linux.h b/drivers/spi/spi-mt-transport/spi_transport_os_linux.h deleted file mode 100644 index 40fdd9dd06edb..0000000000000 --- a/drivers/spi/spi-mt-transport/spi_transport_os_linux.h +++ /dev/null @@ -1,40 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -/* - * spi_transport_os_linux.h - Linux kernel OS-adapter for the MultiTracks - * SPI transport core (see core/include/spi_transport/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; - unsigned long 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; no allocation here). - */ -void mt_transport_os_linux_init(struct mt_transport_os_ctx *ctx, struct device *dev, - trSpiTransportOs *pOs); - -#endif /* SPI_TRANSPORT_OS_LINUX_H */ From 271a69daebea810b4cf8b9eb543c40c09d18755f Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Wed, 19 Aug 2026 12:42:01 -0600 Subject: [PATCH 07/35] MT-158113: revive in-tree spi-mt-transport driver, core stays out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverses commit 2b603614814a's removal for the Linux adapter files only, not the vendored core it also deleted -- CODING_STANDARDS.md §7 in the firmware repo forbids GPL/LGPL code there, and this adapter genuinely calls GPL-only-exported kernel symbols (devm_gpiod_get, gpiod_get_value/set_value/to_irq, spi_async, spi_slave_abort, dev_err_probe, sysfs_emit) core to its own function, so it can't be relicensed -- it has to live somewhere GPL is already the designated home, which per that same policy doc is this repo. The portable protocol core (firmware-common/spi-transport/src/*.c, inc/spi_transport/*.h in the firmware repo) is unaffected and stays put -- it's plain C11, calls no kernel APIs, and doesn't need to be GPL. It's also deliberately NOT vendored back into drivers/spi/ spi-mt-transport/core/ here (that's exactly the duplication problem the original vendored version had) -- core/ is populated at Yocto build time instead, fetched fresh from the firmware repo by imx8mmini-bb-evk's meta-mt-transport-evk linux-imx_%.bbappend, and gitignored here so it never gets committed. --- arch/arm64/configs/imx_v8_defconfig | 1 + drivers/spi/Kconfig | 12 + drivers/spi/Makefile | 1 + drivers/spi/spi-mt-transport/.gitignore | 5 + drivers/spi/spi-mt-transport/Makefile | 20 + .../spi-mt-transport/kernel-compat/stdarg.h | 13 + .../spi-mt-transport/kernel-compat/stdbool.h | 14 + .../spi-mt-transport/kernel-compat/stdint.h | 14 + .../spi-mt-transport/kernel-compat/string.h | 15 + .../spi-mt-transport/spi_mt_transport_drv.c | 622 ++++++++++++++++++ .../spi-mt-transport/spi_transport_hw_linux.c | 226 +++++++ .../spi-mt-transport/spi_transport_hw_linux.h | 71 ++ .../spi-mt-transport/spi_transport_os_linux.c | 130 ++++ .../spi-mt-transport/spi_transport_os_linux.h | 49 ++ 14 files changed, 1193 insertions(+) create mode 100644 drivers/spi/spi-mt-transport/.gitignore create mode 100644 drivers/spi/spi-mt-transport/Makefile create mode 100644 drivers/spi/spi-mt-transport/kernel-compat/stdarg.h create mode 100644 drivers/spi/spi-mt-transport/kernel-compat/stdbool.h create mode 100644 drivers/spi/spi-mt-transport/kernel-compat/stdint.h create mode 100644 drivers/spi/spi-mt-transport/kernel-compat/string.h create mode 100644 drivers/spi/spi-mt-transport/spi_mt_transport_drv.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_hw_linux.h create mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.c create mode 100644 drivers/spi/spi-mt-transport/spi_transport_os_linux.h diff --git a/arch/arm64/configs/imx_v8_defconfig b/arch/arm64/configs/imx_v8_defconfig index 6e3754f5be47a..5b2b6b04171dd 100644 --- a/arch/arm64/configs/imx_v8_defconfig +++ b/arch/arm64/configs/imx_v8_defconfig @@ -451,6 +451,7 @@ CONFIG_SPI_FSL_QUADSPI=y CONFIG_SPI_NXP_FLEXSPI=y CONFIG_SPI_IMX=y CONFIG_SPI_FSL_DSPI=y +CONFIG_SPI_MT_TRANSPORT=m CONFIG_SPI_PL022=y CONFIG_SPI_ROCKCHIP=y CONFIG_SPI_SPIDEV=y diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 3ce0fd5df8e9c..c685bb5b05642 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1211,6 +1211,18 @@ 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)" + depends on ARCH_MXC || COMPILE_TEST + depends on GPIOLIB + 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..bd7c5b8bac546 --- /dev/null +++ b/drivers/spi/spi-mt-transport/Makefile @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0 +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 + +# core/ is not committed here -- it's staged at build time from the firmware +# repo's firmware-common/spi-transport/ (single source of truth for the +# portable protocol core; see imx8mmini-bb-evk's meta-mt-transport-evk +# linux-imx_%.bbappend for the fetch + do_patch[postfuncs] staging step). +# $(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..d127ef346b604 --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -0,0 +1,622 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * spi_mt_transport_drv.c - Linux kernel Host-role driver for the MultiTracks + * SPI transport protocol. Builds as an out-of-tree Yocto kernel module (see + * imx8mmini-bb-evk's meta-mt-transport-evk recipes-kernel/spi-mt-transport/) + * against the portable protocol core at ../../{,../inc/spi_transport}/ in + * this same source tree -- referenced in place, not vendored/copied. + * + * Talks Host role to an STM32-class Client peer over a raw SPI bus plus a + * companion NRDY GPIO handshake line. See ../../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 (misc device + a small TX ring, see mt_transport_tx_service()) + * is still a placeholder ahead of MT-158682's real design: single + * hardcoded channel, single in-flight RX message, no ioctl/config surface. + */ + +#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_CHANNEL 1 +#define MT_TRANSPORT_TX_QUEUE_DEPTH 10 + +struct mt_transport_tx_slot +{ + uint8_t buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; + uint16_t len; +}; + +struct mt_transport_priv { + struct spi_device *spi; + struct device *dev; + + struct gpio_desc *nss_gpiod; + struct gpio_desc *nrdy_gpiod; + int nrdy_irq; + + 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; + + /* Minimal placeholder userspace interface -- MT-158682 owns the real + * design. Single hardcoded channel, 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; /* -1 if nothing submitted yet */ + wait_queue_head_t tx_free_wq; + + /* 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. + */ + 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; +}; + +/* 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_priv *priv = pContext; + unsigned long irqflags; + + (void)flags; + if (channel != MT_TRANSPORT_CHANNEL) + return; + if (length > sizeof(priv->rx_buf)) + length = sizeof(priv->rx_buf); + + spin_lock_irqsave(&priv->rx_lock, irqflags); + memcpy(priv->rx_buf, pBuffer, length); + priv->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(priv->rx_valid, true); + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + + wake_up_interruptible(&priv->rx_wq); +} + +static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) +{ + struct mt_transport_priv *priv = pContext; + + 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; + } +} + +/// @brief 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 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_priv *priv) +{ + unsigned long flags; + unsigned int idx; + uint16_t len; + teSpiTransportError err; + + spin_lock_irqsave(&priv->tx_lock, flags); + if (priv->tx_queued_count == 0) { + spin_unlock_irqrestore(&priv->tx_lock, flags); + return; + } + idx = priv->tx_head; + len = priv->tx_slots[idx].len; + spin_unlock_irqrestore(&priv->tx_lock, flags); + + err = spiTransportSend(priv->htransport, MT_TRANSPORT_CHANNEL, priv->tx_slots[idx].buf, len, + true); + if (err != eSpiTransportErrorNone) + return; + + spin_lock_irqsave(&priv->tx_lock, flags); + priv->tx_queued_count--; + priv->tx_in_flight_idx = idx; + priv->tx_head = (priv->tx_head + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; + spin_unlock_irqrestore(&priv->tx_lock, flags); + wake_up_interruptible(&priv->tx_free_wq); +} + +static int mt_transport_tick_thread_fn(void *data) +{ + struct mt_transport_priv *priv = data; + + while (!kthread_should_stop()) { + priv->os.pTaskNotifyWait(priv->os.pContext, 2); + spiTransportTick(priv->htransport); + mt_transport_tx_service(priv); + } + return 0; +} + +/* --- Minimal userspace interface (placeholder ahead of MT-158682) --- */ + +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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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 (filp->f_flags & O_NONBLOCK) { + spin_lock_irqsave(&priv->rx_lock, irqflags); + if (!priv->rx_valid) { + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + return -EAGAIN; + } + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + } else { + ret = wait_event_interruptible(priv->rx_wq, READ_ONCE(priv->rx_valid)); + if (ret) + return ret; + } + + /* 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. + */ + spin_lock_irqsave(&priv->rx_lock, irqflags); + len = priv->rx_len; + if (len > count) + len = count; + memcpy(scratch, priv->rx_buf, len); + WRITE_ONCE(priv->rx_valid, false); + spin_unlock_irqrestore(&priv->rx_lock, irqflags); + + if (copy_to_user(buf, scratch, len)) + return -EFAULT; + + return len; +} + +/// @brief 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_priv *priv) +{ + unsigned int occupied = priv->tx_queued_count + (priv->tx_in_flight_idx >= 0 ? 1 : 0); + + return occupied < MT_TRANSPORT_TX_QUEUE_DEPTH; +} + +/// @brief 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_priv *priv) +{ + unsigned long flags; + bool room; + + spin_lock_irqsave(&priv->tx_lock, flags); + room = mt_transport_tx_room_locked(priv); + spin_unlock_irqrestore(&priv->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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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 (len > sizeof(scratch)) + len = sizeof(scratch); + if (copy_from_user(scratch, buf, len)) + return -EFAULT; + + for (;;) { + spin_lock_irqsave(&priv->tx_lock, flags); + if (mt_transport_tx_room_locked(priv)) + break; + spin_unlock_irqrestore(&priv->tx_lock, flags); + + if (filp->f_flags & O_NONBLOCK) + return -EAGAIN; + ret = wait_event_interruptible(priv->tx_free_wq, mt_transport_tx_has_room(priv)); + if (ret) + return ret; + } + + idx = priv->tx_tail; + memcpy(priv->tx_slots[idx].buf, scratch, len); + priv->tx_slots[idx].len = len; + priv->tx_tail = (priv->tx_tail + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; + priv->tx_queued_count++; + spin_unlock_irqrestore(&priv->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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + __poll_t mask = 0; + + poll_wait(filp, &priv->rx_wq, wait); + if (READ_ONCE(priv->rx_valid)) + mask |= EPOLLIN | EPOLLRDNORM; + return mask; +} + +static const struct file_operations mt_transport_misc_fops = { + .owner = THIS_MODULE, + .read = mt_transport_misc_read, + .write = mt_transport_misc_write, + .poll = mt_transport_misc_poll, +}; + +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; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->spi = spi; + priv->dev = dev; + spi_set_drvdata(spi, priv); + + init_waitqueue_head(&priv->rx_wq); + spin_lock_init(&priv->rx_lock); + spin_lock_init(&priv->tx_lock); + init_waitqueue_head(&priv->tx_free_wq); + priv->tx_in_flight_idx = -1; + + /* Custom "mt-nss"/"mt-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. + */ + priv->nss_gpiod = devm_gpiod_get(dev, "mt-nss", GPIOD_OUT_HIGH); + if (IS_ERR(priv->nss_gpiod)) + return dev_err_probe(dev, PTR_ERR(priv->nss_gpiod), + "failed to get mt-nss-gpios\n"); + + priv->nrdy_gpiod = devm_gpiod_get(dev, "mt-nrdy", GPIOD_IN); + if (IS_ERR(priv->nrdy_gpiod)) + return dev_err_probe(dev, PTR_ERR(priv->nrdy_gpiod), + "failed to get mt-nrdy-gpios\n"); + + ret = mt_transport_os_linux_init(&priv->os_ctx, dev, &priv->os); + if (ret) + 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) + return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); + + if (spiTransportRegisterChannel(priv->htransport, MT_TRANSPORT_CHANNEL, + mt_transport_rx_callback, mt_transport_event_callback, + priv) + != eSpiTransportErrorNone) + return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel failed\n"); + + /* 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); + 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 { + 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)) + return dev_err_probe(dev, PTR_ERR(priv->tick_thread), + "failed to start tick thread\n"); + + if (spiTransportStart(priv->htransport) != eSpiTransportErrorNone) { + kthread_stop(priv->tick_thread); + return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); + } + + /* Name hardcoded, not suffixed per-device (flagged by Copilot review -- + * see PR discussion): a second bound spi-mt-transport device would + * collide here, but this specific hardware only ever binds one, and + * changing the path breaks every existing script/tool this session + * built against /dev/mt_spi_transport. Left as-is pending a decision; + * see PR #785. + */ + priv->misc.minor = MISC_DYNAMIC_MINOR; + priv->misc.name = "mt_spi_transport"; + priv->misc.fops = &mt_transport_misc_fops; + ret = misc_register(&priv->misc); + if (ret) { + /* kthread_stop() before spiTransportStop(): the tick thread + * must not be able to call into the core after it's been + * stopped (same ordering as mt_transport_remove() below). + */ + kthread_stop(priv->tick_thread); + spiTransportStop(priv->htransport); + return dev_err_probe(dev, ret, "misc_register failed\n"); + } + + dev_info(dev, "MultiTracks SPI transport driver probed (Host role)\n"); + return 0; +} + +static void mt_transport_remove(struct spi_device *spi) +{ + struct mt_transport_priv *priv = spi_get_drvdata(spi); + + misc_deregister(&priv->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); +} + +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..bb4a6e5ee54cd --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -0,0 +1,226 @@ +// 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 "mt-nss-gpios"/"mt-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()'s logical value always equals the literal pin level -- + * matching this whole contract's "true = pin HIGH" convention exactly. + */ + +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; + + /* Signal "msg/xfer no longer referenced by the SPI core" before + * notifying the core -- pNotify may wake the tick thread straight + * into a new pTransferStart(), which gates on this same completion. + */ + complete(&ctx->transferComplete); + + if (ctx->pHw->pOnTransferComplete) + ctx->pHw->pOnTransferComplete(ctx->pHw->pCoreCtx, length); + + 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(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; + + return gpiod_get_value(ctx->nrdy_gpiod) ? 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; + bool high = gpiod_get_value(ctx->nrdy_gpiod) ? true : false; + + if (ctx->pHw->pOnReadyEvent) + ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, high); + + 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..77fe9928fd7eb --- /dev/null +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c @@ -0,0 +1,130 @@ +// 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; + struct va_format vaf = { .fmt = pFormat, .va = &args }; + + dev_dbg(ctx->dev, "%pV", &vaf); +} + +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 */ From 969c3d4e938ed73f34dbee7ad5f4d48280e17fc1 Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Wed, 19 Aug 2026 15:36:10 -0600 Subject: [PATCH 08/35] MT-158113: fix Copilot review findings on the revived in-tree driver - mt_hw_select_assert()/mt_hw_ready_read()/mt_transport_hw_linux_nrdy_irq() used plain gpiod_get_value()/gpiod_set_value() from non-atomic contexts (tick kthread, and a threaded IRQ handler that can sleep by definition) -- switched to the _cansleep variants, correct per the GPIO consumer API's own contract for sleepable callers, even though this board's actual GPIO controller happens to be non-sleeping today. - Updated spi_mt_transport_drv.c's file banner: still described the driver as an out-of-tree Yocto module with a relative-path core reference and a relative ProtocolSpec.md path, both stale now that it's in-tree here with the core fetched from a separate repo. - Documented the Makefile/defconfig's build-time dependency on the Yocto-staged core/ directory explicitly, so enabling CONFIG_SPI_MT_TRANSPORT outside the imx8mmini-bb-evk BSP pipeline fails with an understood, expected error instead of a cryptic one. Found by Copilot's review of PR #46 after the adapter moved here. --- arch/arm64/configs/imx_v8_defconfig | 5 +++++ drivers/spi/spi-mt-transport/Makefile | 18 ++++++++++++---- .../spi-mt-transport/spi_mt_transport_drv.c | 16 ++++++++------ .../spi-mt-transport/spi_transport_hw_linux.c | 21 ++++++++++++++----- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/arch/arm64/configs/imx_v8_defconfig b/arch/arm64/configs/imx_v8_defconfig index 5b2b6b04171dd..339f0b7b44c1c 100644 --- a/arch/arm64/configs/imx_v8_defconfig +++ b/arch/arm64/configs/imx_v8_defconfig @@ -451,6 +451,11 @@ CONFIG_SPI_FSL_QUADSPI=y CONFIG_SPI_NXP_FLEXSPI=y CONFIG_SPI_IMX=y CONFIG_SPI_FSL_DSPI=y +# CONFIG_SPI_MT_TRANSPORT requires drivers/spi/spi-mt-transport/core/ to be +# staged first (Yocto-only, not committed here -- see that directory's +# Makefile for why). A plain `make imx_v8_defconfig && make modules` build +# outside the imx8mmini-bb-evk BSP pipeline will fail on this module for +# that reason -- expected, not a defconfig bug. CONFIG_SPI_MT_TRANSPORT=m CONFIG_SPI_PL022=y CONFIG_SPI_ROCKCHIP=y diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile index bd7c5b8bac546..11327e2e4fb59 100644 --- a/drivers/spi/spi-mt-transport/Makefile +++ b/drivers/spi/spi-mt-transport/Makefile @@ -1,4 +1,18 @@ # 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. Enabling CONFIG_SPI_MT_TRANSPORT +# outside that pipeline will fail with a generic "No rule to make target +# core/spi_transport.o" -- that's expected, not a bug; get the core staged +# first (or just build via bitbake, which always does this automatically). obj-$(CONFIG_SPI_MT_TRANSPORT) += spi-mt-transport.o spi-mt-transport-y := \ core/spi_transport.o \ @@ -10,10 +24,6 @@ spi-mt-transport-y := \ spi_transport_hw_linux.o \ spi_mt_transport_drv.o -# core/ is not committed here -- it's staged at build time from the firmware -# repo's firmware-common/spi-transport/ (single source of truth for the -# portable protocol core; see imx8mmini-bb-evk's meta-mt-transport-evk -# linux-imx_%.bbappend for the fetch + do_patch[postfuncs] staging step). # $(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. diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index d127ef346b604..79d6a22b917b2 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -1,14 +1,18 @@ // SPDX-License-Identifier: GPL-2.0 /* * spi_mt_transport_drv.c - Linux kernel Host-role driver for the MultiTracks - * SPI transport protocol. Builds as an out-of-tree Yocto kernel module (see - * imx8mmini-bb-evk's meta-mt-transport-evk recipes-kernel/spi-mt-transport/) - * against the portable protocol core at ../../{,../inc/spi_transport}/ in - * this same source tree -- referenced in place, not vendored/copied. + * 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 ../../docs/ProtocolSpec.md for the - * wire protocol and handshake state machine this ports into the kernel. + * 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 diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index bb4a6e5ee54cd..43e3b70c88401 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -34,8 +34,19 @@ * 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()'s logical value always equals the literal pin level -- - * matching this whole contract's "true = pin HIGH" convention exactly. + * 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) @@ -105,7 +116,7 @@ static void mt_hw_select_assert(void *pContext, bool high) { struct mt_transport_hw_ctx *ctx = pContext; - gpiod_set_value(ctx->nss_gpiod, high ? 1 : 0); + gpiod_set_value_cansleep(ctx->nss_gpiod, high ? 1 : 0); } /* Client only -- Host never calls this; left wired to a harmless stub so a @@ -124,7 +135,7 @@ static bool mt_hw_ready_read(void *pContext) { struct mt_transport_hw_ctx *ctx = pContext; - return gpiod_get_value(ctx->nrdy_gpiod) ? true : false; + return gpiod_get_value_cansleep(ctx->nrdy_gpiod) ? true : false; } /* @@ -214,7 +225,7 @@ void mt_transport_hw_linux_set_notify(struct mt_transport_hw_ctx *ctx, irqreturn_t mt_transport_hw_linux_nrdy_irq(int irq, void *dev_id) { struct mt_transport_hw_ctx *ctx = dev_id; - bool high = gpiod_get_value(ctx->nrdy_gpiod) ? true : false; + bool high = gpiod_get_value_cansleep(ctx->nrdy_gpiod) ? true : false; if (ctx->pHw->pOnReadyEvent) ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, high); From a3ec60e6a9078d0a0e05332ea35ac1e95a3d992b Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Wed, 19 Aug 2026 15:52:49 -0600 Subject: [PATCH 09/35] MT-158113: second round of Copilot review fixes - Converted three C++-style '///' Doxygen comment blocks in spi_mt_transport_drv.c to standard kernel /* */ style -- checkpatch flags '//' comments outside the SPDX line, and this is in-tree code now. - gpiod_to_irq() can return -EPROBE_DEFER if the backing IRQ chip isn't ready yet, not just "no IRQ available" -- the old `if (nrdy_irq > 0)` check silently and permanently disabled the IRQ optimization on that race instead of deferring the whole probe(). Now propagates -EPROBE_DEFER explicitly. - Removed CONFIG_SPI_MT_TRANSPORT=m from imx_v8_defconfig itself -- defconfigs should be buildable from a plain checkout, and this one can't be (core/ only exists via Yocto staging). Moved to a proper Yocto config fragment instead (imx8mmini-bb-evk's meta-mt-transport-evk/recipes-kernel/linux-imx/files/ spi-mt-transport.cfg, wired via SRC_URI). - Added a devicetree binding doc (Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml) for the multitracks,spi-transport compatible string and its mt-nss-gpios/mt-nrdy-gpios properties, so dtbs_check can validate the schema. - Fixed two more stale references in the .dts file's own comment ("vendored core", gpiod_get/set_value() without _cansleep). Found by Copilot's second review pass on PR #46. --- .../spi/multitracks,spi-transport.yaml | 74 +++++++++++++++++++ .../freescale/imx8mm-evk-spi-transport.dts | 7 +- arch/arm64/configs/imx_v8_defconfig | 6 -- .../spi-mt-transport/spi_mt_transport_drv.c | 72 ++++++++++-------- 4 files changed, 120 insertions(+), 39 deletions(-) create mode 100644 Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml 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..65599f2231340 --- /dev/null +++ b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml @@ -0,0 +1,74 @@ +# 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. + +properties: + compatible: + const: multitracks,spi-transport + + reg: + maxItems: 1 + + spi-max-frequency: true + + mt-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"). + + mt-nrdy-gpios: + maxItems: 1 + description: + Client-driven NRDY (ready/ack) line, read by the Host. Same + GPIO_ACTIVE_HIGH convention as mt-nss-gpios. + +required: + - compatible + - reg + - mt-nss-gpios + - mt-nrdy-gpios + +additionalProperties: false + +examples: + - | + #include + spi { + #address-cells = <1>; + #size-cells = <0>; + + spi@0 { + compatible = "multitracks,spi-transport"; + reg = <0>; + spi-max-frequency = <500000>; + + mt-nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + mt-nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; + }; + }; + +... diff --git a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts index 95790e7a1dd8a..0a9b51b2be1ac 100644 --- a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -14,9 +14,10 @@ // 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() calls always see the -// literal pin level, matching the protocol's own "true = pin HIGH" -// convention (see spi_transport_hw.h in the vendored core). +// 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" diff --git a/arch/arm64/configs/imx_v8_defconfig b/arch/arm64/configs/imx_v8_defconfig index 339f0b7b44c1c..6e3754f5be47a 100644 --- a/arch/arm64/configs/imx_v8_defconfig +++ b/arch/arm64/configs/imx_v8_defconfig @@ -451,12 +451,6 @@ CONFIG_SPI_FSL_QUADSPI=y CONFIG_SPI_NXP_FLEXSPI=y CONFIG_SPI_IMX=y CONFIG_SPI_FSL_DSPI=y -# CONFIG_SPI_MT_TRANSPORT requires drivers/spi/spi-mt-transport/core/ to be -# staged first (Yocto-only, not committed here -- see that directory's -# Makefile for why). A plain `make imx_v8_defconfig && make modules` build -# outside the imx8mmini-bb-evk BSP pipeline will fail on this module for -# that reason -- expected, not a defconfig bug. -CONFIG_SPI_MT_TRANSPORT=m CONFIG_SPI_PL022=y CONFIG_SPI_ROCKCHIP=y CONFIG_SPI_SPIDEV=y diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 79d6a22b917b2..6624df19fcff9 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -216,22 +216,23 @@ static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEve } } -/// @brief 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 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. +/* + * 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 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_priv *priv) { unsigned long flags; @@ -328,11 +329,13 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ return len; } -/// @brief 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. +/* + * 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_priv *priv) { unsigned int occupied = priv->tx_queued_count + (priv->tx_in_flight_idx >= 0 ? 1 : 0); @@ -340,15 +343,16 @@ static inline bool mt_transport_tx_room_locked(struct mt_transport_priv *priv) return occupied < MT_TRANSPORT_TX_QUEUE_DEPTH; } -/// @brief 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. +/* + * 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_priv *priv) { unsigned long flags; @@ -541,6 +545,14 @@ static int mt_transport_probe(struct spi_device *spi) * 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) + return dev_err_probe(dev, -EPROBE_DEFER, "NRDY IRQ not ready yet\n"); if (priv->nrdy_irq > 0) { ret = devm_request_threaded_irq(dev, priv->nrdy_irq, NULL, mt_transport_hw_linux_nrdy_irq, From 05ac4fc944e5fa3577884b57be2a457d4c495333 Mon Sep 17 00:00:00 2001 From: Michael Loh Date: Wed, 19 Aug 2026 16:20:39 -0600 Subject: [PATCH 10/35] MT-158113: third round of Copilot review fixes (DT schema, fail-fast) - multitracks,spi-transport.yaml: additionalProperties: false without referencing spi-peripheral-props.yaml prevented standard SPI properties (spi-cpha, spi-cpol, etc.) and diverged from this tree's own SPI binding convention (see spi-mux.yaml). Switched to allOf + $ref: spi-peripheral-props.yaml# + unevaluatedProperties: false, and added spi-max-frequency to required (generally expected for SPI peripherals). - Makefile: added an explicit $(error ...) check for core/'s presence before the obj-y list, so a build attempted outside the Yocto pipeline fails immediately with a clear message instead of an opaque "No rule to make target core/spi_transport.o" deep in the build. Found by Copilot's third review pass on PR #46. --- .../spi/multitracks,spi-transport.yaml | 8 +++++--- drivers/spi/spi-mt-transport/Makefile | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml index 65599f2231340..f634b20835439 100644 --- a/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml +++ b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml @@ -22,6 +22,9 @@ description: | 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 @@ -29,8 +32,6 @@ properties: reg: maxItems: 1 - spi-max-frequency: true - mt-nss-gpios: maxItems: 1 description: @@ -49,10 +50,11 @@ properties: required: - compatible - reg + - spi-max-frequency - mt-nss-gpios - mt-nrdy-gpios -additionalProperties: false +unevaluatedProperties: false examples: - | diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile index 11327e2e4fb59..210ad299dad81 100644 --- a/drivers/spi/spi-mt-transport/Makefile +++ b/drivers/spi/spi-mt-transport/Makefile @@ -9,10 +9,20 @@ # 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. Enabling CONFIG_SPI_MT_TRANSPORT -# outside that pipeline will fail with a generic "No rule to make target -# core/spi_transport.o" -- that's expected, not a bug; get the core staged -# first (or just build via bitbake, which always does this automatically). +# 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. +ifeq ($(wildcard $(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 \ From 47c3d31357d5643da7f8d9a8162e411ad5a59488 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:42:28 -0600 Subject: [PATCH 11/35] MT-158113: fourth round of Copilot review fixes (IRQ0, remove() race) - gpiod_to_irq() can legally return 0 as a valid IRQ number on some platforms/irqdomains; the probe()'s "if (nrdy_irq > 0)" check treated IRQ0 as "no IRQ available" and silently fell back to tick-poll only. Changed to ">= 0" per gpiod_to_irq()'s own contract (only negative values mean "no IRQ"), matching the convention in drivers/pps/clients/pps-gpio.c. - mt_transport_remove() had a teardown race: the NRDY threaded IRQ is devm-managed, so it's only actually freed by the driver core *after* remove() returns -- it stays live through misc_deregister()/ kthread_stop()/spiTransportStop(), and a real STM32 Client peer doesn't stop toggling NRDY just because this side is unbinding. If it fired during that window, mt_transport_hw_linux_nrdy_irq() could call priv->hw.pOnReadyEvent() into a core that's mid-teardown. Added a nrdy_irq_requested flag (set only on successful IRQ registration) and an explicit devm_free_irq() as the first action in remove() -- devm_free_irq() blocks until any in-flight threaded-handler invocation finishes, so it can't race an in-flight callback either. - Makefile: the core/ fail-fast guard's $(wildcard $(src)/...) check was missing the $(srctree)/ prefix that this same file's ccflags-y line already documented as required for Yocto's out-of-tree (O=) builds -- caused a false-positive "core/ is not staged" failure even when core/ was genuinely present. Added the missing prefix. --- drivers/spi/spi-mt-transport/Makefile | 7 ++++- .../spi-mt-transport/spi_mt_transport_drv.c | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-mt-transport/Makefile b/drivers/spi/spi-mt-transport/Makefile index 210ad299dad81..f867f1e30b3c0 100644 --- a/drivers/spi/spi-mt-transport/Makefile +++ b/drivers/spi/spi-mt-transport/Makefile @@ -16,7 +16,12 @@ # 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. -ifeq ($(wildcard $(src)/core/spi_transport.c),) +# $(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 \ diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 6624df19fcff9..4a283c42b8a2e 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -61,6 +61,9 @@ struct mt_transport_priv { 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; @@ -553,7 +556,12 @@ static int mt_transport_probe(struct spi_device *spi) */ if (priv->nrdy_irq == -EPROBE_DEFER) return dev_err_probe(dev, -EPROBE_DEFER, "NRDY IRQ not ready yet\n"); - if (priv->nrdy_irq > 0) { + /* >= 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 @@ -561,6 +569,8 @@ static int mt_transport_probe(struct spi_device *spi) 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"); } @@ -604,6 +614,21 @@ static void mt_transport_remove(struct spi_device *spi) { struct mt_transport_priv *priv = spi_get_drvdata(spi); + /* 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); + misc_deregister(&priv->misc); /* kthread_stop() blocks until the tick thread's loop actually exits, * guaranteeing no thread is still calling spiTransportTick()/ From 0ddfa209daabdefc8c87d533f9ef4a53040dd9a1 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:08:05 -0600 Subject: [PATCH 12/35] MT-158113: fifth round of Copilot review fixes (miscdevice UAF, reader race) Copilot's review of commit 47c3d313 (the IRQ0/remove()-race fix) found two new issues: - priv is devm_kzalloc'd against the SPI device, so it's freed as soon as remove() returns -- but nothing stops /dev/mt_spi_transport from staying open across an unbind. The next read()/write() on a surviving fd would touch freed memory (the miscdevice itself is embedded in priv). Fixed by switching priv to a plain kzalloc + kref: one reference is held by the driver instance (taken in probe(), dropped at the end of remove()), and one more per open fd (mt_transport_misc_open()/_release()) -- priv is only actually freed once both are gone. Every probe() error path after the allocation now does kref_put() instead of relying on devm. Also added a `removed` flag, set first thing in remove(), checked in read()/write(): this is a fail-fast measure on top of the memory-safety fix above -- e.g. write()'s priv->os.pTaskNotifyGive() call would otherwise still reach into a tick thread/os_ctx that remove() just tore down, even though touching that (now up-to-date) memory itself is safe. - mt_transport_misc_read()'s rx_valid check (either the O_NONBLOCK branch or wait_event_interruptible) isn't atomic with the later lock-protected consume step, so two concurrent readers could both observe rx_valid and both consume the same buffered message. Fixed by enforcing single-open semantics (an `available` atomic_t) in mt_transport_misc_open() -- matches Copilot's own suggested resolution, and is the correct semantic for this single-channel, single-in-flight-message placeholder interface (see the file banner) with exactly one intended userspace peer, not just a workaround. --- .../spi-mt-transport/spi_mt_transport_drv.c | 146 ++++++++++++++++-- 1 file changed, 132 insertions(+), 14 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 4a283c42b8a2e..13e8985f8c6a2 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -38,6 +38,7 @@ #include #include #include +#include #include "spi_transport/spi_transport.h" #include "spi_transport/spi_transport_types.h" @@ -136,6 +137,31 @@ struct mt_transport_priv { 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 miscdevice can outlive the SPI device across an + * unbind if userspace still holds /dev/mt_spi_transport 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 + * (mt_transport_misc_open()/_release()). Found by Copilot's PR #46 + * review (miscdevice/priv lifetime UAF). + */ + struct kref refcount; + atomic_t available; /* 1 = no fd currently open, 0 = one is -- + * enforces single-open semantics, 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, there is only ever one reader. + */ + 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 @@ -279,6 +305,44 @@ static int mt_transport_tick_thread_fn(void *data) /* --- 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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + + (void)inode; + + if (READ_ONCE(priv->removed)) + return -ENODEV; + + if (!atomic_dec_and_test(&priv->available)) { + atomic_inc(&priv->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_priv *priv = container_of(misc, struct mt_transport_priv, misc); + + (void)inode; + + atomic_inc(&priv->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) { @@ -297,6 +361,9 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ if (count == 0) return 0; + if (READ_ONCE(priv->removed)) + return -ENODEV; + if (filp->f_flags & O_NONBLOCK) { spin_lock_irqsave(&priv->rx_lock, irqflags); if (!priv->rx_valid) { @@ -386,6 +453,9 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf 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)) @@ -433,6 +503,8 @@ static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) 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, @@ -493,10 +565,18 @@ static int mt_transport_probe(struct spi_device *spi) trSpiTransportConfig config; int ret; - priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + /* 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 + * /dev/mt_spi_transport open. + */ + priv = kzalloc(sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; + kref_init(&priv->refcount); + atomic_set(&priv->available, 1); + priv->spi = spi; priv->dev = dev; spi_set_drvdata(spi, priv); @@ -512,18 +592,24 @@ static int mt_transport_probe(struct spi_device *spi) * outside the SPI core's own chip-select handling. */ priv->nss_gpiod = devm_gpiod_get(dev, "mt-nss", GPIOD_OUT_HIGH); - if (IS_ERR(priv->nss_gpiod)) - return dev_err_probe(dev, PTR_ERR(priv->nss_gpiod), - "failed to get mt-nss-gpios\n"); + 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 mt-nss-gpios\n"); + } priv->nrdy_gpiod = devm_gpiod_get(dev, "mt-nrdy", GPIOD_IN); - if (IS_ERR(priv->nrdy_gpiod)) - return dev_err_probe(dev, PTR_ERR(priv->nrdy_gpiod), - "failed to get mt-nrdy-gpios\n"); + 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 mt-nrdy-gpios\n"); + } ret = mt_transport_os_linux_init(&priv->os_ctx, dev, &priv->os); - if (ret) + 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); @@ -531,14 +617,18 @@ static int mt_transport_probe(struct spi_device *spi) config.prOs = &priv->os; config.prHw = &priv->hw; - if (spiTransportInit(&config, &priv->htransport) != eSpiTransportErrorNone) + if (spiTransportInit(&config, &priv->htransport) != eSpiTransportErrorNone) { + kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); + } if (spiTransportRegisterChannel(priv->htransport, MT_TRANSPORT_CHANNEL, mt_transport_rx_callback, mt_transport_event_callback, priv) - != eSpiTransportErrorNone) + != eSpiTransportErrorNone) { + kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel failed\n"); + } /* Optional latency optimization -- if the NRDY line has no usable * IRQ, tick-driven pReadyRead() polling (nominally every 2ms, see the @@ -554,8 +644,10 @@ static int mt_transport_probe(struct spi_device *spi) * 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) + 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 @@ -576,12 +668,15 @@ static int mt_transport_probe(struct spi_device *spi) } priv->tick_thread = kthread_run(mt_transport_tick_thread_fn, priv, "%s-tick", DRIVER_NAME); - if (IS_ERR(priv->tick_thread)) - return dev_err_probe(dev, PTR_ERR(priv->tick_thread), - "failed to start tick thread\n"); + if (IS_ERR(priv->tick_thread)) { + ret = PTR_ERR(priv->tick_thread); + 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) { kthread_stop(priv->tick_thread); + kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); } @@ -603,6 +698,7 @@ static int mt_transport_probe(struct spi_device *spi) */ kthread_stop(priv->tick_thread); spiTransportStop(priv->htransport); + kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, ret, "misc_register failed\n"); } @@ -614,6 +710,19 @@ static void mt_transport_remove(struct spi_device *spi) { struct mt_transport_priv *priv = spi_get_drvdata(spi); + /* 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/available/removed comment). Found by + * Copilot's PR #46 review. + */ + WRITE_ONCE(priv->removed, true); + /* 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 @@ -639,6 +748,15 @@ static void mt_transport_remove(struct spi_device *spi) */ 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, this is the last + * reference and priv is freed right here -- same timing as the + * devm-based lifetime this replaced. If userspace still holds + * /dev/mt_spi_transport 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[] = { From b9362b9c6be20123c50c269576e3d93741531027 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:26:13 -0600 Subject: [PATCH 13/35] MT-158113: sixth round of Copilot review fixes (wake removed waiters) Copilot's review of commit 0ddfa209 (the miscdevice UAF/reader-race fix) found the removed flag it added only helps a read()/write() call that checks it on entry -- a reader/writer already blocked in wait_event_interruptible() on rx_wq/tx_free_wq when mt_transport_remove() runs would never wake up, since neither wait condition considered removed and remove() never woke either queue. Fixed: - rx_wq's and tx_free_wq's wait_event_interruptible() conditions now also check READ_ONCE(priv->removed); the caller returns -ENODEV if that's why it woke. - mt_transport_remove() now calls wake_up_interruptible_all() on both queues immediately after setting removed, so any waiter already asleep re-checks its condition and exits instead of hanging across unbind. - mt_transport_misc_poll() now reports EPOLLHUP|EPOLLERR once removed is set, so an fd that outlives unbind can detect teardown via poll() too, not just its next read()/write(). --- .../spi-mt-transport/spi_mt_transport_drv.c | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 13e8985f8c6a2..8d3ca0bc2a23f 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -372,9 +372,19 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ } spin_unlock_irqrestore(&priv->rx_lock, irqflags); } else { - ret = wait_event_interruptible(priv->rx_wq, READ_ONCE(priv->rx_valid)); + /* 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(priv->rx_wq, + READ_ONCE(priv->rx_valid) || READ_ONCE(priv->removed)); if (ret) return ret; + if (READ_ONCE(priv->removed)) + return -ENODEV; } /* Snapshot into a local buffer under the lock, then copy_to_user() @@ -469,9 +479,18 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf if (filp->f_flags & O_NONBLOCK) return -EAGAIN; - ret = wait_event_interruptible(priv->tx_free_wq, mt_transport_tx_has_room(priv)); + /* 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(priv->tx_free_wq, + mt_transport_tx_has_room(priv) || READ_ONCE(priv->removed)); if (ret) return ret; + if (READ_ONCE(priv->removed)) + return -ENODEV; } idx = priv->tx_tail; @@ -496,6 +515,14 @@ static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) __poll_t mask = 0; poll_wait(filp, &priv->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(priv->rx_valid)) mask |= EPOLLIN | EPOLLRDNORM; return mask; @@ -722,6 +749,14 @@ static void mt_transport_remove(struct spi_device *spi) * Copilot's PR #46 review. */ WRITE_ONCE(priv->removed, true); + /* Wake any reader/writer already blocked in wait_event_interruptible() + * on 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. + */ + wake_up_interruptible_all(&priv->rx_wq); + wake_up_interruptible_all(&priv->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 From a50620713c11ea9195833d55047c209737a55c10 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:42:49 -0600 Subject: [PATCH 14/35] MT-158113: seventh round of Copilot review fixes (brace style nit) struct mt_transport_tx_slot had its opening brace on its own line, inconsistent with Linux kernel style and every other struct in this file (e.g. struct mt_transport_priv). Matches checkpatch's expectation. --- drivers/spi/spi-mt-transport/spi_mt_transport_drv.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 8d3ca0bc2a23f..bef7b6226a840 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -49,8 +49,7 @@ #define MT_TRANSPORT_CHANNEL 1 #define MT_TRANSPORT_TX_QUEUE_DEPTH 10 -struct mt_transport_tx_slot -{ +struct mt_transport_tx_slot { uint8_t buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; uint16_t len; }; From 3315caaefef44ed51fc09187dc50df9fe3167acc Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:00:24 -0600 Subject: [PATCH 15/35] MT-158113: document known tx_in_flight_idx capacity gap (MT-158925) Copilot's review correctly noted tx_in_flight_idx is set on every successful spiTransportSend() but never cleared back to -1, so mt_transport_tx_room_locked() permanently counts one slot as occupied from the first successful send onward -- DEPTH-1 usable slots instead of DEPTH. Not fixing inline: this is a symptom of an already-tracked core API gap (MT-158925 item 1 -- spiTransportSend() has no TX-completion signal), not something the driver can correctly resolve on its own. A driver-side guess (e.g. a timeout) would reintroduce the exact flat-timeout anti-pattern MT-158925 already documents as having cost ~200x real throughput earlier in this ticket. Documented in code with the tracking reference instead, matching this file's existing convention for deliberately-deferred decisions (see the hardcoded misc device name comment). --- .../spi-mt-transport/spi_mt_transport_drv.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index bef7b6226a840..59f6ad19de26e 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -113,7 +113,23 @@ struct mt_transport_priv { * tx_queued_count + (tx_in_flight_idx >= 0 ? 1 : 0) < DEPTH. */ unsigned int tx_queued_count; - int tx_in_flight_idx; /* -1 if nothing submitted yet */ + 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; /* Link-wide event counters -- mirrors the STM32 Client harness's From cbe5980e1253a95155219a175305ebb1f424c74a Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:14:11 -0600 Subject: [PATCH 16/35] MT-158113: ninth round of Copilot review fixes (probe() IRQ-vs-kref UAF, COMPILE_TEST) - Converting priv to a kref-refcounted allocation (previous round) fixed the miscdevice-outliving-unbind UAF, but introduced a narrower one: probe()'s error paths after the NRDY IRQ is successfully requested (tick_thread failure, spiTransportStart failure, misc_register failure) now call kref_put(), which can free priv's memory (including the embedded hw_ctx the IRQ handler dereferences) synchronously and immediately -- unlike the old devm_kzalloc, which deferred that same free to the same devm unwind pass that frees the IRQ first (reverse registration order), so the two frees used to always happen in a safe order automatically. Fixed by explicitly freeing the IRQ (mirroring mt_transport_remove()'s own guard) before kref_put() in all three paths -- Copilot's review only flagged two of these (spiTransportStart, misc_register); the tick_thread failure path has the identical bug since IRQ registration happens even earlier in probe(), fixed here too. - SPI_MT_TRANSPORT depended on "ARCH_MXC || COMPILE_TEST", but this module's core/ only exists once Yocto's meta-mt-transport-evk stages it in -- COMPILE_TEST=y would let allmodconfig/allyesconfig CI builds select it outside that pipeline and hit the Makefile's hard error. Dropped COMPILE_TEST from the depends line. --- drivers/spi/Kconfig | 7 ++++++- .../spi/spi-mt-transport/spi_mt_transport_drv.c | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index c685bb5b05642..0a76fb0b763c6 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1213,7 +1213,12 @@ config SPI_LOOPBACK_TEST config SPI_MT_TRANSPORT tristate "MultiTracks SPI transport protocol driver (Host role)" - depends on ARCH_MXC || COMPILE_TEST + # 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 help Kernel Host-role port of the MultiTracks SPI transport protocol diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 59f6ad19de26e..e7afd8b27f461 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -712,12 +712,27 @@ static int mt_transport_probe(struct spi_device *spi) 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) { kthread_stop(priv->tick_thread); + 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, -EINVAL, "spiTransportStart failed\n"); } @@ -740,6 +755,8 @@ static int mt_transport_probe(struct spi_device *spi) */ kthread_stop(priv->tick_thread); spiTransportStop(priv->htransport); + 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, "misc_register failed\n"); } From d6d05b21a9f7983edea29033946a4a6c4f54a003 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:35:01 -0600 Subject: [PATCH 17/35] MT-158113: tenth round of Copilot review fixes (va_list portability) mt_os_log() built struct va_format with `.va = &args`, where args is a va_list function parameter. On architectures where va_list is an array type, array-type parameters decay to pointers, so &args points at the local decayed pointer rather than a real va_list object -- %pV's va_arg()-based consumption of vaf->va would then be operating on the wrong representation. Fixed with the standard portable idiom: va_copy() into a genuinely local va_list and point .va at that copy instead, which is valid regardless of the platform's va_list representation. --- .../spi-mt-transport/spi_transport_os_linux.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/spi_transport_os_linux.c b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c index 77fe9928fd7eb..945874e3d0f48 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_os_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_os_linux.c @@ -99,9 +99,23 @@ static void mt_os_critical_exit(void *pContext) static void mt_os_log(void *pContext, const char *pFormat, va_list args) { struct mt_transport_os_ctx *ctx = pContext; - struct va_format vaf = { .fmt = pFormat, .va = &args }; + 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, From f6dab878c98b44941cc7fddf4fb98738e0ea3b78 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:55:58 -0600 Subject: [PATCH 18/35] MT-158113: add channel 2 (Property Model) support to spi-mt-transport --- .../spi-mt-transport/spi_mt_transport_drv.c | 397 +++++++++++------- 1 file changed, 244 insertions(+), 153 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index e7afd8b27f461..d9dc9e471bc6e 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -16,9 +16,12 @@ * * Scope note (MT-158113): this is the driver only. The EVK-side test * framework (MT-158682) is a separate ticket -- the userspace interface - * below (misc device + a small TX ring, see mt_transport_tx_service()) - * is still a placeholder ahead of MT-158682's real design: single - * hardcoded channel, single in-flight RX message, no ioctl/config surface. + * below (one misc device per channel + a small per-channel TX ring, see + * mt_transport_tx_service()) is still a placeholder ahead of a real config + * surface: exactly two hardcoded channels (1: Transport Services, 2: + * Property Model -- see firmware-common/mt_ipc_fbs/docs/ChannelMapping.md, + * MT-148208), one misc device per channel, single in-flight RX message per + * channel, no ioctl/config surface. */ #include @@ -46,36 +49,45 @@ #include "spi_transport_hw_linux.h" #define DRIVER_NAME "spi-mt-transport" -#define MT_TRANSPORT_CHANNEL 1 #define MT_TRANSPORT_TX_QUEUE_DEPTH 10 +/* Exactly two channels, per firmware-common/mt_ipc_fbs/docs/ChannelMapping.md + * (MT-148208): 1 = Transport Services, 2 = Property Model. Deliberately not + * a general N-channel design -- see this file's header. Channel index 0's + * misc device name/number are unchanged from the original single-channel + * driver, for backward compatibility with existing tools/scripts. + */ +#define MT_TRANSPORT_NUM_CHANNELS 2 +#define MT_TRANSPORT_CHANNEL_1 1 +#define MT_TRANSPORT_CHANNEL_2 2 + struct mt_transport_tx_slot { uint8_t buf[SPI_TRANSPORT_CHANNEL_MESSAGE_MAX]; uint16_t len; }; -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 mt_transport_priv; - struct task_struct *tick_thread; +/* 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; /* Minimal placeholder userspace interface -- MT-158682 owns the real - * design. Single hardcoded channel, single in-flight RX message, - * blocking read()/write(), best-effort poll(). + * 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; @@ -132,6 +144,44 @@ struct mt_transport_priv { */ 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; + + /* Two channels, per firmware-common/mt_ipc_fbs/docs/ChannelMapping.md + * (MT-148208): index 0 = Transport Services (channel 1, the + * original single-channel device, unchanged name/number), index 1 = + * Property Model (channel 2, new). See 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 @@ -143,7 +193,9 @@ struct mt_transport_priv { * 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. + * 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; @@ -154,23 +206,15 @@ struct mt_transport_priv { atomic_t evt_dma_timeout; /* priv is kzalloc'd, not devm_kzalloc'd (see mt_transport_probe()): - * the embedded miscdevice can outlive the SPI device across an - * unbind if userspace still holds /dev/mt_spi_transport open, so + * 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 - * (mt_transport_misc_open()/_release()). Found by Copilot's PR #46 - * review (miscdevice/priv lifetime UAF). + * (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; - atomic_t available; /* 1 = no fd currently open, 0 = one is -- - * enforces single-open semantics, 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, there is only ever one reader. - */ 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 @@ -194,18 +238,22 @@ static void mt_transport_tick_notify(void *pNotifyCtx) static void mt_transport_rx_callback(void *pContext, uint8_t channel, const uint8_t *pBuffer, uint16_t length, uint8_t flags) { - struct mt_transport_priv *priv = pContext; + struct mt_transport_channel *chan = pContext; unsigned long irqflags; (void)flags; - if (channel != MT_TRANSPORT_CHANNEL) - return; - if (length > sizeof(priv->rx_buf)) - length = sizeof(priv->rx_buf); + /* 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(&priv->rx_lock, irqflags); - memcpy(priv->rx_buf, pBuffer, length); - priv->rx_len = length; + 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 @@ -215,10 +263,10 @@ static void mt_transport_rx_callback(void *pContext, uint8_t channel, const uint * ordering -- this is about being explicit for readers/tooling like * KCSAN, not fixing an actual race). */ - WRITE_ONCE(priv->rx_valid, true); - spin_unlock_irqrestore(&priv->rx_lock, irqflags); + WRITE_ONCE(chan->rx_valid, true); + spin_unlock_irqrestore(&chan->rx_lock, irqflags); - wake_up_interruptible(&priv->rx_wq); + wake_up_interruptible(&chan->rx_wq); } static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) @@ -265,8 +313,8 @@ static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEve * 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 tick thread iteration; a Busy - * return just means retry next tick, no state changes. + * 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 @@ -277,43 +325,45 @@ static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEve * 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_priv *priv) +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(&priv->tx_lock, flags); - if (priv->tx_queued_count == 0) { - spin_unlock_irqrestore(&priv->tx_lock, flags); + spin_lock_irqsave(&chan->tx_lock, flags); + if (chan->tx_queued_count == 0) { + spin_unlock_irqrestore(&chan->tx_lock, flags); return; } - idx = priv->tx_head; - len = priv->tx_slots[idx].len; - spin_unlock_irqrestore(&priv->tx_lock, flags); + idx = chan->tx_head; + len = chan->tx_slots[idx].len; + spin_unlock_irqrestore(&chan->tx_lock, flags); - err = spiTransportSend(priv->htransport, MT_TRANSPORT_CHANNEL, priv->tx_slots[idx].buf, len, - true); + err = spiTransportSend(chan->priv->htransport, chan->channel_num, chan->tx_slots[idx].buf, + len, true); if (err != eSpiTransportErrorNone) return; - spin_lock_irqsave(&priv->tx_lock, flags); - priv->tx_queued_count--; - priv->tx_in_flight_idx = idx; - priv->tx_head = (priv->tx_head + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; - spin_unlock_irqrestore(&priv->tx_lock, flags); - wake_up_interruptible(&priv->tx_free_wq); + 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); - mt_transport_tx_service(priv); + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) + mt_transport_tx_service(&priv->channels[i]); } return 0; } @@ -330,15 +380,16 @@ static void mt_transport_priv_release(struct kref *kref) static int mt_transport_misc_open(struct inode *inode, struct file *filp) { struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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(&priv->available)) { - atomic_inc(&priv->available); + if (!atomic_dec_and_test(&chan->available)) { + atomic_inc(&chan->available); return -EBUSY; } @@ -349,11 +400,12 @@ static int mt_transport_misc_open(struct inode *inode, struct file *filp) static int mt_transport_misc_release(struct inode *inode, struct file *filp) { struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + struct mt_transport_channel *chan = container_of(misc, struct mt_transport_channel, misc); + struct mt_transport_priv *priv = chan->priv; (void)inode; - atomic_inc(&priv->available); + atomic_inc(&chan->available); kref_put(&priv->refcount, mt_transport_priv_release); return 0; } @@ -362,7 +414,8 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ loff_t *ppos) { struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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; @@ -380,12 +433,12 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ return -ENODEV; if (filp->f_flags & O_NONBLOCK) { - spin_lock_irqsave(&priv->rx_lock, irqflags); - if (!priv->rx_valid) { - spin_unlock_irqrestore(&priv->rx_lock, irqflags); + spin_lock_irqsave(&chan->rx_lock, irqflags); + if (!chan->rx_valid) { + spin_unlock_irqrestore(&chan->rx_lock, irqflags); return -EAGAIN; } - spin_unlock_irqrestore(&priv->rx_lock, irqflags); + spin_unlock_irqrestore(&chan->rx_lock, irqflags); } else { /* removed is included in the wait condition (not just checked * up front) so a reader already blocked here when @@ -394,8 +447,8 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ * setting removed for exactly this reason. Found by * Copilot's PR #46 review. */ - ret = wait_event_interruptible(priv->rx_wq, - READ_ONCE(priv->rx_valid) || READ_ONCE(priv->removed)); + 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)) @@ -410,13 +463,13 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ * consumes the buffered message rather than leaving it for retry, * a minor, acceptable behavior change for this placeholder interface. */ - spin_lock_irqsave(&priv->rx_lock, irqflags); - len = priv->rx_len; + spin_lock_irqsave(&chan->rx_lock, irqflags); + len = chan->rx_len; if (len > count) len = count; - memcpy(scratch, priv->rx_buf, len); - WRITE_ONCE(priv->rx_valid, false); - spin_unlock_irqrestore(&priv->rx_lock, irqflags); + memcpy(scratch, chan->rx_buf, len); + WRITE_ONCE(chan->rx_valid, false); + spin_unlock_irqrestore(&chan->rx_lock, irqflags); if (copy_to_user(buf, scratch, len)) return -EFAULT; @@ -431,9 +484,9 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ * 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_priv *priv) +static inline bool mt_transport_tx_room_locked(struct mt_transport_channel *chan) { - unsigned int occupied = priv->tx_queued_count + (priv->tx_in_flight_idx >= 0 ? 1 : 0); + unsigned int occupied = chan->tx_queued_count + (chan->tx_in_flight_idx >= 0 ? 1 : 0); return occupied < MT_TRANSPORT_TX_QUEUE_DEPTH; } @@ -448,14 +501,14 @@ static inline bool mt_transport_tx_room_locked(struct mt_transport_priv *priv) * 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_priv *priv) +static bool mt_transport_tx_has_room(struct mt_transport_channel *chan) { unsigned long flags; bool room; - spin_lock_irqsave(&priv->tx_lock, flags); - room = mt_transport_tx_room_locked(priv); - spin_unlock_irqrestore(&priv->tx_lock, flags); + spin_lock_irqsave(&chan->tx_lock, flags); + room = mt_transport_tx_room_locked(chan); + spin_unlock_irqrestore(&chan->tx_lock, flags); return room; } @@ -463,7 +516,8 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf loff_t *ppos) { struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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; @@ -487,10 +541,10 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf return -EFAULT; for (;;) { - spin_lock_irqsave(&priv->tx_lock, flags); - if (mt_transport_tx_room_locked(priv)) + spin_lock_irqsave(&chan->tx_lock, flags); + if (mt_transport_tx_room_locked(chan)) break; - spin_unlock_irqrestore(&priv->tx_lock, flags); + spin_unlock_irqrestore(&chan->tx_lock, flags); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; @@ -500,20 +554,20 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf * forever; remove() wakes tx_free_wq right after setting * removed. Found by Copilot's PR #46 review. */ - ret = wait_event_interruptible(priv->tx_free_wq, - mt_transport_tx_has_room(priv) || READ_ONCE(priv->removed)); + 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 = priv->tx_tail; - memcpy(priv->tx_slots[idx].buf, scratch, len); - priv->tx_slots[idx].len = len; - priv->tx_tail = (priv->tx_tail + 1) % MT_TRANSPORT_TX_QUEUE_DEPTH; - priv->tx_queued_count++; - spin_unlock_irqrestore(&priv->tx_lock, flags); + 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. @@ -526,10 +580,11 @@ static ssize_t mt_transport_misc_write(struct file *filp, const char __user *buf static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) { struct miscdevice *misc = filp->private_data; - struct mt_transport_priv *priv = container_of(misc, struct mt_transport_priv, misc); + 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, &priv->rx_wq, wait); + 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 @@ -538,7 +593,7 @@ static __poll_t mt_transport_misc_poll(struct file *filp, poll_table *wait) */ if (READ_ONCE(priv->removed)) return EPOLLHUP | EPOLLERR; - if (READ_ONCE(priv->rx_valid)) + if (READ_ONCE(chan->rx_valid)) mask |= EPOLLIN | EPOLLRDNORM; return mask; } @@ -605,29 +660,49 @@ static int mt_transport_probe(struct spi_device *spi) struct device *dev = &spi->dev; struct mt_transport_priv *priv; trSpiTransportConfig config; + static const uint8_t channel_nums[MT_TRANSPORT_NUM_CHANNELS] = { + MT_TRANSPORT_CHANNEL_1, + MT_TRANSPORT_CHANNEL_2, + }; + /* Channel 1's name is unchanged from the original single-channel + * driver -- every existing tool/script keeps working unmodified. + * Channel 2 is new (MT-148208's Property Model). + */ + static const char *const channel_names[MT_TRANSPORT_NUM_CHANNELS] = { + "mt_spi_transport", + "mt_spi_transport_ch2", + }; 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 - * /dev/mt_spi_transport open. + * 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); - atomic_set(&priv->available, 1); priv->spi = spi; priv->dev = dev; spi_set_drvdata(spi, priv); - init_waitqueue_head(&priv->rx_wq); - spin_lock_init(&priv->rx_lock); - spin_lock_init(&priv->tx_lock); - init_waitqueue_head(&priv->tx_free_wq); - priv->tx_in_flight_idx = -1; + for (i = 0; i < MT_TRANSPORT_NUM_CHANNELS; i++) { + struct mt_transport_channel *chan = &priv->channels[i]; + + chan->priv = priv; + chan->channel_num = channel_nums[i]; + 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 "mt-nss"/"mt-nrdy" bindings, not the standard "cs-gpios" -- * see spi_transport_hw_linux.c's file comment for why these must stay @@ -664,12 +739,18 @@ static int mt_transport_probe(struct spi_device *spi) return dev_err_probe(dev, -EINVAL, "spiTransportInit failed\n"); } - if (spiTransportRegisterChannel(priv->htransport, MT_TRANSPORT_CHANNEL, - mt_transport_rx_callback, mt_transport_event_callback, - priv) - != eSpiTransportErrorNone) { - kref_put(&priv->refcount, mt_transport_priv_release); - return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel 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 @@ -737,37 +818,44 @@ static int mt_transport_probe(struct spi_device *spi) return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); } - /* Name hardcoded, not suffixed per-device (flagged by Copilot review -- - * see PR discussion): a second bound spi-mt-transport device would - * collide here, but this specific hardware only ever binds one, and - * changing the path breaks every existing script/tool this session - * built against /dev/mt_spi_transport. Left as-is pending a decision; - * see PR #785. - */ - priv->misc.minor = MISC_DYNAMIC_MINOR; - priv->misc.name = "mt_spi_transport"; - priv->misc.fops = &mt_transport_misc_fops; - ret = misc_register(&priv->misc); - if (ret) { - /* kthread_stop() before spiTransportStop(): the tick thread - * must not be able to call into the core after it's been - * stopped (same ordering as mt_transport_remove() below). - */ - kthread_stop(priv->tick_thread); - spiTransportStop(priv->htransport); - 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, "misc_register 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 = channel_names[i]; + chan->misc.fops = &mt_transport_misc_fops; + ret = misc_register(&chan->misc); + if (ret) { + int j; + + /* 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); + 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, "misc_register(%s) failed\n", + channel_names[i]); + } + started_misc++; } - dev_info(dev, "MultiTracks SPI transport driver probed (Host role)\n"); + 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 @@ -777,18 +865,20 @@ static void mt_transport_remove(struct spi_device *spi) * 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/available/removed comment). Found by + * 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 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. + * 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. */ - wake_up_interruptible_all(&priv->rx_wq); - wake_up_interruptible_all(&priv->tx_free_wq); + 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 @@ -805,7 +895,8 @@ static void mt_transport_remove(struct spi_device *spi) if (priv->nrdy_irq_requested) devm_free_irq(priv->dev, priv->nrdy_irq, &priv->hw_ctx); - misc_deregister(&priv->misc); + 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. @@ -817,10 +908,10 @@ static void mt_transport_remove(struct spi_device *spi) spiTransportStop(priv->htransport); /* Drops the driver instance's own reference (taken via kref_init() - * in probe()). If no fd is currently open, this is the last - * reference and priv is freed right here -- same timing as the - * devm-based lifetime this replaced. If userspace still holds - * /dev/mt_spi_transport open, priv stays alive (kept by that fd's + * 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); From 13bf6a0ebb4e37dfc844c8f47c16af07b9365f83 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:26:05 -0600 Subject: [PATCH 19/35] MT-158113: fix event_callback context type confusion (NULL-deref Oops) --- .../spi/spi-mt-transport/spi_mt_transport_drv.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index d9dc9e471bc6e..47337f4219363 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -271,7 +271,19 @@ static void mt_transport_rx_callback(void *pContext, uint8_t channel, const uint static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEvent) { - struct mt_transport_priv *priv = pContext; + /* 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: From 89bb2f26d563c3d4ee51df842c358b9abc1e6e29 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:01:24 -0600 Subject: [PATCH 20/35] MT-158113: eleventh round of Copilot review fixes (transferComplete ordering, NRDY errno-to-bool, probe IRQ-free ordering) Four real findings from PR #46's latest Copilot review: - mt_hw_spi_complete() signaled transferComplete before calling pOnTransferComplete(), letting mt_hw_abort() (waited on that same completion, runs on the tick thread) proceed while this SPI-completion context was still mutating core state -- a concurrent unsynchronized access. Reordered so the core callback runs first. - mt_hw_ready_read() and the NRDY IRQ handler both collapsed gpiod_get_value_cansleep()'s negative errno into a bare ternary, reporting a failed GPIO read as ready/high. Now check for the error: the tick-poll path fails closed (reports not-ready), the IRQ path skips the event entirely and relies on tick polling as the fallback. - probe()'s spiTransportStart()-failure and misc_register()-failure paths both freed the NRDY IRQ only after kthread_stop()/ spiTransportStop(), unlike mt_transport_remove()'s established ordering -- the threaded handler could still fire mid-teardown and call pOnReadyEvent() into an invalid transport. Reordered both to free the IRQ first, matching remove(). Compile-verified via a forced bitbake do_compile_kernelmodules rebuild: spi_transport_hw_linux.o, spi_mt_transport_drv.o, and spi-mt-transport.ko all built clean, zero warnings on the changed lines. Co-Authored-By: Claude Sonnet 5 --- .../spi-mt-transport/spi_mt_transport_drv.c | 19 +++++-- .../spi-mt-transport/spi_transport_hw_linux.c | 50 ++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 47337f4219363..a652a75c5fb23 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -823,9 +823,16 @@ static int mt_transport_probe(struct spi_device *spi) } if (spiTransportStart(priv->htransport) != eSpiTransportErrorNone) { - kthread_stop(priv->tick_thread); + /* 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"); } @@ -840,6 +847,14 @@ static int mt_transport_probe(struct spi_device *spi) 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); /* Roll back any channel's misc device already * registered before this one failed -- otherwise a * second channel's registration failure would leak @@ -850,8 +865,6 @@ static int mt_transport_probe(struct spi_device *spi) misc_deregister(&priv->channels[j].misc); kthread_stop(priv->tick_thread); spiTransportStop(priv->htransport); - 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, "misc_register(%s) failed\n", channel_names[i]); diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 43e3b70c88401..574454e4d5f10 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -54,15 +54,25 @@ 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; - /* Signal "msg/xfer no longer referenced by the SPI core" before - * notifying the core -- pNotify may wake the tick thread straight - * into a new pTransferStart(), which gates on this same completion. + /* 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. */ - complete(&ctx->transferComplete); - 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); } @@ -134,8 +144,20 @@ static void mt_hw_ready_assert(void *pContext, bool high) static bool mt_hw_ready_read(void *pContext) { struct mt_transport_hw_ctx *ctx = pContext; + int val = gpiod_get_value_cansleep(ctx->nrdy_gpiod); - return gpiod_get_value_cansleep(ctx->nrdy_gpiod) ? true : false; + /* 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; } /* @@ -225,10 +247,22 @@ void mt_transport_hw_linux_set_notify(struct mt_transport_hw_ctx *ctx, irqreturn_t mt_transport_hw_linux_nrdy_irq(int irq, void *dev_id) { struct mt_transport_hw_ctx *ctx = dev_id; - bool high = gpiod_get_value_cansleep(ctx->nrdy_gpiod) ? true : false; + 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, high); + ctx->pHw->pOnReadyEvent(ctx->pHw->pCoreCtx, val ? true : false); if (ctx->pNotify) ctx->pNotify(ctx->pNotifyCtx); From 1496c4bb20ee5ed6a18ebe875de77c4c1d9b1df9 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:32:36 -0600 Subject: [PATCH 21/35] MT-158113: address PR #46's suppressed Copilot findings (read race, rollback wakeup, no_llseek, SYSFS dep, vendor-prefixed GPIO bindings) Six real findings from the "suppressed comments" section of PR #46's Copilot reviews: - mt_transport_misc_read() only checked rx_valid once (either the O_NONBLOCK branch or the wait_event_interruptible() condition) before falling through to consume the buffer. Two threads sharing the same fd could both pass that check and race into the consume block -- the loser would copy the winner's already-cleared rx_buf/rx_len as a "fresh" message. Restructured into a loop that rechecks rx_valid under rx_lock immediately before consuming, so only the thread that actually observes it true there consumes it. - probe()'s misc_register() failure-rollback path deregistered any already-registered channel's misc device without first setting priv->removed or waking its wait queues, unlike mt_transport_remove(). A reader/writer already blocked on that channel's fd (registered before a later channel failed) could hang forever. Fixed to match remove()'s ordering. - Added .llseek = no_llseek to the misc device fops -- this device doesn't support offsets, matching the in-tree convention (e.g. drivers/spi/spidev.c). - Added `depends on SYSFS` to the Kconfig entry -- the driver unconditionally registers dev_groups/DEVICE_ATTR_RO sysfs attributes and calls sysfs_emit(). - Vendor-prefixed the driver-private GPIO bindings: mt-nss-gpios / mt-nrdy-gpios -> multitracks,nss-gpios / multitracks,nrdy-gpios, across the DT binding, the imx8mm-evk-spi-transport.dts overlay, and the driver's devm_gpiod_get() con_id strings. Registered "multitracks" in vendor-prefixes.yaml. Compile-verified via forced bitbake rebuilds: do_compile (Image + all dtbs, including the renamed imx8mm-evk-spi-transport.dtb -- DTC ran clean) and do_compile_kernelmodules (spi-mt-transport.ko built clean, CONFIG_SPI_MT_TRANSPORT=m unaffected by the new SYSFS dependency since CONFIG_SYSFS=y is already set on this board). Co-Authored-By: Claude Sonnet 5 --- .../spi/multitracks,spi-transport.yaml | 14 +-- .../devicetree/bindings/vendor-prefixes.yaml | 2 + .../freescale/imx8mm-evk-spi-transport.dts | 9 +- drivers/spi/Kconfig | 4 + .../spi-mt-transport/spi_mt_transport_drv.c | 97 +++++++++++++------ .../spi-mt-transport/spi_transport_hw_linux.c | 5 +- 6 files changed, 91 insertions(+), 40 deletions(-) diff --git a/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml index f634b20835439..b35a575c07cbe 100644 --- a/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml +++ b/Documentation/devicetree/bindings/spi/multitracks,spi-transport.yaml @@ -32,7 +32,7 @@ properties: reg: maxItems: 1 - mt-nss-gpios: + multitracks,nss-gpios: maxItems: 1 description: Host-driven NSS (transfer request) line, held low across the whole @@ -41,18 +41,18 @@ properties: value always equals the literal pin level (the protocol's own convention is "true = pin HIGH"). - mt-nrdy-gpios: + multitracks,nrdy-gpios: maxItems: 1 description: Client-driven NRDY (ready/ack) line, read by the Host. Same - GPIO_ACTIVE_HIGH convention as mt-nss-gpios. + GPIO_ACTIVE_HIGH convention as multitracks,nss-gpios. required: - compatible - reg - spi-max-frequency - - mt-nss-gpios - - mt-nrdy-gpios + - multitracks,nss-gpios + - multitracks,nrdy-gpios unevaluatedProperties: false @@ -68,8 +68,8 @@ examples: reg = <0>; spi-max-frequency = <500000>; - mt-nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; - mt-nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; + 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/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts index 0a9b51b2be1ac..9605044c8b35f 100644 --- a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -8,8 +8,9 @@ // 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 "mt-nss-gpios"/"mt-nrdy-gpios" -// properties, NOT the standard "cs-gpios" -- the transport protocol holds +// 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 @@ -33,8 +34,8 @@ reg = <0>; spi-max-frequency = <500000>; /* matches mt-connect.dts; revisit once link timing is measured */ - mt-nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; - mt-nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; + 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 0a76fb0b763c6..8710a56cc1306 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -1220,6 +1220,10 @@ config SPI_MT_TRANSPORT # 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, diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index a652a75c5fb23..27ddcc812f6e0 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -444,14 +444,46 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ if (READ_ONCE(priv->removed)) return -ENODEV; - if (filp->f_flags & O_NONBLOCK) { + /* 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) { + 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); - return -EAGAIN; + break; } spin_unlock_irqrestore(&chan->rx_lock, irqflags); - } else { + + 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 @@ -467,22 +499,6 @@ static ssize_t mt_transport_misc_read(struct file *filp, char __user *buf, size_ return -ENODEV; } - /* 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. - */ - spin_lock_irqsave(&chan->rx_lock, irqflags); - 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); - if (copy_to_user(buf, scratch, len)) return -EFAULT; @@ -617,6 +633,12 @@ static const struct file_operations mt_transport_misc_fops = { .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) @@ -716,22 +738,29 @@ static int mt_transport_probe(struct spi_device *spi) chan->tx_in_flight_idx = -1; } - /* Custom "mt-nss"/"mt-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. + /* 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, "mt-nss", GPIOD_OUT_HIGH); + 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 mt-nss-gpios\n"); + return dev_err_probe(dev, ret, "failed to get multitracks,nss-gpios\n"); } - priv->nrdy_gpiod = devm_gpiod_get(dev, "mt-nrdy", GPIOD_IN); + 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 mt-nrdy-gpios\n"); + 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); @@ -855,6 +884,20 @@ static int mt_transport_probe(struct spi_device *spi) */ 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 diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 574454e4d5f10..a18ce42ed7e46 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -29,8 +29,9 @@ * 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 "mt-nss-gpios"/"mt-nrdy-gpios" devicetree - * binding (see spi_mt_transport_drv.c) rather than the standard "cs-gpios" + * 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 From fa0c1708e759db20cb2f4bd71749c939d3cc357f Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:17:29 -0600 Subject: [PATCH 22/35] MT-159369: generalize spi-mt-transport to register all raw channels Replaces the hardcoded 2-channel design (channel_nums[]/channel_names[] static arrays, one array slot per known channel purpose) with a loop over every raw channel the spi-transport core supports (MT_TRANSPORT_NUM_CHANNELS = SPI_TRANSPORT_CHANNELS_MAX - 1, now 19 after the companion firmware-repo constant bump). Channel 1 keeps its unprefixed "mt_spi_transport" name for backward compatibility; every other channel gets "mt_spi_transport_ch" built at probe() time into a new per-channel misc_name[] buffer (misc.name must outlive misc_register(), so it can't be a probe()-local buffer). This removes the need for any further kernel-driver change when a new channel gets a purpose assigned above this layer (legacy IPC tunnel on channel 3 for MT-159369, future MIDI/debug/vendor channels on 4+) -- what a channel carries is now purely a userspace/documentation fact (firmware-common/mt_ipc_fbs/docs/ChannelMapping.md), never something this driver needs to know about. Co-Authored-By: Claude Sonnet 5 --- .../spi-mt-transport/spi_mt_transport_drv.c | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index 27ddcc812f6e0..a58dcc7efd485 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -16,12 +16,18 @@ * * 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 channel + a small per-channel TX ring, see + * 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: exactly two hardcoded channels (1: Transport Services, 2: - * Property Model -- see firmware-common/mt_ipc_fbs/docs/ChannelMapping.md, - * MT-148208), one misc device per channel, single in-flight RX message per - * channel, no ioctl/config surface. + * 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 @@ -51,15 +57,19 @@ #define DRIVER_NAME "spi-mt-transport" #define MT_TRANSPORT_TX_QUEUE_DEPTH 10 -/* Exactly two channels, per firmware-common/mt_ipc_fbs/docs/ChannelMapping.md - * (MT-148208): 1 = Transport Services, 2 = Property Model. Deliberately not - * a general N-channel design -- see this file's header. Channel index 0's - * misc device name/number are unchanged from the original single-channel - * driver, for backward compatibility with existing tools/scripts. +/* 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 2 -#define MT_TRANSPORT_CHANNEL_1 1 -#define MT_TRANSPORT_CHANNEL_2 2 +#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]; @@ -84,6 +94,13 @@ struct mt_transport_channel { * 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 @@ -175,10 +192,11 @@ struct mt_transport_priv { struct task_struct *tick_thread; - /* Two channels, per firmware-common/mt_ipc_fbs/docs/ChannelMapping.md - * (MT-148208): index 0 = Transport Services (channel 1, the - * original single-channel device, unchanged name/number), index 1 = - * Property Model (channel 2, new). See struct mt_transport_channel. + /* 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]; @@ -694,18 +712,6 @@ static int mt_transport_probe(struct spi_device *spi) struct device *dev = &spi->dev; struct mt_transport_priv *priv; trSpiTransportConfig config; - static const uint8_t channel_nums[MT_TRANSPORT_NUM_CHANNELS] = { - MT_TRANSPORT_CHANNEL_1, - MT_TRANSPORT_CHANNEL_2, - }; - /* Channel 1's name is unchanged from the original single-channel - * driver -- every existing tool/script keeps working unmodified. - * Channel 2 is new (MT-148208's Property Model). - */ - static const char *const channel_names[MT_TRANSPORT_NUM_CHANNELS] = { - "mt_spi_transport", - "mt_spi_transport_ch2", - }; int ret; int i; int started_misc = 0; @@ -729,7 +735,12 @@ static int mt_transport_probe(struct spi_device *spi) struct mt_transport_channel *chan = &priv->channels[i]; chan->priv = priv; - chan->channel_num = channel_nums[i]; + 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); @@ -870,7 +881,7 @@ static int mt_transport_probe(struct spi_device *spi) struct mt_transport_channel *chan = &priv->channels[i]; chan->misc.minor = MISC_DYNAMIC_MINOR; - chan->misc.name = channel_names[i]; + chan->misc.name = chan->misc_name; chan->misc.fops = &mt_transport_misc_fops; ret = misc_register(&chan->misc); if (ret) { @@ -910,7 +921,7 @@ static int mt_transport_probe(struct spi_device *spi) spiTransportStop(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, ret, "misc_register(%s) failed\n", - channel_names[i]); + chan->misc_name); } started_misc++; } From 9c926b220e1122d81e336770c0598d86652183b1 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:03:28 -0600 Subject: [PATCH 23/35] MT-159369: bind spi-mt-transport onto mt-connect.dts's ecspi2 (was spidev1) Ports the binding already hardware-validated on imx8mm-evk-spi-transport.dts (MT-158113) back to the production board -- same NSS/NRDY pins (GPIO5_IO13/GPIO4_IO29, this board's own SPI2_NSS/SPI2_INT pin names). Unlike the EVK, this board's existing pinctrl_ecspi2 group already covers both pin muxes, so no pinctrl changes were needed, just the child node swap and dropping cs-gpios (the driver owns NSS directly via multitracks,nss-gpios). This unblocks MT-159369's ipcspi.c rewrite (firmware repo) from actually being testable on real hardware -- /dev/mt_spi_transport_ch3 didn't exist on this devicetree before this commit. Co-Authored-By: Claude Sonnet 5 --- arch/arm64/boot/dts/freescale/mt-connect.dts | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index c56236f14e9f2..926fdaf2c465c 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>; + + multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; + multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; }; }; From de532a115b3cd8b14690496d6c72e8f2102c537c Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:10:11 -0600 Subject: [PATCH 24/35] MT-159369: bump spi-transport SPI clock from 500kHz to 10MHz Host drives the clock, so the only real constraint is whatever the STM32 Client side can match -- 500kHz was an arbitrary conservative starting point for initial bring-up, not a measured ceiling. STM32-side AI updating to match. Co-Authored-By: Claude Sonnet 5 --- arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts | 2 +- arch/arm64/boot/dts/freescale/mt-connect.dts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts index 9605044c8b35f..d7d0b29b2b73a 100644 --- a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -32,7 +32,7 @@ mt_transport0: spi@0 { compatible = "multitracks,spi-transport"; reg = <0>; - spi-max-frequency = <500000>; /* matches mt-connect.dts; revisit once link timing is measured */ + spi-max-frequency = <10000000>; /* matches mt-connect.dts -- bumped from 500kHz for MT-159369 */ multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index 926fdaf2c465c..518aa8d8d27bc 100644 --- a/arch/arm64/boot/dts/freescale/mt-connect.dts +++ b/arch/arm64/boot/dts/freescale/mt-connect.dts @@ -531,7 +531,7 @@ mt_transport0: spi@0 { compatible = "multitracks,spi-transport"; reg = <0>; - spi-max-frequency = <500000>; + spi-max-frequency = <10000000>; multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; From cac7f7c1bc21c9fe91ae40066b65ef36f54ab667 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:38:28 -0600 Subject: [PATCH 25/35] MT-159369: step spi-transport clock down to 4MHz to isolate rate-dependent stall 10MHz produces frequent DMA-arm-failure/CRC-error bursts within seconds on real MT Connect hardware; 500kHz was soak-tested clean for 1.17M+ writes across three separate multi-hour runs (never once). Testing 4MHz as a midpoint to see whether the stall clears, to distinguish a hard rate limit from a lingering software bug. Co-Authored-By: Claude Sonnet 5 --- arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts | 2 +- arch/arm64/boot/dts/freescale/mt-connect.dts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts index d7d0b29b2b73a..271cc35835278 100644 --- a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -32,7 +32,7 @@ mt_transport0: spi@0 { compatible = "multitracks,spi-transport"; reg = <0>; - spi-max-frequency = <10000000>; /* matches mt-connect.dts -- bumped from 500kHz for MT-159369 */ + 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>; diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index 518aa8d8d27bc..77053bf7b90a2 100644 --- a/arch/arm64/boot/dts/freescale/mt-connect.dts +++ b/arch/arm64/boot/dts/freescale/mt-connect.dts @@ -531,7 +531,7 @@ mt_transport0: spi@0 { compatible = "multitracks,spi-transport"; reg = <0>; - spi-max-frequency = <10000000>; + 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>; From 81e7f70f926e3afd6caf1b4ba0ffb948bfb0d241 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:55:57 -0600 Subject: [PATCH 26/35] MT-159369: step spi-transport clock back to 500kHz, isolate structural vs. clock-rate issue 10MHz and 4MHz both showed the identical 100%-failure DMA-arm-failure/CRC pattern (zero good frames received on either side, in any test) -- no improvement from the 20x-slower clock. Stepping back to the soak-tested rate to check whether this is even a clock-rate-dependent issue at all, before chasing it as a structural/framing bug instead. Timeouts (bumped 100ms->250ms / 250ms->500ms) are being kept as-is regardless of this clock change -- they address a real, separate Client-busy scenario, not tied to clock speed. Co-Authored-By: Claude Sonnet 5 --- arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts | 2 +- arch/arm64/boot/dts/freescale/mt-connect.dts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts index 271cc35835278..20784eec6695c 100644 --- a/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts +++ b/arch/arm64/boot/dts/freescale/imx8mm-evk-spi-transport.dts @@ -32,7 +32,7 @@ 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 */ + spi-max-frequency = <500000>; /* matches mt-connect.dts -- back to the soak-tested-clean rate, MT-159369 */ multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index 77053bf7b90a2..6a77d257548da 100644 --- a/arch/arm64/boot/dts/freescale/mt-connect.dts +++ b/arch/arm64/boot/dts/freescale/mt-connect.dts @@ -531,7 +531,7 @@ mt_transport0: spi@0 { compatible = "multitracks,spi-transport"; reg = <0>; - 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 */ + spi-max-frequency = <500000>; /* MT-159369: back to the soak-tested-clean rate -- 10MHz and 4MHz both showed the same 100%-failure DMA-arm/CRC pattern with zero improvement, so this is isolating whether the issue is clock-rate-dependent at all before chasing it as a structural/framing bug instead */ multitracks,nss-gpios = <&gpio5 13 GPIO_ACTIVE_HIGH>; multitracks,nrdy-gpios = <&gpio4 29 GPIO_ACTIVE_HIGH>; From 70215bb726355219d069020c7ed9076f7c1c2757 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:40:24 -0600 Subject: [PATCH 27/35] MT-159369: add arm-to-complete timing instrumentation for the DMA-arm-failure investigation Records ktime_get() at successful spi_async() submission (mt_hw_transfer_start) and reads it back both in mt_hw_spi_complete() (logs actual arm-to-complete latency, warns above 5ms) and in the "previous transfer still in flight" guard itself (logs how long the stale transfer has already been outstanding at the moment a new arm is refused). Diagnostic-only, not part of the transfer-state contract -- for isolating the real duration/cause of the DMA-arm-failure stalls seen at every clock rate tested (10MHz/4MHz/500kHz). Co-Authored-By: Claude Sonnet 5 --- .../spi-mt-transport/spi_transport_hw_linux.c | 20 ++++++++++++++++++- .../spi-mt-transport/spi_transport_hw_linux.h | 10 ++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index a18ce42ed7e46..830bfe4d13bf3 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -54,6 +54,16 @@ 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; + /* MT-159369 bring-up instrumentation -- see armedAt's struct comment. */ + s64 armToCompleteUs = ktime_us_delta(ktime_get(), ctx->armedAt); + + if (armToCompleteUs > 5000) + dev_warn(&ctx->spi->dev, + "[MT-159369] slow transfer: arm-to-complete took %lldus (status=%d)\n", + armToCompleteUs, ctx->msg.status); + else + dev_dbg(&ctx->spi->dev, "[MT-159369] transfer complete: arm-to-complete %lldus\n", + armToCompleteUs); /* pOnTransferComplete() must run before transferComplete is signaled: * mt_hw_abort() waits on this same completion, and it runs on the @@ -92,8 +102,14 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p * this is itself a bug elsewhere; refuse rather than corrupt state. */ if (!completion_done(&ctx->transferComplete)) { + /* MT-159369 bring-up instrumentation: how long has the stale + * transfer already been outstanding at the moment this new + * arm is refused? See armedAt's struct comment. */ + s64 outstandingUs = ktime_us_delta(ktime_get(), ctx->armedAt); + dev_err(&ctx->spi->dev, - "pTransferStart() called with a previous transfer still in flight -- refusing to reinitialize shared msg/xfer state\n"); + "pTransferStart() called with a previous transfer still in flight (outstanding %lldus) -- refusing to reinitialize shared msg/xfer state\n", + outstandingUs); return eSpiTransportErrorHardwareFailure; } reinit_completion(&ctx->transferComplete); @@ -107,6 +123,8 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p ctx->msg.complete = mt_hw_spi_complete; ctx->msg.context = ctx; + ctx->armedAt = ktime_get(); /* MT-159369 bring-up instrumentation */ + ret = spi_async(ctx->spi, &ctx->msg); if (ret) { dev_dbg(&ctx->spi->dev, "spi_async failed: %d\n", ret); diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h index e346b1dcba0b1..b2fbabb0c5f7e 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "spi_transport/spi_transport_hw.h" @@ -47,6 +48,15 @@ struct mt_transport_hw_ctx { struct spi_transfer xfer; struct completion transferComplete; + /* MT-159369 bring-up instrumentation: ktime_get() at the instant + * spi_async() is successfully submitted in mt_hw_transfer_start(), + * read back in mt_hw_spi_complete() to log actual arm-to-complete + * latency, and in mt_hw_transfer_start()'s own "previous transfer + * still in flight" guard to log how long the stale transfer has + * already been outstanding at the moment a new arm is refused. + * Diagnostic-only -- not part of the transfer-state contract. */ + ktime_t armedAt; + /* 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(). From 5aca8c20d635bebce9ce373e9670ff0d68f17427 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:08:20 -0600 Subject: [PATCH 28/35] MT-159369: log calling thread on the still-in-flight guard trip current->comm directly identifies which context (tick kthread vs NRDY IRQ thread) is attempting the premature re-arm -- the 3.4-3.6ms consistent "outstanding" duration at trip time (much sooner than the 5-12ms actual completion times measured by the prior instrumentation) means something is retrying well before the previous transfer could possibly have finished, and this pins down which caller. Co-Authored-By: Claude Sonnet 5 --- drivers/spi/spi-mt-transport/spi_transport_hw_linux.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 830bfe4d13bf3..3d864b2197fe1 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "spi_transport_hw_linux.h" @@ -108,8 +110,8 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p s64 outstandingUs = ktime_us_delta(ktime_get(), ctx->armedAt); dev_err(&ctx->spi->dev, - "pTransferStart() called with a previous transfer still in flight (outstanding %lldus) -- refusing to reinitialize shared msg/xfer state\n", - outstandingUs); + "pTransferStart() called with a previous transfer still in flight (outstanding %lldus, caller=%s in_irq=%d in_softirq=%d) -- refusing to reinitialize shared msg/xfer state\n", + outstandingUs, current->comm, (int)in_irq(), (int)in_softirq()); return eSpiTransportErrorHardwareFailure; } reinit_completion(&ctx->transferComplete); From 944a152b96a4fa4c5b28293460dd89e2c111bc96 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:11:21 -0600 Subject: [PATCH 29/35] MT-159369: dump raw RX buffer on the first few header/payload CRC failures Calls the portable core's own spiTransportFrameHeaderCrcOk()/ spiTransportFramePayloadCrcOk() directly against ctx->xfer.rx_buf right after a completed transfer, before pOnTransferComplete() hands the buffer to the core -- same raw content the core's own check will see. Capped to the first 8 occurrences via an atomic counter to avoid flooding dmesg the way the per-event counters already do. Purpose: inspect the actual corruption pattern (scattered bit-flips vs. a consistent byte-shift vs. something else systematic) directly instead of just counting failures. Co-Authored-By: Claude Sonnet 5 --- .../spi-mt-transport/spi_transport_hw_linux.c | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 3d864b2197fe1..4366a82483751 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -10,8 +10,16 @@ #include #include #include +#include #include "spi_transport_hw_linux.h" +#include "spi_transport/spi_transport_frame.h" + +/* MT-159369 bring-up instrumentation: cap the raw-buffer hex dump below to + * the first few CRC failures -- enough to inspect the actual corruption + * pattern by eye without flooding dmesg the way the per-event "link event: + * ... CRC error" warnings already do. */ +static atomic_t gCrcDumpRemaining = ATOMIC_INIT(8); /* * Bound for mt_hw_abort()'s wait on an in-flight transfer's completion. @@ -67,6 +75,23 @@ static void mt_hw_spi_complete(void *context) dev_dbg(&ctx->spi->dev, "[MT-159369] transfer complete: arm-to-complete %lldus\n", armToCompleteUs); + /* MT-159369 bring-up instrumentation: dump the actual raw bytes the + * first few times a completed transfer fails header/payload CRC, so + * the real corruption pattern (single scattered bit-flips vs. a + * consistent byte-shift/offset vs. something else systematic) can be + * inspected directly instead of just counted. Checked here, before + * pOnTransferComplete() below hands the buffer to the core -- this is + * the same raw content the core's own CRC check will see. */ + if ((length > 0) && !spiTransportFrameHeaderCrcOk(ctx->xfer.rx_buf)) { + if (atomic_dec_if_positive(&gCrcDumpRemaining) >= 0) + print_hex_dump(KERN_ERR, "[MT-159369] hdrCrc-fail rx: ", DUMP_PREFIX_OFFSET, + 16, 1, ctx->xfer.rx_buf, length, false); + } else if ((length > 0) && !spiTransportFramePayloadCrcOk(ctx->xfer.rx_buf)) { + if (atomic_dec_if_positive(&gCrcDumpRemaining) >= 0) + print_hex_dump(KERN_ERR, "[MT-159369] payCrc-fail rx: ", DUMP_PREFIX_OFFSET, + 16, 1, ctx->xfer.rx_buf, length, false); + } + /* 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 From 91668e6c13a037161cdc0561516cedc66e9e9ee3 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:47:05 -0600 Subject: [PATCH 30/35] MT-159369: poison RX buffer with sentinel before every arm to test the stale-reuse theory Fills ctx->xfer.rx_buf with 0x37 (a byte value the protocol never legitimately sends) immediately before every spi_async() submission. rx_buf is a single, fixed buffer reused across every transfer -- if a completed transfer's CRC-failure dump still shows 0x37 anywhere, that proves DMA never actually wrote those bytes (a short/partial transfer), directly confirming or refuting the stale-buffer-reuse hypothesis from the previous raw-dump capture. Co-Authored-By: Claude Sonnet 5 --- .../spi/spi-mt-transport/spi_transport_hw_linux.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 4366a82483751..dddeeb71321da 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -141,6 +141,21 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p } reinit_completion(&ctx->transferComplete); + /* MT-159369 bring-up instrumentation: poison the RX buffer with a + * sentinel pattern before every arm, distinct from any real frame + * byte value the protocol would ever legitimately send (0xA5/0x5A + * magic, mostly-zero payloads, small CRC/seq values). rx_buf is a + * single, fixed buffer reused across every transfer (see the struct + * comment on msg/xfer) -- if a completed ("successful") transfer's + * dump still shows this sentinel anywhere, that byte was never + * actually written by DMA, proving a short/partial transfer rather + * than a fully-fresh 128 bytes. Deliberately poisoning the buffer + * the core is about to hand to hardware, not just reading stale + * content after the fact -- rules out "it was already zero from a + * previous frame" as an alternate explanation for an all-zero + * payload region. */ + memset(pRx, 0x37, length); + spi_message_init(&ctx->msg); memset(&ctx->xfer, 0, sizeof(ctx->xfer)); ctx->xfer.tx_buf = pTx; From 5902526c5299520a0c8fe7c7e0fe8709767bc627 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:16:38 -0600 Subject: [PATCH 31/35] MT-159369: add DMA residue verification after RX/TX completion Neither wait_for_completion above proves the DMA engine actually drained transfer->len bytes -- only that each channel's callback fired. Checking dmaengine_tx_status()'s residue directly tests whether short/partial DMA completions explain the CRC-corruption investigation's symptom (frames with plausible-looking header/payload content but a wrong header or payload CRC, isolated to the tail of their respective checksummed regions). --- drivers/spi/spi-imx.c | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-imx.c b/drivers/spi/spi-imx.c index 9087d09b7d9c9..fd99c4feea564 100644 --- a/drivers/spi/spi-imx.c +++ b/drivers/spi/spi-imx.c @@ -1487,6 +1487,15 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, struct scatterlist *last_sg = sg_last(rx->sgl, rx->nents); unsigned int bytes_per_word, i; int ret; + /* MT-159369 bring-up instrumentation: neither wait_for_completion below + * verifies the DMA engine actually drained every byte it was asked to + * move -- completion only means the channel's callback fired, not that + * dma_rx_completion corresponds to a full-residue transfer. Capture + * cookies here so residue can be checked against actual transfer->len + * once both completions fire, to test whether short/partial RX DMA + * completions are the source of the CRC-corruption investigation's + * "correct wire data, wrong bytes in memory" symptom. */ + dma_cookie_t rx_cookie, tx_cookie; if ((is_imx51_ecspi(spi_imx) || is_imx53_ecspi(spi_imx)) && transfer->len > MX53_MAX_TRANSFER_BYTES && spi_imx->target_mode) { @@ -1532,7 +1541,7 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, desc_rx->callback = spi_imx_dma_rx_callback; desc_rx->callback_param = (void *)spi_imx; - dmaengine_submit(desc_rx); + rx_cookie = dmaengine_submit(desc_rx); /* MT-159369 bring-up instrumentation */ reinit_completion(&spi_imx->dma_rx_completion); dma_async_issue_pending(controller->dma_rx); @@ -1547,7 +1556,7 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, desc_tx->callback = spi_imx_dma_tx_callback; desc_tx->callback_param = (void *)spi_imx; - dmaengine_submit(desc_tx); + tx_cookie = dmaengine_submit(desc_tx); /* MT-159369 bring-up instrumentation */ reinit_completion(&spi_imx->dma_tx_completion); dma_async_issue_pending(controller->dma_tx); @@ -1574,6 +1583,33 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, dmaengine_terminate_all(controller->dma_rx); return -ETIMEDOUT; } + + /* MT-159369 bring-up instrumentation: both completions above + * only prove each channel's callback fired -- neither proves + * the engine actually drained transfer->len bytes. Check + * residue directly; a nonzero value here would mean the RX/TX + * buffer genuinely has fewer real bytes than the rest of this + * driver assumes, direct evidence for (or against) the + * CRC-corruption investigation's short-DMA-completion theory. */ + { + struct dma_tx_state rx_state, tx_state; + enum dma_status rx_dma_status, tx_dma_status; + + rx_dma_status = dmaengine_tx_status(controller->dma_rx, rx_cookie, + &rx_state); + tx_dma_status = dmaengine_tx_status(controller->dma_tx, tx_cookie, + &tx_state); + + if (rx_state.residue != 0 || tx_state.residue != 0) + dev_err(spi_imx->dev, + "[MT-159369] DMA residue nonzero after completion: rx_status=%d rx_residue=%u tx_status=%d tx_residue=%u len=%u\n", + rx_dma_status, rx_state.residue, + tx_dma_status, tx_state.residue, transfer->len); + else + dev_dbg(spi_imx->dev, + "[MT-159369] DMA residue clean: len=%u\n", + transfer->len); + } } else { spi_imx->target_aborted = false; From 317dbb791a3b15c2823b4d2f37900f9e694ac5a8 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:30:33 -0600 Subject: [PATCH 32/35] MT-159369: hex-dump first few TX/RX frames for cross-side comparison Matches the STM32 side's commit a4a66d11 -- unconditional raw dumps (not CRC-failure-gated) of the first 5 TX arms and first 5 RX completions, for direct byte-for-byte comparison against the same early exchange's fbs TX/RX dumps on that side. --- .../spi-mt-transport/spi_transport_hw_linux.c | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index dddeeb71321da..6fc16f2b7ed17 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -21,6 +21,20 @@ * ... CRC error" warnings already do. */ static atomic_t gCrcDumpRemaining = ATOMIC_INIT(8); +/* MT-159369 bring-up diagnostic: unconditional (not CRC-failure-gated, see + * gCrcDumpRemaining above) raw dump of the first few TX/RX frames, for + * direct cross-reference against the STM32 side's matching "fbs TX"/ + * "fbs RX" hex dumps (plat_fbs.cpp, commit a4a66d11) of the same early + * exchange -- goal is localizing the dual-headerCrc non-determinism + * finding to a specific hop. Two independent counters rather than a + * paired per-transfer flag: unlike the STM32 side (which boots long + * before Linux is up and has to gate on "first real armed transfer"), + * this Host-side driver only starts arming once the tick thread is + * already running against a live link, so the Nth TX dump and Nth RX + * dump line up by simple chronological order in dmesg. */ +static atomic_t gTxDumpRemaining = ATOMIC_INIT(5); +static atomic_t gRxDumpRemaining = ATOMIC_INIT(5); + /* * 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() @@ -92,6 +106,16 @@ static void mt_hw_spi_complete(void *context) 16, 1, ctx->xfer.rx_buf, length, false); } + /* MT-159369 bring-up diagnostic: unconditional raw RX dump for the + * first few completions -- see gRxDumpRemaining's doc comment. Not + * gated on CRC pass/fail (unlike the dumps just above), so this + * covers whatever the first few real exchanges actually look like, + * cross-referenceable against the STM32 side's "fbs RX" dump for the + * same window. */ + if ((length > 0) && (atomic_dec_if_positive(&gRxDumpRemaining) >= 0)) + print_hex_dump(KERN_ERR, "[MT-159369] RX raw: ", DUMP_PREFIX_OFFSET, 16, 1, + ctx->xfer.rx_buf, length, false); + /* 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 @@ -141,6 +165,16 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p } reinit_completion(&ctx->transferComplete); + /* MT-159369 bring-up diagnostic: unconditional raw TX dump for the + * first few arms -- see gTxDumpRemaining's doc comment above. This is + * "what we're about to hand to spi_async()", captured before anything + * else touches pTx/pRx this call, for direct cross-reference against + * the STM32 side's "fbs TX" dump (what it believes it armed) for the + * same early exchange. */ + if (atomic_dec_if_positive(&gTxDumpRemaining) >= 0) + print_hex_dump(KERN_ERR, "[MT-159369] TX raw: ", DUMP_PREFIX_OFFSET, 16, 1, pTx, + length, false); + /* MT-159369 bring-up instrumentation: poison the RX buffer with a * sentinel pattern before every arm, distinct from any real frame * byte value the protocol would ever legitimately send (0xA5/0x5A From d5d1f5736d2620019fb7267f76a8e9430402f223 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:30:02 -0600 Subject: [PATCH 33/35] MT-159369: ratelimit per-event/per-frame logging -- unthrottled dev_warn/dev_err was saturating the console and causing the boot-time hang mt_transport_event_callback() logged every single header/payload CRC error, sequence gap, and DMA failure/timeout via plain dev_warn()/ dev_info() (changed from dev_dbg() during MT-158113 bring-up for visibility during ~50 manually-supervised test cycles). Same for mt_hw_spi_complete()'s slow-transfer warning and mt_hw_transfer_start()'s still-in-flight rejection, both unconditional per-frame/per-request. Under a real link-quality storm (hundreds of events/sec instead of a few dozen over a supervised session) this saturates imx_uart_console_write(), which holds port.lock with local IRQs (and its own RX-ready IRQ) disabled for the full synchronous, poll-driven duration of each line at the console baud rate. Confirmed this is why serial BREAK + magic sysrq got zero response during the hang -- not a genuine deadlock, the RX path needed for that response was disabled by this same code, repeatedly, with no gap. Also explains why softlockup/ hung-task detection produced nothing even after being enabled: their own timer tick needs the same IRQs this was starving. Switched all of these to their _ratelimited variants, matching the precedent already set by this driver's own NRDY-read-failure logging (dev_err_ratelimited in this same file). Counters (atomic_inc) stay unthrottled -- only the console output is gated. --- .../spi-mt-transport/spi_mt_transport_drv.c | 32 ++++++++++++++----- .../spi-mt-transport/spi_transport_hw_linux.c | 16 ++++++++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index a58dcc7efd485..f309de99a1a14 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -303,37 +303,53 @@ static void mt_transport_event_callback(void *pContext, teSpiTransportEvent eEve struct mt_transport_channel *chan = pContext; struct mt_transport_priv *priv = chan->priv; + /* MT-159369: these were plain dev_info()/dev_warn() -- unlike + * spi_transport_hw_linux.c's dev_err_ratelimited() for the analogous + * NRDY-read-failure case, nothing here throttled the print rate. + * Fine for the bounded, human-supervised bring-up testing this was + * written for (MT-158113: ~50 connect/disconnect cycles, a few dozen + * injected DMA failures), but a real link-quality storm can raise + * these into the hundreds/sec. imx_uart_console_write() holds + * port.lock with local IRQs (and RRDYEN, its own RX-ready IRQ) + * disabled for the full synchronous, poll-driven duration of each + * line at the console baud rate -- confirmed live: this is why + * serial BREAK + magic sysrq got no response during the hang, not a + * genuine deadlock. Back-to-back events leave no gap for anything + * else -- including the RX path a BREAK needs, and the timer tick + * the softlockup/hung-task watchdogs themselves need -- to run. + * _ratelimited keeps these visible without the storm. + */ switch (eEvent) { case eSpiTransportEventConnected: atomic_inc(&priv->evt_connected); - dev_info(priv->dev, "link event: connected\n"); + dev_info_ratelimited(priv->dev, "link event: connected\n"); break; case eSpiTransportEventDisconnected: atomic_inc(&priv->evt_disconnected); - dev_info(priv->dev, "link event: disconnected\n"); + dev_info_ratelimited(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"); + dev_warn_ratelimited(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"); + dev_warn_ratelimited(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"); + dev_warn_ratelimited(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"); + dev_warn_ratelimited(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"); + dev_warn_ratelimited(priv->dev, "link event: DMA timeout\n"); break; default: - dev_warn(priv->dev, "link event: unknown (%d)\n", (int)eEvent); + dev_warn_ratelimited(priv->dev, "link event: unknown (%d)\n", (int)eEvent); break; } } diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 6fc16f2b7ed17..7da6f48f8ce7e 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -81,8 +81,15 @@ static void mt_hw_spi_complete(void *context) /* MT-159369 bring-up instrumentation -- see armedAt's struct comment. */ s64 armToCompleteUs = ktime_us_delta(ktime_get(), ctx->armedAt); + /* MT-159369: ratelimited -- during a real link-quality storm this + * can fire on nearly every frame. Unthrottled, it joins + * mt_transport_event_callback()'s per-event logging (see that + * function's comment) in saturating imx_uart_console_write()'s + * IRQ-disabled, poll-driven console path -- confirmed live as the + * actual mechanism behind the boot-time hang, not a genuine + * deadlock. */ if (armToCompleteUs > 5000) - dev_warn(&ctx->spi->dev, + dev_warn_ratelimited(&ctx->spi->dev, "[MT-159369] slow transfer: arm-to-complete took %lldus (status=%d)\n", armToCompleteUs, ctx->msg.status); else @@ -158,7 +165,12 @@ static teSpiTransportError mt_hw_transfer_start(void *pContext, const uint8_t *p * arm is refused? See armedAt's struct comment. */ s64 outstandingUs = ktime_us_delta(ktime_get(), ctx->armedAt); - dev_err(&ctx->spi->dev, + /* MT-159369: ratelimited for the same reason as + * mt_hw_spi_complete()'s slow-transfer warning above -- this + * guard can trip on nearly every request during a real + * retry storm, and unthrottled it was part of what saturated + * the console path and produced the boot-time hang. */ + dev_err_ratelimited(&ctx->spi->dev, "pTransferStart() called with a previous transfer still in flight (outstanding %lldus, caller=%s in_irq=%d in_softirq=%d) -- refusing to reinitialize shared msg/xfer state\n", outstandingUs, current->comm, (int)in_irq(), (int)in_softirq()); return eSpiTransportErrorHardwareFailure; From 59f578915652f31461900e9329add7f00dc96367 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:54:36 -0600 Subject: [PATCH 34/35] MT-159369: NRDY IRQ handler should only wake the tick thread, not act on the edge directly mt_transport_hw_linux_nrdy_irq() read the GPIO level and called pOnReadyEvent() synchronously on every edge -- real per-transfer work (hostArmTransferIfAcked()/hostIssueRequestIfReady(), frame build + pTransferStart()/spi_async()) documented in the core as "a pure latency optimization" over the tick-driven poll path, which is fully correct and sufficient on its own. Every threaded IRQ handler runs as SCHED_FIFO (kernel/irq/manage.c, irq_thread() -> sched_set_fifo()), and this board's kernel disables the normal RT-throttling cap (disable-rt-throttling.cfg) for real-time audio. Doing real work here means a rapidly-toggling NRDY line during a genuine link-quality storm turns into unbounded SCHED_FIFO CPU consumption with nothing capping it -- the mechanism behind a boot-time hang where even the softlockup/hung-task watchdogs (plain SCHED_OTHER) never got to run, confirmed live. NRDY is an edge-triggered GPIO used only to wake a consumer; its real state lives in the level, not the edge. This handler now only wakes the tick thread -- it neither reads the GPIO nor calls pOnReadyEvent() -- which reads the *current* level itself via the existing pReadyRead() poll path when it runs. No matter how fast NRDY bounces, real work stays capped at the tick thread's own SCHED_OTHER pace instead of scaling with edge frequency. --- .../spi-mt-transport/spi_transport_hw_linux.c | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index 7da6f48f8ce7e..f0ae0e8cb3f2e 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -351,25 +351,39 @@ void mt_transport_hw_linux_set_notify(struct mt_transport_hw_ctx *ctx, ctx->pNotifyCtx = pNotifyCtx; } +/* MT-159369: this used to read the GPIO level and call pOnReadyEvent() + * (onReadyEvent() in the core -- hostArmTransferIfAcked()/ + * hostIssueRequestIfReady(), real frame-building and pTransferStart()/ + * spi_async() work) synchronously, right here, on every edge -- a "pure + * latency optimization" per the core's own comment on onReadyEvent(), + * which also documents that the tick-driven poll path is fully correct + * and sufficient on its own without it. + * + * That optimization is exactly backwards for what NRDY actually is: an + * edge-triggered GPIO used only to wake a consumer, whose real state + * lives in the level, not the edge. genirq runs every threaded IRQ + * handler -- this one included -- as SCHED_FIFO (kernel/irq/manage.c, + * irq_thread() -> sched_set_fifo()), and this board's kernel disables + * the normal RT-throttling cap (disable-rt-throttling.cfg, deliberately, + * for real-time audio). Doing real per-transfer work here means a + * rapidly-toggling NRDY line (a real link-quality storm, not a bug in + * NRDY itself) turns into unbounded SCHED_FIFO CPU consumption with + * nothing capping it -- confirmed as the mechanism behind a boot-time + * hang where even the softlockup/hung-task watchdogs (plain SCHED_OTHER) + * never got to run. + * + * Fix: this handler is now purely a trigger, not an actor. It does not + * read the GPIO or call pOnReadyEvent() at all -- it only wakes the tick + * thread, which reads the *current* level via pReadyRead() when it + * actually runs (hostServiceTick()/hostIssueRequestIfReady(), already + * documented as correct and sufficient standalone). No matter how fast + * NRDY bounces, the amount of real work stays capped at the tick + * thread's own (SCHED_OTHER, non-starving) pace instead of scaling with + * edge frequency. + */ 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); From 1f7da38b054fe6f848664d5ac965889598c989b7 Mon Sep 17 00:00:00 2001 From: Michael Loh <150379488+MT-MichaelLoh@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:56:10 -0600 Subject: [PATCH 35/35] MT-159369: address 5 of 6 unresolved PR #46 review threads - spi_transport_hw_linux.c: mt_hw_spi_complete() now uses min(msg.actual_length, xfer.len) instead of the bare requested xfer.len, so a short-but-"successful" DMA completion no longer hands the core stale/unwritten tail bytes. - spi_mt_transport_drv.c: set SPI_NO_CS + spi_setup() in probe() (the DTS "spi-no-cs;" property proposed in review doesn't exist as a parsed binding in this kernel's SPI core -- verified against spi.c's of_spi_parse_dt() -- so this is the actual mechanism). - spi-imx.c: dev_err_ratelimited() for the DMA-residue mismatch log (same unthrottled-hot-path-logging class as the confirmed boot-time-hang fix in d5d1f5736d2); dma_submit_error() checks after both dmaengine_submit() calls, with cleanup matched to each call site's actual in-flight state (rx_cookie failure needs no channel cleanup; tx_cookie failure must terminate both channels since RX is already submitted+issued by then). Compile-verified via `bitbake -c compile_kernelmodules -f linux-imx` against the firmware repo's current core (see below) -- all 4 changed objects (spi-imx.o, plus the 3 spi-mt-transport objects) compiled with zero errors/warnings, spi-mt-transport.ko linked clean. Also calls a new spiTransportDeinit() from every post-Init probe failure path and from mt_transport_remove(), fixing the gInstances[] pool leak (thread #2). That function is added on the firmware repo's MT-159369 branch, still WIP/uncommitted there -- see PR comment. Thread #1 (spi_mt_transport_drv.c:52, "where are these?") is reply-only, no code change; answered directly on the review thread. Co-Authored-By: Claude Sonnet 5 --- drivers/spi/spi-imx.c | 32 ++++++++++- .../spi-mt-transport/spi_mt_transport_drv.c | 57 +++++++++++++++++++ .../spi-mt-transport/spi_transport_hw_linux.c | 8 ++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-imx.c b/drivers/spi/spi-imx.c index fd99c4feea564..dd9a1e2225ab0 100644 --- a/drivers/spi/spi-imx.c +++ b/drivers/spi/spi-imx.c @@ -1542,6 +1542,16 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, desc_rx->callback = spi_imx_dma_rx_callback; desc_rx->callback_param = (void *)spi_imx; rx_cookie = dmaengine_submit(desc_rx); /* MT-159369 bring-up instrumentation */ + /* Nothing has been issued yet at this point (dma_async_issue_pending() + * for either channel is still below) -- a failed submit here means + * neither channel is actually in flight, so the plain no-cleanup + * dma_failure_no_start path (same one desc_rx's own prep failure just + * above already uses) is sufficient. Found by Copilot's PR #46 review. + */ + if (dma_submit_error(rx_cookie)) { + ret = rx_cookie; + goto dma_failure_no_start; + } reinit_completion(&spi_imx->dma_rx_completion); dma_async_issue_pending(controller->dma_rx); @@ -1557,6 +1567,18 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, desc_tx->callback = spi_imx_dma_tx_callback; desc_tx->callback_param = (void *)spi_imx; tx_cookie = dmaengine_submit(desc_tx); /* MT-159369 bring-up instrumentation */ + /* Unlike the rx_cookie check above, RX is already submitted+issued by + * this point -- a failed TX submit here must terminate both channels, + * same as the desc_tx prep-failure case immediately above, not just + * bail via dma_failure_no_start (which performs no channel cleanup and + * would leak the already-in-flight RX DMA). Found by Copilot's PR #46 + * review. + */ + if (dma_submit_error(tx_cookie)) { + dmaengine_terminate_all(controller->dma_tx); + dmaengine_terminate_all(controller->dma_rx); + return tx_cookie; + } reinit_completion(&spi_imx->dma_tx_completion); dma_async_issue_pending(controller->dma_tx); @@ -1600,8 +1622,16 @@ static int spi_imx_dma_transfer(struct spi_imx_data *spi_imx, tx_dma_status = dmaengine_tx_status(controller->dma_tx, tx_cookie, &tx_state); + /* Ratelimited: this runs on every non-target-mode DMA + * transfer, so during a real link-quality storm it's the + * same unthrottled-hot-path-logging mechanism that + * commit d5d1f5736d2 already fixed for + * mt_transport_event_callback() (the confirmed cause of + * the MT-159369 boot-time hang) -- this dev_err() was + * missed by that pass. Found by Copilot's PR #46 review. + */ if (rx_state.residue != 0 || tx_state.residue != 0) - dev_err(spi_imx->dev, + dev_err_ratelimited(spi_imx->dev, "[MT-159369] DMA residue nonzero after completion: rx_status=%d rx_residue=%u tx_status=%d tx_residue=%u len=%u\n", rx_dma_status, rx_state.residue, tx_dma_status, tx_state.residue, transfer->len); diff --git a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c index f309de99a1a14..5a569fde6095c 100644 --- a/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c +++ b/drivers/spi/spi-mt-transport/spi_mt_transport_drv.c @@ -732,6 +732,25 @@ static int mt_transport_probe(struct spi_device *spi) int i; int started_misc = 0; + /* SPI_NO_CS, not a devicetree property: there is no generic + * "spi-no-cs"/"spi-no-cs;" binding parsed anywhere in this kernel's + * SPI core (checked drivers/spi/spi.c's of_spi_parse_dt() -- only + * spi-cpha/spi-cpol/spi-3wire/spi-lsb-first/spi-cs-high are wired to + * spi->mode there), so setting it from the DTS would silently do + * nothing. Since this driver already owns NSS as a private manually- + * driven GPIO instead of the standard "cs-gpios" (see this file's own + * multitracks,nss-gpios comment below and spi_transport_hw_linux.c's + * file comment), it must also tell the SPI core to leave its own + * chip-select alone entirely -- spi-imx.c's controller->mode_bits + * already advertises SPI_NO_CS support, so this is purely opting in. + * Must precede spi_setup() so the mode change actually takes effect. + * Found by Copilot's PR #46 review. + */ + spi->mode |= SPI_NO_CS; + ret = spi_setup(spi); + if (ret) + return dev_err_probe(dev, ret, "spi_setup(SPI_NO_CS) failed\n"); + /* 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 @@ -814,6 +833,13 @@ static int mt_transport_probe(struct spi_device *spi) mt_transport_rx_callback, mt_transport_event_callback, chan) != eSpiTransportErrorNone) { + /* Release the gInstances[] pool slot spiTransportInit() + * claimed above -- every post-Init probe failure path + * must do this or repeated failed probes permanently + * exhaust SPI_TRANSPORT_INSTANCES_MAX. Found by + * Copilot's PR #46 review. + */ + spiTransportDeinit(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EINVAL, "spiTransportRegisterChannel(%u) failed\n", @@ -836,6 +862,12 @@ static int mt_transport_probe(struct spi_device *spi) * instead of the actual IRQ becoming available a bit later. */ if (priv->nrdy_irq == -EPROBE_DEFER) { + /* Release the gInstances[] pool slot spiTransportInit() + * claimed above -- see the RegisterChannel-loop failure path + * above for the full reasoning. Found by Copilot's PR #46 + * review. + */ + spiTransportDeinit(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EPROBE_DEFER, "NRDY IRQ not ready yet\n"); } @@ -874,6 +906,12 @@ static int mt_transport_probe(struct spi_device *spi) */ if (priv->nrdy_irq_requested) devm_free_irq(dev, priv->nrdy_irq, &priv->hw_ctx); + /* Release the gInstances[] pool slot spiTransportInit() + * claimed above -- see the RegisterChannel-loop failure path + * above for the full reasoning. Found by Copilot's PR #46 + * review. + */ + spiTransportDeinit(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, ret, "failed to start tick thread\n"); } @@ -889,6 +927,12 @@ static int mt_transport_probe(struct spi_device *spi) if (priv->nrdy_irq_requested) devm_free_irq(dev, priv->nrdy_irq, &priv->hw_ctx); kthread_stop(priv->tick_thread); + /* Release the gInstances[] pool slot spiTransportInit() + * claimed above -- see the RegisterChannel-loop failure path + * above for the full reasoning. Found by Copilot's PR #46 + * review. + */ + spiTransportDeinit(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, -EINVAL, "spiTransportStart failed\n"); } @@ -935,6 +979,12 @@ static int mt_transport_probe(struct spi_device *spi) misc_deregister(&priv->channels[j].misc); kthread_stop(priv->tick_thread); spiTransportStop(priv->htransport); + /* Release the gInstances[] pool slot spiTransportInit() + * claimed above -- see the RegisterChannel-loop failure path + * earlier in this function for the full reasoning. Found by + * Copilot's PR #46 review. + */ + spiTransportDeinit(priv->htransport); kref_put(&priv->refcount, mt_transport_priv_release); return dev_err_probe(dev, ret, "misc_register(%s) failed\n", chan->misc_name); @@ -1001,6 +1051,13 @@ static void mt_transport_remove(struct spi_device *spi) */ kthread_stop(priv->tick_thread); spiTransportStop(priv->htransport); + /* Balances spiTransportInit() in probe() -- without this, a + * sysfs-triggered unbind/rebind cycle (not just a failed probe + * retry) also permanently leaks a gInstances[] slot, since + * spiTransportStop() itself never clears inUse. Found by + * Copilot's PR #46 review. + */ + spiTransportDeinit(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 diff --git a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c index f0ae0e8cb3f2e..456d9541b4702 100644 --- a/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c +++ b/drivers/spi/spi-mt-transport/spi_transport_hw_linux.c @@ -77,7 +77,13 @@ static atomic_t gRxDumpRemaining = ATOMIC_INIT(5); 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; + /* actual_length, not the bare requested xfer.len -- a short-but- + * "successful" (status==0) DMA completion would otherwise hand the + * core stale/unwritten tail bytes as if they were real data. Found by + * Copilot's PR #46 review. */ + uint16_t length = ctx->msg.status == 0 + ? min_t(unsigned, ctx->msg.actual_length, ctx->xfer.len) + : 0; /* MT-159369 bring-up instrumentation -- see armedAt's struct comment. */ s64 armToCompleteUs = ktime_us_delta(ktime_get(), ctx->armedAt);