Доработки по тесту драйвера

This commit is contained in:
2026-08-14 14:39:39 +03:00
parent 037bb37e62
commit 035792aedd
10 changed files with 307 additions and 140 deletions

View File

@@ -38,6 +38,16 @@ float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0); return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0);
} }
bool configuredTxPulseLightOn(const Settings &settings) {
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
? txActiveLightOn(settings) : true;
}
const char *configuredLevelName(const Settings &settings) {
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
? lightCodeName(static_cast<LightCode>(settings.lightCode)) : "AUTO";
}
void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) { void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char frequency[16], pulse[12]; char frequency[16], pulse[12];
Display::formatPwmFrequency(hz, frequency, sizeof(frequency)); 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) { 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. // Exact nanoseconds remain available in the Serial diagnostic.
if (ns < 1000ULL) snprintf(out, size, "%llun", ns); if (ns < 1000ULL) snprintf(out, size, "%llun", ns);
else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0); else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0);
@@ -269,7 +279,7 @@ void App::finishInitialization(bool factoryReset) {
} }
sanitizeRange(); sanitizeRange();
params_ = store_.params(settings_); 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"); if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
initialized_ = true; initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } 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 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 (ms)");
Serial.println(" set light HH|HL|LH|LL"); Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
} }
void App::printSerialStatus() { void App::printSerialStatus() {
@@ -471,7 +481,7 @@ void App::printSerialStatus() {
appStateName(state_), roleName(static_cast<Role>(settings_.role)), appStateName(state_), roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz, testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs, params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
lightCodeName(static_cast<LightCode>(settings_.lightCode)), configuredLevelName(settings_),
usbHostPresent() ? "connected" : "disconnected"); usbHostPresent() ? "connected" : "disconnected");
} }
@@ -485,7 +495,7 @@ void App::finishSerialSettingsChange() {
state_ = AppState::IDLE; state_ = AppState::IDLE;
sanitizeRange(); sanitizeRange();
params_ = store_.params(settings_); params_ = store_.params(settings_);
pwm_.configureActiveLight(txActiveLightOn(settings_)); pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
const bool saved = store_.save(settings_); const bool saved = store_.save(settings_);
Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no"); Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave(); if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
@@ -577,6 +587,10 @@ void App::handleSerialCommand(char *line) {
const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric); const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric);
if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; } if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; }
} else if (!strcmp(name, "light")) { } else if (!strcmp(name, "light")) {
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
Serial.println("ERR level setting is available only in DRIVER test");
return;
}
if (!strcmp(value, "hh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HH); accepted = true; } if (!strcmp(value, "hh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HH); accepted = true; }
else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HL); accepted = true; } else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HL); accepted = true; }
else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LH); accepted = true; } else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LH); accepted = true; }
@@ -634,7 +648,6 @@ void App::sanitizeRange() {
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin); DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
if (settings_.minPulseIndex < firstDriverMin) if (settings_.minPulseIndex < firstDriverMin)
settings_.minPulseIndex = firstDriverMin; settings_.minPulseIndex = firstDriverMin;
settings_.lightCode = static_cast<uint8_t>(LightCode::HL);
} }
} }
@@ -672,17 +685,22 @@ void App::changeMenu(int d) {
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break; case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); 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 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 5: value = &settings_.lightCode; count = 4; break; case 5:
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
showMenu();
return;
}
value = &settings_.lightCode; count = 4; break;
default: return; default: return;
} }
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d); *value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
} }
sanitizeRange(); params_ = store_.params(settings_); 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", 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, menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex, settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
lightCodeName(static_cast<LightCode>(settings_.lightCode))); configuredLevelName(settings_));
showMenu(); showMenu();
} }
@@ -712,6 +730,13 @@ void App::showMenu() {
label = UiText::MENU_TEST_TIME; label = UiText::MENU_TEST_TIME;
break; break;
case 5: { case 5: {
if (static_cast<TestKind>(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<LightCode>(settings_.lightCode)); const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
snprintf(value, sizeof(value), "%s", code); snprintf(value, sizeof(value), "%s", code);
label = UiText::MENU_LIGHT_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", Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)), roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), testKindName(static_cast<TestKind>(settings_.testKind)),
lightCodeName(static_cast<LightCode>(settings_.lightCode)), stageCount_); configuredLevelName(settings_), stageCount_);
if (SERIAL_MINIMAL_LOG) { if (SERIAL_MINIMAL_LOG) {
Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu", Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)), roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz, testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs, params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
lightCodeName(static_cast<LightCode>(settings_.lightCode)), configuredLevelName(settings_),
stageCount_); stageCount_);
} }
printConfiguration(); printConfiguration();
@@ -836,7 +861,7 @@ bool App::startLocalMeasurement(float hz, float duty) {
receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty), receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs); PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, 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, const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
static_cast<uint32_t>(hz + 0.5f)); static_cast<uint32_t>(hz + 0.5f));
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL; const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
@@ -1381,15 +1406,20 @@ void App::printConfiguration() {
Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board, Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board,
roleName(static_cast<Role>(settings_.role)), roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), testKindName(static_cast<TestKind>(settings_.testKind)),
lightCodeName(static_cast<LightCode>(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("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);
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n", if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs, const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
params_.accuracyPct, params_.testTimeMs, Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
lightCodeName(static_cast<LightCode>(settings_.lightCode))[0], params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
lightCodeName(static_cast<LightCode>(settings_.lightCode))[1]); 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); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
Serial.printf("Pulse widths descending (%lu): ", stageCount_); Serial.printf("Pulse widths descending (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i) for (uint32_t i = 0; i < stageCount_; ++i)
@@ -1484,27 +1514,23 @@ void App::showDriverResult(const DriverStats &s, bool testPassed) {
delay, response); delay, response);
} else if (s.reason != FailReason::NONE) { } else if (s.reason != FailReason::NONE) {
snprintf(one, sizeof(one), "%s", uiFailName(s.reason)); snprintf(one, sizeof(one), "%s", uiFailName(s.reason));
char elapsed[12] = "---", errorDelay[12] = "---", errorPulse[12] = "---"; char elapsed[12] = "---", errorPulse[12] = "---";
if (s.errorElapsedTicks) { if (s.errorTriggerValid) {
const uint64_t elapsedNs = const uint64_t elapsedNs =
(s.errorElapsedTicks * 1000000000ULL + driverTest_.tickHz() / 2U) / (static_cast<uint64_t>(s.errorTriggerTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) /
driverTest_.tickHz(); driverTest_.tickHz();
formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed)); formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed));
} }
if (s.errorDelayValid) {
const uint64_t errorDelayNs =
(static_cast<uint64_t>(s.errorDelayTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
formatElapsedNs(errorDelayNs, errorDelay, sizeof(errorDelay));
}
if (s.errorPulseValid) { if (s.errorPulseValid) {
const uint64_t errorPulseNs = const uint64_t errorPulseNs =
(static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL + (static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz(); driverTest_.tickHz() / 2U) / driverTest_.tickHz();
formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse)); formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
} }
snprintf(two, sizeof(two), "T:%s D:%s P:%s", if (s.errorPulseValid)
elapsed, errorDelay, errorPulse); snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse);
else snprintf(two, sizeof(two), "T:%s", elapsed);
} else { } else {
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one)); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT, snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,

