diff --git a/src/AudioThread.h b/src/AudioThread.h index 1129ee087..3a44bf823 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -12,11 +12,18 @@ #include #include +// A board with an I2S amplifier opts in by defining AUDIO_AMP_ENABLE(on) in its variant.h to power the +// amp on/off around playback (e.g. an enable pin on an I/O expander). The includes below expose the +// expander instances (io / mcpIoExpander) those macros typically reference. #ifdef USE_XL9555 #include "ExtensionIOXL9555.hpp" extern ExtensionIOXL9555 io; #endif +#ifdef USE_MCP23017 +#include "platform/esp32/ExtensionIOMCP23017.h" +#endif + #define AUDIO_THREAD_INTERVAL_MS 100 class AudioThread : public concurrency::OSThread @@ -26,8 +33,8 @@ class AudioThread : public concurrency::OSThread void beginRttl(const void *data, uint32_t len) { -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(true); #endif setCPUFast(true); rtttlFile = std::unique_ptr(new AudioFileSourcePROGMEM(data, len)); @@ -54,8 +61,8 @@ class AudioThread : public concurrency::OSThread rtttlFile = nullptr; setCPUFast(false); -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(false); #endif } @@ -66,14 +73,14 @@ class AudioThread : public concurrency::OSThread i2sRtttl = nullptr; } -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(true); #endif auto sam = std::unique_ptr(new ESP8266SAM); sam->Say(audioOut.get(), text); setCPUFast(false); -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(false); #endif } diff --git a/src/main.cpp b/src/main.cpp index 0e941e6a3..da07c6e01 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -173,6 +173,10 @@ AudioThread *audioThread = nullptr; ExtensionIOXL9555 io; #endif +#ifdef USE_MCP23017 +#include "platform/esp32/ExtensionIOMCP23017.h" +#endif + #if HAS_TFT extern void tftSetup(void); #endif @@ -606,6 +610,12 @@ void setup() powerStatus->observe(&power->newStatus); power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration +#ifdef USE_MCP23017 + // Bring up the I2C IO expander (LoRa reset, LCD reset, GPS wake) now that the PMU rails are up, + // before the I2C scan and radio/display init + mcp23017EarlyInit(); +#endif + #if !MESHTASTIC_EXCLUDE_I2C // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to // accessories diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d59064ba9..fac3aff59 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -30,6 +30,10 @@ #include "platform/portduino/USBHal.h" #endif +#if defined(ARCH_ESP32) && defined(USE_MCP23017) +#include "platform/esp32/MCP23017LockingArduinoHal.h" +#endif + #ifdef ARCH_STM32WL #include "STM32WLE5JCInterface.h" #endif @@ -401,6 +405,10 @@ std::unique_ptr initLoRa() #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings); RadioLibHAL = loraHal; +#elif defined(ARCH_ESP32) && defined(USE_MCP23017) + // Radio control lines (RESET/DIO1/BUSY) are virtual pins on an MCP23017 I2C expander + LockingArduinoHal *loraHal = new MCP23017LockingArduinoHal(SPI, loraSpiSettings, mcpIoExpander); + RadioLibHAL = loraHal; #else // HW_SPI1_DEVICE LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings); RadioLibHAL = loraHal; diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 832e421e5..17a97f286 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -363,6 +363,40 @@ The CW size is determined by setTransmitDelay() and depends either on the curren of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is currently active. */ +// In software-IRQ-poll mode (LORA_DIO1_SOFTWARE_POLL) a 1ms poll tick is almost always pending, so +// TX timers must be allowed to overwrite the pending notification or TX scheduling starves. On all +// other targets keep the historical non-overwriting behavior. +#ifdef LORA_DIO1_SOFTWARE_POLL +static constexpr bool txTimerOverwrite = true; +#else +static constexpr bool txTimerOverwrite = false; +#endif + +// cppcheck-suppress constParameterPointer ; a function pointer can't meaningfully point to const +bool RadioLibInterface::isIsrTxCallback(void (*callback)()) +{ + return callback == isrTxLevel0; +} + +void RadioLibInterface::scheduleIrqPollTick() +{ + // Never overwrite a pending notification (especially TRANSMIT_DELAY_COMPLETED), + // otherwise poll ticks would starve TX scheduling. + // + // There is a single notification slot, so while a TX is queued and the radio is busy receiving, + // the self-rescheduling TRANSMIT_DELAY_COMPLETED timer (which does overwrite, see txTimerOverwrite) + // can keep the slot and prevent a poll tick from being scheduled. In that window a completing + // RX/TX is not seen by the poll; RadioInterface's pollMissedIrqs() (~1s) is the backup that + // recovers it, so the effect is bounded added latency under heavy contention, not a lost event. + notifyLater(1, ISR_POLL_TICK, false); +} + +void RadioLibInterface::deliverPendingIrqFromPoll(PendingISR cause) +{ + disableInterrupt(); // stop polling; this is the poll-path equivalent of isrLevel0Common() + notify(cause, true); +} + void RadioLibInterface::onNotify(uint32_t notification) { @@ -385,6 +419,9 @@ void RadioLibInterface::onNotify(uint32_t notification) startReceive(); setTransmitDelay(); break; + case ISR_POLL_TICK: + handleSoftwareLoraIrqPoll(); + break; case TRANSMIT_DELAY_COMPLETED: // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread @@ -398,7 +435,7 @@ void RadioLibInterface::onNotify(uint32_t notification) long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; if (delay_remaining > 0) { // There's still some delay pending on this packet, so resume waiting for it to elapse - notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); + notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); #if !MESHTASTIC_EXCLUDE_BEACON } else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) { // The beacon's target radio config is invalid (bad preset/region, or an @@ -455,7 +492,7 @@ void RadioLibInterface::setTransmitDelay() unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); - notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); + notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); } else if (p->rx_snr == 0 && p->rx_rssi == 0) { /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. * This assumption is valid because of the offset generated by the radio to account for the noise @@ -474,7 +511,7 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable } } @@ -483,7 +520,7 @@ void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = getTxDelayMsecWeighted(p); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index e903703f7..ab822ef24 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -52,17 +52,17 @@ class STM32WLx_ModuleWrapper : public STM32WLx_Module class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread { + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + + protected: /// Used as our notification from the ISR - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED, ISR_POLL_TICK }; /** * Raw ISR handler that just calls our polymorphic method */ static void isrTxLevel0(), isrLevel0Common(PendingISR code); - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); - - protected: ModemType_t modemType = RADIOLIB_MODEM_LORA; DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } PacketConfig_t getPacketConfig() const @@ -350,4 +350,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified void checkRxDoneIrqFlag(); void checkTxDoneIrqFlag(); + + /** Software-poll substitute for a hardware DIO interrupt, for radios whose IRQ line sits behind + * an I2C IO expander with no INT routed to the MCU (e.g. Meshnology W10, LORA_DIO1_SOFTWARE_POLL). + * The chip-specific subclass polls the radio's IRQ status register from the radio thread and + * synthesizes ISR_TX/ISR_RX events equivalent to the hardware DIO1 interrupt. */ + void deliverPendingIrqFromPoll(PendingISR cause); + void scheduleIrqPollTick(); + static bool isIsrTxCallback(void (*callback)()); + virtual void handleSoftwareLoraIrqPoll() {} }; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index b4b9060a9..ae4c485dd 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -274,11 +274,62 @@ template int16_t SX126xInterface::getCurrentRSSI() return (int16_t)round(rssi); } +template void SX126xInterface::enableInterrupt(void (*callback)()) +{ +#ifdef LORA_DIO1_SOFTWARE_POLL + irqPollingActive = true; + pollTxMode = isIsrTxCallback(callback); + scheduleIrqPollTick(); +#else + lora.setDio1Action(callback); +#endif +} + template void SX126xInterface::disableInterrupt() { +#ifdef LORA_DIO1_SOFTWARE_POLL + irqPollingActive = false; +#else lora.clearDio1Action(); +#endif } +#ifdef LORA_DIO1_SOFTWARE_POLL +template void SX126xInterface::handleSoftwareLoraIrqPoll() +{ + if (!irqPollingActive) + return; + + // getIrqFlags()/clearIrqFlags() both operate on the raw SX126x IRQ register, so use the + // chip-specific RADIOLIB_SX126X_IRQ_* masks on both the read and the clear. + uint16_t irq = lora.getIrqFlags(); + const uint16_t rxEventMask = + RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT | RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR; + const uint16_t noisyRxMask = RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID; + + // Do NOT treat a preamble/header-only IRQ as a full RX event: noisy preamble detections would + // repeatedly trigger readData() and starve TX scheduling. Clear these non-terminal bits, or the + // poll loop spins at high rate while they stay latched. + if (!pollTxMode && (irq & noisyRxMask) && ((irq & ~noisyRxMask) == 0U)) { + lora.clearIrqFlags(noisyRxMask); + scheduleIrqPollTick(); + return; + } + + if (pollTxMode) { + if (irq & (RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT)) { + deliverPendingIrqFromPoll(ISR_TX); + return; + } + } else if (irq & rxEventMask) { + deliverPendingIrqFromPoll(ISR_RX); + return; + } + + scheduleIrqPollTick(); +} +#endif + template void SX126xInterface::setStandby() { checkNotification(); // handle any pending interrupts before we force standby diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b7d3675ab..0bf977ba2 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -2,6 +2,7 @@ #if RADIOLIB_EXCLUDE_SX126X != 1 #include "RadioLibInterface.h" +#include "configuration.h" /** * \brief Adapter for SX126x radio family. Implements common logic for child classes. @@ -51,7 +52,11 @@ template class SX126xInterface : public RadioLibInterface /** * Enable a particular ISR callback glue function */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + virtual void enableInterrupt(void (*callback)()) override; + +#ifdef LORA_DIO1_SOFTWARE_POLL + void handleSoftwareLoraIrqPoll() override; +#endif /** can we detect a LoRa preamble on the current channel? */ virtual bool isChannelActive() override; @@ -79,6 +84,10 @@ template class SX126xInterface : public RadioLibInterface uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } private: +#ifdef LORA_DIO1_SOFTWARE_POLL + bool irqPollingActive = false; + bool pollTxMode = false; +#endif /** Some boards require GPIO control of tx vs rx paths */ void setTransmitEnable(bool txon); }; diff --git a/src/modules/HopScalingModule.cpp b/src/modules/HopScalingModule.cpp index 77269043e..1045e4f02 100644 --- a/src/modules/HopScalingModule.cpp +++ b/src/modules/HopScalingModule.cpp @@ -420,7 +420,7 @@ void HopScalingModule::trimIfNeeded() newCount++; } } - count = newCount; + this->count = newCount; } } diff --git a/src/platform/esp32/ExtensionIOMCP23017.cpp b/src/platform/esp32/ExtensionIOMCP23017.cpp new file mode 100644 index 000000000..48c1058c9 --- /dev/null +++ b/src/platform/esp32/ExtensionIOMCP23017.cpp @@ -0,0 +1,192 @@ +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "ExtensionIOMCP23017.h" +#include "concurrency/LockGuard.h" + +ExtensionIOMCP23017 mcpIoExpander; + +void ExtensionIOMCP23017::begin(TwoWire &wire, uint8_t addr, int sda, int scl) +{ + if (_begun) + return; + _wire = &wire; + _addr = addr; + wire.begin(sda, scl); + _begun = true; +} + +bool ExtensionIOMCP23017::readReg(uint8_t reg, uint8_t &val) +{ + if (!_wire || !_begun) + return false; + _wire->beginTransmission(_addr); + _wire->write(reg); + if (_wire->endTransmission() != 0) + return false; + if (_wire->requestFrom((int)_addr, 1) != 1 || !_wire->available()) + return false; + val = (uint8_t)_wire->read(); + return true; +} + +uint8_t ExtensionIOMCP23017::readReg(uint8_t reg) +{ + uint8_t val = 0; + readReg(reg, val); + return val; +} + +void ExtensionIOMCP23017::writeReg(uint8_t reg, uint8_t val) +{ + if (!_wire || !_begun) + return; + _wire->beginTransmission(_addr); + _wire->write(reg); + _wire->write(val); + _wire->endTransmission(); +} + +void ExtensionIOMCP23017::updateDirectionBit(uint8_t pin, bool asOutput) +{ + uint8_t reg = iodirRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip the write rather than clobber the other 7 pins on a failed read + return; + uint8_t mask = bitForPin(pin); + if (asOutput) + v &= ~mask; // output: bit 0 + else + v |= mask; // input: bit 1 + writeReg(reg, v); +} + +void ExtensionIOMCP23017::pinMode(uint8_t pin, uint8_t mode) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + if (mode == OUTPUT) { + updateDirectionBit(pin, true); + } else { + updateDirectionBit(pin, false); + uint8_t reg = gppuRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip rather than clobber the bank on a failed read + return; + if (mode == INPUT_PULLUP) + v |= bitForPin(pin); + else + v &= ~bitForPin(pin); + writeReg(reg, v); + } +} + +void ExtensionIOMCP23017::digitalWrite(uint8_t pin, uint8_t value) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + uint8_t reg = olatRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip rather than clobber the bank (e.g. drop LORA_NRST) on a failed read + return; + uint8_t mask = bitForPin(pin); + if (value == LOW) + v &= ~mask; + else + v |= mask; + writeReg(reg, v); +} + +int ExtensionIOMCP23017::digitalRead(uint8_t pin) +{ + if (pin > 15) + return LOW; + concurrency::LockGuard guard(&_lock); + uint8_t v; + if (!readReg(gpioRegForPin(pin), v)) + return HIGH; // fail-safe: a failed read must not look like "LoRa BUSY released" and let RadioLib + // start an SPI transaction too early. DIO1 is polled via the radio IRQ register, not + // this pin, so reading it high on error is harmless there. + return (v & bitForPin(pin)) ? HIGH : LOW; +} + +void ExtensionIOMCP23017::enablePinChangeInterrupt(uint8_t pin, bool enable) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + uint8_t regG = gpintenRegForPin(pin); + uint8_t v; + if (!readReg(regG, v)) // skip rather than clobber the bank on a failed read + return; + if (enable) + v |= bitForPin(pin); + else + v &= ~bitForPin(pin); + writeReg(regG, v); + // INTCON: 0 = interrupt on pin change from previous value + uint8_t regI = intconRegForPin(pin); + if (!readReg(regI, v)) + return; + v &= ~bitForPin(pin); + writeReg(regI, v); +} + +void ExtensionIOMCP23017::clearInterruptLatches() +{ + if (!_begun) + return; + concurrency::LockGuard guard(&_lock); + (void)readReg(0x10); // INTCAPA - clears INTA condition + (void)readReg(0x11); // INTCAPB +} + +uint8_t ExtensionIOMCP23017::readRegister(uint8_t reg) +{ + concurrency::LockGuard guard(&_lock); + return readReg(reg); +} + +void mcp23017EarlyInit() +{ + mcpIoExpander.begin(Wire, MCP23017_ADDR, I2C_SDA, I2C_SCL); + +#ifdef EXIO_LCD_RST + mcpIoExpander.pinMode(EXIO_LCD_RST, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_LCD_RST, LOW); + delay(10); + mcpIoExpander.digitalWrite(EXIO_LCD_RST, HIGH); + delay(20); +#endif + +#ifdef EXIO_LORA_NRST + mcpIoExpander.pinMode(EXIO_LORA_NRST, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_LORA_NRST, HIGH); +#endif + +#ifdef EXIO_LORA_BUSY + mcpIoExpander.pinMode(EXIO_LORA_BUSY, INPUT); +#endif + +#ifdef EXIO_LORA_DIO1 + mcpIoExpander.pinMode(EXIO_LORA_DIO1, INPUT); +#endif + +#ifdef EXIO_GPS_WAKE + mcpIoExpander.pinMode(EXIO_GPS_WAKE, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_GPS_WAKE, HIGH); +#endif + +#ifdef EXIO_IMU_INT1 + mcpIoExpander.pinMode(EXIO_IMU_INT1, INPUT); +#endif + + LOG_INFO("MCP23017 0x%02x: IODIRA=0x%02x IODIRB=0x%02x GPIOA=0x%02x GPIOB=0x%02x", MCP23017_ADDR, + mcpIoExpander.readRegister(0x00), mcpIoExpander.readRegister(0x01), mcpIoExpander.readRegister(0x12), + mcpIoExpander.readRegister(0x13)); +} + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/ExtensionIOMCP23017.h b/src/platform/esp32/ExtensionIOMCP23017.h new file mode 100644 index 000000000..f540019c7 --- /dev/null +++ b/src/platform/esp32/ExtensionIOMCP23017.h @@ -0,0 +1,66 @@ +#pragma once + +#include "concurrency/Lock.h" +#include +#include + +/** + * MCP23017 16-bit I2C GPIO expander (pins 0-15 = GPA0-7, GPB0-7). + * + * Used by boards that route radio/display control lines through the expander + * (e.g. Meshnology W10). RadioLib access goes through MCP23017LockingArduinoHal, + * which maps virtual pins MCP23017_VPIN_BASE..+15 onto local pins 0-15. + */ +class ExtensionIOMCP23017 +{ + public: + ExtensionIOMCP23017() : _wire(nullptr), _addr(0), _begun(false) {} + + void begin(TwoWire &wire, uint8_t addr, int sda, int scl); + + /** Local pin index 0-15 (GPA0=0 ... GPB7=15), not the virtual RadioLib pin. */ + void pinMode(uint8_t pin, uint8_t mode); + void digitalWrite(uint8_t pin, uint8_t value); + int digitalRead(uint8_t pin); + + /** Enable the MCP23017 pin-change interrupt so /INT asserts (only useful if /INT is wired to the MCU). */ + void enablePinChangeInterrupt(uint8_t pin, bool enable); + + /** Read INTCAPx to clear a latched interrupt condition after MCP /INT asserts. */ + void clearInterruptLatches(); + + /** Raw register read (0x00-0x1A), for bring-up / debug. */ + uint8_t readRegister(uint8_t reg); + + private: + uint8_t readReg(uint8_t reg); + // Checked read: returns false (and leaves val untouched) on any I2C error, so a glitched read + // can't drive a read-modify-write that clobbers the rest of the bank. + bool readReg(uint8_t reg, uint8_t &val); + void writeReg(uint8_t reg, uint8_t val); + void updateDirectionBit(uint8_t pin, bool asOutput); + uint8_t iodirRegForPin(uint8_t pin) const { return pin < 8 ? 0x00 : 0x01; } + uint8_t gpioRegForPin(uint8_t pin) const { return pin < 8 ? 0x12 : 0x13; } + uint8_t olatRegForPin(uint8_t pin) const { return pin < 8 ? 0x14 : 0x15; } + uint8_t gppuRegForPin(uint8_t pin) const { return pin < 8 ? 0x0C : 0x0D; } + uint8_t gpintenRegForPin(uint8_t pin) const { return pin < 8 ? 0x04 : 0x05; } + uint8_t intconRegForPin(uint8_t pin) const { return pin < 8 ? 0x08 : 0x09; } + uint8_t bitForPin(uint8_t pin) const { return 1u << (pin & 7); } + + TwoWire *_wire; + uint8_t _addr; + bool _begun; + // Serializes register access: the radio HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) both + // reach this expander from different threads, and the read-modify-write paths are not atomic. + concurrency::Lock _lock; +}; + +/** Global instance shared by the early-init hook and the RadioLib HAL. */ +extern ExtensionIOMCP23017 mcpIoExpander; + +/** + * Bring up the expander and set board-specific pin directions/levels (LoRa reset, + * LCD reset, GPS wake, ...). Must run after power->setup() so the PMU rails are up, + * and before the I2C scan / radio / display init. + */ +void mcp23017EarlyInit(); diff --git a/src/platform/esp32/MCP23017LockingArduinoHal.cpp b/src/platform/esp32/MCP23017LockingArduinoHal.cpp new file mode 100644 index 000000000..3182b9ae3 --- /dev/null +++ b/src/platform/esp32/MCP23017LockingArduinoHal.cpp @@ -0,0 +1,107 @@ +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "MCP23017LockingArduinoHal.h" +#include "SPILock.h" + +MCP23017LockingArduinoHal::MCP23017LockingArduinoHal(SPIClass &spi, SPISettings spiSettings, ExtensionIOMCP23017 &expander) + : LockingArduinoHal(spi, spiSettings), mcp(expander) +{ +#if MCP23017_INT_ESP32_PIN < 0 +#if defined(LORA_DIO1_SOFTWARE_POLL) + LOG_INFO("MCP23017 /INT not wired: LoRa DIO1 IRQ simulated by polling the radio IRQ status register"); +#else + LOG_WARN("MCP23017_INT_ESP32_PIN unset and no LORA_DIO1_SOFTWARE_POLL: LoRa DIO1 interrupts will not work"); +#endif +#endif +} + +bool MCP23017LockingArduinoHal::isMcpPin(uint32_t pin) +{ + return pin >= MCP23017_VPIN_BASE && pin <= MCP23017_VPIN_BASE + 15; +} + +void MCP23017LockingArduinoHal::pinMode(uint32_t pin, uint32_t mode) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + mcp.pinMode(local, mode == GpioModeOutput ? OUTPUT : INPUT); + return; + } + ArduinoHal::pinMode(pin, mode); +} + +void MCP23017LockingArduinoHal::digitalWrite(uint32_t pin, uint32_t value) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + mcp.digitalWrite(local, value == GpioLevelHigh ? HIGH : LOW); + return; + } + ArduinoHal::digitalWrite(pin, value); +} + +uint32_t MCP23017LockingArduinoHal::digitalRead(uint32_t pin) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + return (uint32_t)mcp.digitalRead(local); + } + return ArduinoHal::digitalRead(pin); +} + +void MCP23017LockingArduinoHal::attachInterrupt(uint32_t interruptNum, void (*cb)(void), uint32_t mode) +{ +#if MCP23017_INT_ESP32_PIN >= 0 + uint8_t idx = (uint8_t)(SX126X_DIO1 - MCP23017_VPIN_BASE); + if (idx <= 15) + mcp.enablePinChangeInterrupt(idx, true); + // MCP23017 /INT is open-drain active-low: trigger on the falling edge of the INT line + // (RadioLib passes the DIO rising-edge mode, which applies to the DIO pin, not /INT). + ArduinoHal::attachInterrupt(interruptNum, cb, GpioInterruptFalling); +#else + ArduinoHal::attachInterrupt(interruptNum, cb, mode); +#endif +} + +void MCP23017LockingArduinoHal::detachInterrupt(uint32_t interruptNum) +{ + ArduinoHal::detachInterrupt(interruptNum); +#if MCP23017_INT_ESP32_PIN >= 0 + uint8_t idx = (uint8_t)(SX126X_DIO1 - MCP23017_VPIN_BASE); + if (idx <= 15) + mcp.enablePinChangeInterrupt(idx, false); +#endif +} + +uint32_t MCP23017LockingArduinoHal::pinToInterrupt(uint32_t pin) +{ + if (isMcpPin(pin)) { +#if MCP23017_INT_ESP32_PIN >= 0 + return ::digitalPinToInterrupt((unsigned int)MCP23017_INT_ESP32_PIN); +#else + return RADIOLIB_NC; +#endif + } + return ArduinoHal::pinToInterrupt(pin); +} + +#if MCP23017_INT_ESP32_PIN >= 0 +void MCP23017LockingArduinoHal::spiBeginTransaction() +{ + spiLock->lock(); + // Clear any latched expander interrupt before talking to the radio, so a stale /INT + // level doesn't mask the next DIO1 edge. + mcp.clearInterruptLatches(); + ArduinoHal::spiBeginTransaction(); +} + +void MCP23017LockingArduinoHal::spiEndTransaction() +{ + ArduinoHal::spiEndTransaction(); + spiLock->unlock(); +} +#endif + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/MCP23017LockingArduinoHal.h b/src/platform/esp32/MCP23017LockingArduinoHal.h new file mode 100644 index 000000000..87984bc03 --- /dev/null +++ b/src/platform/esp32/MCP23017LockingArduinoHal.h @@ -0,0 +1,48 @@ +#pragma once + +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "mesh/RadioLibInterface.h" +#include "platform/esp32/ExtensionIOMCP23017.h" +#include + +#ifndef MCP23017_VPIN_BASE +#define MCP23017_VPIN_BASE 100 +#endif + +#ifndef MCP23017_INT_ESP32_PIN +#define MCP23017_INT_ESP32_PIN (-1) +#endif + +/** + * Routes RadioLib virtual GPIO MCP23017_VPIN_BASE..+15 to an MCP23017 I2C expander (GPA0-GPB7); + * all other pins fall through to the regular Arduino GPIO HAL. + * + * DIO1 interrupts: if the expander /INT line is wired to an ESP32 GPIO, set MCP23017_INT_ESP32_PIN + * and a real edge interrupt is used. If not (MCP23017_INT_ESP32_PIN < 0), define + * LORA_DIO1_SOFTWARE_POLL so the radio thread polls the radio's IRQ status register instead. + */ +class MCP23017LockingArduinoHal : public LockingArduinoHal +{ + public: + MCP23017LockingArduinoHal(SPIClass &spi, SPISettings spiSettings, ExtensionIOMCP23017 &expander); + + void pinMode(uint32_t pin, uint32_t mode) override; + void digitalWrite(uint32_t pin, uint32_t value) override; + uint32_t digitalRead(uint32_t pin) override; + void attachInterrupt(uint32_t interruptNum, void (*cb)(void), uint32_t mode) override; + void detachInterrupt(uint32_t interruptNum) override; + uint32_t pinToInterrupt(uint32_t pin) override; +#if MCP23017_INT_ESP32_PIN >= 0 + void spiBeginTransaction() override; + void spiEndTransaction() override; +#endif + + private: + ExtensionIOMCP23017 &mcp; + static bool isMcpPin(uint32_t pin); +}; + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index e4ec807f8..e58a9b520 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -208,6 +208,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 #elif defined(M5STACK_CARDPUTER_ADV) #define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV +#elif defined(MESHNOLOGY_W10) +#define HW_VENDOR meshtastic_HardwareModel_MESHNOLOGY_W10 #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/src/platform/extra_variants/meshnology_w10/variant.cpp b/src/platform/extra_variants/meshnology_w10/variant.cpp new file mode 100644 index 000000000..3a6c9c8b1 --- /dev/null +++ b/src/platform/extra_variants/meshnology_w10/variant.cpp @@ -0,0 +1,61 @@ +#include "configuration.h" + +#ifdef MESHNOLOGY_W10 + +#include + +#ifdef HAS_I2S +// NOTE: do not include main.h / AudioThread.h here. AudioBoard.h does `using namespace audio_driver`, +// which pulls audio_driver::GpioPin into global scope and collides with Meshtastic's class GpioPin if +// GpioLogic.h is also visible in this TU. Keeping this file codec-only (like the other ES8311 boards) +// avoids that. +#include "AudioBoard.h" +#include "platform/esp32/ExtensionIOMCP23017.h" // mcpIoExpander (NS4150 amp enable on EXIO_PA_CTRL) + +DriverPins PinsAudioBoardES8311; +AudioBoard audioCodecBoard(AudioDriverES8311, PinsAudioBoardES8311); +#endif + +// Meshnology W10 late init: bring up the ES8311 codec so the NS4150 -> speaker path can play +// notification tones over I2S. Called after power->setup() and the radio init, so the I2C bus and +// the MCP23017 are already up. The amp itself is toggled by AudioThread around playback. +void lateInitVariant() +{ +#ifdef HAS_I2S + // Keep the NS4150 amp muted until AudioThread turns it on for playback (avoids idle hiss). + mcpIoExpander.pinMode(EXIO_PA_CTRL, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_PA_CTRL, LOW); + + // I2C: function, Wire (shared bus, ES8311 at 0x18); I2S: function, mclk, bck, ws, dout, din + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + audioCodecBoard.begin(cfg); + + // ES8311 register setup (matches the vendor demo / other Meshtastic ES8311 boards) + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 I2C address + Wire.write(reg); + Wire.write(val); + uint8_t err = Wire.endTransmission(); + if (err != 0) + LOG_WARN("ES8311 reg 0x%02x write failed (err=%d)", reg, err); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // clock manager, MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0 dB) + es8311_write_reg(0x37, 0x08); // EQ bypass + LOG_INFO("Meshnology W10: ES8311 audio codec initialized"); +#endif // HAS_I2S +} + +#endif // MESHNOLOGY_W10 diff --git a/src/sleep.cpp b/src/sleep.cpp index 86d226d3a..a958a4009 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -404,9 +404,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r { // LOG_DEBUG("Enter light sleep"); - // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering - // LightSleep. -#if defined(SENSECAP_INDICATOR) + // LORA_DIO1 is an extended IO pin (on an I/O expander). Setting it as a wake-up pin will cause problems, + // such as the device not entering light sleep. Boards opt in with LORA_DIO1_EXTENDED_IO in their variant. +#if defined(LORA_DIO1_EXTENDED_IO) return ESP_SLEEP_WAKEUP_TIMER; #endif @@ -586,7 +586,9 @@ bool shouldLoraWake(uint32_t msecToWake) void enableLoraInterrupt() { -#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) +#if defined(LORA_DIO1_EXTENDED_IO) + // DIO1 is a virtual pin on an I/O expander - it cannot be a GPIO wakeup source +#elif SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) esp_err_t res; res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); if (res != ESP_OK) { diff --git a/variants/esp32s3/meshnology-w10/pins_arduino.h b/variants/esp32s3/meshnology-w10/pins_arduino.h new file mode 100644 index 000000000..6bc0b156e --- /dev/null +++ b/variants/esp32s3/meshnology-w10/pins_arduino.h @@ -0,0 +1,62 @@ +// Meshnology W10 - shadows the generic esp32s3 variant pins. +// Deliberately omits PIN_RGB_LED / LED_BUILTIN / RGB_BUILTIN: the generic definitions make the +// core's digitalWrite() reference the RMT-backed RGB LED HAL, which fails to link against this +// build's trimmed FreeRTOS config (no xEventGroupSetBitsFromISR). +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 8; +static const uint8_t SCL = 7; + +// Shared LoRa/LCD SPI bus (E22_NSS is the default SS) +static const uint8_t SS = 14; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 11; +static const uint8_t SCK = 12; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/meshnology-w10/platformio.ini b/variants/esp32s3/meshnology-w10/platformio.ini new file mode 100644 index 000000000..27194a06c --- /dev/null +++ b/variants/esp32s3/meshnology-w10/platformio.ini @@ -0,0 +1,41 @@ +[env:meshnology_w10] +custom_meshtastic_hw_model = 140 +custom_meshtastic_hw_model_slug = MESHNOLOGY_W10 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Meshnology W10 +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + +; ESP32-S3R8: 8 MB OPI PSRAM, W25Q128JVSIQ = 16 MB QIO flash (pg1) +board = esp32-s3-devkitc-1 +board_level = pr +board_build.partitions = default_16MB.csv +board_upload.flash_size = 16MB +board_build.flash_mode = qio +board_build.psram_type = opi +board_build.arduino.memory_type = qio_opi + +extends = esp32s3_base +build_flags = + ${esp32s3_base.build_flags} + -D MESHNOLOGY_W10 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -I variants/esp32s3/meshnology-w10 + +lib_deps = + ${esp32s3_base.lib_deps} + ; ST7789/ST7796 TFT (USE_TFTDISPLAY) + ; renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.21 + ; PCF85063 RTC driver (PCF85063_RTC) + ; renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + ; ES8311 audio codec + I2S notification tones (HAS_I2S) + ; renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.3.0.zip + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/meshnology-w10/variant.h b/variants/esp32s3/meshnology-w10/variant.h new file mode 100644 index 000000000..98677b218 --- /dev/null +++ b/variants/esp32s3/meshnology-w10/variant.h @@ -0,0 +1,171 @@ +#pragma once + +// Meshnology W10 "LoRa AIOT Dev Kit" - ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + +// Quectel L76KB-A58 GPS + SPI TFT. The SX1262 RESET/DIO1/BUSY and the LCD reset are wired to an +// MCP23017 I2C expander (0x20) whose /INT is not routed to the MCU, so DIO1 uses a software poll. + +// ─── I2C bus ────────────────────────────────────────────────────────────────── +// Shared by: AXP2101 PMIC, MCP23017 I/O expander, PCF85063ATL RTC, QMI8658 IMU, +// ES8311 codec, SHT41 temp/humidity sensor (pg1: ESP_SDA=GPIO8, ESP_SCL=GPIO7) +#define I2C_SDA 8 +#define I2C_SCL 7 + +// ─── Power management ───────────────────────────────────────────────────────── +// AXP2101 PMIC on I2C (pg3). AXP_IRQ → EXIO5 (expander, whose /INT is not routed to the +// ESP32), so no PMU_IRQ is possible; battery state is polled via the AXP2101 fuel gauge. +// There is no direct battery ADC - the PMIC is the only voltage source. +#define HAS_AXP2101 + +// ─── RTC ────────────────────────────────────────────────────────────────────── +// PCF85063ATL on I2C (pg3); RTC_INT → EXIO4 (unused) +#define PCF85063_RTC 0x51 + +// ─── I/O expander ───────────────────────────────────────────────────────────── +// U7 is drawn as "TCA9555PWR(UMW)" on schematic V1.1, but production boards use the MCP23017 +// register map (V1.2 placement labels the part MCP23017T-E, and all vendor firmware/demo code +// drives IODIR 0x00/01, GPIO 0x12/13, OLAT 0x14/15). A0/A1/A2 = 0 → I2C address 0x20. +// The expander /INT output is NOT routed to the ESP32. +#define USE_MCP23017 +#define MCP23017_ADDR 0x20 +#define MCP23017_VPIN_BASE 100 // RadioLib virtual pins 100..115 = expander GPA0..GPB7 +#define MCP23017_INT_ESP32_PIN (-1) // /INT not wired to any ESP32 GPIO +#if MCP23017_INT_ESP32_PIN < 0 +// No hardware DIO1 interrupt possible: SX126x IRQ status register is polled from the radio thread +#define LORA_DIO1_SOFTWARE_POLL 1 +#endif + +// LORA_DIO1 is an expander pin, not an ESP32 GPIO, so it can't be used as a sleep/GPIO wakeup source +// (shared capability; see its use in sleep.cpp). +#define LORA_DIO1_EXTENDED_IO + +// Expander pin map (pg2 "I/O Extensions"; EXIO0..7 = GPA0..7 / P00..P07, EXIO8..15 = GPB0..7 / P10..P17) +// Not wired up here: EXIO0 CAM_PWDN, EXIO2 TP_INT, EXIO5 AXP_IRQ, EXIO6 SYS_OUT, +// EXIO11 GPS RESET_N (driver FET Q3 unpopulated), EXIO13 TP_RST +#define EXIO_LCD_RST 1 // GPA1: LCD reset +#define EXIO_LORA_NRST 3 // GPA3: E22 NRST +#define EXIO_RTC_INT 4 // GPA4: PCF85063 INT (unused) +#define EXIO_PA_CTRL 7 // GPA7: NS4150 speaker amp enable (driven by AudioThread during playback) +#define EXIO_IMU_INT1 8 // GPB0: QMI8658 INT1 (input only, no MCU interrupt) +#define EXIO_LORA_DIO1 9 // GPB1: E22 DIO1 +#define EXIO_LORA_BUSY 10 // GPB2: E22 BUSY +#define EXIO_GPS_WAKE 12 // GPB4: L76KB WAKE_UP (driven high at boot) + +// ─── LoRa radio ─────────────────────────────────────────────────────────────── +// EBYTE E22-900MM22S (SX1262) - pg4 U10. SPI shared with the LCD, separate chip selects. +// Bus pins via 0R links: E22_SCK=GPIO12, E22_MOSI=GPIO13, E22_MISO=GPIO11, E22_NSS=GPIO14 (pg4) +#define USE_SX1262 +#define LORA_SCK 12 +#define LORA_MOSI 13 +#define LORA_MISO 11 +#define LORA_CS 14 + +// Control lines route through the MCP23017 (pg4: E22_NRST=EXIO3, E22_DIO1=EXIO9, E22_BUSY=EXIO10), +// handled as RadioLib virtual pins by MCP23017LockingArduinoHal +#define LORA_DIO1 (MCP23017_VPIN_BASE + EXIO_LORA_DIO1) // 109 +#define LORA_BUSY (MCP23017_VPIN_BASE + EXIO_LORA_BUSY) // 110 +#define LORA_RESET (MCP23017_VPIN_BASE + EXIO_LORA_NRST) // 103 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET + +// RF switch is fully hardware-automatic on this board: DIO2 drives TXEN directly and RXEN through +// inverting FET Q4 (pg4). Do not assign TXEN/RXEN GPIOs. +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_RXEN RADIOLIB_NC + +// E22-900MM22S uses a plain 32 MHz crystal (no TCXO) - SX126X_DIO3_TCXO_VOLTAGE deliberately not defined +#define SX126X_MAX_POWER 22 + +// ─── Display ────────────────────────────────────────────────────────────────── +// SPI TFT on the "OLED"-silkscreened header / LCD FPC, sharing the LoRa SPI bus (pg1). +// Vendor ships two panels on identical pins; the default kit has the ST7789 1.54" IPS 240x240. +// Build with -D MESHNOLOGY_W10_LCD_ST7796_35 for the 3.5" ST7796 320x480 panel instead. +#define TFT_CS 10 // pg1: OLED_CS=GPIO10 +#define TFT_DC 16 // pg1: OLED_DC=GPIO16 +#define TFT_BL 6 // pg1: OLED_BL=GPIO6 (backlight PWM) +#define TFT_RST -1 // panel reset is EXIO1 on the expander, toggled in mcp23017EarlyInit() +#define USE_TFTDISPLAY 1 + +#define SPI_FREQUENCY 75000000 +#define SPI_READ_FREQUENCY 16000000 + +#ifdef MESHNOLOGY_W10_LCD_ST7796_35 +// ST7796 3.5" 320x480 (capacitive touch on I2C - not enabled yet) +#define ST7796_SPI_HOST SPI2_HOST +#define ST7796_CS TFT_CS +#define ST7796_RS TFT_DC +#define ST7796_SDA LORA_MOSI +#define ST7796_SCK LORA_SCK +#define ST7796_MISO LORA_MISO +#define ST7796_RESET TFT_RST +#define ST7796_BL TFT_BL +#define ST7796_BUSY -1 +#define TFT_WIDTH 320 +#define TFT_HEIGHT 480 +#define TFT_OFFSET_ROTATION 3 +#else +// ST7789 1.54" IPS 240x240 (default kit panel) +#define ST7789_SPI_HOST SPI2_HOST +#define ST7789_CS TFT_CS +#define ST7789_RS TFT_DC +#define ST7789_SDA LORA_MOSI +#define ST7789_SCK LORA_SCK +#define ST7789_MISO LORA_MISO +#define ST7789_RESET TFT_RST +#define ST7789_BL TFT_BL +#define ST7789_BUSY -1 +#define TFT_WIDTH 240 +#define TFT_HEIGHT 240 +#define TFT_OFFSET_ROTATION 1 +#endif +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 + +// ─── GPS ────────────────────────────────────────────────────────────────────── +// Quectel L76KB-A58 on UART0 pins (pg4: GPS_TXD→U0RXD=GPIO44, GPS_RXD→U0TXD=GPIO43; console is USB) +// 1PPS only drives LED3 (pg3); RESET_N driver FET is unpopulated; WAKE_UP = EXIO12, driven high at boot +#define HAS_GPS 1 +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define GPS_BAUDRATE 9600 + +// ─── User input ─────────────────────────────────────────────────────────────── +// pg1: SW2 pulls GPIO0 to GND (BOOT doubles as user button). SW3 is the AXP2101 power button. +#define BUTTON_PIN 0 +#define BUTTON_NEED_PULLUP + +// ─── LED ────────────────────────────────────────────────────────────────────── +// TX1812 (WS2812-compatible) RGB LED on GPIO48 via 33R (pg1, U35). The other LEDs are +// hardware-driven: AXP2101 CHGLED, VSYS power LED, GPS 1PPS LED, UART0 TX/RX activity LEDs. +// TODO: enabling HAS_NEOPIXEL currently fails to link (Adafruit NeoPixel pulls in the Arduino RMT +// HAL, which needs xEventGroupSetBitsFromISR - not present in this build's FreeRTOS config) +// #define HAS_NEOPIXEL +// #define NEOPIXEL_COUNT 1 +// #define NEOPIXEL_DATA 48 +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) + +// ─── Audio ──────────────────────────────────────────────────────────────────── +// ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker, initialized in the variant's lateInitVariant(). +// Used for notification tones / ringtones over the I2S "buzzer" path (turn on the +// use_i2s_as_buzzer external-notification option). Codec2 voice is SX1280-only, so it does not +// apply to this sub-GHz board. The NS4150 amp enable is on the MCP23017 (EXIO_PA_CTRL / GPA7) and +// is toggled by AudioThread around playback. pg3 I2S wiring below. +#define HAS_I2S +#define DAC_I2S_MCLK 1 // pg3: ES8311 MCLK +#define DAC_I2S_BCK 2 // pg3: ES8311 BCLK/SCLK +#define DAC_I2S_WS 4 // pg3: ES8311 LRCK +#define DAC_I2S_DOUT 5 // pg3: playback data (ESP32 -> ES8311) +#define DAC_I2S_DIN 3 // pg3: record data (ES8311 -> ESP32) +// AudioThread powers the NS4150 amp on/off around playback via this (opt-in) hook. +#define AUDIO_AMP_ENABLE(on) mcpIoExpander.digitalWrite(EXIO_PA_CTRL, (on) ? HIGH : LOW) + +// ─── On-board peripherals not wired up yet ──────────────────────────────────── +// SHT41 temp/humidity (0x44) and QMI8658 IMU (0x6B): auto-detected on the I2C scan +// microSD: CS on GPIO9, shares the LCD/LoRa SPI bus - not enabled +// Camera interface: GPIO38-42/45-48 (pg1) - not enabled + +// ─── Board identity ─────────────────────────────────────────────────────────── +#define MESHNOLOGY_W10 1 diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index dd5866861..b59e6e059 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -68,6 +68,9 @@ #define LORA_DIO1 (3 | IO_EXPANDER) // SX1262 IRQ #define LORA_DIO2 (2 | IO_EXPANDER) // SX1262 BUSY #define LORA_DIO3 +// LORA_DIO1 is an expander pin, not an ESP32 GPIO, so it can't be used as a sleep/GPIO wakeup source +// (shared capability; see its use in sleep.cpp). +#define LORA_DIO1_EXTENDED_IO #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index 3e4a38b36..52a060dd1 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -92,6 +92,8 @@ #define EXPANDS_DRV_EN (0) #define EXPANDS_AMP_EN (1) #define EXPANDS_KB_RST (2) +// AudioThread powers the amp on/off around playback via this (opt-in) hook. +#define AUDIO_AMP_ENABLE(on) io.digitalWrite(EXPANDS_AMP_EN, (on) ? HIGH : LOW) #define EXPANDS_LORA_EN (3) #define EXPANDS_GPS_EN (4) #define EXPANDS_NFC_EN (5)