попытки запустится до 1 МГц
This commit is contained in:
@@ -93,7 +93,13 @@ void App::update() {
|
|||||||
if (state_ == AppState::SOLO_MEASURE) {
|
if (state_ == AppState::SOLO_MEASURE) {
|
||||||
const MeasureState ms = measurement_.update();
|
const MeasureState ms = measurement_.update();
|
||||||
if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), requestedHz_); finish(false, measurement_.reason()); }
|
if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), requestedHz_); finish(false, measurement_.reason()); }
|
||||||
else if (ms == MeasureState::PASS) { printStageStats(measurement_.stats(), requestedHz_); stagePassed(); }
|
else if (ms == MeasureState::PASS) {
|
||||||
|
printStageStats(measurement_.stats(), requestedHz_);
|
||||||
|
if (measurement_.reason() == FailReason::DATA_LOST) {
|
||||||
|
sweepHadDataLoss_ = true; if (!firstDataLossHz_) firstDataLossHz_ = requestedHz_;
|
||||||
|
}
|
||||||
|
stagePassed();
|
||||||
|
}
|
||||||
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
|
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
|
||||||
state_ == AppState::MASTER_WAIT_RESULT) {
|
state_ == AppState::MASTER_WAIT_RESULT) {
|
||||||
handleRadio(); updateMaster();
|
handleRadio(); updateMaster();
|
||||||
@@ -150,7 +156,7 @@ void App::showMenu() {
|
|||||||
|
|
||||||
void App::startTest() {
|
void App::startTest() {
|
||||||
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
||||||
stageIndex_ = 0; pendingReason_ = FailReason::NONE;
|
stageIndex_ = 0; pendingReason_ = FailReason::NONE; sweepHadDataLoss_ = false; firstDataLossHz_ = 0;
|
||||||
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
|
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
|
||||||
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
|
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
|
||||||
if (SERIAL_MINIMAL_LOG)
|
if (SERIAL_MINIMAL_LOG)
|
||||||
@@ -208,7 +214,11 @@ bool App::startLocalMeasurement(float hz, float duty) {
|
|||||||
void App::stagePassed() {
|
void App::stagePassed() {
|
||||||
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
|
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
|
||||||
pwm_.stop();
|
pwm_.stop();
|
||||||
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
|
if (++stageIndex_ >= stageCount_) {
|
||||||
|
if (sweepHadDataLoss_) { requestedHz_ = firstDataLossHz_; finish(false, FailReason::DATA_LOST); }
|
||||||
|
else finish(true, FailReason::NONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
|
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
|
||||||
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
|
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
|
||||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
|
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
|
||||||
@@ -320,7 +330,8 @@ void App::updateSlave() {
|
|||||||
const MeasureState ms = measurement_.update();
|
const MeasureState ms = measurement_.update();
|
||||||
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return;
|
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return;
|
||||||
printStageStats(measurement_.stats(), requestedHz_);
|
printStageStats(measurement_.stats(), requestedHz_);
|
||||||
pendingPacket_ = makePacket(MessageType::RESULT); pendingPacket_.passed = ms == MeasureState::PASS;
|
pendingPacket_ = makePacket(MessageType::RESULT);
|
||||||
|
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
|
||||||
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
|
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
|
||||||
pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod;
|
pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod;
|
||||||
pendingPacket_.sequence = ++sequence_; radio_.sendTo(peer_, pendingPacket_);
|
pendingPacket_.sequence = ++sequence_; radio_.sendTo(peer_, pendingPacket_);
|
||||||
@@ -377,7 +388,9 @@ void App::printStageStats(const StageStats &s, uint32_t hz) {
|
|||||||
if (!s.periods) return;
|
if (!s.periods) return;
|
||||||
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
||||||
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
||||||
Log::printf("RESULT", "%luHz %s periods=%lu measured=%.2fHz duty=%.2f%%%s%s",
|
const char *status = s.reason == FailReason::NONE ? "PASS" :
|
||||||
hz, s.reason == FailReason::NONE ? "PASS" : "FAIL", s.periods, measuredHz, measuredDuty,
|
(s.reason == FailReason::DATA_LOST ? "DATA_LOST" : "FAIL");
|
||||||
|
Log::printf("RESULT", "%luHz %s periods=%lu measured=%.2fHz duty=%.2f%% lost=%lu%s%s",
|
||||||
|
hz, status, s.periods, measuredHz, measuredDuty, s.lostItems,
|
||||||
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
|
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,5 +64,7 @@ class App {
|
|||||||
uint8_t retries_ = 0;
|
uint8_t retries_ = 0;
|
||||||
ProtocolPacket pendingPacket_ = {};
|
ProtocolPacket pendingPacket_ = {};
|
||||||
bool initialized_ = false, bootResetCandidate_ = false;
|
bool initialized_ = false, bootResetCandidate_ = false;
|
||||||
|
bool sweepHadDataLoss_ = false;
|
||||||
|
uint32_t firstDataLossHz_ = 0;
|
||||||
uint32_t bootCheckStartedMs_ = 0;
|
uint32_t bootCheckStartedMs_ = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8;
|
|||||||
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
|
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
|
||||||
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;
|
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;
|
||||||
constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
|
constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
|
||||||
|
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
|
||||||
|
constexpr uint16_t PERIOD_BATCH_SIZE = 128;
|
||||||
|
|
||||||
constexpr uint32_t C3_STRICT_MAX_HZ = 1000000;
|
constexpr uint32_t C3_STRICT_MAX_HZ = 1000000;
|
||||||
constexpr uint32_t S3_STRICT_MAX_HZ = 1000000;
|
constexpr uint32_t S3_STRICT_MAX_HZ = 1000000;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const char *roleName(Role r) {
|
|||||||
const char *failName(FailReason 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", "DUTY OUT",
|
||||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST",
|
"EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST",
|
||||||
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
|
"UNSUPPORTED", "RESOLUTION", "ABORTED", "DATA LOST"};
|
||||||
const uint8_t i = static_cast<uint8_t>(r);
|
const uint8_t i = static_cast<uint8_t>(r);
|
||||||
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
|
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,10 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac
|
|||||||
if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION;
|
if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION;
|
||||||
const float timerPeriodError = 100.0f / periodTicks;
|
const float timerPeriodError = 100.0f / periodTicks;
|
||||||
const float timerDutyError = 100.0f / periodTicks;
|
const float timerDutyError = 100.0f / periodTicks;
|
||||||
const float pwmDutyStep = 100.0f / static_cast<float>((1UL << pwmBits) - 1UL);
|
// Measurement uses the duty actually programmed into LEDC. A coarse PWM
|
||||||
return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct || pwmDutyStep > accuracyPct)
|
// step is not itself an error when the requested value (e.g. 50%) is exactly
|
||||||
|
// representable; only the selected value's actual quantization matters.
|
||||||
|
return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct)
|
||||||
? FailReason::RESOLUTION : FailReason::NONE;
|
? FailReason::RESOLUTION : FailReason::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,3 +109,56 @@ FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedH
|
|||||||
}
|
}
|
||||||
return reason;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
||||||
enum class FailReason : uint8_t {
|
enum class FailReason : uint8_t {
|
||||||
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
|
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
|
||||||
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
|
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED, DATA_LOST
|
||||||
};
|
};
|
||||||
|
|
||||||
const char *roleName(Role role);
|
const char *roleName(Role role);
|
||||||
@@ -53,10 +53,18 @@ struct StageStats {
|
|||||||
uint8_t firstBadRepeat;
|
uint8_t firstBadRepeat;
|
||||||
float badFrequency;
|
float badFrequency;
|
||||||
float badDuty;
|
float badDuty;
|
||||||
|
uint32_t lostItems;
|
||||||
FailReason reason;
|
FailReason reason;
|
||||||
void reset();
|
void reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PeriodLimits {
|
||||||
|
uint32_t minPeriodTicks;
|
||||||
|
uint32_t maxPeriodTicks;
|
||||||
|
uint32_t minDutyX100;
|
||||||
|
uint32_t maxDutyX100;
|
||||||
|
};
|
||||||
|
|
||||||
uint32_t settingsChecksum(const Settings &s);
|
uint32_t settingsChecksum(const Settings &s);
|
||||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz);
|
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz);
|
||||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index);
|
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index);
|
||||||
@@ -70,3 +78,8 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac
|
|||||||
FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz,
|
FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz,
|
||||||
float expectedDuty, float tolerancePct, uint8_t repeat,
|
float expectedDuty, float tolerancePct, uint8_t repeat,
|
||||||
StageStats &stats);
|
StageStats &stats);
|
||||||
|
bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerancePct,
|
||||||
|
uint32_t tickHz, PeriodLimits &limits);
|
||||||
|
FailReason evaluatePeriodFast(const PulsePeriod &period, uint32_t tickHz,
|
||||||
|
const PeriodLimits &limits, uint8_t repeat,
|
||||||
|
StageStats &stats);
|
||||||
|
|||||||
@@ -4,12 +4,17 @@
|
|||||||
|
|
||||||
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
||||||
uint8_t repeats, uint8_t settleCycles) {
|
uint8_t repeats, uint8_t settleCycles) {
|
||||||
if (!hz || !timeMs || !repeats || repeats > 10 || !receiver_.start(static_cast<uint32_t>(hz))) return false;
|
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
|
||||||
expectedHz_ = hz; expectedDuty_ = duty; tolerance_ = tolerance;
|
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 ||
|
||||||
|
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) ||
|
||||||
|
!receiver_.start(expectedHz_)) return false;
|
||||||
timeMs_ = timeMs; repeats_ = repeats; settleLeft_ = settleCycles;
|
timeMs_ = timeMs; repeats_ = repeats; settleLeft_ = settleCycles;
|
||||||
stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_));
|
stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_));
|
||||||
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
||||||
measurementStartMs_ = lastPeriodMs_ = 0;
|
measurementStartMs_ = lastPeriodMs_ = 0;
|
||||||
|
currentRepeat_ = 0;
|
||||||
|
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
|
||||||
|
if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
|
||||||
state_ = MeasureState::SETTLING; return true;
|
state_ = MeasureState::SETTLING; return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,53 +23,65 @@ void Measurement::fail(FailReason reason) {
|
|||||||
receiver_.stop(); state_ = MeasureState::FAIL;
|
receiver_.stop(); state_ = MeasureState::FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Measurement::completeWindow() {
|
||||||
|
receiver_.stop();
|
||||||
|
stats_.lostItems += receiver_.takeDroppedItems();
|
||||||
|
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
||||||
|
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
|
||||||
|
fail(FailReason::TOO_FEW_PERIODS); return;
|
||||||
|
}
|
||||||
|
if (stats_.lostItems && stats_.reason == FailReason::NONE) stats_.reason = FailReason::DATA_LOST;
|
||||||
|
state_ = MeasureState::PASS;
|
||||||
|
}
|
||||||
|
|
||||||
MeasureState Measurement::update() {
|
MeasureState Measurement::update() {
|
||||||
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
|
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
|
||||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
|
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
|
||||||
PulsePeriod period;
|
bool receivedPeriod = false;
|
||||||
while (receiver_.poll(period)) {
|
for (;;) {
|
||||||
|
const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE);
|
||||||
|
stats_.lostItems += receiver_.takeDroppedItems();
|
||||||
|
if (!periodCount) break;
|
||||||
|
receivedPeriod = true;
|
||||||
|
for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) {
|
||||||
|
const PulsePeriod &period = periodBatch_[periodIndex];
|
||||||
if (state_ == MeasureState::SETTLING) {
|
if (state_ == MeasureState::SETTLING) {
|
||||||
if (settleLeft_) --settleLeft_;
|
if (settleLeft_) --settleLeft_;
|
||||||
if (!settleLeft_) {
|
if (!settleLeft_) {
|
||||||
measurementStartTick_ = period.startTick + period.periodTicks;
|
measurementStartTick_ = period.startTick + period.periodTicks;
|
||||||
repeatTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs_ / 1000ULL;
|
repeatTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs_ / 1000ULL;
|
||||||
deadlineTick_ = measurementStartTick_ + repeatTicks_ * repeats_;
|
deadlineTick_ = measurementStartTick_ + repeatTicks_ * repeats_;
|
||||||
|
nextRepeatTick_ = measurementStartTick_ + repeatTicks_;
|
||||||
stats_.reset(); measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
stats_.reset(); measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const uint64_t endTick = period.startTick + period.periodTicks;
|
const uint64_t endTick = period.startTick + period.periodTicks;
|
||||||
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
||||||
if (endTick > deadlineTick_) break; // trailing incomplete period
|
if (endTick > deadlineTick_) { completeWindow(); return state_; } // trailing incomplete period
|
||||||
uint8_t repeat = static_cast<uint8_t>((period.startTick - measurementStartTick_) / repeatTicks_);
|
while (currentRepeat_ + 1U < repeats_ && period.startTick >= nextRepeatTick_) {
|
||||||
if (repeat >= repeats_) repeat = repeats_ - 1;
|
++currentRepeat_; nextRepeatTick_ += repeatTicks_;
|
||||||
++repeatPeriods_[repeat];
|
}
|
||||||
lastPeriodMs_ = millis();
|
++repeatPeriods_[currentRepeat_];
|
||||||
const FailReason r = evaluatePeriod(period, receiver_.tickHz(), expectedHz_, expectedDuty_,
|
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_);
|
||||||
tolerance_, repeat + 1, stats_);
|
|
||||||
if (r != FailReason::NONE) { fail(r); return state_; }
|
if (r != FailReason::NONE) { fail(r); return state_; }
|
||||||
}
|
}
|
||||||
uint64_t expectedPeriodMs = static_cast<uint64_t>(ceilf(1000.0f / expectedHz_));
|
}
|
||||||
if (!expectedPeriodMs) expectedPeriodMs = 1;
|
if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis();
|
||||||
const uint64_t edgeBasedTimeout =
|
const uint64_t edgeBasedTimeout =
|
||||||
static_cast<uint64_t>(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs + 20;
|
static_cast<uint64_t>(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
|
||||||
const uint64_t rmtBatchTimeout =
|
const uint64_t rmtBatchTimeout =
|
||||||
static_cast<uint64_t>(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs + 20;
|
static_cast<uint64_t>(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
|
||||||
const uint64_t settleTimeout = edgeBasedTimeout > rmtBatchTimeout ? edgeBasedTimeout : rmtBatchTimeout;
|
const uint64_t settleTimeout = edgeBasedTimeout > rmtBatchTimeout ? edgeBasedTimeout : rmtBatchTimeout;
|
||||||
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
|
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
|
||||||
if (state_ == MeasureState::RUNNING && measurementStartTick_) {
|
if (state_ == MeasureState::RUNNING && measurementStartTick_) {
|
||||||
const uint32_t now = millis();
|
const uint32_t now = millis();
|
||||||
const uint32_t totalMs = timeMs_ * repeats_;
|
const uint32_t totalMs = timeMs_ * repeats_;
|
||||||
const uint32_t edgeTimeoutMs = static_cast<uint32_t>(expectedPeriodMs * NO_SIGNAL_TIMEOUT_PERIODS + 2);
|
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2;
|
||||||
if (now - measurementStartMs_ < totalMs && now - lastPeriodMs_ > edgeTimeoutMs) {
|
if (now - measurementStartMs_ < totalMs && now - lastPeriodMs_ > edgeTimeoutMs) {
|
||||||
fail(FailReason::LOST_EDGE); return state_;
|
fail(FailReason::LOST_EDGE); return state_;
|
||||||
}
|
}
|
||||||
if (now - measurementStartMs_ > totalMs + expectedPeriodMs + 2) {
|
if (now - measurementStartMs_ > totalMs + expectedPeriodMs_ + 2) completeWindow();
|
||||||
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
|
|
||||||
fail(FailReason::TOO_FEW_PERIODS); return state_;
|
|
||||||
}
|
|
||||||
receiver_.stop(); state_ = MeasureState::PASS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state_;
|
return state_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ class Measurement {
|
|||||||
const StageStats &stats() const { return stats_; }
|
const StageStats &stats() const { return stats_; }
|
||||||
private:
|
private:
|
||||||
void fail(FailReason reason);
|
void fail(FailReason reason);
|
||||||
|
void completeWindow();
|
||||||
PulseReceiver &receiver_;
|
PulseReceiver &receiver_;
|
||||||
MeasureState state_ = MeasureState::IDLE;
|
MeasureState state_ = MeasureState::IDLE;
|
||||||
StageStats stats_ = {};
|
StageStats stats_ = {};
|
||||||
float expectedHz_ = 0, expectedDuty_ = 0, tolerance_ = 0;
|
PeriodLimits limits_ = {};
|
||||||
|
uint32_t expectedHz_ = 0;
|
||||||
uint32_t timeMs_ = 0;
|
uint32_t timeMs_ = 0;
|
||||||
uint8_t repeats_ = 0, settleLeft_ = 0;
|
uint8_t repeats_ = 0, settleLeft_ = 0, currentRepeat_ = 0;
|
||||||
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, repeatTicks_ = 0;
|
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, repeatTicks_ = 0, nextRepeatTick_ = 0;
|
||||||
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
|
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
|
||||||
|
uint32_t expectedPeriodMs_ = 1;
|
||||||
uint32_t repeatPeriods_[10] = {};
|
uint32_t repeatPeriods_[10] = {};
|
||||||
|
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
|||||||
const uint8_t bits = choosePwmResolution(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS);
|
const uint8_t bits = choosePwmResolution(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS);
|
||||||
if (!bits) return false;
|
if (!bits) return false;
|
||||||
if (!ledcAttachChannel(GPIO_PWM, hz, bits, LEDC_CHANNEL)) return false;
|
if (!ledcAttachChannel(GPIO_PWM, hz, bits, LEDC_CHANNEL)) return false;
|
||||||
const uint32_t top = (1UL << bits) - 1UL;
|
const uint32_t levels = 1UL << bits;
|
||||||
const uint32_t duty = (static_cast<uint64_t>(top) * dutyPct + 50U) / 100U;
|
const uint32_t duty = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U;
|
||||||
if (!ledcWriteChannel(LEDC_CHANNEL, duty)) { ledcDetach(GPIO_PWM); return false; }
|
if (!ledcWriteChannel(LEDC_CHANNEL, duty)) { ledcDetach(GPIO_PWM); return false; }
|
||||||
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
|
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
|
||||||
if (!actualHz) { ledcDetach(GPIO_PWM); return false; }
|
if (!actualHz) { ledcDetach(GPIO_PWM); return false; }
|
||||||
a = {hz, actualHz, 100.0f * duty / top, bits};
|
a = {hz, actualHz, 100.0f * duty / levels, bits};
|
||||||
running_ = true;
|
running_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ uint32_t PulseReceiver::tickHz() const {
|
|||||||
|
|
||||||
bool PulseReceiver::begin() {
|
bool PulseReceiver::begin() {
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
queue_ = xQueueCreate(16, sizeof(SymbolBlock));
|
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
|
||||||
rmt_rx_channel_config_t cfg = {};
|
rmt_rx_channel_config_t cfg = {};
|
||||||
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ;
|
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ;
|
||||||
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
|
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
|
||||||
@@ -77,7 +77,7 @@ void PulseReceiver::stop() {
|
|||||||
|
|
||||||
void PulseReceiver::resetStream() {
|
void PulseReceiver::resetStream() {
|
||||||
if (queue_) xQueueReset(queue_);
|
if (queue_) xQueueReset(queue_);
|
||||||
overflow_ = false; haveRise_ = haveFall_ = haveRawTick_ = false;
|
overflow_ = false; droppedItems_ = 0; haveRise_ = haveFall_ = haveRawTick_ = false;
|
||||||
lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0;
|
lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0;
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0;
|
block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0;
|
||||||
@@ -109,17 +109,22 @@ bool PulseReceiver::overflowed() {
|
|||||||
const bool value = overflow_; overflow_ = false; return value;
|
const bool value = overflow_; overflow_ = false; return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t PulseReceiver::takeDroppedItems() {
|
||||||
|
return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED);
|
||||||
|
}
|
||||||
|
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) {
|
bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) {
|
||||||
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
|
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
|
||||||
BaseType_t wake = pdFALSE;
|
BaseType_t wake = pdFALSE;
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
while (offset < data->num_symbols) {
|
while (offset < data->num_symbols) {
|
||||||
SymbolBlock b = {};
|
SymbolBlock &b = self->isrBlock_;
|
||||||
b.count = static_cast<uint16_t>((data->num_symbols - offset) > BLOCK_SYMBOLS ?
|
b.count = static_cast<uint16_t>((data->num_symbols - offset) > BLOCK_SYMBOLS ?
|
||||||
BLOCK_SYMBOLS : (data->num_symbols - offset));
|
BLOCK_SYMBOLS : (data->num_symbols - offset));
|
||||||
memcpy(b.symbols, data->received_symbols + offset, b.count * sizeof(rmt_symbol_word_t));
|
memcpy(b.symbols, data->received_symbols + offset, b.count * sizeof(rmt_symbol_word_t));
|
||||||
if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE) self->overflow_ = true;
|
if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE)
|
||||||
|
__atomic_fetch_add(&self->droppedItems_, b.count, __ATOMIC_RELAXED);
|
||||||
offset += b.count;
|
offset += b.count;
|
||||||
}
|
}
|
||||||
return wake == pdTRUE;
|
return wake == pdTRUE;
|
||||||
@@ -146,10 +151,12 @@ bool PulseReceiver::nextRmtEdge(Edge &edge) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PulseReceiver::poll(PulsePeriod &period) {
|
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) {
|
||||||
|
size_t count = 0;
|
||||||
Edge e;
|
Edge e;
|
||||||
while (nextRmtEdge(e)) if (consumeEdge(e, period)) return true;
|
while (count < capacity && nextRmtEdge(e))
|
||||||
return false;
|
if (consumeEdge(e, periods[count])) ++count;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
|
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
|
||||||
@@ -158,13 +165,16 @@ void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
|
|||||||
if (RX_SIGNAL_INVERTED) level = !level;
|
if (RX_SIGNAL_INVERTED) level = !level;
|
||||||
Edge e = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)};
|
Edge e = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)};
|
||||||
BaseType_t wake = pdFALSE;
|
BaseType_t wake = pdFALSE;
|
||||||
if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE) self->overflow_ = true;
|
if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE)
|
||||||
|
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
|
||||||
if (wake) portYIELD_FROM_ISR();
|
if (wake) portYIELD_FROM_ISR();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PulseReceiver::poll(PulsePeriod &period) {
|
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) {
|
||||||
|
size_t count = 0;
|
||||||
Edge e;
|
Edge e;
|
||||||
while (xQueueReceive(queue_, &e, 0) == pdTRUE) if (consumeEdge(e, period)) return true;
|
while (count < capacity && xQueueReceive(queue_, &e, 0) == pdTRUE)
|
||||||
return false;
|
if (consumeEdge(e, periods[count])) ++count;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ class PulseReceiver {
|
|||||||
bool start(uint32_t expectedHz);
|
bool start(uint32_t expectedHz);
|
||||||
void stop();
|
void stop();
|
||||||
void resetStream();
|
void resetStream();
|
||||||
bool poll(PulsePeriod &period);
|
size_t readPeriods(PulsePeriod *periods, size_t capacity);
|
||||||
bool overflowed();
|
bool overflowed();
|
||||||
|
uint32_t takeDroppedItems();
|
||||||
uint32_t tickHz() const;
|
uint32_t tickHz() const;
|
||||||
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
||||||
bool highRateBackend() const {
|
bool highRateBackend() const {
|
||||||
@@ -34,13 +35,14 @@ class PulseReceiver {
|
|||||||
bool consumeEdge(const Edge &edge, PulsePeriod &period);
|
bool consumeEdge(const Edge &edge, PulsePeriod &period);
|
||||||
|
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
static constexpr size_t BLOCK_SYMBOLS = 64;
|
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS;
|
||||||
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
|
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
|
||||||
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
|
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
|
||||||
bool nextRmtEdge(Edge &edge);
|
bool nextRmtEdge(Edge &edge);
|
||||||
rmt_channel_handle_t channel_ = nullptr;
|
rmt_channel_handle_t channel_ = nullptr;
|
||||||
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
|
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
|
||||||
uint16_t receiveChunkSymbols_ = 0;
|
uint16_t receiveChunkSymbols_ = 0;
|
||||||
|
SymbolBlock isrBlock_ = {};
|
||||||
SymbolBlock block_ = {};
|
SymbolBlock block_ = {};
|
||||||
uint16_t blockIndex_ = 0;
|
uint16_t blockIndex_ = 0;
|
||||||
uint8_t phase_ = 0;
|
uint8_t phase_ = 0;
|
||||||
@@ -53,6 +55,7 @@ class PulseReceiver {
|
|||||||
#endif
|
#endif
|
||||||
QueueHandle_t queue_ = nullptr;
|
QueueHandle_t queue_ = nullptr;
|
||||||
volatile bool overflow_ = false;
|
volatile bool overflow_ = false;
|
||||||
|
volatile uint32_t droppedItems_ = 0;
|
||||||
bool running_ = false;
|
bool running_ = false;
|
||||||
bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false;
|
bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false;
|
||||||
uint32_t lastRawTick_ = 0;
|
uint32_t lastRawTick_ = 0;
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ PWM_SETTLE_CYCLES / actualFrequency + TEST_TIME * REPEATS
|
|||||||
|
|
||||||
| Target | Arduino-ESP32 | Flash | RAM | Результат |
|
| Target | Arduino-ESP32 | Flash | RAM | Результат |
|
||||||
|---|---:|---:|---:|---|
|
|---|---:|---:|---:|---|
|
||||||
| ESP32-C3 | 3.3.10 | 1,024,299 B (78%) | 39,460 B (12%) | PASS |
|
| ESP32-C3 | 3.3.10 | 1,025,021 B (78%) | 45,380 B (13%) | PASS |
|
||||||
| ESP32-S3 | 3.3.10 | 950,608 B (72%) | 48,620 B (14%) | PASS |
|
| ESP32-S3 | 3.3.10 | 950,608 B (72%) | 48,620 B (14%) | PASS |
|
||||||
|
|
||||||
Локальные unit-тесты: `core tests: PASS`, `button tests: PASS`. Они покрывают неделимый диапазон, END без дубля, ALL, границы допусков, немедленный FAIL, resolution, checksum настроек, CRC протокола и отсутствие short после long.
|
Локальные unit-тесты: `core tests: PASS`, `button tests: PASS`. Они покрывают неделимый диапазон, END без дубля, ALL, границы допусков, немедленный FAIL, resolution, checksum настроек, CRC протокола и отсутствие short после long.
|
||||||
|
|||||||
Reference in New Issue
Block a user