Глобальная переделка. тест сделан по длине импульса и заданной частоте шим, а не меандру

This commit is contained in:
2026-08-12 18:10:05 +03:00
parent 1db89fca79
commit a17e8962b4
19 changed files with 958 additions and 642 deletions

View File

@@ -8,10 +8,13 @@
#include <esp_system.h> #include <esp_system.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <Wire.h>
#include <math.h> #include <math.h>
#include <string.h> #include <string.h>
namespace { namespace {
const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) { const char *appStateName(AppState state) {
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER", static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER",
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START", "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
@@ -26,9 +29,40 @@ const char *buttonEventName(ButtonEvent event) {
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN"; return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
} }
void formatErrorDuty(float duty, char *out, size_t size) { uint32_t pulseFromDuty(float hz, float dutyPct) {
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty); return hz > 0.0f ? static_cast<uint32_t>(lroundf(dutyPct * 10000000.0f / hz)) : 0U;
else snprintf(out, size, "%.1f%%", duty); }
float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0);
}
void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char frequency[16], pulse[12];
Display::formatPwmFrequency(hz, frequency, sizeof(frequency));
Display::formatPulse(pulseNs, pulse, sizeof(pulse));
snprintf(out, size, UiText::TEST_FORMAT, frequency, pulse);
}
void formatMeasured(float hz, uint32_t pulseNs, char *out, size_t size) {
char frequency[12], pulse[12];
Display::formatFrequency(hz, frequency, sizeof(frequency));
Display::formatPulse(pulseNs, pulse, sizeof(pulse), true);
snprintf(out, size, "F:%s, P:%s", frequency, pulse);
}
void formatTestTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char target[32];
formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, "TEST: %s", target);
}
void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
char *out, size_t size) {
(void)reason;
char target[32];
formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, "FAIL AT %s", target);
} }
size_t utf8CharacterCount(const char *text) { size_t utf8CharacterCount(const char *text) {
@@ -81,12 +115,94 @@ uint32_t overallProgressTotal(uint32_t stageCount) {
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) { uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL); return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
} }
uint8_t lastValidMaxPulseIndex(uint32_t frequencyHz) {
uint8_t last = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS) - 1U);
while (last && static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[last]) * frequencyHz >= 1000000000ULL)
--last;
return last;
}
uint8_t firstMaxPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
for (uint8_t i = 0; i <= last; ++i)
if (MAX_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
return last;
}
uint8_t lastMinPulseIndexAtMost(uint32_t pulseNs) {
for (size_t i = countOf(MIN_PULSE_OPTIONS_NS); i > 0; --i)
if (MIN_PULSE_OPTIONS_NS[i - 1U] <= pulseNs) return static_cast<uint8_t>(i - 1U);
return 0;
}
uint32_t plannedPulseCaptureHz(uint32_t frequencyHz, float dutyPct) {
if (!frequencyHz || dutyPct <= 0.0f || dutyPct >= 100.0f) return 0;
return MCPWM_CAPTURE_RESOLUTION_HZ;
}
uint32_t plannedCaptureHz(uint32_t frequencyHz, float dutyPct) {
// One 32-bit S3 MCPWM capture timer measures period and pulse at 80 MHz.
return plannedPulseCaptureHz(frequencyHz, dutyPct);
}
bool pulsePointHasResolution(uint32_t hz, uint32_t pulseNs, float accuracyPct) {
uint32_t actualHz = 0, actualPulseNs = 0;
uint8_t bits = 0;
if (TARGET_IS_C3) {
IntegerPwmConfig config = {};
if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
actualHz = config.actualHz;
actualPulseNs = config.actualPulseNs;
bits = config.bits;
} else {
if (!hz || MCPWM_RESOLUTION_HZ % hz) return false;
const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
uint32_t activeTicks = static_cast<uint32_t>(
(static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) / 1000000000ULL);
if (!activeTicks || activeTicks >= periodTicks) return false;
actualHz = hz;
actualPulseNs = static_cast<uint32_t>(
(static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
MCPWM_RESOLUTION_HZ);
bits = 1;
for (uint32_t ticks = periodTicks; ticks > 1U; ticks >>= 1U) ++bits;
}
if (!periodWithin(actualHz, hz, accuracyPct) ||
!periodWithin(actualPulseNs, pulseNs, accuracyPct)) return false;
const float dutyPct = dutyFromPulse(actualHz, actualPulseNs);
const uint32_t captureHz = plannedCaptureHz(actualHz, dutyPct);
const uint32_t pulseCaptureHz = plannedPulseCaptureHz(actualHz, dutyPct);
return captureHz && pulseCaptureHz && validateResolution(actualHz, dutyPct, accuracyPct,
captureHz, pulseCaptureHz, bits, MEASUREMENT_AVERAGING_PERIODS) == FailReason::NONE;
}
uint32_t minimumPulseForAccuracy(uint32_t frequencyHz, float accuracyPct) {
for (uint32_t pulseNs : TEST_PULSE_WIDTHS_NS)
if (pulsePointHasResolution(frequencyHz, pulseNs, accuracyPct)) return pulseNs;
return UINT32_MAX;
}
uint8_t firstMinPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
for (uint8_t i = 0; i <= last; ++i)
if (MIN_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
return last;
}
uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) {
if (first >= last) return first;
if (direction > 0) return value >= last ? first : static_cast<uint8_t>(value + 1U);
return value <= first ? last : static_cast<uint8_t>(value - 1U);
}
} }
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {} App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
void App::begin() { void App::begin() {
Serial.begin(SERIAL_BAUD); Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD); Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD);
startButton_.begin(); modeButton_.begin(); pwm_.begin(); startButton_.begin(); modeButton_.begin(); pwm_.begin();
bootCheckStartedMs_ = millis(); bootCheckStartedMs_ = millis();
@@ -103,11 +219,12 @@ void App::finishInitialization(bool factoryReset) {
} else if (!store_.load(settings_)) { } else if (!store_.load(settings_)) {
store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded"); store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
} }
sanitizeRange();
params_ = store_.params(settings_); params_ = store_.params(settings_);
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational"); if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
initialized_ = true; initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong"); Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
setActivePerformance(false); setActivePerformance(false);
printConfiguration(); printConfiguration();
@@ -166,7 +283,7 @@ void App::update() {
} }
if (state_ == AppState::MENU) { if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 4U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
} }
else if (modeEvent == ButtonEvent::LONG) { else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
@@ -179,20 +296,25 @@ void App::update() {
return; return;
} }
if (state_ == AppState::SOLO_MEASURE) { if (state_ == AppState::SOLO_MEASURE) {
if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0) {
Log::event("MEASURE", "local stage watchdog expired");
measurement_.forceFail(FailReason::LOST_EDGE);
}
const MeasureState ms = measurement_.update(); const MeasureState ms = measurement_.update();
if (ms == MeasureState::FAIL) { if (ms == MeasureState::FAIL) {
pwm_.stop();
printStageStats(measurement_.stats(), actual_.actualHz); printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats()); showStageResult(measurement_.stats());
finish(false, measurement_.reason(), true); finish(false, measurement_.reason(), true);
} }
else if (ms == MeasureState::PASS) { else if (ms == MeasureState::PASS) {
pwm_.stop();
printStageStats(measurement_.stats(), actual_.actualHz); printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats()); showStageResult(measurement_.stats());
stagePassed(); stagePassed();
} else if (ms == MeasureState::STEP_READY) { } else if (measurement_.takeProgressUpdate()) {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) showStageResult(live); if (measurement_.statsSnapshot(live)) showStageResult(live);
measurement_.continueAfterDisplay();
} }
} 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_FINALIZE) { state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
@@ -212,8 +334,21 @@ void App::showIdle() {
} }
void App::sanitizeRange() { void App::sanitizeRange() {
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ); settings_.role = static_cast<uint8_t>(Role::SOLO);
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS);
settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS);
settings_.accuracyIndex %= countOf(ACCURACY_OPTIONS_PCT);
settings_.timeIndex %= countOf(TEST_TIME_OPTIONS_MS);
const uint32_t hz = PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex];
const uint8_t lastValid = lastValidMaxPulseIndex(hz);
if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid;
const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin;
const uint8_t firstMin = firstMinPulseIndexAtLeast(
minimumPulseForAccuracy(hz, ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), lastMin);
if (settings_.minPulseIndex < firstMin) settings_.minPulseIndex = firstMin;
} }
void App::serviceRxPinStateLog() { void App::serviceRxPinStateLog() {
@@ -231,17 +366,36 @@ void App::serviceRxPinStateLog() {
void App::changeMenu(int d) { void App::changeMenu(int d) {
sanitizeRange(); sanitizeRange();
uint8_t *value = nullptr; size_t count = 0; if (menuItem_ == 1) {
switch (menuItem_) { const uint8_t last = lastValidMaxPulseIndex(
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break; PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break; const uint8_t first = firstMaxPulseIndexAtLeast(
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break; MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break; settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
default: return; first, last, d);
} else if (menuItem_ == 2) {
const uint8_t last = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t first = firstMinPulseIndexAtLeast(
minimumPulseForAccuracy(PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex],
ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), last);
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex,
first, last, d);
} else {
uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) {
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
default: return;
}
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
} }
*value = static_cast<uint8_t>((*value + count + d) % count); sanitizeRange(); params_ = store_.params(settings_);
Log::printf("ACTION", "menu item=%u changed direction=%+d new-index=%u", menuItem_, d, *value); Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u",
sanitizeRange(); params_ = store_.params(settings_); showMenu(); menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex);
showMenu();
} }
void App::showMenu() { void App::showMenu() {
@@ -250,23 +404,26 @@ void App::showMenu() {
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all)); Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) { switch (menuItem_) {
case 0: case 0:
Display::formatTestFrequency(params_.startHz, value, sizeof(value)); Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U); label = UiText::MENU_FREQUENCY;
label = UiText::MENU_START_FREQUENCY;
break; break;
case 1: case 1:
Display::formatTestFrequency(params_.endHz, value, sizeof(value)); Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U); label = UiText::MENU_MAX_PULSE;
label = UiText::MENU_END_FREQUENCY;
break; break;
case 2: case 2:
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
label = UiText::MENU_MIN_PULSE;
break;
case 3:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct); snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = UiText::MENU_ACCURACY; label = UiText::MENU_ACCURACY;
break; break;
default: case 4:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f); snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = UiText::MENU_TEST_TIME; label = UiText::MENU_TEST_TIME;
break; break;
default: return;
} }
formatMenuLine(label, value, one, sizeof(one)); formatMenuLine(label, value, one, sizeof(one));
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total)); formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
@@ -277,18 +434,17 @@ void App::startTest() {
leaveIdlePowerSave(); leaveIdlePowerSave();
pwm_.stop(); pwm_.stop();
setActivePerformance(true); setActivePerformance(true);
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 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) {
char startText[12], endText[12]; Log::printf("CONFIG", "mode=%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums TX=%s RX=AUTO stages=%lu",
Display::formatFrequency(params_.startHz, startText, sizeof(startText)); roleName(static_cast<Role>(settings_.role)), params_.frequencyHz,
Display::formatFrequency(params_.endHz, endText, sizeof(endText)); params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu", PWM_ACTIVE_LEVEL == HIGH ? "HIGH" : "LOW",
roleName(static_cast<Role>(settings_.role)), startText, endText, stageCount_);
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
} }
printConfiguration(); printConfiguration();
const Role role = static_cast<Role>(settings_.role); const Role role = static_cast<Role>(settings_.role);
@@ -305,8 +461,8 @@ bool App::armSlave(bool preserveDisplay) {
pwm_.stop(); pwm_.stop();
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
params_ = store_.params(settings_); params_ = store_.params(settings_);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); stageIndex_ = 0; stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
if (!radio_.begin()) { if (!radio_.begin()) {
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST; state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
@@ -323,29 +479,40 @@ bool App::armSlave(bool preserveDisplay) {
} }
bool App::prepareStage(bool showProgress) { bool App::prepareStage(bool showProgress) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
actual_ = {}; actual_ = {};
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ : const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ); (receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; } if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; }
Log::printf("PWM", "starting GPIO=%u requested=%luHz duty=%u%%", GPIO_PWM, requestedHz_, params_.dutyPct); Log::printf("PWM", "starting GPIO=%u requested=%luHz pulse=%luns", GPIO_PWM, requestedHz_, requestedPulseNs_);
if (!pwm_.start(requestedHz_, params_.dutyPct, actual_)) { if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) {
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz; LEDC attach/write/read failed", Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz pulse=%luns; PWM setup failed",
GPIO_PWM, requestedHz_); GPIO_PWM, requestedHz_, requestedPulseNs_);
finish(false, FailReason::RESOLUTION); return false;
}
if (!periodWithin(actual_.actualHz, requestedHz_, params_.accuracyPct) ||
!periodWithin(actual_.actualPulseNs, requestedPulseNs_, params_.accuracyPct)) {
Log::printf("PWM", "requested point cannot be generated within tolerance: requested=%luHz/%luns actual=%luHz/%luns tolerance=%.2f%%",
requestedHz_, requestedPulseNs_, actual_.actualHz, actual_.actualPulseNs,
params_.accuracyPct);
finish(false, FailReason::RESOLUTION); return false; finish(false, FailReason::RESOLUTION); return false;
} }
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct); const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct, const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct,
plannedRxHz, actual_.bits, plannedRxHz, plannedPulseRxHz, actual_.bits,
MEASUREMENT_AVERAGING_PERIODS); MEASUREMENT_AVERAGING_PERIODS);
if (resolution != FailReason::NONE) { if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%", Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, plannedPulseRxHz,
effectiveTolerancePct(params_.accuracyPct)); effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false; finish(false, resolution); return false;
} }
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED", Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
stageIndex_ + 1, stageCount_, requestedHz_, actual_.actualHz, actual_.actualDutyPct, actual_.bits); stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
if (showProgress) showStageProgress(); if (showProgress) showStageProgress();
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
finish(false, FailReason::UNSUPPORTED); return false; finish(false, FailReason::UNSUPPORTED); return false;
@@ -354,23 +521,33 @@ bool App::prepareStage(bool showProgress) {
} }
bool App::startLocalMeasurement(float hz, float duty) { bool App::startLocalMeasurement(float hz, float duty) {
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums average=%u periods; per-pulse logging suspended", Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% period-capture=%luHz pulse-capture=%luHz settle=%u cycles window=%lums; every pulse validated",
hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty), hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs, MEASUREMENT_AVERAGING_PERIODS); receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES); MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES);
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED", const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
receiver_.receiveChunkSymbols()); static_cast<uint32_t>(hz + 0.5f));
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
Log::printf("MEASURE", "receiver start %s, continuous edge capture", ok ? "OK" : "FAILED");
return ok; return ok;
} }
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();
// With PWM already quiet it is safe to stop capture before clearing its
// queue for the next pulse width. Never reset a FreeRTOS queue concurrently
// with the capture ISR.
if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop();
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; } if (++stageIndex_ >= stageCount_) { 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, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
actual_ = {}; actual_ = {};
stageStartConfirmed_ = false; stageStartConfirmed_ = false;
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE); pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
@@ -380,7 +557,8 @@ void App::stagePassed() {
void App::startMasterDiscovery() { void App::startMasterDiscovery() {
session_ = esp_random(); if (!session_) session_ = 1; session_ = esp_random(); if (!session_) session_ = 1;
sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush(); sequence_ = 1; stageIndex_ = 0; requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
opticalWakeActive_ = true; opticalWakeActive_ = true;
lastOpticalWakeToggleMs_ = millis(); lastOpticalWakeToggleMs_ = millis();
pwm_.active(); pwm_.active();
@@ -394,9 +572,8 @@ ProtocolPacket App::makePacket(MessageType type) const {
ProtocolPacket p = {}; ProtocolPacket p = {};
p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_; p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_;
p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_; p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_;
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz; p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct; p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
p.testTimeMs = params_.testTimeMs; p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES; p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
return p; return p;
@@ -458,7 +635,8 @@ void App::handleRadio() {
opticalWakeActive_ = false; opticalWakeActive_ = false;
pwm_.stop(); pwm_.stop();
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis(); memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue; char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
} }
@@ -484,7 +662,8 @@ void App::handleRadio() {
sequence_ = r.packet.sequence; sequence_ = r.packet.sequence;
state_ = AppState::SLAVE_WAIT_START; state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs; params_.testTimeMs = r.packet.testTimeMs;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz; params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
stageCount_ = r.packet.stageCount; stageCount_ = r.packet.stageCount;
actual_ = {}; actual_ = {};
ProtocolPacket ready = makePacket(MessageType::READY); ProtocolPacket ready = makePacket(MessageType::READY);
@@ -498,8 +677,10 @@ void App::handleRadio() {
r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED) r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED; ? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED;
if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz; if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz;
if (r.packet.requestedPulseNs) requestedPulseNs_ = r.packet.requestedPulseNs;
actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_; actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_;
actual_.actualDutyPct = r.packet.actualDutyX100 ? r.packet.actualDutyX100 / 100.0f : params_.dutyPct; actual_.actualPulseNs = r.packet.actualPulseNs ? r.packet.actualPulseNs : requestedPulseNs_;
actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
measurement_.abort(); finish(false, reason); continue; measurement_.abort(); finish(false, reason); continue;
} }
if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) { if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) {
@@ -535,7 +716,8 @@ void App::handleRadio() {
sendLinked(pendingPacket_); sendLinked(pendingPacket_);
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) { } else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
sequence_ = r.packet.sequence; sequence_ = r.packet.sequence;
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f; actual_.actualHz = r.packet.actualHz; actual_.actualPulseNs = r.packet.actualPulseNs;
actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; } if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
showStageProgress(); showStageProgress();
state_ = AppState::SLAVE_MEASURE; state_ = AppState::SLAVE_MEASURE;
@@ -549,6 +731,10 @@ void App::handleRadio() {
started.sequence = r.packet.sequence; sendLinked(started); started.sequence = r.packet.sequence; sendLinked(started);
} else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) { } else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) {
if (pendingPacket_.passed) { if (pendingPacket_.passed) {
// Master sends ACK only after stopping its PWM. Disable capture for
// every completed stage, so the next start can clear its queue without
// racing the ISR (not only after the final stage).
receiver_.stop();
if (r.packet.passed) { if (r.packet.passed) {
radio_.end(); pendingReason_ = FailReason::NONE; radio_.end(); pendingReason_ = FailReason::NONE;
if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER); if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
@@ -601,8 +787,12 @@ void App::updateSlave() {
updateHeartbeat(); updateHeartbeat();
if (state_ == AppState::FINISHED) return; if (state_ == AppState::FINISHED) return;
if (state_ == AppState::SLAVE_MEASURE) { if (state_ == AppState::SLAVE_MEASURE) {
if (static_cast<int32_t>(millis() - localMeasurementDeadlineMs_) >= 0) {
Log::event("MEASURE", "Slave local stage watchdog expired");
measurement_.forceFail(FailReason::LOST_EDGE);
}
const MeasureState ms = measurement_.update(); const MeasureState ms = measurement_.update();
if (ms == MeasureState::STEP_READY) { if (measurement_.takeProgressUpdate()) {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) { if (measurement_.statsSnapshot(live)) {
ProtocolPacket progress = makePacket(MessageType::PROGRESS); ProtocolPacket progress = makePacket(MessageType::PROGRESS);
@@ -611,7 +801,6 @@ void App::updateSlave() {
progress.sequence = sequence_; sendLinked(progress); progress.sequence = sequence_; sendLinked(progress);
showStageResult(live); showStageResult(live);
} }
measurement_.continueAfterDisplay();
return; return;
} }
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) { if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
@@ -641,7 +830,7 @@ void App::sendAbort(FailReason reason) {
++sequence_; ++sequence_;
ProtocolPacket packet = makePacket(MessageType::ABORT); ProtocolPacket packet = makePacket(MessageType::ABORT);
packet.reason = static_cast<uint8_t>(reason); packet.reason = static_cast<uint8_t>(reason);
if (!packet.actualDutyX100) packet.actualDutyX100 = params_.dutyPct * 100U; if (!packet.actualPulseNs) packet.actualPulseNs = requestedPulseNs_;
sendLinked(packet); sendLinked(packet);
} }
@@ -675,10 +864,9 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
setActivePerformance(false); setActivePerformance(false);
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
if (slaveLinkLost) { if (slaveLinkLost) {
char target[12], one[64]; char target[32], one[64];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target)); formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_, display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
roleCorner(Role::SLAVE)); roleCorner(Role::SLAVE));
armSlave(true); armSlave(true);
@@ -700,10 +888,9 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN); display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
} }
else if (requestedHz_) { else if (requestedHz_) {
char frequency[12]; char target[32];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency)); formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency, snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_, display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
roleCorner(static_cast<Role>(settings_.role))); roleCorner(static_cast<Role>(settings_.role)));
} else { } else {
@@ -713,7 +900,10 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
} }
bool App::idlePowerSaveAllowed() const { bool App::idlePowerSaveAllowed() const {
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::MENU || // Never enter blocking light sleep while the settings screen is open. A
// wake-up press is deliberately consumed by the button state machine, which
// is useful in IDLE but makes menu navigation appear frozen.
return initialized_ && (state_ == AppState::IDLE ||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY); state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
} }
@@ -767,7 +957,9 @@ void App::serviceIdlePowerSave() {
idleSleepRadioStopped_ = true; idleSleepRadioStopped_ = true;
} }
display_.setPower(false); display_.setPower(false);
Log::event("POWER", "idle timeout; OLED off and light sleep started"); Log::event("POWER", "idle timeout; preparing light sleep");
Serial.flush();
delay(2);
} }
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START), gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START),
@@ -775,10 +967,6 @@ void App::serviceIdlePowerSave() {
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE), gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE),
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
if (static_cast<Role>(settings_.role) == Role::SLAVE) { if (static_cast<Role>(settings_.role) == Role::SLAVE) {
// Light-sleep GPIO wake is level-triggered in ESP-IDF. Arm the level
// opposite to the one sampled immediately before sleep, which makes a
// transition (either edge) necessary and prevents a steady RX level from
// waking Slave continuously.
const bool currentRxHigh = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0; const bool currentRxHigh = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0;
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_RX), gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_RX),
currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
@@ -786,25 +974,46 @@ void App::serviceIdlePowerSave() {
esp_sleep_enable_gpio_wakeup(); esp_sleep_enable_gpio_wakeup();
const esp_err_t result = esp_light_sleep_start(); const esp_err_t result = esp_light_sleep_start();
if (result != ESP_OK) { if (result != ESP_OK) {
leaveIdlePowerSave(true);
delay(1); delay(1);
return; return;
} }
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) { const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL || const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL; digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
if (buttonWake) { if (buttonWake) {
// The wake-up press is deliberately consumed. Holding or releasing it startButton_.suppressUntilRelease();
// must not later turn into a SHORT, LONG, or REPEAT event. modeButton_.suppressUntilRelease();
startButton_.suppressUntilRelease();
modeButton_.suppressUntilRelease();
leaveIdlePowerSave();
Log::event("POWER", "button wake consumed; next press will perform the action");
} else if (static_cast<Role>(settings_.role) == Role::SLAVE) {
leaveIdlePowerSave();
Log::event("POWER", "optical input woke Slave");
}
} }
// GPIO wake worked, so disarm all level sources before peripherals and the
// button state machines are brought back up.
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_START));
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE));
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_RX));
// Native USB and I2C can retain stale driver state across light sleep even
// though their clocks have stopped. A full end/begin cycle prevents the
// several-second button stalls and restores Serial output after wake.
setActivePerformance(false);
Serial.end();
delay(2);
Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
Wire.end();
Wire.begin(GPIO_SDA, GPIO_SCL);
Wire.setClock(400000);
Wire.setTimeOut(30);
// This also restores ESP-NOW when Slave stopped it before sleeping.
leaveIdlePowerSave(true);
Log::printf("POWER", "light sleep wake cause=%u button=%s; peripherals restored",
static_cast<unsigned>(cause), buttonWake ? "YES" : "NO");
if (buttonWake)
Log::event("POWER", "wake button consumed; next press will perform the action");
} }
void App::printConfiguration() { void App::printConfiguration() {
@@ -815,12 +1024,16 @@ void App::printConfiguration() {
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX, Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL); GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n", Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, RX AUTO\n",
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct); params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); params_.accuracyPct, params_.testTimeMs);
Serial.printf("Frequencies (%lu): ", stageCount_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ","); Serial.printf("Pulse widths descending (%lu): ", stageCount_);
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong"); for (uint32_t i = 0; i < stageCount_; ++i)
Serial.printf("%lu%s", pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, i),
i + 1 == stageCount_ ? " ns\n" : ",");
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(),
receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
} }
uint64_t App::actualNominalTotalUs() { uint64_t App::actualNominalTotalUs() {
@@ -833,28 +1046,32 @@ uint64_t App::actualNominalTotalUs() {
void App::printStageStats(const StageStats &s, uint32_t hz) { 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 uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
static_cast<double>(s.activeSum) * 1000000000.0 /
(static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
char requestedText[12], measuredText[12]; char requestedText[12], measuredText[12];
Display::formatFrequency(hz, requestedText, sizeof(requestedText)); Display::formatFrequency(hz, requestedText, sizeof(requestedText));
Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText)); Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL"; const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
Log::printf("RESULT", "%s %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s", Log::printf("RESULT", "%s/%luns %s periods=%lu measured=%s/%luns skipped=%lu%s%s",
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems, requestedText, requestedPulseNs_, status, s.periods, measuredText, measuredPulseNs, s.droppedItems,
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason)); s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
} }
void App::showStageResult(const StageStats &s) { void App::showStageResult(const StageStats &s) {
char one[64], two[64]; char one[64], two[64];
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
if (s.reason != FailReason::NONE) { if (s.reason != FailReason::NONE) {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct); formatFailure(s.reason, requestedHz_, requestedPulseNs_, one, sizeof(one));
if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) { if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) {
char frequency[12]; char frequency[12];
Display::formatTestFrequency(static_cast<uint32_t>(lroundf(s.badFrequency)), frequency, sizeof(frequency)); Display::formatFrequency(s.badFrequency, frequency, sizeof(frequency));
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency); snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
} else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) { } else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
char duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty)); char pulse[12];
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty); Display::formatPulse(pulseFromDuty(s.badFrequency, s.badDuty), pulse,
sizeof(pulse), true);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
} else { } else {
snprintf(two, sizeof(two), "%s", uiFailName(s.reason)); snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
} }
@@ -862,17 +1079,16 @@ void App::showStageResult(const StageStats &s) {
overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role))); overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role)));
return; return;
} }
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
if (!s.periods || !s.periodSum) { if (!s.periods || !s.periodSum) {
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()), display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
return; 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 uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency)); static_cast<double>(s.activeSum) * 1000000000.0 /
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty); (static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
formatMeasured(measuredHz, measuredPulseNs, two, sizeof(two));
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
} }
@@ -880,31 +1096,25 @@ void App::showStageResult(const StageStats &s) {
void App::showRemoteResult(const ProtocolPacket &packet) { void App::showRemoteResult(const ProtocolPacket &packet) {
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED) const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED; ? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
char target[12], one[64], two[64]; char one[64], two[64];
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz, formatTestTarget(packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
target, sizeof(target));
if (reason == FailReason::NONE) { if (reason == FailReason::NONE) {
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT,
target, packet.actualDutyX100 / 100.0f, stage);
if (packet.measuredHzX10) { if (packet.measuredHzX10) {
char measured[12]; formatMeasured(packet.measuredHzX10 / 10.0f, packet.measuredPulseNs, two, sizeof(two));
Display::formatFrequency(packet.measuredHzX10 / 10.0f, measured, sizeof(measured));
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", measured, packet.measuredDutyX10 / 10.0f);
} else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT); } else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) { } else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f); char frequency[12];
char measured[12]; Display::formatFrequency(packet.measuredHzX10 / 10.0f, frequency, sizeof(frequency));
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured)); snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured); } else if (reason == FailReason::DUTY_OUT && packet.measuredPulseNs) {
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) { char pulse[12];
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f); Display::formatPulse(packet.measuredPulseNs, pulse, sizeof(pulse), true);
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty)); snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
} else { } else {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", uiFailName(reason)); snprintf(two, sizeof(two), "%s", uiFailName(reason));
} }
if (reason != FailReason::NONE)
formatFailure(reason, packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
display_.show(one, two, overallProgress(stageIndex_, packet.progressStep), display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr : overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr :
roleCorner(static_cast<Role>(settings_.role))); roleCorner(static_cast<Role>(settings_.role)));
@@ -918,16 +1128,15 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
stats.badFrequency > 0.0f; stats.badFrequency > 0.0f;
const float measuredHz = badPeriod ? stats.badFrequency : const float measuredHz = badPeriod ? stats.badFrequency :
static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum; static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum;
const float measuredDuty = badPeriod ? stats.badDuty : 100.0f * stats.activeSum / stats.periodSum;
packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f)); packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f));
packet.measuredDutyX10 = static_cast<uint16_t>(lroundf(measuredDuty * 10.0f)); packet.measuredPulseNs = badPeriod ? pulseFromDuty(measuredHz, stats.badDuty) :
static_cast<uint32_t>(lround(static_cast<double>(stats.activeSum) * 1000000000.0 /
(static_cast<uint64_t>(receiver_.pulseTickHz()) * stats.periods)));
} }
void App::showStageProgress() { void App::showStageProgress() {
char target[12], one[64], stage[12]; char one[64];
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0), display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
} }

