Skip to content

File ShiftBus.cpp

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

Go to the documentation of this file


#ifdef ARDUINO_ARCH_STM32

#include "ShiftBus.h"
#include <STM32Board.h>

// ── Standard pin assignment for the pre-defined ShiftBus1 (override via build flags) ──

#ifndef SHIFTBUS_SCK
#define SHIFTBUS_SCK   PB3   // SPI1 remap (JTDO — JTAG released in begin(), SWD unaffected)
#endif
#ifndef SHIFTBUS_MISO
#define SHIFTBUS_MISO  PB4   // SPI1 remap (NJTRST)
#endif
#ifndef SHIFTBUS_MOSI
#define SHIFTBUS_MOSI  PB5   // SPI1 remap
#endif
#ifndef SHIFTBUS_LOAD
#define SHIFTBUS_LOAD  PB8   // '165 SH/LD̄
#endif
#ifndef SHIFTBUS_LATCH
#define SHIFTBUS_LATCH PB9   // '595 STCP
#endif

// ~1 MHz: comfortable for 74HC at 3.3 V and for a ~12" remote harness leg (33 Ω series R).
static const SPISettings kShiftBusSettings(1000000, MSBFIRST, SPI_MODE0);

namespace OpenSkyhawk {

ShiftBus* ShiftBus::_isrInstance = nullptr;

ShiftBus::ShiftBus(SPIClass& spi, uint8_t sckPin, uint8_t misoPin, uint8_t mosiPin,
                   uint8_t loadPin, uint8_t latchPin)
    : _spi(&spi), _sckPin(sckPin), _misoPin(misoPin), _mosiPin(mosiPin),
      _loadPin(loadPin), _latchPin(latchPin) {}
    // Nothing else: constructors of global buses run during static init, before HAL init.

// ── configure-time notification (PinRef::configureAsInput/Output) ──────────────

void ShiftBus::noteInput(uint8_t chip) {
    if (chip >= MAX_CHAIN) return;
    if (chip + 1 > _nIn) _nIn = chip + 1;
    _active = true;
}

void ShiftBus::noteOutput(uint8_t chip) {
    if (chip >= MAX_CHAIN) return;
    if (chip + 1 > _nOut) _nOut = chip + 1;
    _active = true;
}

// ── Frame access ────────────────────────────────────────────────────────────────

bool ShiftBus::readBit(uint8_t chip, uint8_t bit) const {
    if (chip >= MAX_CHAIN || bit > 7) return false;
    return (_inFrame[chip] >> bit) & 1u;
}

bool ShiftBus::readOutBit(uint8_t chip, uint8_t bit) const {
    if (chip >= MAX_CHAIN || bit > 7) return false;
    return (_stage[chip] >> bit) & 1u;   // last written (staged) value
}

void ShiftBus::writeBit(uint8_t chip, uint8_t bit, bool v) {
    if (chip >= MAX_CHAIN || bit > 7) return;
    // Stage is loop-owned — the ISR only ships the committed _outFrame, so a multi-pin
    // group (four stepper coils) staged across several calls can never reach the outputs
    // half-written. No masking needed here; the commit in doTransfer() is the sync point.
    const uint8_t mask = (uint8_t)(1u << bit);
    if (v) _stage[chip] |=  mask;
    else   _stage[chip] &= ~mask;
    _dirty = true;
}

bool ShiftBus::readLiveBit(uint8_t chip, uint8_t bit) {
    transfer();
    return readBit(chip, bit);
}

// ── Lifecycle ───────────────────────────────────────────────────────────────────

void ShiftBus::begin() {
    if (_begun || !_active) return;
    _begun = true;

#if defined(STM32F1xx)
    // Release JTAG so PB3 (JTDO) / PB4 (NJTRST) become GPIO/SPI. SWD (PA13/PA14) retained.
    // (The core's pin_DisconnectDebug does this too when the pins map; explicit is cheap
    // insurance and self-documenting.)
    __HAL_RCC_AFIO_CLK_ENABLE();
    __HAL_AFIO_REMAP_SWJ_NOJTAG();
#endif

    // Strobes idle: LOAD high ('165 shift mode), LATCH low ('595 waits for a rising edge).
    pinMode(_loadPin, OUTPUT);
    digitalWrite(_loadPin, HIGH);
    pinMode(_latchPin, OUTPUT);
    digitalWrite(_latchPin, LOW);

#ifdef SHIFTBUS_TEST
    if (_testBypass) { doTransfer(true); return; }  // no SPI in chip-less logic tests
#endif

    _spi->setSCLK(_sckPin);
    _spi->setMISO(_misoPin);
    _spi->setMOSI(_mosiPin);
    _spi->begin();

    // Defined '595 state as early as possible: the power-on shift/storage registers are
    // undefined (OĒ is tied to GND — stance (a), boot twitch is cosmetic). The stage is
    // all zeros here, so the first transfer publishes an all-off frame, then primes the
    // '165 cache for the forceReport() boot burst.
    doTransfer(true);

    if (STM32Board::isDebug()) {
        auto& d = STM32Board::diagSerial();
        d.print(F("[ShiftBus] begin: in="));  d.print(_nIn);
        d.print(F(" out="));                  d.print(_nOut);
        d.print(F(" frame="));                d.print((int)max(_nIn, _nOut));
        d.println(F("B"));
    }
}

void ShiftBus::transfer() {
    if (!_begun) return;
    // Loop-context call while the sampling ISR owns the cadence: mask interrupts for the
    // µs-scale transaction so ISR and loop never interleave on the frame buffers. The
    // window is bounded (≤ MAX_CHAIN bytes at 1 MHz ≈ 64 µs + strobes); no CAN/I2C inside.
    if (_isrActive) {
        noInterrupts();
        doTransfer(true);
        interrupts();
    } else {
        doTransfer(true);
    }
}

void ShiftBus::doTransfer(bool commitStage) {
    // Commit: publish staged writeBit() changes into the frame the wire ships. Only
    // loop-context paths commit (transfer/flushNow — masked when the ISR runs); the ISR
    // never commits, so it can never latch a half-staged multi-pin group.
    if (commitStage && _dirty) {
        for (uint8_t i = 0; i < MAX_CHAIN; i++) _outFrame[i] = _stage[i];
        _dirty = false;
    }

    const uint8_t n = max(_nIn, _nOut);
    if (n == 0) return;

#ifdef SHIFTBUS_TEST
    _transferCount++;
#endif

    // Build the wire frame. MSB-first, so byte chip bit n = pin Dn/Qn.
    // '595: the LAST byte shifted lands in chip 0 (nearest the MCU) → transmit
    // outFrame[nOut-1] … outFrame[0], padded in FRONT when the '165 chain is longer
    // (leading pad bits fall off the far end of the '595 chain).
    // '165: the FIRST byte received is chip 0 → inFrame[i] = wire[i] straight.
    const uint8_t pad = n - _nOut;
    for (uint8_t i = 0; i < pad; i++)  _wire[i] = 0;
    for (uint8_t i = 0; i < _nOut; i++) _wire[pad + i] = _outFrame[_nOut - 1 - i];

#ifdef SHIFTBUS_TEST
    for (uint8_t i = 0; i < MAX_CHAIN; i++) _wireTx[i] = _wire[i];
    if (_testBypass) {
        for (uint8_t i = 0; i < MAX_CHAIN; i++) _inFrame[i] = _testIn[i];
        return;
    }
#endif

    // '165 capture: pulse SH/LD̄ low. Data-sheet minimum pulse width is tens of ns; two
    // digitalWrite() calls at 72 MHz are comfortably above it.
    digitalWrite(_loadPin, LOW);
    digitalWrite(_loadPin, HIGH);

    _spi->beginTransaction(kShiftBusSettings);
    _spi->transfer(_wire, n);   // in-place full-duplex: TX consumed, RX replaces
    _spi->endTransaction();

    for (uint8_t i = 0; i < _nIn; i++) _inFrame[i] = _wire[i];

    // '595 publish: STCP rising edge copies the (freshly re-shifted) frame to the outputs.
    digitalWrite(_latchPin, HIGH);
    digitalWrite(_latchPin, LOW);
}

// ── Timer-ISR sampling ──────────────────────────────────────────────────────────

void ShiftBus::isrTrampoline() {
    ShiftBus* bus = _isrInstance;
    if (!bus) return;
    bus->doTransfer(false);  // ISR ships the committed frame only — never commits stage
    for (uint8_t i = 0; i < bus->_consumerCount; i++)
        bus->_consumers[i].hook(bus->_consumers[i].ctx);
}

void ShiftBus::beginIsrSampling(TIM_TypeDef* tim, uint16_t sampleHz) {
    if (!_begun || _isrActive || sampleHz == 0) return;
    if (_isrInstance && _isrInstance != this) {
        STM32Board::log("[ShiftBus] ISR sampling already claimed by another bus — skipped");
        return;
    }
    _isrInstance = this;

    // Setup-time allocation only (never in the ISR); the timer lives for the node's uptime.
    HardwareTimer* timer = new HardwareTimer(tim);
    timer->setOverflow(sampleHz, HERTZ_FORMAT);
    timer->attachInterrupt(isrTrampoline);
    _isrActive = true;   // set before resume so the first tick sees a consistent flag
    timer->resume();

    if (STM32Board::isDebug()) {
        auto& d = STM32Board::diagSerial();
        d.print(F("[ShiftBus] ISR sampling @")); d.print(sampleHz); d.println(F(" Hz"));
    }
}

void ShiftBus::addIsrConsumer(void (*hook)(void* ctx), void* ctx) {
    if (_consumerCount >= MAX_ISR_CONSUMERS) {
        STM32Board::log("[ShiftBus] MAX_ISR_CONSUMERS exceeded — consumer dropped");
        return;
    }
    if (_isrActive) noInterrupts();
    _consumers[_consumerCount] = { hook, ctx };
    _consumerCount++;
    if (_isrActive) interrupts();
}

}  // namespace OpenSkyhawk

// ── Pre-defined global instance ────────────────────────────────────────────────
// Static-init safe: the constructor stores the SPI pointer + pin numbers and nothing else.

OpenSkyhawk::ShiftBus ShiftBus1(SPI, SHIFTBUS_SCK, SHIFTBUS_MISO, SHIFTBUS_MOSI,
                                SHIFTBUS_LOAD, SHIFTBUS_LATCH);

#endif  // ARDUINO_ARCH_STM32