From 035792aedd46db32487d762544b54677862b002c Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Fri, 14 Aug 2026 14:39:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=82=D0=B5=D1=81=D1=82=D1=83=20?= =?UTF-8?q?=D0=B4=D1=80=D0=B0=D0=B9=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpticalChannelTester/App.cpp | 84 ++++++++++------ OpticalChannelTester/Config.h | 8 +- OpticalChannelTester/Config_Text.h | 12 +-- OpticalChannelTester/Core.cpp | 3 +- OpticalChannelTester/Core.h | 3 +- OpticalChannelTester/DriverTest.cpp | 147 +++++++++++++++++++++------- OpticalChannelTester/DriverTest.h | 22 ++++- OpticalChannelTester/Receiver.cpp | 104 ++++++++++++++------ OpticalChannelTester/Receiver.h | 6 +- README.md | 58 ++++++----- 10 files changed, 307 insertions(+), 140 deletions(-) diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index b78fe5d..29f5c12 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -38,6 +38,16 @@ float dutyFromPulse(uint32_t hz, uint32_t pulseNs) { return static_cast(static_cast(hz) * pulseNs / 10000000.0); } +bool configuredTxPulseLightOn(const Settings &settings) { + return static_cast(settings.testKind) == TestKind::DRIVER + ? txActiveLightOn(settings) : true; +} + +const char *configuredLevelName(const Settings &settings) { + return static_cast(settings.testKind) == TestKind::DRIVER + ? lightCodeName(static_cast(settings.lightCode)) : "AUTO"; +} + void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) { char frequency[16], pulse[12]; Display::formatPwmFrequency(hz, frequency, sizeof(frequency)); @@ -67,7 +77,7 @@ void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs, } void formatElapsedNs(uint64_t ns, char *out, size_t size) { - // Compact form keeps `T:... D:... P:...` within 21 OLED columns. + // Compact form keeps error timing within 21 OLED columns. // Exact nanoseconds remain available in the Serial diagnostic. if (ns < 1000ULL) snprintf(out, size, "%llun", ns); else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0); @@ -269,7 +279,7 @@ void App::finishInitialization(bool factoryReset) { } sanitizeRange(); params_ = store_.params(settings_); - pwm_.configureActiveLight(txActiveLightOn(settings_)); + pwm_.configureActiveLight(configuredTxPulseLightOn(settings_)); if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational"); initialized_ = true; if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } @@ -458,7 +468,7 @@ void App::printSerialHelp() { Serial.println(" set min 250|500|1000|2000|5000|10000|50000"); Serial.println(" set accuracy 1|2|5|10"); Serial.println(" set time 100|250|500|1000|2000|5000 (ms)"); - Serial.println(" set light HH|HL|LH|LL"); + Serial.println(" set light HH|HL|LH|LL (DRIVER only)"); } void App::printSerialStatus() { @@ -471,7 +481,7 @@ void App::printSerialStatus() { appStateName(state_), roleName(static_cast(settings_.role)), testKindName(static_cast(settings_.testKind)), params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs, - lightCodeName(static_cast(settings_.lightCode)), + configuredLevelName(settings_), usbHostPresent() ? "connected" : "disconnected"); } @@ -485,7 +495,7 @@ void App::finishSerialSettingsChange() { state_ = AppState::IDLE; sanitizeRange(); params_ = store_.params(settings_); - pwm_.configureActiveLight(txActiveLightOn(settings_)); + pwm_.configureActiveLight(configuredTxPulseLightOn(settings_)); const bool saved = store_.save(settings_); Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no"); if (static_cast(settings_.role) == Role::SLAVE) armSlave(); @@ -577,6 +587,10 @@ void App::handleSerialCommand(char *line) { const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric); if (index >= 0) { settings_.timeIndex = static_cast(index); accepted = true; } } else if (!strcmp(name, "light")) { + if (static_cast(settings_.testKind) != TestKind::DRIVER) { + Serial.println("ERR level setting is available only in DRIVER test"); + return; + } if (!strcmp(value, "hh")) { settings_.lightCode = static_cast(LightCode::HH); accepted = true; } else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast(LightCode::HL); accepted = true; } else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast(LightCode::LH); accepted = true; } @@ -634,7 +648,6 @@ void App::sanitizeRange() { DRIVER_MIN_INPUT_PULSE_NS, driverLastMin); if (settings_.minPulseIndex < firstDriverMin) settings_.minPulseIndex = firstDriverMin; - settings_.lightCode = static_cast(LightCode::HL); } } @@ -672,17 +685,22 @@ void App::changeMenu(int d) { case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_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_.lightCode; count = 4; break; + case 5: + if (static_cast(settings_.testKind) != TestKind::DRIVER) { + showMenu(); + return; + } + value = &settings_.lightCode; count = 4; break; default: return; } *value = cycleIndex(*value, 0, static_cast(count - 1U), d); } sanitizeRange(); params_ = store_.params(settings_); - pwm_.configureActiveLight(txActiveLightOn(settings_)); + pwm_.configureActiveLight(configuredTxPulseLightOn(settings_)); Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u light=%s", menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex, settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex, - lightCodeName(static_cast(settings_.lightCode))); + configuredLevelName(settings_)); showMenu(); } @@ -712,6 +730,13 @@ void App::showMenu() { label = UiText::MENU_TEST_TIME; break; case 5: { + if (static_cast(settings_.testKind) != TestKind::DRIVER) { + snprintf(value, sizeof(value), "%s", UiText::LIGHT_AUTO); + label = UiText::MENU_LIGHT_CODE; + formatMenuLine(label, value, one, sizeof(one)); + display_.show(one, UiText::LIGHT_AUTO_FORMAT); + return; + } const char *code = lightCodeName(static_cast(settings_.lightCode)); snprintf(value, sizeof(value), "%s", code); label = UiText::MENU_LIGHT_CODE; @@ -739,13 +764,13 @@ void App::startTest() { Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu", roleName(static_cast(settings_.role)), testKindName(static_cast(settings_.testKind)), - lightCodeName(static_cast(settings_.lightCode)), stageCount_); + configuredLevelName(settings_), stageCount_); if (SERIAL_MINIMAL_LOG) { Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu", roleName(static_cast(settings_.role)), testKindName(static_cast(settings_.testKind)), params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs, - lightCodeName(static_cast(settings_.lightCode)), + configuredLevelName(settings_), stageCount_); } printConfiguration(); @@ -836,7 +861,7 @@ bool App::startLocalMeasurement(float hz, float duty) { receiver_.plannedPulseTickHz(static_cast(hz + 0.5f), duty), PWM_SETTLE_CYCLES, params_.testTimeMs); const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, - MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, rxActiveLightOn(settings_)); + MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, true); const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, static_cast(hz + 0.5f)); const uint64_t watchdogMs = static_cast(nominalMs) * 2ULL + 2000ULL; @@ -1381,15 +1406,20 @@ void App::printConfiguration() { Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board, roleName(static_cast(settings_.role)), testKindName(static_cast(settings_.testKind)), - lightCodeName(static_cast(settings_.lightCode))); + configuredLevelName(settings_)); 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 Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n", - params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, - params_.accuracyPct, params_.testTimeMs, - lightCodeName(static_cast(settings_.lightCode))[0], - lightCodeName(static_cast(settings_.lightCode))[1]); + if (static_cast(settings_.testKind) == TestKind::DRIVER) { + const char *code = lightCodeName(static_cast(settings_.lightCode)); + Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n", + params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, + params_.accuracyPct, params_.testTimeMs, code[0], code[1]); + } else { + Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, optical polarity=AUTO\n", + params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, + params_.accuracyPct, params_.testTimeMs); + } stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs); Serial.printf("Pulse widths descending (%lu): ", stageCount_); for (uint32_t i = 0; i < stageCount_; ++i) @@ -1484,27 +1514,23 @@ void App::showDriverResult(const DriverStats &s, bool testPassed) { delay, response); } else if (s.reason != FailReason::NONE) { snprintf(one, sizeof(one), "%s", uiFailName(s.reason)); - char elapsed[12] = "---", errorDelay[12] = "---", errorPulse[12] = "---"; - if (s.errorElapsedTicks) { + char elapsed[12] = "---", errorPulse[12] = "---"; + if (s.errorTriggerValid) { const uint64_t elapsedNs = - (s.errorElapsedTicks * 1000000000ULL + driverTest_.tickHz() / 2U) / + (static_cast(s.errorTriggerTicks) * 1000000000ULL + + driverTest_.tickHz() / 2U) / driverTest_.tickHz(); formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed)); } - if (s.errorDelayValid) { - const uint64_t errorDelayNs = - (static_cast(s.errorDelayTicks) * 1000000000ULL + - driverTest_.tickHz() / 2U) / driverTest_.tickHz(); - formatElapsedNs(errorDelayNs, errorDelay, sizeof(errorDelay)); - } if (s.errorPulseValid) { const uint64_t errorPulseNs = (static_cast(s.errorPulseTicks) * 1000000000ULL + driverTest_.tickHz() / 2U) / driverTest_.tickHz(); formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse)); } - snprintf(two, sizeof(two), "T:%s D:%s P:%s", - elapsed, errorDelay, errorPulse); + if (s.errorPulseValid) + snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse); + else snprintf(two, sizeof(two), "T:%s", elapsed); } else { formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one)); snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT, diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 3ba2bd3..a4d0037 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -145,11 +145,13 @@ constexpr uint32_t DRIVER_ACK_DELAY_NS = 250; constexpr uint32_t DRIVER_ACK_WIDTH_NS = 700; constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000; constexpr uint32_t DRIVER_ACK_MERGE_MARGIN_NS = 250; +// The first MCPWM TX end may belong to a pulse that was already active when +// capture was enabled. The following period also drains capture events that +// were pending independently in the rising/falling channels. Validation +// therefore begins at the third TX period. +constexpr uint8_t DRIVER_CAPTURE_SYNC_CYCLES = 2; // Any response this long is a fault, not a normal acknowledgement. constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500; -// A short circuit is about 9 us. Gate-monitoring may be stretched by an -// overlapping turn-off ACK, but remains shorter on the tested driver. -constexpr uint32_t DRIVER_SHORT_CIRCUIT_MIN_NS = 6000; constexpr uint32_t DRIVER_RX_STUCK_MIN_NS = 20000; // Retained by the generic receiver backend; the driver test itself uses the // stricter ACK start deadline above. diff --git a/OpticalChannelTester/Config_Text.h b/OpticalChannelTester/Config_Text.h index 9272914..1d06d5c 100644 --- a/OpticalChannelTester/Config_Text.h +++ b/OpticalChannelTester/Config_Text.h @@ -37,9 +37,7 @@ constexpr const char *FAIL_NAMES[] = { "НЕТ ОТВЕТА ACK", "ТАЙМИНГ ACK", "АВАРИЯ ДРАЙВЕРА", - "ОТВЕТЫ ACK СЛИЛИСЬ", - "ОШИБКА ЗАТВОРА", - "КОРОТКОЕ ЗАМЫКАНИЕ" + "ОТВЕТЫ ACK СЛИЛИСЬ" }; constexpr const char *MODE_PREFIX = "РЕЖИМ: "; @@ -52,6 +50,8 @@ constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:"; constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; +constexpr const char *LIGHT_AUTO = "АВТО"; +constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: АВТО"; constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:"; constexpr const char *FREQUENCY_UNIT = " Гц"; @@ -103,9 +103,7 @@ constexpr const char *FAIL_NAMES[] = { "ACK MISSING", "ACK TIMING", "DRIVER FAULT", - "ACK MERGED", - "GATE FAULT", - "SHORT CIRCUIT FAULT" + "ACK MERGED" }; constexpr const char *MODE_PREFIX = "MODE: "; @@ -118,6 +116,8 @@ constexpr const char *MENU_ACCURACY = "ACCURACY:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:"; constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:"; constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; +constexpr const char *LIGHT_AUTO = "AUTO"; +constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: AUTO"; constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:"; constexpr const char *FREQUENCY_UNIT = " Hz"; diff --git a/OpticalChannelTester/Core.cpp b/OpticalChannelTester/Core.cpp index 3ab7fbb..90733d6 100644 --- a/OpticalChannelTester/Core.cpp +++ b/OpticalChannelTester/Core.cpp @@ -25,8 +25,7 @@ const char *failName(FailReason r) { static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING", - "DRIVER FAULT", "ACK MERGED", "GATE MONITORING FAULT", - "SHORT CIRCUIT FAULT"}; + "DRIVER FAULT", "ACK MERGED"}; const uint8_t i = static_cast(r); return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN"; } diff --git a/OpticalChannelTester/Core.h b/OpticalChannelTester/Core.h index 8a5fb94..ec93ff2 100644 --- a/OpticalChannelTester/Core.h +++ b/OpticalChannelTester/Core.h @@ -9,8 +9,7 @@ enum class LightCode : uint8_t { HH, HL, LH, LL }; enum class FailReason : uint8_t { NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE, DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED, - ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED, - GATE_MONITOR_FAULT, SHORT_CIRCUIT_FAULT + ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED }; const char *roleName(Role role); diff --git a/OpticalChannelTester/DriverTest.cpp b/OpticalChannelTester/DriverTest.cpp index d42fbae..c02a109 100644 --- a/OpticalChannelTester/DriverTest.cpp +++ b/OpticalChannelTester/DriverTest.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -36,7 +37,7 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs, uint8_t settleCycles, bool activeTxLightOn, bool activeRxLightOn) { (void)tolerancePct; - (void)activeRxLightOn; + (void)settleCycles; if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs || !testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false; @@ -49,8 +50,16 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs, "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; @@ -62,16 +71,16 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs, const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL; pollTxStartRawHigh_ = activeTxRaw == HIGH; - rxActiveRawHigh_ = RX_LIGHT_ON_GPIO_LEVEL == LOW; // ACK/fault = light OFF + rxActiveRawHigh_ = + ((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn); ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS); faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS); - shortCircuitTicks_ = nsToTicks(DRIVER_SHORT_CIRCUIT_MIN_NS); stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS); testTicks_ = static_cast(captureHz_) * testTimeMs / 1000ULL; subsampleTicks_ = testTicks_ / SUBSAMPLE_COUNT; - if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || !ackStartMaxTicks_ || - !faultLongTicks_ || !shortCircuitTicks_ || !stuckTicks_ || + if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || + !ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ || !testTicks_ || !subsampleTicks_) return false; @@ -81,12 +90,23 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs, pendingCount_ = 0; response_ = {}; measurementStartTick_ = deadlineTick_ = 0; - pointOriginTick_ = lastEventTick_ = 0; - settleCycles_ = settleCycles; + 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; @@ -118,6 +138,8 @@ bool DriverTest::armCapture() { state_ = DriverState::IDLE; return false; } + settlingDeadlineUs_ = static_cast(esp_timer_get_time()) + + settlingTimeoutUs_; xTaskNotifyGive(analyzerTask_); return true; } @@ -133,8 +155,11 @@ bool DriverTest::resumeSubsample() { 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; @@ -147,7 +172,7 @@ void DriverTest::pollTaskEntry(void *context) { static_cast(context)->pollTaskLoop(); } -void DriverTest::pollTaskLoop() { +void IRAM_ATTR DriverTest::pollTaskLoop() { constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX); for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); @@ -202,6 +227,10 @@ void DriverTest::pollTaskLoop() { 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; @@ -248,12 +277,24 @@ void DriverTest::analyzerTaskLoop() { 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; @@ -270,12 +311,15 @@ void DriverTest::processSettling(const TimedEvent &event) { } const uint8_t rawLevel = event.rising ? HIGH : LOW; const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL; - if (!lightOn) return; + if (lightOn != txPulseLightOn_) return; if (settledCycles_ < settleCycles_) { ++settledCycles_; return; } - if (rxActive_) return; + if (rxActive_) { + fail(FailReason::DRIVER_FAULT, event.tick); + return; + } state_ = DriverState::RUNNING; measurementStartTick_ = event.tick; const uint64_t measuredBefore = @@ -284,26 +328,35 @@ void DriverTest::processSettling(const TimedEvent &event) { completedSubsamples_ + 1U == SUBSAMPLE_COUNT ? testTicks_ - measuredBefore : subsampleTicks_; deadlineTick_ = event.tick + thisSubsampleTicks; - processTx(event, true); + processTx(event, lightOn); } void DriverTest::processRunning(const TimedEvent &event) { - expirePending(event.tick); - if (state_ != DriverState::RUNNING) return; - if (response_.active && event.tick - response_.startTick >= stuckTicks_) { const uint64_t delay = response_.associated ? response_.startTick - response_.tx.tick : 0; - fail(FailReason::SHORT_CIRCUIT_FAULT, event.tick, delay, - event.tick - response_.startTick); + 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 processRx(event, event.rising == rxActiveRawHigh_); + } 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); } @@ -318,6 +371,10 @@ bool DriverTest::addPending(uint64_t tick, bool lightOn) { void DriverTest::processTx(const TimedEvent &event, bool lightOn) { if (!addPending(event.tick, lightOn)) return; + if (lightOn == txPulseLightOn_) { + lastActiveTxTick_ = event.tick; + haveLastActiveTx_ = true; + } ++stats_.inputEdges; } @@ -358,10 +415,11 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) { 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(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT : - FailReason::GATE_MONITOR_FAULT, - event.tick, 0, width); + fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger); return; } @@ -370,17 +428,19 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) { 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); + 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(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT : - FailReason::GATE_MONITOR_FAULT, - event.tick, delay, width); + fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger); return; } @@ -424,8 +484,11 @@ void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width, void DriverTest::expirePending(uint64_t now) { for (uint8_t i = 0; i < pendingCount_; ++i) { - if (now <= pending_[i].tick + ackStartMaxTicks_) continue; - fail(FailReason::ACK_MISSING, now, now - pending_[i].tick, 0); + 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; } } @@ -456,12 +519,18 @@ void DriverTest::completeIfPossible(uint64_t now) { } void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay, - uint64_t pulseWidth) { + 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); @@ -603,16 +672,20 @@ void DriverTest::printSummary() const { static_cast(publishedStats_.unexpectedResponses), failName(publishedStats_.reason)); if (publishedStats_.reason != FailReason::NONE) { - Log::printf("DRIVER", - "error timing: T=%lluns D=%s%lluns P=%s%lluns", - static_cast( - ticksToNs(publishedStats_.errorElapsedTicks)), - publishedStats_.errorDelayValid ? "" : "N/A/", - static_cast( - ticksToNs(publishedStats_.errorDelayTicks)), - publishedStats_.errorPulseValid ? "" : "N/A/", - static_cast( - ticksToNs(publishedStats_.errorPulseTicks))); + 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); diff --git a/OpticalChannelTester/DriverTest.h b/OpticalChannelTester/DriverTest.h index fc83719..fca035e 100644 --- a/OpticalChannelTester/DriverTest.h +++ b/OpticalChannelTester/DriverTest.h @@ -31,8 +31,10 @@ struct DriverStats { uint32_t droppedItems; uint32_t unexpectedResponses; uint64_t errorElapsedTicks; + uint32_t errorTriggerTicks; uint32_t errorDelayTicks; uint32_t errorPulseTicks; + bool errorTriggerValid; bool errorDelayValid; bool errorPulseValid; DriverEdgeStats turnOn; @@ -59,10 +61,13 @@ class DriverTest { const DriverStats &stats() const { return publishedStats_; } private: - enum class Source : uint8_t { TX, RX }; + enum class Source : uint8_t { TX, RX, WINDOW_END }; struct RawEvent { uint32_t tick; bool rising; Source source; }; struct TimedEvent { uint64_t tick; bool rising; Source source; }; - struct PendingTx { uint64_t tick; bool lightOn; }; + struct PendingTx { + uint64_t tick; + bool lightOn; + }; struct Response { bool active; bool associated; @@ -80,7 +85,7 @@ class DriverTest { static void analyzerTaskEntry(void *context); static void pollTaskEntry(void *context); void analyzerTaskLoop(); - void pollTaskLoop(); + void IRAM_ATTR pollTaskLoop(); void processEvent(const TimedEvent &event); void processSettling(const TimedEvent &event); void processRunning(const TimedEvent &event); @@ -94,7 +99,7 @@ class DriverTest { uint64_t mergeGuardTicks() const; void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn); void fail(FailReason reason, uint64_t tick = 0, uint64_t delay = 0, - uint64_t pulseWidth = 0); + uint64_t pulseWidth = 0, uint64_t triggerAfterTx = 0); void publishStats(); void rememberTrace(const TimedEvent &event); bool armCapture(); @@ -130,10 +135,13 @@ class DriverTest { volatile bool captureReady_ = false; volatile bool core0WdtDisabled_ = false; uint32_t captureHz_ = 0; + uint32_t captureFrequencyHz_ = 0; + uint32_t capturePulseNs_ = 0; uint32_t pollPeriodCycles_ = 0; uint32_t pollWindowBeforeCycles_ = 0; uint32_t pollWindowAfterCycles_ = 0; bool pollTxStartRawHigh_ = false; + bool captureTxLightOn_ = true; portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED; PendingTx pending_[MAX_PENDING] = {}; @@ -141,21 +149,25 @@ class DriverTest { Response response_ = {}; uint64_t ackStartMaxTicks_ = 0; uint64_t faultLongTicks_ = 0; - uint64_t shortCircuitTicks_ = 0; uint64_t stuckTicks_ = 0; uint64_t testTicks_ = 0; uint64_t subsampleTicks_ = 0; + uint64_t settlingTimeoutUs_ = 0; + uint64_t settlingDeadlineUs_ = 0; uint64_t measurementStartTick_ = 0; uint64_t deadlineTick_ = 0; uint64_t pointOriginTick_ = 0; uint64_t lastEventTick_ = 0; + uint64_t lastActiveTxTick_ = 0; uint8_t settleCycles_ = 0; uint8_t settledCycles_ = 0; uint8_t completedSubsamples_ = 0; bool rxActiveRawHigh_ = true; + bool txPulseLightOn_ = true; bool rxActive_ = false; bool measurementClosed_ = false; bool havePointOrigin_ = false; + bool haveLastActiveTx_ = false; bool haveRawTick_ = false; uint32_t lastRawTick_ = 0; uint64_t tickEpoch_ = 0; diff --git a/OpticalChannelTester/Receiver.cpp b/OpticalChannelTester/Receiver.cpp index 7d97afe..dd08b1b 100644 --- a/OpticalChannelTester/Receiver.cpp +++ b/OpticalChannelTester/Receiver.cpp @@ -75,6 +75,7 @@ bool PulseReceiver::begin() { bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn) { + (void)activeLightOn; if (!plannedTickHz(expectedHz, expectedDutyPct)) return false; expectedHz_ = expectedHz; expectedDutyPct_ = expectedDutyPct; @@ -83,11 +84,7 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct, if (!cpuTickHz_) return false; #endif resetStream(); - const bool rawHighMeansLightOn = RX_LIGHT_ON_GPIO_LEVEL == HIGH; - activeStartRising_ = rawHighMeansLightOn == activeLightOn; - Log::printf("CAPTURE", "RX active optical level=%s, raw active starts on %s", - activeLightOn ? "H/light-on" : "L/light-off", - activeStartRising_ ? "RISING" : "FALLING"); + Log::event("CAPTURE", "RX optical polarity will be detected automatically"); return startCapture(false); } @@ -209,6 +206,8 @@ void PulseReceiver::resetStream() { droppedItems_ = 0; polarityKnown_ = false; activeStartRising_ = false; + polarityEdgeCount_ = 0; + memset(polarityEdges_, 0, sizeof(polarityEdges_)); waitingForActiveEnd_ = true; activeStart_ = activeEnd_ = 0; haveRawTick_ = false; @@ -244,12 +243,42 @@ bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) { return true; } - // HH/HL/LH/LL defines the active optical state explicitly. Synchronize on - // its physical starting edge instead of guessing polarity from pulse width. - if (edge.rising != activeStartRising_) return false; + // Optical mode does not use the DRIVER level setting. Compare the first two + // alternating intervals with the configured active duration and select the + // level that is actually present at RX. Three edges are enough to determine + // polarity and, when the first interval is active, produce the first period. + polarityEdges_[polarityEdgeCount_++] = edge; + if (polarityEdgeCount_ < 3U) return false; + + const uint64_t firstInterval = + polarityEdges_[1].tick - polarityEdges_[0].tick; + const uint64_t secondInterval = + polarityEdges_[2].tick - polarityEdges_[1].tick; + const uint64_t expectedPeriod = tickHz() / expectedHz_; + const uint64_t expectedActive = static_cast( + expectedPeriod * expectedDutyPct_ / 100.0f + 0.5f); + const uint64_t firstError = firstInterval > expectedActive ? + firstInterval - expectedActive : expectedActive - firstInterval; + const uint64_t secondError = secondInterval > expectedActive ? + secondInterval - expectedActive : expectedActive - secondInterval; + const bool firstIntervalIsActive = firstError <= secondError; + activeStartRising_ = firstIntervalIsActive ? polarityEdges_[0].rising : + polarityEdges_[1].rising; polarityKnown_ = true; - activeStart_ = edge.tick; - waitingForActiveEnd_ = true; + polarityEdgeCount_ = 0; + + if (firstIntervalIsActive) { + out = {polarityEdges_[0].tick, + static_cast(polarityEdges_[2].tick - polarityEdges_[0].tick), + static_cast(firstInterval), tickHz()}; + activeStart_ = polarityEdges_[2].tick; + waitingForActiveEnd_ = true; + return true; + } + + activeStart_ = polarityEdges_[1].tick; + activeEnd_ = polarityEdges_[2].tick; + waitingForActiveEnd_ = false; return false; } @@ -268,10 +297,11 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel, static_cast(channel == self->txChannel_ ? CaptureSource::TX : CaptureSource::RX)}; if (self->txCaptureEnabled_) { - // Three capture channels are independent ISR producers. Serialize their - // reservation/publication of a ring slot; treating this as an SPSC ring - // loses or duplicates RX events when TX and RX interrupts overlap. - portENTER_CRITICAL_ISR(&self->driverRingMux_); + // All channels in one MCPWM group are dispatched serially by the same + // group ISR. Keep this callback shorter than the minimum interval between + // equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic + // RMW operations here can leave the channel pending until its capture + // register is overwritten by the next edge. if (self->haveLastDriverEdge_ && self->lastDriverEdge_.tick == edge.tick && self->lastDriverEdge_.rising == edge.rising && @@ -279,22 +309,20 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel, // The same channel callback can be delivered twice while several MCPWM // capture status bits are pending. Two physical edges cannot have the // same source, direction and 12.5 ns hardware timestamp. - portEXIT_CRITICAL_ISR(&self->driverRingMux_); return false; } - const uint16_t write = __atomic_load_n( - &self->driverRingWrite_, __ATOMIC_RELAXED); + const uint16_t write = self->driverRingWrite_; const uint16_t next = static_cast( - (write + 1U) % DRIVER_RING_CAPACITY); - if (next == __atomic_load_n(&self->driverRingRead_, __ATOMIC_ACQUIRE)) { - __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); + (write + 1U) & (DRIVER_RING_CAPACITY - 1U)); + if (next == self->driverRingRead_) { + ++self->droppedItems_; } else { self->driverRing_[write] = edge; self->lastDriverEdge_ = edge; self->haveLastDriverEdge_ = true; - __atomic_store_n(&self->driverRingWrite_, next, __ATOMIC_RELEASE); + asm volatile("memw" ::: "memory"); + self->driverRingWrite_ = next; } - portEXIT_CRITICAL_ISR(&self->driverRingMux_); return false; } BaseType_t wake = pdFALSE; @@ -350,7 +378,7 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks) { - if (!events || capacity < 2U || !txCaptureEnabled_ || !driverPulseTicks_ || + if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ || !driverReleaseSlackTicks_) return 0; constexpr size_t MAX_BATCH = 64; @@ -367,30 +395,42 @@ size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity, // can the missing start interrupt be reconstructed and sorted before RX. const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE); uint16_t scan = read; - bool haveTxEnd = false; - uint32_t lastTxEnd = 0; + bool haveLatestTxEnd = false; + bool haveReleaseTxEnd = false; + uint32_t latestTxEnd = 0; + uint32_t releaseTxEnd = 0; size_t projectedCount = 0; while (scan != write) { const Edge &edge = driverRing_[scan]; const size_t needed = edge.source == static_cast(CaptureSource::TX) ? 2U : 1U; - if (projectedCount + needed > limit) break; + // Reserve one output slot for WINDOW_END. + if (projectedCount + needed + 1U > limit) break; projectedCount += needed; if (edge.source == static_cast(CaptureSource::TX)) { - lastTxEnd = edge.tick; - haveTxEnd = true; + if (haveLatestTxEnd) { + releaseTxEnd = latestTxEnd; + haveReleaseTxEnd = true; + } + latestTxEnd = edge.tick; + haveLatestTxEnd = true; } scan = static_cast((scan + 1U) % DRIVER_RING_CAPACITY); } - if (!haveTxEnd) { + // Keep the newest TX period in the ring. Arrival of the following TX end + // proves that the previous end's ACK/fault window has completely elapsed. + if (!haveReleaseTxEnd) { if (waitTicks) vTaskDelay(waitTicks); return 0; } - const uint32_t releaseThrough = lastTxEnd + driverReleaseSlackTicks_; + const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_; size_t count = 0; while (read != write) { const Edge edge = driverRing_[read]; + if (edge.source == static_cast(CaptureSource::TX) && + static_cast(edge.tick - releaseTxEnd) > 0) + break; if (edge.source == static_cast(CaptureSource::RX) && static_cast(edge.tick - releaseThrough) > 0) break; @@ -426,5 +466,9 @@ size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity, events[i] = {timed.tick, timed.rising, static_cast(ordered[i].source)}; } + const Edge marker = {releaseThrough, 0, + static_cast(CaptureSource::WINDOW_END)}; + const TimedEdge timedMarker = extendEdge(marker); + events[count++] = {timedMarker.tick, false, CaptureSource::WINDOW_END}; return count; } diff --git a/OpticalChannelTester/Receiver.h b/OpticalChannelTester/Receiver.h index 3ae90bb..6ed2a4a 100644 --- a/OpticalChannelTester/Receiver.h +++ b/OpticalChannelTester/Receiver.h @@ -12,7 +12,7 @@ #define OPTICAL_USE_MCPWM_CAPTURE 0 #endif -enum class CaptureSource : uint8_t { RX, TX }; +enum class CaptureSource : uint8_t { RX, TX, WINDOW_END }; struct CaptureEvent { uint64_t tick; @@ -43,6 +43,8 @@ class PulseReceiver { private: struct Edge { uint32_t tick; uint8_t rising; uint8_t source; }; static constexpr uint16_t DRIVER_RING_CAPACITY = 512; + static_assert((DRIVER_RING_CAPACITY & (DRIVER_RING_CAPACITY - 1U)) == 0, + "driver capture ring must be a power of two"); struct TimedEdge { uint64_t tick; bool rising; }; bool startCapture(bool withTx); bool consumeEdge(const Edge &edge, PulsePeriod &period); @@ -78,6 +80,8 @@ class PulseReceiver { uint32_t expectedHz_ = 0; float expectedDutyPct_ = 50.0f; bool polarityKnown_ = false, activeStartRising_ = false; + TimedEdge polarityEdges_[3] = {}; + uint8_t polarityEdgeCount_ = 0; bool waitingForActiveEnd_ = true; uint64_t activeStart_ = 0, activeEnd_ = 0; bool haveRawTick_ = false; diff --git a/README.md b/README.md index 292d9b7..920292d 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ - общие для оптического и драйверного тестов частота PWM и диапазон импульсов; - четыре рабочих режима: `SOLO OPTICAL`, `SOLO DRIVER` (только S3), `MASTER OPTICAL`, `SLAVE OPTICAL`; -- настраиваемый оптический код `HH`, `HL`, `LH`, `LL`, не зависящий от - электрической инверсии GPIO; +- настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте + оптики полярность входного сигнала определяется автоматически; - аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса; - Arduino-ESP32 3.3.10: на S3 два аппаратных канала MCPWM Capture из отдельной группы фиксируют RISING и FALLING в независимых регистрах общего 32-битного таймера 80 МГц; - безопасный fallback для старых Arduino-ESP32 3.x через GPIO edge ISR и аппаратный CPU cycle counter (верхний предел при этом автоматически ограничен 100 кГц); @@ -156,7 +156,7 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE **GPIO ESP32 допускают только логические уровни 0…3.3 В.** Не подавайте 5 В на `GPIO_RX`; применяйте согласование уровня. Полярность выхода оптического приёмника задаётся однозначно его активным уровнем: -Физический уровень выхода приёмника при включённом свете задаёт `RX_LIGHT_ON_GPIO_LEVEL`. Вторая буква `HH/HL/LH/LL` выбирает, свет ON (`H`) или OFF (`L`) является активным импульсом принимаемой последовательности. Поэтому полярность не угадывается по длительности и однозначна даже при заполнении 50%. +Физический уровень выхода приёмника при включённом свете задаёт `RX_LIGHT_ON_GPIO_LEVEL`. Код `HH/HL/LH/LL` используется только тестом драйвера. В тесте оптики приёмник сравнивает первые интервалы с ожидаемой длительностью и автоматически выбирает фактически пришедшую полярность; при заполнении 50% выбор стороны не меняет измеренную длительность. ## Управление @@ -178,7 +178,7 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE ### Энергосбережение -В ожидании процессор работает на 80 МГц, во время теста и связанной MASTER/SLAVE-сессии — на 160 МГц. В `SOLO ОПТИКА` и Master передатчик удерживается в активном оптическом состоянии, выбранном первой буквой кода света. В `SOLO ДРАЙВЕР`, Slave, при остановленном PWM и во сне свет всегда принудительно выключен: драйвер не может остаться включённым после теста. После тайм-аута бездействия в `IDLE`, `FINISHED` или `SLAVE READY` OLED выключается, а ESP переходит в непрерывный light sleep без периодических таймерных пробуждений. +В ожидании процессор работает на 80 МГц, во время теста и связанной MASTER/SLAVE-сессии — на 160 МГц. В тесте оптики активный импульс передатчика всегда соответствует свету ON и не зависит от настройки уровней. В `SOLO ДРАЙВЕР`, Slave, при остановленном PWM и во сне свет всегда принудительно выключен: драйвер не может остаться включённым после теста. После тайм-аута бездействия в `IDLE`, `FINISHED` или `SLAVE READY` OLED выключается, а ESP переходит в непрерывный light sleep без периодических таймерных пробуждений. START и MODE будят устройство. Первое, пробуждающее нажатие не выполняет действие и полностью подавляется до отпускания кнопки; действие выполняет только следующее нажатие. Оптический wake-сигнал также немедленно выводит Slave из энергосбережения. Тайм-аут задаётся `IDLE_POWER_SAVE_TIMEOUT_MS` в `Config.h`. @@ -191,7 +191,7 @@ START и MODE будят устройство. Первое, пробуждаю 3. минимальная длительность импульса из отдельного `MIN_PULSE_OPTIONS_NS`; 4. точность; 5. время выборки. -6. код света `HH`, `HL`, `LH`, `LL`. +6. код света `HH`, `HL`, `LH`, `LL` — только для теста драйвера. Экран активного уровня: @@ -200,11 +200,11 @@ START и MODE будят устройство. Первое, пробуждаю TX:H, RX:L ``` -Первая буква задаёт активное состояние света TX, вторая — активное состояние -света RX. Буквы никогда не обозначают уровни GPIO. Для обычной прямой оптики -типичен `HH`; для драйвера 1SP0635, у которого управление выполняется светом ON, -а ACK передаётся светом OFF, типичен `HL`. В MASTER/SLAVE Master передаёт код -света Slave вместе с параметрами этапа. +В тесте драйвера первая буква задаёт активное состояние света TX, вторая — +активное состояние света RX. Буквы никогда не обозначают уровни GPIO. Для +1SP0635, у которого управление выполняется светом ON, а ACK передаётся светом +OFF, типичен `HL`. В режимах оптики этот пункт показывает `АВТО`, кнопки не +изменяют сохранённый код, а входная полярность определяется по принятому PWM. Тест работает на одной выбранной частоте и проходит длительности из списка от максимальной к минимальной. Значения, равные периоду или превышающие его, автоматически исключаются при выборе частоты. На S3 минимальная длительность ограничивается расчётной точностью генератора и MCPWM Capture 80 МГц: длинная пауза не снижает точность короткого импульса. Меню не позволяет задать точку, которой аппаратно не хватает выбранного допуска. Сохранённые индексы и их сочетания проверяются и приводятся к допустимому диапазону. @@ -224,7 +224,7 @@ max(TEST_TIME, periods / RX_PROCESSING_PERIODS_PER_SECOND) В тесте оптики после каждой перенастройки PWM приёмник: -1. применяет явно выбранную полярность света Rx; отдельные каналы MCPWM Capture S3 фиксируют RISING и FALLING в независимых регистрах общего 32-битного таймера 80 МГц; +1. автоматически определяет фактически пришедшую полярность Rx; отдельные каналы MCPWM Capture S3 фиксируют RISING и FALLING в независимых регистрах общего 32-битного таймера 80 МГц; 2. отбрасывает ровно `PWM_SETTLE_CYCLES` полных периодов; 3. очищает статистику; 4. непрерывно держит MCPWM Capture включённым на всём измерительном этапе, проверяет каждый полный импульс ровно один раз и на десяти границах TEST TIME показывает промежуточный результат; границы прогресса не останавливают capture и не выбрасывают пересекающий их импульс. Между длительностями сначала останавливается PWM, затем capture, поэтому очистка очереди не пересекается с ISR. @@ -242,8 +242,8 @@ F:2.000k, P:2.00u вышедшим за допуск: ```text -FAIL AT 2kHz, 200us -PULSE OUT 300.01u +СБОЙ: 2kHz, 200us +ИМПУЛЬС: 300.01u ``` В MASTER/SLAVE передаются выбранная и фактическая длительность импульса и измеренный результат. На принимающей стороне полярность Rx определяется автоматически. @@ -260,10 +260,12 @@ PULSE OUT 300.01u 2 мкс автоматически заменяется на 2 мкс. Частота, MAX, MIN и время точки остаются общими настраиваемыми параметрами. -Реальные фронты `PIN_TX` и `PIN_RX` фиксируются прямым GPIO sampling в -выделенной высокоприоритетной задаче ядра 0 по общему 240-МГц счётчику CPU. -Ни один фронт TX не восстанавливается расчётом. Анализ выполняется отдельной -задачей ядра 1. +Фронты `PIN_RX` и конец каждого импульса `PIN_TX` фиксируются аппаратным +MCPWM Capture по общему 80-МГц таймеру с шагом 12,5 нс. Начало TX +восстанавливается от аппаратно захваченного конца и фактической длительности, +запрограммированной в MCPWM. Период выдаётся анализатору с задержкой в один +цикл: следующий захваченный TX гарантирует, что оба 700-нс фронта предыдущего +ACK уже находятся в аппаратном буфере. Для каждого фронта TX ожидается активный ответ RX. Паспортные 250 нс до начала ACK и 700 нс его длительности используются как справочные величины, но не как @@ -275,14 +277,19 @@ ACK и 700 нс его длительности используются как На OLED в режиме драйвера первая строка показывает параметры текущей точки `ТЕСТ: частота, импульс`, а вторая — измеренную задержку от фронта TX до начала ответа и длительность ответа в формате `D: ..., P: ...`. -Отсутствие ответа в течение 10 мкс даёт `НЕТ ОТВЕТА ACK`. Отдельный импульс +Отсутствие начала ответа до предела 2 мкс даёт `НЕТ ОТВЕТА ACK`; проверка +выполняется сразу для первого пропущенного фронта, без ожидания конца всей +выборки. Отдельный импульс статуса драйвера и ответ, остающийся активным дольше 1,5 мкс, считаются `АВАРИЯ ДРАЙВЕРА`. -Для каждого фронта TX всегда требуется отдельный полный ACK. Перед началом -измерения анализатор пропускает заданные циклы установления и ещё один тайм-аут -ACK, после чего начинает проверку с нового фронта TX. Ответ с границы запуска -не используется для синхронизации и не попадает в рабочую статистику. +Для каждого проверяемого фронта TX всегда требуется отдельный полный ACK. При +включении MCPWM Capture первый TX-импульс может уже идти: его конец будет +захвачен, но начало ACK могло произойти раньше включения периферии. Поэтому +два первых восстановленных периода используются только для синхронизации и +освобождения отложенных событий независимых каналов фронтов. Проверка +начинается с третьего периода; при 500 Гц это добавляет 4 мс, а не ожидание +общего тайм-аута. Время каждой точки делится на 10 равных подвыборок. Во время подвыборки OLED и Serial не обновляются. После каждой подвыборки захват и PWM останавливаются, @@ -295,8 +302,9 @@ I2C-обновления дисплея. При ошибке русская локализация не использует английские фразы `FAIL AT`, `TEST`, `FREQ OUT` или `PULSE OUT`. Первая строка показывает причину по-русски, -вторая — `T` (время от начала точки до ошибки), `D` (задержку ошибочного ответа, -если она определима) и `P` (длительность ошибочного RX-импульса). Для +вторая — `T` (время от вызвавшего аварию фронта TX до начала аварийного +RX-импульса), `D` (задержку ошибочного ответа, если она определима) и `P` +(длительность ошибочного RX-импульса). Для отсутствующего ACK значение `P` выводится как `---`. Точные значения в наносекундах дополнительно записываются в Serial-журнал. @@ -326,7 +334,7 @@ VBUS. 5. Для первого опыта оставьте defaults: 2 кГц, импульсы 200…2 мкс, accuracy 5%. Полярность Rx определяется автоматически. 6. Коротко нажмите START. 7. Serial покажет запрошенные и фактические параметры PWM, список длительностей, ALL и статистику каждой точки. -8. Успех: `PASS ...` / `START=AGAIN`. Ошибка: `FAIL AT ...` и точная причина. +8. Успех: `ТЕСТ ПРОЙДЕН`. Ошибка: русская причина и строка таймингов. ## Проверка проекта