View File

@@ -66,7 +66,7 @@ class App {
AppState state_ = AppState::IDLE; AppState state_ = AppState::IDLE;
uint8_t menuItem_ = 0; uint8_t menuItem_ = 0;
uint32_t stageIndex_ = 0, stageCount_ = 0; uint32_t stageIndex_ = 0, stageCount_ = 0;
uint32_t requestedHz_ = 0; uint32_t requestedHz_ = 0, requestedPulseNs_ = 0;
ActualPwm actual_ = {}; ActualPwm actual_ = {};
FailReason pendingReason_ = FailReason::NONE; FailReason pendingReason_ = FailReason::NONE;
uint32_t session_ = 0; uint32_t session_ = 0;
@@ -74,6 +74,7 @@ class App {
uint8_t peer_[6] = {}; uint8_t peer_[6] = {};
bool havePeer_ = false; bool havePeer_ = false;
uint32_t deadlineMs_ = 0, lastSendMs_ = 0; uint32_t deadlineMs_ = 0, lastSendMs_ = 0;
uint32_t localMeasurementDeadlineMs_ = 0;
uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0; uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0;
uint8_t retries_ = 0; uint8_t retries_ = 0;
ProtocolPacket pendingPacket_ = {}; ProtocolPacket pendingPacket_ = {};

View File

@@ -17,8 +17,8 @@
constexpr uint32_t PWM_OUTPUT_TEST_FREQUENCY_HZ = 1000; constexpr uint32_t PWM_OUTPUT_TEST_FREQUENCY_HZ = 1000;
constexpr uint32_t PWM_OUTPUT_TEST_SWEEP_PERIOD_MS = 2000; constexpr uint32_t PWM_OUTPUT_TEST_SWEEP_PERIOD_MS = 2000;
constexpr uint32_t PWM_OUTPUT_TEST_UPDATE_MS = 10; constexpr uint32_t PWM_OUTPUT_TEST_UPDATE_MS = 10;
constexpr uint8_t PWM_OUTPUT_TEST_MIN_DUTY_PCT = 5; constexpr uint32_t PWM_OUTPUT_TEST_MIN_PULSE_NS = 1000;
constexpr uint8_t PWM_OUTPUT_TEST_MAX_DUTY_PCT = 95; constexpr uint32_t PWM_OUTPUT_TEST_MAX_PULSE_NS = 10000;
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
constexpr bool TARGET_IS_C3 = true; constexpr bool TARGET_IS_C3 = true;
@@ -66,17 +66,21 @@ constexpr uint8_t OLED_ROTATION = 0;
constexpr uint8_t OLED_ADDRESS = 0x3C; constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6; constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6;
constexpr uint32_t SERIAL_BAUD = 115200; constexpr uint32_t SERIAL_BAUD = 115200;
// Native USB CDC may keep a stale "connected" state after light sleep. Keep
// logging non-blocking so a missing host can never delay button polling.
constexpr uint32_t SERIAL_TX_TIMEOUT_MS = 2;
constexpr bool SERIAL_ACTION_LOG = true; constexpr bool SERIAL_ACTION_LOG = true;
constexpr bool SERIAL_LOG_TIMESTAMPS = true; constexpr bool SERIAL_LOG_TIMESTAMPS = true;
constexpr bool SERIAL_MINIMAL_LOG = true; constexpr bool SERIAL_MINIMAL_LOG = true;
#define BUTTON_ACTIVE_LEVEL LOW #define BUTTON_ACTIVE_LEVEL LOW
// Raw GPIO_RX level that means the optical receiver is active. // Raw GPIO_RX level that means the optical receiver is active.
#define RX_ACTIVE_LEVEL LOW #define RX_ACTIVE_LEVEL HIGH
// PWM_SAFE_LEVEL must switch the optical transmitter fully off and is used // PWM_ACTIVE_LEVEL is the electrical level of the active test pulse and is
// during tests whenever PWM is stopped, and while the controller sleeps. // also used for the constant active output while awake outside a test. During
// PWM_ACTIVE_LEVEL intentionally keeps the transmitter active while the // the remainder of a running PWM period the output is !PWM_ACTIVE_LEVEL.
// controller is awake and no test is in progress. // PWM_SAFE_LEVEL is used only while PWM is stopped and during sleep; it is
// independent of the PWM inactive level and may equal PWM_ACTIVE_LEVEL.
#define PWM_SAFE_LEVEL HIGH #define PWM_SAFE_LEVEL HIGH
#define PWM_ACTIVE_LEVEL LOW #define PWM_ACTIVE_LEVEL LOW
#define PWM_SETTLE_CYCLES 5U #define PWM_SETTLE_CYCLES 5U
@@ -99,14 +103,10 @@ constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50; constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50;
constexpr uint8_t FINAL_ACK_RETRIES = 2; constexpr uint8_t FINAL_ACK_RETRIES = 2;
constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8; constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8;
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;
constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
constexpr uint16_t PERIOD_BATCH_SIZE = 128; constexpr uint16_t PERIOD_BATCH_SIZE = 128;
// Frequency and duty are validated only by their averages over this many // Retained as the minimum statistical depth used by the hardware-resolution
// complete periods. Individual tick variation is retained for diagnostics but // calculation and diagnostics. PASS/FAIL is evaluated for every complete
// is not itself a test failure. // pulse independently; accumulated values are used only for display.
constexpr uint16_t MEASUREMENT_AVERAGING_PERIODS = 100; constexpr uint16_t MEASUREMENT_AVERAGING_PERIODS = 100;
static_assert(MEASUREMENT_AVERAGING_PERIODS > 0, static_assert(MEASUREMENT_AVERAGING_PERIODS > 0,
"Averaging window must contain at least one period"); "Averaging window must contain at least one period");
@@ -123,38 +123,40 @@ constexpr uint32_t RX_PROCESSING_PERIODS_PER_SECOND = 300000;
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;
// RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that // S3 MCPWM Capture uses one 32-bit 80 MHz timer for both edges. Unlike RMT,
// still fits both levels of the current PWM signal: 20, 40 or 80 MHz. // its width does not constrain long LOW/HIGH intervals, so capture precision
constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000}; // stays at 12.5 ns for every selectable PWM frequency and pulse length.
constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766; constexpr uint32_t MCPWM_CAPTURE_RESOLUTION_HZ = 80000000;
// C3 uses the 40 MHz crystal as the LEDC clock. // C3 uses the 40 MHz crystal as the LEDC clock.
// Keep this explicit so the resolution calculation never asks LEDC for an // Keep this explicit so the resolution calculation never asks LEDC for an
// impossible frequency/resolution combination. // impossible frequency/resolution combination.
constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000; constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000;
constexpr uint8_t LEDC_CHANNEL = 0; constexpr uint8_t LEDC_CHANNEL = 0;
constexpr uint8_t LEDC_MAX_BITS = 14; constexpr uint8_t LEDC_MAX_BITS = 14;
// S3 uses the dedicated MCPWM peripheral. A 40 MHz timer clock keeps the // S3 uses the dedicated MCPWM peripheral. A 20 MHz timer clock keeps the
// longest 1 kHz period within the S3's 16-bit MCPWM counter and makes every // selectable 500 Hz period within the S3's 16-bit counter while retaining
// frequency in TEST_FREQUENCIES_HZ exact. // 50 ns pulse resolution and exact periods for every menu frequency.
constexpr uint32_t MCPWM_RESOLUTION_HZ = 40000000; constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000;
constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535; constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535;
// -------------------------- Menu value arrays ----------------------------- // -------------------------- Menu value arrays -----------------------------
// START and END deliberately have separate, independently cycling menu lists. // The test uses one selected PWM frequency and walks the pulse-width list from
// Every value is exactly achievable from a 40 MHz timer clock. The test walks // the selected maximum down to the selected minimum. Widths are stored in
// TEST_FREQUENCIES_HZ between the selected endpoints, so there is no // nanoseconds so sub-microsecond pulses remain representable without floats.
// separately configurable step. constexpr uint32_t PWM_FREQUENCY_OPTIONS_HZ[] = {
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 10000, 100000}; 500, 1000, 2000, 5000, 10000, 25000,
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 500000, 1000000}; };
constexpr uint32_t MAX_PULSE_OPTIONS_NS[] = {
// All achievable whole-number frequencies in the supported 1 kHz..1 MHz 20000, 50000, 100000, 200000, 500000
// range, used for adjacent test stages rather than direct menu selection. };
constexpr uint32_t TEST_FREQUENCIES_HZ[] = { constexpr uint32_t MIN_PULSE_OPTIONS_NS[] = {
1000, 2000, 5000, 10000, 25000, 50000, 250, 500, 1000, 2000, 5000, 10000
100000, 200000, 312500, 400000, 500000, 625000, 800000, 1000000 };
constexpr uint32_t TEST_PULSE_WIDTHS_NS[] = {
250, 500, 1000, 2000, 5000, 10000, 20000, 50000,
100000, 200000, 500000, 1000000
}; };
constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f}; constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f};
constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000}; constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000};
constexpr uint8_t TEST_DUTY_PCT = 50;
template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; } template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; }

