Compare commits

1 Commits
master ... test

Author SHA1 Message Date
Razvalyaev
040853e7dc Всякие тесты неинтересные 2026-09-24 07:58:47 +03:00
14 changed files with 456 additions and 32 deletions

View File

@@ -19,7 +19,7 @@ constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7;
const char *uiFailName(FailReason reason); const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) { 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", "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"}; "SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
const uint8_t index = static_cast<uint8_t>(state); const uint8_t index = static_cast<uint8_t>(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<uint8_t>(value - 1U); return value <= first ? last : static_cast<uint8_t>(value - 1U);
} }
uint8_t nextMenuItem(uint8_t current, TestGroup group) { uint8_t nextMenuItem(uint8_t current, TestGroup group, TestKind kind) {
if (group == TestGroup::BOARD) if (group == TestGroup::BOARD)
return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U; 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 return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U
: static_cast<uint8_t>(current + 1U); : static_cast<uint8_t>(current + 1U);
} }
@@ -356,6 +359,12 @@ void App::update() {
} }
return; 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 && if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
startEvent == ButtonEvent::LONG) { abortTest(); return; } startEvent == ButtonEvent::LONG) { abortTest(); return; }
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED && if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
@@ -399,7 +408,8 @@ void App::update() {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
leaveOpticalCalibration(); leaveOpticalCalibration();
menuItem_ = nextMenuItem(menuItem_, menuItem_ = nextMenuItem(menuItem_,
static_cast<TestGroup>(settings_.testGroup)); static_cast<TestGroup>(settings_.testGroup),
static_cast<TestKind>(settings_.testKind));
Log::printf("ACTION", "menu item selected index=%u", menuItem_); Log::printf("ACTION", "menu item selected index=%u", menuItem_);
showMenu(); showMenu();
} }
@@ -529,12 +539,12 @@ void App::printSerialHelp() {
Serial.println(" set group optics|board"); Serial.println(" set group optics|board");
Serial.println(" set boardtest adc|pwm|rx"); Serial.println(" set boardtest adc|pwm|rx");
Serial.println(" set role solo|master|slave"); Serial.println(" set role solo|master|slave");
Serial.println(" set test optical|driver"); Serial.println(" set test optical|driver|pwm (driver/pwm: SOLO only)");
Serial.println(" set frequency 500|1000|2000|5000|10000|25000"); Serial.println(" set frequency 500|1000|2000|5000|10000");
Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000"); 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 min 250|500|1000|2000|5000|10000|50000");
Serial.println(" set accuracy 1|2|5|10"); 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)"); 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) { else if (state_ == AppState::BOARD_TEST) {
Serial.println("OK board test stopped"); Serial.println("OK board test stopped");
stopBoardTest(); stopBoardTest();
} else if (state_ == AppState::PWM_OUTPUT) {
Serial.println("OK PWM output stopped");
stopPwmOutput();
} else if (state_ == AppState::MENU) { } else if (state_ == AppState::MENU) {
leaveOpticalCalibration(); leaveOpticalCalibration();
state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed"); 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 && else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
static_cast<Role>(settings_.role) == Role::SOLO) { static_cast<Role>(settings_.role) == Role::SOLO) {
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true; settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
} else if (!strcmp(value, "pwm") &&
static_cast<Role>(settings_.role) == Role::SOLO) {
settings_.testKind = static_cast<uint8_t>(TestKind::PWM_OUTPUT); accepted = true;
} }
} else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) { } else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric); const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
@@ -705,6 +721,8 @@ void App::cycleRunMode() {
const TestKind kind = static_cast<TestKind>(settings_.testKind); const TestKind kind = static_cast<TestKind>(settings_.testKind);
if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) { if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
} else if (role == Role::SOLO && kind != TestKind::PWM_OUTPUT) {
settings_.testKind = static_cast<uint8_t>(TestKind::PWM_OUTPUT);
} else if (role == Role::SOLO) { } else if (role == Role::SOLO) {
settings_.role = static_cast<uint8_t>(Role::MASTER); settings_.role = static_cast<uint8_t>(Role::MASTER);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
@@ -725,11 +743,12 @@ void App::sanitizeRange() {
settings_.boardTest = static_cast<uint8_t>(defaultBoardTest()); settings_.boardTest = static_cast<uint8_t>(defaultBoardTest());
if (settings_.role > static_cast<uint8_t>(Role::SLAVE)) if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
settings_.role = static_cast<uint8_t>(Role::SOLO); settings_.role = static_cast<uint8_t>(Role::SOLO);
if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER)) if (settings_.testKind > static_cast<uint8_t>(TestKind::PWM_OUTPUT))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL)) if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
settings_.lightCode = static_cast<uint8_t>(LightCode::HH); settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
if (settings_.role != static_cast<uint8_t>(Role::SOLO) || if ((settings_.role != static_cast<uint8_t>(Role::SOLO) &&
settings_.testKind != static_cast<uint8_t>(TestKind::OPTICAL)) ||
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER))) (TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ); settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
@@ -760,8 +779,8 @@ void App::changeMenu(int d) {
} else if (menuItem_ == 2) { } else if (menuItem_ == 2) {
const uint8_t last = lastValidMaxPulseIndex( const uint8_t last = lastValidMaxPulseIndex(
PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]); PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
const uint8_t first = firstMaxPulseIndexAtLeast( const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT
MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last); ? 0U : firstMaxPulseIndexAtLeast(MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex, settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
first, last, d); first, last, d);
} else if (menuItem_ == 3) { } else if (menuItem_ == 3) {
@@ -811,7 +830,8 @@ void App::showMenu() {
break; break;
case 2: case 2:
Display::formatPulse(params_.maxPulseNs, value, sizeof(value)); Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
label = UiText::MENU_MAX_PULSE; label = static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT
? UiText::MENU_PWM_PULSE : UiText::MENU_MAX_PULSE;
break; break;
case 3: case 3:
Display::formatPulse(params_.minPulseNs, value, sizeof(value)); Display::formatPulse(params_.minPulseNs, value, sizeof(value));
@@ -847,7 +867,8 @@ void App::showMenu() {
default: return; default: return;
} }
formatMenuLine(label, value, one, sizeof(one)); formatMenuLine(label, value, one, sizeof(one));
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD ||
static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT)
snprintf(total, sizeof(total), "%s", UiText::BOARD_READY); snprintf(total, sizeof(total), "%s", UiText::BOARD_READY);
else else
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total)); formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
@@ -900,6 +921,10 @@ void App::startTest() {
startBoardTest(); startBoardTest();
return; return;
} }
if (static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT) {
startPwmOutput();
return;
}
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs); params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE; stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
@@ -1019,6 +1044,37 @@ void App::stopBoardTest() {
showIdle(); 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) { bool App::armSlave(bool preserveDisplay) {
setActivePerformance(false); setActivePerformance(false);
pwm_.stop(); 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("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, 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); GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
if (static_cast<TestKind>(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<TestKind>(settings_.testKind) == TestKind::DRIVER) { if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode)); const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n", Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",

View File

@@ -9,7 +9,7 @@
#include "SettingsStore.h" #include "SettingsStore.h"
enum class AppState : uint8_t { 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, MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
SLAVE_WAIT_ACK, FINISHED SLAVE_WAIT_ACK, FINISHED
}; };
@@ -33,6 +33,8 @@ class App {
void startBoardTest(); void startBoardTest();
void updateBoardTest(uint32_t now); void updateBoardTest(uint32_t now);
void stopBoardTest(); void stopBoardTest();
void startPwmOutput();
void stopPwmOutput();
bool armSlave(bool preserveDisplay = false); bool armSlave(bool preserveDisplay = false);
bool prepareStage(bool showProgress = true); bool prepareStage(bool showProgress = true);
bool startLocalMeasurement(float hz, float duty); bool startLocalMeasurement(float hz, float duty);

View File

@@ -2,18 +2,21 @@
#include <Arduino.h> #include <Arduino.h>
// Uncomment to build the standalone PWM/RX pulse probe instead of the tester UI.
#define SIGNAL_PROBE_FIRMWARE
// ------------------------- Hardware configuration ------------------------- // ------------------------- Hardware configuration -------------------------
// Enabled for the hand-wired prototype. Comment out for the production PCB. // 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. // Both profiles map S3 signals by physical header position with 5V/GND aligned.
//#define MAKETKA #define MAKETKA
// Select exactly one populated receiver circuit. Use // Select exactly one populated receiver circuit. Use
// BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX. // BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX.
#define BOARD_RX_INTERFACE_ADC 1 #define BOARD_RX_INTERFACE_ADC 1
#define BOARD_RX_INTERFACE_DIGITAL 2 #define BOARD_RX_INTERFACE_DIGITAL 2
#ifndef BOARD_RX_INTERFACE #ifndef BOARD_RX_INTERFACE
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC #define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL
#endif #endif
#if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \ #if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \

View File

@@ -18,7 +18,7 @@ constexpr const char *ROLE_NAMES[] = {
}; };
constexpr const char *TEST_NAMES[] = { constexpr const char *TEST_NAMES[] = {
"ОПТИКА", "ДРАЙВЕР" "ОПТИКА", "ДРАЙВЕР", "ШИМ"
}; };
constexpr const char *TEST_GROUP_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_FREQUENCY = "ЧАСТОТА ШИМ:";
constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:"; constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:";
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:"; constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
constexpr const char *MENU_PWM_PULSE = "ИМПУЛЬС ШИМ:";
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:"; constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
@@ -97,7 +98,7 @@ constexpr const char *ROLE_NAMES[] = {
}; };
constexpr const char *TEST_NAMES[] = { constexpr const char *TEST_NAMES[] = {
"OPTICAL", "DRIVER" "OPTICAL", "DRIVER", "PWM"
}; };
constexpr const char *TEST_GROUP_NAMES[] = { 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_FREQUENCY = "PWM FREQUENCY:";
constexpr const char *MENU_TEST_GROUP = "TEST GROUP:"; constexpr const char *MENU_TEST_GROUP = "TEST GROUP:";
constexpr const char *MENU_MAX_PULSE = "MAX PULSE:"; 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_MIN_PULSE = "MIN PULSE:";
constexpr const char *MENU_ACCURACY = "ACCURACY:"; constexpr const char *MENU_ACCURACY = "ACCURACY:";
constexpr const char *MENU_TEST_TIME = "TEST TIME:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:";

View File

@@ -16,9 +16,9 @@ const char *testGroupName(TestGroup group) {
} }
const char *testKindName(TestKind kind) { 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<uint8_t>(kind); const uint8_t i = static_cast<uint8_t>(kind);
return i < 2 ? names[i] : "?"; return i < 3 ? names[i] : "?";
} }
const char *boardTestName(BoardTest test) { const char *boardTestName(BoardTest test) {

View File

@@ -5,7 +5,7 @@
enum class Role : uint8_t { SOLO, MASTER, SLAVE }; enum class Role : uint8_t { SOLO, MASTER, SLAVE };
enum class TestGroup : uint8_t { OPTICS, BOARD }; 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 BoardTest : uint8_t { ADC, PWM_OUTPUT, RX_INPUT };
enum class LightCode : uint8_t { HH, HL, LH, LL }; enum class LightCode : uint8_t { HH, HL, LH, LL };
enum class FailReason : uint8_t { enum class FailReason : uint8_t {

View File

@@ -8,7 +8,8 @@
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#include <soc/gpio_struct.h> #include <soc/gpio_reg.h>
#include <soc/soc.h>
#include <string.h> #include <string.h>
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
#include <riscv/rv_utils.h> #include <riscv/rv_utils.h>
@@ -204,7 +205,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() {
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX); constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
for (;;) { for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 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 nextStart = 0;
uint32_t windowEnd = 0; uint32_t windowEnd = 0;
uint32_t lastTxStart = 0; uint32_t lastTxStart = 0;
@@ -216,7 +217,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() {
uint32_t interruptState = 0; uint32_t interruptState = 0;
auto sampleOnce = [&]() { 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; if (current == levels) return;
const uint32_t now = esp_cpu_get_cycle_count(); const uint32_t now = esp_cpu_get_cycle_count();
const uint32_t changed = current ^ levels; const uint32_t changed = current ^ levels;

View File

@@ -1,5 +1,11 @@
#include "Config.h"
#ifdef SIGNAL_PROBE_FIRMWARE
#include "SignalProbe.h"
SignalProbe app;
#else
#include "App.h" #include "App.h"
App app; App app;
#endif
void setup() { app.begin(); } void setup() { app.begin(); }
void loop() { app.update(); } void loop() { app.update(); }

View File

@@ -88,6 +88,19 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
return startCapture(false); 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 #if OPTICAL_USE_MCPWM_CAPTURE
bool PulseReceiver::configureDriverTxCapture(bool risingEdge) { bool PulseReceiver::configureDriverTxCapture(bool risingEdge) {
if (running_) return false; if (running_) return false;
@@ -376,6 +389,18 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
return count; 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, size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
TickType_t waitTicks) { TickType_t waitTicks) {
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ || if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||

View File

@@ -24,11 +24,13 @@ class PulseReceiver {
public: public:
bool begin(); bool begin();
bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn); bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
bool startRaw();
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs, bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
bool activeTxLightOn); bool activeTxLightOn);
void stop(); void stop();
void resetStream(); void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); 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); size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
uint32_t takeDroppedItems(); uint32_t takeDroppedItems();
uint32_t tickHz() const; uint32_t tickHz() const;

View File

@@ -8,10 +8,10 @@
namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; } namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
void SettingsStore::defaults(Settings &s) const { 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<uint8_t>(Role::SOLO), s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO),
static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH), static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH),
2, 6, 3, 2, 3, static_cast<uint8_t>(TestGroup::OPTICS), 2, 4, 3, 2, 3, static_cast<uint8_t>(TestGroup::OPTICS),
static_cast<uint8_t>(BoardTest::ADC), 0}; static_cast<uint8_t>(BoardTest::ADC), 0};
s.checksum = settingsChecksum(s); s.checksum = settingsChecksum(s);
} }
@@ -20,9 +20,9 @@ bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) && return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.testGroup <= static_cast<uint8_t>(TestGroup::BOARD) && s.testGroup <= static_cast<uint8_t>(TestGroup::BOARD) &&
s.boardTest <= static_cast<uint8_t>(BoardTest::RX_INPUT) && s.boardTest <= static_cast<uint8_t>(BoardTest::RX_INPUT) &&
s.testKind <= static_cast<uint8_t>(TestKind::DRIVER) && s.testKind <= static_cast<uint8_t>(TestKind::PWM_OUTPUT) &&
s.lightCode <= static_cast<uint8_t>(LightCode::LL) && s.lightCode <= static_cast<uint8_t>(LightCode::LL) &&
(s.testKind != static_cast<uint8_t>(TestKind::DRIVER) || (s.testKind == static_cast<uint8_t>(TestKind::OPTICAL) ||
s.role == static_cast<uint8_t>(Role::SOLO)) && s.role == static_cast<uint8_t>(Role::SOLO)) &&
s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) && s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) && s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&

