From 21f8fe8e1355adb6b1dd03e8fdabed91ab0567e5 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Sat, 8 Aug 2026 10:18:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - улушчено отображение на OLED - убраны настройки шага частоты и количества повторов - сделан перебор только реализуемых частот - увеличена точность, на 1МГц 1.25%, в остальных до 1% - точное измерение TOTAL TIME --- OpticalChannelTester/App.cpp | 153 ++++++++++++++++--------- OpticalChannelTester/Config.h | 29 +++-- OpticalChannelTester/Core.cpp | 64 ++++++++--- OpticalChannelTester/Core.h | 10 +- OpticalChannelTester/Display.cpp | 6 +- OpticalChannelTester/Measurement.cpp | 48 ++++---- OpticalChannelTester/Measurement.h | 11 +- OpticalChannelTester/Protocol.h | 4 +- OpticalChannelTester/Receiver.cpp | 73 +++++++++--- OpticalChannelTester/Receiver.h | 5 +- OpticalChannelTester/SettingsStore.cpp | 13 +-- README.md | 28 +++-- 12 files changed, 293 insertions(+), 151 deletions(-) diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index 491805b..f196102 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -26,6 +26,27 @@ void formatErrorDuty(float duty, char *out, size_t size) { if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty); else snprintf(out, size, "%.1f%%", duty); } + +void formatMenuLine(const char *label, const char *value, char *out, size_t size) { + constexpr size_t OLED_TEXT_COLUMNS = 21; + const size_t valueLength = strlen(value); + const int labelWidth = static_cast( + valueLength < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - valueLength : 1U); + snprintf(out, size, "%-*s%s", labelWidth, label, value); +} + +uint32_t overallProgress(uint32_t stageIndex, uint8_t step) { + if (step > MEASUREMENT_PROGRESS_STEPS) step = MEASUREMENT_PROGRESS_STEPS; + return stageIndex * MEASUREMENT_PROGRESS_STEPS + step; +} + +uint32_t overallProgressTotal(uint32_t stageCount) { + return stageCount * MEASUREMENT_PROGRESS_STEPS; +} + +uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) { + return static_cast((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL); +} } App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {} @@ -103,7 +124,7 @@ void App::update() { } if (state_ == AppState::MENU) { if (modeEvent == ButtonEvent::SHORT) { - menuItem_ = (menuItem_ + 1U) % 7U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); + menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); } else if (modeEvent == ButtonEvent::LONG) { sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); @@ -145,24 +166,18 @@ void App::showIdle() { } void App::sanitizeRange() { - settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ); - if (END_FREQ_OPTIONS_HZ[settings_.endIndex] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) { - size_t i = 0; - while (i < countOf(END_FREQ_OPTIONS_HZ) && END_FREQ_OPTIONS_HZ[i] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) ++i; - if (i == countOf(END_FREQ_OPTIONS_HZ)) { settings_.startIndex = 0; i = countOf(END_FREQ_OPTIONS_HZ) - 1; } - settings_.endIndex = i; - } + settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); + settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ); } void App::changeMenu(int d) { + sanitizeRange(); uint8_t *value = nullptr; size_t count = 0; switch (menuItem_) { case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break; case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break; - case 2: value = &settings_.stepIndex; count = countOf(STEP_OPTIONS_HZ); break; - case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break; - case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break; - case 5: value = &settings_.repeatIndex; count = countOf(REPEAT_OPTIONS); break; + case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break; + case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break; default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break; } *value = static_cast((*value + count + d) % count); @@ -171,34 +186,51 @@ void App::changeMenu(int d) { } void App::showMenu() { - char one[22], two[22], all[12]; + char one[22], value[12], total[22], all[12]; + const char *label = nullptr; Display::formatDuration(actualNominalTotalUs(), all, sizeof(all)); switch (menuItem_) { - case 0: snprintf(one, sizeof(one), "START FREQ"); Display::formatFrequency(params_.startHz, two, sizeof(two)); break; - case 1: snprintf(one, sizeof(one), "END FREQ"); Display::formatFrequency(params_.endHz, two, sizeof(two)); break; - case 2: snprintf(one, sizeof(one), "FREQ STEP"); Display::formatFrequency(params_.stepHz, two, sizeof(two)); break; - case 3: snprintf(one, sizeof(one), "ACCURACY"); snprintf(two, sizeof(two), "+/-%g%%", params_.accuracyPct); break; - case 4: snprintf(one, sizeof(one), "TEST TIME"); snprintf(two, sizeof(two), "%.1fs", params_.testTimeMs / 1000.0f); break; - case 5: snprintf(one, sizeof(one), "REPEATS"); snprintf(two, sizeof(two), "%ux", params_.repeats); break; - default: snprintf(one, sizeof(one), "PWM DUTY"); snprintf(two, sizeof(two), "%u%%", params_.dutyPct); break; + case 0: + Display::formatTestFrequency(params_.startHz, value, sizeof(value)); + strncat(value, " Hz", sizeof(value) - strlen(value) - 1U); + label = "START FREQ:"; + break; + case 1: + Display::formatTestFrequency(params_.endHz, value, sizeof(value)); + strncat(value, " Hz", sizeof(value) - strlen(value) - 1U); + label = "END FREQ:"; + break; + case 2: + snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct); + label = "ACCURACY:"; + break; + case 3: + snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f); + label = "TEST TIME:"; + break; + default: + snprintf(value, sizeof(value), "%u%%", params_.dutyPct); + label = "PWM DUTY:"; + break; } - const size_t used = strlen(two); snprintf(two + used, sizeof(two) - used, " ALL %s", all); display_.show(one, two); + formatMenuLine(label, value, one, sizeof(one)); + formatMenuLine("TOTAL TIME:", all, total, sizeof(total)); + display_.show(one, total); } void App::startTest() { - params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); + params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 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) { - char startText[12], endText[12], stepText[12]; + char startText[12], endText[12]; Display::formatFrequency(params_.startHz, startText, sizeof(startText)); Display::formatFrequency(params_.endHz, endText, sizeof(endText)); - Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText)); - Log::printf("CONFIG", "mode=%s range=%s..%s step=%s accuracy=%.2f%% time=%lums repeats=%u duty=%u%% stages=%lu", - roleName(static_cast(settings_.role)), startText, endText, stepText, - params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_); + Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu", + roleName(static_cast(settings_.role)), startText, endText, + params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_); } printConfiguration(); const Role role = static_cast(settings_.role); @@ -212,7 +244,7 @@ void App::startTest() { bool App::armSlave(bool preserveDisplay) { params_ = store_.params(settings_); - stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); + stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0; if (!radio_.begin()) { @@ -228,7 +260,7 @@ bool App::armSlave(bool preserveDisplay) { } bool App::prepareStage(bool showProgress) { - requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); + requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); actual_ = {}; const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ : (receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ); @@ -239,11 +271,13 @@ bool App::prepareStage(bool showProgress) { GPIO_PWM, requestedHz_); finish(false, FailReason::RESOLUTION); return false; } + const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct); const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct, - receiver_.tickHz(), actual_.bits); + plannedRxHz, actual_.bits); if (resolution != FailReason::NONE) { Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%", - actual_.actualHz, actual_.actualDutyPct, actual_.bits, receiver_.tickHz(), params_.accuracyPct); + actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, + effectiveTolerancePct(params_.accuracyPct)); finish(false, resolution); return false; } Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED", @@ -256,9 +290,10 @@ bool App::prepareStage(bool showProgress) { } bool App::startLocalMeasurement(float hz, float duty) { - Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% settle=%u cycles window=%lums x%u; per-pulse logging suspended", - hz, duty, params_.accuracyPct, PWM_SETTLE_CYCLES, params_.testTimeMs, params_.repeats); - const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, params_.repeats, PWM_SETTLE_CYCLES); + Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended", + hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast(hz + 0.5f), duty), + PWM_SETTLE_CYCLES, params_.testTimeMs); + const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES); Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED", receiver_.receiveChunkSymbols()); return ok; @@ -270,7 +305,7 @@ void App::stagePassed() { if (++stageIndex_ >= stageCount_) { 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_); + requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); actual_ = {}; stageStartConfirmed_ = false; pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE); @@ -294,7 +329,7 @@ ProtocolPacket App::makePacket(MessageType type) const { p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz; const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct; p.actualDutyX100 = static_cast(packetDuty * 100.0f + 0.5f); - p.testTimeMs = params_.testTimeMs; p.repeats = params_.repeats; + p.testTimeMs = params_.testTimeMs; p.accuracyX100 = static_cast(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES; return p; } @@ -349,7 +384,7 @@ void App::handleRadio() { } if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) { memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis(); - requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); + requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue; } @@ -374,7 +409,7 @@ void App::handleRadio() { stageIndex_ = r.packet.stage; sequence_ = r.packet.sequence; state_ = AppState::SLAVE_WAIT_START; - params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats; + params_.testTimeMs = r.packet.testTimeMs; params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz; stageCount_ = r.packet.stageCount; actual_ = {}; @@ -402,11 +437,12 @@ void App::handleRadio() { r.packet.sequence == pendingPacket_.sequence) { if (!stageStartConfirmed_) showStageProgress(); stageStartConfirmed_ = true; - deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS + - (1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20; + deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + + LINK_REPLY_TIMEOUT_MS + 20; } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) { stageStartConfirmed_ = true; - deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS; + deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + + LINK_REPLY_TIMEOUT_MS; showRemoteResult(r.packet); } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) { ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence; @@ -428,7 +464,8 @@ void App::handleRadio() { actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f; if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; } showStageProgress(); - state_ = AppState::SLAVE_MEASURE; deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS; + state_ = AppState::SLAVE_MEASURE; + deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS; ProtocolPacket started = makePacket(MessageType::READY); started.sequence = r.packet.sequence; sendLinked(started); } else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) { @@ -475,7 +512,7 @@ void App::updateMaster() { Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast(pendingPacket_.type)), retries_ + 1); sendLinked(pendingPacket_); ++retries_; deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ? - (stageStartConfirmed_ ? params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) : + (stageStartConfirmed_ ? stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) : LINK_REPLY_TIMEOUT_MS); } @@ -488,10 +525,9 @@ void App::updateSlave() { StageStats live = {}; if (measurement_.statsSnapshot(live)) { ProtocolPacket progress = makePacket(MessageType::PROGRESS); + progress.progressStep = measurement_.progressStep(); fillMeasuredResult(progress, live); progress.sequence = sequence_; sendLinked(progress); - // oled.display() is synchronous. Resume capture only after the full - // framebuffer has reached the display. showStageResult(live); } measurement_.continueAfterDisplay(); @@ -503,6 +539,8 @@ void App::updateSlave() { printStageStats(measurement_.stats(), actual_.actualHz); showStageResult(measurement_.stats()); pendingPacket_ = makePacket(MessageType::RESULT); + pendingPacket_.progressStep = ms == MeasureState::PASS ? + MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep(); pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE; pendingPacket_.reason = static_cast(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods; fillMeasuredResult(pendingPacket_, measurement_.stats()); @@ -567,7 +605,7 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) { if (pass) { const Role role = static_cast(settings_.role); snprintf(one, sizeof(one), "%s PASS", roleName(role)); - display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=REPEAT"); + display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=AGAIN"); } else if (requestedHz_) { char frequency[12]; @@ -588,11 +626,11 @@ void App::printConfiguration() { Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX, GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL); - Serial.printf("Test %lu..%lu step %lu Hz, accuracy %.2f%%, %lums x%u, duty %u%%\n", - params_.startHz, params_.endHz, params_.stepHz, params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct); - stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); + Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n", + params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct); + stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); Serial.printf("Frequencies (%lu): ", stageCount_); - for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, params_.stepHz, i), i + 1 == stageCount_ ? "\n" : ","); + for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ","); Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong"); } @@ -631,20 +669,23 @@ void App::showStageResult(const StageStats &s) { } else { snprintf(two, sizeof(two), "%s", failName(s.reason)); } - display_.show(one, two, stageIndex_ + 1, stageCount_); + display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), + overallProgressTotal(stageCount_)); return; } snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu", target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_); if (!s.periods || !s.periodSum) { - display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_); + display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, measurement_.progressStep()), + overallProgressTotal(stageCount_)); return; } const float measuredHz = static_cast(receiver_.tickHz()) * s.periods / s.periodSum; const float measuredDuty = 100.0f * s.activeSum / s.periodSum; char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency)); snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty); - display_.show(one, two, stageIndex_ + 1, stageCount_); + display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), + overallProgressTotal(stageCount_)); } void App::showRemoteResult(const ProtocolPacket &packet) { @@ -674,7 +715,8 @@ void App::showRemoteResult(const ProtocolPacket &packet) { snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f); snprintf(two, sizeof(two), "%s", failName(reason)); } - display_.show(one, two, stageIndex_ + 1, stageCount_); + display_.show(one, two, overallProgress(stageIndex_, packet.progressStep), + overallProgressTotal(stageCount_)); } void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const { @@ -695,5 +737,6 @@ void App::showStageProgress() { Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu", target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_); - display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_); + display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, 0), + overallProgressTotal(stageCount_)); } diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 35be25c..dc7184c 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -57,14 +57,16 @@ constexpr uint32_t RMT_TARGET_CHUNK_US = 5000; constexpr uint8_t RMT_QUEUE_BLOCKS = 8; constexpr uint16_t PERIOD_BATCH_SIZE = 128; constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10; +constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15; +// Conservative sustained validation rate calibrated from real C3 logs. +constexpr uint32_t RX_PROCESSING_PERIODS_PER_SECOND = 300000; constexpr uint32_t C3_STRICT_MAX_HZ = 1000000; constexpr uint32_t S3_STRICT_MAX_HZ = 1000000; -// RMT stores each HIGH/LOW duration in 15 bits. At 80 MHz that limits a -// single level to about 409 us, so even a 1 kHz signal with 50% duty cannot -// be captured. 20 MHz still provides 20 ticks at 1 MHz (5% resolution), -// while allowing level durations up to about 1.64 ms for the 1 kHz/90% case. -constexpr uint32_t CAPTURE_RESOLUTION_HZ = 20000000; +// RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that +// still fits both levels of the current PWM signal: 20, 40 or 80 MHz. +constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000}; +constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766; // Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3. // Keep this explicit so the resolution calculation never asks LEDC for an // impossible frequency/resolution combination. @@ -73,12 +75,21 @@ constexpr uint8_t LEDC_CHANNEL = 0; constexpr uint8_t LEDC_MAX_BITS = 14; // -------------------------- Menu value arrays ----------------------------- -constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000}; -constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 200000, 500000, 750000, 1000000}; -constexpr uint32_t STEP_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000, 100000}; +// START and END deliberately have separate, independently cycling menu lists. +// Every value is exactly achievable from the 40 MHz XTAL with an integer LEDC +// divider. The test itself walks TEST_FREQUENCIES_HZ between the selected +// endpoints, so there is no separately configurable step. +constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 10000, 100000}; +constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 500000, 1000000}; + +// All achievable whole-number frequencies in the supported 1 kHz..1 MHz +// range, used for adjacent test stages rather than direct menu selection. +constexpr uint32_t TEST_FREQUENCIES_HZ[] = { + 1000, 2000, 5000, 10000, 25000, 50000, + 100000, 200000, 312500, 400000, 500000, 625000, 800000, 1000000 +}; constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f}; constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000}; -constexpr uint8_t REPEAT_OPTIONS[] = {1, 2, 3, 5, 10}; constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90}; template constexpr size_t countOf(const T (&)[N]) { return N; } diff --git a/OpticalChannelTester/Core.cpp b/OpticalChannelTester/Core.cpp index 1ab0399..9abb4e1 100644 --- a/OpticalChannelTester/Core.cpp +++ b/OpticalChannelTester/Core.cpp @@ -1,4 +1,5 @@ #include "Core.h" +#include "Config.h" #include #include @@ -30,28 +31,52 @@ uint32_t settingsChecksum(const Settings &s) { return hash; } -uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz) { - if (!startHz || !stepHz || endHz <= startHz) return 0; - const uint64_t span = static_cast(endHz) - startHz; - return static_cast(span / stepHz + 1U + ((span % stepHz) ? 1U : 0U)); +uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) { + if (!startHz || endHz <= startHz) return 0; + uint32_t count = 0; + for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) + if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count; + return count; } -uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index) { - const uint32_t count = frequencyPointCount(startHz, endHz, stepHz); - if (!count || index >= count) return 0; - if (index == count - 1) return endHz; - const uint64_t v = static_cast(startHz) + static_cast(stepHz) * index; - return v < endHz ? static_cast(v) : endHz; +uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) { + for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) { + const uint32_t frequency = TEST_FREQUENCIES_HZ[i]; + if (frequency < startHz || frequency > endHz) continue; + if (!index--) return frequency; + } + return 0; +} + +uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles) { + if (!frequencyHz || !sampleTimeMs) return 0; + const uint64_t sampleUs = static_cast(sampleTimeMs) * 1000ULL; + // At high frequency the CPU needs longer than the requested sample window + // to validate every captured period. Use the measured sustained C3 rate. + const uint64_t processingUs = + (static_cast(frequencyHz) * sampleTimeMs * 1000ULL + + RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND; + const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs; + + uint64_t chunkSymbols = + (static_cast(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL; + if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS; + if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS; + const uint64_t batchWaitUs = + ((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS; + const uint64_t settleUs = + (1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz; + // Initial stage screen, nine intermediate screens and the final result. + const uint64_t displayUs = static_cast(OLED_PROGRESS_UPDATE_MS) * 1000ULL * + (MEASUREMENT_PROGRESS_STEPS + 1U); + return samplingWallUs + batchWaitUs + settleUs + displayUs; } uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) { uint64_t total = 0; - const uint32_t count = frequencyPointCount(p.startHz, p.endHz, p.stepHz); - for (uint32_t i = 0; i < count; ++i) { - const uint32_t f = frequencyAt(p.startHz, p.endHz, p.stepHz, i); - total += (1000000ULL * settleCycles + f - 1) / f; - total += static_cast(p.testTimeMs) * 1000ULL * p.repeats; - } + const uint32_t count = frequencyPointCount(p.startHz, p.endHz); + for (uint32_t i = 0; i < count; ++i) + total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles); return total; } @@ -63,6 +88,10 @@ bool dutyWithin(float measured, float expected, float tolerance) { return fabsf(measured - expected) <= tolerance + 0.0001f; } +float effectiveTolerancePct(float configured) { + return configured > 0.0f && configured <= 1.0001f ? 1.25f : configured; +} + uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t maxBits) { if (!frequencyHz || !sourceClockHz || !maxBits) return 0; @@ -148,7 +177,8 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac // 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) + const float effectiveAccuracy = effectiveTolerancePct(accuracyPct); + return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy) ? FailReason::RESOLUTION : FailReason::NONE; } diff --git a/OpticalChannelTester/Core.h b/OpticalChannelTester/Core.h index 202f628..78c4aaa 100644 --- a/OpticalChannelTester/Core.h +++ b/OpticalChannelTester/Core.h @@ -17,10 +17,8 @@ struct Settings { uint8_t role; uint8_t startIndex; uint8_t endIndex; - uint8_t stepIndex; uint8_t accuracyIndex; uint8_t timeIndex; - uint8_t repeatIndex; uint8_t dutyIndex; uint32_t checksum; }; @@ -28,10 +26,8 @@ struct Settings { struct TestParams { uint32_t startHz; uint32_t endHz; - uint32_t stepHz; float accuracyPct; uint32_t testTimeMs; - uint8_t repeats; uint8_t dutyPct; }; @@ -72,11 +68,13 @@ struct IntegerPwmConfig { }; 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); +uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz); +uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index); +uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles); uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles); bool periodWithin(float measuredHz, float expectedHz, float tolerancePct); bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct); +float effectiveTolerancePct(float configuredPct); uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t maxBits); uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, diff --git a/OpticalChannelTester/Display.cpp b/OpticalChannelTester/Display.cpp index 450da25..e681ffe 100644 --- a/OpticalChannelTester/Display.cpp +++ b/OpticalChannelTester/Display.cpp @@ -9,6 +9,7 @@ Display::Display() : oled_(128, 32, &Wire, -1) {} bool Display::begin() { Wire.begin(GPIO_SDA, GPIO_SCL); + Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms // An absent optional OLED produces a large burst of ESP-IDF NACK messages. // Probe it once and keep the I2C driver quiet when no display is connected. esp_log_level_set("i2c.master", ESP_LOG_NONE); @@ -81,7 +82,8 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) { } void Display::formatDuration(uint64_t us, char *out, size_t n) { - const uint64_t minutes = us / 60000000ULL; - if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, (us / 1000000ULL) % 60ULL); + const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL; + const uint64_t minutes = totalSeconds / 60ULL; + if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, totalSeconds % 60ULL); else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL); } diff --git a/OpticalChannelTester/Measurement.cpp b/OpticalChannelTester/Measurement.cpp index f9c4d1c..c372448 100644 --- a/OpticalChannelTester/Measurement.cpp +++ b/OpticalChannelTester/Measurement.cpp @@ -3,24 +3,26 @@ #include bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs, - uint8_t repeats, uint8_t settleCycles) { + uint8_t settleCycles) { if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false; expectedHz_ = static_cast(hz + 0.5f); - if (!expectedHz_ || !timeMs || !repeats || repeats > 10 || - !makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) || - !receiver_.start(expectedHz_)) return false; - repeats_ = repeats; settleCycles_ = settleCycles; settleLeft_ = settleCycles; + expectedDutyPct_ = duty; + tolerance = effectiveTolerancePct(tolerance); + if (!expectedHz_ || !timeMs || + !receiver_.start(expectedHz_, expectedDutyPct_)) return false; + if (!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_)) { + receiver_.stop(); return false; + } + settleCycles_ = settleCycles; settleLeft_ = settleCycles; stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS; stepTicks_ = static_cast(receiver_.tickHz()) * timeMs / (1000ULL * MEASUREMENT_PROGRESS_STEPS); if (!stepTicks_) stepTicks_ = 1; - totalSteps_ = repeats * MEASUREMENT_PROGRESS_STEPS; - currentStep_ = currentRepeat_ = 0; - stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_)); + currentStep_ = 0; + stats_.reset(); publishStats(); measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0; - currentRepeat_ = 0; expectedPeriodMs_ = static_cast((1000ULL + expectedHz_ - 1U) / expectedHz_); if (!expectedPeriodMs_) expectedPeriodMs_ = 1; state_ = MeasureState::SETTLING; @@ -45,17 +47,16 @@ void Measurement::fail(FailReason reason) { receiver_.stop(); state_ = MeasureState::FAIL; } -void Measurement::completeStep() { +void Measurement::completeMeasurement() { receiver_.stop(); stats_.droppedItems += receiver_.takeDroppedItems(); if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; } - ++currentStep_; publishStats(); - if (currentStep_ < totalSteps_) { + if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) { state_ = MeasureState::STEP_READY; return; } - for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) { + if (!stats_.periods) { fail(FailReason::TOO_FEW_PERIODS); return; } state_ = MeasureState::PASS; @@ -91,16 +92,14 @@ MeasureState Measurement::processOnce() { if (!settleLeft_) { measurementStartTick_ = period.startTick + period.periodTicks; deadlineTick_ = measurementStartTick_ + stepTicks_; - currentRepeat_ = currentStep_ / MEASUREMENT_PROGRESS_STEPS; measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING; } continue; } const uint64_t endTick = period.startTick + period.periodTicks; if (period.startTick < measurementStartTick_) continue; // leading incomplete period - if (endTick > deadlineTick_) { completeStep(); return state_; } // trailing incomplete period - ++repeatPeriods_[currentRepeat_]; - const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_); + if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period + const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_); if (r != FailReason::NONE) { fail(r); return state_; } } } @@ -113,11 +112,18 @@ MeasureState Measurement::processOnce() { if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL); if (state_ == MeasureState::RUNNING && measurementStartTick_) { const uint32_t now = millis(); - const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2; - if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) { + // RMT reports a block only after its user buffer has filled. At 1 kHz the + // minimum 48-symbol C3 block contains roughly 48 PWM periods and therefore + // arrives much later than the old 8-period timeout. Do not call that + // normal batching delay a lost edge. + const uint32_t batchPeriods = receiver_.receiveChunkSymbols(); + const uint32_t batchTimeoutMs = expectedPeriodMs_ * (batchPeriods + NO_SIGNAL_TIMEOUT_PERIODS) + 2U; + const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U; + const uint32_t receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : edgeTimeoutMs; + if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > receiveTimeoutMs) { fail(FailReason::LOST_EDGE); return state_; } - if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeStep(); + if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement(); } return state_; } @@ -126,7 +132,7 @@ MeasureState Measurement::update() { return state_; } bool Measurement::continueAfterDisplay() { if (state_ != MeasureState::STEP_READY) return false; - if (!receiver_.start(expectedHz_)) { + if (!receiver_.start(expectedHz_, expectedDutyPct_)) { fail(FailReason::UNSUPPORTED); return false; } diff --git a/OpticalChannelTester/Measurement.h b/OpticalChannelTester/Measurement.h index 5491a95..9426145 100644 --- a/OpticalChannelTester/Measurement.h +++ b/OpticalChannelTester/Measurement.h @@ -7,20 +7,21 @@ class Measurement { public: explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {} bool start(float expectedHz, float expectedDuty, float tolerancePct, - uint32_t testTimeMs, uint8_t repeats, uint8_t settleCycles); + uint32_t testTimeMs, uint8_t settleCycles); MeasureState update(); bool continueAfterDisplay(); void abort(); MeasureState state() const { return state_; } FailReason reason() const { return stats_.reason; } const StageStats &stats() const { return stats_; } + uint8_t progressStep() const { return currentStep_; } bool statsSnapshot(StageStats &out) const; private: static void taskEntry(void *context); void taskLoop(); MeasureState processOnce(); void fail(FailReason reason); - void completeStep(); + void completeMeasurement(); void publishStats(); PulseReceiver &receiver_; volatile MeasureState state_ = MeasureState::IDLE; @@ -30,11 +31,11 @@ class Measurement { mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED; PeriodLimits limits_ = {}; uint32_t expectedHz_ = 0; - uint8_t repeats_ = 0, settleCycles_ = 0, settleLeft_ = 0; - uint8_t currentRepeat_ = 0, currentStep_ = 0, totalSteps_ = 0; + float expectedDutyPct_ = 0.0f; + uint8_t settleCycles_ = 0, settleLeft_ = 0; uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0; uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0; uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1; - uint32_t repeatPeriods_[10] = {}; + volatile uint8_t currentStep_ = 0; PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {}; }; diff --git a/OpticalChannelTester/Protocol.h b/OpticalChannelTester/Protocol.h index 35eccd4..71d8cd4 100644 --- a/OpticalChannelTester/Protocol.h +++ b/OpticalChannelTester/Protocol.h @@ -2,7 +2,7 @@ #include "Core.h" constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; -constexpr uint8_t PROTOCOL_VERSION = 6; +constexpr uint8_t PROTOCOL_VERSION = 8; enum class MessageType : uint8_t { DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT, @@ -24,9 +24,9 @@ struct ProtocolPacket { uint32_t actualHz; uint16_t actualDutyX100; uint32_t testTimeMs; - uint8_t repeats; uint16_t accuracyX100; uint8_t settleCycles; + uint8_t progressStep; uint8_t passed; uint8_t reason; uint32_t periods; diff --git a/OpticalChannelTester/Receiver.cpp b/OpticalChannelTester/Receiver.cpp index d4b55fa..fcdefd3 100644 --- a/OpticalChannelTester/Receiver.cpp +++ b/OpticalChannelTester/Receiver.cpp @@ -8,17 +8,55 @@ uint32_t PulseReceiver::tickHz() const { #if OPTICAL_USE_RMT_DMA - return CAPTURE_RESOLUTION_HZ; + return captureResolutionHz_; #else return cpuTickHz_; #endif } +uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const { +#if OPTICAL_USE_RMT_DMA + if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) + return CAPTURE_RESOLUTION_OPTIONS_HZ[0]; + uint32_t dutyX100 = static_cast(expectedDutyPct * 100.0f + 0.5f); + if (dutyX100 < 5000U) dutyX100 = 10000U - dutyX100; + for (int i = static_cast(countOf(CAPTURE_RESOLUTION_OPTIONS_HZ)) - 1; i >= 0; --i) { + const uint32_t resolution = CAPTURE_RESOLUTION_OPTIONS_HZ[i]; + const uint64_t levelTicksX100 = static_cast(resolution) * dutyX100; + const uint64_t limitX100 = static_cast(expectedHz) * 10000ULL * RMT_MAX_LEVEL_TICKS; + if (levelTicksX100 <= limitX100) return resolution; + } + return CAPTURE_RESOLUTION_OPTIONS_HZ[0]; +#else + (void)expectedHz; (void)expectedDutyPct; + return cpuTickHz_; +#endif +} + bool PulseReceiver::begin() { #if OPTICAL_USE_RMT_DMA queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock)); + return queue_ && configureRmt(CAPTURE_RESOLUTION_OPTIONS_HZ[0]); +#else + queue_ = xQueueCreate(256, sizeof(Edge)); + if (!queue_) return false; + pinMode(GPIO_RX, INPUT); + cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; + attachInterruptArg(GPIO_RX, onGpio, this, CHANGE); + return cpuTickHz_ != 0; +#endif +} + +#if OPTICAL_USE_RMT_DMA +bool PulseReceiver::configureRmt(uint32_t resolutionHz) { + if (channel_ && captureResolutionHz_ == resolutionHz) return true; + stop(); + if (channel_) { + if (rmt_del_channel(channel_) != ESP_OK) return false; + channel_ = nullptr; + } rmt_rx_channel_config_t cfg = {}; - cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ; + cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz; cfg.gpio_num = static_cast(GPIO_RX); cfg.flags.invert_in = RX_SIGNAL_INVERTED; #if CONFIG_IDF_TARGET_ESP32S3 @@ -30,20 +68,23 @@ bool PulseReceiver::begin() { cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS; cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception #endif - if (!queue_ || rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false; + if (rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false; rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt; - return rmt_rx_register_event_callbacks(channel_, &callbacks, this) == ESP_OK; -#else - queue_ = xQueueCreate(256, sizeof(Edge)); - if (!queue_) return false; - pinMode(GPIO_RX, INPUT); - cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; - attachInterruptArg(GPIO_RX, onGpio, this, CHANGE); - return cpuTickHz_ != 0; -#endif + if (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) { + rmt_del_channel(channel_); channel_ = nullptr; return false; + } + captureResolutionHz_ = resolutionHz; + return true; } +#endif -bool PulseReceiver::start(uint32_t expectedHz) { +bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) { +#if OPTICAL_USE_RMT_DMA + const uint32_t resolutionHz = plannedTickHz(expectedHz, expectedDutyPct); + if (!configureRmt(resolutionHz)) return false; +#else + (void)expectedHz; (void)expectedDutyPct; +#endif resetStream(); #if OPTICAL_USE_RMT_DMA // In partial RX mode the callback is delivered when this user buffer fills. @@ -54,19 +95,17 @@ bool PulseReceiver::start(uint32_t expectedHz) { receiveChunkSymbols_ = static_cast(symbols); if (rmt_enable(channel_) != ESP_OK) return false; rmt_receive_config_t cfg = {}; - cfg.signal_range_min_ns = 1000000000UL / CAPTURE_RESOLUTION_HZ; + cfg.signal_range_min_ns = 1000000000UL / captureResolutionHz_; const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1); // A duration field is 15 bits. Keep the driver's end-of-signal threshold // strictly below that hardware limit (IDF rejects larger values). - const uint64_t hardwareMaxNs = 32766ULL * 1000000000ULL / CAPTURE_RESOLUTION_HZ; + const uint64_t hardwareMaxNs = static_cast(RMT_MAX_LEVEL_TICKS) * 1000000000ULL / captureResolutionHz_; cfg.signal_range_max_ns = static_cast(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs); cfg.flags.en_partial_rx = true; if (rmt_receive(channel_, receiveBuffer_, receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) { rmt_disable(channel_); return false; } -#else - (void)expectedHz; #endif running_ = true; return true; } diff --git a/OpticalChannelTester/Receiver.h b/OpticalChannelTester/Receiver.h index 2494bdd..d68fd1b 100644 --- a/OpticalChannelTester/Receiver.h +++ b/OpticalChannelTester/Receiver.h @@ -15,13 +15,14 @@ class PulseReceiver { public: bool begin(); - bool start(uint32_t expectedHz); + bool start(uint32_t expectedHz, float expectedDutyPct); void stop(); void resetStream(); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); bool overflowed(); uint32_t takeDroppedItems(); uint32_t tickHz() const; + uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const; uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; } bool highRateBackend() const { #if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3 @@ -38,8 +39,10 @@ class PulseReceiver { 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 configureRmt(uint32_t resolutionHz); bool nextRmtEdge(Edge &edge, TickType_t waitTicks); rmt_channel_handle_t channel_ = nullptr; + uint32_t captureResolutionHz_ = 0; rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS]; uint16_t receiveChunkSymbols_ = 0; SymbolBlock isrBlock_ = {}; diff --git a/OpticalChannelTester/SettingsStore.cpp b/OpticalChannelTester/SettingsStore.cpp index f4a55cb..d45df0e 100644 --- a/OpticalChannelTester/SettingsStore.cpp +++ b/OpticalChannelTester/SettingsStore.cpp @@ -3,19 +3,19 @@ #include "Log.h" #include -namespace { constexpr uint16_t SETTINGS_VERSION = 1; constexpr char NAMESPACE[] = "opt-test"; } +namespace { constexpr uint16_t SETTINGS_VERSION = 4; constexpr char NAMESPACE[] = "opt-test"; } void SettingsStore::defaults(Settings &s) const { - s = {SETTINGS_VERSION, static_cast(Role::SOLO), 0, 4, 1, 2, 3, 2, 2, 0}; + s = {SETTINGS_VERSION, static_cast(Role::SOLO), 0, 4, 2, 3, 2, 0}; s.checksum = settingsChecksum(s); } bool SettingsStore::valid(const Settings &s) const { return s.version == SETTINGS_VERSION && s.role <= static_cast(Role::SLAVE) && s.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) && - s.stepIndex < countOf(STEP_OPTIONS_HZ) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) && - s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.repeatIndex < countOf(REPEAT_OPTIONS) && - s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && s.checksum == settingsChecksum(s) && + s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) && + s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && + s.checksum == settingsChecksum(s) && END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex]; } @@ -40,7 +40,6 @@ bool SettingsStore::save(Settings &s) { TestParams SettingsStore::params(const Settings &s) const { return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex], - STEP_OPTIONS_HZ[s.stepIndex], ACCURACY_OPTIONS_PCT[s.accuracyIndex], - TEST_TIME_OPTIONS_MS[s.timeIndex], REPEAT_OPTIONS[s.repeatIndex], + ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex], DUTY_OPTIONS_PCT[s.dutyIndex]}; } diff --git a/README.md b/README.md index 095a8bc..de1b4a1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - отбрасывание ровно `PWM_SETTLE_CYCLES` полных периодов; - потоковая статистика без хранения всех периодов; - немедленный FAIL по первому плохому периоду; -- непрерывное измерение повторов без слепых промежутков; +- десять измерительных участков на каждой частоте с промежуточным отображением результата; - ESP-NOW discovery, handshake, CRC, session/stage/sequence, ACK, повторы и защита от старых пакетов; - SSD1306 128×32: каждый экран всегда состоит ровно из двух строк; - две кнопки, debounce, long press и repeat; после long press ложный short click не создаётся; @@ -78,6 +78,8 @@ OLED можно не подключать. Откройте Serial Monitor на [ 1250][UI ] MODE: SOLO | START=RUN ``` +Полоса прогресса учитывает номер частоты и десять измерительных участков внутри неё. Например, диапазон из 11 частот даёт 110 последовательных позиций прогресса. + В Serial также выводятся: - каждое распознанное нажатие START/MODE и текущее состояние автомата; @@ -154,23 +156,31 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE ## Настройка диапазона -Редактируйте отдельные `constexpr`-массивы в `Config.h`. Начальная и конечная частоты намеренно находятся в разных массивах. Сохранённые индексы всегда проверяются; после изменения массивов повреждённая/несовместимая настройка не приводит к выходу за границы. +START и END выбираются из отдельных массивов `START_FREQ_OPTIONS_HZ` и `END_FREQ_OPTIONS_HZ` в `Config.h` и зацикливаются независимо. Полный список точных частот заранее рассчитан для XTAL 40 МГц, целого делителя LEDC и таймера 1…14 бит и записан в `TEST_FREQUENCIES_HZ`. Сохранённые индексы всегда проверяются; после изменения массивов повреждённая/несовместимая настройка не приводит к выходу за границы. -Последовательность всегда начинается точно с START, идёт с STEP и завершается точно END. Например, `100…1000` с шагом `300` даёт `100, 400, 700, 1000`. Конечная точка не дублируется, вычисления выполняются через 64-битные промежуточные значения. +Отдельной настройки STEP нет: тест начинается с выбранной START, проходит все соседние достижимые точки из полного списка и заканчивается на выбранной END. -`ALL` пересчитывается после изменения START, END, STEP, TEST TIME или REPEATS. Частота каждой точки предварительно запрашивается у LEDC с duty=0 (на выходе остаётся безопасный уровень), поэтому в расчёте используется фактически достижимая частота. Для каждой точки учитывается: +`ALL` пересчитывается после изменения START, END или TEST TIME и отображается с точностью до целой секунды. Для каждой точки учитывается: ```text -PWM_SETTLE_CYCLES / actualFrequency + TEST_TIME * REPEATS +max(TEST_TIME, periods / RX_PROCESSING_PERIODS_PER_SECOND) ++ ожидание заполнения RMT-пакета × 10 ++ PWM_SETTLE_CYCLES / actualFrequency × 10 ++ OLED_PROGRESS_UPDATE_MS × 11 ``` +`TEST TIME` — это чистое время выборки сигнала, а не полная длительность этапа. На высоких частотах полная длительность заметно возрастает из-за проверки каждого периода. Расчёт использует консервативную производительность 300 тысяч периодов в секунду, полученную из реального журнала ESP32-C3. + ## Строгая проверка После каждой перенастройки PWM приёмник: -1. отбрасывает ровно `PWM_SETTLE_CYCLES` полных периодов; -2. очищает статистику; -3. непрерывно проверяет все полные периоды всех повторов. +1. выбирает максимальную допустимую частоту RMT 20, 40 или 80 МГц с учётом частоты и duty; +2. отбрасывает ровно `PWM_SETTLE_CYCLES` полных периодов; +3. очищает статистику; +4. проверяет все полные периоды десяти участков TEST TIME и между ними показывает промежуточный результат. + +Настройка точности `1%` использует фактический допуск `1,25%`, соответствующий одному такту RMT 80 МГц на частоте сигнала 1 МГц. Незавершённый период в начале и конце окна не учитывается. Период, пересекающий границу повторов, не теряется. Для каждого периода отдельно вычисляются частота и duty; средние используются только для диагностики. Любой один выход за допуск немедленно завершает всю проверку. @@ -194,7 +204,7 @@ PWM_SETTLE_CYCLES / actualFrequency + TEST_TIME * REPEATS 5. Для первого опыта оставьте defaults: 100 Гц…10 кГц, duty 50%, accuracy 5%. 6. Коротко нажмите START. 7. Serial покажет запрошенные и фактические параметры LEDC, список частот, ALL и статистику каждой точки. -8. Успех: `PASS ...` / `START=REPEAT`. Ошибка: `FAIL AT ...` и точная причина. +8. Успех: `PASS ...` / `START=AGAIN`. Ошибка: `FAIL AT ...` и точная причина. ## Проверка проекта