Улучшения

- улушчено отображение на OLED
- убраны настройки шага частоты и количества повторов
- сделан перебор только реализуемых частот
- увеличена точность, на 1МГц 1.25%, в остальных до 1%
- точное измерение TOTAL TIME
This commit is contained in:
2026-08-08 10:18:50 +03:00
parent a8099bf2b8
commit 21f8fe8e13
12 changed files with 293 additions and 151 deletions

View File

@@ -26,6 +26,27 @@ void formatErrorDuty(float duty, char *out, size_t size) {
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty);
else snprintf(out, size, "%.1f%%", duty);
}
void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
constexpr size_t OLED_TEXT_COLUMNS = 21;
const size_t valueLength = strlen(value);
const int labelWidth = static_cast<int>(
valueLength < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - valueLength : 1U);
snprintf(out, size, "%-*s%s", labelWidth, label, value);
}
uint32_t overallProgress(uint32_t stageIndex, uint8_t step) {
if (step > MEASUREMENT_PROGRESS_STEPS) step = MEASUREMENT_PROGRESS_STEPS;
return stageIndex * MEASUREMENT_PROGRESS_STEPS + step;
}
uint32_t overallProgressTotal(uint32_t stageCount) {
return stageCount * MEASUREMENT_PROGRESS_STEPS;
}
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
}
}
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
@@ -103,7 +124,7 @@ void App::update() {
}
if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 7U; 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) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
@@ -145,24 +166,18 @@ void App::showIdle() {
}
void App::sanitizeRange() {
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
if (END_FREQ_OPTIONS_HZ[settings_.endIndex] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) {
size_t i = 0;
while (i < countOf(END_FREQ_OPTIONS_HZ) && END_FREQ_OPTIONS_HZ[i] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) ++i;
if (i == countOf(END_FREQ_OPTIONS_HZ)) { settings_.startIndex = 0; i = countOf(END_FREQ_OPTIONS_HZ) - 1; }
settings_.endIndex = i;
}
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
}
void App::changeMenu(int d) {
sanitizeRange();
uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) {
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break;
case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break;
case 2: value = &settings_.stepIndex; count = countOf(STEP_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;
case 5: value = &settings_.repeatIndex; count = countOf(REPEAT_OPTIONS); break;
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
}
*value = static_cast<uint8_t>((*value + count + d) % count);
@@ -171,34 +186,51 @@ void App::changeMenu(int d) {
}
void App::showMenu() {
char one[22], two[22], all[12];
char one[22], value[12], total[22], all[12];
const char *label = nullptr;
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) {
case 0: snprintf(one, sizeof(one), "START FREQ"); Display::formatFrequency(params_.startHz, two, sizeof(two)); break;
case 1: snprintf(one, sizeof(one), "END FREQ"); Display::formatFrequency(params_.endHz, two, sizeof(two)); break;
case 2: snprintf(one, sizeof(one), "FREQ STEP"); Display::formatFrequency(params_.stepHz, two, sizeof(two)); break;
case 3: snprintf(one, sizeof(one), "ACCURACY"); snprintf(two, sizeof(two), "+/-%g%%", params_.accuracyPct); break;
case 4: snprintf(one, sizeof(one), "TEST TIME"); snprintf(two, sizeof(two), "%.1fs", params_.testTimeMs / 1000.0f); break;
case 5: snprintf(one, sizeof(one), "REPEATS"); snprintf(two, sizeof(two), "%ux", params_.repeats); break;
default: snprintf(one, sizeof(one), "PWM DUTY"); snprintf(two, sizeof(two), "%u%%", params_.dutyPct); break;
case 0:
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
label = "START FREQ:";
break;
case 1:
Display::formatTestFrequency(params_.endHz, value, sizeof(value));
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
label = "END FREQ:";
break;
case 2:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = "ACCURACY:";
break;
case 3:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = "TEST TIME:";
break;
default:
snprintf(value, sizeof(value), "%u%%", params_.dutyPct);
label = "PWM DUTY:";
break;
}
const size_t used = strlen(two); snprintf(two + used, sizeof(two) - used, " ALL %s", all); display_.show(one, two);
formatMenuLine(label, value, one, sizeof(one));
formatMenuLine("TOTAL TIME:", all, total, sizeof(total));
display_.show(one, total);
}
void App::startTest() {
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
if (SERIAL_MINIMAL_LOG) {
char startText[12], endText[12], stepText[12];
char startText[12], endText[12];
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText));
Log::printf("CONFIG", "mode=%s range=%s..%s step=%s accuracy=%.2f%% time=%lums repeats=%u duty=%u%% stages=%lu",
roleName(static_cast<Role>(settings_.role)), startText, endText, stepText,
params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_);
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu",
roleName(static_cast<Role>(settings_.role)), startText, endText,
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
}
printConfiguration();
const Role role = static_cast<Role>(settings_.role);
@@ -212,7 +244,7 @@ void App::startTest() {
bool App::armSlave(bool preserveDisplay) {
params_ = store_.params(settings_);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
if (!radio_.begin()) {
@@ -228,7 +260,7 @@ bool App::armSlave(bool preserveDisplay) {
}
bool App::prepareStage(bool showProgress) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
actual_ = {};
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
@@ -239,11 +271,13 @@ bool App::prepareStage(bool showProgress) {
GPIO_PWM, requestedHz_);
finish(false, FailReason::RESOLUTION); return false;
}
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct,
receiver_.tickHz(), actual_.bits);
plannedRxHz, actual_.bits);
if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, receiver_.tickHz(), params_.accuracyPct);
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false;
}
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
@@ -256,9 +290,10 @@ bool App::prepareStage(bool showProgress) {
}
bool App::startLocalMeasurement(float hz, float duty) {
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% settle=%u cycles window=%lums x%u; per-pulse logging suspended",
hz, duty, params_.accuracyPct, PWM_SETTLE_CYCLES, params_.testTimeMs, params_.repeats);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, params_.repeats, PWM_SETTLE_CYCLES);
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended",
hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES);
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED",
receiver_.receiveChunkSymbols());
return ok;
@@ -270,7 +305,7 @@ void App::stagePassed() {
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
actual_ = {};
stageStartConfirmed_ = false;
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
@@ -294,7 +329,7 @@ ProtocolPacket App::makePacket(MessageType type) const {
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz;
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
p.testTimeMs = params_.testTimeMs; p.repeats = params_.repeats;
p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
return p;
}
@@ -349,7 +384,7 @@ void App::handleRadio() {
}
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
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;
}
@@ -374,7 +409,7 @@ void App::handleRadio() {
stageIndex_ = r.packet.stage;
sequence_ = r.packet.sequence;
state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats;
params_.testTimeMs = r.packet.testTimeMs;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
stageCount_ = r.packet.stageCount;
actual_ = {};
@@ -402,11 +437,12 @@ void App::handleRadio() {
r.packet.sequence == pendingPacket_.sequence) {
if (!stageStartConfirmed_) showStageProgress();
stageStartConfirmed_ = true;
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS +
(1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
LINK_REPLY_TIMEOUT_MS + 20;
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
stageStartConfirmed_ = true;
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS;
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
LINK_REPLY_TIMEOUT_MS;
showRemoteResult(r.packet);
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence;
@@ -428,7 +464,8 @@ void App::handleRadio() {
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
showStageProgress();
state_ = AppState::SLAVE_MEASURE; deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS;
state_ = AppState::SLAVE_MEASURE;
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS;
ProtocolPacket started = makePacket(MessageType::READY);
started.sequence = r.packet.sequence; sendLinked(started);
} else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) {
@@ -475,7 +512,7 @@ void App::updateMaster() {
Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
sendLinked(pendingPacket_); ++retries_;
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
(stageStartConfirmed_ ? params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
(stageStartConfirmed_ ? stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
LINK_REPLY_TIMEOUT_MS);
}
@@ -488,10 +525,9 @@ void App::updateSlave() {
StageStats live = {};
if (measurement_.statsSnapshot(live)) {
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
progress.progressStep = measurement_.progressStep();
fillMeasuredResult(progress, live);
progress.sequence = sequence_; sendLinked(progress);
// oled.display() is synchronous. Resume capture only after the full
// framebuffer has reached the display.
showStageResult(live);
}
measurement_.continueAfterDisplay();
@@ -503,6 +539,8 @@ void App::updateSlave() {
printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats());
pendingPacket_ = makePacket(MessageType::RESULT);
pendingPacket_.progressStep = ms == MeasureState::PASS ?
MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep();
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
fillMeasuredResult(pendingPacket_, measurement_.stats());
@@ -567,7 +605,7 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
if (pass) {
const Role role = static_cast<Role>(settings_.role);
snprintf(one, sizeof(one), "%s PASS", roleName(role));
display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=REPEAT");
display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=AGAIN");
}
else if (requestedHz_) {
char frequency[12];
@@ -588,11 +626,11 @@ 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("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);
Serial.printf("Test %lu..%lu step %lu Hz, accuracy %.2f%%, %lums x%u, duty %u%%\n",
params_.startHz, params_.endHz, params_.stepHz, params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct);
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n",
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct);
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
Serial.printf("Frequencies (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, params_.stepHz, i), i + 1 == stageCount_ ? "\n" : ",");
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ",");
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
}
@@ -631,20 +669,23 @@ void App::showStageResult(const StageStats &s) {
} else {
snprintf(two, sizeof(two), "%s", failName(s.reason));
}
display_.show(one, two, stageIndex_ + 1, stageCount_);
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return;
}
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
if (!s.periods || !s.periodSum) {
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_);
display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return;
}
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty);
display_.show(one, two, stageIndex_ + 1, stageCount_);
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
}
void App::showRemoteResult(const ProtocolPacket &packet) {
@@ -674,7 +715,8 @@ void App::showRemoteResult(const ProtocolPacket &packet) {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", failName(reason));
}
display_.show(one, two, stageIndex_ + 1, stageCount_);
display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
overallProgressTotal(stageCount_));
}
void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
@@ -695,5 +737,6 @@ void App::showStageProgress() {
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_);
display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_));
}