File PanelGroup.cpp
File List > Firmware > Libraries > PanelGroup > PanelGroup.cpp
Go to the documentation of this file
#ifdef ARDUINO_ARCH_STM32
#include "PanelGroup.h"
#include <STM32Board.h>
// ── Internal state ────────────────────────────────────────────────────────────
namespace {
static constexpr uint8_t MAX_EXPANDERS = 8;
static constexpr uint8_t MAX_ADCS = 8;
static constexpr uint8_t MAX_INT_PINS = 8; // max unique STM32 interrupt pins
static constexpr uint8_t NO_INT_PIN = 0xFF; // polling-fallback sentinel
struct ExpanderEntry {
MCP23017* chip;
uint8_t intaPin;
uint8_t intbPin;
bool mirrored; // intaPin == intbPin at registration; MIRROR mode
bool openDrain; // shares interrupt line; IOCON.ODR = 1
uint8_t portAcache; // last-known GPA0–7 state
uint8_t portBcache; // last-known GPB0–7 state
bool portAdirty; // GPA cache changed since last flush (deferred batched write)
bool portBdirty; // GPB cache changed since last flush
};
struct ADCEntry {
ADS1115* adc;
uint8_t addr;
TwoWire* wire;
};
static ExpanderEntry _expanders[MAX_EXPANDERS];
static uint8_t _expanderCount = 0;
static ADCEntry _adcs[MAX_ADCS];
static uint8_t _adcCount = 0;
// Unique STM32 interrupt pins and their flags
static uint8_t _intPins[MAX_INT_PINS];
static uint8_t _intPinCount = 0;
static volatile bool _intFlags[MAX_INT_PINS];
// Master-loss watchdog: HB_0 (PanelBridge master heartbeat) timeout → WARNING.
// 3× the 500 ms HB_0 cadence, matching the Bridge's own HB_TIMEOUT/HB ratio (3000/500).
static constexpr uint32_t MASTER_TIMEOUT_MS = 1500;
// Timers
static uint32_t _lastHeartbeatMs = 0;
static uint32_t _lastFallbackMs = 0;
static uint32_t _lastMasterMs = 0; // millis() of last HB_0 seen
static bool _everSawMaster = false; // arm the timeout only after the first HB_0
static uint16_t _rxCount = 0; // diagnostic only; wraps at 65535 (~131 s at full bus load)
// ── ISR functions — one static function per interrupt-pin slot ───────────────
// No dynamic ISR registration; each slot has a dedicated handler.
// _intFlags is volatile; ISR writes are single-byte — atomic on ARM Cortex-M3.
static void isr0() { _intFlags[0] = true; }
static void isr1() { _intFlags[1] = true; }
static void isr2() { _intFlags[2] = true; }
static void isr3() { _intFlags[3] = true; }
static void isr4() { _intFlags[4] = true; }
static void isr5() { _intFlags[5] = true; }
static void isr6() { _intFlags[6] = true; }
static void isr7() { _intFlags[7] = true; }
static constexpr void (*_isrTable[MAX_INT_PINS])() = {
isr0, isr1, isr2, isr3, isr4, isr5, isr6, isr7
};
// Returns the slot index for the given STM32 pin, adding it if not yet seen.
// Returns 0xFF if the slot table is full.
static uint8_t findOrAddIntPin(uint8_t pin) {
for (uint8_t i = 0; i < _intPinCount; i++)
if (_intPins[i] == pin) return i;
if (_intPinCount < MAX_INT_PINS) {
_intPins[_intPinCount] = pin;
_intFlags[_intPinCount] = false;
return _intPinCount++;
}
return 0xFF;
}
// ── CAN callbacks ─────────────────────────────────────────────────────────────
static void onCanReceive(uint32_t canId, const uint8_t* data, uint8_t len) {
// HB_0 — PanelBridge master heartbeat. An unconditional liveness beacon (independent
// of DCS data flow): refresh the master watchdog and clear any no-master WARNING.
if (canId == canIdHb(0)) {
_lastMasterMs = millis();
_everSawMaster = true;
STM32Board::setWarning(false);
return;
}
if (canId != CAN_ID_CTRL_BCAST) return;
if (len != sizeof(ControlPacketPair)) return; // malformed — discard
STM32Board::setLinkActive(true); // valid CTRL_BCAST → data flowing → CONNECTED (green solid)
_rxCount++;
ControlPacketPair pair;
memcpy(&pair, data, sizeof(pair));
auto dispatch = [](const ControlPacket& pkt) {
if (pkt.controlId == 0x0000) return;
for (auto* o = OpenSkyhawk::OutputBase::head(); o; o = o->next())
o->onControlPacket(pkt.controlId, pkt.value);
};
dispatch(pair.a);
dispatch(pair.b);
}
static void onSyncReq() {
STM32Board::log("[PanelGroup] SYNC_REQ -> forceReport burst");
for (auto* p = OpenSkyhawk::InputBase::head(); p; p = p->next())
p->forceReport();
CANProtocol::flushBatched(canIdEvt(NODE_ID));
}
} // anonymous namespace
// ── OpenSkyhawk::InputBase ────────────────────────────────────────────────────
OpenSkyhawk::InputBase* OpenSkyhawk::InputBase::_head = nullptr;
OpenSkyhawk::OutputBase* OpenSkyhawk::OutputBase::_head = nullptr;
OpenSkyhawk::InputBase::InputBase() : _next(_head) { _head = this; }
OpenSkyhawk::OutputBase::OutputBase() : _next(_head) { _head = this; }
OpenSkyhawk::InputBase* OpenSkyhawk::InputBase::head() { return _head; }
OpenSkyhawk::InputBase* OpenSkyhawk::InputBase::next() const { return _next; }
OpenSkyhawk::OutputBase* OpenSkyhawk::OutputBase::head() { return _head; }
OpenSkyhawk::OutputBase* OpenSkyhawk::OutputBase::next() const { return _next; }
// ── PanelGroup::registerADC / registerExpander ────────────────────────────────
namespace PanelGroup {
void registerADC(ADS1115& adc, uint8_t addr, TwoWire& wire) {
if (_adcCount >= MAX_ADCS) return;
// Deduplicate: skip if already registered
for (uint8_t i = 0; i < _adcCount; i++)
if (_adcs[i].adc == &adc) return;
auto& e = _adcs[_adcCount++];
e.adc = &adc;
e.addr = addr;
e.wire = &wire;
}
void registerExpander(MCP23017& chip, uint8_t intaPin, uint8_t intbPin) {
if (_expanderCount >= MAX_EXPANDERS) return;
auto& e = _expanders[_expanderCount++];
e.chip = &chip;
e.intaPin = intaPin;
e.intbPin = intbPin;
e.mirrored = (intaPin == intbPin);
e.openDrain = false; // resolved in setup()
e.portAcache = 0xFF;
e.portBcache = 0xFF;
e.portAdirty = false;
e.portBdirty = false;
}
void registerExpander(MCP23017& chip) {
registerExpander(chip, NO_INT_PIN, NO_INT_PIN);
}
// ── PanelGroup::setup ─────────────────────────────────────────────────────────
void setup() {
// Step 1 — STM32Board init
STM32Board::begin();
if (STM32Board::isDebug()) {
auto& d = STM32Board::diagSerial();
d.println(F("=============================="));
d.print(F(" PanelGroup NODE_ID=")); d.println((int)NODE_ID);
d.println(F("=============================="));
}
CANProtocol::onStatusChange(STM32Board::onCanStatus);
// Step 2a — ADC begin (address and bus were captured at registerADC time)
for (uint8_t i = 0; i < _adcCount; i++)
_adcs[i].adc->begin(_adcs[i].addr, _adcs[i].wire);
// Step 2b — determine open-drain: any two expanders sharing an interrupt pin
for (uint8_t i = 0; i < _expanderCount; i++) {
if (_expanders[i].intaPin == NO_INT_PIN) continue;
for (uint8_t j = 0; j < _expanderCount; j++) {
if (i == j) continue;
if (_expanders[j].intaPin == NO_INT_PIN) continue;
bool shared = (_expanders[i].intaPin == _expanders[j].intaPin)
|| (_expanders[i].intaPin == _expanders[j].intbPin)
|| (_expanders[i].intbPin != NO_INT_PIN &&
(_expanders[i].intbPin == _expanders[j].intaPin
|| _expanders[i].intbPin == _expanders[j].intbPin));
if (shared) { _expanders[i].openDrain = true; break; }
}
}
// Step 2c — chip init and IOCON configuration
for (uint8_t i = 0; i < _expanderCount; i++) {
auto& e = _expanders[i];
e.chip->init();
if (e.mirrored) {
e.chip->interruptMode(MCP23017InterruptMode::Or);
}
if (e.openDrain) {
uint8_t iocon = e.chip->readRegister(MCP23017Register::IOCON);
e.chip->writeRegister(MCP23017Register::IOCON, iocon | 0x04u); // ODR bit
}
}
// Step 3 — configure each pin via its owning input/output object
for (auto* p = OpenSkyhawk::InputBase::head(); p; p = p->next()) p->configure();
for (auto* p = OpenSkyhawk::OutputBase::head(); p; p = p->next()) p->configure();
// Step 2d — enable interrupt-on-change, read baseline, attach STM32 ISRs
for (uint8_t i = 0; i < _expanderCount; i++) {
auto& e = _expanders[i];
// Enable interrupt-on-change only on input pins, excluding GPA7/GPB7.
// GPA7/GPB7 must be outputs per Microchip silicon bug (Rev D erratum):
// asserting GPINTEN on them while in input mode corrupts SDA mid-transfer.
// IODIR bit=1 means input; mask to 0x7F to exclude bit 7 on both ports.
uint8_t gpintenA = e.chip->readRegister(MCP23017Register::IODIR_A) & 0x7Fu;
uint8_t gpintenB = e.chip->readRegister(MCP23017Register::IODIR_B) & 0x7Fu;
e.chip->writeRegister(MCP23017Register::GPINTEN_A, gpintenA);
e.chip->writeRegister(MCP23017Register::GPINTEN_B, gpintenB);
// Baseline read — captures initial state after configure() sets IODIR
e.portAcache = e.chip->readPort(MCP23017Port::A);
e.portBcache = e.chip->readPort(MCP23017Port::B);
if (e.intaPin == NO_INT_PIN) continue; // polling-fallback chip
uint8_t slotA = findOrAddIntPin(e.intaPin);
if (slotA == 0xFF) {
STM32Board::log("[PanelGroup] MAX_INT_PINS exceeded — chip falls back to polling");
continue;
}
// Attach INTA ISR if this is the first chip claiming this pin
if (_intPins[slotA] == e.intaPin) {
bool firstClaim = true;
for (uint8_t k = 0; k < i; k++) {
if (_expanders[k].intaPin == e.intaPin || _expanders[k].intbPin == e.intaPin) {
firstClaim = false; break;
}
}
if (firstClaim) {
pinMode(e.intaPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(e.intaPin), _isrTable[slotA], FALLING);
}
}
if (!e.mirrored && e.intbPin != NO_INT_PIN) {
uint8_t slotB = findOrAddIntPin(e.intbPin);
if (slotB != 0xFF) {
bool firstClaim = true;
for (uint8_t k = 0; k < i; k++) {
if (_expanders[k].intaPin == e.intbPin || _expanders[k].intbPin == e.intbPin) {
firstClaim = false; break;
}
}
if (firstClaim) {
pinMode(e.intbPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(e.intbPin), _isrTable[slotB], FALLING);
}
}
}
}
// Step 4/5 — CAN callbacks and start
CANProtocol::onReceive(onCanReceive);
CANProtocol::onSyncReq(onSyncReq);
CANProtocol::filterAcceptId(canIdHb(0)); // accept the master heartbeat (HB_0)
CANProtocol::start();
// Step 6 — boot EVT burst
for (auto* p = OpenSkyhawk::InputBase::head(); p; p = p->next()) p->forceReport();
// Step 7 — flush trailing batch
CANProtocol::flushBatched(canIdEvt(NODE_ID));
// Step 8 — READY frame + arm heartbeat / master-watchdog timers
CANProtocol::send(canIdReady(NODE_ID), nullptr, 0);
_lastHeartbeatMs = millis();
_lastMasterMs = millis(); // seed; timeout stays disarmed until first HB_0 seen
}
// ── PanelGroup::loop ──────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// 1. Interrupt dispatch: check flags, read INTCAP, update caches
for (uint8_t slot = 0; slot < _intPinCount; slot++) {
if (!_intFlags[slot]) continue;
_intFlags[slot] = false;
uint8_t pin = _intPins[slot];
// All chips on this line — must visit all to clear open-drain assertion
for (uint8_t j = 0; j < _expanderCount; j++) {
auto& e = _expanders[j];
if (e.intaPin != pin && e.intbPin != pin) continue;
uint8_t intfA = 0, intfB = 0;
e.chip->interruptedBy(intfA, intfB);
if (intfA == 0 && intfB == 0) continue;
uint8_t capA = 0, capB = 0;
e.chip->clearInterrupts(capA, capB);
// INTCAP (capA/capB) holds the pin state at interrupt time, not now.
// A fast bounce can clear before loop() runs, leaving the cache stale.
// Read live GPIO to guarantee cache matches current pin state.
if (intfA) e.portAcache = e.chip->readPort(MCP23017Port::A);
if (intfB) e.portBcache = e.chip->readPort(MCP23017Port::B);
}
}
// 2. Polling fallback (~20 ms) for chips with no interrupt pin
if (now - _lastFallbackMs >= 20) {
_lastFallbackMs = now;
for (uint8_t i = 0; i < _expanderCount; i++) {
auto& e = _expanders[i];
if (e.intaPin != NO_INT_PIN) continue;
e.portAcache = e.chip->readPort(MCP23017Port::A);
e.portBcache = e.chip->readPort(MCP23017Port::B);
}
}
// 3. Poll all inputs
for (auto* p = OpenSkyhawk::InputBase::head(); p; p = p->next()) p->poll();
// 4. CAN drain: CTRL_BCAST → onControlPacket; SYNC_REQ → onSyncReq; TEST_SEQ → echo
CANProtocol::drain();
// 5. Update all outputs (steppers, PWM)
for (auto* p = OpenSkyhawk::OutputBase::head(); p; p = p->next()) p->update();
// 6. Heartbeat every 500 ms
if (now - _lastHeartbeatMs >= 500) {
_lastHeartbeatMs = now;
HeartbeatPayload hb = CANProtocol::makeHeartbeatPayload(NODE_ID, _rxCount);
CANProtocol::send(canIdHb(NODE_ID),
reinterpret_cast<const uint8_t*>(&hb), sizeof(hb));
}
// 7. Master-loss watchdog: once a master has been seen, raise WARNING if HB_0 stops.
// Cleared in onCanReceive() when the next HB_0 arrives.
if (_everSawMaster && now - _lastMasterMs > MASTER_TIMEOUT_MS) {
STM32Board::setWarning(true);
}
}
// ── MCP cache bridge ──────────────────────────────────────────────────────────
bool readCachedPin(const MCP23017& chip, uint8_t port, uint8_t bit) {
for (uint8_t i = 0; i < _expanderCount; i++) {
if (_expanders[i].chip == &chip) {
uint8_t cache = (port == 0) ? _expanders[i].portAcache
: _expanders[i].portBcache;
return (cache >> bit) & 1u;
}
}
return false;
}
void writeCachedPin(MCP23017& chip, uint8_t port, uint8_t bit, bool value) {
for (uint8_t i = 0; i < _expanderCount; i++) {
if (_expanders[i].chip != &chip) continue;
uint8_t& cache = (port == 0) ? _expanders[i].portAcache
: _expanders[i].portBcache;
if (value) cache |= (1u << bit);
else cache &= ~(1u << bit);
chip.digitalWrite(port * 8 + bit, value ? HIGH : LOW);
return;
}
}
// Deferred batched write: update only the cache + mark the port dirty — NO I2C. Pair with
// flushExpanderWrites() to push each touched port in a single writePort() (1 transaction)
// instead of one read-modify-write per pin. Used by multi-pin outputs (e.g. StepperMotor
// coils) where per-pin I2C would otherwise dominate the step rate.
void writeCachedPinDeferred(MCP23017& chip, uint8_t port, uint8_t bit, bool value) {
for (uint8_t i = 0; i < _expanderCount; i++) {
if (_expanders[i].chip != &chip) continue;
uint8_t& cache = (port == 0) ? _expanders[i].portAcache
: _expanders[i].portBcache;
if (value) cache |= (1u << bit);
else cache &= ~(1u << bit);
if (port == 0) _expanders[i].portAdirty = true;
else _expanders[i].portBdirty = true;
return;
}
}
// Push every port dirtied by writeCachedPinDeferred() — one writePort() per dirty port.
// Cheap no-op when nothing is pending (the common GPIO-only path just scans the flags).
void flushExpanderWrites() {
for (uint8_t i = 0; i < _expanderCount; i++) {
ExpanderEntry& e = _expanders[i];
if (e.portAdirty) { e.chip->writePort(MCP23017Port::A, e.portAcache); e.portAdirty = false; }
if (e.portBdirty) { e.chip->writePort(MCP23017Port::B, e.portBcache); e.portBdirty = false; }
}
}
bool readLivePin(MCP23017& chip, uint8_t port, uint8_t bit) {
for (uint8_t i = 0; i < _expanderCount; i++) {
if (_expanders[i].chip != &chip) continue;
uint8_t v = chip.readPort(port == 0 ? MCP23017Port::A : MCP23017Port::B); // live I2C read
if (port == 0) _expanders[i].portAcache = v;
else _expanders[i].portBcache = v;
return (v >> bit) & 1u;
}
return false;
}
} // namespace PanelGroup
#endif // ARDUINO_ARCH_STM32