Глобальная переделка. тест сделан по длине импульса и заданной частоте шим, а не меандру
This commit is contained in:
@@ -10,7 +10,7 @@ const char *roleName(Role r) {
|
||||
}
|
||||
|
||||
const char *failName(FailReason r) {
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT",
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
|
||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
|
||||
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
|
||||
const uint8_t i = static_cast<uint8_t>(r);
|
||||
@@ -31,19 +31,19 @@ uint32_t settingsChecksum(const Settings &s) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) {
|
||||
if (!startHz || endHz <= startHz) return 0;
|
||||
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_FREQUENCIES_HZ); ++i)
|
||||
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count;
|
||||
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 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;
|
||||
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;
|
||||
}
|
||||
@@ -58,26 +58,17 @@ uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t se
|
||||
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
|
||||
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
|
||||
|
||||
uint64_t chunkSymbols =
|
||||
(static_cast<uint64_t>(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<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
|
||||
(MEASUREMENT_PROGRESS_STEPS + 1U);
|
||||
return samplingWallUs + batchWaitUs + settleUs + displayUs;
|
||||
return samplingWallUs + 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;
|
||||
return static_cast<uint64_t>(pulseWidthPointCount(p.maxPulseNs, p.minPulseNs)) *
|
||||
nominalStageUs(p.frequencyHz, p.testTimeMs, settleCycles);
|
||||
}
|
||||
|
||||
bool periodWithin(float measured, float expected, float tolerance) {
|
||||
@@ -116,49 +107,54 @@ uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
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 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 bestErrorHz = 0;
|
||||
uint32_t bestDutyError = 0;
|
||||
uint32_t bestLevels = 1;
|
||||
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;
|
||||
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<uint64_t>(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 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 bool frequencyBetter = !found || errorHz < bestErrorHz;
|
||||
const bool frequencyEqual = found && errorHz == bestErrorHz;
|
||||
const bool dutyBetter = frequencyEqual &&
|
||||
static_cast<uint64_t>(dutyError) * bestLevels <
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
const bool dutyEqual = frequencyEqual &&
|
||||
static_cast<uint64_t>(dutyError) * bestLevels ==
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) 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;
|
||||
|
||||
config.actualHz = actualHz;
|
||||
config.divider = static_cast<uint16_t>(divider);
|
||||
config.bits = bits;
|
||||
bestErrorHz = errorHz;
|
||||
bestDutyError = dutyError;
|
||||
bestLevels = levels;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -166,40 +162,70 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
}
|
||||
|
||||
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
|
||||
uint32_t captureHz, uint8_t pwmBits,
|
||||
uint32_t periodCaptureHz, uint32_t pulseCaptureHz,
|
||||
uint8_t pwmBits,
|
||||
uint16_t averagingPeriods) {
|
||||
if (!frequencyHz || !captureHz || !pwmBits || !averagingPeriods)
|
||||
if (!frequencyHz || !periodCaptureHz || !pulseCaptureHz || !pwmBits || !averagingPeriods)
|
||||
return FailReason::RESOLUTION;
|
||||
const float periodTicks = static_cast<float>(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 averagedTicks = periodTicks * averagingPeriods;
|
||||
const float timerPeriodError = 100.0f / averagedTicks;
|
||||
const float timerDutyError = 100.0f / averagedTicks;
|
||||
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 || timerDutyError > effectiveAccuracy)
|
||||
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) {
|
||||
if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE;
|
||||
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 = 100.0f * p.activeTicks / 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;
|
||||
FailReason reason = FailReason::NONE;
|
||||
if (!periodWithin(hz, expectedHz, tolerance)) reason = FailReason::PERIOD_OUT;
|
||||
else if (!dutyWithin(duty, expectedDuty, tolerance)) reason = FailReason::DUTY_OUT;
|
||||
// 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;
|
||||
@@ -275,7 +301,7 @@ FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
|
||||
100.0 * static_cast<double>(activeSum) / periodSum);
|
||||
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
|
||||
if (!frequencyOk && maxPeriod == minPeriod + 1U) {
|
||||
// At a tolerance boundary, alternating adjacent RMT counts prove that the
|
||||
// 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
|
||||
@@ -289,24 +315,28 @@ FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
|
||||
frequencyOk = periodWithin(correctedHz, expectedHz, tolerance);
|
||||
}
|
||||
|
||||
bool dutyOk = dutyWithin(duty, expectedDuty, tolerance);
|
||||
if (!dutyOk) {
|
||||
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 = duty > expectedDuty;
|
||||
const bool dutyHigh = measuredPulseTicks > expectedPulseTicks;
|
||||
const uint64_t correctedActive = dutyHigh
|
||||
? (activeSum > periodCount ? activeSum - periodCount : 0U)
|
||||
: activeSum + periodCount;
|
||||
const float correctedDuty = static_cast<float>(
|
||||
100.0 * static_cast<double>(correctedActive) / periodSum);
|
||||
dutyOk = dutyWithin(correctedDuty, expectedDuty, tolerance);
|
||||
const double correctedPulseTicks = static_cast<double>(correctedActive) / periodCount;
|
||||
pulseOk = fabs(correctedPulseTicks - expectedPulseTicks) * 100.0 /
|
||||
expectedPulseTicks <= tolerance + 0.0001;
|
||||
}
|
||||
|
||||
FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
|
||||
(!dutyOk ? FailReason::DUTY_OUT : FailReason::NONE);
|
||||
(!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;
|
||||
|
||||
Reference in New Issue
Block a user