View File

@@ -21,7 +21,7 @@ constexpr const char *FAIL_NAMES[] = {
"НЕТ ОШИБКИ", "НЕТ ОШИБКИ",
"НЕТ СИГНАЛА", "НЕТ СИГНАЛА",
"ПЕРИОД ВНЕ ДОПУСКА", "ПЕРИОД ВНЕ ДОПУСКА",
"ЗАПОЛН. ВНЕ ДОПУСКА", "ИМПУЛЬС ВНЕ ДОПУСКА",
"ЛИШНИЙ ФРОНТ", "ЛИШНИЙ ФРОНТ",
"ИМПУЛЬСНАЯ ПОМЕХА", "ИМПУЛЬСНАЯ ПОМЕХА",
"ПРОПУЩЕН ФРОНТ", "ПРОПУЩЕН ФРОНТ",
@@ -35,8 +35,9 @@ constexpr const char *FAIL_NAMES[] = {
constexpr const char *MODE_PREFIX = "РЕЖИМ: "; constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ"; constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *MENU_START_FREQUENCY = "ЧАСТОТА ОТ:"; constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
constexpr const char *MENU_END_FREQUENCY = "ЧАСТОТА ДО:"; constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:"; constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
@@ -54,11 +55,11 @@ constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН"; constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН"; constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
constexpr const char *FAIL_FORMAT = "СБОЙ %s %.0f%%"; constexpr const char *FAIL_FORMAT = "СБОЙ %s";
constexpr const char *TEST_FORMAT = "Тест:%-6s %2.0f%% %5s"; constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "ОШИБКА ЧАСТОТЫ %s"; constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "ОШИБКА ЗАПОЛН. %s"; constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%"; constexpr const char *NO_MEASUREMENT = "F:---, P:---";
#elif UI_LANGUAGE == UI_LANGUAGE_EN #elif UI_LANGUAGE == UI_LANGUAGE_EN
@@ -70,7 +71,7 @@ constexpr const char *FAIL_NAMES[] = {
"NONE", "NONE",
"NO SIGNAL", "NO SIGNAL",
"PERIOD OUT", "PERIOD OUT",
"DUTY OUT", "PULSE OUT",
"EXTRA EDGE", "EXTRA EDGE",
"GLITCH", "GLITCH",
"LOST EDGE", "LOST EDGE",
@@ -84,8 +85,9 @@ constexpr const char *FAIL_NAMES[] = {
constexpr const char *MODE_PREFIX = "MODE: "; constexpr const char *MODE_PREFIX = "MODE: ";
constexpr const char *START_RUN = "READY TO START"; constexpr const char *START_RUN = "READY TO START";
constexpr const char *MENU_START_FREQUENCY = "START FREQ:"; constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
constexpr const char *MENU_END_FREQUENCY = "END FREQ:"; constexpr const char *MENU_MAX_PULSE = "MAX PULSE:";
constexpr const char *MENU_MIN_PULSE = "MIN PULSE:";
constexpr const char *MENU_ACCURACY = "ACCURACY:"; constexpr const char *MENU_ACCURACY = "ACCURACY:";
constexpr const char *MENU_TEST_TIME = "TEST TIME:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:";
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:"; constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
@@ -103,11 +105,11 @@ constexpr const char *START_AGAIN = "READY TO START";
constexpr const char *TEST_FAILED = "TEST FAILED"; constexpr const char *TEST_FAILED = "TEST FAILED";
constexpr const char *PASS_WORD = "TEST PASS"; constexpr const char *PASS_WORD = "TEST PASS";
constexpr const char *FAIL_FORMAT = "FAIL %s %.0f%%"; constexpr const char *FAIL_FORMAT = "FAIL %s";
constexpr const char *TEST_FORMAT = "Test:%-6s %2.0f%% %5s"; constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "PERIOD OUT %s"; constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "DUTY OUT %s"; constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%"; constexpr const char *NO_MEASUREMENT = "F:---, P:---";
#else #else
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU" #error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"

View File

@@ -10,7 +10,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", "PULSE OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED"}; "UNSUPPORTED", "RESOLUTION", "ABORTED"};
const uint8_t i = static_cast<uint8_t>(r); const uint8_t i = static_cast<uint8_t>(r);
@@ -31,19 +31,19 @@ uint32_t settingsChecksum(const Settings &s) {
return hash; return hash;
} }
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) { uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs) {
if (!startHz || endHz <= startHz) return 0; if (!minPulseNs || maxPulseNs < minPulseNs) return 0;
uint32_t count = 0; uint32_t count = 0;
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) for (size_t i = 0; i < countOf(TEST_PULSE_WIDTHS_NS); ++i)
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count; if (TEST_PULSE_WIDTHS_NS[i] >= minPulseNs && TEST_PULSE_WIDTHS_NS[i] <= maxPulseNs) ++count;
return count; return count;
} }
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) { uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index) {
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) { for (size_t i = countOf(TEST_PULSE_WIDTHS_NS); i > 0; --i) {
const uint32_t frequency = TEST_FREQUENCIES_HZ[i]; const uint32_t pulseNs = TEST_PULSE_WIDTHS_NS[i - 1U];
if (frequency < startHz || frequency > endHz) continue; if (pulseNs < minPulseNs || pulseNs > maxPulseNs) continue;
if (!index--) return frequency; if (!index--) return pulseNs;
} }
return 0; 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; RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs; 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 = const uint64_t settleUs =
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz; (1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
// Initial stage screen, nine intermediate screens and the final result. // Initial stage screen, nine intermediate screens and the final result.
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL * const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
(MEASUREMENT_PROGRESS_STEPS + 1U); (MEASUREMENT_PROGRESS_STEPS + 1U);
return samplingWallUs + batchWaitUs + settleUs + displayUs; return samplingWallUs + settleUs + displayUs;
} }
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) { uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
uint64_t total = 0; return static_cast<uint64_t>(pulseWidthPointCount(p.maxPulseNs, p.minPulseNs)) *
const uint32_t count = frequencyPointCount(p.startHz, p.endHz); nominalStageUs(p.frequencyHz, p.testTimeMs, settleCycles);
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) { bool periodWithin(float measured, float expected, float tolerance) {
@@ -116,49 +107,54 @@ uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
return fallback; return fallback;
} }
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz, bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
uint8_t maxBits, uint8_t dutyPct, uint32_t sourceClockHz, uint8_t maxBits,
IntegerPwmConfig &config) { IntegerPwmConfig &config) {
if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false; 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; bool found = false;
uint32_t bestErrorHz = 0; uint32_t bestFrequencyError = UINT32_MAX;
uint32_t bestDutyError = 0; uint32_t bestPulseError = UINT32_MAX;
uint32_t bestLevels = 1;
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) { for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
const uint32_t levels = 1UL << bits; const uint32_t levels = 1UL << bits;
for (uint32_t divider = 1; divider <= 1023U; ++divider) { const uint64_t dividerNumerator = static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE;
const uint32_t denominator = levels * divider; const uint64_t dividerDenominator = static_cast<uint64_t>(requestedHz) * levels;
// A fixed integer divider gives identical PWM periods. Requiring an const uint32_t dividerFloor = static_cast<uint32_t>(dividerNumerator / dividerDenominator);
// exact division also guarantees that the physical frequency is a const uint32_t candidates[] = {dividerFloor, dividerFloor + 1U};
// whole number of hertz rather than a rounded value. for (uint32_t dividerRaw : candidates) {
if (sourceClockHz % denominator) continue; if (dividerRaw < FRACTION_SCALE || dividerRaw > MAX_DIVIDER_RAW) continue;
const uint32_t actualHz = sourceClockHz / denominator; const uint64_t frequencyDenominator = static_cast<uint64_t>(levels) * dividerRaw;
const uint32_t errorHz = actualHz > requestedHz const uint32_t actualHz = static_cast<uint32_t>(
? actualHz - requestedHz : requestedHz - actualHz; (dividerNumerator + frequencyDenominator / 2U) / frequencyDenominator);
const uint32_t dutyCount = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U; if (!actualHz) continue;
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 uint64_t dutyNumerator = static_cast<uint64_t>(requestedPulseNs) *
const bool frequencyEqual = found && errorHz == bestErrorHz; sourceClockHz * FRACTION_SCALE;
const bool dutyBetter = frequencyEqual && const uint64_t dutyDenominator = static_cast<uint64_t>(dividerRaw) * 1000000000ULL;
static_cast<uint64_t>(dutyError) * bestLevels < uint32_t dutyCount = static_cast<uint32_t>((dutyNumerator + dutyDenominator / 2U) /
static_cast<uint64_t>(bestDutyError) * levels; dutyDenominator);
const bool dutyEqual = frequencyEqual && if (!dutyCount) dutyCount = 1U;
static_cast<uint64_t>(dutyError) * bestLevels == if (dutyCount >= levels) dutyCount = levels - 1U;
static_cast<uint64_t>(bestDutyError) * levels; if (!dutyCount) continue;
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue;
config.actualHz = actualHz; const uint32_t actualPulseNs = static_cast<uint32_t>(
config.divider = static_cast<uint16_t>(divider); (static_cast<uint64_t>(dutyCount) * dividerRaw * 1000000000ULL +
config.bits = bits; static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE / 2U) /
bestErrorHz = errorHz; (static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE));
bestDutyError = dutyError; const uint32_t frequencyError = actualHz > requestedHz ? actualHz - requestedHz : requestedHz - actualHz;
bestLevels = levels; 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; found = true;
} }
} }
@@ -166,40 +162,70 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
} }
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct, 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) { uint16_t averagingPeriods) {
if (!frequencyHz || !captureHz || !pwmBits || !averagingPeriods) if (!frequencyHz || !periodCaptureHz || !pulseCaptureHz || !pwmBits || !averagingPeriods)
return FailReason::RESOLUTION; return FailReason::RESOLUTION;
const float periodTicks = static_cast<float>(captureHz) / frequencyHz; const float periodTicks = static_cast<float>(periodCaptureHz) / frequencyHz;
const float activeTicks = periodTicks * dutyPct / 100.0f; const float activeTicks = static_cast<float>(pulseCaptureHz) * dutyPct /
const float inactiveTicks = periodTicks - activeTicks; (100.0f * frequencyHz);
if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION; if (periodTicks < 4.0f || activeTicks < 2.0f) return FailReason::RESOLUTION;
const float averagedTicks = periodTicks * averagingPeriods; const float timerPeriodError = 100.0f / periodTicks;
const float timerPeriodError = 100.0f / averagedTicks; const float timerPulseError = 100.0f / activeTicks;
const float timerDutyError = 100.0f / averagedTicks;
// Measurement uses the duty actually programmed into LEDC. A coarse PWM // 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 // step is not itself an error when the requested value (e.g. 50%) is exactly
// representable; only the selected value's actual quantization matters. // representable; only the selected value's actual quantization matters.
const float effectiveAccuracy = effectiveTolerancePct(accuracyPct); const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy) return (timerPeriodError > effectiveAccuracy || timerPulseError > effectiveAccuracy)
? FailReason::RESOLUTION : FailReason::NONE; ? FailReason::RESOLUTION : FailReason::NONE;
} }
FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz, FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz,
float expectedDuty, float tolerance, uint8_t repeat, float expectedDuty, float tolerance, uint8_t repeat,
StageStats &s) { 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 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.periods;
s.periodSum += p.periodTicks; s.activeSum += p.activeTicks; s.periodSum += p.periodTicks; s.activeSum += p.activeTicks;
if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks; if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks;
if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks; if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks;
if (p.activeTicks < s.minActive) s.minActive = p.activeTicks; if (p.activeTicks < s.minActive) s.minActive = p.activeTicks;
if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks; if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks;
FailReason reason = FailReason::NONE; // Validate every complete period independently. A single capture tick is the
if (!periodWithin(hz, expectedHz, tolerance)) reason = FailReason::PERIOD_OUT; // unavoidable endpoint uncertainty, so only that one tick may be corrected
else if (!dutyWithin(duty, expectedDuty, tolerance)) reason = FailReason::DUTY_OUT; // 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) { if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat; s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat;
s.badFrequency = hz; s.badDuty = duty; 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); 100.0 * static_cast<double>(activeSum) / periodSum);
bool frequencyOk = periodWithin(hz, expectedHz, tolerance); bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
if (!frequencyOk && maxPeriod == minPeriod + 1U) { 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 // result is quantization-limited. Accept only when a one-tick correction
// toward the expected value returns the averaged frequency into tolerance. // toward the expected value returns the averaged frequency into tolerance.
// Consecutive periods telescope into one first-to-last edge interval, so // 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); frequencyOk = periodWithin(correctedHz, expectedHz, tolerance);
} }
bool dutyOk = dutyWithin(duty, expectedDuty, tolerance); const double expectedPulseTicks = static_cast<double>(tickHz) * expectedDuty /
if (!dutyOk) { (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 // Unlike full periods, active intervals do not telescope: every pulse is
// bounded by a different rising/falling edge pair. With slowly drifting // bounded by a different rising/falling edge pair. With slowly drifting
// asynchronous clocks an entire short window can therefore quantize to // 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 // the same adjacent count (e.g. 41/80 for a true 50% duty). Apply one tick
// per active interval even when minActive == maxActive. // per active interval even when minActive == maxActive.
const bool dutyHigh = duty > expectedDuty; const bool dutyHigh = measuredPulseTicks > expectedPulseTicks;
const uint64_t correctedActive = dutyHigh const uint64_t correctedActive = dutyHigh
? (activeSum > periodCount ? activeSum - periodCount : 0U) ? (activeSum > periodCount ? activeSum - periodCount : 0U)
: activeSum + periodCount; : activeSum + periodCount;
const float correctedDuty = static_cast<float>( const double correctedPulseTicks = static_cast<double>(correctedActive) / periodCount;
100.0 * static_cast<double>(correctedActive) / periodSum); pulseOk = fabs(correctedPulseTicks - expectedPulseTicks) * 100.0 /
dutyOk = dutyWithin(correctedDuty, expectedDuty, tolerance); expectedPulseTicks <= tolerance + 0.0001;
} }
FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT : 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) { if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason; s.reason = reason;
s.firstBadPeriod = s.periods >= periodCount ? s.periods - periodCount + 1U : 1U; s.firstBadPeriod = s.periods >= periodCount ? s.periods - periodCount + 1U : 1U;

View File

@@ -15,26 +15,27 @@ const char *failName(FailReason reason);
struct Settings { struct Settings {
uint16_t version; uint16_t version;
uint8_t role; uint8_t role;
uint8_t startIndex; uint8_t frequencyIndex;
uint8_t endIndex; uint8_t maxPulseIndex;
uint8_t minPulseIndex;
uint8_t accuracyIndex; uint8_t accuracyIndex;
uint8_t timeIndex; uint8_t timeIndex;
uint8_t reserved;
uint32_t checksum; uint32_t checksum;
}; };
struct TestParams { struct TestParams {
uint32_t startHz; uint32_t frequencyHz;
uint32_t endHz; uint32_t maxPulseNs;
uint32_t minPulseNs;
float accuracyPct; float accuracyPct;
uint32_t testTimeMs; uint32_t testTimeMs;
uint8_t dutyPct;
}; };
struct PulsePeriod { struct PulsePeriod {
uint64_t startTick; uint64_t startTick;
uint32_t periodTicks; uint32_t periodTicks;
uint32_t activeTicks; uint32_t activeTicks;
uint32_t activeTickHz;
}; };
struct StageStats { struct StageStats {
@@ -63,13 +64,15 @@ struct PeriodLimits {
struct IntegerPwmConfig { struct IntegerPwmConfig {
uint32_t actualHz; uint32_t actualHz;
uint16_t divider; uint32_t dividerRaw;
uint32_t dutyCount;
uint32_t actualPulseNs;
uint8_t bits; uint8_t bits;
}; };
uint32_t settingsChecksum(const Settings &s); uint32_t settingsChecksum(const Settings &s);
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz); uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs);
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index); uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index);
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles); uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles);
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles); uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles);
bool periodWithin(float measuredHz, float expectedHz, float tolerancePct); bool periodWithin(float measuredHz, float expectedHz, float tolerancePct);
@@ -79,11 +82,12 @@ uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits); uint8_t maxBits);
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct); uint8_t maxBits, uint8_t dutyPct);
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz, bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
uint8_t maxBits, uint8_t dutyPct, uint32_t sourceClockHz, uint8_t maxBits,
IntegerPwmConfig &config); IntegerPwmConfig &config);
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct, FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t captureResolutionHz, uint8_t pwmBits, uint32_t periodResolutionHz, uint32_t pulseResolutionHz,
uint8_t pwmBits,
uint16_t averagingPeriods); uint16_t averagingPeriods);
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,

