From 040853e7dc3b2d5dbb33cba76742b76c869a5cc4 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Thu, 24 Sep 2026 07:58:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=81=D1=8F=D0=BA=D0=B8=D0=B5=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B5=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=BD=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpticalChannelTester/App.cpp | 87 ++++++- OpticalChannelTester/App.h | 4 +- OpticalChannelTester/Config.h | 7 +- OpticalChannelTester/Config_Text.h | 6 +- OpticalChannelTester/Core.cpp | 4 +- OpticalChannelTester/Core.h | 2 +- OpticalChannelTester/DriverTest.cpp | 7 +- OpticalChannelTester/OpticalChannelTester.ino | 6 + OpticalChannelTester/Receiver.cpp | 25 ++ OpticalChannelTester/Receiver.h | 2 + OpticalChannelTester/SettingsStore.cpp | 8 +- OpticalChannelTester/SignalProbe.cpp | 236 ++++++++++++++++++ OpticalChannelTester/SignalProbe.h | 49 ++++ README.md | 45 +++- 14 files changed, 456 insertions(+), 32 deletions(-) create mode 100644 OpticalChannelTester/SignalProbe.cpp create mode 100644 OpticalChannelTester/SignalProbe.h diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index 755e9e7..c1498cb 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -19,7 +19,7 @@ constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7; const char *uiFailName(FailReason reason); const char *appStateName(AppState state) { - static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER", + static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "PWM_OUTPUT", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER", "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START", "SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"}; const uint8_t index = static_cast(state); @@ -250,9 +250,12 @@ uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) { return value <= first ? last : static_cast(value - 1U); } -uint8_t nextMenuItem(uint8_t current, TestGroup group) { +uint8_t nextMenuItem(uint8_t current, TestGroup group, TestKind kind) { if (group == TestGroup::BOARD) return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U; + if (kind == TestKind::PWM_OUTPUT) + return current == 0U ? 1U : current == 1U ? 2U + : current == 2U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U; return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U : static_cast(current + 1U); } @@ -356,6 +359,12 @@ void App::update() { } return; } + if (state_ == AppState::PWM_OUTPUT) { + if (startEvent == ButtonEvent::LONG) stopPwmOutput(); + else if (modeEvent != ButtonEvent::NONE) + Log::event("ACTION", "MODE ignored while PWM output is active"); + return; + } if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED && startEvent == ButtonEvent::LONG) { abortTest(); return; } if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED && @@ -399,7 +408,8 @@ void App::update() { if (modeEvent == ButtonEvent::SHORT) { leaveOpticalCalibration(); menuItem_ = nextMenuItem(menuItem_, - static_cast(settings_.testGroup)); + static_cast(settings_.testGroup), + static_cast(settings_.testKind)); Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); } @@ -529,12 +539,12 @@ void App::printSerialHelp() { Serial.println(" set group optics|board"); Serial.println(" set boardtest adc|pwm|rx"); Serial.println(" set role solo|master|slave"); - Serial.println(" set test optical|driver"); - Serial.println(" set frequency 500|1000|2000|5000|10000|25000"); - Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000"); + Serial.println(" set test optical|driver|pwm (driver/pwm: SOLO only)"); + Serial.println(" set frequency 500|1000|2000|5000|10000"); + Serial.println(" set max 2000|5000|10000|50000|100000|500000 (PWM pulse in SOLO PWM)"); 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 time 100|250|500|1000|2000|5000|60000 (ms)"); Serial.println(" set light HH|HL|LH|LL (DRIVER only)"); } @@ -605,6 +615,9 @@ void App::handleSerialCommand(char *line) { else if (state_ == AppState::BOARD_TEST) { Serial.println("OK board test stopped"); stopBoardTest(); + } else if (state_ == AppState::PWM_OUTPUT) { + Serial.println("OK PWM output stopped"); + stopPwmOutput(); } else if (state_ == AppState::MENU) { leaveOpticalCalibration(); state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed"); @@ -659,6 +672,9 @@ void App::handleSerialCommand(char *line) { else if (!strcmp(value, "driver") && !TARGET_IS_C3 && static_cast(settings_.role) == Role::SOLO) { settings_.testKind = static_cast(TestKind::DRIVER); accepted = true; + } else if (!strcmp(value, "pwm") && + static_cast(settings_.role) == Role::SOLO) { + settings_.testKind = static_cast(TestKind::PWM_OUTPUT); accepted = true; } } else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) { const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric); @@ -705,6 +721,8 @@ void App::cycleRunMode() { const TestKind kind = static_cast(settings_.testKind); if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) { settings_.testKind = static_cast(TestKind::DRIVER); + } else if (role == Role::SOLO && kind != TestKind::PWM_OUTPUT) { + settings_.testKind = static_cast(TestKind::PWM_OUTPUT); } else if (role == Role::SOLO) { settings_.role = static_cast(Role::MASTER); settings_.testKind = static_cast(TestKind::OPTICAL); @@ -725,11 +743,12 @@ void App::sanitizeRange() { settings_.boardTest = static_cast(defaultBoardTest()); if (settings_.role > static_cast(Role::SLAVE)) settings_.role = static_cast(Role::SOLO); - if (settings_.testKind > static_cast(TestKind::DRIVER)) + if (settings_.testKind > static_cast(TestKind::PWM_OUTPUT)) settings_.testKind = static_cast(TestKind::OPTICAL); if (settings_.lightCode > static_cast(LightCode::LL)) settings_.lightCode = static_cast(LightCode::HH); - if (settings_.role != static_cast(Role::SOLO) || + if ((settings_.role != static_cast(Role::SOLO) && + settings_.testKind != static_cast(TestKind::OPTICAL)) || (TARGET_IS_C3 && settings_.testKind == static_cast(TestKind::DRIVER))) settings_.testKind = static_cast(TestKind::OPTICAL); settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ); @@ -760,8 +779,8 @@ void App::changeMenu(int d) { } else if (menuItem_ == 2) { const uint8_t last = lastValidMaxPulseIndex( PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]); - const uint8_t first = firstMaxPulseIndexAtLeast( - MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last); + const uint8_t first = static_cast(settings_.testKind) == TestKind::PWM_OUTPUT + ? 0U : firstMaxPulseIndexAtLeast(MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last); settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex, first, last, d); } else if (menuItem_ == 3) { @@ -811,7 +830,8 @@ void App::showMenu() { break; case 2: Display::formatPulse(params_.maxPulseNs, value, sizeof(value)); - label = UiText::MENU_MAX_PULSE; + label = static_cast(settings_.testKind) == TestKind::PWM_OUTPUT + ? UiText::MENU_PWM_PULSE : UiText::MENU_MAX_PULSE; break; case 3: Display::formatPulse(params_.minPulseNs, value, sizeof(value)); @@ -847,7 +867,8 @@ void App::showMenu() { default: return; } formatMenuLine(label, value, one, sizeof(one)); - if (static_cast(settings_.testGroup) == TestGroup::BOARD) + if (static_cast(settings_.testGroup) == TestGroup::BOARD || + static_cast(settings_.testKind) == TestKind::PWM_OUTPUT) snprintf(total, sizeof(total), "%s", UiText::BOARD_READY); else formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total)); @@ -900,6 +921,10 @@ void App::startTest() { startBoardTest(); return; } + if (static_cast(settings_.testKind) == TestKind::PWM_OUTPUT) { + startPwmOutput(); + return; + } params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs); stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; @@ -1019,6 +1044,37 @@ void App::stopBoardTest() { showIdle(); } +void App::startPwmOutput() { + params_ = store_.params(settings_); + requestedHz_ = params_.frequencyHz; + requestedPulseNs_ = params_.maxPulseNs; + stageIndex_ = 0; + stageCount_ = 1; + pwm_.configureActiveLight(true); + if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) { + Log::printf("PWM", "start failed: frequency=%luHz pulse=%luns", + requestedHz_, requestedPulseNs_); + finish(false, FailReason::RESOLUTION); + return; + } + state_ = AppState::PWM_OUTPUT; + char target[32], one[48]; + formatTarget(actual_.actualHz, actual_.actualPulseNs, target, sizeof(target)); + snprintf(one, sizeof(one), "%s: %s", uiTestName(TestKind::PWM_OUTPUT), target); + display_.show(one, UiText::BOARD_STOP); + Log::printf("PWM", "continuous output GPIO=%u requested=%luHz/%luns actual=%luHz/%luns duty=%.2f%%", + GPIO_PWM, requestedHz_, requestedPulseNs_, actual_.actualHz, + actual_.actualPulseNs, actual_.actualDutyPct); +} + +void App::stopPwmOutput() { + Log::event("PWM", "continuous output stopped"); + pwm_.stop(); + state_ = AppState::IDLE; + setActivePerformance(false); + showIdle(); +} + bool App::armSlave(bool preserveDisplay) { setActivePerformance(false); pwm_.stop(); @@ -1652,6 +1708,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); + if (static_cast(settings_.testKind) == TestKind::PWM_OUTPUT) { + Serial.printf("Continuous PWM output %lu Hz, pulse %lu ns; RX unused\n", + params_.frequencyHz, params_.maxPulseNs); + return; + } 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", diff --git a/OpticalChannelTester/App.h b/OpticalChannelTester/App.h index 504a21c..37429b5 100644 --- a/OpticalChannelTester/App.h +++ b/OpticalChannelTester/App.h @@ -9,7 +9,7 @@ #include "SettingsStore.h" enum class AppState : uint8_t { - IDLE, MENU, BOARD_TEST, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY, + IDLE, MENU, BOARD_TEST, PWM_OUTPUT, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY, MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE, SLAVE_WAIT_ACK, FINISHED }; @@ -33,6 +33,8 @@ class App { void startBoardTest(); void updateBoardTest(uint32_t now); void stopBoardTest(); + void startPwmOutput(); + void stopPwmOutput(); bool armSlave(bool preserveDisplay = false); bool prepareStage(bool showProgress = true); bool startLocalMeasurement(float hz, float duty); diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 82d53de..300f443 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -2,18 +2,21 @@ #include +// Uncomment to build the standalone PWM/RX pulse probe instead of the tester UI. +#define SIGNAL_PROBE_FIRMWARE + // ------------------------- Hardware configuration ------------------------- // Enabled for the hand-wired prototype. Comment out for the production PCB. // Both profiles map S3 signals by physical header position with 5V/GND aligned. -//#define MAKETKA +#define MAKETKA // Select exactly one populated receiver circuit. Use // BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX. #define BOARD_RX_INTERFACE_ADC 1 #define BOARD_RX_INTERFACE_DIGITAL 2 #ifndef BOARD_RX_INTERFACE -#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC +#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL #endif #if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \ diff --git a/OpticalChannelTester/Config_Text.h b/OpticalChannelTester/Config_Text.h index 687b082..bac1355 100644 --- a/OpticalChannelTester/Config_Text.h +++ b/OpticalChannelTester/Config_Text.h @@ -18,7 +18,7 @@ constexpr const char *ROLE_NAMES[] = { }; constexpr const char *TEST_NAMES[] = { - "ОПТИКА", "ДРАЙВЕР" + "ОПТИКА", "ДРАЙВЕР", "ШИМ" }; constexpr const char *TEST_GROUP_NAMES[] = { @@ -54,6 +54,7 @@ constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ"; constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:"; constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:"; constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:"; +constexpr const char *MENU_PWM_PULSE = "ИМПУЛЬС ШИМ:"; constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:"; constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; @@ -97,7 +98,7 @@ constexpr const char *ROLE_NAMES[] = { }; constexpr const char *TEST_NAMES[] = { - "OPTICAL", "DRIVER" + "OPTICAL", "DRIVER", "PWM" }; constexpr const char *TEST_GROUP_NAMES[] = { @@ -133,6 +134,7 @@ constexpr const char *START_RUN = "READY TO START"; constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:"; constexpr const char *MENU_TEST_GROUP = "TEST GROUP:"; constexpr const char *MENU_MAX_PULSE = "MAX PULSE:"; +constexpr const char *MENU_PWM_PULSE = "PWM PULSE:"; constexpr const char *MENU_MIN_PULSE = "MIN PULSE:"; constexpr const char *MENU_ACCURACY = "ACCURACY:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:"; diff --git a/OpticalChannelTester/Core.cpp b/OpticalChannelTester/Core.cpp index 51534f7..fe12739 100644 --- a/OpticalChannelTester/Core.cpp +++ b/OpticalChannelTester/Core.cpp @@ -16,9 +16,9 @@ const char *testGroupName(TestGroup group) { } const char *testKindName(TestKind kind) { - static const char *names[] = {"OPTICAL", "DRIVER"}; + static const char *names[] = {"OPTICAL", "DRIVER", "PWM OUTPUT"}; const uint8_t i = static_cast(kind); - return i < 2 ? names[i] : "?"; + return i < 3 ? names[i] : "?"; } const char *boardTestName(BoardTest test) { diff --git a/OpticalChannelTester/Core.h b/OpticalChannelTester/Core.h index d1d5017..5c42d73 100644 --- a/OpticalChannelTester/Core.h +++ b/OpticalChannelTester/Core.h @@ -5,7 +5,7 @@ enum class Role : uint8_t { SOLO, MASTER, SLAVE }; enum class TestGroup : uint8_t { OPTICS, BOARD }; -enum class TestKind : uint8_t { OPTICAL, DRIVER }; +enum class TestKind : uint8_t { OPTICAL, DRIVER, PWM_OUTPUT }; enum class BoardTest : uint8_t { ADC, PWM_OUTPUT, RX_INPUT }; enum class LightCode : uint8_t { HH, HL, LH, LL }; enum class FailReason : uint8_t { diff --git a/OpticalChannelTester/DriverTest.cpp b/OpticalChannelTester/DriverTest.cpp index 583635e..1d33a65 100644 --- a/OpticalChannelTester/DriverTest.cpp +++ b/OpticalChannelTester/DriverTest.cpp @@ -8,7 +8,8 @@ #include #include #include -#include +#include +#include #include #if CONFIG_IDF_TARGET_ESP32C3 #include @@ -204,7 +205,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() { constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX); for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - uint32_t levels = GPIO.in.val & PIN_MASK; + uint32_t levels = REG_READ(GPIO_IN_REG) & PIN_MASK; uint32_t nextStart = 0; uint32_t windowEnd = 0; uint32_t lastTxStart = 0; @@ -216,7 +217,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() { uint32_t interruptState = 0; auto sampleOnce = [&]() { - const uint32_t current = GPIO.in.val & PIN_MASK; + const uint32_t current = REG_READ(GPIO_IN_REG) & PIN_MASK; if (current == levels) return; const uint32_t now = esp_cpu_get_cycle_count(); const uint32_t changed = current ^ levels; diff --git a/OpticalChannelTester/OpticalChannelTester.ino b/OpticalChannelTester/OpticalChannelTester.ino index 3518534..09d53d7 100644 --- a/OpticalChannelTester/OpticalChannelTester.ino +++ b/OpticalChannelTester/OpticalChannelTester.ino @@ -1,5 +1,11 @@ +#include "Config.h" +#ifdef SIGNAL_PROBE_FIRMWARE +#include "SignalProbe.h" +SignalProbe app; +#else #include "App.h" App app; +#endif void setup() { app.begin(); } void loop() { app.update(); } diff --git a/OpticalChannelTester/Receiver.cpp b/OpticalChannelTester/Receiver.cpp index dd08b1b..591fa9c 100644 --- a/OpticalChannelTester/Receiver.cpp +++ b/OpticalChannelTester/Receiver.cpp @@ -88,6 +88,19 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct, return startCapture(false); } +bool PulseReceiver::startRaw() { + if (!queue_ || running_) return false; +#if OPTICAL_USE_MCPWM_CAPTURE + if (!captureTimer_) return false; +#endif +#if !OPTICAL_USE_MCPWM_CAPTURE + cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; + if (!cpuTickHz_) return false; +#endif + resetStream(); + return startCapture(false); +} + #if OPTICAL_USE_MCPWM_CAPTURE bool PulseReceiver::configureDriverTxCapture(bool risingEdge) { if (running_) return false; @@ -376,6 +389,18 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, return count; } +size_t PulseReceiver::readRawEdges(CaptureEvent *events, size_t capacity, + TickType_t waitTicks) { + if (!events || !capacity) return 0; + size_t count = 0; + Edge edge = {}; + while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) { + const TimedEdge timed = extendEdge(edge); + events[count++] = {timed.tick, timed.rising, CaptureSource::RX}; + } + return count; +} + size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks) { if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ || diff --git a/OpticalChannelTester/Receiver.h b/OpticalChannelTester/Receiver.h index 6ed2a4a..fc6b33c 100644 --- a/OpticalChannelTester/Receiver.h +++ b/OpticalChannelTester/Receiver.h @@ -24,11 +24,13 @@ class PulseReceiver { public: bool begin(); bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn); + bool startRaw(); bool startDriver(uint32_t frequencyHz, uint32_t pulseNs, bool activeTxLightOn); void stop(); void resetStream(); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); + size_t readRawEdges(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0); size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0); uint32_t takeDroppedItems(); uint32_t tickHz() const; diff --git a/OpticalChannelTester/SettingsStore.cpp b/OpticalChannelTester/SettingsStore.cpp index f4f8fe5..47f9e1d 100644 --- a/OpticalChannelTester/SettingsStore.cpp +++ b/OpticalChannelTester/SettingsStore.cpp @@ -8,10 +8,10 @@ namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; } void SettingsStore::defaults(Settings &s) const { - // 2 kHz, 200 us .. 2 us, 5%, 1 s. + // 2 kHz, 100 us .. 2 us, 5%, 1 s. s = {SETTINGS_VERSION, static_cast(Role::SOLO), static_cast(TestKind::OPTICAL), static_cast(LightCode::HH), - 2, 6, 3, 2, 3, static_cast(TestGroup::OPTICS), + 2, 4, 3, 2, 3, static_cast(TestGroup::OPTICS), static_cast(BoardTest::ADC), 0}; s.checksum = settingsChecksum(s); } @@ -20,9 +20,9 @@ bool SettingsStore::valid(const Settings &s) const { return s.version == SETTINGS_VERSION && s.role <= static_cast(Role::SLAVE) && s.testGroup <= static_cast(TestGroup::BOARD) && s.boardTest <= static_cast(BoardTest::RX_INPUT) && - s.testKind <= static_cast(TestKind::DRIVER) && + s.testKind <= static_cast(TestKind::PWM_OUTPUT) && s.lightCode <= static_cast(LightCode::LL) && - (s.testKind != static_cast(TestKind::DRIVER) || + (s.testKind == static_cast(TestKind::OPTICAL) || s.role == static_cast(Role::SOLO)) && s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) && s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) && diff --git a/OpticalChannelTester/SignalProbe.cpp b/OpticalChannelTester/SignalProbe.cpp new file mode 100644 index 0000000..6e53c60 --- /dev/null +++ b/OpticalChannelTester/SignalProbe.cpp @@ -0,0 +1,236 @@ +#include "SignalProbe.h" +#include "Config.h" +#include + +namespace { +constexpr uint32_t DISPLAY_INTERVAL_MS = 250; +constexpr uint8_t PERIOD_TOLERANCE_PCT = 10; + +bool pwmPointAvailable(uint32_t frequencyHz, uint32_t pulseNs) { + return static_cast(frequencyHz) * pulseNs < 1000000000ULL; +} + +void formatWidth(uint64_t nanoseconds, char *out, size_t size) { + if (nanoseconds < 1000ULL) + snprintf(out, size, "%lluns", nanoseconds); + else if (nanoseconds < 1000000ULL) + snprintf(out, size, "%.2fus", nanoseconds / 1000.0); + else + snprintf(out, size, "%.2fms", nanoseconds / 1000000.0); +} + +void formatPulseCount(uint64_t count, char *out, size_t size) { + if (count < 1000000ULL) snprintf(out, size, "%llu", count); + else if (count < 1000000000ULL) snprintf(out, size, "%lluM", count / 1000000ULL); + else snprintf(out, size, "%lluG", count / 1000000000ULL); +} +} + +SignalProbe::SignalProbe() : startButton_(GPIO_BUTTON_START), + modeButton_(GPIO_BUTTON_MODE) {} + +void SignalProbe::begin() { + Serial.begin(SERIAL_BAUD); +#if ARDUINO_USB_CDC_ON_BOOT + Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS); +#endif + setCpuFrequencyMhz(160); + startButton_.begin(); + modeButton_.begin(); + pwm_.begin(); + pwm_.configureActiveLight(true); + display_.begin(); + receiverInitialized_ = receiver_.begin(); + rxReady_ = receiverInitialized_ && captureStart(); + applyPwm(); + Serial.printf("Signal probe: MODE short=frequency, MODE long=pulse, START short=reset RX minimum, START long=PWM on/off, hold both=RX active level; RX=%s\n", + rxActiveHigh_ ? "HIGH" : "LOW"); + show(); +} + +bool SignalProbe::captureStart() { + if (!receiver_.startRaw()) { + Serial.println("RX capture start failed"); + return false; + } + havePulseStart_ = false; + havePulseEnd_ = false; + extraEdgeInPeriod_ = false; + return true; +} + +void SignalProbe::resetMinimum() { + if (rxReady_) receiver_.stop(); + rxReady_ = receiverInitialized_ && captureStart(); + minTicks_ = UINT64_MAX; + pulseCount_ = 0; + edgeCount_ = 0; + rejectedPeriods_ = 0; + displayDirty_ = true; + Serial.println("RX minimum reset"); +} + +void SignalProbe::updateCapture() { + if (!rxReady_) return; + CaptureEvent edges[64]; + for (uint8_t batch = 0; batch < 8; ++batch) { + const size_t count = receiver_.readRawEdges(edges, countOf(edges)); + if (!count) break; + uint64_t batchMinimum = minTicks_; + uint64_t batchPulses = 0; + uint64_t batchRejected = 0; + const bool activeRising = rxActiveHigh_; + const uint32_t expectedHz = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; + const uint64_t expectedPeriod = receiver_.tickHz() / expectedHz; + for (size_t i = 0; i < count; ++i) { + if (edges[i].rising == activeRising) { + if (havePulseStart_) { + const uint64_t period = edges[i].tick - pulseStartTick_; + const uint64_t width = havePulseEnd_ + ? pulseEndTick_ - pulseStartTick_ : 0; + const uint64_t error = period > expectedPeriod + ? period - expectedPeriod : expectedPeriod - period; + if (havePulseEnd_ && !extraEdgeInPeriod_ && period && width && width < period && + error * 100U <= expectedPeriod * PERIOD_TOLERANCE_PCT) { + if (width < batchMinimum) batchMinimum = width; + ++batchPulses; + } else { + ++batchRejected; + } + } + pulseStartTick_ = edges[i].tick; + havePulseStart_ = true; + havePulseEnd_ = false; + extraEdgeInPeriod_ = false; + } else if (havePulseStart_ && !havePulseEnd_ && + edges[i].tick > pulseStartTick_) { + pulseEndTick_ = edges[i].tick; + havePulseEnd_ = true; + } else if (havePulseStart_) { + extraEdgeInPeriod_ = true; + } + } + const uint32_t dropped = receiver_.takeDroppedItems(); + if (dropped) { + Serial.printf("RX capture overflow: %lu edges lost; restarting capture\n", dropped); + receiver_.stop(); + rxReady_ = captureStart(); + displayDirty_ = true; + return; + } + edgeCount_ += count; + pulseCount_ += batchPulses; + rejectedPeriods_ += batchRejected; + displayDirty_ = true; + if (batchMinimum < minTicks_) { + minTicks_ = batchMinimum; + displayDirty_ = true; + } + } +} + +void SignalProbe::applyPwm() { + pwm_.stop(); + if (!pwmEnabled_) { + Serial.println("PWM off"); + displayDirty_ = true; + return; + } + const uint32_t frequency = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; + const uint32_t pulse = MAX_PULSE_OPTIONS_NS[pulseIndex_]; + if (!pwm_.start(frequency, pulse, actual_)) { + Serial.printf("PWM start failed: %lu Hz, %lu ns\n", frequency, pulse); + pwmEnabled_ = false; + } else { + Serial.printf("PWM GPIO=%u requested=%luHz/%luns actual=%luHz/%luns\n", + GPIO_PWM, frequency, pulse, actual_.actualHz, actual_.actualPulseNs); + } + displayDirty_ = true; +} + +void SignalProbe::advanceFrequency() { + const uint8_t count = static_cast(countOf(PWM_FREQUENCY_OPTIONS_HZ)); + for (uint8_t step = 0; step < count; ++step) { + frequencyIndex_ = static_cast((frequencyIndex_ + 1U) % count); + if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_], + MAX_PULSE_OPTIONS_NS[pulseIndex_])) break; + } + applyPwm(); + resetMinimum(); +} + +void SignalProbe::advancePulse() { + const uint8_t count = static_cast(countOf(MAX_PULSE_OPTIONS_NS)); + for (uint8_t step = 0; step < count; ++step) { + pulseIndex_ = static_cast((pulseIndex_ + 1U) % count); + if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_], + MAX_PULSE_OPTIONS_NS[pulseIndex_])) break; + } + applyPwm(); + resetMinimum(); +} + +void SignalProbe::show() { + char frequency[16], pulse[16], first[64], second[64]; + const uint32_t shownFrequency = pwm_.running() ? actual_.actualHz + : PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; + const uint32_t shownPulse = pwm_.running() ? actual_.actualPulseNs + : MAX_PULSE_OPTIONS_NS[pulseIndex_]; + Display::formatPwmFrequency(shownFrequency, + frequency, sizeof(frequency)); + Display::formatPulse(shownPulse, pulse, sizeof(pulse)); + snprintf(first, sizeof(first), "PWM%s %s %s", pwmEnabled_ ? "" : " OFF", + frequency, pulse); + if (!rxReady_) { + snprintf(second, sizeof(second), "RX: ERROR"); + } else if (minTicks_ == UINT64_MAX) { + snprintf(second, sizeof(second), "RX E:%llu BAD:%llu", edgeCount_, rejectedPeriods_); + } else { + char width[24], count[12]; + const uint64_t nanoseconds = + (minTicks_ * 1000000000ULL + receiver_.tickHz() / 2U) / receiver_.tickHz(); + formatWidth(nanoseconds, width, sizeof(width)); + formatPulseCount(pulseCount_, count, sizeof(count)); + snprintf(second, sizeof(second), "MIN:%s N:%s", width, count); + Serial.printf("RX minimum=%lluns, periods=%llu, rejected=%llu, edges=%llu\n", + nanoseconds, pulseCount_, rejectedPeriods_, edgeCount_); + } + display_.show(first, second, 0, 0, rxActiveHigh_ ? "H" : "L"); + lastDisplayMs_ = millis(); + displayDirty_ = false; +} + +void SignalProbe::update() { + const uint32_t now = millis(); + const ButtonEvent start = startButton_.update(now); + const ButtonEvent mode = modeButton_.update(now); + if (startButton_.pressed() && modeButton_.pressed()) { + if (!bothHeld_) { + bothHeld_ = true; + bothHeldSinceMs_ = now; + startButton_.suppressUntilRelease(); + modeButton_.suppressUntilRelease(); + } + if (!bothHeldHandled_ && now - bothHeldSinceMs_ >= BUTTON_LONG_PRESS_MS) { + rxActiveHigh_ = !rxActiveHigh_; + bothHeldHandled_ = true; + resetMinimum(); + Serial.printf("RX active level=%s\n", rxActiveHigh_ ? "HIGH" : "LOW"); + } + } else if (bothHeld_) { + if (!startButton_.pressed() && !modeButton_.pressed()) { + bothHeld_ = false; + bothHeldHandled_ = false; + } + } else { + if (mode == ButtonEvent::SHORT) advanceFrequency(); + else if (mode == ButtonEvent::LONG) advancePulse(); + if (start == ButtonEvent::SHORT) resetMinimum(); + else if (start == ButtonEvent::LONG) { + pwmEnabled_ = !pwmEnabled_; + applyPwm(); + } + } + updateCapture(); + if (displayDirty_ && now - lastDisplayMs_ >= DISPLAY_INTERVAL_MS) show(); +} diff --git a/OpticalChannelTester/SignalProbe.h b/OpticalChannelTester/SignalProbe.h new file mode 100644 index 0000000..4eb00bd --- /dev/null +++ b/OpticalChannelTester/SignalProbe.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Config.h" +#include "Buttons.h" +#include "Display.h" +#include "Pwm.h" +#include "Receiver.h" + +class SignalProbe { + public: + SignalProbe(); + void begin(); + void update(); + + private: + bool captureStart(); + void resetMinimum(); + void updateCapture(); + void advanceFrequency(); + void advancePulse(); + void applyPwm(); + void show(); + + Button startButton_, modeButton_; + Display display_; + PwmGenerator pwm_; + PulseReceiver receiver_; + ActualPwm actual_ = {}; + uint8_t frequencyIndex_ = 2; + uint8_t pulseIndex_ = 0; + bool pwmEnabled_ = true; + bool rxActiveHigh_ = RX_LIGHT_ON_GPIO_LEVEL == HIGH; + bool receiverInitialized_ = false; + bool rxReady_ = false; + bool havePulseStart_ = false; + bool havePulseEnd_ = false; + bool extraEdgeInPeriod_ = false; + bool displayDirty_ = true; + uint64_t pulseStartTick_ = 0; + uint64_t pulseEndTick_ = 0; + uint64_t minTicks_ = UINT64_MAX; + uint64_t pulseCount_ = 0; + uint64_t edgeCount_ = 0; + uint64_t rejectedPeriods_ = 0; + uint32_t lastDisplayMs_ = 0; + uint32_t bothHeldSinceMs_ = 0; + bool bothHeld_ = false; + bool bothHeldHandled_ = false; +}; diff --git a/README.md b/README.md index ef2aa23..9cb9d9a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ - неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`; - общие для оптического и драйверного тестов частота PWM и диапазон импульсов; -- четыре рабочих режима: `SOLO OPTICAL`, `SOLO DRIVER` (только S3), - `MASTER OPTICAL`, `SLAVE OPTICAL`; +- пять рабочих режимов: `SOLO OPTICAL`, `SOLO DRIVER` (только S3), + `SOLO PWM`, `MASTER OPTICAL`, `SLAVE OPTICAL`; - настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте оптики полярность входного сигнала определяется автоматически; - аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса; @@ -40,7 +40,8 @@ Arduino sketch находится в каталоге `OpticalChannelTester`: - `Receiver.*` — MCPWM Capture на S3 / совместимый GPIO fallback; - `Measurement.*` — строгая проверка периодов; - `Protocol.*`, `Radio.*` — ESP-NOW; -- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE. +- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE; +- `SignalProbe.*` — тестовый вариант прошивки PWM/Rx. ## Требования Arduino IDE @@ -187,11 +188,17 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE В ожидании в группе `ОПТИКА` на S3: -- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → MASTER ОПТИКА → SLAVE ОПТИКА`; +- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → SOLO ШИМ → MASTER ОПТИКА → SLAVE ОПТИКА`; - MODE long: открыть настройки; - START short: начать тест; - START long во время теста: ABORT, PWM немедленно выключается. +На C3 режим `SOLO ДРАЙВЕР` пропускается. В `SOLO ШИМ` выход непрерывно выдаёт +выбранную частоту и длительность импульса без проверки Rx. Частота задаётся +пунктом `ЧАСТОТА ШИМ`, длительность — пунктом `ИМПУЛЬС ШИМ` (общим с +`МАКС. ИМПУЛЬС` остальных режимов). Удержание START или команда `stop` в +Serial выключает выход. + В ожидании в группе `ПЛАТА`: - MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`; @@ -256,6 +263,36 @@ START и MODE будят устройство. Первое, пробуждаю 5. время выборки. 6. код света `HH`, `HL`, `LH`, `LL` — только для теста драйвера. +В `SOLO ШИМ` меню показывает только частоту и длительность одного импульса. + +### Тестовая прошивка PWM/Rx + +Для отдельной проверки сигнала раскомментируйте `#define SIGNAL_PROBE_FIRMWARE` +в `OpticalChannelTester/Config.h` и загрузите тот же скетч. Эта прошивка +при включённом выходе выдаёт выбранный PWM на `GPIO_PWM` и независимо измеряет на +`GPIO_RX` минимальную длительность активного импульса и число пойманных +импульсов. Начальный активный уровень входа задаёт `RX_LIGHT_ON_GPIO_LEVEL`. +Результат показывается на OLED и в Serial +115200; при отсутствии OLED достаточно Serial. + +- MODE коротко: следующая частота из `PWM_FREQUENCY_OPTIONS_HZ`; +- MODE удержать: следующая длительность из `MAX_PULSE_OPTIONS_NS`; +- START коротко: сбросить найденный минимум и начать измерение заново; +- START удержать: выключить или включить PWM; +- START и MODE удержать вместе: переключить активный уровень Rx между HIGH и + LOW и сбросить накопленные измерения. + +Верхняя строка OLED показывает частоту и длительность импульса PWM; `OFF` +означает выключенный выход. Буква `H` или `L` справа показывает текущий +активный уровень Rx. Нижняя строка показывает минимум и число пойманных +импульсов. + +Значения, при которых импульс не помещается в период, пропускаются. В статистику +Rx попадают только полные периоды выбранной частоты; число фронтов и отброшенных +периодов видно на экране, пока подходящие импульсы не найдены. Изменение PWM +сбрасывает накопленные измерения. Для возврата к обычному тестеру закомментируйте +`SIGNAL_PROBE_FIRMWARE` и загрузите скетч снова. + Экран активного уровня: ```text