#include "Core.h" #include "Config.h" #include #include const char *roleName(Role r) { static const char *names[] = {"SOLO", "MASTER", "SLAVE"}; const uint8_t i = static_cast(r); return i < 3 ? names[i] : "?"; } const char *failName(FailReason r) { static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "UNSUPPORTED", "RESOLUTION", "ABORTED"}; const uint8_t i = static_cast(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(&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; } uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) { if (!startHz || endHz <= startHz) return 0; uint32_t count = 0; for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count; return count; } uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) { for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) { const uint32_t frequency = TEST_FREQUENCIES_HZ[i]; if (frequency < startHz || frequency > endHz) continue; if (!index--) return frequency; } 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(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(frequencyHz) * sampleTimeMs * 1000ULL + RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND; const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs; uint64_t chunkSymbols = (static_cast(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL; if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS; if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS; const uint64_t batchWaitUs = ((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS; 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(OLED_PROGRESS_UPDATE_MS) * 1000ULL * (MEASUREMENT_PROGRESS_STEPS + 1U); return samplingWallUs + batchWaitUs + settleUs + displayUs; } uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) { uint64_t total = 0; const uint32_t count = frequencyPointCount(p.startHz, p.endHz); for (uint32_t i = 0; i < count; ++i) total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles); return total; } 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 > 0.0f && configured <= 1.0001f ? 1.25f : 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(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(frequencyHz) * levels; if (denominator > sourceClockHz || sourceClockHz % denominator) continue; const uint32_t divider = static_cast(sourceClockHz / denominator); if (!divider || divider > 1024U) continue; if ((static_cast(levels) * dutyPct) % 100U == 0U) return bits; } return fallback; } bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz, uint8_t maxBits, uint8_t dutyPct, IntegerPwmConfig &config) { if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false; bool found = false; uint32_t bestErrorHz = 0; uint32_t bestDutyError = 0; uint32_t bestLevels = 1; for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) { const uint32_t levels = 1UL << bits; for (uint32_t divider = 1; divider <= 1023U; ++divider) { const uint32_t denominator = levels * divider; // A fixed integer divider gives identical PWM periods. Requiring an // exact division also guarantees that the physical frequency is a // whole number of hertz rather than a rounded value. if (sourceClockHz % denominator) continue; const uint32_t actualHz = sourceClockHz / denominator; const uint32_t errorHz = actualHz > requestedHz ? actualHz - requestedHz : requestedHz - actualHz; const uint32_t dutyCount = (static_cast(levels) * dutyPct + 50U) / 100U; const uint32_t representedDuty = dutyCount * 100U; const uint32_t requestedDuty = levels * dutyPct; const uint32_t dutyError = representedDuty > requestedDuty ? representedDuty - requestedDuty : requestedDuty - representedDuty; const bool frequencyBetter = !found || errorHz < bestErrorHz; const bool frequencyEqual = found && errorHz == bestErrorHz; const bool dutyBetter = frequencyEqual && static_cast(dutyError) * bestLevels < static_cast(bestDutyError) * levels; const bool dutyEqual = frequencyEqual && static_cast(dutyError) * bestLevels == static_cast(bestDutyError) * levels; if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue; config.actualHz = actualHz; config.divider = static_cast(divider); config.bits = bits; bestErrorHz = errorHz; bestDutyError = dutyError; bestLevels = levels; found = true; } } return found; } FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct, uint32_t captureHz, uint8_t pwmBits) { if (!frequencyHz || !captureHz || !pwmBits) return FailReason::RESOLUTION; const float periodTicks = static_cast(captureHz) / frequencyHz; const float activeTicks = periodTicks * dutyPct / 100.0f; const float inactiveTicks = periodTicks - activeTicks; if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION; const float timerPeriodError = 100.0f / periodTicks; const float timerDutyError = 100.0f / periodTicks; // 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 || timerDutyError > effectiveAccuracy) ? FailReason::RESOLUTION : FailReason::NONE; } FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz, float expectedDuty, float tolerance, uint8_t repeat, StageStats &s) { if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE; const float hz = static_cast(tickHz) / p.periodTicks; const float duty = 100.0f * p.activeTicks / 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; FailReason reason = FailReason::NONE; if (!periodWithin(hz, expectedHz, tolerance)) reason = FailReason::PERIOD_OUT; else if (!dutyWithin(duty, expectedDuty, tolerance)) reason = FailReason::DUTY_OUT; 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(lroundf(tolerance * 100.0f)); const uint32_t dutyX100 = static_cast(lroundf(expectedDuty * 100.0f)); const uint64_t numerator = static_cast(tickHz) * 10000ULL; const uint64_t highDenominator = static_cast(expectedHz) * (10000U + toleranceX100); const uint64_t lowDenominator = static_cast(expectedHz) * (10000U - toleranceX100); limits.minPeriodTicks = static_cast((numerator + highDenominator - 1U) / highDenominator); limits.maxPeriodTicks = static_cast(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(p.activeTicks) * 10000ULL; const uint64_t minActive = static_cast(p.periodTicks) * limits.minDutyX100; const uint64_t maxActive = static_cast(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(tickHz) / p.periodTicks; s.badDuty = 100.0f * p.activeTicks / p.periodTicks; } return reason; }