View File

@@ -26,6 +26,7 @@ Display::Display() : oled_(128, 32, &Wire, -1) {}
bool Display::begin() { bool Display::begin() {
Wire.begin(GPIO_SDA, GPIO_SCL); Wire.begin(GPIO_SDA, GPIO_SCL);
Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms
Wire.setTimeOut(30); // a faulty/stretched I2C bus must not stall button polling for seconds
// An absent optional OLED produces a large burst of ESP-IDF NACK messages. // An absent optional OLED produces a large burst of ESP-IDF NACK messages.
// Probe it once and keep the I2C driver quiet when no display is connected. // Probe it once and keep the I2C driver quiet when no display is connected.
esp_log_level_set("i2c.master", ESP_LOG_NONE); esp_log_level_set("i2c.master", ESP_LOG_NONE);
@@ -103,7 +104,7 @@ void Display::show(const char *a, const char *b, uint32_t progress,
void Display::formatFrequency(float hz, char *out, size_t n) { void Display::formatFrequency(float hz, char *out, size_t n) {
float value = hz; const char *suffix = "Hz"; float value = hz; const char *suffix = "Hz";
if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; } if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; }
else if (hz >= 1000.0f) { value = hz / 1000.0f; suffix = "k"; } else if (hz >= 999.5f) { value = hz / 1000.0f; suffix = "k"; }
if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f) if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f)
snprintf(out, n, "%.0f%s", value, suffix); snprintf(out, n, "%.0f%s", value, suffix);
else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix); else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix);
@@ -125,6 +126,30 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
snprintf(out, n, "%lu", hz); snprintf(out, n, "%lu", hz);
} }
void Display::formatPwmFrequency(uint32_t hz, char *out, size_t n) {
if (hz >= 1000000U && hz % 1000000U == 0U)
snprintf(out, n, "%luMHz", hz / 1000000U);
else if (hz >= 1000U && hz % 1000U == 0U)
snprintf(out, n, "%lukHz", hz / 1000U);
else if (hz >= 1000U)
snprintf(out, n, "%.3gkHz", hz / 1000.0f);
else
snprintf(out, n, "%luHz", hz);
}
void Display::formatPulse(uint32_t pulseNs, char *out, size_t n, bool measured) {
if (pulseNs >= 1000U) {
const float us = pulseNs / 1000.0f;
if (measured) snprintf(out, n, "%.2fu", us);
else if (pulseNs % 1000U == 0U) snprintf(out, n, "%luus", pulseNs / 1000U);
else snprintf(out, n, "%.2fus", us);
} else if (measured) {
snprintf(out, n, "%.3fu", pulseNs / 1000.0f);
} else {
snprintf(out, n, "%luns", pulseNs);
}
}
void Display::formatDuration(uint64_t us, char *out, size_t n) { void Display::formatDuration(uint64_t us, char *out, size_t n) {
const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL; const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
const uint64_t minutes = totalSeconds / 60ULL; const uint64_t minutes = totalSeconds / 60ULL;

View File

@@ -15,7 +15,9 @@ class Display {
bool powered() const { return powered_; } bool powered() const { return powered_; }
static void formatFrequency(float hz, char *out, size_t size); static void formatFrequency(float hz, char *out, size_t size);
static void formatTestFrequency(uint32_t hz, char *out, size_t size); static void formatTestFrequency(uint32_t hz, char *out, size_t size);
static void formatDuration(uint64_t us, char *out, size_t size); static void formatPwmFrequency(uint32_t hz, char *out, size_t size);
static void formatPulse(uint32_t pulseNs, char *out, size_t size, bool measured = false);
static void formatDuration(uint64_t us, char *out, size_t size);
private: private:
void drawTextLine(const char *text, int16_t y, int16_t startX = 0); void drawTextLine(const char *text, int16_t y, int16_t startX = 0);
Adafruit_SSD1306 oled_; Adafruit_SSD1306 oled_;

View File

@@ -7,7 +7,8 @@ namespace Log {
void event(const char *component, const char *message) { void event(const char *component, const char *message) {
if (!SERIAL_ACTION_LOG) return; if (!SERIAL_ACTION_LOG) return;
if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") && if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") &&
strcmp(component, "CONFIG") && strcmp(component, "RESULT") && strcmp(component, "ESP-NOW")) return; strcmp(component, "CONFIG") && strcmp(component, "RESULT") && strcmp(component, "CAPTURE") &&
strcmp(component, "ESP-NOW")) return;
if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message); if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message);
else Serial.printf("[%-8s] %s\n", component, message); else Serial.printf("[%-8s] %s\n", component, message);
} }

