Доработки по тесту драйвера
This commit is contained in:
@@ -38,6 +38,16 @@ float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
|
||||
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) {
|
||||
char frequency[16], pulse[12];
|
||||
Display::formatPwmFrequency(hz, frequency, sizeof(frequency));
|
||||
@@ -67,7 +77,7 @@ void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
|
||||
}
|
||||
|
||||
void formatElapsedNs(uint64_t ns, char *out, size_t size) {
|
||||
// Compact form keeps `T:... D:... P:...` within 21 OLED columns.
|
||||
// Compact form keeps error timing within 21 OLED columns.
|
||||
// Exact nanoseconds remain available in the Serial diagnostic.
|
||||
if (ns < 1000ULL) snprintf(out, size, "%llun", ns);
|
||||
else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0);
|
||||
@@ -269,7 +279,7 @@ void App::finishInitialization(bool factoryReset) {
|
||||
}
|
||||
sanitizeRange();
|
||||
params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(txActiveLightOn(settings_));
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
|
||||
initialized_ = true;
|
||||
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
|
||||
@@ -458,7 +468,7 @@ void App::printSerialHelp() {
|
||||
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
|
||||
Serial.println(" set accuracy 1|2|5|10");
|
||||
Serial.println(" set time 100|250|500|1000|2000|5000 (ms)");
|
||||
Serial.println(" set light HH|HL|LH|LL");
|
||||
Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
|
||||
}
|
||||
|
||||
void App::printSerialStatus() {
|
||||
@@ -471,7 +481,7 @@ void App::printSerialStatus() {
|
||||
appStateName(state_), roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
|
||||
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode)),
|
||||
configuredLevelName(settings_),
|
||||
usbHostPresent() ? "connected" : "disconnected");
|
||||
}
|
||||
|
||||
@@ -485,7 +495,7 @@ void App::finishSerialSettingsChange() {
|
||||
state_ = AppState::IDLE;
|
||||
sanitizeRange();
|
||||
params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(txActiveLightOn(settings_));
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
const bool saved = store_.save(settings_);
|
||||
Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
||||
@@ -577,6 +587,10 @@ void App::handleSerialCommand(char *line) {
|
||||
const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric);
|
||||
if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} 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; }
|
||||
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; }
|
||||
@@ -634,7 +648,6 @@ void App::sanitizeRange() {
|
||||
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
|
||||
if (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 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
||||
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
||||
case 5: value = &settings_.lightCode; count = 4; break;
|
||||
case 5:
|
||||
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
|
||||
showMenu();
|
||||
return;
|
||||
}
|
||||
value = &settings_.lightCode; count = 4; break;
|
||||
default: return;
|
||||
}
|
||||
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
|
||||
}
|
||||
sanitizeRange(); params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(txActiveLightOn(settings_));
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u light=%s",
|
||||
menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
|
||||
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode)));
|
||||
configuredLevelName(settings_));
|
||||
showMenu();
|
||||
}
|
||||
|
||||
@@ -712,6 +730,13 @@ void App::showMenu() {
|
||||
label = UiText::MENU_TEST_TIME;
|
||||
break;
|
||||
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));
|
||||
snprintf(value, sizeof(value), "%s", code);
|
||||
label = UiText::MENU_LIGHT_CODE;
|
||||
@@ -739,13 +764,13 @@ void App::startTest() {
|
||||
Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)),
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode)), stageCount_);
|
||||
configuredLevelName(settings_), stageCount_);
|
||||
if (SERIAL_MINIMAL_LOG) {
|
||||
Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
|
||||
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode)),
|
||||
configuredLevelName(settings_),
|
||||
stageCount_);
|
||||
}
|
||||
printConfiguration();
|
||||
@@ -836,7 +861,7 @@ bool App::startLocalMeasurement(float hz, float duty) {
|
||||
receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
|
||||
PWM_SETTLE_CYCLES, params_.testTimeMs);
|
||||
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
|
||||
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, rxActiveLightOn(settings_));
|
||||
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, true);
|
||||
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
|
||||
static_cast<uint32_t>(hz + 0.5f));
|
||||
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,
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
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("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
|
||||
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
|
||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
|
||||
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs,
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode))[0],
|
||||
lightCodeName(static_cast<LightCode>(settings_.lightCode))[1]);
|
||||
if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
|
||||
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",
|
||||
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs, code[0], code[1]);
|
||||
} else {
|
||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, optical polarity=AUTO\n",
|
||||
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs);
|
||||
}
|
||||
stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
||||
Serial.printf("Pulse widths descending (%lu): ", stageCount_);
|
||||
for (uint32_t i = 0; i < stageCount_; ++i)
|
||||
@@ -1484,27 +1514,23 @@ void App::showDriverResult(const DriverStats &s, bool testPassed) {
|
||||
delay, response);
|
||||
} else if (s.reason != FailReason::NONE) {
|
||||
snprintf(one, sizeof(one), "%s", uiFailName(s.reason));
|
||||
char elapsed[12] = "---", errorDelay[12] = "---", errorPulse[12] = "---";
|
||||
if (s.errorElapsedTicks) {
|
||||
char elapsed[12] = "---", errorPulse[12] = "---";
|
||||
if (s.errorTriggerValid) {
|
||||
const uint64_t elapsedNs =
|
||||
(s.errorElapsedTicks * 1000000000ULL + driverTest_.tickHz() / 2U) /
|
||||
(static_cast<uint64_t>(s.errorTriggerTicks) * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) /
|
||||
driverTest_.tickHz();
|
||||
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) {
|
||||
const uint64_t errorPulseNs =
|
||||
(static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
|
||||
formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
|
||||
}
|
||||
snprintf(two, sizeof(two), "T:%s D:%s P:%s",
|
||||
elapsed, errorDelay, errorPulse);
|
||||
if (s.errorPulseValid)
|
||||
snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse);
|
||||
else snprintf(two, sizeof(two), "T:%s", elapsed);
|
||||
} else {
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
|
||||
|
||||
@@ -145,11 +145,13 @@ constexpr uint32_t DRIVER_ACK_DELAY_NS = 250;
|
||||
constexpr uint32_t DRIVER_ACK_WIDTH_NS = 700;
|
||||
constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000;
|
||||
constexpr uint32_t DRIVER_ACK_MERGE_MARGIN_NS = 250;
|
||||
// The first MCPWM TX end may belong to a pulse that was already active when
|
||||
// capture was enabled. The following period also drains capture events that
|
||||
// were pending independently in the rising/falling channels. Validation
|
||||
// therefore begins at the third TX period.
|
||||
constexpr uint8_t DRIVER_CAPTURE_SYNC_CYCLES = 2;
|
||||
// Any response this long is a fault, not a normal acknowledgement.
|
||||
constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500;
|
||||
// A short circuit is about 9 us. Gate-monitoring may be stretched by an
|
||||
// overlapping turn-off ACK, but remains shorter on the tested driver.
|
||||
constexpr uint32_t DRIVER_SHORT_CIRCUIT_MIN_NS = 6000;
|
||||
constexpr uint32_t DRIVER_RX_STUCK_MIN_NS = 20000;
|
||||
// Retained by the generic receiver backend; the driver test itself uses the
|
||||
// stricter ACK start deadline above.
|
||||
|
||||
@@ -37,9 +37,7 @@ constexpr const char *FAIL_NAMES[] = {
|
||||
"НЕТ ОТВЕТА ACK",
|
||||
"ТАЙМИНГ ACK",
|
||||
"АВАРИЯ ДРАЙВЕРА",
|
||||
"ОТВЕТЫ ACK СЛИЛИСЬ",
|
||||
"ОШИБКА ЗАТВОРА",
|
||||
"КОРОТКОЕ ЗАМЫКАНИЕ"
|
||||
"ОТВЕТЫ ACK СЛИЛИСЬ"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
|
||||
@@ -52,6 +50,8 @@ constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
|
||||
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
|
||||
constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:";
|
||||
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
|
||||
constexpr const char *LIGHT_AUTO = "АВТО";
|
||||
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: АВТО";
|
||||
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Гц";
|
||||
|
||||
@@ -103,9 +103,7 @@ constexpr const char *FAIL_NAMES[] = {
|
||||
"ACK MISSING",
|
||||
"ACK TIMING",
|
||||
"DRIVER FAULT",
|
||||
"ACK MERGED",
|
||||
"GATE FAULT",
|
||||
"SHORT CIRCUIT FAULT"
|
||||
"ACK MERGED"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "MODE: ";
|
||||
@@ -118,6 +116,8 @@ constexpr const char *MENU_ACCURACY = "ACCURACY:";
|
||||
constexpr const char *MENU_TEST_TIME = "TEST TIME:";
|
||||
constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:";
|
||||
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
|
||||
constexpr const char *LIGHT_AUTO = "AUTO";
|
||||
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: AUTO";
|
||||
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Hz";
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ const char *failName(FailReason r) {
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
|
||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
|
||||
"UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING",
|
||||
"DRIVER FAULT", "ACK MERGED", "GATE MONITORING FAULT",
|
||||
"SHORT CIRCUIT FAULT"};
|
||||
"DRIVER FAULT", "ACK MERGED"};
|
||||
const uint8_t i = static_cast<uint8_t>(r);
|
||||
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ enum class LightCode : uint8_t { HH, HL, LH, LL };
|
||||
enum class FailReason : uint8_t {
|
||||
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
|
||||
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
|
||||
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED,
|
||||
GATE_MONITOR_FAULT, SHORT_CIRCUIT_FAULT
|
||||
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED
|
||||
};
|
||||
|
||||
const char *roleName(Role role);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_timer.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#include <soc/gpio_struct.h>
|
||||
#include <string.h>
|
||||
@@ -36,7 +37,7 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
uint8_t settleCycles, bool activeTxLightOn,
|
||||
bool activeRxLightOn) {
|
||||
(void)tolerancePct;
|
||||
(void)activeRxLightOn;
|
||||
(void)settleCycles;
|
||||
if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
|
||||
!testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false;
|
||||
|
||||
@@ -49,8 +50,16 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
"driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
|
||||
return false;
|
||||
|
||||
// The ACK edges can be less than 1 us apart. MCPWM capture delivers all
|
||||
// channels through one group ISR and can overwrite an earlier channel
|
||||
// timestamp before that ISR reaches it. During DRIVER test dedicate core 0
|
||||
// to direct GPIO sampling; PWM itself remains fully hardware-generated.
|
||||
captureHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||
if (!captureHz_ || captureHz_ % frequencyHz) return false;
|
||||
captureFrequencyHz_ = frequencyHz;
|
||||
capturePulseNs_ = pulseNs;
|
||||
captureTxLightOn_ = activeTxLightOn;
|
||||
txPulseLightOn_ = activeTxLightOn;
|
||||
pollPeriodCycles_ = captureHz_ / frequencyHz;
|
||||
pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us
|
||||
const uint64_t periodNs = 1000000000ULL / frequencyHz;
|
||||
@@ -62,16 +71,16 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL :
|
||||
TX_LIGHT_OFF_GPIO_LEVEL;
|
||||
pollTxStartRawHigh_ = activeTxRaw == HIGH;
|
||||
rxActiveRawHigh_ = RX_LIGHT_ON_GPIO_LEVEL == LOW; // ACK/fault = light OFF
|
||||
rxActiveRawHigh_ =
|
||||
((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn);
|
||||
|
||||
ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
|
||||
faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
|
||||
shortCircuitTicks_ = nsToTicks(DRIVER_SHORT_CIRCUIT_MIN_NS);
|
||||
stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
|
||||
testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
|
||||
subsampleTicks_ = testTicks_ / SUBSAMPLE_COUNT;
|
||||
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || !ackStartMaxTicks_ ||
|
||||
!faultLongTicks_ || !shortCircuitTicks_ || !stuckTicks_ ||
|
||||
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ ||
|
||||
!ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ ||
|
||||
!testTicks_ || !subsampleTicks_)
|
||||
return false;
|
||||
|
||||
@@ -81,12 +90,23 @@ bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
pendingCount_ = 0;
|
||||
response_ = {};
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
pointOriginTick_ = lastEventTick_ = 0;
|
||||
settleCycles_ = settleCycles;
|
||||
pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0;
|
||||
// Skip two complete periods after the polling task synchronizes with TX.
|
||||
settleCycles_ = DRIVER_CAPTURE_SYNC_CYCLES;
|
||||
Log::printf("DRIVER", "capture=GPIO-%luMHz sync-periods=%u ACK-timeout=%luns",
|
||||
static_cast<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;
|
||||
completedSubsamples_ = 0;
|
||||
measurementClosed_ = false;
|
||||
havePointOrigin_ = false;
|
||||
haveLastActiveTx_ = false;
|
||||
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
||||
rxActiveRawHigh_;
|
||||
currentStep_ = 0;
|
||||
@@ -118,6 +138,8 @@ bool DriverTest::armCapture() {
|
||||
state_ = DriverState::IDLE;
|
||||
return false;
|
||||
}
|
||||
settlingDeadlineUs_ = static_cast<uint64_t>(esp_timer_get_time()) +
|
||||
settlingTimeoutUs_;
|
||||
xTaskNotifyGive(analyzerTask_);
|
||||
return true;
|
||||
}
|
||||
@@ -133,8 +155,11 @@ bool DriverTest::resumeSubsample() {
|
||||
pendingCount_ = 0;
|
||||
response_ = {};
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
lastActiveTxTick_ = 0;
|
||||
settlingDeadlineUs_ = 0;
|
||||
settledCycles_ = 0;
|
||||
measurementClosed_ = false;
|
||||
haveLastActiveTx_ = false;
|
||||
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
||||
rxActiveRawHigh_;
|
||||
state_ = DriverState::SETTLING;
|
||||
@@ -147,7 +172,7 @@ void DriverTest::pollTaskEntry(void *context) {
|
||||
static_cast<DriverTest *>(context)->pollTaskLoop();
|
||||
}
|
||||
|
||||
void DriverTest::pollTaskLoop() {
|
||||
void IRAM_ATTR DriverTest::pollTaskLoop() {
|
||||
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
@@ -202,6 +227,10 @@ void DriverTest::pollTaskLoop() {
|
||||
portEXIT_CRITICAL(&pollMux_);
|
||||
critical = false;
|
||||
flushHot();
|
||||
// This marker is written only after every TX/RX edge from the completed
|
||||
// sampling window. The analyzer may now safely decide that an ACK was
|
||||
// absent without racing the producer that writes those edges.
|
||||
recordRaw(esp_cpu_get_cycle_count(), false, Source::WINDOW_END);
|
||||
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
|
||||
nextStart = lastTxStart + pollPeriodCycles_;
|
||||
sawTxStart = false;
|
||||
@@ -248,12 +277,24 @@ void DriverTest::analyzerTaskLoop() {
|
||||
stats_.droppedItems += dropped;
|
||||
fail(FailReason::DATA_LOSS, lastEventTick_);
|
||||
}
|
||||
if (!count && state_ == DriverState::SETTLING &&
|
||||
static_cast<uint64_t>(esp_timer_get_time()) >=
|
||||
settlingDeadlineUs_) {
|
||||
fail(FailReason::ACK_MISSING, lastEventTick_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DriverTest::processEvent(const TimedEvent &event) {
|
||||
lastEventTick_ = event.tick;
|
||||
if (event.source == Source::WINDOW_END) {
|
||||
if (state_ == DriverState::RUNNING) {
|
||||
expirePending(event.tick);
|
||||
if (state_ == DriverState::RUNNING) completeIfPossible(event.tick);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!havePointOrigin_) {
|
||||
pointOriginTick_ = event.tick;
|
||||
havePointOrigin_ = true;
|
||||
@@ -270,12 +311,15 @@ void DriverTest::processSettling(const TimedEvent &event) {
|
||||
}
|
||||
const uint8_t rawLevel = event.rising ? HIGH : LOW;
|
||||
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
|
||||
if (!lightOn) return;
|
||||
if (lightOn != txPulseLightOn_) return;
|
||||
if (settledCycles_ < settleCycles_) {
|
||||
++settledCycles_;
|
||||
return;
|
||||
}
|
||||
if (rxActive_) return;
|
||||
if (rxActive_) {
|
||||
fail(FailReason::DRIVER_FAULT, event.tick);
|
||||
return;
|
||||
}
|
||||
state_ = DriverState::RUNNING;
|
||||
measurementStartTick_ = event.tick;
|
||||
const uint64_t measuredBefore =
|
||||
@@ -284,26 +328,35 @@ void DriverTest::processSettling(const TimedEvent &event) {
|
||||
completedSubsamples_ + 1U == SUBSAMPLE_COUNT ?
|
||||
testTicks_ - measuredBefore : subsampleTicks_;
|
||||
deadlineTick_ = event.tick + thisSubsampleTicks;
|
||||
processTx(event, true);
|
||||
processTx(event, lightOn);
|
||||
}
|
||||
|
||||
void DriverTest::processRunning(const TimedEvent &event) {
|
||||
expirePending(event.tick);
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
|
||||
if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
|
||||
const uint64_t delay = response_.associated ?
|
||||
response_.startTick - response_.tx.tick : 0;
|
||||
fail(FailReason::SHORT_CIRCUIT_FAULT, event.tick, delay,
|
||||
event.tick - response_.startTick);
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : delay;
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, delay,
|
||||
event.tick - response_.startTick, trigger);
|
||||
return;
|
||||
}
|
||||
if (event.source == Source::TX) {
|
||||
expirePending(event.tick);
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
const uint8_t rawLevel = event.rising ? HIGH : LOW;
|
||||
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
|
||||
if (event.tick < deadlineTick_) processTx(event, lightOn);
|
||||
else measurementClosed_ = true;
|
||||
} else processRx(event, event.rising == rxActiveRawHigh_);
|
||||
} else {
|
||||
// A delayed fault indication can start after the normal ACK deadline.
|
||||
// Measure the RX pulse before expiring its possible causal TX edge.
|
||||
processRx(event, event.rising == rxActiveRawHigh_);
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
if (!response_.active) expirePending(event.tick);
|
||||
}
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
completeIfPossible(event.tick);
|
||||
}
|
||||
|
||||
@@ -318,6 +371,10 @@ bool DriverTest::addPending(uint64_t tick, bool lightOn) {
|
||||
|
||||
void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
|
||||
if (!addPending(event.tick, lightOn)) return;
|
||||
if (lightOn == txPulseLightOn_) {
|
||||
lastActiveTxTick_ = event.tick;
|
||||
haveLastActiveTx_ = true;
|
||||
}
|
||||
++stats_.inputEdges;
|
||||
}
|
||||
|
||||
@@ -358,10 +415,11 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
|
||||
if (!response_.active) return;
|
||||
const uint64_t width = event.tick - response_.startTick;
|
||||
if (!response_.associated) {
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : 0;
|
||||
response_ = {};
|
||||
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
|
||||
FailReason::GATE_MONITOR_FAULT,
|
||||
event.tick, 0, width);
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,17 +428,19 @@ void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
|
||||
if (pending_[i].tick > response_.startTick &&
|
||||
event.tick - pending_[i].tick >= guard) {
|
||||
const uint64_t delay = response_.startTick - response_.tx.tick;
|
||||
const uint64_t trigger = event.tick - pending_[i].tick;
|
||||
response_ = {};
|
||||
fail(FailReason::ACK_MERGED, event.tick, delay, width);
|
||||
fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (width >= faultLongTicks_) {
|
||||
const uint64_t delay = response_.startTick - response_.tx.tick;
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : delay;
|
||||
response_ = {};
|
||||
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
|
||||
FailReason::GATE_MONITOR_FAULT,
|
||||
event.tick, delay, width);
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -424,8 +484,11 @@ void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
|
||||
|
||||
void DriverTest::expirePending(uint64_t now) {
|
||||
for (uint8_t i = 0; i < pendingCount_; ++i) {
|
||||
if (now <= pending_[i].tick + ackStartMaxTicks_) continue;
|
||||
fail(FailReason::ACK_MISSING, now, now - pending_[i].tick, 0);
|
||||
if (now < pending_[i].tick + ackStartMaxTicks_) continue;
|
||||
// The failure belongs to the ACK deadline itself. A later TX edge or the
|
||||
// end-of-window marker is only the safe moment when absence is confirmed.
|
||||
fail(FailReason::ACK_MISSING,
|
||||
pending_[i].tick + ackStartMaxTicks_, ackStartMaxTicks_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -456,12 +519,18 @@ void DriverTest::completeIfPossible(uint64_t now) {
|
||||
}
|
||||
|
||||
void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay,
|
||||
uint64_t pulseWidth) {
|
||||
uint64_t pulseWidth, uint64_t triggerAfterTx) {
|
||||
if (state_ == DriverState::FAIL || state_ == DriverState::PASS) return;
|
||||
if (stats_.reason == FailReason::NONE) {
|
||||
stats_.reason = reason;
|
||||
if (tick && havePointOrigin_ && tick >= pointOriginTick_)
|
||||
stats_.errorElapsedTicks = tick - pointOriginTick_;
|
||||
const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay;
|
||||
if (trigger) {
|
||||
stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(trigger);
|
||||
stats_.errorTriggerValid = true;
|
||||
}
|
||||
if (delay) {
|
||||
stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(delay);
|
||||
@@ -603,16 +672,20 @@ void DriverTest::printSummary() const {
|
||||
static_cast<unsigned long>(publishedStats_.unexpectedResponses),
|
||||
failName(publishedStats_.reason));
|
||||
if (publishedStats_.reason != FailReason::NONE) {
|
||||
Log::printf("DRIVER",
|
||||
"error timing: T=%lluns D=%s%lluns P=%s%lluns",
|
||||
static_cast<unsigned long long>(
|
||||
ticksToNs(publishedStats_.errorElapsedTicks)),
|
||||
publishedStats_.errorDelayValid ? "" : "N/A/",
|
||||
static_cast<unsigned long long>(
|
||||
ticksToNs(publishedStats_.errorDelayTicks)),
|
||||
publishedStats_.errorPulseValid ? "" : "N/A/",
|
||||
static_cast<unsigned long long>(
|
||||
ticksToNs(publishedStats_.errorPulseTicks)));
|
||||
auto formatOptional = [&](bool valid, uint32_t ticks,
|
||||
char *out, size_t size) {
|
||||
if (!valid) snprintf(out, size, "---");
|
||||
else snprintf(out, size, "%lluns",
|
||||
static_cast<unsigned long long>(ticksToNs(ticks)));
|
||||
};
|
||||
char trigger[24], pulse[24];
|
||||
formatOptional(publishedStats_.errorTriggerValid,
|
||||
publishedStats_.errorTriggerTicks, trigger, sizeof(trigger));
|
||||
formatOptional(publishedStats_.errorPulseValid,
|
||||
publishedStats_.errorPulseTicks, pulse, sizeof(pulse));
|
||||
if (publishedStats_.errorPulseValid)
|
||||
Log::printf("DRIVER", "error timing: T=%s P=%s", trigger, pulse);
|
||||
else Log::printf("DRIVER", "error timing: T=%s", trigger);
|
||||
}
|
||||
printEdge("ON", publishedStats_.turnOn);
|
||||
printEdge("OFF", publishedStats_.turnOff);
|
||||
|
||||
@@ -31,8 +31,10 @@ struct DriverStats {
|
||||
uint32_t droppedItems;
|
||||
uint32_t unexpectedResponses;
|
||||
uint64_t errorElapsedTicks;
|
||||
uint32_t errorTriggerTicks;
|
||||
uint32_t errorDelayTicks;
|
||||
uint32_t errorPulseTicks;
|
||||
bool errorTriggerValid;
|
||||
bool errorDelayValid;
|
||||
bool errorPulseValid;
|
||||
DriverEdgeStats turnOn;
|
||||
@@ -59,10 +61,13 @@ class DriverTest {
|
||||
const DriverStats &stats() const { return publishedStats_; }
|
||||
|
||||
private:
|
||||
enum class Source : uint8_t { TX, RX };
|
||||
enum class Source : uint8_t { TX, RX, WINDOW_END };
|
||||
struct RawEvent { uint32_t tick; bool rising; Source source; };
|
||||
struct TimedEvent { uint64_t tick; bool rising; Source source; };
|
||||
struct PendingTx { uint64_t tick; bool lightOn; };
|
||||
struct PendingTx {
|
||||
uint64_t tick;
|
||||
bool lightOn;
|
||||
};
|
||||
struct Response {
|
||||
bool active;
|
||||
bool associated;
|
||||
@@ -80,7 +85,7 @@ class DriverTest {
|
||||
static void analyzerTaskEntry(void *context);
|
||||
static void pollTaskEntry(void *context);
|
||||
void analyzerTaskLoop();
|
||||
void pollTaskLoop();
|
||||
void IRAM_ATTR pollTaskLoop();
|
||||
void processEvent(const TimedEvent &event);
|
||||
void processSettling(const TimedEvent &event);
|
||||
void processRunning(const TimedEvent &event);
|
||||
@@ -94,7 +99,7 @@ class DriverTest {
|
||||
uint64_t mergeGuardTicks() const;
|
||||
void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn);
|
||||
void fail(FailReason reason, uint64_t tick = 0, uint64_t delay = 0,
|
||||
uint64_t pulseWidth = 0);
|
||||
uint64_t pulseWidth = 0, uint64_t triggerAfterTx = 0);
|
||||
void publishStats();
|
||||
void rememberTrace(const TimedEvent &event);
|
||||
bool armCapture();
|
||||
@@ -130,10 +135,13 @@ class DriverTest {
|
||||
volatile bool captureReady_ = false;
|
||||
volatile bool core0WdtDisabled_ = false;
|
||||
uint32_t captureHz_ = 0;
|
||||
uint32_t captureFrequencyHz_ = 0;
|
||||
uint32_t capturePulseNs_ = 0;
|
||||
uint32_t pollPeriodCycles_ = 0;
|
||||
uint32_t pollWindowBeforeCycles_ = 0;
|
||||
uint32_t pollWindowAfterCycles_ = 0;
|
||||
bool pollTxStartRawHigh_ = false;
|
||||
bool captureTxLightOn_ = true;
|
||||
portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
PendingTx pending_[MAX_PENDING] = {};
|
||||
@@ -141,21 +149,25 @@ class DriverTest {
|
||||
Response response_ = {};
|
||||
uint64_t ackStartMaxTicks_ = 0;
|
||||
uint64_t faultLongTicks_ = 0;
|
||||
uint64_t shortCircuitTicks_ = 0;
|
||||
uint64_t stuckTicks_ = 0;
|
||||
uint64_t testTicks_ = 0;
|
||||
uint64_t subsampleTicks_ = 0;
|
||||
uint64_t settlingTimeoutUs_ = 0;
|
||||
uint64_t settlingDeadlineUs_ = 0;
|
||||
uint64_t measurementStartTick_ = 0;
|
||||
uint64_t deadlineTick_ = 0;
|
||||
uint64_t pointOriginTick_ = 0;
|
||||
uint64_t lastEventTick_ = 0;
|
||||
uint64_t lastActiveTxTick_ = 0;
|
||||
uint8_t settleCycles_ = 0;
|
||||
uint8_t settledCycles_ = 0;
|
||||
uint8_t completedSubsamples_ = 0;
|
||||
bool rxActiveRawHigh_ = true;
|
||||
bool txPulseLightOn_ = true;
|
||||
bool rxActive_ = false;
|
||||
bool measurementClosed_ = false;
|
||||
bool havePointOrigin_ = false;
|
||||
bool haveLastActiveTx_ = false;
|
||||
bool haveRawTick_ = false;
|
||||
uint32_t lastRawTick_ = 0;
|
||||
uint64_t tickEpoch_ = 0;
|
||||
|
||||
@@ -75,6 +75,7 @@ bool PulseReceiver::begin() {
|
||||
|
||||
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
|
||||
bool activeLightOn) {
|
||||
(void)activeLightOn;
|
||||
if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
|
||||
expectedHz_ = expectedHz;
|
||||
expectedDutyPct_ = expectedDutyPct;
|
||||
@@ -83,11 +84,7 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
|
||||
if (!cpuTickHz_) return false;
|
||||
#endif
|
||||
resetStream();
|
||||
const bool rawHighMeansLightOn = RX_LIGHT_ON_GPIO_LEVEL == HIGH;
|
||||
activeStartRising_ = rawHighMeansLightOn == activeLightOn;
|
||||
Log::printf("CAPTURE", "RX active optical level=%s, raw active starts on %s",
|
||||
activeLightOn ? "H/light-on" : "L/light-off",
|
||||
activeStartRising_ ? "RISING" : "FALLING");
|
||||
Log::event("CAPTURE", "RX optical polarity will be detected automatically");
|
||||
return startCapture(false);
|
||||
}
|
||||
|
||||
@@ -209,6 +206,8 @@ void PulseReceiver::resetStream() {
|
||||
droppedItems_ = 0;
|
||||
polarityKnown_ = false;
|
||||
activeStartRising_ = false;
|
||||
polarityEdgeCount_ = 0;
|
||||
memset(polarityEdges_, 0, sizeof(polarityEdges_));
|
||||
waitingForActiveEnd_ = true;
|
||||
activeStart_ = activeEnd_ = 0;
|
||||
haveRawTick_ = false;
|
||||
@@ -244,12 +243,42 @@ bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// HH/HL/LH/LL defines the active optical state explicitly. Synchronize on
|
||||
// its physical starting edge instead of guessing polarity from pulse width.
|
||||
if (edge.rising != activeStartRising_) return false;
|
||||
// Optical mode does not use the DRIVER level setting. Compare the first two
|
||||
// alternating intervals with the configured active duration and select the
|
||||
// level that is actually present at RX. Three edges are enough to determine
|
||||
// polarity and, when the first interval is active, produce the first period.
|
||||
polarityEdges_[polarityEdgeCount_++] = edge;
|
||||
if (polarityEdgeCount_ < 3U) return false;
|
||||
|
||||
const uint64_t firstInterval =
|
||||
polarityEdges_[1].tick - polarityEdges_[0].tick;
|
||||
const uint64_t secondInterval =
|
||||
polarityEdges_[2].tick - polarityEdges_[1].tick;
|
||||
const uint64_t expectedPeriod = tickHz() / expectedHz_;
|
||||
const uint64_t expectedActive = static_cast<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;
|
||||
activeStart_ = edge.tick;
|
||||
waitingForActiveEnd_ = true;
|
||||
polarityEdgeCount_ = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -268,10 +297,11 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
|
||||
static_cast<uint8_t>(channel == self->txChannel_ ? CaptureSource::TX :
|
||||
CaptureSource::RX)};
|
||||
if (self->txCaptureEnabled_) {
|
||||
// Three capture channels are independent ISR producers. Serialize their
|
||||
// reservation/publication of a ring slot; treating this as an SPSC ring
|
||||
// loses or duplicates RX events when TX and RX interrupts overlap.
|
||||
portENTER_CRITICAL_ISR(&self->driverRingMux_);
|
||||
// All channels in one MCPWM group are dispatched serially by the same
|
||||
// group ISR. Keep this callback shorter than the minimum interval between
|
||||
// equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic
|
||||
// RMW operations here can leave the channel pending until its capture
|
||||
// register is overwritten by the next edge.
|
||||
if (self->haveLastDriverEdge_ &&
|
||||
self->lastDriverEdge_.tick == edge.tick &&
|
||||
self->lastDriverEdge_.rising == edge.rising &&
|
||||
@@ -279,22 +309,20 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
|
||||
// The same channel callback can be delivered twice while several MCPWM
|
||||
// capture status bits are pending. Two physical edges cannot have the
|
||||
// same source, direction and 12.5 ns hardware timestamp.
|
||||
portEXIT_CRITICAL_ISR(&self->driverRingMux_);
|
||||
return false;
|
||||
}
|
||||
const uint16_t write = __atomic_load_n(
|
||||
&self->driverRingWrite_, __ATOMIC_RELAXED);
|
||||
const uint16_t write = self->driverRingWrite_;
|
||||
const uint16_t next = static_cast<uint16_t>(
|
||||
(write + 1U) % DRIVER_RING_CAPACITY);
|
||||
if (next == __atomic_load_n(&self->driverRingRead_, __ATOMIC_ACQUIRE)) {
|
||||
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
|
||||
(write + 1U) & (DRIVER_RING_CAPACITY - 1U));
|
||||
if (next == self->driverRingRead_) {
|
||||
++self->droppedItems_;
|
||||
} else {
|
||||
self->driverRing_[write] = edge;
|
||||
self->lastDriverEdge_ = edge;
|
||||
self->haveLastDriverEdge_ = true;
|
||||
__atomic_store_n(&self->driverRingWrite_, next, __ATOMIC_RELEASE);
|
||||
asm volatile("memw" ::: "memory");
|
||||
self->driverRingWrite_ = next;
|
||||
}
|
||||
portEXIT_CRITICAL_ISR(&self->driverRingMux_);
|
||||
return false;
|
||||
}
|
||||
BaseType_t wake = pdFALSE;
|
||||
@@ -350,7 +378,7 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
|
||||
|
||||
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
||||
TickType_t waitTicks) {
|
||||
if (!events || capacity < 2U || !txCaptureEnabled_ || !driverPulseTicks_ ||
|
||||
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
|
||||
!driverReleaseSlackTicks_) return 0;
|
||||
|
||||
constexpr size_t MAX_BATCH = 64;
|
||||
@@ -367,30 +395,42 @@ size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
||||
// can the missing start interrupt be reconstructed and sorted before RX.
|
||||
const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE);
|
||||
uint16_t scan = read;
|
||||
bool haveTxEnd = false;
|
||||
uint32_t lastTxEnd = 0;
|
||||
bool haveLatestTxEnd = false;
|
||||
bool haveReleaseTxEnd = false;
|
||||
uint32_t latestTxEnd = 0;
|
||||
uint32_t releaseTxEnd = 0;
|
||||
size_t projectedCount = 0;
|
||||
while (scan != write) {
|
||||
const Edge &edge = driverRing_[scan];
|
||||
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
|
||||
? 2U : 1U;
|
||||
if (projectedCount + needed > limit) break;
|
||||
// Reserve one output slot for WINDOW_END.
|
||||
if (projectedCount + needed + 1U > limit) break;
|
||||
projectedCount += needed;
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
|
||||
lastTxEnd = edge.tick;
|
||||
haveTxEnd = true;
|
||||
if (haveLatestTxEnd) {
|
||||
releaseTxEnd = latestTxEnd;
|
||||
haveReleaseTxEnd = true;
|
||||
}
|
||||
latestTxEnd = edge.tick;
|
||||
haveLatestTxEnd = true;
|
||||
}
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t releaseThrough = lastTxEnd + driverReleaseSlackTicks_;
|
||||
const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_;
|
||||
size_t count = 0;
|
||||
while (read != write) {
|
||||
const Edge edge = driverRing_[read];
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::TX) &&
|
||||
static_cast<int32_t>(edge.tick - releaseTxEnd) > 0)
|
||||
break;
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::RX) &&
|
||||
static_cast<int32_t>(edge.tick - releaseThrough) > 0)
|
||||
break;
|
||||
@@ -426,5 +466,9 @@ size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
||||
events[i] = {timed.tick, timed.rising,
|
||||
static_cast<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;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#define OPTICAL_USE_MCPWM_CAPTURE 0
|
||||
#endif
|
||||
|
||||
enum class CaptureSource : uint8_t { RX, TX };
|
||||
enum class CaptureSource : uint8_t { RX, TX, WINDOW_END };
|
||||
|
||||
struct CaptureEvent {
|
||||
uint64_t tick;
|
||||
@@ -43,6 +43,8 @@ class PulseReceiver {
|
||||
private:
|
||||
struct Edge { uint32_t tick; uint8_t rising; uint8_t source; };
|
||||
static constexpr uint16_t DRIVER_RING_CAPACITY = 512;
|
||||
static_assert((DRIVER_RING_CAPACITY & (DRIVER_RING_CAPACITY - 1U)) == 0,
|
||||
"driver capture ring must be a power of two");
|
||||
struct TimedEdge { uint64_t tick; bool rising; };
|
||||
bool startCapture(bool withTx);
|
||||
bool consumeEdge(const Edge &edge, PulsePeriod &period);
|
||||
@@ -78,6 +80,8 @@ class PulseReceiver {
|
||||
uint32_t expectedHz_ = 0;
|
||||
float expectedDutyPct_ = 50.0f;
|
||||
bool polarityKnown_ = false, activeStartRising_ = false;
|
||||
TimedEdge polarityEdges_[3] = {};
|
||||
uint8_t polarityEdgeCount_ = 0;
|
||||
bool waitingForActiveEnd_ = true;
|
||||
uint64_t activeStart_ = 0, activeEnd_ = 0;
|
||||
bool haveRawTick_ = false;
|
||||
|
||||
Reference in New Issue
Block a user