// SPDX-License-Identifier: LGPL-3.0-or-later // Hardware timer pulse guiding with deferred ISR-safe stop // // Pattern: esp_timer callback -> semaphore -> high-priority task -> mutex -> axis.stop() // The mutex protects the multi-field state (activeAxis_, activeTracker_, active_) // from concurrent pulse()/cancel() calls across ESP32 cores. #include "ST4Pulse.h" ST4Pulse* ST4Pulse::instance_ = nullptr; static const int PULSE_TASK_PRIORITY = configMAX_PRIORITIES - 2; static const int PULSE_TASK_STACK = 2048; ST4Pulse::ST4Pulse() : timer_(nullptr), pulseDoneSem_(nullptr), mutex_(nullptr), pulseTaskHandle_(nullptr), activeAxis_(nullptr), activeTracker_(nullptr), active_(false), shutdown_(false) {} ST4Pulse::~ST4Pulse() { // 1. Stop and delete timer (prevents new semaphore gives) if (timer_) { esp_timer_stop(timer_); esp_timer_delete(timer_); timer_ = nullptr; } // 2. Signal task to exit and unblock it shutdown_ = true; if (pulseDoneSem_) xSemaphoreGive(pulseDoneSem_); // 3. Wait for task to see shutdown flag if (pulseTaskHandle_) { vTaskDelay(pdMS_TO_TICKS(10)); vTaskDelete(pulseTaskHandle_); pulseTaskHandle_ = nullptr; } // 4. Delete synchronization primitives last if (pulseDoneSem_) vSemaphoreDelete(pulseDoneSem_); if (mutex_) vSemaphoreDelete(mutex_); instance_ = nullptr; } void ST4Pulse::timerCallback(void* arg) { ST4Pulse* self = static_cast(arg); // Give semaphore to wake the pulse-stop task // Safe for ESP_TIMER_TASK dispatch (the default) xSemaphoreGive(self->pulseDoneSem_); } void ST4Pulse::pulseTaskFunc(void* arg) { ST4Pulse* self = static_cast(arg); for (;;) { if (xSemaphoreTake(self->pulseDoneSem_, portMAX_DELAY) == pdTRUE) { if (self->shutdown_) break; xSemaphoreTake(self->mutex_, portMAX_DELAY); if (self->active_) { if (self->activeAxis_) self->activeAxis_->stop(); if (self->activeTracker_) self->activeTracker_->stop(); self->active_ = false; } xSemaphoreGive(self->mutex_); } } // Task deletes itself on shutdown vTaskDelete(nullptr); } void ST4Pulse::begin() { configASSERT(instance_ == nullptr); instance_ = this; mutex_ = xSemaphoreCreateMutex(); configASSERT(mutex_); pulseDoneSem_ = xSemaphoreCreateBinary(); configASSERT(pulseDoneSem_); esp_timer_create_args_t timerArgs = {}; timerArgs.callback = timerCallback; timerArgs.arg = this; timerArgs.name = "st4_pulse"; esp_err_t err = esp_timer_create(&timerArgs, &timer_); configASSERT(err == ESP_OK); BaseType_t created = xTaskCreatePinnedToCore( pulseTaskFunc, "st4_pulse", PULSE_TASK_STACK, this, PULSE_TASK_PRIORITY, &pulseTaskHandle_, 1 ); configASSERT(created == pdPASS); } void ST4Pulse::cancelLocked() { // Caller must hold mutex_ esp_timer_stop(timer_); // Drain any pending semaphore from a just-fired timer xSemaphoreTake(pulseDoneSem_, 0); if (activeAxis_) activeAxis_->stop(); if (activeTracker_) activeTracker_->stop(); active_ = false; activeAxis_ = nullptr; activeTracker_ = nullptr; } bool ST4Pulse::pulse(ST4Axis& axis, ST4Tracker& tracker, ST4Direction dir, double slewRate, uint32_t ms) { xSemaphoreTake(mutex_, portMAX_DELAY); if (active_) cancelLocked(); if (dir == ST4Direction::STOP || ms == 0) { xSemaphoreGive(mutex_); return false; } if (ms > MAX_PULSE_MS) ms = MAX_PULSE_MS; activeAxis_ = &axis; activeTracker_ = &tracker; active_ = true; axis.move(dir); tracker.start(slewRate); // Start one-shot timer (microseconds) esp_err_t timerErr = esp_timer_start_once(timer_, static_cast(ms) * 1000); if (timerErr != ESP_OK) { // Timer failed -- rollback hardware activation axis.stop(); tracker.stop(); active_ = false; activeAxis_ = nullptr; activeTracker_ = nullptr; xSemaphoreGive(mutex_); return false; } xSemaphoreGive(mutex_); return true; } bool ST4Pulse::isActive() const { return active_; } void ST4Pulse::cancel() { xSemaphoreTake(mutex_, portMAX_DELAY); cancelLocked(); xSemaphoreGive(mutex_); }