mirror of
https://github.com/meshtastic/firmware.git
synced 2026-08-01 19:08:59 -04:00
* Add software LoRa IRQ polling and an MCP23017 expander HAL for radios behind an I2C expander
Some boards route the SX126x control lines (NRESET/DIO1/BUSY) through an I2C
GPIO expander instead of native GPIOs, and don't wire the expander's /INT to
the MCU, so RadioLib cannot take a hardware DIO1 interrupt.
Two reusable pieces, both gated behind USE_MCP23017 / LORA_DIO1_SOFTWARE_POLL
so builds that don't define them are unaffected:
- ExtensionIOMCP23017 + MCP23017LockingArduinoHal: a RadioLib HAL that maps
virtual pins (MCP23017_VPIN_BASE .. +15) onto an MCP23017's GPIO, so
RESET/BUSY/DIO1 reads and writes become I2C transactions while the real SPI
GPIOs (SCK/MOSI/MISO/CS) pass straight through. A failed I2C read skips the
read-modify-write rather than clobbering the rest of the bank.
- LORA_DIO1_SOFTWARE_POLL: with no DIO1 interrupt available, the SX126x
interface polls the radio IRQ status register from the radio thread and
synthesizes the ISR_TX/ISR_RX events, filtering noisy preamble/header IRQs so
they can't starve TX. A 1 ms ISR_POLL_TICK drives the poll; TX timers may
overwrite the pending tick (pollMissedIrqs() is the backup that bounds the
latency of the busy-Rx contention window). Behaviour is unchanged on all
boards that keep the hardware interrupt.
* Add Meshnology W10 AIOT Dev Kit variant (SX1262 via MCP23017 expander)
ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + Quectel L76KB GPS +
SPI TFT, with the radio's RESET/DIO1/BUSY and the LCD reset routed through an
MCP23017 I2C expander at 0x20. The expander's /INT is not wired to the ESP32,
so DIO1 uses the software poll. Pins come from the board schematic and the
vendor firmware; the expander is an MCP23017 despite the V1.1 schematic's stale
'TCA9555' label (the V1.2 placement diagram and all vendor code use the MCP23017
register map).
A local pins_arduino.h shadows the generic esp32s3 variant to drop RGB_BUILTIN,
which otherwise pulls the Arduino RMT HAL into the link and fails against this
build's trimmed FreeRTOS config.
Reports HardwareModel 140 (MESHNOLOGY_W10, already present in the protobufs).
Hardware-verified on a real W10 AIOT Dev Kit: AXP2101 PMU, MCP23017, SX1262
init + a full over-the-air TX/RX round trip through the expander, PCF85063 RTC,
SHT41 and QMI8658 auto-detected, and an L76KB GPS fix.
* meshnology-w10: enable ES8311 speaker for notification tones
Bring up the ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker so the board can
play notification tones / ringtones over the I2S buzzer path (the
use_i2s_as_buzzer external-notification option). Codec2 voice stays out of
scope: it is SX1280-only and this is a sub-GHz board.
- variant.h: HAS_I2S + DAC_I2S pins (MCLK=1, BCK=2, WS=4, DOUT=5, DIN=3)
- platformio.ini: arduino-audio-driver + ESP8266Audio + ESP8266SAM
- extra_variants/meshnology_w10/variant.cpp: lateInitVariant() configures the
ES8311, mirroring the other Meshtastic ES8311 boards. Kept codec-only (no
main.h) so the audio driver's 'using namespace audio_driver' does not pull a
conflicting GpioPin into scope alongside Meshtastic's class GpioPin.
- AudioThread.h: toggle the NS4150 amp (MCP23017 EXIO_PA_CTRL) around playback.
Verified on hardware: a test tune played audibly through the speaker.
* meshnology-w10: address review feedback on the MCP23017 driver
- ExtensionIOMCP23017: serialize register access with a mutex, since the radio
HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) now reach the expander from
different threads and the read-modify-write paths are not atomic.
- digitalRead: return a fail-safe HIGH on a failed read instead of LOW, so a
transient I2C error on the LoRa BUSY line can't look like 'ready' and let
RadioLib start an SPI transaction early. (DIO1 is polled via the radio IRQ
register, not this pin.)
- enablePinChangeInterrupt: use the checked readReg() overload and skip on a
failed read, matching the other read-modify-write helpers.
- meshnology_w10 variant.cpp: log a warning if an ES8311 register write NACKs
instead of silently leaving the codec half-configured.
- RadioInterface.cpp: guard the MCP23017 HAL branch with ARCH_ESP32 to match the
include and driver-file guards.
* Replace board-model audio/sleep ifdefs with reusable variant capability macros
Two shared-code spots keyed off specific hardware models; move the board-specific
detail into opt-in macros the variants define, so the core code stays generic and
new boards can opt into the behavior class without touching shared files.
- AudioThread amp control: drop the T_LORA_PAGER / MESHNOLOGY_W10 ifdefs. A board
with an I2S amp now defines AUDIO_AMP_ENABLE(on) in its variant.h (T-LoRa Pager
and Meshnology W10 do), and AudioThread just calls it around playback. The
expander includes are keyed on USE_XL9555 / USE_MCP23017 (capabilities) instead
of the model name.
- Light-sleep / DIO1 wakeup: drop the SENSECAP_INDICATOR check. Boards whose
LORA_DIO1 is an expander pin (not an ESP32 GPIO) define LORA_DIO1_EXTENDED_IO
(SenseCAP Indicator and Meshnology W10), and sleep.cpp keys off that for both
the light-sleep skip and the DIO1 GPIO-wakeup skip. This also fixes a latent
issue on the SenseCAP: it previously reached the GPIO-wakeup path and called
gpio_pulldown_en() on a virtual expander pin; it now skips that cleanly.
Builds verified on meshnology_w10, tlora-pager, and seeed-sensecap-indicator.
* meshnology-w10: silence cppcheck constParameterPointer on the ISR-callback helper
isIsrTxCallback() takes a function pointer it only compares; cppcheck's
constParameterPointer wants it 'pointer to const', which is meaningless for a
function pointer (the codebase already globally suppresses the sibling
constParameterCallback). Inline-suppress it to keep the check green.
* HopScaling: qualify the member assignment as this->count
cppcheck (CI's version) flags 'count = newCount;' at the end of trimIfNeeded()
as uselessAssignmentArg ('assignment of function parameter has no effect') - a
false positive, since count is a member that outlives the call. Write it as
this->count (matching the Step-1 assignment above) so the analyzer sees a member
write. This finding comes in via the develop merge, not this board; fixing it
here to unblock the PR's cppcheck check.
113 lines
2.9 KiB
C++
113 lines
2.9 KiB
C++
#pragma once
|
|
#include "PowerFSM.h"
|
|
#include "concurrency/OSThread.h"
|
|
#include "configuration.h"
|
|
#include "main.h"
|
|
#include "sleep.h"
|
|
#include <memory>
|
|
|
|
#ifdef HAS_I2S
|
|
#include <AudioFileSourcePROGMEM.h>
|
|
#include <AudioGeneratorRTTTL.h>
|
|
#include <AudioOutputI2S.h>
|
|
#include <ESP8266SAM.h>
|
|
|
|
// 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
|
|
{
|
|
public:
|
|
AudioThread() : OSThread("Audio") { initOutput(); }
|
|
|
|
void beginRttl(const void *data, uint32_t len)
|
|
{
|
|
#ifdef AUDIO_AMP_ENABLE
|
|
AUDIO_AMP_ENABLE(true);
|
|
#endif
|
|
setCPUFast(true);
|
|
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
|
|
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
|
|
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
|
|
}
|
|
|
|
// Also handles actually playing the RTTTL, needs to be called in loop
|
|
bool isPlaying()
|
|
{
|
|
if (i2sRtttl != nullptr) {
|
|
return i2sRtttl->isRunning() && i2sRtttl->loop();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (i2sRtttl != nullptr) {
|
|
i2sRtttl->stop();
|
|
i2sRtttl = nullptr;
|
|
}
|
|
|
|
rtttlFile = nullptr;
|
|
|
|
setCPUFast(false);
|
|
#ifdef AUDIO_AMP_ENABLE
|
|
AUDIO_AMP_ENABLE(false);
|
|
#endif
|
|
}
|
|
|
|
void readAloud(const char *text)
|
|
{
|
|
if (i2sRtttl != nullptr) {
|
|
i2sRtttl->stop();
|
|
i2sRtttl = nullptr;
|
|
}
|
|
|
|
#ifdef AUDIO_AMP_ENABLE
|
|
AUDIO_AMP_ENABLE(true);
|
|
#endif
|
|
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
|
|
sam->Say(audioOut.get(), text);
|
|
setCPUFast(false);
|
|
#ifdef AUDIO_AMP_ENABLE
|
|
AUDIO_AMP_ENABLE(false);
|
|
#endif
|
|
}
|
|
|
|
protected:
|
|
int32_t runOnce() override
|
|
{
|
|
canSleep = true; // Assume we should not keep the board awake
|
|
|
|
// if (i2sRtttl != nullptr && i2sRtttl->isRunning()) {
|
|
// i2sRtttl->loop();
|
|
// }
|
|
return AUDIO_THREAD_INTERVAL_MS;
|
|
}
|
|
|
|
private:
|
|
void initOutput()
|
|
{
|
|
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
|
|
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
|
|
audioOut->SetGain(0.2);
|
|
};
|
|
|
|
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
|
|
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
|
|
|
|
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
|
|
};
|
|
|
|
#endif
|