636 lines
22 KiB
C++
636 lines
22 KiB
C++
#include "DriverTest.h"
|
|
|
|
#include "Config.h"
|
|
#include "Log.h"
|
|
|
|
#include <driver/gpio.h>
|
|
#include <esp_cpu.h>
|
|
#include <esp32-hal-cpu.h>
|
|
#include <soc/gpio_struct.h>
|
|
#include <string.h>
|
|
|
|
void DriverEdgeStats::reset() {
|
|
memset(this, 0, sizeof(*this));
|
|
minDelayTicks = minResponseTicks = UINT32_MAX;
|
|
}
|
|
|
|
void DriverStats::reset() {
|
|
memset(this, 0, sizeof(*this));
|
|
minDelayTicks = minResponseTicks = UINT32_MAX;
|
|
turnOn.reset();
|
|
turnOff.reset();
|
|
reason = FailReason::NONE;
|
|
}
|
|
|
|
uint64_t DriverTest::nsToTicks(uint32_t ns) const {
|
|
return (static_cast<uint64_t>(ns) * captureHz_ + 999999999ULL) /
|
|
1000000000ULL;
|
|
}
|
|
|
|
uint64_t DriverTest::ticksToNs(uint64_t ticks) const {
|
|
return (ticks * 1000000000ULL + captureHz_ / 2U) / captureHz_;
|
|
}
|
|
|
|
bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
|
float tolerancePct, uint32_t testTimeMs,
|
|
uint8_t settleCycles, bool activeTxLightOn,
|
|
bool activeRxLightOn) {
|
|
(void)tolerancePct;
|
|
(void)activeRxLightOn;
|
|
if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
|
|
!testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false;
|
|
|
|
requestCaptureStop();
|
|
if (!waitCaptureStopped(25U)) return false;
|
|
if (!pollTask_ && xTaskCreatePinnedToCore(pollTaskEntry, "driver-poll",
|
|
3072, this, configMAX_PRIORITIES - 1U, &pollTask_, 0) != pdPASS)
|
|
return false;
|
|
if (!analyzerTask_ && xTaskCreatePinnedToCore(analyzerTaskEntry,
|
|
"driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
|
|
return false;
|
|
|
|
captureHz_ = getCpuFrequencyMhz() * 1000000UL;
|
|
if (!captureHz_ || captureHz_ % frequencyHz) return false;
|
|
pollPeriodCycles_ = captureHz_ / frequencyHz;
|
|
pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us
|
|
const uint64_t periodNs = 1000000000ULL / frequencyHz;
|
|
uint64_t windowNs = pulseNs + 50000ULL;
|
|
const uint64_t maximumWindowNs = periodNs * 3ULL / 4ULL;
|
|
if (windowNs > maximumWindowNs) windowNs = maximumWindowNs;
|
|
pollWindowAfterCycles_ = static_cast<uint32_t>(
|
|
windowNs * captureHz_ / 1000000000ULL);
|
|
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
|
|
|
|
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_ ||
|
|
!testTicks_ || !subsampleTicks_)
|
|
return false;
|
|
|
|
clearCapture();
|
|
stats_.reset();
|
|
publishStats();
|
|
pendingCount_ = 0;
|
|
response_ = {};
|
|
measurementStartTick_ = deadlineTick_ = 0;
|
|
pointOriginTick_ = lastEventTick_ = 0;
|
|
settleCycles_ = settleCycles;
|
|
settledCycles_ = 0;
|
|
completedSubsamples_ = 0;
|
|
measurementClosed_ = false;
|
|
havePointOrigin_ = false;
|
|
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
|
rxActiveRawHigh_;
|
|
currentStep_ = 0;
|
|
traceWrite_ = traceCount_ = 0;
|
|
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
|
state_ = DriverState::SETTLING;
|
|
return armCapture();
|
|
}
|
|
|
|
bool DriverTest::armCapture() {
|
|
Serial.flush();
|
|
if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) {
|
|
const bool disabled = disableCore0WDT();
|
|
__atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE);
|
|
if (!disabled) {
|
|
state_ = DriverState::IDLE;
|
|
return false;
|
|
}
|
|
}
|
|
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
|
|
__atomic_store_n(&captureActive_, true, __ATOMIC_RELEASE);
|
|
xTaskNotifyGive(pollTask_);
|
|
const uint32_t readyDeadline = millis() + 25U;
|
|
while (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
|
|
static_cast<int32_t>(millis() - readyDeadline) < 0) delay(0);
|
|
if (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) {
|
|
requestCaptureStop();
|
|
waitCaptureStopped(25U);
|
|
state_ = DriverState::IDLE;
|
|
return false;
|
|
}
|
|
xTaskNotifyGive(analyzerTask_);
|
|
return true;
|
|
}
|
|
|
|
bool DriverTest::resumeSubsample() {
|
|
if (state_ != DriverState::SUBSAMPLE_DONE) return false;
|
|
if (!waitCaptureStopped(25U)) {
|
|
fail(FailReason::DATA_LOSS, lastEventTick_);
|
|
return false;
|
|
}
|
|
|
|
clearCapture();
|
|
pendingCount_ = 0;
|
|
response_ = {};
|
|
measurementStartTick_ = deadlineTick_ = 0;
|
|
settledCycles_ = 0;
|
|
measurementClosed_ = false;
|
|
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
|
rxActiveRawHigh_;
|
|
state_ = DriverState::SETTLING;
|
|
if (armCapture()) return true;
|
|
fail(FailReason::DATA_LOSS, lastEventTick_);
|
|
return false;
|
|
}
|
|
|
|
void DriverTest::pollTaskEntry(void *context) {
|
|
static_cast<DriverTest *>(context)->pollTaskLoop();
|
|
}
|
|
|
|
void DriverTest::pollTaskLoop() {
|
|
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
|
|
for (;;) {
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
uint32_t levels = GPIO.in & PIN_MASK;
|
|
uint32_t nextStart = 0;
|
|
uint32_t windowEnd = 0;
|
|
uint32_t lastTxStart = 0;
|
|
RawEvent hotEvents[32] = {};
|
|
uint8_t hotCount = 0;
|
|
bool sawTxStart = false;
|
|
bool critical = false;
|
|
|
|
auto sampleOnce = [&]() {
|
|
const uint32_t current = GPIO.in & PIN_MASK;
|
|
if (current == levels) return;
|
|
const uint32_t now = esp_cpu_get_cycle_count();
|
|
const uint32_t changed = current ^ levels;
|
|
if ((changed & (1UL << GPIO_PWM)) && hotCount < 32U)
|
|
hotEvents[hotCount++] = {now,
|
|
(current & (1UL << GPIO_PWM)) != 0U, Source::TX};
|
|
if ((changed & (1UL << GPIO_RX)) && hotCount < 32U)
|
|
hotEvents[hotCount++] = {now,
|
|
(current & (1UL << GPIO_RX)) != 0U, Source::RX};
|
|
if ((changed & (1UL << GPIO_PWM)) &&
|
|
((current & (1UL << GPIO_PWM)) != 0U) == pollTxStartRawHigh_) {
|
|
lastTxStart = now;
|
|
sawTxStart = true;
|
|
}
|
|
levels = current;
|
|
};
|
|
|
|
auto flushHot = [&]() {
|
|
for (uint8_t i = 0; i < hotCount; ++i)
|
|
recordRaw(hotEvents[i].tick, hotEvents[i].rising,
|
|
hotEvents[i].source);
|
|
hotCount = 0;
|
|
};
|
|
|
|
portENTER_CRITICAL(&pollMux_);
|
|
critical = true;
|
|
__atomic_store_n(&captureReady_, true, __ATOMIC_RELEASE);
|
|
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && !sawTxStart)
|
|
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
|
if (sawTxStart) windowEnd = lastTxStart + pollWindowAfterCycles_;
|
|
const bool synchronized = sawTxStart;
|
|
|
|
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && synchronized) {
|
|
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
|
|
static_cast<int32_t>(esp_cpu_get_cycle_count() - windowEnd) < 0)
|
|
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
|
|
|
portEXIT_CRITICAL(&pollMux_);
|
|
critical = false;
|
|
flushHot();
|
|
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
|
|
nextStart = lastTxStart + pollPeriodCycles_;
|
|
sawTxStart = false;
|
|
|
|
uint32_t outsideSpins = 0;
|
|
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
|
|
static_cast<int32_t>(esp_cpu_get_cycle_count() -
|
|
(nextStart - pollWindowBeforeCycles_)) < 0) {
|
|
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
|
if (++outsideSpins >= 256U) {
|
|
outsideSpins = 0;
|
|
taskYIELD();
|
|
}
|
|
}
|
|
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
|
|
|
|
portENTER_CRITICAL(&pollMux_);
|
|
critical = true;
|
|
windowEnd = nextStart + pollWindowAfterCycles_;
|
|
}
|
|
if (critical) portEXIT_CRITICAL(&pollMux_);
|
|
flushHot();
|
|
if (__atomic_exchange_n(&core0WdtDisabled_, false,
|
|
__ATOMIC_ACQ_REL)) enableCore0WDT();
|
|
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
|
|
}
|
|
}
|
|
|
|
void DriverTest::analyzerTaskEntry(void *context) {
|
|
static_cast<DriverTest *>(context)->analyzerTaskLoop();
|
|
}
|
|
|
|
void DriverTest::analyzerTaskLoop() {
|
|
TimedEvent events[64] = {};
|
|
for (;;) {
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
while (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING) {
|
|
const size_t count = readRaw(events, 64, pdMS_TO_TICKS(1));
|
|
for (size_t i = 0; i < count &&
|
|
(state_ == DriverState::SETTLING || state_ == DriverState::RUNNING);
|
|
++i) processEvent(events[i]);
|
|
const uint32_t dropped = takeDropped();
|
|
if (dropped) {
|
|
stats_.droppedItems += dropped;
|
|
fail(FailReason::DATA_LOSS, lastEventTick_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DriverTest::processEvent(const TimedEvent &event) {
|
|
lastEventTick_ = event.tick;
|
|
if (!havePointOrigin_) {
|
|
pointOriginTick_ = event.tick;
|
|
havePointOrigin_ = true;
|
|
}
|
|
rememberTrace(event);
|
|
if (state_ == DriverState::SETTLING) processSettling(event);
|
|
else if (state_ == DriverState::RUNNING) processRunning(event);
|
|
}
|
|
|
|
void DriverTest::processSettling(const TimedEvent &event) {
|
|
if (event.source == Source::RX) {
|
|
rxActive_ = event.rising == rxActiveRawHigh_;
|
|
return;
|
|
}
|
|
const uint8_t rawLevel = event.rising ? HIGH : LOW;
|
|
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
|
|
if (!lightOn) return;
|
|
if (settledCycles_ < settleCycles_) {
|
|
++settledCycles_;
|
|
return;
|
|
}
|
|
if (rxActive_) return;
|
|
state_ = DriverState::RUNNING;
|
|
measurementStartTick_ = event.tick;
|
|
const uint64_t measuredBefore =
|
|
static_cast<uint64_t>(completedSubsamples_) * subsampleTicks_;
|
|
const uint64_t thisSubsampleTicks =
|
|
completedSubsamples_ + 1U == SUBSAMPLE_COUNT ?
|
|
testTicks_ - measuredBefore : subsampleTicks_;
|
|
deadlineTick_ = event.tick + thisSubsampleTicks;
|
|
processTx(event, true);
|
|
}
|
|
|
|
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);
|
|
return;
|
|
}
|
|
if (event.source == Source::TX) {
|
|
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_);
|
|
completeIfPossible(event.tick);
|
|
}
|
|
|
|
bool DriverTest::addPending(uint64_t tick, bool lightOn) {
|
|
if (pendingCount_ >= MAX_PENDING) {
|
|
fail(FailReason::DATA_LOSS, tick);
|
|
return false;
|
|
}
|
|
pending_[pendingCount_++] = {tick, lightOn};
|
|
return true;
|
|
}
|
|
|
|
void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
|
|
if (!addPending(event.tick, lightOn)) return;
|
|
++stats_.inputEdges;
|
|
}
|
|
|
|
int8_t DriverTest::matchingPending(uint64_t rxTick) const {
|
|
for (uint8_t i = 0; i < pendingCount_; ++i)
|
|
if (rxTick >= pending_[i].tick &&
|
|
rxTick - pending_[i].tick <= ackStartMaxTicks_)
|
|
return static_cast<int8_t>(i);
|
|
return -1;
|
|
}
|
|
|
|
void DriverTest::removePending(uint8_t index) {
|
|
if (index >= pendingCount_) return;
|
|
for (uint8_t i = index + 1U; i < pendingCount_; ++i)
|
|
pending_[i - 1U] = pending_[i];
|
|
--pendingCount_;
|
|
}
|
|
|
|
void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
|
|
rxActive_ = activeNow;
|
|
if (activeNow) {
|
|
if (response_.active) {
|
|
fail(FailReason::DATA_LOSS, event.tick);
|
|
return;
|
|
}
|
|
response_ = {};
|
|
response_.active = true;
|
|
response_.startTick = event.tick;
|
|
const int8_t index = matchingPending(event.tick);
|
|
if (index >= 0) {
|
|
response_.associated = true;
|
|
response_.tx = pending_[index];
|
|
removePending(static_cast<uint8_t>(index));
|
|
} else ++stats_.unexpectedResponses;
|
|
return;
|
|
}
|
|
|
|
if (!response_.active) return;
|
|
const uint64_t width = event.tick - response_.startTick;
|
|
if (!response_.associated) {
|
|
response_ = {};
|
|
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
|
|
FailReason::GATE_MONITOR_FAULT,
|
|
event.tick, 0, width);
|
|
return;
|
|
}
|
|
|
|
const uint64_t guard = mergeGuardTicks();
|
|
for (uint8_t i = 0; i < pendingCount_; ++i) {
|
|
if (pending_[i].tick > response_.startTick &&
|
|
event.tick - pending_[i].tick >= guard) {
|
|
const uint64_t delay = response_.startTick - response_.tx.tick;
|
|
response_ = {};
|
|
fail(FailReason::ACK_MERGED, event.tick, delay, width);
|
|
return;
|
|
}
|
|
}
|
|
if (width >= faultLongTicks_) {
|
|
const uint64_t delay = response_.startTick - response_.tx.tick;
|
|
response_ = {};
|
|
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
|
|
FailReason::GATE_MONITOR_FAULT,
|
|
event.tick, delay, width);
|
|
return;
|
|
}
|
|
|
|
const uint64_t delay = response_.startTick - response_.tx.tick;
|
|
const bool lightOn = response_.tx.lightOn;
|
|
response_ = {};
|
|
acceptAcknowledgement(delay, width, lightOn);
|
|
}
|
|
|
|
uint64_t DriverTest::mergeGuardTicks() const {
|
|
uint32_t observedMax = stats_.maxDelayTicks;
|
|
const uint64_t baseline = observedMax ? observedMax :
|
|
nsToTicks(DRIVER_ACK_DELAY_NS + 500U);
|
|
return baseline + nsToTicks(DRIVER_ACK_MERGE_MARGIN_NS);
|
|
}
|
|
|
|
void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
|
|
bool lightOn) {
|
|
const uint32_t delay32 = delay > UINT32_MAX ? UINT32_MAX :
|
|
static_cast<uint32_t>(delay);
|
|
const uint32_t width32 = width > UINT32_MAX ? UINT32_MAX :
|
|
static_cast<uint32_t>(width);
|
|
stats_.lastDelayTicks = delay32;
|
|
stats_.lastResponseTicks = width32;
|
|
++stats_.responses;
|
|
if (delay32 < stats_.minDelayTicks) stats_.minDelayTicks = delay32;
|
|
if (delay32 > stats_.maxDelayTicks) stats_.maxDelayTicks = delay32;
|
|
if (width32 < stats_.minResponseTicks) stats_.minResponseTicks = width32;
|
|
if (width32 > stats_.maxResponseTicks) stats_.maxResponseTicks = width32;
|
|
|
|
DriverEdgeStats &edge = lightOn ? stats_.turnOn : stats_.turnOff;
|
|
++edge.responses;
|
|
edge.delaySumTicks += delay32;
|
|
edge.responseSumTicks += width32;
|
|
if (delay32 < edge.minDelayTicks) edge.minDelayTicks = delay32;
|
|
if (delay32 > edge.maxDelayTicks) edge.maxDelayTicks = delay32;
|
|
if (width32 < edge.minResponseTicks) edge.minResponseTicks = width32;
|
|
if (width32 > edge.maxResponseTicks) edge.maxResponseTicks = width32;
|
|
publishStats();
|
|
}
|
|
|
|
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);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void DriverTest::completeIfPossible(uint64_t now) {
|
|
if (state_ != DriverState::RUNNING) return;
|
|
if (!measurementClosed_ && now >= deadlineTick_) measurementClosed_ = true;
|
|
if (!measurementClosed_ || response_.active || pendingCount_) return;
|
|
if (!stats_.turnOn.responses || !stats_.turnOff.responses) {
|
|
fail(FailReason::ACK_MISSING, now);
|
|
return;
|
|
}
|
|
++completedSubsamples_;
|
|
currentStep_ = completedSubsamples_;
|
|
publishStats();
|
|
requestCaptureStop();
|
|
if (!waitCaptureStopped(25U)) {
|
|
fail(FailReason::DATA_LOSS, lastEventTick_);
|
|
return;
|
|
}
|
|
if (completedSubsamples_ >= SUBSAMPLE_COUNT) {
|
|
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
|
state_ = DriverState::PASS;
|
|
} else {
|
|
__atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
|
|
state_ = DriverState::SUBSAMPLE_DONE;
|
|
}
|
|
}
|
|
|
|
void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay,
|
|
uint64_t pulseWidth) {
|
|
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_;
|
|
if (delay) {
|
|
stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
|
|
static_cast<uint32_t>(delay);
|
|
stats_.errorDelayValid = true;
|
|
}
|
|
if (pulseWidth) {
|
|
stats_.errorPulseTicks = pulseWidth > UINT32_MAX ? UINT32_MAX :
|
|
static_cast<uint32_t>(pulseWidth);
|
|
stats_.errorPulseValid = true;
|
|
}
|
|
}
|
|
publishStats();
|
|
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
|
requestCaptureStop();
|
|
state_ = DriverState::FAIL;
|
|
}
|
|
|
|
void DriverTest::publishStats() {
|
|
portENTER_CRITICAL(&statsMux_);
|
|
publishedStats_ = stats_;
|
|
portEXIT_CRITICAL(&statsMux_);
|
|
}
|
|
|
|
void DriverTest::forceFail(FailReason reason) {
|
|
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
|
|
state_ == DriverState::SUBSAMPLE_DONE)
|
|
fail(reason, lastEventTick_);
|
|
}
|
|
|
|
void DriverTest::abort() {
|
|
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
|
|
state_ == DriverState::SUBSAMPLE_DONE)
|
|
fail(FailReason::ABORTED, lastEventTick_);
|
|
else {
|
|
requestCaptureStop();
|
|
state_ = DriverState::IDLE;
|
|
}
|
|
}
|
|
|
|
bool DriverTest::takeProgressUpdate() {
|
|
return __atomic_exchange_n(&progressUpdatePending_, false,
|
|
__ATOMIC_ACQ_REL);
|
|
}
|
|
|
|
void DriverTest::requestCaptureStop() {
|
|
__atomic_store_n(&captureActive_, false, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
bool DriverTest::waitCaptureStopped(uint32_t timeoutMs) {
|
|
const uint32_t deadline = millis() + timeoutMs;
|
|
while (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
|
|
static_cast<int32_t>(millis() - deadline) < 0) delay(0);
|
|
if (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) return false;
|
|
if (__atomic_exchange_n(&core0WdtDisabled_, false,
|
|
__ATOMIC_ACQ_REL)) enableCore0WDT();
|
|
return true;
|
|
}
|
|
|
|
void DriverTest::clearCapture() {
|
|
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
|
|
__atomic_store_n(&ringRead_, write, __ATOMIC_RELEASE);
|
|
__atomic_store_n(&droppedItems_, 0U, __ATOMIC_RELEASE);
|
|
haveRawTick_ = false;
|
|
lastRawTick_ = 0;
|
|
tickEpoch_ = 0;
|
|
}
|
|
|
|
void IRAM_ATTR DriverTest::recordRaw(uint32_t tick, bool rising,
|
|
Source source) {
|
|
const uint16_t write = ringWrite_;
|
|
const uint16_t next = static_cast<uint16_t>(
|
|
(write + 1U) & (RING_CAPACITY - 1U));
|
|
if (next == ringRead_) {
|
|
++droppedItems_;
|
|
return;
|
|
}
|
|
ring_[write] = {tick, rising, source};
|
|
asm volatile("memw" ::: "memory");
|
|
ringWrite_ = next;
|
|
}
|
|
|
|
size_t DriverTest::readRaw(TimedEvent *events, size_t capacity,
|
|
TickType_t waitTicks) {
|
|
if (!events || !capacity) return 0;
|
|
uint16_t read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
|
|
if (read == __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
|
|
vTaskDelay(waitTicks);
|
|
read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
|
|
}
|
|
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
|
|
size_t count = 0;
|
|
while (read != write && count < capacity) {
|
|
const RawEvent raw = ring_[read];
|
|
read = static_cast<uint16_t>((read + 1U) & (RING_CAPACITY - 1U));
|
|
if (haveRawTick_ && raw.tick < lastRawTick_ &&
|
|
lastRawTick_ - raw.tick > 0x80000000UL) tickEpoch_ += 1ULL << 32U;
|
|
lastRawTick_ = raw.tick;
|
|
haveRawTick_ = true;
|
|
events[count++] = {tickEpoch_ + raw.tick, raw.rising, raw.source};
|
|
}
|
|
__atomic_store_n(&ringRead_, read, __ATOMIC_RELEASE);
|
|
return count;
|
|
}
|
|
|
|
uint32_t DriverTest::takeDropped() {
|
|
return __atomic_exchange_n(&droppedItems_, 0U, __ATOMIC_ACQ_REL);
|
|
}
|
|
|
|
void DriverTest::rememberTrace(const TimedEvent &event) {
|
|
trace_[traceWrite_] = {event.tick, static_cast<uint8_t>(event.source),
|
|
static_cast<uint8_t>(event.rising), static_cast<uint8_t>(state_),
|
|
pendingCount_};
|
|
traceWrite_ = static_cast<uint8_t>((traceWrite_ + 1U) % TRACE_CAPACITY);
|
|
if (traceCount_ < TRACE_CAPACITY) ++traceCount_;
|
|
}
|
|
|
|
void DriverTest::printSummary() const {
|
|
auto printEdge = [&](const char *name, const DriverEdgeStats &edge) {
|
|
if (!edge.responses) {
|
|
Log::printf("DRIVER", "%s ACK=0", name);
|
|
return;
|
|
}
|
|
Log::printf("DRIVER",
|
|
"%s ACK=%lu D=%lluns/%lluns/%lluns P=%lluns/%lluns/%lluns",
|
|
name, static_cast<unsigned long>(edge.responses),
|
|
static_cast<unsigned long long>(ticksToNs(edge.minDelayTicks)),
|
|
static_cast<unsigned long long>(ticksToNs(
|
|
edge.delaySumTicks / edge.responses)),
|
|
static_cast<unsigned long long>(ticksToNs(edge.maxDelayTicks)),
|
|
static_cast<unsigned long long>(ticksToNs(edge.minResponseTicks)),
|
|
static_cast<unsigned long long>(ticksToNs(
|
|
edge.responseSumTicks / edge.responses)),
|
|
static_cast<unsigned long long>(ticksToNs(edge.maxResponseTicks)));
|
|
};
|
|
Log::printf("DRIVER", "TX edges=%lu responses=%lu dropped=%lu unexpected=%lu result=%s",
|
|
static_cast<unsigned long>(publishedStats_.inputEdges),
|
|
static_cast<unsigned long>(publishedStats_.responses),
|
|
static_cast<unsigned long>(publishedStats_.droppedItems),
|
|
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)));
|
|
}
|
|
printEdge("ON", publishedStats_.turnOn);
|
|
printEdge("OFF", publishedStats_.turnOff);
|
|
}
|
|
|
|
void DriverTest::printTrace() const {
|
|
if (!traceCount_) return;
|
|
const uint8_t first = static_cast<uint8_t>(
|
|
(traceWrite_ + TRACE_CAPACITY - traceCount_) % TRACE_CAPACITY);
|
|
const uint64_t origin = trace_[first].tick;
|
|
Log::printf("DRIVER", "RAM trace: %u events, tick=%luHz", traceCount_,
|
|
static_cast<unsigned long>(captureHz_));
|
|
for (uint8_t i = 0; i < traceCount_; ++i) {
|
|
const TraceEvent &event = trace_[(first + i) % TRACE_CAPACITY];
|
|
Log::printf("DRIVER", "E%02u +%lluns %s/%s state=%u pending=%u", i,
|
|
static_cast<unsigned long long>(ticksToNs(event.tick - origin)),
|
|
event.source == static_cast<uint8_t>(Source::TX) ? "TX" : "RX",
|
|
event.rising ? "rise" : "fall", event.state, event.pending);
|
|
}
|
|
}
|