diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 63f4e9b13b2..f79ea08625c 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -2,6 +2,11 @@ #include "NodeDB.h" #include "configuration.h" +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#include +#define HAS_PWM_RTTTL 1 +#endif + #if !defined(ARCH_ESP32) && !defined(ARCH_RP2040) && !defined(ARCH_PORTDUINO) #include "Tone.h" #endif @@ -150,6 +155,10 @@ void playTones(const ToneDuration *tone_durations, int size) config.device.buzzer_gpio = PIN_BUZZER; #endif if (config.device.buzzer_gpio) { +#ifdef HAS_PWM_RTTTL + if (rtttl::isPlaying()) + return; // a notification ringtone owns the PWM, don't reprogram it mid-note +#endif for (int i = 0; i < size; i++) { const auto &tone_duration = tone_durations[i]; tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index f0e22d485db..99f7b3f2ad7 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -44,6 +44,9 @@ bool ascending = true; #if defined(HAS_I2S_SPEAKER_NRF52) #include "platform/nrf52/NRF52RtttlPlayer.h" #endif +#ifdef ARCH_NRF52 +#include "platform/nrf52/NRF52RtttlTicker.h" +#endif /* Documentation: @@ -60,6 +63,35 @@ bool ascending = true; #define EXT_NOTIFICATION_FAST_THREAD_MS 25 +// The PWM buzzer sequencer is normally polled from this cooperative thread, so a slow display refresh +// delays the next note. nRF52 runs it from a FreeRTOS timer instead (NRF52RtttlTicker). +static void pwmRtttlBegin(uint8_t pin, const char *song) +{ +#ifdef ARCH_NRF52 + NRF52RtttlTicker::begin(pin, song); +#else + rtttl::begin(pin, song); +#endif +} + +static void pwmRtttlPump() +{ +#ifdef ARCH_NRF52 + NRF52RtttlTicker::pump(); +#else + rtttl::play(); +#endif +} + +static void pwmRtttlStop() +{ +#ifdef ARCH_NRF52 + NRF52RtttlTicker::stop(); +#else + rtttl::stop(); +#endif +} + #define ASCII_BELL 0x07 meshtastic_RTTTLConfig rtttlConfig; @@ -78,6 +110,8 @@ int32_t ExternalNotificationModule::runOnce() return INT32_MAX; // we don't need this thread here... } else { uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; + // Racy by design: the sequencer's flag is one byte, stale only for a cycle at song end, which + // just defers stopNow(). Locking it would block this loop on the timer task it hands work to. bool isRtttlPlaying = rtttl::isPlaying(); #ifdef HAS_I2S // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop @@ -172,10 +206,10 @@ int32_t ExternalNotificationModule::runOnce() // now let the PWM buzzer play if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz() && buzzerShouldAlert) { if (rtttl::isPlaying()) { - rtttl::play(); + pwmRtttlPump(); } else if (isNagging && !Throttle::deadlinePassed(nagCycleCutoff)) { // start the song again if we have time left - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + pwmRtttlBegin(config.device.buzzer_gpio, rtttlConfig.ringtone); } // we need fast updates to play the RTTTL delay = EXT_NOTIFICATION_FAST_THREAD_MS; @@ -283,7 +317,7 @@ void ExternalNotificationModule::stopNow() { LOG_INFO("Turning off external notification: "); LOG_INFO("Stop RTTTL playback"); - rtttl::stop(); + pwmRtttlStop(); #ifdef HAS_I2S LOG_INFO("Stop audioThread playback"); audioThread->stop(); @@ -488,7 +522,7 @@ void ExternalNotificationModule::triggerBuzzerOutput() audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); #endif } else if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + pwmRtttlBegin(config.device.buzzer_gpio, rtttlConfig.ringtone); } else { setExternalState(2, true); } diff --git a/src/platform/nrf52/NRF52RtttlTicker.cpp b/src/platform/nrf52/NRF52RtttlTicker.cpp new file mode 100644 index 00000000000..e2efd5ab5d5 --- /dev/null +++ b/src/platform/nrf52/NRF52RtttlTicker.cpp @@ -0,0 +1,103 @@ +#include "NRF52RtttlTicker.h" + +#ifdef ARCH_NRF52 + +#include "DebugConfiguration.h" +#include "freertosinc.h" +#include +#include + +namespace NRF52RtttlTicker +{ +namespace +{ +// 5 ms is inaudible against the tens-of-ms stall being fixed, and holds the daemon to 200 wakeups/s. +// Must exceed one tick, or pdMS_TO_TICKS() rounds to zero and xTimerCreate() fails back into polling. +constexpr uint32_t kTickMs = 5; +static_assert(kTickMs * configTICK_RATE_HZ >= 1000, "kTickMs rounds to zero ticks"); + +TimerHandle_t timer = nullptr; +SemaphoreHandle_t lock = nullptr; +// True only while the timer is servicing a song; pump() polls from the main loop whenever it is not. +bool timerRunning = false; +bool reportedInitFailure = false; + +void onTick(TimerHandle_t) +{ + // The main thread holds the lock only across begin()/stop(); skip this tick rather than block the timer task. + if (xSemaphoreTake(lock, 0) != pdTRUE) + return; + // onTick -> rtttl::play -> tone -> applyConfiguration is the deepest chain on the timer daemon's + // 256 word stack, shared with Bluefruit; measured 61 words peak on a T-Echo Plus. + if (rtttl::isPlaying()) + rtttl::play(); + else + xTimerStop(timer, 0); // song finished on its own + xSemaphoreGive(lock); +} + +bool ensureInit() +{ + if (timer) + return true; + if (!lock) + lock = xSemaphoreCreateMutex(); + if (lock) + timer = xTimerCreate("rtttl", pdMS_TO_TICKS(kTickMs), pdTRUE, nullptr, onTick); + if (!timer && !reportedInitFailure) { + // begin() runs again on every nag restart, so only complain the first time. + reportedInitFailure = true; + LOG_ERROR("RTTTL timer unavailable, falling back to main-loop playback"); + } + return timer != nullptr; +} +} // namespace + +void begin(uint8_t pin, const char *song) +{ + if (!ensureInit()) { + rtttl::begin(pin, song); + return; + } + xSemaphoreTake(lock, portMAX_DELAY); + rtttl::begin(pin, song); + xSemaphoreGive(lock); + // A start rejected by a full timer command queue must fall back to polling, or the song never advances. + timerRunning = xTimerStart(timer, pdMS_TO_TICKS(10)) == pdPASS; + if (!timerRunning) + LOG_WARN("RTTTL timer start rejected, falling back to main-loop playback"); +} + +void pump() +{ + if (timerRunning || !rtttl::isPlaying()) + return; + if (!lock) { // no mutex means ensureInit() never made a timer, so nothing can race us + rtttl::play(); + return; + } + // A rejected start or stop leaves the auto-reload timer live even with timerRunning clear, so take + // the lock rather than assume onTick() is idle. play() is time gated, so a skipped tick costs nothing. + if (xSemaphoreTake(lock, 0) != pdTRUE) + return; + rtttl::play(); + xSemaphoreGive(lock); +} + +void stop() +{ + if (!timer) { + rtttl::stop(); + return; + } + // Unlike the start, a rejected stop is harmless: rtttl::stop() below clears the playing flag, + // so the next onTick() stops the timer itself. + (void)xTimerStop(timer, pdMS_TO_TICKS(10)); + timerRunning = false; + xSemaphoreTake(lock, portMAX_DELAY); + rtttl::stop(); + xSemaphoreGive(lock); +} +} // namespace NRF52RtttlTicker + +#endif diff --git a/src/platform/nrf52/NRF52RtttlTicker.h b/src/platform/nrf52/NRF52RtttlTicker.h new file mode 100644 index 00000000000..d0b7f7902b5 --- /dev/null +++ b/src/platform/nrf52/NRF52RtttlTicker.h @@ -0,0 +1,21 @@ +#pragma once + +#include "configuration.h" + +#ifdef ARCH_NRF52 + +#include + +// Advances the NonBlockingRTTTL sequencer from a FreeRTOS timer, so the next note does not wait +// behind the cooperative main loop. tone() is hardware timed, so only note starts need servicing. +namespace NRF52RtttlTicker +{ +void begin(uint8_t pin, const char *song); + +// Only advances the song if the timer could not be created or started; otherwise a no-op. +void pump(); + +void stop(); +} // namespace NRF52RtttlTicker + +#endif