Files
OptoTest/OpticalChannelTester/Core.cpp

371 lines
17 KiB
C++

#include "Core.h"
#include "Config.h"
#include <math.h>
#include <string.h>
const char *roleName(Role r) {
static const char *names[] = {"SOLO", "MASTER", "SLAVE"};
const uint8_t i = static_cast<uint8_t>(r);
return i < 3 ? names[i] : "?";
}
const char *testKindName(TestKind kind) {
static const char *names[] = {"OPTICAL", "DRIVER"};
const uint8_t i = static_cast<uint8_t>(kind);
return i < 2 ? names[i] : "?";
}
const char *lightCodeName(LightCode code) {
static const char *names[] = {"HH", "HL", "LH", "LL"};
const uint8_t i = static_cast<uint8_t>(code);
return i < 4 ? names[i] : "??";
}
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"};
const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
}
void StageStats::reset() {
memset(this, 0, sizeof(*this));
minPeriod = minActive = UINT32_MAX;
reason = FailReason::NONE;
}
uint32_t settingsChecksum(const Settings &s) {
const uint8_t *p = reinterpret_cast<const uint8_t *>(&s);
const size_t n = offsetof(Settings, checksum);
uint32_t hash = 2166136261UL;
for (size_t i = 0; i < n; ++i) { hash ^= p[i]; hash *= 16777619UL; }
return hash;
}
bool txActiveLightOn(const Settings &s) {
return static_cast<uint8_t>(s.lightCode) < static_cast<uint8_t>(LightCode::LH);
}
bool rxActiveLightOn(const Settings &s) {
return (static_cast<uint8_t>(s.lightCode) & 1U) == 0U;
}
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs) {
if (!minPulseNs || maxPulseNs < minPulseNs) return 0;
uint32_t count = 0;
for (size_t i = 0; i < countOf(TEST_PULSE_WIDTHS_NS); ++i)
if (TEST_PULSE_WIDTHS_NS[i] >= minPulseNs && TEST_PULSE_WIDTHS_NS[i] <= maxPulseNs) ++count;
return count;
}
uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index) {
for (size_t i = countOf(TEST_PULSE_WIDTHS_NS); i > 0; --i) {
const uint32_t pulseNs = TEST_PULSE_WIDTHS_NS[i - 1U];
if (pulseNs < minPulseNs || pulseNs > maxPulseNs) continue;
if (!index--) return pulseNs;
}
return 0;
}
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles) {
if (!frequencyHz || !sampleTimeMs) return 0;
const uint64_t sampleUs = static_cast<uint64_t>(sampleTimeMs) * 1000ULL;
// At high frequency the CPU needs longer than the requested sample window
// to validate every captured period. Use the measured sustained C3 rate.
const uint64_t processingUs =
(static_cast<uint64_t>(frequencyHz) * sampleTimeMs * 1000ULL +
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
const uint64_t settleUs =
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
// Initial stage screen, nine intermediate screens and the final result.
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
(MEASUREMENT_PROGRESS_STEPS + 1U);
return samplingWallUs + settleUs + displayUs;
}
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
return static_cast<uint64_t>(pulseWidthPointCount(p.maxPulseNs, p.minPulseNs)) *
nominalStageUs(p.frequencyHz, p.testTimeMs, settleCycles);
}
bool periodWithin(float measured, float expected, float tolerance) {
return expected > 0.0f && fabsf(measured - expected) * 100.0f / expected <= tolerance + 0.0001f;
}
bool dutyWithin(float measured, float expected, float tolerance) {
return fabsf(measured - expected) <= tolerance + 0.0001f;
}
float effectiveTolerancePct(float configured) {
return configured;
}
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits) {
if (!frequencyHz || !sourceClockHz || !maxBits) return 0;
uint8_t bits = maxBits;
while (bits > 1 && static_cast<uint64_t>(frequencyHz) * (1ULL << bits) > sourceClockHz)
--bits;
return bits;
}
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct) {
const uint8_t fallback = choosePwmResolution(frequencyHz, sourceClockHz, maxBits);
if (!fallback || dutyPct > 100U) return fallback;
for (uint8_t bits = fallback; bits > 0; --bits) {
const uint32_t levels = 1UL << bits;
const uint64_t denominator = static_cast<uint64_t>(frequencyHz) * levels;
if (denominator > sourceClockHz || sourceClockHz % denominator) continue;
const uint32_t divider = static_cast<uint32_t>(sourceClockHz / denominator);
if (!divider || divider > 1024U) continue;
if ((static_cast<uint32_t>(levels) * dutyPct) % 100U == 0U) return bits;
}
return fallback;
}
bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
uint32_t sourceClockHz, uint8_t maxBits,
IntegerPwmConfig &config) {
if (!requestedHz || !requestedPulseNs || !sourceClockHz || !maxBits) return false;
constexpr uint32_t FRACTION_SCALE = 256U;
constexpr uint32_t MAX_DIVIDER_RAW = 1024U * FRACTION_SCALE - 1U;
bool found = false;
uint32_t bestFrequencyError = UINT32_MAX;
uint32_t bestPulseError = UINT32_MAX;
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
const uint32_t levels = 1UL << bits;
const uint64_t dividerNumerator = static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE;
const uint64_t dividerDenominator = static_cast<uint64_t>(requestedHz) * levels;
const uint32_t dividerFloor = static_cast<uint32_t>(dividerNumerator / dividerDenominator);
const uint32_t candidates[] = {dividerFloor, dividerFloor + 1U};
for (uint32_t dividerRaw : candidates) {
if (dividerRaw < FRACTION_SCALE || dividerRaw > MAX_DIVIDER_RAW) continue;
const uint64_t frequencyDenominator = static_cast<uint64_t>(levels) * dividerRaw;
const uint32_t actualHz = static_cast<uint32_t>(
(dividerNumerator + frequencyDenominator / 2U) / frequencyDenominator);
if (!actualHz) continue;
const uint64_t dutyNumerator = static_cast<uint64_t>(requestedPulseNs) *
sourceClockHz * FRACTION_SCALE;
const uint64_t dutyDenominator = static_cast<uint64_t>(dividerRaw) * 1000000000ULL;
uint32_t dutyCount = static_cast<uint32_t>((dutyNumerator + dutyDenominator / 2U) /
dutyDenominator);
if (!dutyCount) dutyCount = 1U;
if (dutyCount >= levels) dutyCount = levels - 1U;
if (!dutyCount) continue;
const uint32_t actualPulseNs = static_cast<uint32_t>(
(static_cast<uint64_t>(dutyCount) * dividerRaw * 1000000000ULL +
static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE / 2U) /
(static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE));
const uint32_t frequencyError = actualHz > requestedHz ? actualHz - requestedHz : requestedHz - actualHz;
const uint32_t pulseError = actualPulseNs > requestedPulseNs
? actualPulseNs - requestedPulseNs : requestedPulseNs - actualPulseNs;
if (found && (frequencyError > bestFrequencyError ||
(frequencyError == bestFrequencyError && pulseError > bestPulseError) ||
(frequencyError == bestFrequencyError && pulseError == bestPulseError && bits <= config.bits)))
continue;
config = {actualHz, dividerRaw, dutyCount, actualPulseNs, bits};
bestFrequencyError = frequencyError;
bestPulseError = pulseError;
found = true;
}
}
return found;
}
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t periodCaptureHz, uint32_t pulseCaptureHz,
uint8_t pwmBits,
uint16_t averagingPeriods) {
if (!frequencyHz || !periodCaptureHz || !pulseCaptureHz || !pwmBits || !averagingPeriods)
return FailReason::RESOLUTION;
const float periodTicks = static_cast<float>(periodCaptureHz) / frequencyHz;
const float activeTicks = static_cast<float>(pulseCaptureHz) * dutyPct /
(100.0f * frequencyHz);
if (periodTicks < 4.0f || activeTicks < 2.0f) return FailReason::RESOLUTION;
const float timerPeriodError = 100.0f / periodTicks;
const float timerPulseError = 100.0f / activeTicks;
// Measurement uses the duty actually programmed into LEDC. A coarse PWM
// step is not itself an error when the requested value (e.g. 50%) is exactly
// representable; only the selected value's actual quantization matters.
const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
return (timerPeriodError > effectiveAccuracy || timerPulseError > effectiveAccuracy)
? FailReason::RESOLUTION : FailReason::NONE;
}
FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz,
float expectedDuty, float tolerance, uint8_t repeat,
StageStats &s) {
const uint32_t pulseTickHz = p.activeTickHz ? p.activeTickHz : tickHz;
if (!p.periodTicks || !p.activeTicks || !tickHz || !pulseTickHz || expectedHz <= 0.0f)
return FailReason::EXTRA_EDGE;
const float hz = static_cast<float>(tickHz) / p.periodTicks;
const float duty = static_cast<float>(100.0 * p.activeTicks * tickHz /
(static_cast<double>(pulseTickHz) * p.periodTicks));
++s.periods;
s.periodSum += p.periodTicks; s.activeSum += p.activeTicks;
if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks;
if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks;
if (p.activeTicks < s.minActive) s.minActive = p.activeTicks;
if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks;
// Validate every complete period independently. A single capture tick is the
// unavoidable endpoint uncertainty, so only that one tick may be corrected
// toward the expected value. It cannot hide a larger isolated distortion.
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
if (!frequencyOk) {
uint32_t correctedPeriod = p.periodTicks;
if (hz > expectedHz) ++correctedPeriod;
else if (correctedPeriod > 1U) --correctedPeriod;
frequencyOk = periodWithin(static_cast<float>(tickHz) / correctedPeriod,
expectedHz, tolerance);
}
const double expectedPulseTicks = static_cast<double>(pulseTickHz) * expectedDuty /
(100.0 * expectedHz);
bool pulseOk = expectedPulseTicks > 0.0 &&
fabs(static_cast<double>(p.activeTicks) - expectedPulseTicks) * 100.0 /
expectedPulseTicks <= tolerance + 0.0001;
if (!pulseOk) {
uint32_t correctedActive = p.activeTicks;
if (correctedActive > expectedPulseTicks) {
if (correctedActive) --correctedActive;
} else {
++correctedActive;
}
pulseOk = fabs(static_cast<double>(correctedActive) - expectedPulseTicks) *
100.0 / expectedPulseTicks <= tolerance + 0.0001;
}
const FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat;
s.badFrequency = hz; s.badDuty = duty;
}
return reason;
}
bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerance,
uint32_t tickHz, PeriodLimits &limits) {
if (!expectedHz || !tickHz || tolerance < 0.0f || tolerance >= 100.0f ||
expectedDuty <= 0.0f || expectedDuty >= 100.0f) return false;
const uint32_t toleranceX100 = static_cast<uint32_t>(lroundf(tolerance * 100.0f));
const uint32_t dutyX100 = static_cast<uint32_t>(lroundf(expectedDuty * 100.0f));
const uint64_t numerator = static_cast<uint64_t>(tickHz) * 10000ULL;
const uint64_t highDenominator = static_cast<uint64_t>(expectedHz) * (10000U + toleranceX100);
const uint64_t lowDenominator = static_cast<uint64_t>(expectedHz) * (10000U - toleranceX100);
limits.minPeriodTicks = static_cast<uint32_t>((numerator + highDenominator - 1U) / highDenominator);
limits.maxPeriodTicks = static_cast<uint32_t>(numerator / lowDenominator);
limits.minDutyX100 = dutyX100 > toleranceX100 ? dutyX100 - toleranceX100 : 0;
limits.maxDutyX100 = dutyX100 + toleranceX100;
return limits.minPeriodTicks && limits.maxPeriodTicks >= limits.minPeriodTicks;
}
FailReason evaluatePeriodFast(const PulsePeriod &p, uint32_t tickHz,
const PeriodLimits &limits, uint8_t repeat,
StageStats &s) {
if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE;
++s.periods;
s.periodSum += p.periodTicks; s.activeSum += p.activeTicks;
if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks;
if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks;
if (p.activeTicks < s.minActive) s.minActive = p.activeTicks;
if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks;
FailReason reason = FailReason::NONE;
if (p.periodTicks < limits.minPeriodTicks || p.periodTicks > limits.maxPeriodTicks) {
reason = FailReason::PERIOD_OUT;
} else {
// The configured range (>= 1 kHz at 80 MHz capture) fits these products
// into 32 bits. Keep a 64-bit fallback for unusually slow external input.
if (p.periodTicks <= UINT32_MAX / 10000U && limits.maxDutyX100 <= 10000U) {
const uint32_t scaledActive = p.activeTicks * 10000U;
const uint32_t minActive = p.periodTicks * limits.minDutyX100;
const uint32_t maxActive = p.periodTicks * limits.maxDutyX100;
if (scaledActive < minActive || scaledActive > maxActive) reason = FailReason::DUTY_OUT;
} else {
const uint64_t scaledActive = static_cast<uint64_t>(p.activeTicks) * 10000ULL;
const uint64_t minActive = static_cast<uint64_t>(p.periodTicks) * limits.minDutyX100;
const uint64_t maxActive = static_cast<uint64_t>(p.periodTicks) * limits.maxDutyX100;
if (scaledActive < minActive || scaledActive > maxActive) reason = FailReason::DUTY_OUT;
}
}
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat;
s.badFrequency = static_cast<float>(tickHz) / p.periodTicks;
s.badDuty = 100.0f * p.activeTicks / p.periodTicks;
}
return reason;
}
FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
uint32_t periodCount, uint32_t tickHz,
float expectedHz, float expectedDuty,
float tolerance,
uint32_t minPeriod, uint32_t maxPeriod,
uint8_t repeat,
StageStats &s) {
if (!periodSum || !periodCount || activeSum >= periodSum || !tickHz)
return FailReason::EXTRA_EDGE;
const float hz = static_cast<float>(
static_cast<double>(tickHz) * periodCount / periodSum);
const float duty = static_cast<float>(
100.0 * static_cast<double>(activeSum) / periodSum);
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
if (!frequencyOk && maxPeriod == minPeriod + 1U) {
// At a tolerance boundary, alternating adjacent capture counts prove that the
// result is quantization-limited. Accept only when a one-tick correction
// toward the expected value returns the averaged frequency into tolerance.
// Consecutive periods telescope into one first-to-last edge interval, so
// the whole window has a one-tick endpoint uncertainty, not one tick per
// period.
uint64_t correctedPeriodSum = periodSum;
if (hz > expectedHz) ++correctedPeriodSum;
else if (periodSum > 1U) --correctedPeriodSum;
const float correctedHz = static_cast<float>(
static_cast<double>(tickHz) * periodCount / correctedPeriodSum);
frequencyOk = periodWithin(correctedHz, expectedHz, tolerance);
}
const double expectedPulseTicks = static_cast<double>(tickHz) * expectedDuty /
(100.0 * expectedHz);
const double measuredPulseTicks = static_cast<double>(activeSum) / periodCount;
bool pulseOk = expectedPulseTicks > 0.0 &&
fabs(measuredPulseTicks - expectedPulseTicks) * 100.0 / expectedPulseTicks <= tolerance + 0.0001;
if (!pulseOk) {
// Unlike full periods, active intervals do not telescope: every pulse is
// bounded by a different rising/falling edge pair. With slowly drifting
// asynchronous clocks an entire short window can therefore quantize to
// the same adjacent count (e.g. 41/80 for a true 50% duty). Apply one tick
// per active interval even when minActive == maxActive.
const bool dutyHigh = measuredPulseTicks > expectedPulseTicks;
const uint64_t correctedActive = dutyHigh
? (activeSum > periodCount ? activeSum - periodCount : 0U)
: activeSum + periodCount;
const double correctedPulseTicks = static_cast<double>(correctedActive) / periodCount;
pulseOk = fabs(correctedPulseTicks - expectedPulseTicks) * 100.0 /
expectedPulseTicks <= tolerance + 0.0001;
}
FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason;
s.firstBadPeriod = s.periods >= periodCount ? s.periods - periodCount + 1U : 1U;
s.firstBadRepeat = repeat;
s.badFrequency = hz;
s.badDuty = duty;
}
return reason;
}