#include "DriverTest.h" #include "Config.h" #include "Log.h" #include #include #include #include #include #include #include static inline uint32_t IRAM_ATTR maskAllInterrupts() { uint32_t state; asm volatile("rsil %0, 15" : "=a"(state) :: "memory"); return state; } static inline void IRAM_ATTR restoreInterrupts(uint32_t state) { asm volatile("wsr %0, ps\nrsync" :: "a"(state) : "memory"); } void DriverEdgeStats::reset() { memset(this, 0, sizeof(*this)); minDelayTicks = minResponseTicks = UINT32_MAX; } void DriverStats::reset() { memset(this, 0, sizeof(*this)); minDelayTicks = minResponseTicks = UINT32_MAX; turnOn.reset(); turnOff.reset(); reason = FailReason::NONE; } uint64_t DriverTest::nsToTicks(uint32_t ns) const { return (static_cast(ns) * captureHz_ + 999999999ULL) / 1000000000ULL; } uint64_t DriverTest::ticksToNs(uint64_t ticks) const { return (ticks * 1000000000ULL + captureHz_ / 2U) / captureHz_; } bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs, float tolerancePct, uint32_t testTimeMs, uint8_t settleCycles, bool activeTxLightOn, bool activeRxLightOn) { (void)tolerancePct; (void)settleCycles; if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs || !testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false; requestCaptureStop(); if (!waitCaptureStopped(25U)) return false; if (!pollTask_ && xTaskCreatePinnedToCore(pollTaskEntry, "driver-poll", 3072, this, configMAX_PRIORITIES - 1U, &pollTask_, 0) != pdPASS) return false; if (!analyzerTask_ && xTaskCreatePinnedToCore(analyzerTaskEntry, "driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS) return false; // The ACK edges can be less than 1 us apart. MCPWM capture delivers all // channels through one group ISR and can overwrite an earlier channel // timestamp before that ISR reaches it. During DRIVER test dedicate core 0 // to direct GPIO sampling; PWM itself remains fully hardware-generated. captureHz_ = getCpuFrequencyMhz() * 1000000UL; if (!captureHz_ || captureHz_ % frequencyHz) return false; captureFrequencyHz_ = frequencyHz; capturePulseNs_ = pulseNs; captureTxLightOn_ = activeTxLightOn; txPulseLightOn_ = activeTxLightOn; pollPeriodCycles_ = captureHz_ / frequencyHz; pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us const uint64_t periodNs = 1000000000ULL / frequencyHz; uint64_t windowNs = pulseNs + 50000ULL; const uint64_t maximumWindowNs = periodNs * 3ULL / 4ULL; if (windowNs > maximumWindowNs) windowNs = maximumWindowNs; pollWindowAfterCycles_ = static_cast( windowNs * captureHz_ / 1000000000ULL); const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL; pollTxStartRawHigh_ = activeTxRaw == HIGH; rxActiveRawHigh_ = ((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn); ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS); faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS); stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS); testTicks_ = static_cast(captureHz_) * testTimeMs / 1000ULL; const uint32_t requestedSubsamples = testTimeMs < 1000U ? DRIVER_SHORT_SAMPLE_PROGRESS_STEPS : (testTimeMs + DRIVER_PROGRESS_INTERVAL_MS - 1U) / DRIVER_PROGRESS_INTERVAL_MS; subsampleCount_ = static_cast( requestedSubsamples > UINT8_MAX ? UINT8_MAX : requestedSubsamples); subsampleTicks_ = testTicks_ / subsampleCount_; if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || !ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ || !testTicks_ || !subsampleTicks_) return false; clearCapture(); stats_.reset(); publishStats(); pendingCount_ = 0; response_ = {}; measurementStartTick_ = deadlineTick_ = 0; pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0; // Skip two complete periods after the polling task synchronizes with TX. settleCycles_ = DRIVER_CAPTURE_SYNC_CYCLES; Log::printf("DRIVER", "capture=GPIO-%luMHz sync-periods=%u ACK-timeout=%luns", static_cast(captureHz_ / 1000000UL), static_cast(settleCycles_), static_cast(DRIVER_ACK_START_MAX_NS)); const uint64_t periodUs = (1000000ULL + frequencyHz - 1ULL) / frequencyHz; settlingTimeoutUs_ = periodUs * (static_cast(DRIVER_CAPTURE_SYNC_CYCLES) + 2ULL) + 1000ULL; settlingDeadlineUs_ = 0; settledCycles_ = 0; completedSubsamples_ = 0; measurementClosed_ = false; havePointOrigin_ = false; haveLastActiveTx_ = false; rxActive_ = (gpio_get_level(static_cast(GPIO_RX)) != 0) == rxActiveRawHigh_; currentStep_ = 0; traceWrite_ = traceCount_ = 0; __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE); state_ = DriverState::SETTLING; return armCapture(); } bool DriverTest::armCapture() { // Never wait for USB/Serial here: a disconnected or slow host must not // delay a subsample or consume the test's global timeout. if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) { TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0); const bool watched = idle0 && esp_task_wdt_status(idle0) == ESP_OK; const bool disabled = watched && disableCore0WDT(); __atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE); // If IDLE0 is not watched there is nothing to remove or restore. } __atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE); __atomic_store_n(&captureActive_, true, __ATOMIC_RELEASE); xTaskNotifyGive(pollTask_); const uint32_t readyDeadline = millis() + 25U; while (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) && static_cast(millis() - readyDeadline) < 0) delay(0); if (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) { requestCaptureStop(); waitCaptureStopped(25U); state_ = DriverState::IDLE; return false; } settlingDeadlineUs_ = static_cast(esp_timer_get_time()) + settlingTimeoutUs_; xTaskNotifyGive(analyzerTask_); return true; } bool DriverTest::resumeSubsample() { if (state_ != DriverState::SUBSAMPLE_DONE) return false; if (!waitCaptureStopped(25U)) { fail(FailReason::DATA_LOSS, lastEventTick_); return false; } clearCapture(); pendingCount_ = 0; response_ = {}; measurementStartTick_ = deadlineTick_ = 0; lastActiveTxTick_ = 0; settlingDeadlineUs_ = 0; settledCycles_ = 0; measurementClosed_ = false; haveLastActiveTx_ = false; rxActive_ = (gpio_get_level(static_cast(GPIO_RX)) != 0) == rxActiveRawHigh_; state_ = DriverState::SETTLING; if (armCapture()) return true; fail(FailReason::DATA_LOSS, lastEventTick_); return false; } void DriverTest::pollTaskEntry(void *context) { static_cast(context)->pollTaskLoop(); } void IRAM_ATTR DriverTest::pollTaskLoop() { constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX); for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); uint32_t levels = GPIO.in & PIN_MASK; uint32_t nextStart = 0; uint32_t windowEnd = 0; uint32_t lastTxStart = 0; RawEvent hotEvents[32] = {}; uint8_t hotCount = 0; bool sawTxStart = false; bool critical = false; bool allInterruptsMasked = false; uint32_t interruptState = 0; auto sampleOnce = [&]() { const uint32_t current = GPIO.in & PIN_MASK; if (current == levels) return; const uint32_t now = esp_cpu_get_cycle_count(); const uint32_t changed = current ^ levels; if ((changed & (1UL << GPIO_PWM)) && hotCount < 32U) hotEvents[hotCount++] = {now, (current & (1UL << GPIO_PWM)) != 0U, Source::TX}; if ((changed & (1UL << GPIO_RX)) && hotCount < 32U) hotEvents[hotCount++] = {now, (current & (1UL << GPIO_RX)) != 0U, Source::RX}; if ((changed & (1UL << GPIO_PWM)) && ((current & (1UL << GPIO_PWM)) != 0U) == pollTxStartRawHigh_) { lastTxStart = now; sawTxStart = true; } levels = current; }; auto flushHot = [&]() { for (uint8_t i = 0; i < hotCount; ++i) recordRaw(hotEvents[i].tick, hotEvents[i].rising, hotEvents[i].source); hotCount = 0; }; portENTER_CRITICAL(&pollMux_); critical = true; __atomic_store_n(&captureReady_, true, __ATOMIC_RELEASE); while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && !sawTxStart) for (uint8_t i = 0; i < 16U; ++i) sampleOnce(); if (sawTxStart) windowEnd = lastTxStart + pollWindowAfterCycles_; const bool synchronized = sawTxStart; if (synchronized) { interruptState = maskAllInterrupts(); allInterruptsMasked = true; } while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && synchronized) { while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && static_cast(esp_cpu_get_cycle_count() - windowEnd) < 0) for (uint8_t i = 0; i < 16U; ++i) sampleOnce(); restoreInterrupts(interruptState); allInterruptsMasked = false; portEXIT_CRITICAL(&pollMux_); critical = false; flushHot(); // This marker is written only after every TX/RX edge from the completed // sampling window. The analyzer may now safely decide that an ACK was // absent without racing the producer that writes those edges. recordRaw(esp_cpu_get_cycle_count(), false, Source::WINDOW_END); if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break; nextStart = lastTxStart + pollPeriodCycles_; sawTxStart = false; uint32_t outsideSpins = 0; while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && static_cast(esp_cpu_get_cycle_count() - (nextStart - pollWindowBeforeCycles_)) < 0) { for (uint8_t i = 0; i < 16U; ++i) sampleOnce(); if (++outsideSpins >= 256U) { outsideSpins = 0; taskYIELD(); } } if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break; portENTER_CRITICAL(&pollMux_); critical = true; interruptState = maskAllInterrupts(); allInterruptsMasked = true; windowEnd = nextStart + pollWindowAfterCycles_; } if (allInterruptsMasked) restoreInterrupts(interruptState); if (critical) portEXIT_CRITICAL(&pollMux_); flushHot(); if (__atomic_exchange_n(&core0WdtDisabled_, false, __ATOMIC_ACQ_REL)) enableCore0WDT(); __atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE); } } void DriverTest::analyzerTaskEntry(void *context) { static_cast(context)->analyzerTaskLoop(); } void DriverTest::analyzerTaskLoop() { TimedEvent events[64] = {}; for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); while (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING) { const size_t count = readRaw(events, 64, pdMS_TO_TICKS(1)); for (size_t i = 0; i < count && (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING); ++i) processEvent(events[i]); const uint32_t dropped = takeDropped(); if (dropped) { stats_.droppedItems += dropped; fail(FailReason::DATA_LOSS, lastEventTick_); } if (!count && state_ == DriverState::SETTLING && static_cast(esp_timer_get_time()) >= settlingDeadlineUs_) { fail(FailReason::ACK_MISSING, lastEventTick_); } } } } void DriverTest::processEvent(const TimedEvent &event) { lastEventTick_ = event.tick; if (event.source == Source::WINDOW_END) { if (state_ == DriverState::RUNNING) { expirePending(event.tick); if (state_ == DriverState::RUNNING) completeIfPossible(event.tick); } return; } if (!havePointOrigin_) { pointOriginTick_ = event.tick; havePointOrigin_ = true; } rememberTrace(event); if (state_ == DriverState::SETTLING) processSettling(event); else if (state_ == DriverState::RUNNING) processRunning(event); } void DriverTest::processSettling(const TimedEvent &event) { if (event.source == Source::RX) { rxActive_ = event.rising == rxActiveRawHigh_; return; } const uint8_t rawLevel = event.rising ? HIGH : LOW; const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL; if (lightOn != txPulseLightOn_) return; if (settledCycles_ < settleCycles_) { ++settledCycles_; return; } if (rxActive_) { fail(FailReason::DRIVER_FAULT, event.tick); return; } state_ = DriverState::RUNNING; measurementStartTick_ = event.tick; const uint64_t measuredBefore = static_cast(completedSubsamples_) * subsampleTicks_; const uint64_t thisSubsampleTicks = completedSubsamples_ + 1U == subsampleCount_ ? testTicks_ - measuredBefore : subsampleTicks_; deadlineTick_ = event.tick + thisSubsampleTicks; processTx(event, lightOn); } void DriverTest::processRunning(const TimedEvent &event) { if (response_.active && event.tick - response_.startTick >= stuckTicks_) { const uint64_t delay = response_.associated ? response_.startTick - response_.tx.tick : 0; const uint64_t trigger = haveLastActiveTx_ && response_.startTick >= lastActiveTxTick_ ? response_.startTick - lastActiveTxTick_ : delay; fail(FailReason::DRIVER_FAULT, event.tick, delay, event.tick - response_.startTick, trigger); return; } if (event.source == Source::TX) { expirePending(event.tick); if (state_ != DriverState::RUNNING) return; const uint8_t rawLevel = event.rising ? HIGH : LOW; const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL; if (event.tick < deadlineTick_) processTx(event, lightOn); else measurementClosed_ = true; } else { // A delayed fault indication can start after the normal ACK deadline. // Measure the RX pulse before expiring its possible causal TX edge. processRx(event, event.rising == rxActiveRawHigh_); if (state_ != DriverState::RUNNING) return; if (!response_.active) expirePending(event.tick); } if (state_ != DriverState::RUNNING) return; completeIfPossible(event.tick); } bool DriverTest::addPending(uint64_t tick, bool lightOn) { if (pendingCount_ >= MAX_PENDING) { fail(FailReason::DATA_LOSS, tick); return false; } pending_[pendingCount_++] = {tick, lightOn}; return true; } void DriverTest::processTx(const TimedEvent &event, bool lightOn) { if (!addPending(event.tick, lightOn)) return; if (lightOn == txPulseLightOn_) { lastActiveTxTick_ = event.tick; haveLastActiveTx_ = true; } ++stats_.inputEdges; } int8_t DriverTest::matchingPending(uint64_t rxTick) const { for (uint8_t i = 0; i < pendingCount_; ++i) if (rxTick >= pending_[i].tick && rxTick - pending_[i].tick <= ackStartMaxTicks_) return static_cast(i); return -1; } void DriverTest::removePending(uint8_t index) { if (index >= pendingCount_) return; for (uint8_t i = index + 1U; i < pendingCount_; ++i) pending_[i - 1U] = pending_[i]; --pendingCount_; } void DriverTest::processRx(const TimedEvent &event, bool activeNow) { rxActive_ = activeNow; if (activeNow) { if (response_.active) { fail(FailReason::DATA_LOSS, event.tick); return; } response_ = {}; response_.active = true; response_.startTick = event.tick; const int8_t index = matchingPending(event.tick); if (index >= 0) { response_.associated = true; response_.tx = pending_[index]; removePending(static_cast(index)); } else ++stats_.unexpectedResponses; return; } if (!response_.active) return; const uint64_t width = event.tick - response_.startTick; if (!response_.associated) { const uint64_t trigger = haveLastActiveTx_ && response_.startTick >= lastActiveTxTick_ ? response_.startTick - lastActiveTxTick_ : 0; response_ = {}; fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger); return; } const uint64_t guard = mergeGuardTicks(); for (uint8_t i = 0; i < pendingCount_; ++i) { if (pending_[i].tick > response_.startTick && event.tick - pending_[i].tick >= guard) { const uint64_t delay = response_.startTick - response_.tx.tick; const uint64_t trigger = event.tick - pending_[i].tick; response_ = {}; fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger); return; } } if (width >= faultLongTicks_) { const uint64_t delay = response_.startTick - response_.tx.tick; const uint64_t trigger = haveLastActiveTx_ && response_.startTick >= lastActiveTxTick_ ? response_.startTick - lastActiveTxTick_ : delay; response_ = {}; fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger); return; } const uint64_t delay = response_.startTick - response_.tx.tick; const bool lightOn = response_.tx.lightOn; response_ = {}; acceptAcknowledgement(delay, width, lightOn); } uint64_t DriverTest::mergeGuardTicks() const { uint32_t observedMax = stats_.maxDelayTicks; const uint64_t baseline = observedMax ? observedMax : nsToTicks(DRIVER_ACK_DELAY_NS + 500U); return baseline + nsToTicks(DRIVER_ACK_MERGE_MARGIN_NS); } void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn) { const uint32_t delay32 = delay > UINT32_MAX ? UINT32_MAX : static_cast(delay); const uint32_t width32 = width > UINT32_MAX ? UINT32_MAX : static_cast(width); stats_.lastDelayTicks = delay32; stats_.lastResponseTicks = width32; ++stats_.responses; if (delay32 < stats_.minDelayTicks) stats_.minDelayTicks = delay32; if (delay32 > stats_.maxDelayTicks) stats_.maxDelayTicks = delay32; if (width32 < stats_.minResponseTicks) stats_.minResponseTicks = width32; if (width32 > stats_.maxResponseTicks) stats_.maxResponseTicks = width32; DriverEdgeStats &edge = lightOn ? stats_.turnOn : stats_.turnOff; ++edge.responses; edge.delaySumTicks += delay32; edge.responseSumTicks += width32; if (delay32 < edge.minDelayTicks) edge.minDelayTicks = delay32; if (delay32 > edge.maxDelayTicks) edge.maxDelayTicks = delay32; if (width32 < edge.minResponseTicks) edge.minResponseTicks = width32; if (width32 > edge.maxResponseTicks) edge.maxResponseTicks = width32; publishStats(); } void DriverTest::expirePending(uint64_t now) { for (uint8_t i = 0; i < pendingCount_; ++i) { if (now < pending_[i].tick + ackStartMaxTicks_) continue; // The failure belongs to the ACK deadline itself. A later TX edge or the // end-of-window marker is only the safe moment when absence is confirmed. fail(FailReason::ACK_MISSING, pending_[i].tick + ackStartMaxTicks_, ackStartMaxTicks_); return; } } void DriverTest::completeIfPossible(uint64_t now) { if (state_ != DriverState::RUNNING) return; if (!measurementClosed_ && now >= deadlineTick_) measurementClosed_ = true; if (!measurementClosed_ || response_.active || pendingCount_) return; if (!stats_.turnOn.responses || !stats_.turnOff.responses) { fail(FailReason::ACK_MISSING, now); return; } ++completedSubsamples_; currentStep_ = completedSubsamples_; publishStats(); requestCaptureStop(); if (!waitCaptureStopped(25U)) { fail(FailReason::DATA_LOSS, lastEventTick_); return; } if (completedSubsamples_ >= subsampleCount_) { __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE); state_ = DriverState::PASS; } else { __atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE); state_ = DriverState::SUBSAMPLE_DONE; } } void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay, uint64_t pulseWidth, uint64_t triggerAfterTx) { if (state_ == DriverState::FAIL || state_ == DriverState::PASS) return; if (stats_.reason == FailReason::NONE) { stats_.reason = reason; if (tick && havePointOrigin_ && tick >= pointOriginTick_) stats_.errorElapsedTicks = tick - pointOriginTick_; const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay; if (trigger) { stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX : static_cast(trigger); stats_.errorTriggerValid = true; } if (delay) { stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX : static_cast(delay); stats_.errorDelayValid = true; } if (pulseWidth) { stats_.errorPulseTicks = pulseWidth > UINT32_MAX ? UINT32_MAX : static_cast(pulseWidth); stats_.errorPulseValid = true; } } publishStats(); __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE); requestCaptureStop(); state_ = DriverState::FAIL; } void DriverTest::publishStats() { portENTER_CRITICAL(&statsMux_); publishedStats_ = stats_; portEXIT_CRITICAL(&statsMux_); } void DriverTest::forceFail(FailReason reason) { if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING || state_ == DriverState::SUBSAMPLE_DONE) fail(reason, lastEventTick_); } void DriverTest::abort() { if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING || state_ == DriverState::SUBSAMPLE_DONE) fail(FailReason::ABORTED, lastEventTick_); else { requestCaptureStop(); state_ = DriverState::IDLE; } } bool DriverTest::takeProgressUpdate() { return __atomic_exchange_n(&progressUpdatePending_, false, __ATOMIC_ACQ_REL); } void DriverTest::requestCaptureStop() { __atomic_store_n(&captureActive_, false, __ATOMIC_RELEASE); } bool DriverTest::waitCaptureStopped(uint32_t timeoutMs) { const uint32_t deadline = millis() + timeoutMs; while (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) && static_cast(millis() - deadline) < 0) delay(0); if (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) return false; if (__atomic_exchange_n(&core0WdtDisabled_, false, __ATOMIC_ACQ_REL)) enableCore0WDT(); return true; } void DriverTest::clearCapture() { const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE); __atomic_store_n(&ringRead_, write, __ATOMIC_RELEASE); __atomic_store_n(&droppedItems_, 0U, __ATOMIC_RELEASE); haveRawTick_ = false; lastRawTick_ = 0; tickEpoch_ = 0; } void IRAM_ATTR DriverTest::recordRaw(uint32_t tick, bool rising, Source source) { const uint16_t write = ringWrite_; const uint16_t next = static_cast( (write + 1U) & (RING_CAPACITY - 1U)); if (next == ringRead_) { ++droppedItems_; return; } ring_[write] = {tick, rising, source}; asm volatile("memw" ::: "memory"); ringWrite_ = next; } size_t DriverTest::readRaw(TimedEvent *events, size_t capacity, TickType_t waitTicks) { if (!events || !capacity) return 0; uint16_t read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED); if (read == __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE) && waitTicks) { vTaskDelay(waitTicks); read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED); } const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE); size_t count = 0; while (read != write && count < capacity) { const RawEvent raw = ring_[read]; read = static_cast((read + 1U) & (RING_CAPACITY - 1U)); if (haveRawTick_ && raw.tick < lastRawTick_ && lastRawTick_ - raw.tick > 0x80000000UL) tickEpoch_ += 1ULL << 32U; lastRawTick_ = raw.tick; haveRawTick_ = true; events[count++] = {tickEpoch_ + raw.tick, raw.rising, raw.source}; } __atomic_store_n(&ringRead_, read, __ATOMIC_RELEASE); return count; } uint32_t DriverTest::takeDropped() { return __atomic_exchange_n(&droppedItems_, 0U, __ATOMIC_ACQ_REL); } void DriverTest::rememberTrace(const TimedEvent &event) { trace_[traceWrite_] = {event.tick, static_cast(event.source), static_cast(event.rising), static_cast(state_), pendingCount_}; traceWrite_ = static_cast((traceWrite_ + 1U) % TRACE_CAPACITY); if (traceCount_ < TRACE_CAPACITY) ++traceCount_; } void DriverTest::printSummary() const { auto printEdge = [&](const char *name, const DriverEdgeStats &edge) { if (!edge.responses) { Log::printf("DRIVER", "%s ACK=0", name); return; } Log::printf("DRIVER", "%s ACK=%lu D=%lluns/%lluns/%lluns P=%lluns/%lluns/%lluns", name, static_cast(edge.responses), static_cast(ticksToNs(edge.minDelayTicks)), static_cast(ticksToNs( edge.delaySumTicks / edge.responses)), static_cast(ticksToNs(edge.maxDelayTicks)), static_cast(ticksToNs(edge.minResponseTicks)), static_cast(ticksToNs( edge.responseSumTicks / edge.responses)), static_cast(ticksToNs(edge.maxResponseTicks))); }; Log::printf("DRIVER", "TX edges=%lu responses=%lu dropped=%lu unexpected=%lu result=%s", static_cast(publishedStats_.inputEdges), static_cast(publishedStats_.responses), static_cast(publishedStats_.droppedItems), static_cast(publishedStats_.unexpectedResponses), failName(publishedStats_.reason)); if (publishedStats_.reason != FailReason::NONE) { auto formatOptional = [&](bool valid, uint32_t ticks, char *out, size_t size) { if (!valid) snprintf(out, size, "---"); else snprintf(out, size, "%lluns", static_cast(ticksToNs(ticks))); }; char trigger[24], pulse[24]; formatOptional(publishedStats_.errorTriggerValid, publishedStats_.errorTriggerTicks, trigger, sizeof(trigger)); formatOptional(publishedStats_.errorPulseValid, publishedStats_.errorPulseTicks, pulse, sizeof(pulse)); if (publishedStats_.errorPulseValid) Log::printf("DRIVER", "error timing: T=%s P=%s", trigger, pulse); else Log::printf("DRIVER", "error timing: T=%s", trigger); } printEdge("ON", publishedStats_.turnOn); printEdge("OFF", publishedStats_.turnOff); } void DriverTest::printTrace() const { if (!traceCount_) return; const uint8_t first = static_cast( (traceWrite_ + TRACE_CAPACITY - traceCount_) % TRACE_CAPACITY); const uint64_t origin = trace_[first].tick; Log::printf("DRIVER", "RAM trace: %u events, tick=%luHz", traceCount_, static_cast(captureHz_)); for (uint8_t i = 0; i < traceCount_; ++i) { const TraceEvent &event = trace_[(first + i) % TRACE_CAPACITY]; Log::printf("DRIVER", "E%02u +%lluns %s/%s state=%u pending=%u", i, static_cast(ticksToNs(event.tick - origin)), event.source == static_cast(Source::TX) ? "TX" : "RX", event.rising ? "rise" : "fall", event.state, event.pending); } }