View File

@@ -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_WIDTH_NS = 700;
constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000; constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000;
constexpr uint32_t DRIVER_ACK_MERGE_MARGIN_NS = 250; 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. // Any response this long is a fault, not a normal acknowledgement.
constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500; 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; constexpr uint32_t DRIVER_RX_STUCK_MIN_NS = 20000;
// Retained by the generic receiver backend; the driver test itself uses the // Retained by the generic receiver backend; the driver test itself uses the
// stricter ACK start deadline above. // stricter ACK start deadline above.

View File

@@ -37,9 +37,7 @@ constexpr const char *FAIL_NAMES[] = {
"НЕТ ОТВЕТА ACK", "НЕТ ОТВЕТА ACK",
"ТАЙМИНГ ACK", "ТАЙМИНГ ACK",
"АВАРИЯ ДРАЙВЕРА", "АВАРИЯ ДРАЙВЕРА",
"ОТВЕТЫ ACK СЛИЛИСЬ", "ОТВЕТЫ ACK СЛИЛИСЬ"
"ОШИБКА ЗАТВОРА",
"КОРОТКОЕ ЗАМЫКАНИЕ"
}; };
constexpr const char *MODE_PREFIX = "РЕЖИМ: "; constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
@@ -52,6 +50,8 @@ constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:"; constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:";
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; 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 *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
constexpr const char *FREQUENCY_UNIT = " Гц"; constexpr const char *FREQUENCY_UNIT = " Гц";
@@ -103,9 +103,7 @@ constexpr const char *FAIL_NAMES[] = {
"ACK MISSING", "ACK MISSING",
"ACK TIMING", "ACK TIMING",
"DRIVER FAULT", "DRIVER FAULT",
"ACK MERGED", "ACK MERGED"
"GATE FAULT",
"SHORT CIRCUIT FAULT"
}; };
constexpr const char *MODE_PREFIX = "MODE: "; 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_TEST_TIME = "TEST TIME:";
constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:"; constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:";
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; 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 *MENU_TOTAL_TIME = "TOTAL TIME:";
constexpr const char *FREQUENCY_UNIT = " Hz"; constexpr const char *FREQUENCY_UNIT = " Hz";

View File

