#include "Receiver.h" #include "Log.h" #include #if !OPTICAL_USE_MCPWM_CAPTURE #include #include #endif uint32_t PulseReceiver::tickHz() const { #if OPTICAL_USE_MCPWM_CAPTURE return captureResolutionHz_; #else return cpuTickHz_; #endif } uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const { if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) return 0; #if OPTICAL_USE_MCPWM_CAPTURE return captureResolutionHz_ ? captureResolutionHz_ : MCPWM_CAPTURE_RESOLUTION_HZ; #else return cpuTickHz_; #endif } bool PulseReceiver::begin() { queue_ = xQueueCreate(512, sizeof(Edge)); if (!queue_) return false; #if OPTICAL_USE_MCPWM_CAPTURE // PWM generation uses MCPWM group 0. Group 1 is dedicated to input capture, // so RX cannot exhaust or conflict with the generator's resources. mcpwm_capture_timer_config_t timerConfig = {}; timerConfig.group_id = 1; timerConfig.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT; timerConfig.resolution_hz = MCPWM_CAPTURE_RESOLUTION_HZ; if (mcpwm_new_capture_timer(&timerConfig, &captureTimer_) != ESP_OK) return false; if (mcpwm_capture_timer_get_resolution(captureTimer_, &captureResolutionHz_) != ESP_OK || !captureResolutionHz_) return false; mcpwm_capture_channel_config_t channelConfig = {}; channelConfig.gpio_num = GPIO_RX; // ACK pulses are sub-microsecond and consecutive acknowledgements can be // only 1 us apart. A low-priority capture interrupt can leave the channel // status pending long enough for the next timestamp to overwrite it. channelConfig.intr_priority = 3; channelConfig.prescale = 1; channelConfig.flags.pos_edge = true; channelConfig.flags.neg_edge = false; if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &risingChannel_) != ESP_OK) return false; mcpwm_capture_event_callbacks_t callbacks = {}; callbacks.on_cap = onCapture; if (mcpwm_capture_channel_register_event_callbacks( risingChannel_, &callbacks, this) != ESP_OK) return false; channelConfig.flags.pos_edge = false; channelConfig.flags.neg_edge = true; if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK) return false; if (mcpwm_capture_channel_register_event_callbacks( fallingChannel_, &callbacks, this) != ESP_OK) return false; // The TX channel is created immediately before a driver test. Only the end // of the active PWM pulse is armed; handling its start here would occupy the // shared MCPWM ISR during the RX acknowledgement only ~300 ns later. return true; #else pinMode(GPIO_RX, INPUT); cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; attachInterruptArg(GPIO_RX, onGpio, this, CHANGE); return cpuTickHz_ != 0; #endif } bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn) { (void)activeLightOn; if (!plannedTickHz(expectedHz, expectedDutyPct)) return false; expectedHz_ = expectedHz; expectedDutyPct_ = expectedDutyPct; #if !OPTICAL_USE_MCPWM_CAPTURE cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; if (!cpuTickHz_) return false; #endif resetStream(); Log::event("CAPTURE", "RX optical polarity will be detected automatically"); return startCapture(false); } #if OPTICAL_USE_MCPWM_CAPTURE bool PulseReceiver::configureDriverTxCapture(bool risingEdge) { if (running_) return false; if (txChannel_) { if (mcpwm_del_capture_channel(txChannel_) != ESP_OK) return false; txChannel_ = nullptr; } mcpwm_capture_channel_config_t config = {}; config.gpio_num = GPIO_PWM; config.intr_priority = 3; config.prescale = 1; config.flags.pos_edge = risingEdge; config.flags.neg_edge = !risingEdge; config.flags.io_loop_back = true; if (mcpwm_new_capture_channel(captureTimer_, &config, &txChannel_) != ESP_OK) return false; mcpwm_capture_event_callbacks_t callbacks = {}; callbacks.on_cap = onCapture; return mcpwm_capture_channel_register_event_callbacks( txChannel_, &callbacks, this) == ESP_OK; } #endif bool PulseReceiver::startDriver(uint32_t frequencyHz, uint32_t pulseNs, bool activeTxLightOn) { #if OPTICAL_USE_MCPWM_CAPTURE if (!frequencyHz || !pulseNs || !tickHz() || running_) return false; resetStream(); const uint64_t pulseTicks = (static_cast(pulseNs) * tickHz() + 500000000ULL) / 1000000000ULL; const uint32_t periodTicks = tickHz() / frequencyHz; if (!pulseTicks || pulseTicks >= periodTicks || pulseTicks > UINT32_MAX) return false; driverPulseTicks_ = static_cast(pulseTicks); driverReleaseSlackTicks_ = static_cast( (static_cast(DRIVER_RESPONSE_TIMEOUT_NS) * tickHz() + 999999999ULL) / 1000000000ULL); const uint8_t activeRawLevel = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL; const bool pulseEndIsRising = activeRawLevel == LOW; if (!driverReleaseSlackTicks_ || !configureDriverTxCapture(pulseEndIsRising)) return false; return startCapture(true); #else return false; #endif } bool PulseReceiver::startCapture(bool withTx) { #if OPTICAL_USE_MCPWM_CAPTURE // Progress updates keep one capture session alive. Pulse-width stages stop // capture only after PWM is quiet, so resetStream never races the ISR. if (running_) return true; if (mcpwm_capture_timer_enable(captureTimer_) != ESP_OK) return false; if (mcpwm_capture_channel_enable(risingChannel_) != ESP_OK) { mcpwm_capture_timer_disable(captureTimer_); return false; } if (mcpwm_capture_channel_enable(fallingChannel_) != ESP_OK) { mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_timer_disable(captureTimer_); return false; } if (withTx && mcpwm_capture_channel_enable(txChannel_) != ESP_OK) { mcpwm_capture_channel_disable(fallingChannel_); mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_timer_disable(captureTimer_); return false; } txCaptureEnabled_ = withTx; running_ = true; if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) { running_ = false; if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_); txCaptureEnabled_ = false; mcpwm_capture_channel_disable(fallingChannel_); mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_timer_disable(captureTimer_); return false; } #else if (withTx) return false; running_ = true; #endif return true; } void PulseReceiver::stop() { const bool wasRunning = running_; running_ = false; #if OPTICAL_USE_MCPWM_CAPTURE if (wasRunning) { // Mask capture interrupts before stopping the shared capture timer. if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_); txCaptureEnabled_ = false; mcpwm_capture_channel_disable(fallingChannel_); mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_timer_stop(captureTimer_); mcpwm_capture_timer_disable(captureTimer_); } #else (void)wasRunning; #endif } void PulseReceiver::resetStream() { if (queue_) xQueueReset(queue_); haveReorderEdge_ = false; __atomic_store_n(&driverRingWrite_, 0U, __ATOMIC_RELEASE); __atomic_store_n(&driverRingRead_, 0U, __ATOMIC_RELEASE); haveLastDriverEdge_ = false; lastDriverEdge_ = {}; driverPulseTicks_ = 0; driverReleaseSlackTicks_ = 0; droppedItems_ = 0; polarityKnown_ = false; activeStartRising_ = false; polarityEdgeCount_ = 0; memset(polarityEdges_, 0, sizeof(polarityEdges_)); waitingForActiveEnd_ = true; activeStart_ = activeEnd_ = 0; haveRawTick_ = false; lastRawTick_ = 0; tickEpoch_ = 0; } PulseReceiver::TimedEdge PulseReceiver::extendEdge(const Edge &e) { if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL) tickEpoch_ += 0x100000000ULL; haveRawTick_ = true; lastRawTick_ = e.tick; return {tickEpoch_ + e.tick, e.rising != 0}; } bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) { const TimedEdge edge = extendEdge(rawEdge); if (polarityKnown_) { // Deliberately ignore edge type after synchronization. A PWM waveform is // just alternating intervals: active, inactive, active, inactive. An // extra or missing edge therefore becomes a concrete wrong pulse/period // instead of an ambiguous GLITCH state. if (waitingForActiveEnd_) { activeEnd_ = edge.tick; waitingForActiveEnd_ = false; return false; } const uint64_t periodTicks = edge.tick - activeStart_; const uint64_t activeTicks = activeEnd_ - activeStart_; out = {activeStart_, static_cast(periodTicks), static_cast(activeTicks), tickHz()}; activeStart_ = edge.tick; waitingForActiveEnd_ = true; return true; } // Optical mode does not use the DRIVER level setting. Compare the first two // alternating intervals with the configured active duration and select the // level that is actually present at RX. Three edges are enough to determine // polarity and, when the first interval is active, produce the first period. polarityEdges_[polarityEdgeCount_++] = edge; if (polarityEdgeCount_ < 3U) return false; const uint64_t firstInterval = polarityEdges_[1].tick - polarityEdges_[0].tick; const uint64_t secondInterval = polarityEdges_[2].tick - polarityEdges_[1].tick; const uint64_t expectedPeriod = tickHz() / expectedHz_; const uint64_t expectedActive = static_cast( expectedPeriod * expectedDutyPct_ / 100.0f + 0.5f); const uint64_t firstError = firstInterval > expectedActive ? firstInterval - expectedActive : expectedActive - firstInterval; const uint64_t secondError = secondInterval > expectedActive ? secondInterval - expectedActive : expectedActive - secondInterval; const bool firstIntervalIsActive = firstError <= secondError; activeStartRising_ = firstIntervalIsActive ? polarityEdges_[0].rising : polarityEdges_[1].rising; polarityKnown_ = true; polarityEdgeCount_ = 0; if (firstIntervalIsActive) { out = {polarityEdges_[0].tick, static_cast(polarityEdges_[2].tick - polarityEdges_[0].tick), static_cast(firstInterval), tickHz()}; activeStart_ = polarityEdges_[2].tick; waitingForActiveEnd_ = true; return true; } activeStart_ = polarityEdges_[1].tick; activeEnd_ = polarityEdges_[2].tick; waitingForActiveEnd_ = false; return false; } uint32_t PulseReceiver::takeDroppedItems() { return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED); } #if OPTICAL_USE_MCPWM_CAPTURE bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel, const mcpwm_capture_event_data_t *data, void *ctx) { PulseReceiver *self = static_cast(ctx); if (!self->running_) return false; const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS; const Edge edge = {data->cap_value, static_cast(rawRising), static_cast(channel == self->txChannel_ ? CaptureSource::TX : CaptureSource::RX)}; if (self->txCaptureEnabled_) { // All channels in one MCPWM group are dispatched serially by the same // group ISR. Keep this callback shorter than the minimum interval between // equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic // RMW operations here can leave the channel pending until its capture // register is overwritten by the next edge. if (self->haveLastDriverEdge_ && self->lastDriverEdge_.tick == edge.tick && self->lastDriverEdge_.rising == edge.rising && self->lastDriverEdge_.source == edge.source) { // The same channel callback can be delivered twice while several MCPWM // capture status bits are pending. Two physical edges cannot have the // same source, direction and 12.5 ns hardware timestamp. return false; } const uint16_t write = self->driverRingWrite_; const uint16_t next = static_cast( (write + 1U) & (DRIVER_RING_CAPACITY - 1U)); if (next == self->driverRingRead_) { ++self->droppedItems_; } else { self->driverRing_[write] = edge; self->lastDriverEdge_ = edge; self->haveLastDriverEdge_ = true; asm volatile("memw" ::: "memory"); self->driverRingWrite_ = next; } return false; } BaseType_t wake = pdFALSE; if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE) __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); return wake == pdTRUE; } #else void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { PulseReceiver *self = static_cast(ctx); if (!self->running_) return; const bool level = gpio_get_level(static_cast(GPIO_RX)); const Edge edge = {esp_cpu_get_cycle_count(), static_cast(level), static_cast(CaptureSource::RX)}; BaseType_t wake = pdFALSE; if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE) __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); if (wake) portYIELD_FROM_ISR(); } #endif bool PulseReceiver::nextOrderedEdge(Edge &edge, TickType_t waitTicks) { if (!haveReorderEdge_) { if (xQueueReceive(queue_, &reorderEdge_, waitTicks) != pdTRUE) return false; haveReorderEdge_ = true; } Edge next = {}; // Keep one-event look-ahead. If both channel interrupts were pending while // OLED/I2C ran, the MCPWM driver may dispatch them by channel number rather // than timestamp. The signed modular comparison restores their real order. if (xQueueReceive(queue_, &next, waitTicks) != pdTRUE) return false; if (static_cast(next.tick - reorderEdge_.tick) < 0) { edge = next; } else { edge = reorderEdge_; reorderEdge_ = next; } return true; } size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) { size_t count = 0; Edge edge = {}; while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) { if (consumeEdge(edge, periods[count])) { periods[count].activeTickHz = tickHz(); ++count; } } return count; } size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks) { if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ || !driverReleaseSlackTicks_) return 0; constexpr size_t MAX_BATCH = 64; const size_t limit = capacity < MAX_BATCH ? capacity : MAX_BATCH; Edge ordered[MAX_BATCH] = {}; uint16_t read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED); if (read == __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE) && waitTicks) { vTaskDelay(waitTicks); read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED); } // Work on one immutable producer snapshot. RX belonging to a pulse start is // deliberately retained until that pulse's captured end arrives: only then // can the missing start interrupt be reconstructed and sorted before RX. const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE); uint16_t scan = read; bool haveLatestTxEnd = false; bool haveReleaseTxEnd = false; uint32_t latestTxEnd = 0; uint32_t releaseTxEnd = 0; size_t projectedCount = 0; while (scan != write) { const Edge &edge = driverRing_[scan]; const size_t needed = edge.source == static_cast(CaptureSource::TX) ? 2U : 1U; // Reserve one output slot for WINDOW_END. if (projectedCount + needed + 1U > limit) break; projectedCount += needed; if (edge.source == static_cast(CaptureSource::TX)) { if (haveLatestTxEnd) { releaseTxEnd = latestTxEnd; haveReleaseTxEnd = true; } latestTxEnd = edge.tick; haveLatestTxEnd = true; } scan = static_cast((scan + 1U) % DRIVER_RING_CAPACITY); } // Keep the newest TX period in the ring. Arrival of the following TX end // proves that the previous end's ACK/fault window has completely elapsed. if (!haveReleaseTxEnd) { if (waitTicks) vTaskDelay(waitTicks); return 0; } const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_; size_t count = 0; while (read != write) { const Edge edge = driverRing_[read]; if (edge.source == static_cast(CaptureSource::TX) && static_cast(edge.tick - releaseTxEnd) > 0) break; if (edge.source == static_cast(CaptureSource::RX) && static_cast(edge.tick - releaseThrough) > 0) break; const size_t needed = edge.source == static_cast(CaptureSource::TX) ? 2U : 1U; if (count + needed > limit) break; read = static_cast((read + 1U) % DRIVER_RING_CAPACITY); if (edge.source == static_cast(CaptureSource::TX)) { Edge pulseStart = edge; pulseStart.tick -= driverPulseTicks_; pulseStart.rising = !edge.rising; ordered[count++] = pulseStart; } ordered[count++] = edge; } __atomic_store_n(&driverRingRead_, read, __ATOMIC_RELEASE); // MCPWM channels share one timer but their callbacks can be dispatched in // channel order when several interrupts are pending. Restore the hardware // order inside the captured batch using the common timestamp. for (size_t i = 1; i < count; ++i) { const Edge key = ordered[i]; size_t j = i; while (j && static_cast(ordered[j - 1].tick - key.tick) > 0) { ordered[j] = ordered[j - 1]; --j; } ordered[j] = key; } for (size_t i = 0; i < count; ++i) { const TimedEdge timed = extendEdge(ordered[i]); events[i] = {timed.tick, timed.rising, static_cast(ordered[i].source)}; } const Edge marker = {releaseThrough, 0, static_cast(CaptureSource::WINDOW_END)}; const TimedEdge timedMarker = extendEdge(marker); events[count++] = {timedMarker.tick, false, CaptureSource::WINDOW_END}; return count; }