View File

@@ -12,13 +12,12 @@ bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
!receiver_.start(expectedHz_, expectedDutyPct_)) return false; !receiver_.start(expectedHz_, expectedDutyPct_)) return false;
settleCycles_ = settleCycles; settleLeft_ = settleCycles; settleCycles_ = settleCycles; settleLeft_ = settleCycles;
tolerancePct_ = tolerance; tolerancePct_ = tolerance;
averagingPeriods_ = averagingPeriods;
resetAveragingWindow();
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS; stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;
stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs / stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs /
(1000ULL * MEASUREMENT_PROGRESS_STEPS); (1000ULL * MEASUREMENT_PROGRESS_STEPS);
if (!stepTicks_) stepTicks_ = 1; if (!stepTicks_) stepTicks_ = 1;
currentStep_ = 0; currentStep_ = 0;
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
stats_.reset(); stats_.reset();
publishStats(); publishStats();
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis(); measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
@@ -30,13 +29,6 @@ bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
return true; return true;
} }
void Measurement::resetAveragingWindow() {
windowPeriodCount_ = 0;
windowPeriodSum_ = windowActiveSum_ = 0;
windowMinPeriod_ = UINT32_MAX;
windowMaxPeriod_ = 0;
}
void Measurement::taskEntry(void *context) { void Measurement::taskEntry(void *context) {
static_cast<Measurement *>(context)->taskLoop(); static_cast<Measurement *>(context)->taskLoop();
} }
@@ -50,30 +42,33 @@ void Measurement::taskLoop() {
void Measurement::fail(FailReason reason) { void Measurement::fail(FailReason reason) {
if (stats_.reason == FailReason::NONE) stats_.reason = reason; if (stats_.reason == FailReason::NONE) stats_.reason = reason;
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
publishStats(); publishStats();
receiver_.stop(); state_ = MeasureState::FAIL; // App stops PWM first and only then disables capture. Disabling MCPWM from
// this RX task while input edges are still arriving can race its ISR.
state_ = MeasureState::FAIL;
} }
void Measurement::completeMeasurement() { void Measurement::completeMeasurement() {
receiver_.stop(); const uint32_t dropped = receiver_.takeDroppedItems();
stats_.droppedItems += receiver_.takeDroppedItems(); stats_.droppedItems += dropped;
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; } if (dropped) { fail(FailReason::DATA_LOSS); return; }
if (windowPeriodCount_) {
const FailReason result = evaluatePeriodWindow(
windowPeriodSum_, windowActiveSum_, windowPeriodCount_, receiver_.tickHz(),
expectedHz_, expectedDutyPct_, tolerancePct_,
windowMinPeriod_, windowMaxPeriod_,
currentStep_ + 1U, stats_);
if (result != FailReason::NONE) { fail(result); return; }
}
publishStats(); publishStats();
if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) { if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) {
state_ = MeasureState::STEP_READY; // Capture and validation continue while the main task draws OLED. Pausing
// here would overflow the edge queue at higher PWM frequencies; stopping
// MCPWM Capture can race an edge ISR. Publish a snapshot, then advance the
// edge-based window without interrupting the RX pipeline.
__atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
measurementStartTick_ = deadlineTick_;
deadlineTick_ += stepTicks_;
measurementStartMs_ = lastPeriodMs_ = millis();
return; return;
} }
if (!stats_.periods) { if (!stats_.periods) {
fail(FailReason::DATA_LOSS); return; fail(FailReason::DATA_LOSS); return;
} }
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
state_ = MeasureState::PASS; state_ = MeasureState::PASS;
} }
@@ -92,11 +87,12 @@ bool Measurement::statsSnapshot(StageStats &out) const {
MeasureState Measurement::processOnce() { MeasureState Measurement::processOnce() {
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_; }
bool receivedPeriod = false; bool receivedPeriod = false;
for (;;) { for (;;) {
const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE, pdMS_TO_TICKS(2)); const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE, pdMS_TO_TICKS(2));
stats_.droppedItems += receiver_.takeDroppedItems(); const uint32_t dropped = receiver_.takeDroppedItems();
stats_.droppedItems += dropped;
if (dropped) { fail(FailReason::DATA_LOSS); return state_; }
if (!periodCount) break; if (!periodCount) break;
receivedPeriod = true; receivedPeriod = true;
for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) { for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) {
@@ -111,55 +107,32 @@ MeasureState Measurement::processOnce() {
} }
continue; continue;
} }
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_) { completeMeasurement(); return state_; } // trailing incomplete period while (period.startTick >= deadlineTick_) {
if (!period.periodTicks || period.activeTicks >= period.periodTicks) { // Progress boundaries never discard a pulse. A complete period is
// assigned by its start edge, then validated exactly once. The nine
// intermediate boundaries only publish UI snapshots.
completeMeasurement();
if (state_ != MeasureState::RUNNING) return state_;
}
if (!period.periodTicks || !period.activeTicks || !period.activeTickHz) {
fail(FailReason::EXTRA_EDGE); return state_; fail(FailReason::EXTRA_EDGE); return state_;
} }
++stats_.periods; const FailReason result = evaluatePeriod(period, receiver_.tickHz(),
stats_.periodSum += period.periodTicks; expectedHz_, expectedDutyPct_, tolerancePct_, currentStep_ + 1U, stats_);
stats_.activeSum += period.activeTicks; if (result != FailReason::NONE) { fail(result); return state_; }
if (period.periodTicks < stats_.minPeriod) stats_.minPeriod = period.periodTicks;
if (period.periodTicks > stats_.maxPeriod) stats_.maxPeriod = period.periodTicks;
if (period.activeTicks < stats_.minActive) stats_.minActive = period.activeTicks;
if (period.activeTicks > stats_.maxActive) stats_.maxActive = period.activeTicks;
if (period.periodTicks < windowMinPeriod_) windowMinPeriod_ = period.periodTicks;
if (period.periodTicks > windowMaxPeriod_) windowMaxPeriod_ = period.periodTicks;
++windowPeriodCount_;
windowPeriodSum_ += period.periodTicks;
windowActiveSum_ += period.activeTicks;
if (windowPeriodCount_ >= averagingPeriods_) {
const FailReason result = evaluatePeriodWindow(
windowPeriodSum_, windowActiveSum_, windowPeriodCount_, receiver_.tickHz(),
expectedHz_, expectedDutyPct_, tolerancePct_,
windowMinPeriod_, windowMaxPeriod_,
currentStep_ + 1U, stats_);
resetAveragingWindow();
if (result != FailReason::NONE) { fail(result); return state_; }
}
} }
} }
if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis(); 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 settleTimeout = edgeBasedTimeout;
static_cast<uint64_t>(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
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();
// RMT reports a block only after its user buffer has filled. At 1 kHz the
// minimum 48-symbol C3 block contains roughly 48 PWM periods and therefore
// arrives much later than the old 8-period timeout. Do not call that
// normal batching delay a lost edge.
const uint32_t batchPeriods = receiver_.receiveChunkSymbols();
const uint32_t batchTimeoutMs = expectedPeriodMs_ * (batchPeriods + NO_SIGNAL_TIMEOUT_PERIODS) + 2U;
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U; const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U;
const uint32_t receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : edgeTimeoutMs; if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > receiveTimeoutMs) {
fail(FailReason::LOST_EDGE); return state_; fail(FailReason::LOST_EDGE); return state_;
} }
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement(); if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
@@ -169,24 +142,16 @@ MeasureState Measurement::processOnce() {
MeasureState Measurement::update() { return state_; } MeasureState Measurement::update() { return state_; }
bool Measurement::continueAfterDisplay() { bool Measurement::takeProgressUpdate() {
if (state_ != MeasureState::STEP_READY) return false; return __atomic_exchange_n(&progressUpdatePending_, false, __ATOMIC_ACQ_REL);
if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
fail(FailReason::UNSUPPORTED);
return false;
}
// receiver_.start() starts a new RMT timebase and therefore a new sampling
// phase. Start a fresh averaging window for the new continuous capture.
resetAveragingWindow();
settleLeft_ = settleCycles_;
measurementStartTick_ = deadlineTick_ = 0;
startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0;
state_ = MeasureState::SETTLING;
xTaskNotifyGive(task_);
return true;
} }
void Measurement::abort() { void Measurement::abort() {
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING || if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
state_ == MeasureState::STEP_READY) fail(FailReason::ABORTED); fail(FailReason::ABORTED);
}
void Measurement::forceFail(FailReason reason) {
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
fail(reason);
} }