View File

@@ -0,0 +1,236 @@
#include "SignalProbe.h"
#include "Config.h"
#include <esp32-hal-cpu.h>
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<uint64_t>(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<uint8_t>(countOf(PWM_FREQUENCY_OPTIONS_HZ));
for (uint8_t step = 0; step < count; ++step) {
frequencyIndex_ = static_cast<uint8_t>((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<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS));
for (uint8_t step = 0; step < count; ++step) {
pulseIndex_ = static_cast<uint8_t>((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();
}

View File

@@ -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;
};

View File

@@ -6,8 +6,8 @@
- неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`; - неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`;
- общие для оптического и драйверного тестов частота PWM и диапазон импульсов; - общие для оптического и драйверного тестов частота PWM и диапазон импульсов;
- четыре рабочих режима: `SOLO OPTICAL`, `SOLO DRIVER` (только S3), - пять рабочих режимов: `SOLO OPTICAL`, `SOLO DRIVER` (только S3),
`MASTER OPTICAL`, `SLAVE OPTICAL`; `SOLO PWM`, `MASTER OPTICAL`, `SLAVE OPTICAL`;
- настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте - настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте
оптики полярность входного сигнала определяется автоматически; оптики полярность входного сигнала определяется автоматически;
- аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса; - аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса;
@@ -40,7 +40,8 @@ Arduino sketch находится в каталоге `OpticalChannelTester`:
- `Receiver.*` — MCPWM Capture на S3 / совместимый GPIO fallback; - `Receiver.*` — MCPWM Capture на S3 / совместимый GPIO fallback;
- `Measurement.*` — строгая проверка периодов; - `Measurement.*` — строгая проверка периодов;
- `Protocol.*`, `Radio.*` — ESP-NOW; - `Protocol.*`, `Radio.*` — ESP-NOW;
- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE. - `App.*` — общий конечный автомат SOLO/MASTER/SLAVE;
- `SignalProbe.*` — тестовый вариант прошивки PWM/Rx.
## Требования Arduino IDE ## Требования Arduino IDE
@@ -187,11 +188,17 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE
В ожидании в группе `ОПТИКА` на S3: В ожидании в группе `ОПТИКА` на S3:
- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → MASTER ОПТИКА → SLAVE ОПТИКА`; - MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → SOLO ШИМ → MASTER ОПТИКА → SLAVE ОПТИКА`;
- MODE long: открыть настройки; - MODE long: открыть настройки;
- START short: начать тест; - START short: начать тест;
- START long во время теста: ABORT, PWM немедленно выключается. - START long во время теста: ABORT, PWM немедленно выключается.
На C3 режим `SOLO ДРАЙВЕР` пропускается. В `SOLO ШИМ` выход непрерывно выдаёт
выбранную частоту и длительность импульса без проверки Rx. Частота задаётся
пунктом `ЧАСТОТА ШИМ`, длительность — пунктом `ИМПУЛЬС ШИМ` (общим с
`МАКС. ИМПУЛЬС` остальных режимов). Удержание START или команда `stop` в
Serial выключает выход.
В ожидании в группе `ПЛАТА`: В ожидании в группе `ПЛАТА`:
- MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`; - MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`;
@@ -256,6 +263,36 @@ START и MODE будят устройство. Первое, пробуждаю
5. время выборки. 5. время выборки.
6. код света `HH`, `HL`, `LH`, `LL` — только для теста драйвера. 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 ```text