Улучшения
- улушчено отображение на OLED - убраны настройки шага частоты и количества повторов - сделан перебор только реализуемых частот - увеличена точность, на 1МГц 1.25%, в остальных до 1% - точное измерение TOTAL TIME
This commit is contained in:
@@ -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);
|
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty);
|
||||||
else snprintf(out, size, "%.1f%%", 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_) {}
|
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
|
||||||
@@ -103,7 +124,7 @@ void App::update() {
|
|||||||
}
|
}
|
||||||
if (state_ == AppState::MENU) {
|
if (state_ == AppState::MENU) {
|
||||||
if (modeEvent == ButtonEvent::SHORT) {
|
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) {
|
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_);
|
||||||
@@ -145,24 +166,18 @@ void App::showIdle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::sanitizeRange() {
|
void App::sanitizeRange() {
|
||||||
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
|
||||||
if (END_FREQ_OPTIONS_HZ[settings_.endIndex] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) {
|
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::changeMenu(int d) {
|
void App::changeMenu(int d) {
|
||||||
|
sanitizeRange();
|
||||||
uint8_t *value = nullptr; size_t count = 0;
|
uint8_t *value = nullptr; size_t count = 0;
|
||||||
switch (menuItem_) {
|
switch (menuItem_) {
|
||||||
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break;
|
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 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break;
|
||||||
case 2: value = &settings_.stepIndex; count = countOf(STEP_OPTIONS_HZ); break;
|
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
||||||
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
||||||
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
|
||||||
case 5: value = &settings_.repeatIndex; count = countOf(REPEAT_OPTIONS); break;
|
|
||||||
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
|
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
|
||||||
}
|
}
|
||||||
*value = static_cast<uint8_t>((*value + count + d) % count);
|
*value = static_cast<uint8_t>((*value + count + d) % count);
|
||||||
@@ -171,34 +186,51 @@ void App::changeMenu(int d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::showMenu() {
|
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));
|
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
|
||||||
switch (menuItem_) {
|
switch (menuItem_) {
|
||||||
case 0: snprintf(one, sizeof(one), "START FREQ"); Display::formatFrequency(params_.startHz, two, sizeof(two)); break;
|
case 0:
|
||||||
case 1: snprintf(one, sizeof(one), "END FREQ"); Display::formatFrequency(params_.endHz, two, sizeof(two)); break;
|
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
|
||||||
case 2: snprintf(one, sizeof(one), "FREQ STEP"); Display::formatFrequency(params_.stepHz, two, sizeof(two)); break;
|
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
|
||||||
case 3: snprintf(one, sizeof(one), "ACCURACY"); snprintf(two, sizeof(two), "+/-%g%%", params_.accuracyPct); break;
|
label = "START FREQ:";
|
||||||
case 4: snprintf(one, sizeof(one), "TEST TIME"); snprintf(two, sizeof(two), "%.1fs", params_.testTimeMs / 1000.0f); break;
|
break;
|
||||||
case 5: snprintf(one, sizeof(one), "REPEATS"); snprintf(two, sizeof(two), "%ux", params_.repeats); break;
|
case 1:
|
||||||
default: snprintf(one, sizeof(one), "PWM DUTY"); snprintf(two, sizeof(two), "%u%%", params_.dutyPct); break;
|
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() {
|
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;
|
stageIndex_ = 0; requestedHz_ = 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], stepText[12];
|
char startText[12], endText[12];
|
||||||
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
||||||
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
||||||
Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText));
|
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu",
|
||||||
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,
|
||||||
roleName(static_cast<Role>(settings_.role)), startText, endText, stepText,
|
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
|
||||||
params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_);
|
|
||||||
}
|
}
|
||||||
printConfiguration();
|
printConfiguration();
|
||||||
const Role role = static_cast<Role>(settings_.role);
|
const Role role = static_cast<Role>(settings_.role);
|
||||||
@@ -212,7 +244,7 @@ void App::startTest() {
|
|||||||
|
|
||||||
bool App::armSlave(bool preserveDisplay) {
|
bool App::armSlave(bool preserveDisplay) {
|
||||||
params_ = store_.params(settings_);
|
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;
|
requestedHz_ = 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()) {
|
||||||
@@ -228,7 +260,7 @@ bool App::armSlave(bool preserveDisplay) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool App::prepareStage(bool showProgress) {
|
bool App::prepareStage(bool showProgress) {
|
||||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
|
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, 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);
|
||||||
@@ -239,11 +271,13 @@ bool App::prepareStage(bool showProgress) {
|
|||||||
GPIO_PWM, requestedHz_);
|
GPIO_PWM, requestedHz_);
|
||||||
finish(false, FailReason::RESOLUTION); return false;
|
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,
|
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct,
|
||||||
receiver_.tickHz(), actual_.bits);
|
plannedRxHz, actual_.bits);
|
||||||
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 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;
|
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 actual=%luHz duty=%.2f%% bits=%u STARTED",
|
||||||
@@ -256,9 +290,10 @@ 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%% settle=%u cycles window=%lums x%u; per-pulse logging suspended",
|
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended",
|
||||||
hz, duty, params_.accuracyPct, PWM_SETTLE_CYCLES, params_.testTimeMs, params_.repeats);
|
hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
|
||||||
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, params_.repeats, PWM_SETTLE_CYCLES);
|
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",
|
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED",
|
||||||
receiver_.receiveChunkSymbols());
|
receiver_.receiveChunkSymbols());
|
||||||
return ok;
|
return ok;
|
||||||
@@ -270,7 +305,7 @@ void App::stagePassed() {
|
|||||||
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, params_.stepHz, stageIndex_);
|
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||||
actual_ = {};
|
actual_ = {};
|
||||||
stageStartConfirmed_ = false;
|
stageStartConfirmed_ = false;
|
||||||
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
||||||
@@ -294,7 +329,7 @@ ProtocolPacket App::makePacket(MessageType type) const {
|
|||||||
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz;
|
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz;
|
||||||
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
|
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
|
||||||
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
|
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;
|
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -349,7 +384,7 @@ void App::handleRadio() {
|
|||||||
}
|
}
|
||||||
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
|
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
|
||||||
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, 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;
|
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;
|
||||||
}
|
}
|
||||||
@@ -374,7 +409,7 @@ void App::handleRadio() {
|
|||||||
stageIndex_ = r.packet.stage;
|
stageIndex_ = r.packet.stage;
|
||||||
sequence_ = r.packet.sequence;
|
sequence_ = r.packet.sequence;
|
||||||
state_ = AppState::SLAVE_WAIT_START;
|
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;
|
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
|
||||||
stageCount_ = r.packet.stageCount;
|
stageCount_ = r.packet.stageCount;
|
||||||
actual_ = {};
|
actual_ = {};
|
||||||
@@ -402,11 +437,12 @@ void App::handleRadio() {
|
|||||||
r.packet.sequence == pendingPacket_.sequence) {
|
r.packet.sequence == pendingPacket_.sequence) {
|
||||||
if (!stageStartConfirmed_) showStageProgress();
|
if (!stageStartConfirmed_) showStageProgress();
|
||||||
stageStartConfirmed_ = true;
|
stageStartConfirmed_ = true;
|
||||||
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS +
|
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
|
||||||
(1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
|
LINK_REPLY_TIMEOUT_MS + 20;
|
||||||
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
|
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
|
||||||
stageStartConfirmed_ = true;
|
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);
|
showRemoteResult(r.packet);
|
||||||
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
|
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
|
||||||
ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence;
|
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;
|
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
|
||||||
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; 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);
|
ProtocolPacket started = makePacket(MessageType::READY);
|
||||||
started.sequence = r.packet.sequence; sendLinked(started);
|
started.sequence = r.packet.sequence; sendLinked(started);
|
||||||
} else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) {
|
} 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);
|
Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
|
||||||
sendLinked(pendingPacket_); ++retries_;
|
sendLinked(pendingPacket_); ++retries_;
|
||||||
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
|
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);
|
LINK_REPLY_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,10 +525,9 @@ void App::updateSlave() {
|
|||||||
StageStats live = {};
|
StageStats live = {};
|
||||||
if (measurement_.statsSnapshot(live)) {
|
if (measurement_.statsSnapshot(live)) {
|
||||||
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
||||||
|
progress.progressStep = measurement_.progressStep();
|
||||||
fillMeasuredResult(progress, live);
|
fillMeasuredResult(progress, live);
|
||||||
progress.sequence = sequence_; sendLinked(progress);
|
progress.sequence = sequence_; sendLinked(progress);
|
||||||
// oled.display() is synchronous. Resume capture only after the full
|
|
||||||
// framebuffer has reached the display.
|
|
||||||
showStageResult(live);
|
showStageResult(live);
|
||||||
}
|
}
|
||||||
measurement_.continueAfterDisplay();
|
measurement_.continueAfterDisplay();
|
||||||
@@ -503,6 +539,8 @@ void App::updateSlave() {
|
|||||||
printStageStats(measurement_.stats(), actual_.actualHz);
|
printStageStats(measurement_.stats(), actual_.actualHz);
|
||||||
showStageResult(measurement_.stats());
|
showStageResult(measurement_.stats());
|
||||||
pendingPacket_ = makePacket(MessageType::RESULT);
|
pendingPacket_ = makePacket(MessageType::RESULT);
|
||||||
|
pendingPacket_.progressStep = ms == MeasureState::PASS ?
|
||||||
|
MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep();
|
||||||
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
|
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
|
||||||
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
|
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
|
||||||
fillMeasuredResult(pendingPacket_, measurement_.stats());
|
fillMeasuredResult(pendingPacket_, measurement_.stats());
|
||||||
@@ -567,7 +605,7 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
|||||||
if (pass) {
|
if (pass) {
|
||||||
const Role role = static_cast<Role>(settings_.role);
|
const Role role = static_cast<Role>(settings_.role);
|
||||||
snprintf(one, sizeof(one), "%s PASS", roleName(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_) {
|
else if (requestedHz_) {
|
||||||
char frequency[12];
|
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("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 step %lu Hz, accuracy %.2f%%, %lums x%u, duty %u%%\n",
|
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n",
|
||||||
params_.startHz, params_.endHz, params_.stepHz, params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct);
|
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct);
|
||||||
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||||
Serial.printf("Frequencies (%lu): ", stageCount_);
|
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");
|
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 {
|
} else {
|
||||||
snprintf(two, sizeof(two), "%s", failName(s.reason));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||||
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
||||||
if (!s.periods || !s.periodSum) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
||||||
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
||||||
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
|
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
|
||||||
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty);
|
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) {
|
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(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||||
snprintf(two, sizeof(two), "%s", failName(reason));
|
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 {
|
void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
|
||||||
@@ -695,5 +737,6 @@ void App::showStageProgress() {
|
|||||||
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||||
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
||||||
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_);
|
display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, 0),
|
||||||
|
overallProgressTotal(stageCount_));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,14 +57,16 @@ constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
|
|||||||
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
|
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
|
||||||
constexpr uint16_t PERIOD_BATCH_SIZE = 128;
|
constexpr uint16_t PERIOD_BATCH_SIZE = 128;
|
||||||
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
|
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
|
||||||
|
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
|
||||||
|
// Conservative sustained validation rate calibrated from real C3 logs.
|
||||||
|
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. At 80 MHz that limits a
|
// RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that
|
||||||
// single level to about 409 us, so even a 1 kHz signal with 50% duty cannot
|
// still fits both levels of the current PWM signal: 20, 40 or 80 MHz.
|
||||||
// be captured. 20 MHz still provides 20 ticks at 1 MHz (5% resolution),
|
constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000};
|
||||||
// while allowing level durations up to about 1.64 ms for the 1 kHz/90% case.
|
constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766;
|
||||||
constexpr uint32_t CAPTURE_RESOLUTION_HZ = 20000000;
|
|
||||||
// Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3.
|
// Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3.
|
||||||
// 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.
|
||||||
@@ -73,12 +75,21 @@ constexpr uint8_t LEDC_CHANNEL = 0;
|
|||||||
constexpr uint8_t LEDC_MAX_BITS = 14;
|
constexpr uint8_t LEDC_MAX_BITS = 14;
|
||||||
|
|
||||||
// -------------------------- Menu value arrays -----------------------------
|
// -------------------------- Menu value arrays -----------------------------
|
||||||
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000};
|
// START and END deliberately have separate, independently cycling menu lists.
|
||||||
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 200000, 500000, 750000, 1000000};
|
// Every value is exactly achievable from the 40 MHz XTAL with an integer LEDC
|
||||||
constexpr uint32_t STEP_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000, 100000};
|
// divider. The test itself walks TEST_FREQUENCIES_HZ between the selected
|
||||||
|
// endpoints, so there is no separately configurable step.
|
||||||
|
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 10000, 100000};
|
||||||
|
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 500000, 1000000};
|
||||||
|
|
||||||
|
// All achievable whole-number frequencies in the supported 1 kHz..1 MHz
|
||||||
|
// range, used for adjacent test stages rather than direct menu selection.
|
||||||
|
constexpr uint32_t TEST_FREQUENCIES_HZ[] = {
|
||||||
|
1000, 2000, 5000, 10000, 25000, 50000,
|
||||||
|
100000, 200000, 312500, 400000, 500000, 625000, 800000, 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 REPEAT_OPTIONS[] = {1, 2, 3, 5, 10};
|
|
||||||
constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90};
|
constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90};
|
||||||
|
|
||||||
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; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Core.h"
|
#include "Core.h"
|
||||||
|
#include "Config.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -30,28 +31,52 @@ uint32_t settingsChecksum(const Settings &s) {
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz) {
|
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) {
|
||||||
if (!startHz || !stepHz || endHz <= startHz) return 0;
|
if (!startHz || endHz <= startHz) return 0;
|
||||||
const uint64_t span = static_cast<uint64_t>(endHz) - startHz;
|
uint32_t count = 0;
|
||||||
return static_cast<uint32_t>(span / stepHz + 1U + ((span % stepHz) ? 1U : 0U));
|
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i)
|
||||||
|
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index) {
|
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) {
|
||||||
const uint32_t count = frequencyPointCount(startHz, endHz, stepHz);
|
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) {
|
||||||
if (!count || index >= count) return 0;
|
const uint32_t frequency = TEST_FREQUENCIES_HZ[i];
|
||||||
if (index == count - 1) return endHz;
|
if (frequency < startHz || frequency > endHz) continue;
|
||||||
const uint64_t v = static_cast<uint64_t>(startHz) + static_cast<uint64_t>(stepHz) * index;
|
if (!index--) return frequency;
|
||||||
return v < endHz ? static_cast<uint32_t>(v) : endHz;
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles) {
|
||||||
|
if (!frequencyHz || !sampleTimeMs) return 0;
|
||||||
|
const uint64_t sampleUs = static_cast<uint64_t>(sampleTimeMs) * 1000ULL;
|
||||||
|
// At high frequency the CPU needs longer than the requested sample window
|
||||||
|
// to validate every captured period. Use the measured sustained C3 rate.
|
||||||
|
const uint64_t processingUs =
|
||||||
|
(static_cast<uint64_t>(frequencyHz) * sampleTimeMs * 1000ULL +
|
||||||
|
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
|
||||||
|
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
|
||||||
|
|
||||||
|
uint64_t chunkSymbols =
|
||||||
|
(static_cast<uint64_t>(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL;
|
||||||
|
if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS;
|
||||||
|
if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS;
|
||||||
|
const uint64_t batchWaitUs =
|
||||||
|
((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS;
|
||||||
|
const uint64_t settleUs =
|
||||||
|
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
|
||||||
|
// Initial stage screen, nine intermediate screens and the final result.
|
||||||
|
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
|
||||||
|
(MEASUREMENT_PROGRESS_STEPS + 1U);
|
||||||
|
return samplingWallUs + batchWaitUs + settleUs + displayUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
|
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
|
||||||
uint64_t total = 0;
|
uint64_t total = 0;
|
||||||
const uint32_t count = frequencyPointCount(p.startHz, p.endHz, p.stepHz);
|
const uint32_t count = frequencyPointCount(p.startHz, p.endHz);
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i)
|
||||||
const uint32_t f = frequencyAt(p.startHz, p.endHz, p.stepHz, i);
|
total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles);
|
||||||
total += (1000000ULL * settleCycles + f - 1) / f;
|
|
||||||
total += static_cast<uint64_t>(p.testTimeMs) * 1000ULL * p.repeats;
|
|
||||||
}
|
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +88,10 @@ bool dutyWithin(float measured, float expected, float tolerance) {
|
|||||||
return fabsf(measured - expected) <= tolerance + 0.0001f;
|
return fabsf(measured - expected) <= tolerance + 0.0001f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float effectiveTolerancePct(float configured) {
|
||||||
|
return configured > 0.0f && configured <= 1.0001f ? 1.25f : configured;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||||
uint8_t maxBits) {
|
uint8_t maxBits) {
|
||||||
if (!frequencyHz || !sourceClockHz || !maxBits) return 0;
|
if (!frequencyHz || !sourceClockHz || !maxBits) return 0;
|
||||||
@@ -148,7 +177,8 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac
|
|||||||
// 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.
|
||||||
return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct)
|
const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
|
||||||
|
return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy)
|
||||||
? FailReason::RESOLUTION : FailReason::NONE;
|
? FailReason::RESOLUTION : FailReason::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ struct Settings {
|
|||||||
uint8_t role;
|
uint8_t role;
|
||||||
uint8_t startIndex;
|
uint8_t startIndex;
|
||||||
uint8_t endIndex;
|
uint8_t endIndex;
|
||||||
uint8_t stepIndex;
|
|
||||||
uint8_t accuracyIndex;
|
uint8_t accuracyIndex;
|
||||||
uint8_t timeIndex;
|
uint8_t timeIndex;
|
||||||
uint8_t repeatIndex;
|
|
||||||
uint8_t dutyIndex;
|
uint8_t dutyIndex;
|
||||||
uint32_t checksum;
|
uint32_t checksum;
|
||||||
};
|
};
|
||||||
@@ -28,10 +26,8 @@ struct Settings {
|
|||||||
struct TestParams {
|
struct TestParams {
|
||||||
uint32_t startHz;
|
uint32_t startHz;
|
||||||
uint32_t endHz;
|
uint32_t endHz;
|
||||||
uint32_t stepHz;
|
|
||||||
float accuracyPct;
|
float accuracyPct;
|
||||||
uint32_t testTimeMs;
|
uint32_t testTimeMs;
|
||||||
uint8_t repeats;
|
|
||||||
uint8_t dutyPct;
|
uint8_t dutyPct;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,11 +68,13 @@ struct IntegerPwmConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
uint32_t settingsChecksum(const Settings &s);
|
uint32_t settingsChecksum(const Settings &s);
|
||||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz);
|
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz);
|
||||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index);
|
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index);
|
||||||
|
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);
|
||||||
bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct);
|
bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct);
|
||||||
|
float effectiveTolerancePct(float configuredPct);
|
||||||
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
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,
|
||||||
|
|||||||
@@ -9,6 +9,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
|
||||||
// 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);
|
||||||
@@ -81,7 +82,8 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 minutes = us / 60000000ULL;
|
const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
|
||||||
if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, (us / 1000000ULL) % 60ULL);
|
const uint64_t minutes = totalSeconds / 60ULL;
|
||||||
|
if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, totalSeconds % 60ULL);
|
||||||
else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL);
|
else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,26 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
||||||
uint8_t repeats, uint8_t settleCycles) {
|
uint8_t settleCycles) {
|
||||||
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
|
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
|
||||||
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
|
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
|
||||||
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 ||
|
expectedDutyPct_ = duty;
|
||||||
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) ||
|
tolerance = effectiveTolerancePct(tolerance);
|
||||||
!receiver_.start(expectedHz_)) return false;
|
if (!expectedHz_ || !timeMs ||
|
||||||
repeats_ = repeats; settleCycles_ = settleCycles; settleLeft_ = settleCycles;
|
!receiver_.start(expectedHz_, expectedDutyPct_)) return false;
|
||||||
|
if (!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_)) {
|
||||||
|
receiver_.stop(); return false;
|
||||||
|
}
|
||||||
|
settleCycles_ = settleCycles; settleLeft_ = settleCycles;
|
||||||
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;
|
||||||
totalSteps_ = repeats * MEASUREMENT_PROGRESS_STEPS;
|
currentStep_ = 0;
|
||||||
currentStep_ = currentRepeat_ = 0;
|
stats_.reset();
|
||||||
stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_));
|
|
||||||
publishStats();
|
publishStats();
|
||||||
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
||||||
measurementStartMs_ = lastPeriodMs_ = 0;
|
measurementStartMs_ = lastPeriodMs_ = 0;
|
||||||
currentRepeat_ = 0;
|
|
||||||
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
|
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
|
||||||
if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
|
if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
|
||||||
state_ = MeasureState::SETTLING;
|
state_ = MeasureState::SETTLING;
|
||||||
@@ -45,17 +47,16 @@ void Measurement::fail(FailReason reason) {
|
|||||||
receiver_.stop(); state_ = MeasureState::FAIL;
|
receiver_.stop(); state_ = MeasureState::FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Measurement::completeStep() {
|
void Measurement::completeMeasurement() {
|
||||||
receiver_.stop();
|
receiver_.stop();
|
||||||
stats_.droppedItems += receiver_.takeDroppedItems();
|
stats_.droppedItems += receiver_.takeDroppedItems();
|
||||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
||||||
++currentStep_;
|
|
||||||
publishStats();
|
publishStats();
|
||||||
if (currentStep_ < totalSteps_) {
|
if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) {
|
||||||
state_ = MeasureState::STEP_READY;
|
state_ = MeasureState::STEP_READY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
|
if (!stats_.periods) {
|
||||||
fail(FailReason::TOO_FEW_PERIODS); return;
|
fail(FailReason::TOO_FEW_PERIODS); return;
|
||||||
}
|
}
|
||||||
state_ = MeasureState::PASS;
|
state_ = MeasureState::PASS;
|
||||||
@@ -91,16 +92,14 @@ MeasureState Measurement::processOnce() {
|
|||||||
if (!settleLeft_) {
|
if (!settleLeft_) {
|
||||||
measurementStartTick_ = period.startTick + period.periodTicks;
|
measurementStartTick_ = period.startTick + period.periodTicks;
|
||||||
deadlineTick_ = measurementStartTick_ + stepTicks_;
|
deadlineTick_ = measurementStartTick_ + stepTicks_;
|
||||||
currentRepeat_ = currentStep_ / MEASUREMENT_PROGRESS_STEPS;
|
|
||||||
measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const uint64_t endTick = period.startTick + period.periodTicks;
|
const uint64_t endTick = period.startTick + period.periodTicks;
|
||||||
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
||||||
if (endTick > deadlineTick_) { completeStep(); return state_; } // trailing incomplete period
|
if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period
|
||||||
++repeatPeriods_[currentRepeat_];
|
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_);
|
||||||
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_);
|
|
||||||
if (r != FailReason::NONE) { fail(r); return state_; }
|
if (r != FailReason::NONE) { fail(r); return state_; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,11 +112,18 @@ MeasureState Measurement::processOnce() {
|
|||||||
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
|
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
|
||||||
if (state_ == MeasureState::RUNNING && measurementStartTick_) {
|
if (state_ == MeasureState::RUNNING && measurementStartTick_) {
|
||||||
const uint32_t now = millis();
|
const uint32_t now = millis();
|
||||||
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2;
|
// RMT reports a block only after its user buffer has filled. At 1 kHz the
|
||||||
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
|
// 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 receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : 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) completeStep();
|
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
|
||||||
}
|
}
|
||||||
return state_;
|
return state_;
|
||||||
}
|
}
|
||||||
@@ -126,7 +132,7 @@ MeasureState Measurement::update() { return state_; }
|
|||||||
|
|
||||||
bool Measurement::continueAfterDisplay() {
|
bool Measurement::continueAfterDisplay() {
|
||||||
if (state_ != MeasureState::STEP_READY) return false;
|
if (state_ != MeasureState::STEP_READY) return false;
|
||||||
if (!receiver_.start(expectedHz_)) {
|
if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
|
||||||
fail(FailReason::UNSUPPORTED);
|
fail(FailReason::UNSUPPORTED);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,21 @@ class Measurement {
|
|||||||
public:
|
public:
|
||||||
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
|
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
|
||||||
bool start(float expectedHz, float expectedDuty, float tolerancePct,
|
bool start(float expectedHz, float expectedDuty, float tolerancePct,
|
||||||
uint32_t testTimeMs, uint8_t repeats, uint8_t settleCycles);
|
uint32_t testTimeMs, uint8_t settleCycles);
|
||||||
MeasureState update();
|
MeasureState update();
|
||||||
bool continueAfterDisplay();
|
bool continueAfterDisplay();
|
||||||
void abort();
|
void abort();
|
||||||
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_; }
|
||||||
|
uint8_t progressStep() const { return currentStep_; }
|
||||||
bool statsSnapshot(StageStats &out) const;
|
bool statsSnapshot(StageStats &out) const;
|
||||||
private:
|
private:
|
||||||
static void taskEntry(void *context);
|
static void taskEntry(void *context);
|
||||||
void taskLoop();
|
void taskLoop();
|
||||||
MeasureState processOnce();
|
MeasureState processOnce();
|
||||||
void fail(FailReason reason);
|
void fail(FailReason reason);
|
||||||
void completeStep();
|
void completeMeasurement();
|
||||||
void publishStats();
|
void publishStats();
|
||||||
PulseReceiver &receiver_;
|
PulseReceiver &receiver_;
|
||||||
volatile MeasureState state_ = MeasureState::IDLE;
|
volatile MeasureState state_ = MeasureState::IDLE;
|
||||||
@@ -30,11 +31,11 @@ class Measurement {
|
|||||||
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||||
PeriodLimits limits_ = {};
|
PeriodLimits limits_ = {};
|
||||||
uint32_t expectedHz_ = 0;
|
uint32_t expectedHz_ = 0;
|
||||||
uint8_t repeats_ = 0, settleCycles_ = 0, settleLeft_ = 0;
|
float expectedDutyPct_ = 0.0f;
|
||||||
uint8_t currentRepeat_ = 0, currentStep_ = 0, totalSteps_ = 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;
|
||||||
uint32_t repeatPeriods_[10] = {};
|
volatile uint8_t currentStep_ = 0;
|
||||||
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = 6;
|
constexpr uint8_t PROTOCOL_VERSION = 8;
|
||||||
|
|
||||||
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,
|
||||||
@@ -24,9 +24,9 @@ struct ProtocolPacket {
|
|||||||
uint32_t actualHz;
|
uint32_t actualHz;
|
||||||
uint16_t actualDutyX100;
|
uint16_t actualDutyX100;
|
||||||
uint32_t testTimeMs;
|
uint32_t testTimeMs;
|
||||||
uint8_t repeats;
|
|
||||||
uint16_t accuracyX100;
|
uint16_t accuracyX100;
|
||||||
uint8_t settleCycles;
|
uint8_t settleCycles;
|
||||||
|
uint8_t progressStep;
|
||||||
uint8_t passed;
|
uint8_t passed;
|
||||||
uint8_t reason;
|
uint8_t reason;
|
||||||
uint32_t periods;
|
uint32_t periods;
|
||||||
|
|||||||
@@ -8,17 +8,55 @@
|
|||||||
|
|
||||||
uint32_t PulseReceiver::tickHz() const {
|
uint32_t PulseReceiver::tickHz() const {
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
return CAPTURE_RESOLUTION_HZ;
|
return captureResolutionHz_;
|
||||||
#else
|
#else
|
||||||
return cpuTickHz_;
|
return cpuTickHz_;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const {
|
||||||
|
#if OPTICAL_USE_RMT_DMA
|
||||||
|
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f)
|
||||||
|
return CAPTURE_RESOLUTION_OPTIONS_HZ[0];
|
||||||
|
uint32_t dutyX100 = static_cast<uint32_t>(expectedDutyPct * 100.0f + 0.5f);
|
||||||
|
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
|
||||||
|
(void)expectedHz; (void)expectedDutyPct;
|
||||||
|
return cpuTickHz_;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool PulseReceiver::begin() {
|
bool PulseReceiver::begin() {
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
|
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;
|
||||||
|
pinMode(GPIO_RX, INPUT);
|
||||||
|
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||||
|
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
|
||||||
|
return cpuTickHz_ != 0;
|
||||||
|
#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 = {};
|
rmt_rx_channel_config_t cfg = {};
|
||||||
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ;
|
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz;
|
||||||
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
|
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
|
||||||
cfg.flags.invert_in = RX_SIGNAL_INVERTED;
|
cfg.flags.invert_in = RX_SIGNAL_INVERTED;
|
||||||
#if CONFIG_IDF_TARGET_ESP32S3
|
#if CONFIG_IDF_TARGET_ESP32S3
|
||||||
@@ -30,20 +68,23 @@ bool PulseReceiver::begin() {
|
|||||||
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
|
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
|
||||||
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
|
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
|
||||||
#endif
|
#endif
|
||||||
if (!queue_ || rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false;
|
if (rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false;
|
||||||
rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt;
|
rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt;
|
||||||
return rmt_rx_register_event_callbacks(channel_, &callbacks, this) == ESP_OK;
|
if (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) {
|
||||||
#else
|
rmt_del_channel(channel_); channel_ = nullptr; return false;
|
||||||
queue_ = xQueueCreate(256, sizeof(Edge));
|
}
|
||||||
if (!queue_) return false;
|
captureResolutionHz_ = resolutionHz;
|
||||||
pinMode(GPIO_RX, INPUT);
|
return true;
|
||||||
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
|
|
||||||
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
|
|
||||||
return cpuTickHz_ != 0;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool PulseReceiver::start(uint32_t expectedHz) {
|
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
|
||||||
|
#if OPTICAL_USE_RMT_DMA
|
||||||
|
const uint32_t resolutionHz = plannedTickHz(expectedHz, expectedDutyPct);
|
||||||
|
if (!configureRmt(resolutionHz)) return false;
|
||||||
|
#else
|
||||||
|
(void)expectedHz; (void)expectedDutyPct;
|
||||||
|
#endif
|
||||||
resetStream();
|
resetStream();
|
||||||
#if OPTICAL_USE_RMT_DMA
|
#if OPTICAL_USE_RMT_DMA
|
||||||
// In partial RX mode the callback is delivered when this user buffer fills.
|
// In partial RX mode the callback is delivered when this user buffer fills.
|
||||||
@@ -54,19 +95,17 @@ bool PulseReceiver::start(uint32_t expectedHz) {
|
|||||||
receiveChunkSymbols_ = static_cast<uint16_t>(symbols);
|
receiveChunkSymbols_ = static_cast<uint16_t>(symbols);
|
||||||
if (rmt_enable(channel_) != ESP_OK) return false;
|
if (rmt_enable(channel_) != ESP_OK) return false;
|
||||||
rmt_receive_config_t cfg = {};
|
rmt_receive_config_t cfg = {};
|
||||||
cfg.signal_range_min_ns = 1000000000UL / CAPTURE_RESOLUTION_HZ;
|
cfg.signal_range_min_ns = 1000000000UL / captureResolutionHz_;
|
||||||
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1);
|
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1);
|
||||||
// A duration field is 15 bits. Keep the driver's end-of-signal threshold
|
// A duration field is 15 bits. Keep the driver's end-of-signal threshold
|
||||||
// strictly below that hardware limit (IDF rejects larger values).
|
// strictly below that hardware limit (IDF rejects larger values).
|
||||||
const uint64_t hardwareMaxNs = 32766ULL * 1000000000ULL / CAPTURE_RESOLUTION_HZ;
|
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.signal_range_max_ns = static_cast<uint32_t>(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs);
|
||||||
cfg.flags.en_partial_rx = true;
|
cfg.flags.en_partial_rx = true;
|
||||||
if (rmt_receive(channel_, receiveBuffer_,
|
if (rmt_receive(channel_, receiveBuffer_,
|
||||||
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) {
|
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) {
|
||||||
rmt_disable(channel_); return false;
|
rmt_disable(channel_); return false;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
(void)expectedHz;
|
|
||||||
#endif
|
#endif
|
||||||
running_ = true; return true;
|
running_ = true; return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
class PulseReceiver {
|
class PulseReceiver {
|
||||||
public:
|
public:
|
||||||
bool begin();
|
bool begin();
|
||||||
bool start(uint32_t expectedHz);
|
bool start(uint32_t expectedHz, float expectedDutyPct);
|
||||||
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();
|
bool overflowed();
|
||||||
uint32_t takeDroppedItems();
|
uint32_t takeDroppedItems();
|
||||||
uint32_t tickHz() const;
|
uint32_t tickHz() const;
|
||||||
|
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
|
||||||
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
||||||
bool highRateBackend() const {
|
bool highRateBackend() const {
|
||||||
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
|
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
|
||||||
@@ -38,8 +39,10 @@ class PulseReceiver {
|
|||||||
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS;
|
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS;
|
||||||
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
|
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
|
||||||
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
|
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
|
||||||
|
bool configureRmt(uint32_t resolutionHz);
|
||||||
bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
|
bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
|
||||||
rmt_channel_handle_t channel_ = nullptr;
|
rmt_channel_handle_t channel_ = nullptr;
|
||||||
|
uint32_t captureResolutionHz_ = 0;
|
||||||
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
|
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
|
||||||
uint16_t receiveChunkSymbols_ = 0;
|
uint16_t receiveChunkSymbols_ = 0;
|
||||||
SymbolBlock isrBlock_ = {};
|
SymbolBlock isrBlock_ = {};
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
|
|
||||||
namespace { constexpr uint16_t SETTINGS_VERSION = 1; constexpr char NAMESPACE[] = "opt-test"; }
|
namespace { constexpr uint16_t SETTINGS_VERSION = 4; 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, 1, 2, 3, 2, 2, 0};
|
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 2, 3, 2, 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.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) &&
|
||||||
s.stepIndex < countOf(STEP_OPTIONS_HZ) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
|
s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
|
||||||
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.repeatIndex < countOf(REPEAT_OPTIONS) &&
|
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) &&
|
||||||
s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && s.checksum == settingsChecksum(s) &&
|
s.checksum == settingsChecksum(s) &&
|
||||||
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
|
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,6 @@ 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 {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex],
|
||||||
STEP_OPTIONS_HZ[s.stepIndex], ACCURACY_OPTIONS_PCT[s.accuracyIndex],
|
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex],
|
||||||
TEST_TIME_OPTIONS_MS[s.timeIndex], REPEAT_OPTIONS[s.repeatIndex],
|
|
||||||
DUTY_OPTIONS_PCT[s.dutyIndex]};
|
DUTY_OPTIONS_PCT[s.dutyIndex]};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user