View File

@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "Receiver.h" #include "Receiver.h"
enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, STEP_READY, PASS, FAIL }; enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, PASS, FAIL };
class Measurement { class Measurement {
public: public:
@@ -10,8 +10,9 @@ class Measurement {
uint32_t testTimeMs, uint16_t averagingPeriods, uint32_t testTimeMs, uint16_t averagingPeriods,
uint8_t settleCycles); uint8_t settleCycles);
MeasureState update(); MeasureState update();
bool continueAfterDisplay(); bool takeProgressUpdate();
void abort(); void abort();
void forceFail(FailReason reason);
MeasureState state() const { return state_; } MeasureState state() const { return state_; }
FailReason reason() const { return stats_.reason; } FailReason reason() const { return stats_.reason; }
const StageStats &stats() const { return stats_; } const StageStats &stats() const { return stats_; }
@@ -24,7 +25,6 @@ class Measurement {
void fail(FailReason reason); void fail(FailReason reason);
void completeMeasurement(); void completeMeasurement();
void publishStats(); void publishStats();
void resetAveragingWindow();
PulseReceiver &receiver_; PulseReceiver &receiver_;
volatile MeasureState state_ = MeasureState::IDLE; volatile MeasureState state_ = MeasureState::IDLE;
TaskHandle_t task_ = nullptr; TaskHandle_t task_ = nullptr;
@@ -33,14 +33,11 @@ class Measurement {
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED; mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
uint32_t expectedHz_ = 0; uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 0.0f, tolerancePct_ = 0.0f; float expectedDutyPct_ = 0.0f, tolerancePct_ = 0.0f;
uint16_t averagingPeriods_ = 1;
uint32_t windowPeriodCount_ = 0;
uint64_t windowPeriodSum_ = 0, windowActiveSum_ = 0;
uint32_t windowMinPeriod_ = UINT32_MAX, windowMaxPeriod_ = 0;
uint8_t settleCycles_ = 0, settleLeft_ = 0; uint8_t settleCycles_ = 0, settleLeft_ = 0;
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0; uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0; uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1; uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
volatile uint8_t currentStep_ = 0; volatile uint8_t currentStep_ = 0;
volatile bool progressUpdatePending_ = false;
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {}; PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
}; };

View File

@@ -4,24 +4,24 @@
#ifdef PWM_OUTPUT_TEST #ifdef PWM_OUTPUT_TEST
PwmGenerator pwmOutputTest; PwmGenerator pwmOutputTest;
uint8_t pwmOutputTestDuty = 50; uint32_t pwmOutputTestPulseNs = PWM_OUTPUT_TEST_MIN_PULSE_NS;
uint32_t pwmOutputTestUpdatedMs = 0; uint32_t pwmOutputTestUpdatedMs = 0;
void setup() { void setup() {
Serial.begin(SERIAL_BAUD); Serial.begin(SERIAL_BAUD);
delay(200); delay(200);
Serial.printf("\nPWM OUTPUT TEST: GPIO=%u requested=%luHz duty=%u..%u%% sine=%lums safe=%s\n", Serial.printf("\nPWM OUTPUT TEST: GPIO=%u requested=%luHz pulse=%lu..%luns sine=%lums safe=%s\n",
GPIO_PWM, PWM_OUTPUT_TEST_FREQUENCY_HZ, GPIO_PWM, PWM_OUTPUT_TEST_FREQUENCY_HZ,
PWM_OUTPUT_TEST_MIN_DUTY_PCT, PWM_OUTPUT_TEST_MAX_DUTY_PCT, PWM_OUTPUT_TEST_MIN_PULSE_NS, PWM_OUTPUT_TEST_MAX_PULSE_NS,
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS, PWM_OUTPUT_TEST_SWEEP_PERIOD_MS,
PWM_SAFE_LEVEL == HIGH ? "HIGH" : "LOW"); PWM_SAFE_LEVEL == HIGH ? "HIGH" : "LOW");
pwmOutputTest.begin(); pwmOutputTest.begin();
ActualPwm actual = {}; ActualPwm actual = {};
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ,
pwmOutputTestDuty, actual)) { pwmOutputTestPulseNs, actual)) {
Serial.printf("PWM OUTPUT TEST STARTED: actual=%luHz duty=%.2f%% bits=%u\n", Serial.printf("PWM OUTPUT TEST STARTED: actual=%luHz pulse=%luns bits=%u\n",
actual.actualHz, actual.actualDutyPct, actual.bits); actual.actualHz, actual.actualPulseNs, actual.bits);
} else { } else {
Serial.println("PWM OUTPUT TEST FAILED"); Serial.println("PWM OUTPUT TEST FAILED");
} }
@@ -35,16 +35,16 @@ void loop() {
constexpr float PWM_TWO_PI = 6.28318530718f; constexpr float PWM_TWO_PI = 6.28318530718f;
const float phase = PWM_TWO_PI * (now % PWM_OUTPUT_TEST_SWEEP_PERIOD_MS) / const float phase = PWM_TWO_PI * (now % PWM_OUTPUT_TEST_SWEEP_PERIOD_MS) /
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS; PWM_OUTPUT_TEST_SWEEP_PERIOD_MS;
const float center = (PWM_OUTPUT_TEST_MIN_DUTY_PCT + PWM_OUTPUT_TEST_MAX_DUTY_PCT) * 0.5f; const float center = (PWM_OUTPUT_TEST_MIN_PULSE_NS + PWM_OUTPUT_TEST_MAX_PULSE_NS) * 0.5f;
const float amplitude = (PWM_OUTPUT_TEST_MAX_DUTY_PCT - PWM_OUTPUT_TEST_MIN_DUTY_PCT) * 0.5f; const float amplitude = (PWM_OUTPUT_TEST_MAX_PULSE_NS - PWM_OUTPUT_TEST_MIN_PULSE_NS) * 0.5f;
const uint8_t duty = static_cast<uint8_t>(center + amplitude * sinf(phase) + 0.5f); const uint32_t pulseNs = static_cast<uint32_t>(center + amplitude * sinf(phase) + 0.5f);
if (duty == pwmOutputTestDuty) return; if (pulseNs == pwmOutputTestPulseNs) return;
ActualPwm actual = {}; ActualPwm actual = {};
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, duty, actual)) { if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, pulseNs, actual)) {
pwmOutputTestDuty = duty; pwmOutputTestPulseNs = pulseNs;
} else { } else {
Serial.printf("PWM OUTPUT TEST UPDATE FAILED: duty=%u%%\n", duty); Serial.printf("PWM OUTPUT TEST UPDATE FAILED: pulse=%luns\n", pulseNs);
delay(100); delay(100);
} }
} }

View File

@@ -2,7 +2,7 @@
#include "Core.h" #include "Core.h"
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
constexpr uint8_t PROTOCOL_VERSION = 8; constexpr uint8_t PROTOCOL_VERSION = 11;
enum class MessageType : uint8_t { enum class MessageType : uint8_t {
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT, DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
@@ -21,8 +21,9 @@ struct ProtocolPacket {
uint16_t stageCount; uint16_t stageCount;
uint16_t sequence; uint16_t sequence;
uint32_t requestedHz; uint32_t requestedHz;
uint32_t requestedPulseNs;
uint32_t actualHz; uint32_t actualHz;
uint16_t actualDutyX100; uint32_t actualPulseNs;
uint32_t testTimeMs; uint32_t testTimeMs;
uint16_t accuracyX100; uint16_t accuracyX100;
uint8_t settleCycles; uint8_t settleCycles;
@@ -31,14 +32,14 @@ struct ProtocolPacket {
uint8_t reason; uint8_t reason;
uint32_t periods; uint32_t periods;
uint32_t measuredHzX10; uint32_t measuredHzX10;
uint16_t measuredDutyX10; uint32_t measuredPulseNs;
uint32_t minPeriodTicks; uint32_t minPeriodTicks;
uint32_t maxPeriodTicks; uint32_t maxPeriodTicks;
uint16_t crc; uint16_t crc;
}; };
#pragma pack(pop) #pragma pack(pop)
static_assert(sizeof(ProtocolPacket) == 54, "Protocol layout changed"); static_assert(sizeof(ProtocolPacket) == 62, "Protocol layout changed");
uint16_t packetCrc(const ProtocolPacket &packet); uint16_t packetCrc(const ProtocolPacket &packet);
void finalizePacket(ProtocolPacket &packet); void finalizePacket(ProtocolPacket &packet);

View File

@@ -12,20 +12,20 @@ namespace {
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE; constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0; constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0;
void setIntegerDivider(uint16_t divider) { void setDivider(uint32_t dividerRaw) {
ledc_dev_t *hardware = LEDC_LL_GET_HW(); ledc_dev_t *hardware = LEDC_LL_GET_HW();
ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER, ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER,
static_cast<uint32_t>(divider) << LEDC_LL_FRACTIONAL_BITS); dividerRaw);
ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER);
} }
bool integerDividerIsSet(uint16_t expected) { bool dividerIsSet(uint32_t expectedRaw) {
uint32_t rawDivider = 0; uint32_t rawDivider = 0;
ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider); ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider);
return rawDivider == (static_cast<uint32_t>(expected) << LEDC_LL_FRACTIONAL_BITS); return rawDivider == expectedRaw;
} }
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
mcpwm_timer_handle_t mcpwmTimer = nullptr; mcpwm_timer_handle_t mcpwmTimer = nullptr;
@@ -34,6 +34,11 @@ mcpwm_cmpr_handle_t mcpwmComparator = nullptr;
mcpwm_gen_handle_t mcpwmGenerator = nullptr; mcpwm_gen_handle_t mcpwmGenerator = nullptr;
uint32_t mcpwmFrequencyHz = 0; uint32_t mcpwmFrequencyHz = 0;
constexpr mcpwm_generator_action_t PWM_ACTIVE_ACTION =
PWM_ACTIVE_LEVEL == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW;
constexpr mcpwm_generator_action_t PWM_INACTIVE_ACTION =
PWM_ACTIVE_LEVEL == HIGH ? MCPWM_GEN_ACTION_LOW : MCPWM_GEN_ACTION_HIGH;
void releaseMcpwm() { void releaseMcpwm() {
if (mcpwmGenerator) { if (mcpwmGenerator) {
mcpwm_del_generator(mcpwmGenerator); mcpwm_del_generator(mcpwmGenerator);
@@ -94,10 +99,10 @@ void PwmGenerator::begin() {
timerConfig.period_ticks / 2U) == ESP_OK; timerConfig.period_ticks / 2U) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator, ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH)) == ESP_OK; MCPWM_TIMER_EVENT_EMPTY, PWM_ACTIVE_ACTION)) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator, ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
mcpwmComparator, MCPWM_GEN_ACTION_LOW)) == ESP_OK; mcpwmComparator, PWM_INACTIVE_ACTION)) == ESP_OK;
ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK; ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
if (!ok) { if (!ok) {
releaseMcpwm(); releaseMcpwm();
@@ -109,23 +114,29 @@ void PwmGenerator::begin() {
#endif #endif
} }
bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) { bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
IntegerPwmConfig config = {}; IntegerPwmConfig config = {};
if (!chooseIntegerPwmConfig(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, dutyPct, config)) return false; if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
const uint8_t bits = config.bits; const uint8_t bits = config.bits;
const uint32_t levels = 1UL << bits; const uint32_t levels = 1UL << bits;
const uint32_t duty = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U; const uint32_t duty = config.dutyCount;
for (uint8_t attempt = 0; attempt < 2; ++attempt) { for (uint8_t attempt = 0; attempt < 2; ++attempt) {
stop(); stop();
const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL); const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL);
if (attached) { if (attached) {
// Arduino's LEDC API normally chooses an 8-bit fractional divider. // Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when
// Force the fractional byte to zero so every PWM period contains the // the configured active pulse level is LOW.
// same integer number of 40 MHz source-clock ticks. if (!ledcOutputInvert(GPIO_PWM, PWM_ACTIVE_LEVEL == LOW)) {
setIntegerDivider(config.divider); ledcDetach(GPIO_PWM);
delay(2);
continue;
}
// Apply the selected Q10.8 divider explicitly. This lets pulse width,
// rather than duty percentage, drive the hardware quantization.
setDivider(config.dividerRaw);
} }
if (attached && integerDividerIsSet(config.divider) && ledcWriteChannel(LEDC_CHANNEL, duty)) { if (attached && dividerIsSet(config.dividerRaw) && ledcWriteChannel(LEDC_CHANNEL, duty)) {
// On the first configuration after power-up the duty update is latched // On the first configuration after power-up the duty update is latched
// on a timer edge. Reading immediately can therefore return zero. // on a timer edge. Reading immediately can therefore return zero.
uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz); uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz);
@@ -133,7 +144,8 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
delayMicroseconds(settleUs); delayMicroseconds(settleUs);
const uint32_t actualHz = ledcReadFreq(GPIO_PWM); const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
if (actualHz == config.actualHz) { if (actualHz == config.actualHz) {
a = {hz, actualHz, 100.0f * duty / levels, bits}; a = {hz, actualHz, pulseNs, config.actualPulseNs,
100.0f * duty / levels, bits};
running_ = true; running_ = true;
return true; return true;
} }
@@ -144,11 +156,13 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL); pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
return false; return false;
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || dutyPct > 100U || if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs ||
MCPWM_RESOLUTION_HZ % hz) return false; MCPWM_RESOLUTION_HZ % hz) return false;
const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz; const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false; if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
uint32_t activeTicks = (static_cast<uint64_t>(periodTicks) * dutyPct + 50U) / 100U; uint32_t activeTicks = static_cast<uint32_t>(
(static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) /
1000000000ULL);
if (activeTicks == 0U) activeTicks = 1U; if (activeTicks == 0U) activeTicks = 1U;
if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U; if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U;
@@ -165,7 +179,11 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
return false; return false;
} }
a = {hz, hz, 100.0f * activeTicks / periodTicks, periodResolutionBits(periodTicks)}; const uint32_t actualPulseNs = static_cast<uint32_t>(
(static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
MCPWM_RESOLUTION_HZ);
a = {hz, hz, pulseNs, actualPulseNs, 100.0f * activeTicks / periodTicks,
periodResolutionBits(periodTicks)};
mcpwmFrequencyHz = hz; mcpwmFrequencyHz = hz;
running_ = true; running_ = true;
return true; return true;
@@ -189,8 +207,8 @@ void PwmGenerator::stop() {
} }
void PwmGenerator::active() { void PwmGenerator::active() {
// First detach/stop the PWM peripheral, then select the independently // First detach/stop the PWM peripheral, then apply the same active level
// configured active level. The active and safe levels may be equal. // that denotes the pulse during a running test.
stop(); stop();
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL); digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL);