@@ -25,8 +25,7 @@ const char *failName(FailReason r) {
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT", static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING", "UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING",
"DRIVER FAULT", "ACK MERGED", "GATE MONITORING FAULT", "DRIVER FAULT", "ACK MERGED"};
"SHORT CIRCUIT FAULT"};
const uint8_t i = static_cast<uint8_t>(r); const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN"; return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
} }

View File

@@ -9,8 +9,7 @@ enum class LightCode : uint8_t { HH, HL, LH, LL };
enum class FailReason : uint8_t { enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE, NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED, DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED, ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED
GATE_MONITOR_FAULT, SHORT_CIRCUIT_FAULT
}; };
const char *roleName(Role role); const char *roleName(Role role);

View File

@@ -5,6 +5,7 @@
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_cpu.h> #include <esp_cpu.h>
#include <esp_timer.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#include <soc/gpio_struct.h> #include <soc/gpio_struct.h>
#include <string.h> #include <string.h>
@@ -36,7 +37,7 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
uint8_t settleCycles, bool activeTxLightOn, uint8_t settleCycles, bool activeTxLightOn,
bool activeRxLightOn) { bool activeRxLightOn) {
(void)tolerancePct; (void)tolerancePct;
(void)activeRxLightOn; (void)settleCycles;
if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs || if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
!testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false; !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) "driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
return false; 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; captureHz_ = getCpuFrequencyMhz() * 1000000UL;
if (!captureHz_ || captureHz_ % frequencyHz) return false; if (!captureHz_ || captureHz_ % frequencyHz) return false;
captureFrequencyHz_ = frequencyHz;
capturePulseNs_ = pulseNs;
captureTxLightOn_ = activeTxLightOn;
txPulseLightOn_ = activeTxLightOn;
pollPeriodCycles_ = captureHz_ / frequencyHz; pollPeriodCycles_ = captureHz_ / frequencyHz;
pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us
const uint64_t periodNs = 1000000000ULL / frequencyHz; 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 : const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL; TX_LIGHT_OFF_GPIO_LEVEL;
pollTxStartRawHigh_ = activeTxRaw == HIGH; 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); ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS); faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
shortCircuitTicks_ = nsToTicks(DRIVER_SHORT_CIRCUIT_MIN_NS);
stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS); stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL; testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
subsampleTicks_ = testTicks_ / SUBSAMPLE_COUNT; subsampleTicks_ = testTicks_ / SUBSAMPLE_COUNT;
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || !ackStartMaxTicks_ || if (!pollPeriodCycles_ || !pollWindowAfterCycles_ ||
!faultLongTicks_ || !shortCircuitTicks_ || !stuckTicks_ || !ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ ||
!testTicks_ || !subsampleTicks_) !testTicks_ || !subsampleTicks_)
return false; return false;
@@ -81,12 +90,23 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
pendingCount_ = 0; pendingCount_ = 0;
response_ = {}; response_ = {};
measurementStartTick_ = deadlineTick_ = 0; measurementStartTick_ = deadlineTick_ = 0;
pointOriginTick_ = lastEventTick_ = 0; pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0;
settleCycles_ = settleCycles; // 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<unsigned long>(captureHz_ / 1000000UL),
static_cast<unsigned>(settleCycles_),
static_cast<unsigned long>(DRIVER_ACK_START_MAX_NS));
const uint64_t periodUs =
(1000000ULL + frequencyHz - 1ULL) / frequencyHz;
settlingTimeoutUs_ = periodUs *
(static_cast<uint64_t>(DRIVER_CAPTURE_SYNC_CYCLES) + 2ULL) + 1000ULL;
settlingDeadlineUs_ = 0;
settledCycles_ = 0; settledCycles_ = 0;
completedSubsamples_ = 0; completedSubsamples_ = 0;
measurementClosed_ = false; measurementClosed_ = false;
havePointOrigin_ = false; havePointOrigin_ = false;
haveLastActiveTx_ = false;
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) == rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
rxActiveRawHigh_; rxActiveRawHigh_;
currentStep_ = 0; currentStep_ = 0;
@@ -118,6 +138,8 @@ bool DriverTest::armCapture() {
state_ = DriverState::IDLE; state_ = DriverState::IDLE;
return false; return false;
} }
settlingDeadlineUs_ = static_cast<uint64_t>(esp_timer_get_time()) +
settlingTimeoutUs_;
xTaskNotifyGive(analyzerTask_); xTaskNotifyGive(analyzerTask_);
return true; return true;
} }
@@ -133,8 +155,11 @@ bool DriverTest::resumeSubsample() {
pendingCount_ = 0; pendingCount_ = 0;
response_ = {}; response_ = {};
measurementStartTick_ = deadlineTick_ = 0; measurementStartTick_ = deadlineTick_ = 0;
lastActiveTxTick_ = 0;
settlingDeadlineUs_ = 0;
settledCycles_ = 0; settledCycles_ = 0;
measurementClosed_ = false; measurementClosed_ = false;
haveLastActiveTx_ = false;
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) == rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
rxActiveRawHigh_; rxActiveRawHigh_;
state_ = DriverState::SETTLING; state_ = DriverState::SETTLING;
@@ -147,7 +172,7 @@ void DriverTest::pollTaskEntry(void *context) {
static_cast<DriverTest *>(context)->pollTaskLoop(); static_cast<DriverTest *>(context)->pollTaskLoop();
} }
void DriverTest::pollTaskLoop() { 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);
@@ -202,6 +227,10 @@ void DriverTest::pollTaskLoop() {
portEXIT_CRITICAL(&pollMux_); portEXIT_CRITICAL(&pollMux_);
critical = false; critical = false;
flushHot(); 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; if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
nextStart = lastTxStart + pollPeriodCycles_; nextStart = lastTxStart + pollPeriodCycles_;
sawTxStart = false; sawTxStart = false;
@@ -248,12 +277,24 @@ void DriverTest::analyzerTaskLoop() {
stats_.droppedItems += dropped; stats_.droppedItems += dropped;
fail(FailReason::DATA_LOSS, lastEventTick_); fail(FailReason::DATA_LOSS, lastEventTick_);
} }
if (!count && state_ == DriverState::SETTLING &&
static_cast<uint64_t>(esp_timer_get_time()) >=
settlingDeadlineUs_) {
fail(FailReason::ACK_MISSING, lastEventTick_);
}
} }
} }
} }
void DriverTest::processEvent(const TimedEvent &event) { void DriverTest::processEvent(const TimedEvent &event) {
lastEventTick_ = event.tick; 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_) { if (!havePointOrigin_) {
pointOriginTick_ = event.tick; pointOriginTick_ = event.tick;
havePointOrigin_ = true; havePointOrigin_ = true;
@@ -270,12 +311,15 @@ void DriverTest::processSettling(const TimedEvent &event) {
} }
const uint8_t rawLevel = event.rising ? HIGH : LOW; const uint8_t rawLevel = event.rising ? HIGH : LOW;
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL; const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
if (!lightOn) return; if (lightOn != txPulseLightOn_) return;
if (settledCycles_ < settleCycles_) { if (settledCycles_ < settleCycles_) {
++settledCycles_; ++settledCycles_;
return; return;
} }
if (rxActive_) return; if (rxActive_) {
fail(FailReason::DRIVER_FAULT, event.tick);
return;
}
state_ = DriverState::RUNNING; state_ = DriverState::RUNNING;
measurementStartTick_ = event.tick; measurementStartTick_ = event.tick;
const uint64_t measuredBefore = const uint64_t measuredBefore =
@@ -284,26 +328,35 @@ void DriverTest::processSettling(const TimedEvent &event) {
completedSubsamples_ + 1U == SUBSAMPLE_COUNT ? completedSubsamples_ + 1U == SUBSAMPLE_COUNT ?
testTicks_ - measuredBefore : subsampleTicks_; testTicks_ - measuredBefore : subsampleTicks_;
deadlineTick_ = event.tick + thisSubsampleTicks; deadlineTick_ = event.tick + thisSubsampleTicks;
processTx(event, true); processTx(event, lightOn);
} }
void DriverTest::processRunning(const TimedEvent &event) { void DriverTest::processRunning(const TimedEvent &event) {
expirePending(event.tick);
if (state_ != DriverState::RUNNING) return;
if (response_.active && event.tick - response_.startTick >= stuckTicks_) { if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
const uint64_t delay = response_.associated ? const uint64_t delay = response_.associated ?
response_.startTick - response_.tx.tick : 0; response_.startTick - response_.tx.tick : 0;
fail(FailReason::SHORT_CIRCUIT_FAULT, event.tick, delay, const uint64_t trigger = haveLastActiveTx_ &&
event.tick - response_.startTick); response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : delay;
fail(FailReason::DRIVER_FAULT, event.tick, delay,
event.tick - response_.startTick, trigger);
return; return;
} }
if (event.source == Source::TX) { if (event.source == Source::TX) {
expirePending(event.tick);
if (state_ != DriverState::RUNNING) return;
const uint8_t rawLevel = event.rising ? HIGH : LOW; const uint8_t rawLevel = event.rising ? HIGH : LOW;
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL; const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
if (event.tick < deadlineTick_) processTx(event, lightOn); if (event.tick < deadlineTick_) processTx(event, lightOn);
else measurementClosed_ = true; 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); completeIfPossible(event.tick);
} }
@@ -318,6 +371,10 @@ bool DriverTest::addPending(uint64_t tick, bool lightOn) {
void DriverTest::processTx(const TimedEvent &event, bool lightOn) { void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
if (!addPending(event.tick, lightOn)) return; if (!addPending(event.tick, lightOn)) return;
if (lightOn == txPulseLightOn_) {
lastActiveTxTick_ = event.tick;
haveLastActiveTx_ = true;
}
++stats_.inputEdges; ++stats_.inputEdges;
} }
@@ -358,10 +415,11 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
if (!response_.active) return; if (!response_.active) return;
const uint64_t width = event.tick - response_.startTick; const uint64_t width = event.tick - response_.startTick;
if (!response_.associated) { if (!response_.associated) {
const uint64_t trigger = haveLastActiveTx_ &&
response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : 0;
response_ = {}; response_ = {};
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT : fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger);
FailReason::GATE_MONITOR_FAULT,
event.tick, 0, width);
return; return;
} }
@@ -370,17 +428,19 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
if (pending_[i].tick > response_.startTick && if (pending_[i].tick > response_.startTick &&
event.tick - pending_[i].tick >= guard) { event.tick - pending_[i].tick >= guard) {
const uint64_t delay = response_.startTick - response_.tx.tick; const uint64_t delay = response_.startTick - response_.tx.tick;
const uint64_t trigger = event.tick - pending_[i].tick;
response_ = {}; response_ = {};
fail(FailReason::ACK_MERGED, event.tick, delay, width); fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger);
return; return;
} }
} }
if (width >= faultLongTicks_) { if (width >= faultLongTicks_) {
const uint64_t delay = response_.startTick - response_.tx.tick; const uint64_t delay = response_.startTick - response_.tx.tick;
const uint64_t trigger = haveLastActiveTx_ &&
response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : delay;
response_ = {}; response_ = {};
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT : fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger);
FailReason::GATE_MONITOR_FAULT,
event.tick, delay, width);
return; return;
} }
@@ -424,8 +484,11 @@ void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
void DriverTest::expirePending(uint64_t now) { void DriverTest::expirePending(uint64_t now) {
for (uint8_t i = 0; i < pendingCount_; ++i) { for (uint8_t i = 0; i < pendingCount_; ++i) {
if (now <= pending_[i].tick + ackStartMaxTicks_) continue; if (now < pending_[i].tick + ackStartMaxTicks_) continue;
fail(FailReason::ACK_MISSING, now, now - pending_[i].tick, 0); // 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; return;
} }
} }
@@ -456,12 +519,18 @@ void DriverTest::completeIfPossible(uint64_t now) {
} }
void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay, 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 (state_ == DriverState::FAIL || state_ == DriverState::PASS) return;
if (stats_.reason == FailReason::NONE) { if (stats_.reason == FailReason::NONE) {
stats_.reason = reason; stats_.reason = reason;
if (tick && havePointOrigin_ && tick >= pointOriginTick_) if (tick && havePointOrigin_ && tick >= pointOriginTick_)
stats_.errorElapsedTicks = tick - pointOriginTick_; stats_.errorElapsedTicks = tick - pointOriginTick_;
const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay;
if (trigger) {
stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(trigger);
stats_.errorTriggerValid = true;
}
if (delay) { if (delay) {
stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX : stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(delay); static_cast<uint32_t>(delay);
@@ -603,16 +672,20 @@ void DriverTest::printSummary() const {
static_cast<unsigned long>(publishedStats_.unexpectedResponses), static_cast<unsigned long>(publishedStats_.unexpectedResponses),
failName(publishedStats_.reason)); failName(publishedStats_.reason));
if (publishedStats_.reason != FailReason::NONE) { if (publishedStats_.reason != FailReason::NONE) {
Log::printf("DRIVER", auto formatOptional = [&](bool valid, uint32_t ticks,
"error timing: T=%lluns D=%s%lluns P=%s%lluns", char *out, size_t size) {
static_cast<unsigned long long>( if (!valid) snprintf(out, size, "---");
ticksToNs(publishedStats_.errorElapsedTicks)), else snprintf(out, size, "%lluns",
publishedStats_.errorDelayValid ? "" : "N/A/", static_cast<unsigned long long>(ticksToNs(ticks)));
static_cast<unsigned long long>( };
ticksToNs(publishedStats_.errorDelayTicks)), char trigger[24], pulse[24];
publishedStats_.errorPulseValid ? "" : "N/A/", formatOptional(publishedStats_.errorTriggerValid,
static_cast<unsigned long long>( publishedStats_.errorTriggerTicks, trigger, sizeof(trigger));
ticksToNs(publishedStats_.errorPulseTicks))); 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("ON", publishedStats_.turnOn);
printEdge("OFF", publishedStats_.turnOff); printEdge("OFF", publishedStats_.turnOff);

View File

@@ -31,8 +31,10 @@ struct DriverStats {
uint32_t droppedItems; uint32_t droppedItems;
uint32_t unexpectedResponses; uint32_t unexpectedResponses;
uint64_t errorElapsedTicks; uint64_t errorElapsedTicks;
uint32_t errorTriggerTicks;
uint32_t errorDelayTicks; uint32_t errorDelayTicks;
uint32_t errorPulseTicks; uint32_t errorPulseTicks;
bool errorTriggerValid;
bool errorDelayValid; bool errorDelayValid;
bool errorPulseValid; bool errorPulseValid;
DriverEdgeStats turnOn; DriverEdgeStats turnOn;
@@ -59,10 +61,13 @@ class DriverTest {
const DriverStats &stats() const { return publishedStats_; } const DriverStats &stats() const { return publishedStats_; }
private: 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 RawEvent { uint32_t tick; bool rising; Source source; };
struct TimedEvent { uint64_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 { struct Response {
bool active; bool active;
bool associated; bool associated;
@@ -80,7 +85,7 @@ class DriverTest {
static void analyzerTaskEntry(void *context); static void analyzerTaskEntry(void *context);
static void pollTaskEntry(void *context); static void pollTaskEntry(void *context);
void analyzerTaskLoop(); void analyzerTaskLoop();
void pollTaskLoop(); void IRAM_ATTR pollTaskLoop();
void processEvent(const TimedEvent &event); void processEvent(const TimedEvent &event);
void processSettling(const TimedEvent &event); void processSettling(const TimedEvent &event);
void processRunning(const TimedEvent &event); void processRunning(const TimedEvent &event);
@@ -94,7 +99,7 @@ class DriverTest {
uint64_t mergeGuardTicks() const; uint64_t mergeGuardTicks() const;
void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn); void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn);
void fail(FailReason reason, uint64_t tick = 0, uint64_t delay = 0, 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 publishStats();
void rememberTrace(const TimedEvent &event); void rememberTrace(const TimedEvent &event);
bool armCapture(); bool armCapture();
@@ -130,10 +135,13 @@ class DriverTest {
volatile bool captureReady_ = false; volatile bool captureReady_ = false;
volatile bool core0WdtDisabled_ = false; volatile bool core0WdtDisabled_ = false;
uint32_t captureHz_ = 0; uint32_t captureHz_ = 0;
uint32_t captureFrequencyHz_ = 0;
uint32_t capturePulseNs_ = 0;
uint32_t pollPeriodCycles_ = 0; uint32_t pollPeriodCycles_ = 0;
uint32_t pollWindowBeforeCycles_ = 0; uint32_t pollWindowBeforeCycles_ = 0;
uint32_t pollWindowAfterCycles_ = 0; uint32_t pollWindowAfterCycles_ = 0;
bool pollTxStartRawHigh_ = false; bool pollTxStartRawHigh_ = false;
bool captureTxLightOn_ = true;
portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED;
PendingTx pending_[MAX_PENDING] = {}; PendingTx pending_[MAX_PENDING] = {};
@@ -141,21 +149,25 @@ class DriverTest {
Response response_ = {}; Response response_ = {};
uint64_t ackStartMaxTicks_ = 0; uint64_t ackStartMaxTicks_ = 0;
uint64_t faultLongTicks_ = 0; uint64_t faultLongTicks_ = 0;
uint64_t shortCircuitTicks_ = 0;
uint64_t stuckTicks_ = 0; uint64_t stuckTicks_ = 0;
uint64_t testTicks_ = 0; uint64_t testTicks_ = 0;
uint64_t subsampleTicks_ = 0; uint64_t subsampleTicks_ = 0;
uint64_t settlingTimeoutUs_ = 0;
uint64_t settlingDeadlineUs_ = 0;
uint64_t measurementStartTick_ = 0; uint64_t measurementStartTick_ = 0;
uint64_t deadlineTick_ = 0; uint64_t deadlineTick_ = 0;
uint64_t pointOriginTick_ = 0; uint64_t pointOriginTick_ = 0;
uint64_t lastEventTick_ = 0; uint64_t lastEventTick_ = 0;
uint64_t lastActiveTxTick_ = 0;
uint8_t settleCycles_ = 0; uint8_t settleCycles_ = 0;
uint8_t settledCycles_ = 0; uint8_t settledCycles_ = 0;
uint8_t completedSubsamples_ = 0; uint8_t completedSubsamples_ = 0;
bool rxActiveRawHigh_ = true; bool rxActiveRawHigh_ = true;
bool txPulseLightOn_ = true;
bool rxActive_ = false; bool rxActive_ = false;
bool measurementClosed_ = false; bool measurementClosed_ = false;
bool havePointOrigin_ = false; bool havePointOrigin_ = false;
bool haveLastActiveTx_ = false;
bool haveRawTick_ = false; bool haveRawTick_ = false;
uint32_t lastRawTick_ = 0; uint32_t lastRawTick_ = 0;
uint64_t tickEpoch_ = 0; uint64_t tickEpoch_ = 0;

View File

@@ -75,6 +75,7 @@ bool PulseReceiver::begin() {
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct, bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
bool activeLightOn) { bool activeLightOn) {
(void)activeLightOn;
if (!plannedTickHz(expectedHz, expectedDutyPct)) return false; if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
expectedHz_ = expectedHz; expectedHz_ = expectedHz;
expectedDutyPct_ = expectedDutyPct; expectedDutyPct_ = expectedDutyPct;
@@ -83,11 +84,7 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
if (!cpuTickHz_) return false; if (!cpuTickHz_) return false;
#endif #endif
resetStream(); resetStream();
const bool rawHighMeansLightOn = RX_LIGHT_ON_GPIO_LEVEL == HIGH; Log::event("CAPTURE", "RX optical polarity will be detected automatically");
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");
return startCapture(false); return startCapture(false);
} }
@@ -209,6 +206,8 @@ void PulseReceiver::resetStream() {
droppedItems_ = 0; droppedItems_ = 0;
polarityKnown_ = false; polarityKnown_ = false;
activeStartRising_ = false; activeStartRising_ = false;
polarityEdgeCount_ = 0;
memset(polarityEdges_, 0, sizeof(polarityEdges_));
waitingForActiveEnd_ = true; waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0; activeStart_ = activeEnd_ = 0;
haveRawTick_ = false; haveRawTick_ = false;
@@ -244,12 +243,42 @@ bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
return true; return true;
} }
// HH/HL/LH/LL defines the active optical state explicitly. Synchronize on // Optical mode does not use the DRIVER level setting. Compare the first two
// its physical starting edge instead of guessing polarity from pulse width. // alternating intervals with the configured active duration and select the
if (edge.rising != activeStartRising_) return false; // 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<uint64_t>(
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; polarityKnown_ = true;
activeStart_ = edge.tick; polarityEdgeCount_ = 0;
waitingForActiveEnd_ = true;
if (firstIntervalIsActive) {
out = {polarityEdges_[0].tick,
static_cast<uint32_t>(polarityEdges_[2].tick - polarityEdges_[0].tick),
static_cast<uint32_t>(firstInterval), tickHz()};
activeStart_ = polarityEdges_[2].tick;
waitingForActiveEnd_ = true;
return true;
}
activeStart_ = polarityEdges_[1].tick;
activeEnd_ = polarityEdges_[2].tick;
waitingForActiveEnd_ = false;
return false; return false;
} }
@@ -268,10 +297,11 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
static_cast<uint8_t>(channel == self->txChannel_ ? CaptureSource::TX : static_cast<uint8_t>(channel == self->txChannel_ ? CaptureSource::TX :
CaptureSource::RX)}; CaptureSource::RX)};
if (self->txCaptureEnabled_) { if (self->txCaptureEnabled_) {
// Three capture channels are independent ISR producers. Serialize their // All channels in one MCPWM group are dispatched serially by the same
// reservation/publication of a ring slot; treating this as an SPSC ring // group ISR. Keep this callback shorter than the minimum interval between
// loses or duplicates RX events when TX and RX interrupts overlap. // equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic
portENTER_CRITICAL_ISR(&self->driverRingMux_); // RMW operations here can leave the channel pending until its capture
// register is overwritten by the next edge.
if (self->haveLastDriverEdge_ && if (self->haveLastDriverEdge_ &&
self->lastDriverEdge_.tick == edge.tick && self->lastDriverEdge_.tick == edge.tick &&
self->lastDriverEdge_.rising == edge.rising && 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 // The same channel callback can be delivered twice while several MCPWM
// capture status bits are pending. Two physical edges cannot have the // capture status bits are pending. Two physical edges cannot have the
// same source, direction and 12.5 ns hardware timestamp. // same source, direction and 12.5 ns hardware timestamp.
portEXIT_CRITICAL_ISR(&self->driverRingMux_);
return false; return false;
} }
const uint16_t write = __atomic_load_n( const uint16_t write = self->driverRingWrite_;
&self->driverRingWrite_, __ATOMIC_RELAXED);
const uint16_t next = static_cast<uint16_t>( const uint16_t next = static_cast<uint16_t>(
(write + 1U) % DRIVER_RING_CAPACITY); (write + 1U) & (DRIVER_RING_CAPACITY - 1U));
if (next == __atomic_load_n(&self->driverRingRead_, __ATOMIC_ACQUIRE)) { if (next == self->driverRingRead_) {
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); ++self->droppedItems_;
} else { } else {
self->driverRing_[write] = edge; self->driverRing_[write] = edge;
self->lastDriverEdge_ = edge; self->lastDriverEdge_ = edge;
self->haveLastDriverEdge_ = true; 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; return false;
} }
BaseType_t wake = pdFALSE; 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, size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
TickType_t waitTicks) { TickType_t waitTicks) {
if (!events || capacity < 2U || !txCaptureEnabled_ || !driverPulseTicks_ || if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
!driverReleaseSlackTicks_) return 0; !driverReleaseSlackTicks_) return 0;
constexpr size_t MAX_BATCH = 64; 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. // can the missing start interrupt be reconstructed and sorted before RX.
const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE); const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE);
uint16_t scan = read; uint16_t scan = read;
bool haveTxEnd = false; bool haveLatestTxEnd = false;
uint32_t lastTxEnd = 0; bool haveReleaseTxEnd = false;
uint32_t latestTxEnd = 0;
uint32_t releaseTxEnd = 0;
size_t projectedCount = 0; size_t projectedCount = 0;
while (scan != write) { while (scan != write) {
const Edge &edge = driverRing_[scan]; const Edge &edge = driverRing_[scan];
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX) const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
? 2U : 1U; ? 2U : 1U;
if (projectedCount + needed > limit) break; // Reserve one output slot for WINDOW_END.
if (projectedCount + needed + 1U > limit) break;
projectedCount += needed; projectedCount += needed;
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) { if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
lastTxEnd = edge.tick; if (haveLatestTxEnd) {
haveTxEnd = true; releaseTxEnd = latestTxEnd;
haveReleaseTxEnd = true;
}
latestTxEnd = edge.tick;
haveLatestTxEnd = true;
} }
scan = static_cast<uint16_t>((scan + 1U) % DRIVER_RING_CAPACITY); scan = static_cast<uint16_t>((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); if (waitTicks) vTaskDelay(waitTicks);
return 0; return 0;
} }
const uint32_t releaseThrough = lastTxEnd + driverReleaseSlackTicks_; const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_;
size_t count = 0; size_t count = 0;
while (read != write) { while (read != write) {
const Edge edge = driverRing_[read]; const Edge edge = driverRing_[read];
if (edge.source == static_cast<uint8_t>(CaptureSource::TX) &&
static_cast<int32_t>(edge.tick - releaseTxEnd) > 0)
break;
if (edge.source == static_cast<uint8_t>(CaptureSource::RX) && if (edge.source == static_cast<uint8_t>(CaptureSource::RX) &&
static_cast<int32_t>(edge.tick - releaseThrough) > 0) static_cast<int32_t>(edge.tick - releaseThrough) > 0)
break; break;
@@ -426,5 +466,9 @@ size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
events[i] = {timed.tick, timed.rising, events[i] = {timed.tick, timed.rising,
static_cast<CaptureSource>(ordered[i].source)}; static_cast<CaptureSource>(ordered[i].source)};
} }
const Edge marker = {releaseThrough, 0,
static_cast<uint8_t>(CaptureSource::WINDOW_END)};
const TimedEdge timedMarker = extendEdge(marker);
events[count++] = {timedMarker.tick, false, CaptureSource::WINDOW_END};
return count; return count;
} }

View File

@@ -12,7 +12,7 @@
#define OPTICAL_USE_MCPWM_CAPTURE 0 #define OPTICAL_USE_MCPWM_CAPTURE 0
#endif #endif
enum class CaptureSource : uint8_t { RX, TX }; enum class CaptureSource : uint8_t { RX, TX, WINDOW_END };
struct CaptureEvent { struct CaptureEvent {
uint64_t tick; uint64_t tick;
@@ -43,6 +43,8 @@ class PulseReceiver {
private: private:
struct Edge { uint32_t tick; uint8_t rising; uint8_t source; }; struct Edge { uint32_t tick; uint8_t rising; uint8_t source; };
static constexpr uint16_t DRIVER_RING_CAPACITY = 512; 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; }; struct TimedEdge { uint64_t tick; bool rising; };
bool startCapture(bool withTx); bool startCapture(bool withTx);
bool consumeEdge(const Edge &edge, PulsePeriod &period); bool consumeEdge(const Edge &edge, PulsePeriod &period);
@@ -78,6 +80,8 @@ class PulseReceiver {
uint32_t expectedHz_ = 0; uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 50.0f; float expectedDutyPct_ = 50.0f;
bool polarityKnown_ = false, activeStartRising_ = false; bool polarityKnown_ = false, activeStartRising_ = false;
TimedEdge polarityEdges_[3] = {};
uint8_t polarityEdgeCount_ = 0;
bool waitingForActiveEnd_ = true; bool waitingForActiveEnd_ = true;
uint64_t activeStart_ = 0, activeEnd_ = 0; uint64_t activeStart_ = 0, activeEnd_ = 0;
bool haveRawTick_ = false; bool haveRawTick_ = false;

File diff suppressed because one or more lines are too long