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

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

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