View File

@@ -1,12 +1,19 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
struct ActualPwm { uint32_t requestedHz; uint32_t actualHz; float actualDutyPct; uint8_t bits; }; struct ActualPwm {
uint32_t requestedHz;
uint32_t actualHz;
uint32_t requestedPulseNs;
uint32_t actualPulseNs;
float actualDutyPct;
uint8_t bits;
};
class PwmGenerator { class PwmGenerator {
public: public:
void begin(); void begin();
bool start(uint32_t frequencyHz, uint8_t dutyPct, ActualPwm &actual); bool start(uint32_t frequencyHz, uint32_t pulseNs, ActualPwm &actual);
void active(); void active();
void stop(); void stop();
bool running() const { return running_; } bool running() const { return running_; }

View File

@@ -1,13 +1,13 @@
#include "Receiver.h" #include "Receiver.h"
#include "Config.h" #include "Log.h"
#include <string.h> #include <math.h>
#if !OPTICAL_USE_RMT_DMA #if !OPTICAL_USE_MCPWM_CAPTURE
#include <esp_cpu.h> #include <esp_cpu.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#endif #endif
uint32_t PulseReceiver::tickHz() const { uint32_t PulseReceiver::tickHz() const {
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_MCPWM_CAPTURE
return captureResolutionHz_; return captureResolutionHz_;
#else #else
return cpuTickHz_; return cpuTickHz_;
@@ -15,31 +15,48 @@ uint32_t PulseReceiver::tickHz() const {
} }
uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const { uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const {
#if OPTICAL_USE_RMT_DMA if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) return 0;
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) #if OPTICAL_USE_MCPWM_CAPTURE
return CAPTURE_RESOLUTION_OPTIONS_HZ[0]; return captureResolutionHz_ ? captureResolutionHz_ :
uint32_t dutyX100 = static_cast<uint32_t>(expectedDutyPct * 100.0f + 0.5f); MCPWM_CAPTURE_RESOLUTION_HZ;
if (dutyX100 < 5000U) dutyX100 = 10000U - dutyX100;
for (int i = static_cast<int>(countOf(CAPTURE_RESOLUTION_OPTIONS_HZ)) - 1; i >= 0; --i) {
const uint32_t resolution = CAPTURE_RESOLUTION_OPTIONS_HZ[i];
const uint64_t levelTicksX100 = static_cast<uint64_t>(resolution) * dutyX100;
const uint64_t limitX100 = static_cast<uint64_t>(expectedHz) * 10000ULL * RMT_MAX_LEVEL_TICKS;
if (levelTicksX100 <= limitX100) return resolution;
}
return CAPTURE_RESOLUTION_OPTIONS_HZ[0];
#else #else
(void)expectedHz; (void)expectedDutyPct;
return cpuTickHz_; return cpuTickHz_;
#endif #endif
} }
bool PulseReceiver::begin() { bool PulseReceiver::begin() {
#if OPTICAL_USE_RMT_DMA queue_ = xQueueCreate(512, sizeof(Edge));
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
return queue_ && configureRmt(CAPTURE_RESOLUTION_OPTIONS_HZ[0]);
#else
queue_ = xQueueCreate(256, sizeof(Edge));
if (!queue_) return false; if (!queue_) return false;
#if OPTICAL_USE_MCPWM_CAPTURE
// PWM generation uses MCPWM group 0. Group 1 is dedicated to input capture,
// so RX cannot exhaust or conflict with the generator's resources.
mcpwm_capture_timer_config_t timerConfig = {};
timerConfig.group_id = 1;
timerConfig.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT;
timerConfig.resolution_hz = MCPWM_CAPTURE_RESOLUTION_HZ;
if (mcpwm_new_capture_timer(&timerConfig, &captureTimer_) != ESP_OK) return false;
if (mcpwm_capture_timer_get_resolution(captureTimer_, &captureResolutionHz_) != ESP_OK ||
!captureResolutionHz_) return false;
mcpwm_capture_channel_config_t channelConfig = {};
channelConfig.gpio_num = GPIO_RX;
channelConfig.prescale = 1;
channelConfig.flags.pos_edge = true;
channelConfig.flags.neg_edge = false;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &risingChannel_) != ESP_OK)
return false;
mcpwm_capture_event_callbacks_t callbacks = {};
callbacks.on_cap = onCapture;
if (mcpwm_capture_channel_register_event_callbacks(
risingChannel_, &callbacks, this) != ESP_OK) return false;
channelConfig.flags.pos_edge = false;
channelConfig.flags.neg_edge = true;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK)
return false;
return mcpwm_capture_channel_register_event_callbacks(
fallingChannel_, &callbacks, this) == ESP_OK;
#else
pinMode(GPIO_RX, INPUT); pinMode(GPIO_RX, INPUT);
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE); attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
@@ -47,178 +64,195 @@ bool PulseReceiver::begin() {
#endif #endif
} }
#if OPTICAL_USE_RMT_DMA
bool PulseReceiver::configureRmt(uint32_t resolutionHz) {
if (channel_ && captureResolutionHz_ == resolutionHz) return true;
stop();
if (channel_) {
if (rmt_del_channel(channel_) != ESP_OK) return false;
channel_ = nullptr;
}
rmt_rx_channel_config_t cfg = {};
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz;
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
// Internally the measurement code always treats HIGH as the active phase.
cfg.flags.invert_in = RX_ACTIVE_LEVEL == LOW;
#if CONFIG_IDF_TARGET_ESP32S3
cfg.mem_block_symbols = 512;
cfg.flags.with_dma = true;
#else
// C3 has 48 RMT symbols per channel and no RMT DMA. A request for 512
// consumes all available blocks and fails with "no free rx channels".
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
#endif
if (rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false;
rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt;
if (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) {
rmt_del_channel(channel_); channel_ = nullptr; return false;
}
captureResolutionHz_ = resolutionHz;
return true;
}
#endif
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) { bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
#if OPTICAL_USE_RMT_DMA if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
const uint32_t resolutionHz = plannedTickHz(expectedHz, expectedDutyPct); expectedHz_ = expectedHz;
if (!configureRmt(resolutionHz)) return false; expectedDutyPct_ = expectedDutyPct;
#else #if !OPTICAL_USE_MCPWM_CAPTURE
(void)expectedHz; (void)expectedDutyPct; cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
if (!cpuTickHz_) return false;
#endif #endif
resetStream(); resetStream();
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_MCPWM_CAPTURE
// In partial RX mode the callback is delivered when this user buffer fills. // Progress updates keep one capture session alive. Pulse-width stages stop
// Keep chunks near 5 ms so low-frequency input is reported before NO SIGNAL. // capture only after PWM is quiet, so resetStream never races the ISR.
uint64_t symbols = (static_cast<uint64_t>(expectedHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL; if (running_) return true;
if (symbols < RMT_MIN_RECEIVE_SYMBOLS) symbols = RMT_MIN_RECEIVE_SYMBOLS; if (mcpwm_capture_timer_enable(captureTimer_) != ESP_OK) return false;
if (symbols > RMT_MAX_RECEIVE_SYMBOLS) symbols = RMT_MAX_RECEIVE_SYMBOLS; if (mcpwm_capture_channel_enable(risingChannel_) != ESP_OK) {
receiveChunkSymbols_ = static_cast<uint16_t>(symbols); mcpwm_capture_timer_disable(captureTimer_);
if (rmt_enable(channel_) != ESP_OK) return false; return false;
rmt_receive_config_t cfg = {};
cfg.signal_range_min_ns = 1000000000UL / captureResolutionHz_;
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1);
// A duration field is 15 bits. Keep the driver's end-of-signal threshold
// strictly below that hardware limit (IDF rejects larger values).
const uint64_t hardwareMaxNs = static_cast<uint64_t>(RMT_MAX_LEVEL_TICKS) * 1000000000ULL / captureResolutionHz_;
cfg.signal_range_max_ns = static_cast<uint32_t>(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs);
cfg.flags.en_partial_rx = true;
if (rmt_receive(channel_, receiveBuffer_,
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) {
rmt_disable(channel_); return false;
} }
if (mcpwm_capture_channel_enable(fallingChannel_) != ESP_OK) {
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
running_ = true;
if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) {
running_ = false;
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
#else
running_ = true;
#endif #endif
running_ = true; return true; return true;
} }
void PulseReceiver::stop() { void PulseReceiver::stop() {
#if OPTICAL_USE_RMT_DMA const bool wasRunning = running_;
if (running_) rmt_disable(channel_);
#endif
running_ = false; running_ = false;
#if OPTICAL_USE_MCPWM_CAPTURE
if (wasRunning) {
// Mask both edge interrupts before stopping the shared capture timer.
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_stop(captureTimer_);
mcpwm_capture_timer_disable(captureTimer_);
}
#else
(void)wasRunning;
#endif
} }
void PulseReceiver::resetStream() { void PulseReceiver::resetStream() {
if (queue_) xQueueReset(queue_); if (queue_) xQueueReset(queue_);
overflow_ = false; droppedItems_ = 0; haveRise_ = haveFall_ = haveRawTick_ = false; haveReorderEdge_ = false;
lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0; droppedItems_ = 0;
#if OPTICAL_USE_RMT_DMA polarityKnown_ = false;
block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0; activeStartRising_ = false;
#endif syncEdgeCount_ = 0;
waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0;
haveRawTick_ = false;
lastRawTick_ = 0;
tickEpoch_ = 0;
} }
bool PulseReceiver::consumeEdge(const Edge &e, PulsePeriod &out) { PulseReceiver::TimedEdge PulseReceiver::extendEdge(const Edge &e) {
if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL) if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL)
tickEpoch_ += 0x100000000ULL; tickEpoch_ += 0x100000000ULL;
haveRawTick_ = true; lastRawTick_ = e.tick; haveRawTick_ = true; lastRawTick_ = e.tick;
const uint64_t tick = tickEpoch_ + e.tick; return {tickEpoch_ + e.tick, e.rising != 0};
if (e.rising) {
if (!haveRise_) { rise_ = tick; haveRise_ = true; haveFall_ = false; return false; }
if (!haveFall_) { overflow_ = true; rise_ = tick; return false; }
const uint32_t period = static_cast<uint32_t>(tick - rise_);
const uint32_t active = fall_ - rise_;
out = {rise_, period, active}; rise_ = tick; haveFall_ = false;
return true;
}
// Reception can begin in the middle of a HIGH pulse. In that case the first
// observable edge is falling and there is no complete period to validate.
// Ignore only this leading partial pulse and synchronize on the next rise.
if (!haveRise_) return false;
if (haveFall_) { overflow_ = true; return false; }
fall_ = tick; haveFall_ = true; return false;
} }
bool PulseReceiver::overflowed() { bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
const bool value = overflow_; overflow_ = false; return value; const TimedEdge edge = extendEdge(rawEdge);
if (polarityKnown_) {
// Deliberately ignore edge type after synchronization. A PWM waveform is
// just alternating intervals: active, inactive, active, inactive. An
// extra or missing edge therefore becomes a concrete wrong pulse/period
// instead of an ambiguous GLITCH state.
if (waitingForActiveEnd_) {
activeEnd_ = edge.tick;
waitingForActiveEnd_ = false;
return false;
}
const uint64_t periodTicks = edge.tick - activeStart_;
const uint64_t activeTicks = activeEnd_ - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeTicks), tickHz()};
activeStart_ = edge.tick;
waitingForActiveEnd_ = true;
return true;
}
syncEdges_[syncEdgeCount_++] = edge;
if (syncEdgeCount_ < 3U) return false;
const uint64_t firstTicks = syncEdges_[1].tick - syncEdges_[0].tick;
const uint64_t secondTicks = syncEdges_[2].tick - syncEdges_[1].tick;
const double expectedTicks = static_cast<double>(tickHz()) * expectedDutyPct_ /
(100.0 * expectedHz_);
const double firstError = fabs(static_cast<double>(firstTicks) - expectedTicks);
const double secondError = fabs(static_cast<double>(secondTicks) - expectedTicks);
activeStartRising_ = firstError <= secondError ? syncEdges_[0].rising : syncEdges_[1].rising;
polarityKnown_ = true;
Log::printf("CAPTURE", "RX polarity auto: active starts on %s, first=%lluns second=%lluns",
activeStartRising_ ? "RISING" : "FALLING",
static_cast<unsigned long long>(firstTicks * 1000000000ULL / tickHz()),
static_cast<unsigned long long>(secondTicks * 1000000000ULL / tickHz()));
bool produced = false;
if (firstError <= secondError) {
activeStart_ = syncEdges_[0].tick;
activeEnd_ = syncEdges_[1].tick;
const uint64_t periodTicks = syncEdges_[2].tick - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeEnd_ - activeStart_), tickHz()};
activeStart_ = syncEdges_[2].tick;
waitingForActiveEnd_ = true;
produced = true;
} else {
activeStart_ = syncEdges_[1].tick;
activeEnd_ = syncEdges_[2].tick;
waitingForActiveEnd_ = false;
}
syncEdgeCount_ = 0;
return produced;
} }
uint32_t PulseReceiver::takeDroppedItems() { uint32_t PulseReceiver::takeDroppedItems() {
return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED); return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED);
} }
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_MCPWM_CAPTURE
bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) { bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t,
const mcpwm_capture_event_data_t *data,
void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return false;
const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS;
const Edge edge = {data->cap_value, static_cast<uint8_t>(rawRising)};
BaseType_t wake = pdFALSE; BaseType_t wake = pdFALSE;
size_t offset = 0; if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
while (offset < data->num_symbols) { __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
SymbolBlock &b = self->isrBlock_;
b.count = static_cast<uint16_t>((data->num_symbols - offset) > BLOCK_SYMBOLS ?
BLOCK_SYMBOLS : (data->num_symbols - offset));
memcpy(b.symbols, data->received_symbols + offset, b.count * sizeof(rmt_symbol_word_t));
if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, b.count, __ATOMIC_RELAXED);
offset += b.count;
}
return wake == pdTRUE; return wake == pdTRUE;
} }
bool PulseReceiver::nextRmtEdge(Edge &edge, TickType_t waitTicks) {
for (;;) {
if (blockIndex_ >= block_.count) {
if (xQueueReceive(queue_, &block_, waitTicks) != pdTRUE) return false;
blockIndex_ = 0; phase_ = 0;
waitTicks = 0;
}
const rmt_symbol_word_t &s = block_.symbols[blockIndex_];
const bool nextLevel = phase_ == 0 ? s.level0 : s.level1;
const uint32_t duration = phase_ == 0 ? s.duration0 : s.duration1;
phase_ ^= 1;
if (phase_ == 0) ++blockIndex_;
if (!duration) continue;
if (!haveLevel_) { haveLevel_ = true; level_ = nextLevel; rmtTick_ += duration; continue; }
if (nextLevel != level_) {
level_ = nextLevel; edge = {rmtTick_, static_cast<uint8_t>(nextLevel)};
rmtTick_ += duration; return true;
}
rmtTick_ += duration;
}
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) {
size_t count = 0;
Edge e;
while (count < capacity && nextRmtEdge(e, count ? 0 : waitTicks))
if (consumeEdge(e, periods[count])) ++count;
return count;
}
#else #else
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return;
bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)); bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
if (RX_ACTIVE_LEVEL == LOW) level = !level; if (RX_ACTIVE_LEVEL == LOW) level = !level;
Edge e = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)}; const Edge edge = {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) if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
if (wake) portYIELD_FROM_ISR(); if (wake) portYIELD_FROM_ISR();
} }
#endif
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) { bool PulseReceiver::nextOrderedEdge(Edge &edge, TickType_t waitTicks) {
if (!haveReorderEdge_) {
if (xQueueReceive(queue_, &reorderEdge_, waitTicks) != pdTRUE) return false;
haveReorderEdge_ = true;
}
Edge next = {};
// Keep one-event look-ahead. If both channel interrupts were pending while
// OLED/I2C ran, the MCPWM driver may dispatch them by channel number rather
// than timestamp. The signed modular comparison restores their real order.
if (xQueueReceive(queue_, &next, waitTicks) != pdTRUE) return false;
if (static_cast<int32_t>(next.tick - reorderEdge_.tick) < 0) {
edge = next;
} else {
edge = reorderEdge_;
reorderEdge_ = next;
}
return true;
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
TickType_t waitTicks) {
size_t count = 0; size_t count = 0;
Edge e; Edge edge = {};
while (count < capacity && xQueueReceive(queue_, &e, count ? 0 : waitTicks) == pdTRUE) while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) {
if (consumeEdge(e, periods[count])) ++count; if (consumeEdge(edge, periods[count])) {
periods[count].activeTickHz = tickHz();
++count;
}
}
return count; return count;
} }
#endif

