Skip to content

File ShiftBus.h

File List > Firmware > Libraries > PanelGroup > Helpers > ShiftBus > ShiftBus.h

Go to the documentation of this file


#pragma once
#ifdef ARDUINO_ARCH_STM32

#include <Arduino.h>
#include <SPI.h>

namespace OpenSkyhawk {

class ShiftBus {
public:
    static constexpr uint8_t MAX_CHAIN         = 8;   
    static constexpr uint8_t MAX_ISR_CONSUMERS = 16;  

    ShiftBus(SPIClass& spi, uint8_t sckPin, uint8_t misoPin, uint8_t mosiPin,
             uint8_t loadPin, uint8_t latchPin);

    // ── PinRef backend — package-internal, mirrors the MCP cache bridge ─────────

    void noteInput(uint8_t chip);

    void noteOutput(uint8_t chip);

    bool readBit(uint8_t chip, uint8_t bit) const;

    bool readOutBit(uint8_t chip, uint8_t bit) const;

    void writeBit(uint8_t chip, uint8_t bit, bool v);

    bool readLiveBit(uint8_t chip, uint8_t bit);

    bool dirty() const { return _dirty; }

    bool active() const { return _active; }

    // ── Lifecycle — called by PanelGroup ─────────────────────────────────────────

    void begin();

    void transfer();

    void flushNow() { transfer(); }

    // ── Timer-ISR sampling (encoder-feel mode, -DSHIFTBUS_ISR_HZ) ────────────────

    void beginIsrSampling(TIM_TypeDef* tim, uint16_t sampleHz);

    void addIsrConsumer(void (*hook)(void* ctx), void* ctx);

    bool isrActive() const { return _isrActive; }

#ifdef SHIFTBUS_TEST
    // ── Test seams — chip-less logic tests (no SPI, no strobe pins) ──────────────
    void testSetBypass(bool on) { _testBypass = on; }
    void testInjectIn(uint8_t chip, uint8_t value) { if (chip < MAX_CHAIN) _testIn[chip] = value; }
    uint8_t testOutFrame(uint8_t chip) const { return (chip < MAX_CHAIN) ? _outFrame[chip] : 0; }
    uint8_t testWire(uint8_t i) const { return (i < MAX_CHAIN) ? _wireTx[i] : 0; }
    uint16_t testTransferCount() const { return _transferCount; }
    uint8_t testChainIn()  const { return _nIn; }
    uint8_t testChainOut() const { return _nOut; }
#endif

private:
    // Unguarded body; callers handle the ISR critical section. commitStage: loop-context
    // paths publish pending writeBit() stage changes; the ISR passes false so it never
    // ships a half-written multi-pin group.
    void doTransfer(bool commitStage);

    SPIClass* _spi;
    uint8_t   _sckPin;
    uint8_t   _misoPin;
    uint8_t   _mosiPin;
    uint8_t   _loadPin;
    uint8_t   _latchPin;

    uint8_t   _nIn  = 0;        // '165 chips in use (auto-sized at configure)
    uint8_t   _nOut = 0;        // '595 chips in use
    bool      _active = false;
    bool      _begun  = false;
    volatile bool _dirty = false;   // stage differs from committed frame

    // Concurrency model (no-LTO assumption): the frames are plain bytes shared between the
    // sampling ISR and loop context. Safe because all cross-context access goes through
    // out-of-line methods in this TU (the compiler cannot cache them across calls) and
    // byte load/store is atomic on Cortex-M3. Revisit if LTO is ever enabled.
    uint8_t   _inFrame[MAX_CHAIN]  = {0};  // '165 cache: [chip] = D7..D0
    uint8_t   _stage[MAX_CHAIN]    = {0};  // '595 staging: loop-owned, writeBit() target
    uint8_t   _outFrame[MAX_CHAIN] = {0};  // '595 committed frame: what transfers ship
    uint8_t   _wire[MAX_CHAIN]     = {0};  // scratch for the in-place full-duplex transfer

    // ISR sampling
    volatile bool _isrActive = false;
    struct IsrConsumer { void (*hook)(void*); void* ctx; };
    IsrConsumer _consumers[MAX_ISR_CONSUMERS];
    uint8_t     _consumerCount = 0;
    static void isrTrampoline();   // static → member via _isrInstance (one ISR bus supported)
    static ShiftBus* _isrInstance;

#ifdef SHIFTBUS_TEST
    bool     _testBypass = false;
    uint8_t  _testIn[MAX_CHAIN]  = {0};
    uint8_t  _wireTx[MAX_CHAIN]  = {0};   // TX wire snapshot (reversal/pad coverage)
    uint16_t _transferCount = 0;
#endif
};

}  // namespace OpenSkyhawk

extern OpenSkyhawk::ShiftBus ShiftBus1;

#endif  // ARDUINO_ARCH_STM32