Files
OptoTest/OpticalChannelTester/DriverTest.cpp

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