View File

@@ -1,15 +1,15 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <esp_idf_version.h> #include <esp_idf_version.h>
#include <driver/gpio.h>
#include "Config.h" #include "Config.h"
#include "Core.h" #include "Core.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #if CONFIG_IDF_TARGET_ESP32S3 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
#define OPTICAL_USE_RMT_DMA 1 #define OPTICAL_USE_MCPWM_CAPTURE 1
#include <driver/rmt_rx.h> #include <driver/mcpwm_cap.h>
#else #else
#define OPTICAL_USE_RMT_DMA 0 #define OPTICAL_USE_MCPWM_CAPTURE 0
#include <driver/gpio.h>
#endif #endif
class PulseReceiver { class PulseReceiver {
@@ -19,48 +19,46 @@ class PulseReceiver {
void stop(); void stop();
void resetStream(); void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
bool overflowed();
uint32_t takeDroppedItems(); uint32_t takeDroppedItems();
uint32_t tickHz() const; uint32_t tickHz() const;
uint32_t pulseTickHz() const { return tickHz(); }
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const; uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; } uint32_t plannedPulseTickHz(uint32_t expectedHz, float expectedDutyPct) const {
bool highRateBackend() const { return plannedTickHz(expectedHz, expectedDutyPct);
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
return true;
#else
return false;
#endif
} }
uint16_t receiveChunkSymbols() const { return 1; }
bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; }
private: private:
struct Edge { uint32_t tick; uint8_t rising; }; struct Edge { uint32_t tick; uint8_t rising; };
struct TimedEdge { uint64_t tick; bool rising; };
bool consumeEdge(const Edge &edge, PulsePeriod &period); bool consumeEdge(const Edge &edge, PulsePeriod &period);
bool nextOrderedEdge(Edge &edge, TickType_t waitTicks);
#if OPTICAL_USE_RMT_DMA TimedEdge extendEdge(const Edge &edge);
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS; #if OPTICAL_USE_MCPWM_CAPTURE
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; }; static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t,
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *); const mcpwm_capture_event_data_t *, void *);
bool configureRmt(uint32_t resolutionHz); mcpwm_cap_timer_handle_t captureTimer_ = nullptr;
bool nextRmtEdge(Edge &edge, TickType_t waitTicks); mcpwm_cap_channel_handle_t risingChannel_ = nullptr;
rmt_channel_handle_t channel_ = nullptr; mcpwm_cap_channel_handle_t fallingChannel_ = nullptr;
uint32_t captureResolutionHz_ = 0; uint32_t captureResolutionHz_ = 0;
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
uint16_t receiveChunkSymbols_ = 0;
SymbolBlock isrBlock_ = {};
SymbolBlock block_ = {};
uint16_t blockIndex_ = 0;
uint8_t phase_ = 0;
bool haveLevel_ = false;
bool level_ = false;
uint32_t rmtTick_ = 0;
#else #else
static void IRAM_ATTR onGpio(void *ctx); static void IRAM_ATTR onGpio(void *ctx);
uint32_t cpuTickHz_ = 0; uint32_t cpuTickHz_ = 0;
#endif #endif
QueueHandle_t queue_ = nullptr; QueueHandle_t queue_ = nullptr;
volatile bool overflow_ = false; Edge reorderEdge_ = {};
bool haveReorderEdge_ = false;
volatile uint32_t droppedItems_ = 0; volatile uint32_t droppedItems_ = 0;
bool running_ = false; volatile bool running_ = false;
bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false; uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 50.0f;
bool polarityKnown_ = false, activeStartRising_ = false;
TimedEdge syncEdges_[3] = {};
uint8_t syncEdgeCount_ = 0;
bool waitingForActiveEnd_ = true;
uint64_t activeStart_ = 0, activeEnd_ = 0;
bool haveRawTick_ = false;
uint32_t lastRawTick_ = 0; uint32_t lastRawTick_ = 0;
uint64_t tickEpoch_ = 0, rise_ = 0, fall_ = 0; uint64_t tickEpoch_ = 0;
}; };

View File

@@ -3,20 +3,25 @@
#include "Log.h" #include "Log.h"
#include <Preferences.h> #include <Preferences.h>
namespace { constexpr uint16_t SETTINGS_VERSION = 4; constexpr char NAMESPACE[] = "opt-test"; } namespace { constexpr uint16_t SETTINGS_VERSION = 9; constexpr char NAMESPACE[] = "opt-test"; }
void SettingsStore::defaults(Settings &s) const { void SettingsStore::defaults(Settings &s) const {
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 2, 3, 0, 0}; // 2 kHz, 200 us .. 2 us, 5%, 1 s.
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 2, 3, 3, 2, 3, 0};
s.checksum = settingsChecksum(s); s.checksum = settingsChecksum(s);
} }
bool SettingsStore::valid(const Settings &s) const { bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) && return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) && s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
s.minPulseIndex < countOf(MIN_PULSE_OPTIONS_NS) &&
MAX_PULSE_OPTIONS_NS[s.maxPulseIndex] >= MIN_PULSE_OPTIONS_NS[s.minPulseIndex] &&
static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[s.maxPulseIndex]) *
PWM_FREQUENCY_OPTIONS_HZ[s.frequencyIndex] < 1000000000ULL &&
s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) &&
s.checksum == settingsChecksum(s) && s.checksum == settingsChecksum(s);
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
} }
bool SettingsStore::load(Settings &s) { bool SettingsStore::load(Settings &s) {
@@ -39,7 +44,7 @@ bool SettingsStore::save(Settings &s) {
} }
TestParams SettingsStore::params(const Settings &s) const { TestParams SettingsStore::params(const Settings &s) const {
return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex], return {PWM_FREQUENCY_OPTIONS_HZ[s.frequencyIndex],
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex], MAX_PULSE_OPTIONS_NS[s.maxPulseIndex], MIN_PULSE_OPTIONS_NS[s.minPulseIndex],
TEST_DUTY_PCT}; ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex]};
} }

File diff suppressed because one or more lines are too long