diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index a7a9be1..62b0854 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -93,7 +93,13 @@ void App::update() { if (state_ == AppState::SOLO_MEASURE) { const MeasureState ms = measurement_.update(); if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), requestedHz_); finish(false, measurement_.reason()); } - else if (ms == MeasureState::PASS) { printStageStats(measurement_.stats(), requestedHz_); stagePassed(); } + else if (ms == MeasureState::PASS) { + printStageStats(measurement_.stats(), requestedHz_); + if (measurement_.reason() == FailReason::DATA_LOST) { + sweepHadDataLoss_ = true; if (!firstDataLossHz_) firstDataLossHz_ = requestedHz_; + } + stagePassed(); + } } else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY || state_ == AppState::MASTER_WAIT_RESULT) { handleRadio(); updateMaster(); @@ -150,7 +156,7 @@ void App::showMenu() { void App::startTest() { params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); - stageIndex_ = 0; pendingReason_ = FailReason::NONE; + stageIndex_ = 0; pendingReason_ = FailReason::NONE; sweepHadDataLoss_ = false; firstDataLossHz_ = 0; if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; } Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast(settings_.role)), stageCount_); if (SERIAL_MINIMAL_LOG) @@ -208,7 +214,11 @@ bool App::startLocalMeasurement(float hz, float duty) { void App::stagePassed() { Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_); pwm_.stop(); - if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; } + if (++stageIndex_ >= stageCount_) { + if (sweepHadDataLoss_) { requestedHz_ = firstDataLossHz_; finish(false, FailReason::DATA_LOST); } + else finish(true, FailReason::NONE); + return; + } if (static_cast(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; } else if (static_cast(settings_.role) == Role::MASTER) { requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); @@ -320,7 +330,8 @@ void App::updateSlave() { const MeasureState ms = measurement_.update(); if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return; printStageStats(measurement_.stats(), requestedHz_); - pendingPacket_ = makePacket(MessageType::RESULT); pendingPacket_.passed = ms == MeasureState::PASS; + pendingPacket_ = makePacket(MessageType::RESULT); + pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE; pendingPacket_.reason = static_cast(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods; pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod; pendingPacket_.sequence = ++sequence_; radio_.sendTo(peer_, pendingPacket_); @@ -377,7 +388,9 @@ void App::printStageStats(const StageStats &s, uint32_t hz) { if (!s.periods) return; const float measuredHz = static_cast(receiver_.tickHz()) * s.periods / s.periodSum; const float measuredDuty = 100.0f * s.activeSum / s.periodSum; - Log::printf("RESULT", "%luHz %s periods=%lu measured=%.2fHz duty=%.2f%%%s%s", - hz, s.reason == FailReason::NONE ? "PASS" : "FAIL", s.periods, measuredHz, measuredDuty, + const char *status = s.reason == FailReason::NONE ? "PASS" : + (s.reason == FailReason::DATA_LOST ? "DATA_LOST" : "FAIL"); + Log::printf("RESULT", "%luHz %s periods=%lu measured=%.2fHz duty=%.2f%% lost=%lu%s%s", + hz, status, s.periods, measuredHz, measuredDuty, s.lostItems, s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason)); } diff --git a/OpticalChannelTester/App.h b/OpticalChannelTester/App.h index 52c3035..922365c 100644 --- a/OpticalChannelTester/App.h +++ b/OpticalChannelTester/App.h @@ -64,5 +64,7 @@ class App { uint8_t retries_ = 0; ProtocolPacket pendingPacket_ = {}; bool initialized_ = false, bootResetCandidate_ = false; + bool sweepHadDataLoss_ = false; + uint32_t firstDataLossHz_ = 0; uint32_t bootCheckStartedMs_ = 0; }; diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 8a465e0..c402650 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -49,6 +49,8 @@ constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8; constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48; constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512; constexpr uint32_t RMT_TARGET_CHUNK_US = 5000; +constexpr uint8_t RMT_QUEUE_BLOCKS = 8; +constexpr uint16_t PERIOD_BATCH_SIZE = 128; constexpr uint32_t C3_STRICT_MAX_HZ = 1000000; constexpr uint32_t S3_STRICT_MAX_HZ = 1000000; diff --git a/OpticalChannelTester/Core.cpp b/OpticalChannelTester/Core.cpp index 893039a..5bb5d4e 100644 --- a/OpticalChannelTester/Core.cpp +++ b/OpticalChannelTester/Core.cpp @@ -11,7 +11,7 @@ const char *roleName(Role r) { const char *failName(FailReason r) { static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT", "EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST", - "UNSUPPORTED", "RESOLUTION", "ABORTED"}; + "UNSUPPORTED", "RESOLUTION", "ABORTED", "DATA LOST"}; const uint8_t i = static_cast(r); return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN"; } @@ -81,8 +81,10 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION; const float timerPeriodError = 100.0f / periodTicks; const float timerDutyError = 100.0f / periodTicks; - const float pwmDutyStep = 100.0f / static_cast((1UL << pwmBits) - 1UL); - return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct || pwmDutyStep > accuracyPct) + // Measurement uses the duty actually programmed into LEDC. A coarse PWM + // step is not itself an error when the requested value (e.g. 50%) is exactly + // representable; only the selected value's actual quantization matters. + return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct) ? FailReason::RESOLUTION : FailReason::NONE; } @@ -107,3 +109,56 @@ FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedH } return reason; } + +bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerance, + uint32_t tickHz, PeriodLimits &limits) { + if (!expectedHz || !tickHz || tolerance < 0.0f || tolerance >= 100.0f || + expectedDuty <= 0.0f || expectedDuty >= 100.0f) return false; + const uint32_t toleranceX100 = static_cast(lroundf(tolerance * 100.0f)); + const uint32_t dutyX100 = static_cast(lroundf(expectedDuty * 100.0f)); + const uint64_t numerator = static_cast(tickHz) * 10000ULL; + const uint64_t highDenominator = static_cast(expectedHz) * (10000U + toleranceX100); + const uint64_t lowDenominator = static_cast(expectedHz) * (10000U - toleranceX100); + limits.minPeriodTicks = static_cast((numerator + highDenominator - 1U) / highDenominator); + limits.maxPeriodTicks = static_cast(numerator / lowDenominator); + limits.minDutyX100 = dutyX100 > toleranceX100 ? dutyX100 - toleranceX100 : 0; + limits.maxDutyX100 = dutyX100 + toleranceX100; + return limits.minPeriodTicks && limits.maxPeriodTicks >= limits.minPeriodTicks; +} + +FailReason evaluatePeriodFast(const PulsePeriod &p, uint32_t tickHz, + const PeriodLimits &limits, uint8_t repeat, + StageStats &s) { + if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE; + ++s.periods; + s.periodSum += p.periodTicks; s.activeSum += p.activeTicks; + if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks; + if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks; + if (p.activeTicks < s.minActive) s.minActive = p.activeTicks; + if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks; + + FailReason reason = FailReason::NONE; + if (p.periodTicks < limits.minPeriodTicks || p.periodTicks > limits.maxPeriodTicks) { + reason = FailReason::PERIOD_OUT; + } else { + // The configured range (>= 1 kHz at 80 MHz capture) fits these products + // into 32 bits. Keep a 64-bit fallback for unusually slow external input. + if (p.periodTicks <= UINT32_MAX / 10000U && limits.maxDutyX100 <= 10000U) { + const uint32_t scaledActive = p.activeTicks * 10000U; + const uint32_t minActive = p.periodTicks * limits.minDutyX100; + const uint32_t maxActive = p.periodTicks * limits.maxDutyX100; + if (scaledActive < minActive || scaledActive > maxActive) reason = FailReason::DUTY_OUT; + } else { + const uint64_t scaledActive = static_cast(p.activeTicks) * 10000ULL; + const uint64_t minActive = static_cast(p.periodTicks) * limits.minDutyX100; + const uint64_t maxActive = static_cast(p.periodTicks) * limits.maxDutyX100; + if (scaledActive < minActive || scaledActive > maxActive) reason = FailReason::DUTY_OUT; + } + } + if (reason != FailReason::NONE && s.reason == FailReason::NONE) { + s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat; + s.badFrequency = static_cast(tickHz) / p.periodTicks; + s.badDuty = 100.0f * p.activeTicks / p.periodTicks; + } + return reason; +} diff --git a/OpticalChannelTester/Core.h b/OpticalChannelTester/Core.h index 64041cc..4646638 100644 --- a/OpticalChannelTester/Core.h +++ b/OpticalChannelTester/Core.h @@ -6,7 +6,7 @@ enum class Role : uint8_t { SOLO, MASTER, SLAVE }; enum class FailReason : uint8_t { NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE, - TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED + TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED, DATA_LOST }; const char *roleName(Role role); @@ -53,10 +53,18 @@ struct StageStats { uint8_t firstBadRepeat; float badFrequency; float badDuty; + uint32_t lostItems; FailReason reason; void reset(); }; +struct PeriodLimits { + uint32_t minPeriodTicks; + uint32_t maxPeriodTicks; + uint32_t minDutyX100; + uint32_t maxDutyX100; +}; + uint32_t settingsChecksum(const Settings &s); uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz); uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index); @@ -70,3 +78,8 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz, float expectedDuty, float tolerancePct, uint8_t repeat, StageStats &stats); +bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerancePct, + uint32_t tickHz, PeriodLimits &limits); +FailReason evaluatePeriodFast(const PulsePeriod &period, uint32_t tickHz, + const PeriodLimits &limits, uint8_t repeat, + StageStats &stats); diff --git a/OpticalChannelTester/Measurement.cpp b/OpticalChannelTester/Measurement.cpp index f220aaa..2682c35 100644 --- a/OpticalChannelTester/Measurement.cpp +++ b/OpticalChannelTester/Measurement.cpp @@ -4,12 +4,17 @@ bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs, uint8_t repeats, uint8_t settleCycles) { - if (!hz || !timeMs || !repeats || repeats > 10 || !receiver_.start(static_cast(hz))) return false; - expectedHz_ = hz; expectedDuty_ = duty; tolerance_ = tolerance; + expectedHz_ = static_cast(hz + 0.5f); + if (!expectedHz_ || !timeMs || !repeats || repeats > 10 || + !makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) || + !receiver_.start(expectedHz_)) return false; timeMs_ = timeMs; repeats_ = repeats; settleLeft_ = settleCycles; stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_)); measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0; + currentRepeat_ = 0; + expectedPeriodMs_ = static_cast((1000ULL + expectedHz_ - 1U) / expectedHz_); + if (!expectedPeriodMs_) expectedPeriodMs_ = 1; state_ = MeasureState::SETTLING; return true; } @@ -18,53 +23,65 @@ void Measurement::fail(FailReason reason) { receiver_.stop(); state_ = MeasureState::FAIL; } +void Measurement::completeWindow() { + receiver_.stop(); + stats_.lostItems += receiver_.takeDroppedItems(); + if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; } + for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) { + fail(FailReason::TOO_FEW_PERIODS); return; + } + if (stats_.lostItems && stats_.reason == FailReason::NONE) stats_.reason = FailReason::DATA_LOST; + state_ = MeasureState::PASS; +} + MeasureState Measurement::update() { if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_; if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; } - PulsePeriod period; - while (receiver_.poll(period)) { - if (state_ == MeasureState::SETTLING) { - if (settleLeft_) --settleLeft_; - if (!settleLeft_) { - measurementStartTick_ = period.startTick + period.periodTicks; - repeatTicks_ = static_cast(receiver_.tickHz()) * timeMs_ / 1000ULL; - deadlineTick_ = measurementStartTick_ + repeatTicks_ * repeats_; - stats_.reset(); measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING; + bool receivedPeriod = false; + for (;;) { + const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE); + stats_.lostItems += receiver_.takeDroppedItems(); + if (!periodCount) break; + receivedPeriod = true; + for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) { + const PulsePeriod &period = periodBatch_[periodIndex]; + if (state_ == MeasureState::SETTLING) { + if (settleLeft_) --settleLeft_; + if (!settleLeft_) { + measurementStartTick_ = period.startTick + period.periodTicks; + repeatTicks_ = static_cast(receiver_.tickHz()) * timeMs_ / 1000ULL; + deadlineTick_ = measurementStartTick_ + repeatTicks_ * repeats_; + nextRepeatTick_ = measurementStartTick_ + repeatTicks_; + stats_.reset(); measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING; + } + continue; } - continue; + const uint64_t endTick = period.startTick + period.periodTicks; + if (period.startTick < measurementStartTick_) continue; // leading incomplete period + if (endTick > deadlineTick_) { completeWindow(); return state_; } // trailing incomplete period + while (currentRepeat_ + 1U < repeats_ && period.startTick >= nextRepeatTick_) { + ++currentRepeat_; nextRepeatTick_ += repeatTicks_; + } + ++repeatPeriods_[currentRepeat_]; + const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_); + if (r != FailReason::NONE) { fail(r); return state_; } } - const uint64_t endTick = period.startTick + period.periodTicks; - if (period.startTick < measurementStartTick_) continue; // leading incomplete period - if (endTick > deadlineTick_) break; // trailing incomplete period - uint8_t repeat = static_cast((period.startTick - measurementStartTick_) / repeatTicks_); - if (repeat >= repeats_) repeat = repeats_ - 1; - ++repeatPeriods_[repeat]; - lastPeriodMs_ = millis(); - const FailReason r = evaluatePeriod(period, receiver_.tickHz(), expectedHz_, expectedDuty_, - tolerance_, repeat + 1, stats_); - if (r != FailReason::NONE) { fail(r); return state_; } } - uint64_t expectedPeriodMs = static_cast(ceilf(1000.0f / expectedHz_)); - if (!expectedPeriodMs) expectedPeriodMs = 1; + if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis(); const uint64_t edgeBasedTimeout = - static_cast(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs + 20; + static_cast(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20; const uint64_t rmtBatchTimeout = - static_cast(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs + 20; + static_cast(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20; const uint64_t settleTimeout = edgeBasedTimeout > rmtBatchTimeout ? edgeBasedTimeout : rmtBatchTimeout; if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL); if (state_ == MeasureState::RUNNING && measurementStartTick_) { const uint32_t now = millis(); const uint32_t totalMs = timeMs_ * repeats_; - const uint32_t edgeTimeoutMs = static_cast(expectedPeriodMs * NO_SIGNAL_TIMEOUT_PERIODS + 2); + const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2; if (now - measurementStartMs_ < totalMs && now - lastPeriodMs_ > edgeTimeoutMs) { fail(FailReason::LOST_EDGE); return state_; } - if (now - measurementStartMs_ > totalMs + expectedPeriodMs + 2) { - for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) { - fail(FailReason::TOO_FEW_PERIODS); return state_; - } - receiver_.stop(); state_ = MeasureState::PASS; - } + if (now - measurementStartMs_ > totalMs + expectedPeriodMs_ + 2) completeWindow(); } return state_; } diff --git a/OpticalChannelTester/Measurement.h b/OpticalChannelTester/Measurement.h index 68a18f7..944d688 100644 --- a/OpticalChannelTester/Measurement.h +++ b/OpticalChannelTester/Measurement.h @@ -15,13 +15,17 @@ class Measurement { const StageStats &stats() const { return stats_; } private: void fail(FailReason reason); + void completeWindow(); PulseReceiver &receiver_; MeasureState state_ = MeasureState::IDLE; StageStats stats_ = {}; - float expectedHz_ = 0, expectedDuty_ = 0, tolerance_ = 0; + PeriodLimits limits_ = {}; + uint32_t expectedHz_ = 0; uint32_t timeMs_ = 0; - uint8_t repeats_ = 0, settleLeft_ = 0; - uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, repeatTicks_ = 0; + uint8_t repeats_ = 0, settleLeft_ = 0, currentRepeat_ = 0; + uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, repeatTicks_ = 0, nextRepeatTick_ = 0; uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0; + uint32_t expectedPeriodMs_ = 1; uint32_t repeatPeriods_[10] = {}; + PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {}; }; diff --git a/OpticalChannelTester/Pwm.cpp b/OpticalChannelTester/Pwm.cpp index c776f09..b9fa72c 100644 --- a/OpticalChannelTester/Pwm.cpp +++ b/OpticalChannelTester/Pwm.cpp @@ -14,12 +14,12 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) { const uint8_t bits = choosePwmResolution(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS); if (!bits) return false; if (!ledcAttachChannel(GPIO_PWM, hz, bits, LEDC_CHANNEL)) return false; - const uint32_t top = (1UL << bits) - 1UL; - const uint32_t duty = (static_cast(top) * dutyPct + 50U) / 100U; + const uint32_t levels = 1UL << bits; + const uint32_t duty = (static_cast(levels) * dutyPct + 50U) / 100U; if (!ledcWriteChannel(LEDC_CHANNEL, duty)) { ledcDetach(GPIO_PWM); return false; } const uint32_t actualHz = ledcReadFreq(GPIO_PWM); if (!actualHz) { ledcDetach(GPIO_PWM); return false; } - a = {hz, actualHz, 100.0f * duty / top, bits}; + a = {hz, actualHz, 100.0f * duty / levels, bits}; running_ = true; return true; } diff --git a/OpticalChannelTester/Receiver.cpp b/OpticalChannelTester/Receiver.cpp index afe4a83..282864e 100644 --- a/OpticalChannelTester/Receiver.cpp +++ b/OpticalChannelTester/Receiver.cpp @@ -16,7 +16,7 @@ uint32_t PulseReceiver::tickHz() const { bool PulseReceiver::begin() { #if OPTICAL_USE_RMT_DMA - queue_ = xQueueCreate(16, sizeof(SymbolBlock)); + queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock)); rmt_rx_channel_config_t cfg = {}; cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ; cfg.gpio_num = static_cast(GPIO_RX); @@ -77,7 +77,7 @@ void PulseReceiver::stop() { void PulseReceiver::resetStream() { if (queue_) xQueueReset(queue_); - overflow_ = false; haveRise_ = haveFall_ = haveRawTick_ = false; + overflow_ = false; droppedItems_ = 0; haveRise_ = haveFall_ = haveRawTick_ = false; lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0; #if OPTICAL_USE_RMT_DMA block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0; @@ -109,17 +109,22 @@ bool PulseReceiver::overflowed() { const bool value = overflow_; overflow_ = false; return value; } +uint32_t PulseReceiver::takeDroppedItems() { + return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED); +} + #if OPTICAL_USE_RMT_DMA bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) { PulseReceiver *self = static_cast(ctx); BaseType_t wake = pdFALSE; size_t offset = 0; while (offset < data->num_symbols) { - SymbolBlock b = {}; + SymbolBlock &b = self->isrBlock_; b.count = static_cast((data->num_symbols - offset) > BLOCK_SYMBOLS ? BLOCK_SYMBOLS : (data->num_symbols - offset)); memcpy(b.symbols, data->received_symbols + offset, b.count * sizeof(rmt_symbol_word_t)); - if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE) self->overflow_ = true; + if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE) + __atomic_fetch_add(&self->droppedItems_, b.count, __ATOMIC_RELAXED); offset += b.count; } return wake == pdTRUE; @@ -146,10 +151,12 @@ bool PulseReceiver::nextRmtEdge(Edge &edge) { } } -bool PulseReceiver::poll(PulsePeriod &period) { +size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) { + size_t count = 0; Edge e; - while (nextRmtEdge(e)) if (consumeEdge(e, period)) return true; - return false; + while (count < capacity && nextRmtEdge(e)) + if (consumeEdge(e, periods[count])) ++count; + return count; } #else void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { @@ -158,13 +165,16 @@ void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { if (RX_SIGNAL_INVERTED) level = !level; Edge e = {esp_cpu_get_cycle_count(), static_cast(level)}; BaseType_t wake = pdFALSE; - if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE) self->overflow_ = true; + if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE) + __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); if (wake) portYIELD_FROM_ISR(); } -bool PulseReceiver::poll(PulsePeriod &period) { +size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) { + size_t count = 0; Edge e; - while (xQueueReceive(queue_, &e, 0) == pdTRUE) if (consumeEdge(e, period)) return true; - return false; + while (count < capacity && xQueueReceive(queue_, &e, 0) == pdTRUE) + if (consumeEdge(e, periods[count])) ++count; + return count; } #endif diff --git a/OpticalChannelTester/Receiver.h b/OpticalChannelTester/Receiver.h index 90f3d83..4629cc4 100644 --- a/OpticalChannelTester/Receiver.h +++ b/OpticalChannelTester/Receiver.h @@ -18,8 +18,9 @@ class PulseReceiver { bool start(uint32_t expectedHz); void stop(); void resetStream(); - bool poll(PulsePeriod &period); + size_t readPeriods(PulsePeriod *periods, size_t capacity); bool overflowed(); + uint32_t takeDroppedItems(); uint32_t tickHz() const; uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; } bool highRateBackend() const { @@ -34,13 +35,14 @@ class PulseReceiver { bool consumeEdge(const Edge &edge, PulsePeriod &period); #if OPTICAL_USE_RMT_DMA - static constexpr size_t BLOCK_SYMBOLS = 64; + static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS; struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; }; static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *); bool nextRmtEdge(Edge &edge); rmt_channel_handle_t channel_ = nullptr; rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS]; uint16_t receiveChunkSymbols_ = 0; + SymbolBlock isrBlock_ = {}; SymbolBlock block_ = {}; uint16_t blockIndex_ = 0; uint8_t phase_ = 0; @@ -53,6 +55,7 @@ class PulseReceiver { #endif QueueHandle_t queue_ = nullptr; volatile bool overflow_ = false; + volatile uint32_t droppedItems_ = 0; bool running_ = false; bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false; uint32_t lastRawTick_ = 0; diff --git a/README.md b/README.md index 52e9484..2a72e64 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ PWM_SETTLE_CYCLES / actualFrequency + TEST_TIME * REPEATS | Target | Arduino-ESP32 | Flash | RAM | Результат | |---|---:|---:|---:|---| -| ESP32-C3 | 3.3.10 | 1,024,299 B (78%) | 39,460 B (12%) | PASS | +| ESP32-C3 | 3.3.10 | 1,025,021 B (78%) | 45,380 B (13%) | PASS | | ESP32-S3 | 3.3.10 | 950,608 B (72%) | 48,620 B (14%) | PASS | Локальные unit-тесты: `core tests: PASS`, `button tests: PASS`. Они покрывают неделимый диапазон, END без дубля, ALL, границы допусков, немедленный FAIL, resolution, checksum настроек, CRC протокола и отсутствие short после long.