Compare commits
5 Commits
862781fa6a
...
49332aa028
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49332aa028 | ||
|
|
035792aedd | ||
|
|
037bb37e62 | ||
|
|
a17e8962b4 | ||
|
|
1db89fca79 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,5 +2,6 @@
|
||||
__Previews/
|
||||
History
|
||||
Project Logs*/
|
||||
/.build/
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
#include <esp_system.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
const char *uiFailName(FailReason reason);
|
||||
|
||||
const char *appStateName(AppState state) {
|
||||
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER",
|
||||
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
|
||||
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
|
||||
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
|
||||
const uint8_t index = static_cast<uint8_t>(state);
|
||||
@@ -26,9 +30,59 @@ const char *buttonEventName(ButtonEvent event) {
|
||||
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
|
||||
}
|
||||
|
||||
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);
|
||||
uint32_t pulseFromDuty(float hz, float dutyPct) {
|
||||
return hz > 0.0f ? static_cast<uint32_t>(lroundf(dutyPct * 10000000.0f / hz)) : 0U;
|
||||
}
|
||||
|
||||
float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
|
||||
return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0);
|
||||
}
|
||||
|
||||
bool configuredTxPulseLightOn(const Settings &settings) {
|
||||
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
|
||||
? txActiveLightOn(settings) : true;
|
||||
}
|
||||
|
||||
const char *configuredLevelName(const Settings &settings) {
|
||||
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
|
||||
? lightCodeName(static_cast<LightCode>(settings.lightCode)) : "AUTO";
|
||||
}
|
||||
|
||||
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, UiText::TEST_TARGET_FORMAT, 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, UiText::FAIL_TARGET_FORMAT, target);
|
||||
}
|
||||
|
||||
void formatElapsedNs(uint64_t ns, char *out, size_t size) {
|
||||
// Compact form keeps error timing within 21 OLED columns.
|
||||
// Exact nanoseconds remain available in the Serial diagnostic.
|
||||
if (ns < 1000ULL) snprintf(out, size, "%llun", ns);
|
||||
else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0);
|
||||
else if (ns < 1000000000ULL) snprintf(out, size, "%.0fm", ns / 1000000.0);
|
||||
else snprintf(out, size, "%.2fs", ns / 1000000000.0);
|
||||
}
|
||||
|
||||
size_t utf8CharacterCount(const char *text) {
|
||||
@@ -69,24 +123,147 @@ void formatMenuLine(const char *label, const char *value, char *out, size_t size
|
||||
snprintf(out, size, "%s%*s%s", label, padding, "", 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 overallProgress(uint32_t stageIndex, uint8_t step,
|
||||
uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
|
||||
if (step > stepsPerStage) step = stepsPerStage;
|
||||
return stageIndex * stepsPerStage + step;
|
||||
}
|
||||
|
||||
uint32_t overallProgressTotal(uint32_t stageCount) {
|
||||
return stageCount * MEASUREMENT_PROGRESS_STEPS;
|
||||
uint32_t overallProgressTotal(
|
||||
uint32_t stageCount,
|
||||
uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
|
||||
return stageCount * stepsPerStage;
|
||||
}
|
||||
|
||||
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
|
||||
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
|
||||
}
|
||||
|
||||
const char *uiTestName(TestKind kind) {
|
||||
const uint8_t index = static_cast<uint8_t>(kind);
|
||||
return index < sizeof(UiText::TEST_NAMES) / sizeof(UiText::TEST_NAMES[0])
|
||||
? UiText::TEST_NAMES[index] : "?";
|
||||
}
|
||||
|
||||
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
|
||||
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);
|
||||
}
|
||||
|
||||
bool parseUnsigned(const char *text, uint32_t &value) {
|
||||
if (!text || !*text || *text == '-') return false;
|
||||
char *end = nullptr;
|
||||
const unsigned long parsed = strtoul(text, &end, 10);
|
||||
if (!end || *end) return false;
|
||||
value = static_cast<uint32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
int optionIndex(const uint32_t (&options)[N], uint32_t value) {
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
if (options[i] == value) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int accuracyOptionIndex(const char *text) {
|
||||
if (!text || !*text) return -1;
|
||||
char *end = nullptr;
|
||||
const float value = strtof(text, &end);
|
||||
if (!end || *end) return -1;
|
||||
for (size_t i = 0; i < countOf(ACCURACY_OPTIONS_PCT); ++i)
|
||||
if (fabsf(ACCURACY_OPTIONS_PCT[i] - value) < 0.001f) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void lowerAscii(char *text) {
|
||||
for (; text && *text; ++text)
|
||||
if (*text >= 'A' && *text <= 'Z') *text = static_cast<char>(*text - 'A' + 'a');
|
||||
}
|
||||
}
|
||||
|
||||
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE),
|
||||
measurement_(receiver_), driverTest_(receiver_) {}
|
||||
|
||||
void App::begin() {
|
||||
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);
|
||||
startButton_.begin(); modeButton_.begin(); pwm_.begin();
|
||||
bootCheckStartedMs_ = millis();
|
||||
@@ -103,11 +280,13 @@ void App::finishInitialization(bool factoryReset) {
|
||||
} else if (!store_.load(settings_)) {
|
||||
store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
|
||||
}
|
||||
sanitizeRange();
|
||||
params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
|
||||
initialized_ = true;
|
||||
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();
|
||||
setActivePerformance(false);
|
||||
printConfiguration();
|
||||
@@ -116,6 +295,7 @@ void App::finishInitialization(bool factoryReset) {
|
||||
}
|
||||
|
||||
void App::update() {
|
||||
serviceSerialConsole();
|
||||
serviceIdlePowerSave();
|
||||
const uint32_t now = millis();
|
||||
const ButtonEvent startEvent = startButton_.update(now);
|
||||
@@ -141,11 +321,14 @@ void App::update() {
|
||||
|
||||
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
|
||||
if (modeEvent == ButtonEvent::SHORT) {
|
||||
settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_);
|
||||
cycleRunMode(); sanitizeRange(); const bool saved = store_.save(settings_);
|
||||
params_ = store_.params(settings_);
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
||||
else showIdle();
|
||||
Log::printf("ACTION", "role changed to %s, NVS=%s", roleName(static_cast<Role>(settings_.role)), saved ? "OK" : "FAILED");
|
||||
Log::printf("ACTION", "mode changed to %s/%s, NVS=%s",
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)),
|
||||
saved ? "OK" : "FAILED");
|
||||
} else if (modeEvent == ButtonEvent::LONG) {
|
||||
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
|
||||
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
|
||||
@@ -156,7 +339,9 @@ void App::update() {
|
||||
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
|
||||
radio_.end(); havePeer_ = false;
|
||||
if (modeEvent == ButtonEvent::SHORT) {
|
||||
settings_.role = static_cast<uint8_t>(Role::SOLO); const bool saved = store_.save(settings_);
|
||||
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
const bool saved = store_.save(settings_);
|
||||
params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
|
||||
Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
|
||||
} else if (modeEvent == ButtonEvent::LONG) {
|
||||
@@ -166,7 +351,7 @@ void App::update() {
|
||||
}
|
||||
if (state_ == AppState::MENU) {
|
||||
if (modeEvent == ButtonEvent::SHORT) {
|
||||
menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
|
||||
menuItem_ = (menuItem_ + 1U) % 6U; 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_);
|
||||
@@ -179,20 +364,58 @@ void App::update() {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
if (ms == MeasureState::FAIL) {
|
||||
pwm_.stop();
|
||||
printStageStats(measurement_.stats(), actual_.actualHz);
|
||||
showStageResult(measurement_.stats());
|
||||
finish(false, measurement_.reason(), true);
|
||||
}
|
||||
else if (ms == MeasureState::PASS) {
|
||||
pwm_.stop();
|
||||
printStageStats(measurement_.stats(), actual_.actualHz);
|
||||
showStageResult(measurement_.stats());
|
||||
stagePassed();
|
||||
} else if (ms == MeasureState::STEP_READY) {
|
||||
} else if (measurement_.takeProgressUpdate()) {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) showStageResult(live);
|
||||
measurement_.continueAfterDisplay();
|
||||
}
|
||||
} else if (state_ == AppState::SOLO_DRIVER) {
|
||||
if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0)
|
||||
driverTest_.forceFail(FailReason::LOST_EDGE);
|
||||
const DriverState ds = driverTest_.update();
|
||||
if (ds == DriverState::SUBSAMPLE_DONE) {
|
||||
// Capture is already stopped. Update the OLED only in this quiet gap,
|
||||
// then restart the same PWM point and arm the next tenth of the sample.
|
||||
pwm_.stop();
|
||||
driverTest_.takeProgressUpdate();
|
||||
showDriverResult(driverTest_.stats());
|
||||
ActualPwm resumed = {};
|
||||
if (!pwm_.start(requestedHz_, requestedPulseNs_, resumed)) {
|
||||
driverTest_.forceFail(FailReason::RESOLUTION);
|
||||
} else {
|
||||
actual_ = resumed;
|
||||
if (!driverTest_.resumeSubsample()) {
|
||||
pwm_.stop();
|
||||
driverTest_.forceFail(FailReason::DATA_LOSS);
|
||||
}
|
||||
}
|
||||
} else if (ds == DriverState::FAIL) {
|
||||
pwm_.stop(); receiver_.stop();
|
||||
driverTest_.printSummary();
|
||||
driverTest_.printTrace();
|
||||
showDriverResult(driverTest_.stats());
|
||||
finish(false, driverTest_.stats().reason, true);
|
||||
} else if (ds == DriverState::PASS) {
|
||||
pwm_.stop();
|
||||
driverTest_.printSummary();
|
||||
const bool finalPoint = stageIndex_ + 1U >= stageCount_;
|
||||
showDriverResult(driverTest_.stats(), finalPoint);
|
||||
stagePassed();
|
||||
}
|
||||
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
|
||||
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
|
||||
@@ -206,14 +429,229 @@ void App::showIdle() {
|
||||
setActivePerformance(false);
|
||||
setStandbyOpticalOutput();
|
||||
lastUserActivityMs_ = millis();
|
||||
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX,
|
||||
uiRoleName(static_cast<Role>(settings_.role)));
|
||||
char one[64]; snprintf(one, sizeof(one), "%s: %s",
|
||||
uiRoleName(static_cast<Role>(settings_.role)),
|
||||
uiTestName(static_cast<TestKind>(settings_.testKind)));
|
||||
display_.show(one, UiText::START_RUN);
|
||||
}
|
||||
|
||||
void App::serviceSerialConsole() {
|
||||
while (Serial.available() > 0) {
|
||||
const int raw = Serial.read();
|
||||
if (raw < 0) break;
|
||||
const char c = static_cast<char>(raw);
|
||||
lastUserActivityMs_ = millis();
|
||||
leaveIdlePowerSave();
|
||||
|
||||
if (c == '\r') continue;
|
||||
if (c == '\n') {
|
||||
if (serialLineOverflow_) Serial.println("ERR command too long");
|
||||
else if (serialLineLength_) {
|
||||
serialLine_[serialLineLength_] = '\0';
|
||||
handleSerialCommand(serialLine_);
|
||||
}
|
||||
serialLineLength_ = 0;
|
||||
serialLineOverflow_ = false;
|
||||
continue;
|
||||
}
|
||||
if (c < ' ' || c > '~') continue;
|
||||
if (serialLineLength_ + 1U < sizeof(serialLine_))
|
||||
serialLine_[serialLineLength_++] = c;
|
||||
else serialLineOverflow_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void App::printSerialHelp() {
|
||||
Serial.println("COMMANDS (send with newline):");
|
||||
Serial.println(" help | status | start | stop | defaults");
|
||||
Serial.println(" set role solo|master|slave");
|
||||
Serial.println(" set test optical|driver");
|
||||
Serial.println(" set frequency 500|1000|2000|5000|10000|25000");
|
||||
Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000");
|
||||
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
|
||||
Serial.println(" set accuracy 1|2|5|10");
|
||||
Serial.println(" set time 100|250|500|1000|2000|5000 (ms)");
|
||||
Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
|
||||
}
|
||||
|
||||
void App::printSerialStatus() {
|
||||
if (!initialized_) {
|
||||
Serial.println("STATUS initializing");
|
||||
return;
|
||||
}
|
||||
params_ = store_.params(settings_);
|
||||
Serial.printf("STATUS state=%s role=%s test=%s frequency=%luHz max=%luns min=%luns accuracy=%.2f%% time=%lums light=%s usb=%s\n",
|
||||
appStateName(state_), roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
|
||||
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
|
||||
configuredLevelName(settings_),
|
||||
usbHostPresent() ? "connected" : "disconnected");
|
||||
}
|
||||
|
||||
bool App::serialSettingsMutable() const {
|
||||
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::FINISHED ||
|
||||
state_ == AppState::MENU || state_ == AppState::SLAVE_READY);
|
||||
}
|
||||
|
||||
void App::finishSerialSettingsChange() {
|
||||
if (state_ == AppState::SLAVE_READY) radio_.end();
|
||||
state_ = AppState::IDLE;
|
||||
sanitizeRange();
|
||||
params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
const bool saved = store_.save(settings_);
|
||||
Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
||||
else showIdle();
|
||||
printSerialStatus();
|
||||
}
|
||||
|
||||
void App::handleSerialCommand(char *line) {
|
||||
lowerAscii(line);
|
||||
char *save = nullptr;
|
||||
char *command = strtok_r(line, " \t", &save);
|
||||
char *name = strtok_r(nullptr, " \t", &save);
|
||||
char *value = strtok_r(nullptr, " \t", &save);
|
||||
char *extra = strtok_r(nullptr, " \t", &save);
|
||||
if (!command) return;
|
||||
|
||||
if ((!strcmp(command, "help") || !strcmp(command, "?")) && !name) {
|
||||
printSerialHelp();
|
||||
return;
|
||||
}
|
||||
if ((!strcmp(command, "status") || !strcmp(command, "get")) && !name) {
|
||||
printSerialStatus();
|
||||
return;
|
||||
}
|
||||
if (!strcmp(command, "start") && !name) {
|
||||
if (!initialized_) Serial.println("ERR still initializing");
|
||||
else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
|
||||
Serial.println("OK test start requested");
|
||||
startTest();
|
||||
} else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave already armed");
|
||||
else Serial.printf("ERR busy state=%s\n", appStateName(state_));
|
||||
return;
|
||||
}
|
||||
if ((!strcmp(command, "stop") || !strcmp(command, "abort")) && !name) {
|
||||
if (!initialized_) Serial.println("ERR still initializing");
|
||||
else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) Serial.println("OK already stopped");
|
||||
else if (state_ == AppState::MENU) {
|
||||
state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed");
|
||||
} else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave is armed; no test is running");
|
||||
else {
|
||||
Serial.println("OK abort requested");
|
||||
abortTest();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!strcmp(command, "defaults") && !name) {
|
||||
if (!serialSettingsMutable()) {
|
||||
Serial.printf("ERR settings locked state=%s\n", appStateName(state_));
|
||||
return;
|
||||
}
|
||||
store_.defaults(settings_);
|
||||
finishSerialSettingsChange();
|
||||
return;
|
||||
}
|
||||
if (strcmp(command, "set") || !name || !value || extra) {
|
||||
Serial.println("ERR unknown command; send 'help'");
|
||||
return;
|
||||
}
|
||||
if (!serialSettingsMutable()) {
|
||||
Serial.printf("ERR settings locked state=%s; stop the test first\n", appStateName(state_));
|
||||
return;
|
||||
}
|
||||
|
||||
bool accepted = false;
|
||||
uint32_t numeric = 0;
|
||||
if (!strcmp(name, "role")) {
|
||||
if (!strcmp(value, "solo")) { settings_.role = static_cast<uint8_t>(Role::SOLO); accepted = true; }
|
||||
else if (!strcmp(value, "master")) { settings_.role = static_cast<uint8_t>(Role::MASTER); accepted = true; }
|
||||
else if (!strcmp(value, "slave")) { settings_.role = static_cast<uint8_t>(Role::SLAVE); accepted = true; }
|
||||
} else if (!strcmp(name, "test")) {
|
||||
if (!strcmp(value, "optical")) { settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); accepted = true; }
|
||||
else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
|
||||
static_cast<Role>(settings_.role) == Role::SOLO) {
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
|
||||
}
|
||||
} else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
|
||||
const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
|
||||
if (index >= 0) { settings_.frequencyIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} else if ((!strcmp(name, "max") || !strcmp(name, "maxpulse")) && parseUnsigned(value, numeric)) {
|
||||
const int index = optionIndex(MAX_PULSE_OPTIONS_NS, numeric);
|
||||
if (index >= 0) { settings_.maxPulseIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} else if ((!strcmp(name, "min") || !strcmp(name, "minpulse")) && parseUnsigned(value, numeric)) {
|
||||
const int index = optionIndex(MIN_PULSE_OPTIONS_NS, numeric);
|
||||
if (index >= 0) { settings_.minPulseIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} else if (!strcmp(name, "accuracy")) {
|
||||
const int index = accuracyOptionIndex(value);
|
||||
if (index >= 0) { settings_.accuracyIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} else if ((!strcmp(name, "time") || !strcmp(name, "duration")) && parseUnsigned(value, numeric)) {
|
||||
const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric);
|
||||
if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; }
|
||||
} else if (!strcmp(name, "light")) {
|
||||
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
|
||||
Serial.println("ERR level setting is available only in DRIVER test");
|
||||
return;
|
||||
}
|
||||
if (!strcmp(value, "hh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HH); accepted = true; }
|
||||
else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HL); accepted = true; }
|
||||
else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LH); accepted = true; }
|
||||
else if (!strcmp(value, "ll")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LL); accepted = true; }
|
||||
}
|
||||
|
||||
if (!accepted) {
|
||||
Serial.println("ERR invalid setting or value; send 'help'");
|
||||
return;
|
||||
}
|
||||
finishSerialSettingsChange();
|
||||
}
|
||||
|
||||
void App::cycleRunMode() {
|
||||
const Role role = static_cast<Role>(settings_.role);
|
||||
const TestKind kind = static_cast<TestKind>(settings_.testKind);
|
||||
if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
|
||||
} else if (role == Role::SOLO) {
|
||||
settings_.role = static_cast<uint8_t>(Role::MASTER);
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
} else if (role == Role::MASTER) {
|
||||
settings_.role = static_cast<uint8_t>(Role::SLAVE);
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
} else {
|
||||
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
}
|
||||
}
|
||||
|
||||
void App::sanitizeRange() {
|
||||
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
|
||||
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
||||
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
|
||||
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
||||
if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER))
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
|
||||
settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
|
||||
if (settings_.role != static_cast<uint8_t>(Role::SOLO) ||
|
||||
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
|
||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||
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;
|
||||
if (settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)) {
|
||||
const uint8_t driverLastMin = lastMinPulseIndexAtMost(
|
||||
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
|
||||
const uint8_t firstDriverMin = firstMinPulseIndexAtLeast(
|
||||
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
|
||||
if (settings_.minPulseIndex < firstDriverMin)
|
||||
settings_.minPulseIndex = firstDriverMin;
|
||||
}
|
||||
}
|
||||
|
||||
void App::serviceRxPinStateLog() {
|
||||
@@ -231,17 +669,42 @@ void App::serviceRxPinStateLog() {
|
||||
|
||||
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_.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;
|
||||
if (menuItem_ == 1) {
|
||||
const uint8_t last = lastValidMaxPulseIndex(
|
||||
PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
|
||||
const uint8_t first = firstMaxPulseIndexAtLeast(
|
||||
MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
|
||||
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
|
||||
first, last, d);
|
||||
} else if (menuItem_ == 2) {
|
||||
const uint8_t last = lastMinPulseIndexAtMost(
|
||||
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
|
||||
const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
|
||||
? firstMinPulseIndexAtLeast(DRIVER_MIN_INPUT_PULSE_NS, last) : 0U;
|
||||
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;
|
||||
case 5:
|
||||
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
|
||||
showMenu();
|
||||
return;
|
||||
}
|
||||
value = &settings_.lightCode; count = 4; break;
|
||||
default: return;
|
||||
}
|
||||
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
|
||||
}
|
||||
*value = static_cast<uint8_t>((*value + count + d) % count);
|
||||
Log::printf("ACTION", "menu item=%u changed direction=%+d new-index=%u", menuItem_, d, *value);
|
||||
sanitizeRange(); params_ = store_.params(settings_); showMenu();
|
||||
sanitizeRange(); params_ = store_.params(settings_);
|
||||
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
|
||||
Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u light=%s",
|
||||
menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
|
||||
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
|
||||
configuredLevelName(settings_));
|
||||
showMenu();
|
||||
}
|
||||
|
||||
void App::showMenu() {
|
||||
@@ -250,27 +713,42 @@ void App::showMenu() {
|
||||
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
|
||||
switch (menuItem_) {
|
||||
case 0:
|
||||
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
|
||||
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
||||
label = UiText::MENU_START_FREQUENCY;
|
||||
Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
|
||||
label = UiText::MENU_FREQUENCY;
|
||||
break;
|
||||
case 1:
|
||||
Display::formatTestFrequency(params_.endHz, value, sizeof(value));
|
||||
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
||||
label = UiText::MENU_END_FREQUENCY;
|
||||
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
|
||||
label = UiText::MENU_MAX_PULSE;
|
||||
break;
|
||||
case 2:
|
||||
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
|
||||
label = UiText::MENU_MIN_PULSE;
|
||||
break;
|
||||
case 3:
|
||||
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
|
||||
label = UiText::MENU_ACCURACY;
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
|
||||
label = UiText::MENU_TEST_TIME;
|
||||
break;
|
||||
default:
|
||||
snprintf(value, sizeof(value), "%u%%", params_.dutyPct);
|
||||
label = UiText::MENU_PWM_DUTY;
|
||||
break;
|
||||
case 5: {
|
||||
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
|
||||
snprintf(value, sizeof(value), "%s", UiText::LIGHT_AUTO);
|
||||
label = UiText::MENU_LIGHT_CODE;
|
||||
formatMenuLine(label, value, one, sizeof(one));
|
||||
display_.show(one, UiText::LIGHT_AUTO_FORMAT);
|
||||
return;
|
||||
}
|
||||
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
|
||||
snprintf(value, sizeof(value), "%s", code);
|
||||
label = UiText::MENU_LIGHT_CODE;
|
||||
formatMenuLine(label, value, one, sizeof(one));
|
||||
snprintf(total, sizeof(total), UiText::LIGHT_CODE_FORMAT, code[0], code[1]);
|
||||
display_.show(one, total);
|
||||
return;
|
||||
}
|
||||
default: return;
|
||||
}
|
||||
formatMenuLine(label, value, one, sizeof(one));
|
||||
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
||||
@@ -281,24 +759,29 @@ void App::startTest() {
|
||||
leaveIdlePowerSave();
|
||||
pwm_.stop();
|
||||
setActivePerformance(true);
|
||||
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE;
|
||||
sanitizeRange();
|
||||
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
||||
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 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_);
|
||||
Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)),
|
||||
configuredLevelName(settings_), stageCount_);
|
||||
if (SERIAL_MINIMAL_LOG) {
|
||||
char startText[12], endText[12];
|
||||
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
||||
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
||||
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_);
|
||||
Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
|
||||
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
|
||||
configuredLevelName(settings_),
|
||||
stageCount_);
|
||||
}
|
||||
printConfiguration();
|
||||
const Role role = static_cast<Role>(settings_.role);
|
||||
if (role == Role::SOLO) {
|
||||
if (!prepareStage()) return;
|
||||
state_ = AppState::SOLO_MEASURE;
|
||||
state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
|
||||
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
|
||||
} else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
|
||||
else if (role == Role::MASTER) startMasterDiscovery();
|
||||
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
|
||||
@@ -309,8 +792,8 @@ bool App::armSlave(bool preserveDisplay) {
|
||||
pwm_.stop();
|
||||
lastUserActivityMs_ = millis();
|
||||
params_ = store_.params(settings_);
|
||||
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
|
||||
stageIndex_ = 0; stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
||||
requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
|
||||
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
|
||||
if (!radio_.begin()) {
|
||||
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
|
||||
@@ -327,52 +810,102 @@ bool App::armSlave(bool preserveDisplay) {
|
||||
}
|
||||
|
||||
bool App::prepareStage(bool showProgress) {
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
requestedHz_ = params_.frequencyHz;
|
||||
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
|
||||
actual_ = {};
|
||||
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
|
||||
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
|
||||
if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; }
|
||||
Log::printf("PWM", "starting GPIO=%u requested=%luHz duty=%u%%", GPIO_PWM, requestedHz_, params_.dutyPct);
|
||||
if (!pwm_.start(requestedHz_, params_.dutyPct, actual_)) {
|
||||
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz; LEDC attach/write/read failed",
|
||||
GPIO_PWM, requestedHz_);
|
||||
Log::printf("PWM", "starting GPIO=%u requested=%luHz pulse=%luns", GPIO_PWM, requestedHz_, requestedPulseNs_);
|
||||
if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) {
|
||||
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz pulse=%luns; PWM setup failed",
|
||||
GPIO_PWM, requestedHz_, requestedPulseNs_);
|
||||
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,
|
||||
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, plannedRxHz,
|
||||
effectiveTolerancePct(params_.accuracyPct));
|
||||
finish(false, 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;
|
||||
}
|
||||
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
|
||||
stageIndex_ + 1, stageCount_, requestedHz_, actual_.actualHz, actual_.actualDutyPct, actual_.bits);
|
||||
if (showProgress) showStageProgress();
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
|
||||
const bool driverMode = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
|
||||
if (!driverMode) {
|
||||
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, plannedRxHz, plannedPulseRxHz, actual_.bits,
|
||||
MEASUREMENT_AVERAGING_PERIODS);
|
||||
if (resolution != FailReason::NONE) {
|
||||
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,
|
||||
plannedPulseRxHz, effectiveTolerancePct(params_.accuracyPct));
|
||||
finish(false, resolution); return false;
|
||||
}
|
||||
} else if (!receiver_.highRateBackend()) {
|
||||
finish(false, FailReason::UNSUPPORTED); return false;
|
||||
}
|
||||
Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
|
||||
stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
|
||||
actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
|
||||
if (showProgress) showStageProgress();
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO) {
|
||||
const bool started = driverMode ? startDriverMeasurement() :
|
||||
startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct);
|
||||
if (!started) { finish(false, FailReason::UNSUPPORTED); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool App::startLocalMeasurement(float hz, float duty) {
|
||||
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; 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),
|
||||
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, PWM_SETTLE_CYCLES);
|
||||
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED",
|
||||
receiver_.receiveChunkSymbols());
|
||||
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
|
||||
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, true);
|
||||
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
|
||||
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;
|
||||
}
|
||||
|
||||
bool App::startDriverMeasurement() {
|
||||
const bool ok = driverTest_.start(actual_.actualHz, actual_.actualPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES,
|
||||
txActiveLightOn(settings_), rxActiveLightOn(settings_));
|
||||
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, actual_.actualHz);
|
||||
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
|
||||
localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
|
||||
watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
|
||||
if (!ok) Log::event("DRIVER", "response test start FAILED");
|
||||
return ok;
|
||||
}
|
||||
|
||||
void App::stagePassed() {
|
||||
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
|
||||
pwm_.stop();
|
||||
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
|
||||
// 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_) {
|
||||
const bool preserveDriverMeasurements =
|
||||
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
|
||||
finish(true, FailReason::NONE, preserveDriverMeasurements);
|
||||
return;
|
||||
}
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO) {
|
||||
if (prepareStage()) state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
|
||||
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
|
||||
}
|
||||
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_ = {};
|
||||
stageStartConfirmed_ = false;
|
||||
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
||||
@@ -382,10 +915,11 @@ void App::stagePassed() {
|
||||
|
||||
void App::startMasterDiscovery() {
|
||||
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;
|
||||
lastOpticalWakeToggleMs_ = millis();
|
||||
pwm_.active();
|
||||
pwm_.lightOn();
|
||||
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
|
||||
lastSendMs_ = millis(); retries_ = 0;
|
||||
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
|
||||
@@ -396,11 +930,11 @@ ProtocolPacket App::makePacket(MessageType type) const {
|
||||
ProtocolPacket p = {};
|
||||
p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_;
|
||||
p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_;
|
||||
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.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
|
||||
p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
|
||||
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.lightCode = settings_.lightCode;
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -460,7 +994,8 @@ void App::handleRadio() {
|
||||
opticalWakeActive_ = false;
|
||||
pwm_.stop();
|
||||
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;
|
||||
char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
|
||||
}
|
||||
@@ -486,7 +1021,10 @@ void App::handleRadio() {
|
||||
sequence_ = r.packet.sequence;
|
||||
state_ = AppState::SLAVE_WAIT_START;
|
||||
params_.testTimeMs = r.packet.testTimeMs;
|
||||
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
|
||||
params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
|
||||
if (r.packet.lightCode <= static_cast<uint8_t>(LightCode::LL))
|
||||
settings_.lightCode = r.packet.lightCode;
|
||||
requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
|
||||
stageCount_ = r.packet.stageCount;
|
||||
actual_ = {};
|
||||
ProtocolPacket ready = makePacket(MessageType::READY);
|
||||
@@ -500,8 +1038,10 @@ void App::handleRadio() {
|
||||
r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
|
||||
? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED;
|
||||
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_.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;
|
||||
}
|
||||
if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) {
|
||||
@@ -537,7 +1077,8 @@ void App::handleRadio() {
|
||||
sendLinked(pendingPacket_);
|
||||
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
|
||||
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; }
|
||||
showStageProgress();
|
||||
state_ = AppState::SLAVE_MEASURE;
|
||||
@@ -551,6 +1092,10 @@ void App::handleRadio() {
|
||||
started.sequence = r.packet.sequence; sendLinked(started);
|
||||
} else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) {
|
||||
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) {
|
||||
radio_.end(); pendingReason_ = FailReason::NONE;
|
||||
if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
|
||||
@@ -569,7 +1114,7 @@ void App::updateMaster() {
|
||||
if (state_ == AppState::MASTER_DISCOVER) {
|
||||
if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) {
|
||||
opticalWakeActive_ = !opticalWakeActive_;
|
||||
if (opticalWakeActive_) pwm_.active();
|
||||
if (opticalWakeActive_) pwm_.lightOn();
|
||||
else pwm_.stop();
|
||||
lastOpticalWakeToggleMs_ = now;
|
||||
}
|
||||
@@ -603,8 +1148,12 @@ void App::updateSlave() {
|
||||
updateHeartbeat();
|
||||
if (state_ == AppState::FINISHED) return;
|
||||
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();
|
||||
if (ms == MeasureState::STEP_READY) {
|
||||
if (measurement_.takeProgressUpdate()) {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) {
|
||||
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
||||
@@ -613,7 +1162,6 @@ void App::updateSlave() {
|
||||
progress.sequence = sequence_; sendLinked(progress);
|
||||
showStageResult(live);
|
||||
}
|
||||
measurement_.continueAfterDisplay();
|
||||
return;
|
||||
}
|
||||
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
|
||||
@@ -643,13 +1191,16 @@ void App::sendAbort(FailReason reason) {
|
||||
++sequence_;
|
||||
ProtocolPacket packet = makePacket(MessageType::ABORT);
|
||||
packet.reason = static_cast<uint8_t>(reason);
|
||||
if (!packet.actualDutyX100) packet.actualDutyX100 = params_.dutyPct * 100U;
|
||||
if (!packet.actualPulseNs) packet.actualPulseNs = requestedPulseNs_;
|
||||
sendLinked(packet);
|
||||
}
|
||||
|
||||
void App::abortTest() {
|
||||
Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM");
|
||||
sendAbort(FailReason::ABORTED); measurement_.abort(); finish(false, FailReason::ABORTED);
|
||||
sendAbort(FailReason::ABORTED);
|
||||
measurement_.abort();
|
||||
driverTest_.abort();
|
||||
finish(false, FailReason::ABORTED);
|
||||
}
|
||||
|
||||
void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
@@ -677,16 +1228,22 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
setActivePerformance(false);
|
||||
lastUserActivityMs_ = millis();
|
||||
if (slaveLinkLost) {
|
||||
char target[12], one[64];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
char target[32], one[64];
|
||||
formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
|
||||
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
|
||||
roleCorner(Role::SLAVE));
|
||||
armSlave(true);
|
||||
return;
|
||||
}
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
|
||||
// The result has already been acknowledged before a normal measurement
|
||||
// failure reaches here. Re-arm ESP-NOW immediately so a quick retry from
|
||||
// Master is not hidden behind the former two-second delay; preserve the
|
||||
// failure screen while listening.
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) {
|
||||
armSlave(true);
|
||||
return;
|
||||
}
|
||||
if (preserveDisplay) return;
|
||||
char one[64];
|
||||
if (pass) {
|
||||
@@ -695,10 +1252,9 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
|
||||
}
|
||||
else if (requestedHz_) {
|
||||
char frequency[12];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
char target[32];
|
||||
formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
|
||||
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
|
||||
roleCorner(static_cast<Role>(settings_.role)));
|
||||
} else {
|
||||
@@ -708,17 +1264,42 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
}
|
||||
|
||||
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 !usbHostPresent() && initialized_ && (state_ == AppState::IDLE ||
|
||||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
|
||||
}
|
||||
|
||||
bool App::usbHostPresent() const {
|
||||
#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT && SOC_USB_SERIAL_JTAG_SUPPORTED
|
||||
// This is driven by USB SOF packets, not by CDC traffic: an enumerated host
|
||||
// keeps the board awake even if COM is closed and no bytes are exchanged.
|
||||
// Retain the state across short SOF/driver glitches.
|
||||
const uint32_t now = millis();
|
||||
if (Serial.isPlugged()) {
|
||||
lastUsbHostSeenMs_ = now ? now : 1U;
|
||||
return true;
|
||||
}
|
||||
return lastUsbHostSeenMs_ &&
|
||||
now - lastUsbHostSeenMs_ <= USB_HOST_DISCONNECT_GRACE_MS;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void App::setStandbyOpticalOutput() {
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) pwm_.stop();
|
||||
// A gate driver must never be held enabled while the tester is idle or
|
||||
// showing a result. DRIVER is SOLO-only, so force real light OFF here.
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE ||
|
||||
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) pwm_.stop();
|
||||
else pwm_.active();
|
||||
}
|
||||
|
||||
void App::setActivePerformance(bool active) {
|
||||
const uint32_t targetMhz = active ? 160U : 80U;
|
||||
const bool driverMode = initialized_ &&
|
||||
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
|
||||
const uint32_t targetMhz = active ? (driverMode ? 240U : 160U) : 80U;
|
||||
if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
|
||||
Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
|
||||
}
|
||||
@@ -762,7 +1343,9 @@ void App::serviceIdlePowerSave() {
|
||||
idleSleepRadioStopped_ = true;
|
||||
}
|
||||
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),
|
||||
@@ -770,10 +1353,6 @@ void App::serviceIdlePowerSave() {
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE),
|
||||
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
||||
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;
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_RX),
|
||||
currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
||||
@@ -781,41 +1360,76 @@ void App::serviceIdlePowerSave() {
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
const esp_err_t result = esp_light_sleep_start();
|
||||
if (result != ESP_OK) {
|
||||
leaveIdlePowerSave(true);
|
||||
delay(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
|
||||
digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
|
||||
if (buttonWake) {
|
||||
// The wake-up press is deliberately consumed. Holding or releasing it
|
||||
// must not later turn into a SHORT, LONG, or REPEAT event.
|
||||
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");
|
||||
}
|
||||
const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||
const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
|
||||
digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
|
||||
if (buttonWake) {
|
||||
startButton_.suppressUntilRelease();
|
||||
modeButton_.suppressUntilRelease();
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (SERIAL_MINIMAL_LOG) return;
|
||||
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
|
||||
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
Serial.printf("\nOptical Channel Tester | %s | mode=%s\n", board, roleName(static_cast<Role>(settings_.role)));
|
||||
Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board,
|
||||
roleName(static_cast<Role>(settings_.role)),
|
||||
testKindName(static_cast<TestKind>(settings_.testKind)),
|
||||
configuredLevelName(settings_));
|
||||
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 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, i), i + 1 == stageCount_ ? "\n" : ",");
|
||||
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
|
||||
if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
|
||||
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
|
||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
|
||||
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs, code[0], code[1]);
|
||||
} else {
|
||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, optical polarity=AUTO\n",
|
||||
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
|
||||
params_.accuracyPct, params_.testTimeMs);
|
||||
}
|
||||
stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
||||
Serial.printf("Pulse widths descending (%lu): ", stageCount_);
|
||||
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() {
|
||||
@@ -828,28 +1442,32 @@ uint64_t App::actualNominalTotalUs() {
|
||||
void App::printStageStats(const StageStats &s, uint32_t hz) {
|
||||
if (!s.periods) return;
|
||||
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];
|
||||
Display::formatFrequency(hz, requestedText, sizeof(requestedText));
|
||||
Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
|
||||
const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
|
||||
Log::printf("RESULT", "%s %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s",
|
||||
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems,
|
||||
Log::printf("RESULT", "%s/%luns %s periods=%lu measured=%s/%luns skipped=%lu%s%s",
|
||||
requestedText, requestedPulseNs_, status, s.periods, measuredText, measuredPulseNs, s.droppedItems,
|
||||
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
|
||||
}
|
||||
|
||||
void App::showStageResult(const StageStats &s) {
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
} else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
|
||||
char duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty));
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
char pulse[12];
|
||||
Display::formatPulse(pulseFromDuty(s.badFrequency, s.badDuty), pulse,
|
||||
sizeof(pulse), true);
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
|
||||
} else {
|
||||
snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
|
||||
}
|
||||
@@ -857,49 +1475,101 @@ void App::showStageResult(const StageStats &s) {
|
||||
overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role)));
|
||||
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) {
|
||||
display_.show(one, UiText::NO_MEASUREMENT, 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);
|
||||
const uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
|
||||
static_cast<double>(s.activeSum) * 1000000000.0 /
|
||||
(static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
|
||||
formatMeasured(measuredHz, measuredPulseNs, two, sizeof(two));
|
||||
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
|
||||
overallProgressTotal(stageCount_));
|
||||
}
|
||||
|
||||
void App::showDriverResult(const DriverStats &s, bool testPassed) {
|
||||
char one[64], two[64];
|
||||
const bool haveResponse = s.responses || s.lastResponseTicks;
|
||||
const uint64_t delaySumTicks =
|
||||
s.turnOn.delaySumTicks + s.turnOff.delaySumTicks;
|
||||
const uint64_t responseSumTicks =
|
||||
s.turnOn.responseSumTicks + s.turnOff.responseSumTicks;
|
||||
const uint64_t displayedDelayTicks = s.responses ?
|
||||
delaySumTicks / s.responses : s.lastDelayTicks;
|
||||
const uint64_t displayedResponseTicks = s.responses ?
|
||||
responseSumTicks / s.responses : s.lastResponseTicks;
|
||||
const uint32_t delayNs = static_cast<uint32_t>(
|
||||
(displayedDelayTicks * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) / driverTest_.tickHz());
|
||||
const uint32_t responseNs = static_cast<uint32_t>(
|
||||
(displayedResponseTicks * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) / driverTest_.tickHz());
|
||||
char delay[12] = "---", response[12] = "---";
|
||||
if (haveResponse) {
|
||||
Display::formatPulse(delayNs, delay, sizeof(delay));
|
||||
Display::formatPulse(responseNs, response, sizeof(response));
|
||||
}
|
||||
if (testPassed) {
|
||||
snprintf(one, sizeof(one), "%s", UiText::PASS_WORD);
|
||||
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
|
||||
delay, response);
|
||||
} else if (s.reason != FailReason::NONE) {
|
||||
snprintf(one, sizeof(one), "%s", uiFailName(s.reason));
|
||||
char elapsed[12] = "---", errorPulse[12] = "---";
|
||||
if (s.errorTriggerValid) {
|
||||
const uint64_t elapsedNs =
|
||||
(static_cast<uint64_t>(s.errorTriggerTicks) * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) /
|
||||
driverTest_.tickHz();
|
||||
formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed));
|
||||
}
|
||||
if (s.errorPulseValid) {
|
||||
const uint64_t errorPulseNs =
|
||||
(static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
|
||||
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
|
||||
formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
|
||||
}
|
||||
if (s.errorPulseValid)
|
||||
snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse);
|
||||
else snprintf(two, sizeof(two), "T:%s", elapsed);
|
||||
} else {
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
|
||||
delay, response);
|
||||
}
|
||||
const bool finished = testPassed || s.reason != FailReason::NONE;
|
||||
const uint8_t progressSteps = driverTest_.progressSteps();
|
||||
display_.show(one, two,
|
||||
finished ? 0U : overallProgress(
|
||||
stageIndex_, driverTest_.progressStep(), progressSteps),
|
||||
finished ? 0U : overallProgressTotal(stageCount_, progressSteps),
|
||||
s.reason == FailReason::NONE ? nullptr : roleCorner(Role::SOLO));
|
||||
}
|
||||
|
||||
void App::showRemoteResult(const ProtocolPacket &packet) {
|
||||
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
|
||||
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
|
||||
char target[12], one[64], two[64];
|
||||
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz,
|
||||
target, sizeof(target));
|
||||
char one[64], two[64];
|
||||
formatTestTarget(packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
|
||||
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) {
|
||||
char measured[12];
|
||||
Display::formatFrequency(packet.measuredHzX10 / 10.0f, measured, sizeof(measured));
|
||||
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", measured, packet.measuredDutyX10 / 10.0f);
|
||||
formatMeasured(packet.measuredHzX10 / 10.0f, packet.measuredPulseNs, two, sizeof(two));
|
||||
} else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
|
||||
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
char measured[12];
|
||||
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured));
|
||||
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured);
|
||||
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty));
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
char frequency[12];
|
||||
Display::formatFrequency(packet.measuredHzX10 / 10.0f, frequency, sizeof(frequency));
|
||||
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
|
||||
} else if (reason == FailReason::DUTY_OUT && packet.measuredPulseNs) {
|
||||
char pulse[12];
|
||||
Display::formatPulse(packet.measuredPulseNs, pulse, sizeof(pulse), true);
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
|
||||
} else {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
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),
|
||||
overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr :
|
||||
roleCorner(static_cast<Role>(settings_.role)));
|
||||
@@ -913,16 +1583,24 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
|
||||
stats.badFrequency > 0.0f;
|
||||
const float measuredHz = badPeriod ? stats.badFrequency :
|
||||
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.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() {
|
||||
char target[12], one[64], stage[12];
|
||||
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
||||
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
||||
if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
|
||||
char one[64], two[64];
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
|
||||
"---", "---");
|
||||
display_.show(one, two, overallProgress(stageIndex_, 0),
|
||||
overallProgressTotal(stageCount_));
|
||||
return;
|
||||
}
|
||||
char one[64];
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
|
||||
overallProgressTotal(stageCount_));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#pragma once
|
||||
#include "Buttons.h"
|
||||
#include "Display.h"
|
||||
#include "DriverTest.h"
|
||||
#include "Measurement.h"
|
||||
#include "Pwm.h"
|
||||
#include "Radio.h"
|
||||
#include "SettingsStore.h"
|
||||
|
||||
enum class AppState : uint8_t {
|
||||
IDLE, MENU, SOLO_MEASURE, MASTER_DISCOVER, MASTER_WAIT_READY,
|
||||
IDLE, MENU, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY,
|
||||
MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
|
||||
SLAVE_WAIT_ACK, FINISHED
|
||||
};
|
||||
@@ -22,11 +23,13 @@ class App {
|
||||
void finishInitialization(bool factoryReset);
|
||||
void showMenu();
|
||||
void changeMenu(int direction);
|
||||
void cycleRunMode();
|
||||
void sanitizeRange();
|
||||
void startTest();
|
||||
bool armSlave(bool preserveDisplay = false);
|
||||
bool prepareStage(bool showProgress = true);
|
||||
bool startLocalMeasurement(float hz, float duty);
|
||||
bool startDriverMeasurement();
|
||||
void startMasterDiscovery();
|
||||
void handleRadio();
|
||||
void updateMaster();
|
||||
@@ -38,6 +41,7 @@ class App {
|
||||
void printConfiguration();
|
||||
void printStageStats(const StageStats &s, uint32_t hz);
|
||||
void showStageResult(const StageStats &s);
|
||||
void showDriverResult(const DriverStats &s, bool testPassed = false);
|
||||
void showRemoteResult(const ProtocolPacket &packet);
|
||||
void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const;
|
||||
void showStageProgress();
|
||||
@@ -49,8 +53,15 @@ class App {
|
||||
bool packetForCurrent(const ProtocolPacket &p) const;
|
||||
void serviceIdlePowerSave();
|
||||
void serviceRxPinStateLog();
|
||||
void serviceSerialConsole();
|
||||
void handleSerialCommand(char *line);
|
||||
void printSerialHelp();
|
||||
void printSerialStatus();
|
||||
bool serialSettingsMutable() const;
|
||||
void finishSerialSettingsChange();
|
||||
void leaveIdlePowerSave(bool wakeDisplay = true);
|
||||
bool idlePowerSaveAllowed() const;
|
||||
bool usbHostPresent() const;
|
||||
void setStandbyOpticalOutput();
|
||||
void setActivePerformance(bool active);
|
||||
|
||||
@@ -62,11 +73,12 @@ class App {
|
||||
PwmGenerator pwm_;
|
||||
PulseReceiver receiver_;
|
||||
Measurement measurement_;
|
||||
DriverTest driverTest_;
|
||||
Radio radio_;
|
||||
AppState state_ = AppState::IDLE;
|
||||
uint8_t menuItem_ = 0;
|
||||
uint32_t stageIndex_ = 0, stageCount_ = 0;
|
||||
uint32_t requestedHz_ = 0;
|
||||
uint32_t requestedHz_ = 0, requestedPulseNs_ = 0;
|
||||
ActualPwm actual_ = {};
|
||||
FailReason pendingReason_ = FailReason::NONE;
|
||||
uint32_t session_ = 0;
|
||||
@@ -74,6 +86,7 @@ class App {
|
||||
uint8_t peer_[6] = {};
|
||||
bool havePeer_ = false;
|
||||
uint32_t deadlineMs_ = 0, lastSendMs_ = 0;
|
||||
uint32_t localMeasurementDeadlineMs_ = 0;
|
||||
uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0;
|
||||
uint8_t retries_ = 0;
|
||||
ProtocolPacket pendingPacket_ = {};
|
||||
@@ -87,4 +100,8 @@ class App {
|
||||
uint32_t lastOpticalWakeToggleMs_ = 0;
|
||||
bool opticalWakeActive_ = false;
|
||||
bool rxPinStateKnown_ = false, rxPinState_ = false;
|
||||
mutable uint32_t lastUsbHostSeenMs_ = 0;
|
||||
char serialLine_[96] = {};
|
||||
uint8_t serialLineLength_ = 0;
|
||||
bool serialLineOverflow_ = false;
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
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_UPDATE_MS = 10;
|
||||
constexpr uint8_t PWM_OUTPUT_TEST_MIN_DUTY_PCT = 5;
|
||||
constexpr uint8_t PWM_OUTPUT_TEST_MAX_DUTY_PCT = 95;
|
||||
constexpr uint32_t PWM_OUTPUT_TEST_MIN_PULSE_NS = 1000;
|
||||
constexpr uint32_t PWM_OUTPUT_TEST_MAX_PULSE_NS = 10000;
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
constexpr bool TARGET_IS_C3 = true;
|
||||
@@ -66,19 +66,20 @@ constexpr uint8_t OLED_ROTATION = 0;
|
||||
constexpr uint8_t OLED_ADDRESS = 0x3C;
|
||||
constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6;
|
||||
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_LOG_TIMESTAMPS = true;
|
||||
constexpr bool SERIAL_MINIMAL_LOG = true;
|
||||
|
||||
#define BUTTON_ACTIVE_LEVEL LOW
|
||||
// Raw GPIO_RX level that means the optical receiver is active.
|
||||
#define RX_ACTIVE_LEVEL LOW
|
||||
// PWM_SAFE_LEVEL must switch the optical transmitter fully off and is used
|
||||
// during tests whenever PWM is stopped, and while the controller sleeps.
|
||||
// PWM_ACTIVE_LEVEL intentionally keeps the transmitter active while the
|
||||
// controller is awake and no test is in progress.
|
||||
#define PWM_SAFE_LEVEL HIGH
|
||||
#define PWM_ACTIVE_LEVEL LOW
|
||||
// Fixed PCB conversion between electrical GPIO levels and actual optical
|
||||
// light. User settings HH/HL/LH/LL operate only in the optical domain and
|
||||
// never change these hardware facts.
|
||||
#define TX_LIGHT_ON_GPIO_LEVEL LOW
|
||||
#define RX_LIGHT_ON_GPIO_LEVEL LOW
|
||||
#define TX_LIGHT_OFF_GPIO_LEVEL (TX_LIGHT_ON_GPIO_LEVEL == HIGH ? LOW : HIGH)
|
||||
#define PWM_SETTLE_CYCLES 5U
|
||||
|
||||
constexpr uint32_t BUTTON_DEBOUNCE_MS = 30;
|
||||
@@ -91,23 +92,30 @@ constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
|
||||
constexpr uint8_t LINK_PACKET_RETRIES = 10;
|
||||
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
|
||||
constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20;
|
||||
// During discovery Master alternates PWM_ACTIVE_LEVEL and PWM_SAFE_LEVEL to
|
||||
// wake a sleeping Slave through the optical channel.
|
||||
// During discovery Master alternates actual optical light ON and OFF to wake
|
||||
// a sleeping Slave through the optical channel.
|
||||
constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50;
|
||||
constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
|
||||
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
|
||||
constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50;
|
||||
constexpr uint8_t FINAL_ACK_RETRIES = 2;
|
||||
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;
|
||||
// Retained as the minimum statistical depth used by the hardware-resolution
|
||||
// calculation and diagnostics. PASS/FAIL is evaluated for every complete
|
||||
// pulse independently; accumulated values are used only for display.
|
||||
constexpr uint16_t MEASUREMENT_AVERAGING_PERIODS = 100;
|
||||
static_assert(MEASUREMENT_AVERAGING_PERIODS > 0,
|
||||
"Averaging window must contain at least one period");
|
||||
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
|
||||
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
|
||||
constexpr uint8_t DRIVER_SHORT_SAMPLE_PROGRESS_STEPS = 10;
|
||||
constexpr uint32_t DRIVER_PROGRESS_INTERVAL_MS = 100;
|
||||
|
||||
constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000;
|
||||
// usb_serial_jtag_is_connected() needs no open COM port or CDC traffic, but a
|
||||
// short SOF detection gap must not send the board to sleep.
|
||||
constexpr uint32_t USB_HOST_DISCONNECT_GRACE_MS = 5000;
|
||||
constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100;
|
||||
constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20;
|
||||
static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS,
|
||||
@@ -117,38 +125,58 @@ constexpr uint32_t RX_PROCESSING_PERIODS_PER_SECOND = 300000;
|
||||
|
||||
constexpr uint32_t C3_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
|
||||
// still fits both levels of the current PWM signal: 20, 40 or 80 MHz.
|
||||
constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000};
|
||||
constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766;
|
||||
// S3 MCPWM Capture uses one 32-bit 80 MHz timer for both edges. Unlike RMT,
|
||||
// its width does not constrain long LOW/HIGH intervals, so capture precision
|
||||
// stays at 12.5 ns for every selectable PWM frequency and pulse length.
|
||||
constexpr uint32_t MCPWM_CAPTURE_RESOLUTION_HZ = 80000000;
|
||||
// C3 uses the 40 MHz crystal as the LEDC clock.
|
||||
// Keep this explicit so the resolution calculation never asks LEDC for an
|
||||
// impossible frequency/resolution combination.
|
||||
constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000;
|
||||
constexpr uint8_t LEDC_CHANNEL = 0;
|
||||
constexpr uint8_t LEDC_MAX_BITS = 14;
|
||||
// S3 uses the dedicated MCPWM peripheral. A 40 MHz timer clock keeps the
|
||||
// longest 1 kHz period within the S3's 16-bit MCPWM counter and makes every
|
||||
// frequency in TEST_FREQUENCIES_HZ exact.
|
||||
constexpr uint32_t MCPWM_RESOLUTION_HZ = 40000000;
|
||||
// S3 uses the dedicated MCPWM peripheral. A 20 MHz timer clock keeps the
|
||||
// selectable 500 Hz period within the S3's 16-bit counter while retaining
|
||||
// 50 ns pulse resolution and exact periods for every menu frequency.
|
||||
constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000;
|
||||
constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535;
|
||||
|
||||
// -------------------------- Menu value arrays -----------------------------
|
||||
// START and END deliberately have separate, independently cycling menu lists.
|
||||
// Every value is exactly achievable from a 40 MHz timer clock. The test 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};
|
||||
// Concept 1SP0635 status acknowledgement, expressed in the optical domain.
|
||||
constexpr uint32_t DRIVER_MIN_INPUT_PULSE_NS = 2000;
|
||||
constexpr uint32_t DRIVER_ACK_DELAY_NS = 250;
|
||||
constexpr uint32_t DRIVER_ACK_WIDTH_NS = 700;
|
||||
constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000;
|
||||
constexpr uint32_t DRIVER_ACK_MERGE_MARGIN_NS = 250;
|
||||
// The first MCPWM TX end may belong to a pulse that was already active when
|
||||
// capture was enabled. The following period also drains capture events that
|
||||
// were pending independently in the rising/falling channels. Validation
|
||||
// therefore begins at the third TX period.
|
||||
constexpr uint8_t DRIVER_CAPTURE_SYNC_CYCLES = 2;
|
||||
// Any response this long is a fault, not a normal acknowledgement.
|
||||
constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500;
|
||||
constexpr uint32_t DRIVER_RX_STUCK_MIN_NS = 20000;
|
||||
// Retained by the generic receiver backend; the driver test itself uses the
|
||||
// stricter ACK start deadline above.
|
||||
constexpr uint32_t DRIVER_RESPONSE_TIMEOUT_NS = 10000;
|
||||
|
||||
// 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
|
||||
// -------------------------- Menu value arrays -----------------------------
|
||||
// The test uses one selected PWM frequency and walks the pulse-width list from
|
||||
// the selected maximum down to the selected minimum. Widths are stored in
|
||||
// nanoseconds so sub-microsecond pulses remain representable without floats.
|
||||
constexpr uint32_t PWM_FREQUENCY_OPTIONS_HZ[] = {
|
||||
500, 1000, 2000, 5000, 10000,
|
||||
};
|
||||
constexpr uint32_t MAX_PULSE_OPTIONS_NS[] = {
|
||||
2000, 5000, 10000, 50000, 100000, 500000
|
||||
};
|
||||
constexpr uint32_t MIN_PULSE_OPTIONS_NS[] = {
|
||||
250, 500, 1000, 2000, 5000, 10000, 50000
|
||||
};
|
||||
constexpr uint32_t TEST_PULSE_WIDTHS_NS[] = {
|
||||
50, 100, 150, 200, 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 uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000};
|
||||
constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90};
|
||||
constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000, 60000};
|
||||
|
||||
template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; }
|
||||
|
||||
@@ -17,11 +17,15 @@ constexpr const char *ROLE_NAMES[] = {
|
||||
"СОЛО", "МАСТЕР", "СЛЕЙВ"
|
||||
};
|
||||
|
||||
constexpr const char *TEST_NAMES[] = {
|
||||
"ОПТИКА", "ДРАЙВЕР"
|
||||
};
|
||||
|
||||
constexpr const char *FAIL_NAMES[] = {
|
||||
"НЕТ ОШИБКИ",
|
||||
"НЕТ СИГНАЛА",
|
||||
"ПЕРИОД ВНЕ ДОПУСКА",
|
||||
"ЗАПОЛН. ВНЕ ДОПУСКА",
|
||||
"ИМПУЛЬС ВНЕ ДОПУСКА",
|
||||
"ЛИШНИЙ ФРОНТ",
|
||||
"ИМПУЛЬСНАЯ ПОМЕХА",
|
||||
"ПРОПУЩЕН ФРОНТ",
|
||||
@@ -29,17 +33,25 @@ constexpr const char *FAIL_NAMES[] = {
|
||||
"СВЯЗЬ ПОТЕРЯНА",
|
||||
"РЕЖИМ НЕ ПОДДЕРЖИВ.",
|
||||
"НЕ ХВАТАЕТ ТОЧНОСТИ",
|
||||
"ТЕСТ ОСТАНОВЛЕН"
|
||||
"ТЕСТ ОСТАНОВЛЕН",
|
||||
"НЕТ ОТВЕТА ACK",
|
||||
"ТАЙМИНГ ACK",
|
||||
"АВАРИЯ ДРАЙВЕРА",
|
||||
"ОТВЕТЫ ACK СЛИЛИСЬ"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
|
||||
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
|
||||
|
||||
constexpr const char *MENU_START_FREQUENCY = "ЧАСТОТА ОТ:";
|
||||
constexpr const char *MENU_END_FREQUENCY = "ЧАСТОТА ДО:";
|
||||
constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
|
||||
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
|
||||
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
|
||||
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
|
||||
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
|
||||
constexpr const char *MENU_PWM_DUTY = "ЗАПОЛНЕНИЕ:";
|
||||
constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:";
|
||||
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
|
||||
constexpr const char *LIGHT_AUTO = "АВТО";
|
||||
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: АВТО";
|
||||
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Гц";
|
||||
|
||||
@@ -48,18 +60,22 @@ constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
|
||||
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
|
||||
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
|
||||
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА";
|
||||
constexpr const char *HOLD_START_STOP = "УДЕРЖ. START ДЛЯ СТОП";
|
||||
constexpr const char *HOLD_START_STOP = "УДЕРЖ. ПУСК ДЛЯ СТОП";
|
||||
constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
|
||||
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
|
||||
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
|
||||
constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
|
||||
|
||||
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
|
||||
constexpr const char *FAIL_FORMAT = "СБОЙ %s %.0f%%";
|
||||
constexpr const char *TEST_FORMAT = "Тест:%-6s %2.0f%% %5s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "ОШИБКА ЧАСТОТЫ %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "ОШИБКА ЗАПОЛН. %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
|
||||
constexpr const char *FAIL_FORMAT = "СБОЙ %s";
|
||||
constexpr const char *TEST_FORMAT = "%s, %s";
|
||||
constexpr const char *TEST_TARGET_FORMAT = "ТЕСТ: %s";
|
||||
constexpr const char *FAIL_TARGET_FORMAT = "СБОЙ: %s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "ЧАСТОТА: %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "ИМПУЛЬС: %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
|
||||
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
|
||||
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
|
||||
|
||||
#elif UI_LANGUAGE == UI_LANGUAGE_EN
|
||||
|
||||
@@ -67,11 +83,15 @@ constexpr const char *ROLE_NAMES[] = {
|
||||
"SOLO", "MASTER", "SLAVE"
|
||||
};
|
||||
|
||||
constexpr const char *TEST_NAMES[] = {
|
||||
"OPTICAL", "DRIVER"
|
||||
};
|
||||
|
||||
constexpr const char *FAIL_NAMES[] = {
|
||||
"NONE",
|
||||
"NO SIGNAL",
|
||||
"PERIOD OUT",
|
||||
"DUTY OUT",
|
||||
"PULSE OUT",
|
||||
"EXTRA EDGE",
|
||||
"GLITCH",
|
||||
"LOST EDGE",
|
||||
@@ -79,17 +99,25 @@ constexpr const char *FAIL_NAMES[] = {
|
||||
"LINK LOST",
|
||||
"UNSUPPORTED",
|
||||
"RESOLUTION",
|
||||
"ABORTED"
|
||||
"ABORTED",
|
||||
"ACK MISSING",
|
||||
"ACK TIMING",
|
||||
"DRIVER FAULT",
|
||||
"ACK MERGED"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "MODE: ";
|
||||
constexpr const char *START_RUN = "READY TO START";
|
||||
|
||||
constexpr const char *MENU_START_FREQUENCY = "START FREQ:";
|
||||
constexpr const char *MENU_END_FREQUENCY = "END FREQ:";
|
||||
constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
|
||||
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_TEST_TIME = "TEST TIME:";
|
||||
constexpr const char *MENU_PWM_DUTY = "PWM DUTY:";
|
||||
constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:";
|
||||
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
|
||||
constexpr const char *LIGHT_AUTO = "AUTO";
|
||||
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: AUTO";
|
||||
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Hz";
|
||||
|
||||
@@ -105,11 +133,15 @@ constexpr const char *START_AGAIN = "READY TO START";
|
||||
constexpr const char *TEST_FAILED = "TEST FAILED";
|
||||
|
||||
constexpr const char *PASS_WORD = "TEST PASS";
|
||||
constexpr const char *FAIL_FORMAT = "FAIL %s %.0f%%";
|
||||
constexpr const char *TEST_FORMAT = "Test:%-6s %2.0f%% %5s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "PERIOD OUT %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "DUTY OUT %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
|
||||
constexpr const char *FAIL_FORMAT = "FAIL %s";
|
||||
constexpr const char *TEST_FORMAT = "%s, %s";
|
||||
constexpr const char *TEST_TARGET_FORMAT = "TEST: %s";
|
||||
constexpr const char *FAIL_TARGET_FORMAT = "FAIL AT %s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
|
||||
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
|
||||
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
|
||||
|
||||
#else
|
||||
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"
|
||||
|
||||
@@ -9,10 +9,23 @@ const char *roleName(Role r) {
|
||||
return i < 3 ? names[i] : "?";
|
||||
}
|
||||
|
||||
const char *testKindName(TestKind kind) {
|
||||
static const char *names[] = {"OPTICAL", "DRIVER"};
|
||||
const uint8_t i = static_cast<uint8_t>(kind);
|
||||
return i < 2 ? names[i] : "?";
|
||||
}
|
||||
|
||||
const char *lightCodeName(LightCode code) {
|
||||
static const char *names[] = {"HH", "HL", "LH", "LL"};
|
||||
const uint8_t i = static_cast<uint8_t>(code);
|
||||
return i < 4 ? names[i] : "??";
|
||||
}
|
||||
|
||||
const char *failName(FailReason r) {
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT",
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
|
||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
|
||||
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
|
||||
"UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING",
|
||||
"DRIVER FAULT", "ACK MERGED"};
|
||||
const uint8_t i = static_cast<uint8_t>(r);
|
||||
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
|
||||
}
|
||||
@@ -31,19 +44,27 @@ uint32_t settingsChecksum(const Settings &s) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) {
|
||||
if (!startHz || endHz <= startHz) return 0;
|
||||
bool txActiveLightOn(const Settings &s) {
|
||||
return static_cast<uint8_t>(s.lightCode) < static_cast<uint8_t>(LightCode::LH);
|
||||
}
|
||||
|
||||
bool rxActiveLightOn(const Settings &s) {
|
||||
return (static_cast<uint8_t>(s.lightCode) & 1U) == 0U;
|
||||
}
|
||||
|
||||
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs) {
|
||||
if (!minPulseNs || maxPulseNs < minPulseNs) return 0;
|
||||
uint32_t count = 0;
|
||||
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i)
|
||||
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count;
|
||||
for (size_t i = 0; i < countOf(TEST_PULSE_WIDTHS_NS); ++i)
|
||||
if (TEST_PULSE_WIDTHS_NS[i] >= minPulseNs && TEST_PULSE_WIDTHS_NS[i] <= maxPulseNs) ++count;
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) {
|
||||
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) {
|
||||
const uint32_t frequency = TEST_FREQUENCIES_HZ[i];
|
||||
if (frequency < startHz || frequency > endHz) continue;
|
||||
if (!index--) return frequency;
|
||||
uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index) {
|
||||
for (size_t i = countOf(TEST_PULSE_WIDTHS_NS); i > 0; --i) {
|
||||
const uint32_t pulseNs = TEST_PULSE_WIDTHS_NS[i - 1U];
|
||||
if (pulseNs < minPulseNs || pulseNs > maxPulseNs) continue;
|
||||
if (!index--) return pulseNs;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -58,26 +79,17 @@ uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t se
|
||||
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
|
||||
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
|
||||
|
||||
uint64_t chunkSymbols =
|
||||
(static_cast<uint64_t>(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL;
|
||||
if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS;
|
||||
if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS;
|
||||
const uint64_t batchWaitUs =
|
||||
((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS;
|
||||
const uint64_t settleUs =
|
||||
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
|
||||
// Initial stage screen, nine intermediate screens and the final result.
|
||||
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
|
||||
(MEASUREMENT_PROGRESS_STEPS + 1U);
|
||||
return samplingWallUs + batchWaitUs + settleUs + displayUs;
|
||||
return samplingWallUs + settleUs + displayUs;
|
||||
}
|
||||
|
||||
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
|
||||
uint64_t total = 0;
|
||||
const uint32_t count = frequencyPointCount(p.startHz, p.endHz);
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles);
|
||||
return total;
|
||||
return static_cast<uint64_t>(pulseWidthPointCount(p.maxPulseNs, p.minPulseNs)) *
|
||||
nominalStageUs(p.frequencyHz, p.testTimeMs, settleCycles);
|
||||
}
|
||||
|
||||
bool periodWithin(float measured, float expected, float tolerance) {
|
||||
@@ -89,7 +101,7 @@ bool dutyWithin(float measured, float expected, float tolerance) {
|
||||
}
|
||||
|
||||
float effectiveTolerancePct(float configured) {
|
||||
return configured > 0.0f && configured <= 1.0001f ? 1.25f : configured;
|
||||
return configured;
|
||||
}
|
||||
|
||||
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
@@ -116,49 +128,54 @@ uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
uint8_t maxBits, uint8_t dutyPct,
|
||||
IntegerPwmConfig &config) {
|
||||
if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false;
|
||||
bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
|
||||
uint32_t sourceClockHz, uint8_t maxBits,
|
||||
IntegerPwmConfig &config) {
|
||||
if (!requestedHz || !requestedPulseNs || !sourceClockHz || !maxBits) return false;
|
||||
|
||||
constexpr uint32_t FRACTION_SCALE = 256U;
|
||||
constexpr uint32_t MAX_DIVIDER_RAW = 1024U * FRACTION_SCALE - 1U;
|
||||
bool found = false;
|
||||
uint32_t bestErrorHz = 0;
|
||||
uint32_t bestDutyError = 0;
|
||||
uint32_t bestLevels = 1;
|
||||
uint32_t bestFrequencyError = UINT32_MAX;
|
||||
uint32_t bestPulseError = UINT32_MAX;
|
||||
|
||||
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
|
||||
const uint32_t levels = 1UL << bits;
|
||||
for (uint32_t divider = 1; divider <= 1023U; ++divider) {
|
||||
const uint32_t denominator = levels * divider;
|
||||
// A fixed integer divider gives identical PWM periods. Requiring an
|
||||
// exact division also guarantees that the physical frequency is a
|
||||
// whole number of hertz rather than a rounded value.
|
||||
if (sourceClockHz % denominator) continue;
|
||||
const uint32_t actualHz = sourceClockHz / denominator;
|
||||
const uint32_t errorHz = actualHz > requestedHz
|
||||
? actualHz - requestedHz : requestedHz - actualHz;
|
||||
const uint32_t dutyCount = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U;
|
||||
const uint32_t representedDuty = dutyCount * 100U;
|
||||
const uint32_t requestedDuty = levels * dutyPct;
|
||||
const uint32_t dutyError = representedDuty > requestedDuty
|
||||
? representedDuty - requestedDuty : requestedDuty - representedDuty;
|
||||
const uint64_t dividerNumerator = static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE;
|
||||
const uint64_t dividerDenominator = static_cast<uint64_t>(requestedHz) * levels;
|
||||
const uint32_t dividerFloor = static_cast<uint32_t>(dividerNumerator / dividerDenominator);
|
||||
const uint32_t candidates[] = {dividerFloor, dividerFloor + 1U};
|
||||
for (uint32_t dividerRaw : candidates) {
|
||||
if (dividerRaw < FRACTION_SCALE || dividerRaw > MAX_DIVIDER_RAW) continue;
|
||||
const uint64_t frequencyDenominator = static_cast<uint64_t>(levels) * dividerRaw;
|
||||
const uint32_t actualHz = static_cast<uint32_t>(
|
||||
(dividerNumerator + frequencyDenominator / 2U) / frequencyDenominator);
|
||||
if (!actualHz) continue;
|
||||
|
||||
const bool frequencyBetter = !found || errorHz < bestErrorHz;
|
||||
const bool frequencyEqual = found && errorHz == bestErrorHz;
|
||||
const bool dutyBetter = frequencyEqual &&
|
||||
static_cast<uint64_t>(dutyError) * bestLevels <
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
const bool dutyEqual = frequencyEqual &&
|
||||
static_cast<uint64_t>(dutyError) * bestLevels ==
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue;
|
||||
const uint64_t dutyNumerator = static_cast<uint64_t>(requestedPulseNs) *
|
||||
sourceClockHz * FRACTION_SCALE;
|
||||
const uint64_t dutyDenominator = static_cast<uint64_t>(dividerRaw) * 1000000000ULL;
|
||||
uint32_t dutyCount = static_cast<uint32_t>((dutyNumerator + dutyDenominator / 2U) /
|
||||
dutyDenominator);
|
||||
if (!dutyCount) dutyCount = 1U;
|
||||
if (dutyCount >= levels) dutyCount = levels - 1U;
|
||||
if (!dutyCount) continue;
|
||||
|
||||
config.actualHz = actualHz;
|
||||
config.divider = static_cast<uint16_t>(divider);
|
||||
config.bits = bits;
|
||||
bestErrorHz = errorHz;
|
||||
bestDutyError = dutyError;
|
||||
bestLevels = levels;
|
||||
const uint32_t actualPulseNs = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(dutyCount) * dividerRaw * 1000000000ULL +
|
||||
static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE / 2U) /
|
||||
(static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE));
|
||||
const uint32_t frequencyError = actualHz > requestedHz ? actualHz - requestedHz : requestedHz - actualHz;
|
||||
const uint32_t pulseError = actualPulseNs > requestedPulseNs
|
||||
? actualPulseNs - requestedPulseNs : requestedPulseNs - actualPulseNs;
|
||||
if (found && (frequencyError > bestFrequencyError ||
|
||||
(frequencyError == bestFrequencyError && pulseError > bestPulseError) ||
|
||||
(frequencyError == bestFrequencyError && pulseError == bestPulseError && bits <= config.bits)))
|
||||
continue;
|
||||
|
||||
config = {actualHz, dividerRaw, dutyCount, actualPulseNs, bits};
|
||||
bestFrequencyError = frequencyError;
|
||||
bestPulseError = pulseError;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@@ -166,37 +183,70 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
}
|
||||
|
||||
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
|
||||
uint32_t captureHz, uint8_t pwmBits) {
|
||||
if (!frequencyHz || !captureHz || !pwmBits) return FailReason::RESOLUTION;
|
||||
const float periodTicks = static_cast<float>(captureHz) / frequencyHz;
|
||||
const float activeTicks = periodTicks * dutyPct / 100.0f;
|
||||
const float inactiveTicks = periodTicks - activeTicks;
|
||||
if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION;
|
||||
uint32_t periodCaptureHz, uint32_t pulseCaptureHz,
|
||||
uint8_t pwmBits,
|
||||
uint16_t averagingPeriods) {
|
||||
if (!frequencyHz || !periodCaptureHz || !pulseCaptureHz || !pwmBits || !averagingPeriods)
|
||||
return FailReason::RESOLUTION;
|
||||
const float periodTicks = static_cast<float>(periodCaptureHz) / frequencyHz;
|
||||
const float activeTicks = static_cast<float>(pulseCaptureHz) * dutyPct /
|
||||
(100.0f * frequencyHz);
|
||||
if (periodTicks < 4.0f || activeTicks < 2.0f) return FailReason::RESOLUTION;
|
||||
const float timerPeriodError = 100.0f / periodTicks;
|
||||
const float timerDutyError = 100.0f / periodTicks;
|
||||
const float timerPulseError = 100.0f / activeTicks;
|
||||
// Measurement uses the duty actually programmed into LEDC. A coarse PWM
|
||||
// step is not itself an error when the requested value (e.g. 50%) is exactly
|
||||
// representable; only the selected value's actual quantization matters.
|
||||
const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
|
||||
return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy)
|
||||
return (timerPeriodError > effectiveAccuracy || timerPulseError > effectiveAccuracy)
|
||||
? FailReason::RESOLUTION : FailReason::NONE;
|
||||
}
|
||||
|
||||
FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz,
|
||||
float expectedDuty, float tolerance, uint8_t repeat,
|
||||
StageStats &s) {
|
||||
if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE;
|
||||
const uint32_t pulseTickHz = p.activeTickHz ? p.activeTickHz : tickHz;
|
||||
if (!p.periodTicks || !p.activeTicks || !tickHz || !pulseTickHz || expectedHz <= 0.0f)
|
||||
return FailReason::EXTRA_EDGE;
|
||||
const float hz = static_cast<float>(tickHz) / p.periodTicks;
|
||||
const float duty = 100.0f * p.activeTicks / p.periodTicks;
|
||||
const float duty = static_cast<float>(100.0 * p.activeTicks * tickHz /
|
||||
(static_cast<double>(pulseTickHz) * p.periodTicks));
|
||||
++s.periods;
|
||||
s.periodSum += p.periodTicks; s.activeSum += p.activeTicks;
|
||||
if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks;
|
||||
if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks;
|
||||
if (p.activeTicks < s.minActive) s.minActive = p.activeTicks;
|
||||
if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks;
|
||||
FailReason reason = FailReason::NONE;
|
||||
if (!periodWithin(hz, expectedHz, tolerance)) reason = FailReason::PERIOD_OUT;
|
||||
else if (!dutyWithin(duty, expectedDuty, tolerance)) reason = FailReason::DUTY_OUT;
|
||||
// Validate every complete period independently. A single capture tick is the
|
||||
// unavoidable endpoint uncertainty, so only that one tick may be corrected
|
||||
// toward the expected value. It cannot hide a larger isolated distortion.
|
||||
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
|
||||
if (!frequencyOk) {
|
||||
uint32_t correctedPeriod = p.periodTicks;
|
||||
if (hz > expectedHz) ++correctedPeriod;
|
||||
else if (correctedPeriod > 1U) --correctedPeriod;
|
||||
frequencyOk = periodWithin(static_cast<float>(tickHz) / correctedPeriod,
|
||||
expectedHz, tolerance);
|
||||
}
|
||||
|
||||
const double expectedPulseTicks = static_cast<double>(pulseTickHz) * expectedDuty /
|
||||
(100.0 * expectedHz);
|
||||
bool pulseOk = expectedPulseTicks > 0.0 &&
|
||||
fabs(static_cast<double>(p.activeTicks) - expectedPulseTicks) * 100.0 /
|
||||
expectedPulseTicks <= tolerance + 0.0001;
|
||||
if (!pulseOk) {
|
||||
uint32_t correctedActive = p.activeTicks;
|
||||
if (correctedActive > expectedPulseTicks) {
|
||||
if (correctedActive) --correctedActive;
|
||||
} else {
|
||||
++correctedActive;
|
||||
}
|
||||
pulseOk = fabs(static_cast<double>(correctedActive) - expectedPulseTicks) *
|
||||
100.0 / expectedPulseTicks <= tolerance + 0.0001;
|
||||
}
|
||||
|
||||
const FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
|
||||
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
|
||||
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
|
||||
s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat;
|
||||
s.badFrequency = hz; s.badDuty = duty;
|
||||
@@ -256,3 +306,64 @@ FailReason evaluatePeriodFast(const PulsePeriod &p, uint32_t tickHz,
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
|
||||
FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
|
||||
uint32_t periodCount, uint32_t tickHz,
|
||||
float expectedHz, float expectedDuty,
|
||||
float tolerance,
|
||||
uint32_t minPeriod, uint32_t maxPeriod,
|
||||
uint8_t repeat,
|
||||
StageStats &s) {
|
||||
if (!periodSum || !periodCount || activeSum >= periodSum || !tickHz)
|
||||
return FailReason::EXTRA_EDGE;
|
||||
const float hz = static_cast<float>(
|
||||
static_cast<double>(tickHz) * periodCount / periodSum);
|
||||
const float duty = static_cast<float>(
|
||||
100.0 * static_cast<double>(activeSum) / periodSum);
|
||||
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
|
||||
if (!frequencyOk && maxPeriod == minPeriod + 1U) {
|
||||
// At a tolerance boundary, alternating adjacent capture counts prove that the
|
||||
// result is quantization-limited. Accept only when a one-tick correction
|
||||
// toward the expected value returns the averaged frequency into tolerance.
|
||||
// Consecutive periods telescope into one first-to-last edge interval, so
|
||||
// the whole window has a one-tick endpoint uncertainty, not one tick per
|
||||
// period.
|
||||
uint64_t correctedPeriodSum = periodSum;
|
||||
if (hz > expectedHz) ++correctedPeriodSum;
|
||||
else if (periodSum > 1U) --correctedPeriodSum;
|
||||
const float correctedHz = static_cast<float>(
|
||||
static_cast<double>(tickHz) * periodCount / correctedPeriodSum);
|
||||
frequencyOk = periodWithin(correctedHz, expectedHz, tolerance);
|
||||
}
|
||||
|
||||
const double expectedPulseTicks = static_cast<double>(tickHz) * expectedDuty /
|
||||
(100.0 * expectedHz);
|
||||
const double measuredPulseTicks = static_cast<double>(activeSum) / periodCount;
|
||||
bool pulseOk = expectedPulseTicks > 0.0 &&
|
||||
fabs(measuredPulseTicks - expectedPulseTicks) * 100.0 / expectedPulseTicks <= tolerance + 0.0001;
|
||||
if (!pulseOk) {
|
||||
// Unlike full periods, active intervals do not telescope: every pulse is
|
||||
// bounded by a different rising/falling edge pair. With slowly drifting
|
||||
// asynchronous clocks an entire short window can therefore quantize to
|
||||
// the same adjacent count (e.g. 41/80 for a true 50% duty). Apply one tick
|
||||
// per active interval even when minActive == maxActive.
|
||||
const bool dutyHigh = measuredPulseTicks > expectedPulseTicks;
|
||||
const uint64_t correctedActive = dutyHigh
|
||||
? (activeSum > periodCount ? activeSum - periodCount : 0U)
|
||||
: activeSum + periodCount;
|
||||
const double correctedPulseTicks = static_cast<double>(correctedActive) / periodCount;
|
||||
pulseOk = fabs(correctedPulseTicks - expectedPulseTicks) * 100.0 /
|
||||
expectedPulseTicks <= tolerance + 0.0001;
|
||||
}
|
||||
|
||||
FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
|
||||
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
|
||||
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
|
||||
s.reason = reason;
|
||||
s.firstBadPeriod = s.periods >= periodCount ? s.periods - periodCount + 1U : 1U;
|
||||
s.firstBadRepeat = repeat;
|
||||
s.badFrequency = hz;
|
||||
s.badDuty = duty;
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
|
||||
@@ -4,37 +4,46 @@
|
||||
#include <stddef.h>
|
||||
|
||||
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
||||
enum class TestKind : uint8_t { OPTICAL, DRIVER };
|
||||
enum class LightCode : uint8_t { HH, HL, LH, LL };
|
||||
enum class FailReason : uint8_t {
|
||||
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
|
||||
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
|
||||
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
|
||||
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED
|
||||
};
|
||||
|
||||
const char *roleName(Role role);
|
||||
const char *testKindName(TestKind kind);
|
||||
const char *lightCodeName(LightCode code);
|
||||
const char *failName(FailReason reason);
|
||||
|
||||
struct Settings {
|
||||
uint16_t version;
|
||||
uint8_t role;
|
||||
uint8_t startIndex;
|
||||
uint8_t endIndex;
|
||||
uint8_t testKind;
|
||||
uint8_t lightCode;
|
||||
uint8_t frequencyIndex;
|
||||
uint8_t maxPulseIndex;
|
||||
uint8_t minPulseIndex;
|
||||
uint8_t accuracyIndex;
|
||||
uint8_t timeIndex;
|
||||
uint8_t dutyIndex;
|
||||
uint16_t reserved;
|
||||
uint32_t checksum;
|
||||
};
|
||||
|
||||
struct TestParams {
|
||||
uint32_t startHz;
|
||||
uint32_t endHz;
|
||||
uint32_t frequencyHz;
|
||||
uint32_t maxPulseNs;
|
||||
uint32_t minPulseNs;
|
||||
float accuracyPct;
|
||||
uint32_t testTimeMs;
|
||||
uint8_t dutyPct;
|
||||
};
|
||||
|
||||
struct PulsePeriod {
|
||||
uint64_t startTick;
|
||||
uint32_t periodTicks;
|
||||
uint32_t activeTicks;
|
||||
uint32_t activeTickHz;
|
||||
};
|
||||
|
||||
struct StageStats {
|
||||
@@ -63,13 +72,17 @@ struct PeriodLimits {
|
||||
|
||||
struct IntegerPwmConfig {
|
||||
uint32_t actualHz;
|
||||
uint16_t divider;
|
||||
uint32_t dividerRaw;
|
||||
uint32_t dutyCount;
|
||||
uint32_t actualPulseNs;
|
||||
uint8_t bits;
|
||||
};
|
||||
|
||||
uint32_t settingsChecksum(const Settings &s);
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz);
|
||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index);
|
||||
bool txActiveLightOn(const Settings &s);
|
||||
bool rxActiveLightOn(const Settings &s);
|
||||
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs);
|
||||
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 nominalTotalUs(const TestParams &p, uint32_t settleCycles);
|
||||
bool periodWithin(float measuredHz, float expectedHz, float tolerancePct);
|
||||
@@ -79,11 +92,13 @@ uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
uint8_t maxBits);
|
||||
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
uint8_t maxBits, uint8_t dutyPct);
|
||||
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
uint8_t maxBits, uint8_t dutyPct,
|
||||
bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
|
||||
uint32_t sourceClockHz, uint8_t maxBits,
|
||||
IntegerPwmConfig &config);
|
||||
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);
|
||||
FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz,
|
||||
float expectedDuty, float tolerancePct, uint8_t repeat,
|
||||
StageStats &stats);
|
||||
@@ -92,3 +107,10 @@ bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerancePc
|
||||
FailReason evaluatePeriodFast(const PulsePeriod &period, uint32_t tickHz,
|
||||
const PeriodLimits &limits, uint8_t repeat,
|
||||
StageStats &stats);
|
||||
FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
|
||||
uint32_t periodCount, uint32_t tickHz,
|
||||
float expectedHz, float expectedDuty,
|
||||
float tolerancePct,
|
||||
uint32_t minPeriod, uint32_t maxPeriod,
|
||||
uint8_t repeat,
|
||||
StageStats &stats);
|
||||
|
||||
@@ -26,6 +26,7 @@ Display::Display() : oled_(128, 32, &Wire, -1) {}
|
||||
bool Display::begin() {
|
||||
Wire.begin(GPIO_SDA, GPIO_SCL);
|
||||
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.
|
||||
// Probe it once and keep the I2C driver quiet when no display is connected.
|
||||
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) {
|
||||
float value = hz; const char *suffix = "Hz";
|
||||
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)
|
||||
snprintf(out, n, "%.0f%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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
|
||||
const uint64_t minutes = totalSeconds / 60ULL;
|
||||
|
||||
@@ -15,7 +15,9 @@ class Display {
|
||||
bool powered() const { return powered_; }
|
||||
static void formatFrequency(float 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:
|
||||
void drawTextLine(const char *text, int16_t y, int16_t startX = 0);
|
||||
Adafruit_SSD1306 oled_;
|
||||
|
||||
736
OpticalChannelTester/DriverTest.cpp
Normal file
736
OpticalChannelTester/DriverTest.cpp
Normal file
@@ -0,0 +1,736 @@
|
||||
#include "DriverTest.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#include <soc/gpio_struct.h>
|
||||
#include <string.h>
|
||||
|
||||
static inline uint32_t IRAM_ATTR maskAllInterrupts() {
|
||||
uint32_t state;
|
||||
asm volatile("rsil %0, 15" : "=a"(state) :: "memory");
|
||||
return state;
|
||||
}
|
||||
|
||||
static inline void IRAM_ATTR restoreInterrupts(uint32_t state) {
|
||||
asm volatile("wsr %0, ps\nrsync" :: "a"(state) : "memory");
|
||||
}
|
||||
|
||||
void DriverEdgeStats::reset() {
|
||||
memset(this, 0, sizeof(*this));
|
||||
minDelayTicks = minResponseTicks = UINT32_MAX;
|
||||
}
|
||||
|
||||
void DriverStats::reset() {
|
||||
memset(this, 0, sizeof(*this));
|
||||
minDelayTicks = minResponseTicks = UINT32_MAX;
|
||||
turnOn.reset();
|
||||
turnOff.reset();
|
||||
reason = FailReason::NONE;
|
||||
}
|
||||
|
||||
uint64_t DriverTest::nsToTicks(uint32_t ns) const {
|
||||
return (static_cast<uint64_t>(ns) * captureHz_ + 999999999ULL) /
|
||||
1000000000ULL;
|
||||
}
|
||||
|
||||
uint64_t DriverTest::ticksToNs(uint64_t ticks) const {
|
||||
return (ticks * 1000000000ULL + captureHz_ / 2U) / captureHz_;
|
||||
}
|
||||
|
||||
bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
float tolerancePct, uint32_t testTimeMs,
|
||||
uint8_t settleCycles, bool activeTxLightOn,
|
||||
bool activeRxLightOn) {
|
||||
(void)tolerancePct;
|
||||
(void)settleCycles;
|
||||
if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
|
||||
!testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false;
|
||||
|
||||
requestCaptureStop();
|
||||
if (!waitCaptureStopped(25U)) return false;
|
||||
if (!pollTask_ && xTaskCreatePinnedToCore(pollTaskEntry, "driver-poll",
|
||||
3072, this, configMAX_PRIORITIES - 1U, &pollTask_, 0) != pdPASS)
|
||||
return false;
|
||||
if (!analyzerTask_ && xTaskCreatePinnedToCore(analyzerTaskEntry,
|
||||
"driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
|
||||
return false;
|
||||
|
||||
// The ACK edges can be less than 1 us apart. MCPWM capture delivers all
|
||||
// channels through one group ISR and can overwrite an earlier channel
|
||||
// timestamp before that ISR reaches it. During DRIVER test dedicate core 0
|
||||
// to direct GPIO sampling; PWM itself remains fully hardware-generated.
|
||||
captureHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||
if (!captureHz_ || captureHz_ % frequencyHz) return false;
|
||||
captureFrequencyHz_ = frequencyHz;
|
||||
capturePulseNs_ = pulseNs;
|
||||
captureTxLightOn_ = activeTxLightOn;
|
||||
txPulseLightOn_ = activeTxLightOn;
|
||||
pollPeriodCycles_ = captureHz_ / frequencyHz;
|
||||
pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us
|
||||
const uint64_t periodNs = 1000000000ULL / frequencyHz;
|
||||
uint64_t windowNs = pulseNs + 50000ULL;
|
||||
const uint64_t maximumWindowNs = periodNs * 3ULL / 4ULL;
|
||||
if (windowNs > maximumWindowNs) windowNs = maximumWindowNs;
|
||||
pollWindowAfterCycles_ = static_cast<uint32_t>(
|
||||
windowNs * captureHz_ / 1000000000ULL);
|
||||
const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL :
|
||||
TX_LIGHT_OFF_GPIO_LEVEL;
|
||||
pollTxStartRawHigh_ = activeTxRaw == HIGH;
|
||||
rxActiveRawHigh_ =
|
||||
((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn);
|
||||
|
||||
ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
|
||||
faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
|
||||
stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
|
||||
testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
|
||||
const uint32_t requestedSubsamples = testTimeMs < 1000U ?
|
||||
DRIVER_SHORT_SAMPLE_PROGRESS_STEPS :
|
||||
(testTimeMs + DRIVER_PROGRESS_INTERVAL_MS - 1U) /
|
||||
DRIVER_PROGRESS_INTERVAL_MS;
|
||||
subsampleCount_ = static_cast<uint8_t>(
|
||||
requestedSubsamples > UINT8_MAX ? UINT8_MAX : requestedSubsamples);
|
||||
subsampleTicks_ = testTicks_ / subsampleCount_;
|
||||
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ ||
|
||||
!ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ ||
|
||||
!testTicks_ || !subsampleTicks_)
|
||||
return false;
|
||||
|
||||
clearCapture();
|
||||
stats_.reset();
|
||||
publishStats();
|
||||
pendingCount_ = 0;
|
||||
response_ = {};
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0;
|
||||
// Skip two complete periods after the polling task synchronizes with TX.
|
||||
settleCycles_ = DRIVER_CAPTURE_SYNC_CYCLES;
|
||||
Log::printf("DRIVER", "capture=GPIO-%luMHz sync-periods=%u ACK-timeout=%luns",
|
||||
static_cast<unsigned long>(captureHz_ / 1000000UL),
|
||||
static_cast<unsigned>(settleCycles_),
|
||||
static_cast<unsigned long>(DRIVER_ACK_START_MAX_NS));
|
||||
const uint64_t periodUs =
|
||||
(1000000ULL + frequencyHz - 1ULL) / frequencyHz;
|
||||
settlingTimeoutUs_ = periodUs *
|
||||
(static_cast<uint64_t>(DRIVER_CAPTURE_SYNC_CYCLES) + 2ULL) + 1000ULL;
|
||||
settlingDeadlineUs_ = 0;
|
||||
settledCycles_ = 0;
|
||||
completedSubsamples_ = 0;
|
||||
measurementClosed_ = false;
|
||||
havePointOrigin_ = false;
|
||||
haveLastActiveTx_ = false;
|
||||
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
||||
rxActiveRawHigh_;
|
||||
currentStep_ = 0;
|
||||
traceWrite_ = traceCount_ = 0;
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
state_ = DriverState::SETTLING;
|
||||
return armCapture();
|
||||
}
|
||||
|
||||
bool DriverTest::armCapture() {
|
||||
// Never wait for USB/Serial here: a disconnected or slow host must not
|
||||
// delay a subsample or consume the test's global timeout.
|
||||
if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) {
|
||||
TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0);
|
||||
const bool watched = idle0 && esp_task_wdt_status(idle0) == ESP_OK;
|
||||
const bool disabled = watched && disableCore0WDT();
|
||||
__atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE);
|
||||
// If IDLE0 is not watched there is nothing to remove or restore.
|
||||
}
|
||||
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&captureActive_, true, __ATOMIC_RELEASE);
|
||||
xTaskNotifyGive(pollTask_);
|
||||
const uint32_t readyDeadline = millis() + 25U;
|
||||
while (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
|
||||
static_cast<int32_t>(millis() - readyDeadline) < 0) delay(0);
|
||||
if (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) {
|
||||
requestCaptureStop();
|
||||
waitCaptureStopped(25U);
|
||||
state_ = DriverState::IDLE;
|
||||
return false;
|
||||
}
|
||||
settlingDeadlineUs_ = static_cast<uint64_t>(esp_timer_get_time()) +
|
||||
settlingTimeoutUs_;
|
||||
xTaskNotifyGive(analyzerTask_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DriverTest::resumeSubsample() {
|
||||
if (state_ != DriverState::SUBSAMPLE_DONE) return false;
|
||||
if (!waitCaptureStopped(25U)) {
|
||||
fail(FailReason::DATA_LOSS, lastEventTick_);
|
||||
return false;
|
||||
}
|
||||
|
||||
clearCapture();
|
||||
pendingCount_ = 0;
|
||||
response_ = {};
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
lastActiveTxTick_ = 0;
|
||||
settlingDeadlineUs_ = 0;
|
||||
settledCycles_ = 0;
|
||||
measurementClosed_ = false;
|
||||
haveLastActiveTx_ = false;
|
||||
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
|
||||
rxActiveRawHigh_;
|
||||
state_ = DriverState::SETTLING;
|
||||
if (armCapture()) return true;
|
||||
fail(FailReason::DATA_LOSS, lastEventTick_);
|
||||
return false;
|
||||
}
|
||||
|
||||
void DriverTest::pollTaskEntry(void *context) {
|
||||
static_cast<DriverTest *>(context)->pollTaskLoop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR DriverTest::pollTaskLoop() {
|
||||
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
uint32_t levels = GPIO.in & PIN_MASK;
|
||||
uint32_t nextStart = 0;
|
||||
uint32_t windowEnd = 0;
|
||||
uint32_t lastTxStart = 0;
|
||||
RawEvent hotEvents[32] = {};
|
||||
uint8_t hotCount = 0;
|
||||
bool sawTxStart = false;
|
||||
bool critical = false;
|
||||
bool allInterruptsMasked = false;
|
||||
uint32_t interruptState = 0;
|
||||
|
||||
auto sampleOnce = [&]() {
|
||||
const uint32_t current = GPIO.in & PIN_MASK;
|
||||
if (current == levels) return;
|
||||
const uint32_t now = esp_cpu_get_cycle_count();
|
||||
const uint32_t changed = current ^ levels;
|
||||
if ((changed & (1UL << GPIO_PWM)) && hotCount < 32U)
|
||||
hotEvents[hotCount++] = {now,
|
||||
(current & (1UL << GPIO_PWM)) != 0U, Source::TX};
|
||||
if ((changed & (1UL << GPIO_RX)) && hotCount < 32U)
|
||||
hotEvents[hotCount++] = {now,
|
||||
(current & (1UL << GPIO_RX)) != 0U, Source::RX};
|
||||
if ((changed & (1UL << GPIO_PWM)) &&
|
||||
((current & (1UL << GPIO_PWM)) != 0U) == pollTxStartRawHigh_) {
|
||||
lastTxStart = now;
|
||||
sawTxStart = true;
|
||||
}
|
||||
levels = current;
|
||||
};
|
||||
|
||||
auto flushHot = [&]() {
|
||||
for (uint8_t i = 0; i < hotCount; ++i)
|
||||
recordRaw(hotEvents[i].tick, hotEvents[i].rising,
|
||||
hotEvents[i].source);
|
||||
hotCount = 0;
|
||||
};
|
||||
|
||||
portENTER_CRITICAL(&pollMux_);
|
||||
critical = true;
|
||||
__atomic_store_n(&captureReady_, true, __ATOMIC_RELEASE);
|
||||
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && !sawTxStart)
|
||||
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
||||
if (sawTxStart) windowEnd = lastTxStart + pollWindowAfterCycles_;
|
||||
const bool synchronized = sawTxStart;
|
||||
if (synchronized) {
|
||||
interruptState = maskAllInterrupts();
|
||||
allInterruptsMasked = true;
|
||||
}
|
||||
|
||||
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && synchronized) {
|
||||
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
|
||||
static_cast<int32_t>(esp_cpu_get_cycle_count() - windowEnd) < 0)
|
||||
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
||||
|
||||
restoreInterrupts(interruptState);
|
||||
allInterruptsMasked = false;
|
||||
portEXIT_CRITICAL(&pollMux_);
|
||||
critical = false;
|
||||
flushHot();
|
||||
// This marker is written only after every TX/RX edge from the completed
|
||||
// sampling window. The analyzer may now safely decide that an ACK was
|
||||
// absent without racing the producer that writes those edges.
|
||||
recordRaw(esp_cpu_get_cycle_count(), false, Source::WINDOW_END);
|
||||
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
|
||||
nextStart = lastTxStart + pollPeriodCycles_;
|
||||
sawTxStart = false;
|
||||
|
||||
uint32_t outsideSpins = 0;
|
||||
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
|
||||
static_cast<int32_t>(esp_cpu_get_cycle_count() -
|
||||
(nextStart - pollWindowBeforeCycles_)) < 0) {
|
||||
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
|
||||
if (++outsideSpins >= 256U) {
|
||||
outsideSpins = 0;
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
|
||||
|
||||
portENTER_CRITICAL(&pollMux_);
|
||||
critical = true;
|
||||
interruptState = maskAllInterrupts();
|
||||
allInterruptsMasked = true;
|
||||
windowEnd = nextStart + pollWindowAfterCycles_;
|
||||
}
|
||||
if (allInterruptsMasked) restoreInterrupts(interruptState);
|
||||
if (critical) portEXIT_CRITICAL(&pollMux_);
|
||||
flushHot();
|
||||
if (__atomic_exchange_n(&core0WdtDisabled_, false,
|
||||
__ATOMIC_ACQ_REL)) enableCore0WDT();
|
||||
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
void DriverTest::analyzerTaskEntry(void *context) {
|
||||
static_cast<DriverTest *>(context)->analyzerTaskLoop();
|
||||
}
|
||||
|
||||
void DriverTest::analyzerTaskLoop() {
|
||||
TimedEvent events[64] = {};
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
while (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING) {
|
||||
const size_t count = readRaw(events, 64, pdMS_TO_TICKS(1));
|
||||
for (size_t i = 0; i < count &&
|
||||
(state_ == DriverState::SETTLING || state_ == DriverState::RUNNING);
|
||||
++i) processEvent(events[i]);
|
||||
const uint32_t dropped = takeDropped();
|
||||
if (dropped) {
|
||||
stats_.droppedItems += dropped;
|
||||
fail(FailReason::DATA_LOSS, lastEventTick_);
|
||||
}
|
||||
if (!count && state_ == DriverState::SETTLING &&
|
||||
static_cast<uint64_t>(esp_timer_get_time()) >=
|
||||
settlingDeadlineUs_) {
|
||||
fail(FailReason::ACK_MISSING, lastEventTick_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DriverTest::processEvent(const TimedEvent &event) {
|
||||
lastEventTick_ = event.tick;
|
||||
if (event.source == Source::WINDOW_END) {
|
||||
if (state_ == DriverState::RUNNING) {
|
||||
expirePending(event.tick);
|
||||
if (state_ == DriverState::RUNNING) completeIfPossible(event.tick);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!havePointOrigin_) {
|
||||
pointOriginTick_ = event.tick;
|
||||
havePointOrigin_ = true;
|
||||
}
|
||||
rememberTrace(event);
|
||||
if (state_ == DriverState::SETTLING) processSettling(event);
|
||||
else if (state_ == DriverState::RUNNING) processRunning(event);
|
||||
}
|
||||
|
||||
void DriverTest::processSettling(const TimedEvent &event) {
|
||||
if (event.source == Source::RX) {
|
||||
rxActive_ = event.rising == rxActiveRawHigh_;
|
||||
return;
|
||||
}
|
||||
const uint8_t rawLevel = event.rising ? HIGH : LOW;
|
||||
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
|
||||
if (lightOn != txPulseLightOn_) return;
|
||||
if (settledCycles_ < settleCycles_) {
|
||||
++settledCycles_;
|
||||
return;
|
||||
}
|
||||
if (rxActive_) {
|
||||
fail(FailReason::DRIVER_FAULT, event.tick);
|
||||
return;
|
||||
}
|
||||
state_ = DriverState::RUNNING;
|
||||
measurementStartTick_ = event.tick;
|
||||
const uint64_t measuredBefore =
|
||||
static_cast<uint64_t>(completedSubsamples_) * subsampleTicks_;
|
||||
const uint64_t thisSubsampleTicks =
|
||||
completedSubsamples_ + 1U == subsampleCount_ ?
|
||||
testTicks_ - measuredBefore : subsampleTicks_;
|
||||
deadlineTick_ = event.tick + thisSubsampleTicks;
|
||||
processTx(event, lightOn);
|
||||
}
|
||||
|
||||
void DriverTest::processRunning(const TimedEvent &event) {
|
||||
if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
|
||||
const uint64_t delay = response_.associated ?
|
||||
response_.startTick - response_.tx.tick : 0;
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : delay;
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, delay,
|
||||
event.tick - response_.startTick, trigger);
|
||||
return;
|
||||
}
|
||||
if (event.source == Source::TX) {
|
||||
expirePending(event.tick);
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
const uint8_t rawLevel = event.rising ? HIGH : LOW;
|
||||
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
|
||||
if (event.tick < deadlineTick_) processTx(event, lightOn);
|
||||
else measurementClosed_ = true;
|
||||
} else {
|
||||
// A delayed fault indication can start after the normal ACK deadline.
|
||||
// Measure the RX pulse before expiring its possible causal TX edge.
|
||||
processRx(event, event.rising == rxActiveRawHigh_);
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
if (!response_.active) expirePending(event.tick);
|
||||
}
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
completeIfPossible(event.tick);
|
||||
}
|
||||
|
||||
bool DriverTest::addPending(uint64_t tick, bool lightOn) {
|
||||
if (pendingCount_ >= MAX_PENDING) {
|
||||
fail(FailReason::DATA_LOSS, tick);
|
||||
return false;
|
||||
}
|
||||
pending_[pendingCount_++] = {tick, lightOn};
|
||||
return true;
|
||||
}
|
||||
|
||||
void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
|
||||
if (!addPending(event.tick, lightOn)) return;
|
||||
if (lightOn == txPulseLightOn_) {
|
||||
lastActiveTxTick_ = event.tick;
|
||||
haveLastActiveTx_ = true;
|
||||
}
|
||||
++stats_.inputEdges;
|
||||
}
|
||||
|
||||
int8_t DriverTest::matchingPending(uint64_t rxTick) const {
|
||||
for (uint8_t i = 0; i < pendingCount_; ++i)
|
||||
if (rxTick >= pending_[i].tick &&
|
||||
rxTick - pending_[i].tick <= ackStartMaxTicks_)
|
||||
return static_cast<int8_t>(i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DriverTest::removePending(uint8_t index) {
|
||||
if (index >= pendingCount_) return;
|
||||
for (uint8_t i = index + 1U; i < pendingCount_; ++i)
|
||||
pending_[i - 1U] = pending_[i];
|
||||
--pendingCount_;
|
||||
}
|
||||
|
||||
void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
|
||||
rxActive_ = activeNow;
|
||||
if (activeNow) {
|
||||
if (response_.active) {
|
||||
fail(FailReason::DATA_LOSS, event.tick);
|
||||
return;
|
||||
}
|
||||
response_ = {};
|
||||
response_.active = true;
|
||||
response_.startTick = event.tick;
|
||||
const int8_t index = matchingPending(event.tick);
|
||||
if (index >= 0) {
|
||||
response_.associated = true;
|
||||
response_.tx = pending_[index];
|
||||
removePending(static_cast<uint8_t>(index));
|
||||
} else ++stats_.unexpectedResponses;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response_.active) return;
|
||||
const uint64_t width = event.tick - response_.startTick;
|
||||
if (!response_.associated) {
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : 0;
|
||||
response_ = {};
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t guard = mergeGuardTicks();
|
||||
for (uint8_t i = 0; i < pendingCount_; ++i) {
|
||||
if (pending_[i].tick > response_.startTick &&
|
||||
event.tick - pending_[i].tick >= guard) {
|
||||
const uint64_t delay = response_.startTick - response_.tx.tick;
|
||||
const uint64_t trigger = event.tick - pending_[i].tick;
|
||||
response_ = {};
|
||||
fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (width >= faultLongTicks_) {
|
||||
const uint64_t delay = response_.startTick - response_.tx.tick;
|
||||
const uint64_t trigger = haveLastActiveTx_ &&
|
||||
response_.startTick >= lastActiveTxTick_ ?
|
||||
response_.startTick - lastActiveTxTick_ : delay;
|
||||
response_ = {};
|
||||
fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t delay = response_.startTick - response_.tx.tick;
|
||||
const bool lightOn = response_.tx.lightOn;
|
||||
response_ = {};
|
||||
acceptAcknowledgement(delay, width, lightOn);
|
||||
}
|
||||
|
||||
uint64_t DriverTest::mergeGuardTicks() const {
|
||||
uint32_t observedMax = stats_.maxDelayTicks;
|
||||
const uint64_t baseline = observedMax ? observedMax :
|
||||
nsToTicks(DRIVER_ACK_DELAY_NS + 500U);
|
||||
return baseline + nsToTicks(DRIVER_ACK_MERGE_MARGIN_NS);
|
||||
}
|
||||
|
||||
void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
|
||||
bool lightOn) {
|
||||
const uint32_t delay32 = delay > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(delay);
|
||||
const uint32_t width32 = width > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(width);
|
||||
stats_.lastDelayTicks = delay32;
|
||||
stats_.lastResponseTicks = width32;
|
||||
++stats_.responses;
|
||||
if (delay32 < stats_.minDelayTicks) stats_.minDelayTicks = delay32;
|
||||
if (delay32 > stats_.maxDelayTicks) stats_.maxDelayTicks = delay32;
|
||||
if (width32 < stats_.minResponseTicks) stats_.minResponseTicks = width32;
|
||||
if (width32 > stats_.maxResponseTicks) stats_.maxResponseTicks = width32;
|
||||
|
||||
DriverEdgeStats &edge = lightOn ? stats_.turnOn : stats_.turnOff;
|
||||
++edge.responses;
|
||||
edge.delaySumTicks += delay32;
|
||||
edge.responseSumTicks += width32;
|
||||
if (delay32 < edge.minDelayTicks) edge.minDelayTicks = delay32;
|
||||
if (delay32 > edge.maxDelayTicks) edge.maxDelayTicks = delay32;
|
||||
if (width32 < edge.minResponseTicks) edge.minResponseTicks = width32;
|
||||
if (width32 > edge.maxResponseTicks) edge.maxResponseTicks = width32;
|
||||
publishStats();
|
||||
}
|
||||
|
||||
void DriverTest::expirePending(uint64_t now) {
|
||||
for (uint8_t i = 0; i < pendingCount_; ++i) {
|
||||
if (now < pending_[i].tick + ackStartMaxTicks_) continue;
|
||||
// The failure belongs to the ACK deadline itself. A later TX edge or the
|
||||
// end-of-window marker is only the safe moment when absence is confirmed.
|
||||
fail(FailReason::ACK_MISSING,
|
||||
pending_[i].tick + ackStartMaxTicks_, ackStartMaxTicks_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void DriverTest::completeIfPossible(uint64_t now) {
|
||||
if (state_ != DriverState::RUNNING) return;
|
||||
if (!measurementClosed_ && now >= deadlineTick_) measurementClosed_ = true;
|
||||
if (!measurementClosed_ || response_.active || pendingCount_) return;
|
||||
if (!stats_.turnOn.responses || !stats_.turnOff.responses) {
|
||||
fail(FailReason::ACK_MISSING, now);
|
||||
return;
|
||||
}
|
||||
++completedSubsamples_;
|
||||
currentStep_ = completedSubsamples_;
|
||||
publishStats();
|
||||
requestCaptureStop();
|
||||
if (!waitCaptureStopped(25U)) {
|
||||
fail(FailReason::DATA_LOSS, lastEventTick_);
|
||||
return;
|
||||
}
|
||||
if (completedSubsamples_ >= subsampleCount_) {
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
state_ = DriverState::PASS;
|
||||
} else {
|
||||
__atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
|
||||
state_ = DriverState::SUBSAMPLE_DONE;
|
||||
}
|
||||
}
|
||||
|
||||
void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay,
|
||||
uint64_t pulseWidth, uint64_t triggerAfterTx) {
|
||||
if (state_ == DriverState::FAIL || state_ == DriverState::PASS) return;
|
||||
if (stats_.reason == FailReason::NONE) {
|
||||
stats_.reason = reason;
|
||||
if (tick && havePointOrigin_ && tick >= pointOriginTick_)
|
||||
stats_.errorElapsedTicks = tick - pointOriginTick_;
|
||||
const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay;
|
||||
if (trigger) {
|
||||
stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(trigger);
|
||||
stats_.errorTriggerValid = true;
|
||||
}
|
||||
if (delay) {
|
||||
stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(delay);
|
||||
stats_.errorDelayValid = true;
|
||||
}
|
||||
if (pulseWidth) {
|
||||
stats_.errorPulseTicks = pulseWidth > UINT32_MAX ? UINT32_MAX :
|
||||
static_cast<uint32_t>(pulseWidth);
|
||||
stats_.errorPulseValid = true;
|
||||
}
|
||||
}
|
||||
publishStats();
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
requestCaptureStop();
|
||||
state_ = DriverState::FAIL;
|
||||
}
|
||||
|
||||
void DriverTest::publishStats() {
|
||||
portENTER_CRITICAL(&statsMux_);
|
||||
publishedStats_ = stats_;
|
||||
portEXIT_CRITICAL(&statsMux_);
|
||||
}
|
||||
|
||||
void DriverTest::forceFail(FailReason reason) {
|
||||
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
|
||||
state_ == DriverState::SUBSAMPLE_DONE)
|
||||
fail(reason, lastEventTick_);
|
||||
}
|
||||
|
||||
void DriverTest::abort() {
|
||||
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
|
||||
state_ == DriverState::SUBSAMPLE_DONE)
|
||||
fail(FailReason::ABORTED, lastEventTick_);
|
||||
else {
|
||||
requestCaptureStop();
|
||||
state_ = DriverState::IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
bool DriverTest::takeProgressUpdate() {
|
||||
return __atomic_exchange_n(&progressUpdatePending_, false,
|
||||
__ATOMIC_ACQ_REL);
|
||||
}
|
||||
|
||||
void DriverTest::requestCaptureStop() {
|
||||
__atomic_store_n(&captureActive_, false, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
bool DriverTest::waitCaptureStopped(uint32_t timeoutMs) {
|
||||
const uint32_t deadline = millis() + timeoutMs;
|
||||
while (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
|
||||
static_cast<int32_t>(millis() - deadline) < 0) delay(0);
|
||||
if (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) return false;
|
||||
if (__atomic_exchange_n(&core0WdtDisabled_, false,
|
||||
__ATOMIC_ACQ_REL)) enableCore0WDT();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DriverTest::clearCapture() {
|
||||
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
|
||||
__atomic_store_n(&ringRead_, write, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&droppedItems_, 0U, __ATOMIC_RELEASE);
|
||||
haveRawTick_ = false;
|
||||
lastRawTick_ = 0;
|
||||
tickEpoch_ = 0;
|
||||
}
|
||||
|
||||
void IRAM_ATTR DriverTest::recordRaw(uint32_t tick, bool rising,
|
||||
Source source) {
|
||||
const uint16_t write = ringWrite_;
|
||||
const uint16_t next = static_cast<uint16_t>(
|
||||
(write + 1U) & (RING_CAPACITY - 1U));
|
||||
if (next == ringRead_) {
|
||||
++droppedItems_;
|
||||
return;
|
||||
}
|
||||
ring_[write] = {tick, rising, source};
|
||||
asm volatile("memw" ::: "memory");
|
||||
ringWrite_ = next;
|
||||
}
|
||||
|
||||
size_t DriverTest::readRaw(TimedEvent *events, size_t capacity,
|
||||
TickType_t waitTicks) {
|
||||
if (!events || !capacity) return 0;
|
||||
uint16_t read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
|
||||
if (read == __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
|
||||
vTaskDelay(waitTicks);
|
||||
read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
|
||||
}
|
||||
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
|
||||
size_t count = 0;
|
||||
while (read != write && count < capacity) {
|
||||
const RawEvent raw = ring_[read];
|
||||
read = static_cast<uint16_t>((read + 1U) & (RING_CAPACITY - 1U));
|
||||
if (haveRawTick_ && raw.tick < lastRawTick_ &&
|
||||
lastRawTick_ - raw.tick > 0x80000000UL) tickEpoch_ += 1ULL << 32U;
|
||||
lastRawTick_ = raw.tick;
|
||||
haveRawTick_ = true;
|
||||
events[count++] = {tickEpoch_ + raw.tick, raw.rising, raw.source};
|
||||
}
|
||||
__atomic_store_n(&ringRead_, read, __ATOMIC_RELEASE);
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t DriverTest::takeDropped() {
|
||||
return __atomic_exchange_n(&droppedItems_, 0U, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
|
||||
void DriverTest::rememberTrace(const TimedEvent &event) {
|
||||
trace_[traceWrite_] = {event.tick, static_cast<uint8_t>(event.source),
|
||||
static_cast<uint8_t>(event.rising), static_cast<uint8_t>(state_),
|
||||
pendingCount_};
|
||||
traceWrite_ = static_cast<uint8_t>((traceWrite_ + 1U) % TRACE_CAPACITY);
|
||||
if (traceCount_ < TRACE_CAPACITY) ++traceCount_;
|
||||
}
|
||||
|
||||
void DriverTest::printSummary() const {
|
||||
auto printEdge = [&](const char *name, const DriverEdgeStats &edge) {
|
||||
if (!edge.responses) {
|
||||
Log::printf("DRIVER", "%s ACK=0", name);
|
||||
return;
|
||||
}
|
||||
Log::printf("DRIVER",
|
||||
"%s ACK=%lu D=%lluns/%lluns/%lluns P=%lluns/%lluns/%lluns",
|
||||
name, static_cast<unsigned long>(edge.responses),
|
||||
static_cast<unsigned long long>(ticksToNs(edge.minDelayTicks)),
|
||||
static_cast<unsigned long long>(ticksToNs(
|
||||
edge.delaySumTicks / edge.responses)),
|
||||
static_cast<unsigned long long>(ticksToNs(edge.maxDelayTicks)),
|
||||
static_cast<unsigned long long>(ticksToNs(edge.minResponseTicks)),
|
||||
static_cast<unsigned long long>(ticksToNs(
|
||||
edge.responseSumTicks / edge.responses)),
|
||||
static_cast<unsigned long long>(ticksToNs(edge.maxResponseTicks)));
|
||||
};
|
||||
Log::printf("DRIVER", "TX edges=%lu responses=%lu dropped=%lu unexpected=%lu result=%s",
|
||||
static_cast<unsigned long>(publishedStats_.inputEdges),
|
||||
static_cast<unsigned long>(publishedStats_.responses),
|
||||
static_cast<unsigned long>(publishedStats_.droppedItems),
|
||||
static_cast<unsigned long>(publishedStats_.unexpectedResponses),
|
||||
failName(publishedStats_.reason));
|
||||
if (publishedStats_.reason != FailReason::NONE) {
|
||||
auto formatOptional = [&](bool valid, uint32_t ticks,
|
||||
char *out, size_t size) {
|
||||
if (!valid) snprintf(out, size, "---");
|
||||
else snprintf(out, size, "%lluns",
|
||||
static_cast<unsigned long long>(ticksToNs(ticks)));
|
||||
};
|
||||
char trigger[24], pulse[24];
|
||||
formatOptional(publishedStats_.errorTriggerValid,
|
||||
publishedStats_.errorTriggerTicks, trigger, sizeof(trigger));
|
||||
formatOptional(publishedStats_.errorPulseValid,
|
||||
publishedStats_.errorPulseTicks, pulse, sizeof(pulse));
|
||||
if (publishedStats_.errorPulseValid)
|
||||
Log::printf("DRIVER", "error timing: T=%s P=%s", trigger, pulse);
|
||||
else Log::printf("DRIVER", "error timing: T=%s", trigger);
|
||||
}
|
||||
printEdge("ON", publishedStats_.turnOn);
|
||||
printEdge("OFF", publishedStats_.turnOff);
|
||||
}
|
||||
|
||||
void DriverTest::printTrace() const {
|
||||
if (!traceCount_) return;
|
||||
const uint8_t first = static_cast<uint8_t>(
|
||||
(traceWrite_ + TRACE_CAPACITY - traceCount_) % TRACE_CAPACITY);
|
||||
const uint64_t origin = trace_[first].tick;
|
||||
Log::printf("DRIVER", "RAM trace: %u events, tick=%luHz", traceCount_,
|
||||
static_cast<unsigned long>(captureHz_));
|
||||
for (uint8_t i = 0; i < traceCount_; ++i) {
|
||||
const TraceEvent &event = trace_[(first + i) % TRACE_CAPACITY];
|
||||
Log::printf("DRIVER", "E%02u +%lluns %s/%s state=%u pending=%u", i,
|
||||
static_cast<unsigned long long>(ticksToNs(event.tick - origin)),
|
||||
event.source == static_cast<uint8_t>(Source::TX) ? "TX" : "RX",
|
||||
event.rising ? "rise" : "fall", event.state, event.pending);
|
||||
}
|
||||
}
|
||||
180
OpticalChannelTester/DriverTest.h
Normal file
180
OpticalChannelTester/DriverTest.h
Normal file
@@ -0,0 +1,180 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "Receiver.h"
|
||||
|
||||
enum class DriverState : uint8_t {
|
||||
IDLE, SETTLING, RUNNING, SUBSAMPLE_DONE, PASS, FAIL
|
||||
};
|
||||
|
||||
struct DriverEdgeStats {
|
||||
uint32_t responses;
|
||||
uint32_t minDelayTicks;
|
||||
uint32_t maxDelayTicks;
|
||||
uint64_t delaySumTicks;
|
||||
uint32_t minResponseTicks;
|
||||
uint32_t maxResponseTicks;
|
||||
uint64_t responseSumTicks;
|
||||
void reset();
|
||||
};
|
||||
|
||||
struct DriverStats {
|
||||
uint32_t inputEdges;
|
||||
uint32_t responses;
|
||||
uint32_t minDelayTicks;
|
||||
uint32_t maxDelayTicks;
|
||||
uint32_t minResponseTicks;
|
||||
uint32_t maxResponseTicks;
|
||||
uint32_t lastDelayTicks;
|
||||
uint32_t lastResponseTicks;
|
||||
uint32_t droppedItems;
|
||||
uint32_t unexpectedResponses;
|
||||
uint64_t errorElapsedTicks;
|
||||
uint32_t errorTriggerTicks;
|
||||
uint32_t errorDelayTicks;
|
||||
uint32_t errorPulseTicks;
|
||||
bool errorTriggerValid;
|
||||
bool errorDelayValid;
|
||||
bool errorPulseValid;
|
||||
DriverEdgeStats turnOn;
|
||||
DriverEdgeStats turnOff;
|
||||
FailReason reason;
|
||||
void reset();
|
||||
};
|
||||
|
||||
class DriverTest {
|
||||
public:
|
||||
explicit DriverTest(PulseReceiver &receiver) : receiver_(receiver) {}
|
||||
bool start(uint32_t frequencyHz, uint32_t pulseNs, float tolerancePct,
|
||||
uint32_t testTimeMs, uint8_t settleCycles,
|
||||
bool activeTxLightOn, bool activeRxLightOn);
|
||||
DriverState update() const { return state_; }
|
||||
void abort();
|
||||
void forceFail(FailReason reason);
|
||||
bool resumeSubsample();
|
||||
bool takeProgressUpdate();
|
||||
void printSummary() const;
|
||||
void printTrace() const;
|
||||
uint8_t progressStep() const { return currentStep_; }
|
||||
uint8_t progressSteps() const { return subsampleCount_; }
|
||||
uint32_t tickHz() const { return captureHz_; }
|
||||
const DriverStats &stats() const { return publishedStats_; }
|
||||
|
||||
private:
|
||||
enum class Source : uint8_t { TX, RX, WINDOW_END };
|
||||
struct RawEvent { uint32_t tick; bool rising; Source source; };
|
||||
struct TimedEvent { uint64_t tick; bool rising; Source source; };
|
||||
struct PendingTx {
|
||||
uint64_t tick;
|
||||
bool lightOn;
|
||||
};
|
||||
struct Response {
|
||||
bool active;
|
||||
bool associated;
|
||||
uint64_t startTick;
|
||||
PendingTx tx;
|
||||
};
|
||||
struct TraceEvent {
|
||||
uint64_t tick;
|
||||
uint8_t source;
|
||||
uint8_t rising;
|
||||
uint8_t state;
|
||||
uint8_t pending;
|
||||
};
|
||||
|
||||
static void analyzerTaskEntry(void *context);
|
||||
static void pollTaskEntry(void *context);
|
||||
void analyzerTaskLoop();
|
||||
void IRAM_ATTR pollTaskLoop();
|
||||
void processEvent(const TimedEvent &event);
|
||||
void processSettling(const TimedEvent &event);
|
||||
void processRunning(const TimedEvent &event);
|
||||
void processTx(const TimedEvent &event, bool lightOn);
|
||||
void processRx(const TimedEvent &event, bool activeNow);
|
||||
void expirePending(uint64_t now);
|
||||
void completeIfPossible(uint64_t now);
|
||||
bool addPending(uint64_t tick, bool lightOn);
|
||||
int8_t matchingPending(uint64_t rxTick) const;
|
||||
void removePending(uint8_t index);
|
||||
uint64_t mergeGuardTicks() const;
|
||||
void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn);
|
||||
void fail(FailReason reason, uint64_t tick = 0, uint64_t delay = 0,
|
||||
uint64_t pulseWidth = 0, uint64_t triggerAfterTx = 0);
|
||||
void publishStats();
|
||||
void rememberTrace(const TimedEvent &event);
|
||||
bool armCapture();
|
||||
void requestCaptureStop();
|
||||
bool waitCaptureStopped(uint32_t timeoutMs);
|
||||
void clearCapture();
|
||||
void recordRaw(uint32_t tick, bool rising, Source source);
|
||||
size_t readRaw(TimedEvent *events, size_t capacity, TickType_t waitTicks);
|
||||
uint32_t takeDropped();
|
||||
uint64_t nsToTicks(uint32_t ns) const;
|
||||
uint64_t ticksToNs(uint64_t ticks) const;
|
||||
|
||||
static constexpr uint8_t MAX_PENDING = 8;
|
||||
static constexpr uint16_t RING_CAPACITY = 2048;
|
||||
static constexpr uint8_t TRACE_CAPACITY = 32;
|
||||
static_assert((RING_CAPACITY & (RING_CAPACITY - 1U)) == 0,
|
||||
"driver ring capacity must be a power of two");
|
||||
|
||||
PulseReceiver &receiver_;
|
||||
TaskHandle_t analyzerTask_ = nullptr;
|
||||
TaskHandle_t pollTask_ = nullptr;
|
||||
volatile DriverState state_ = DriverState::IDLE;
|
||||
DriverStats stats_ = {};
|
||||
DriverStats publishedStats_ = {};
|
||||
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
RawEvent ring_[RING_CAPACITY] = {};
|
||||
volatile uint16_t ringWrite_ = 0;
|
||||
volatile uint16_t ringRead_ = 0;
|
||||
volatile uint32_t droppedItems_ = 0;
|
||||
volatile bool captureActive_ = false;
|
||||
volatile bool captureReady_ = false;
|
||||
volatile bool core0WdtDisabled_ = false;
|
||||
uint32_t captureHz_ = 0;
|
||||
uint32_t captureFrequencyHz_ = 0;
|
||||
uint32_t capturePulseNs_ = 0;
|
||||
uint32_t pollPeriodCycles_ = 0;
|
||||
uint32_t pollWindowBeforeCycles_ = 0;
|
||||
uint32_t pollWindowAfterCycles_ = 0;
|
||||
bool pollTxStartRawHigh_ = false;
|
||||
bool captureTxLightOn_ = true;
|
||||
portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
PendingTx pending_[MAX_PENDING] = {};
|
||||
uint8_t pendingCount_ = 0;
|
||||
Response response_ = {};
|
||||
uint64_t ackStartMaxTicks_ = 0;
|
||||
uint64_t faultLongTicks_ = 0;
|
||||
uint64_t stuckTicks_ = 0;
|
||||
uint64_t testTicks_ = 0;
|
||||
uint64_t subsampleTicks_ = 0;
|
||||
uint64_t settlingTimeoutUs_ = 0;
|
||||
uint64_t settlingDeadlineUs_ = 0;
|
||||
uint64_t measurementStartTick_ = 0;
|
||||
uint64_t deadlineTick_ = 0;
|
||||
uint64_t pointOriginTick_ = 0;
|
||||
uint64_t lastEventTick_ = 0;
|
||||
uint64_t lastActiveTxTick_ = 0;
|
||||
uint8_t settleCycles_ = 0;
|
||||
uint8_t settledCycles_ = 0;
|
||||
uint8_t subsampleCount_ = DRIVER_SHORT_SAMPLE_PROGRESS_STEPS;
|
||||
uint8_t completedSubsamples_ = 0;
|
||||
bool rxActiveRawHigh_ = true;
|
||||
bool txPulseLightOn_ = true;
|
||||
bool rxActive_ = false;
|
||||
bool measurementClosed_ = false;
|
||||
bool havePointOrigin_ = false;
|
||||
bool haveLastActiveTx_ = false;
|
||||
bool haveRawTick_ = false;
|
||||
uint32_t lastRawTick_ = 0;
|
||||
uint64_t tickEpoch_ = 0;
|
||||
volatile uint8_t currentStep_ = 0;
|
||||
volatile bool progressUpdatePending_ = false;
|
||||
TraceEvent trace_[TRACE_CAPACITY] = {};
|
||||
uint8_t traceWrite_ = 0;
|
||||
uint8_t traceCount_ = 0;
|
||||
};
|
||||
@@ -7,7 +7,8 @@ namespace Log {
|
||||
void event(const char *component, const char *message) {
|
||||
if (!SERIAL_ACTION_LOG) return;
|
||||
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, "DRIVER") && strcmp(component, "ESP-NOW")) return;
|
||||
if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message);
|
||||
else Serial.printf("[%-8s] %s\n", component, message);
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
#include <string.h>
|
||||
|
||||
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
||||
uint8_t settleCycles) {
|
||||
uint16_t averagingPeriods, uint8_t settleCycles,
|
||||
bool activeRxLightOn) {
|
||||
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
|
||||
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
|
||||
expectedDutyPct_ = duty;
|
||||
tolerance = effectiveTolerancePct(tolerance);
|
||||
if (!expectedHz_ || !timeMs ||
|
||||
!receiver_.start(expectedHz_, expectedDutyPct_)) return false;
|
||||
if (!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_)) {
|
||||
receiver_.stop(); return false;
|
||||
}
|
||||
if (!expectedHz_ || !timeMs || !averagingPeriods ||
|
||||
!receiver_.start(expectedHz_, expectedDutyPct_, activeRxLightOn)) return false;
|
||||
settleCycles_ = settleCycles; settleLeft_ = settleCycles;
|
||||
tolerancePct_ = tolerance;
|
||||
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;
|
||||
stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs /
|
||||
(1000ULL * MEASUREMENT_PROGRESS_STEPS);
|
||||
if (!stepTicks_) stepTicks_ = 1;
|
||||
currentStep_ = 0;
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
stats_.reset();
|
||||
publishStats();
|
||||
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
||||
@@ -43,22 +43,33 @@ void Measurement::taskLoop() {
|
||||
|
||||
void Measurement::fail(FailReason reason) {
|
||||
if (stats_.reason == FailReason::NONE) stats_.reason = reason;
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
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() {
|
||||
receiver_.stop();
|
||||
stats_.droppedItems += receiver_.takeDroppedItems();
|
||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
||||
const uint32_t dropped = receiver_.takeDroppedItems();
|
||||
stats_.droppedItems += dropped;
|
||||
if (dropped) { fail(FailReason::DATA_LOSS); return; }
|
||||
publishStats();
|
||||
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;
|
||||
}
|
||||
if (!stats_.periods) {
|
||||
fail(FailReason::DATA_LOSS); return;
|
||||
}
|
||||
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
|
||||
state_ = MeasureState::PASS;
|
||||
}
|
||||
|
||||
@@ -77,11 +88,12 @@ bool Measurement::statsSnapshot(StageStats &out) const {
|
||||
|
||||
MeasureState Measurement::processOnce() {
|
||||
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
|
||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
|
||||
bool receivedPeriod = false;
|
||||
for (;;) {
|
||||
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;
|
||||
receivedPeriod = true;
|
||||
for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) {
|
||||
@@ -96,31 +108,32 @@ MeasureState Measurement::processOnce() {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const uint64_t endTick = period.startTick + period.periodTicks;
|
||||
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
||||
if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period
|
||||
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_);
|
||||
if (r != FailReason::NONE) { fail(r); return state_; }
|
||||
while (period.startTick >= deadlineTick_) {
|
||||
// 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_;
|
||||
}
|
||||
|
||||
const FailReason result = evaluatePeriod(period, receiver_.tickHz(),
|
||||
expectedHz_, expectedDutyPct_, tolerancePct_, currentStep_ + 1U, stats_);
|
||||
if (result != FailReason::NONE) { fail(result); return state_; }
|
||||
}
|
||||
}
|
||||
if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis();
|
||||
const uint64_t edgeBasedTimeout =
|
||||
static_cast<uint64_t>(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
|
||||
const uint64_t rmtBatchTimeout =
|
||||
static_cast<uint64_t>(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
|
||||
const uint64_t settleTimeout = edgeBasedTimeout > rmtBatchTimeout ? edgeBasedTimeout : rmtBatchTimeout;
|
||||
const uint64_t settleTimeout = edgeBasedTimeout;
|
||||
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
|
||||
if (state_ == MeasureState::RUNNING && measurementStartTick_) {
|
||||
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 receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : edgeTimeoutMs;
|
||||
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > receiveTimeoutMs) {
|
||||
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
|
||||
fail(FailReason::LOST_EDGE); return state_;
|
||||
}
|
||||
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
|
||||
@@ -130,21 +143,16 @@ MeasureState Measurement::processOnce() {
|
||||
|
||||
MeasureState Measurement::update() { return state_; }
|
||||
|
||||
bool Measurement::continueAfterDisplay() {
|
||||
if (state_ != MeasureState::STEP_READY) return false;
|
||||
if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
|
||||
fail(FailReason::UNSUPPORTED);
|
||||
return false;
|
||||
}
|
||||
settleLeft_ = settleCycles_;
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0;
|
||||
state_ = MeasureState::SETTLING;
|
||||
xTaskNotifyGive(task_);
|
||||
return true;
|
||||
bool Measurement::takeProgressUpdate() {
|
||||
return __atomic_exchange_n(&progressUpdatePending_, false, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
|
||||
void Measurement::abort() {
|
||||
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING ||
|
||||
state_ == MeasureState::STEP_READY) fail(FailReason::ABORTED);
|
||||
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
|
||||
fail(FailReason::ABORTED);
|
||||
}
|
||||
|
||||
void Measurement::forceFail(FailReason reason) {
|
||||
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
|
||||
fail(reason);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#pragma once
|
||||
#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 {
|
||||
public:
|
||||
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
|
||||
bool start(float expectedHz, float expectedDuty, float tolerancePct,
|
||||
uint32_t testTimeMs, uint8_t settleCycles);
|
||||
uint32_t testTimeMs, uint16_t averagingPeriods,
|
||||
uint8_t settleCycles, bool activeRxLightOn);
|
||||
MeasureState update();
|
||||
bool continueAfterDisplay();
|
||||
bool takeProgressUpdate();
|
||||
void abort();
|
||||
void forceFail(FailReason reason);
|
||||
MeasureState state() const { return state_; }
|
||||
FailReason reason() const { return stats_.reason; }
|
||||
const StageStats &stats() const { return stats_; }
|
||||
@@ -29,13 +31,13 @@ class Measurement {
|
||||
StageStats stats_ = {};
|
||||
StageStats publishedStats_ = {};
|
||||
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
PeriodLimits limits_ = {};
|
||||
uint32_t expectedHz_ = 0;
|
||||
float expectedDutyPct_ = 0.0f;
|
||||
float expectedDutyPct_ = 0.0f, tolerancePct_ = 0.0f;
|
||||
uint8_t settleCycles_ = 0, settleLeft_ = 0;
|
||||
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
|
||||
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
|
||||
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
|
||||
volatile uint8_t currentStep_ = 0;
|
||||
volatile bool progressUpdatePending_ = false;
|
||||
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
||||
};
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
|
||||
#ifdef PWM_OUTPUT_TEST
|
||||
PwmGenerator pwmOutputTest;
|
||||
uint8_t pwmOutputTestDuty = 50;
|
||||
uint32_t pwmOutputTestPulseNs = PWM_OUTPUT_TEST_MIN_PULSE_NS;
|
||||
uint32_t pwmOutputTestUpdatedMs = 0;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(SERIAL_BAUD);
|
||||
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 light-off=%s\n",
|
||||
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_SAFE_LEVEL == HIGH ? "HIGH" : "LOW");
|
||||
TX_LIGHT_OFF_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
|
||||
|
||||
pwmOutputTest.begin();
|
||||
ActualPwm actual = {};
|
||||
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ,
|
||||
pwmOutputTestDuty, actual)) {
|
||||
Serial.printf("PWM OUTPUT TEST STARTED: actual=%luHz duty=%.2f%% bits=%u\n",
|
||||
actual.actualHz, actual.actualDutyPct, actual.bits);
|
||||
pwmOutputTestPulseNs, actual)) {
|
||||
Serial.printf("PWM OUTPUT TEST STARTED: actual=%luHz pulse=%luns bits=%u\n",
|
||||
actual.actualHz, actual.actualPulseNs, actual.bits);
|
||||
} else {
|
||||
Serial.println("PWM OUTPUT TEST FAILED");
|
||||
}
|
||||
@@ -35,16 +35,16 @@ void loop() {
|
||||
constexpr float PWM_TWO_PI = 6.28318530718f;
|
||||
const float phase = PWM_TWO_PI * (now % 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 amplitude = (PWM_OUTPUT_TEST_MAX_DUTY_PCT - PWM_OUTPUT_TEST_MIN_DUTY_PCT) * 0.5f;
|
||||
const uint8_t duty = static_cast<uint8_t>(center + amplitude * sinf(phase) + 0.5f);
|
||||
if (duty == pwmOutputTestDuty) return;
|
||||
const float center = (PWM_OUTPUT_TEST_MIN_PULSE_NS + PWM_OUTPUT_TEST_MAX_PULSE_NS) * 0.5f;
|
||||
const float amplitude = (PWM_OUTPUT_TEST_MAX_PULSE_NS - PWM_OUTPUT_TEST_MIN_PULSE_NS) * 0.5f;
|
||||
const uint32_t pulseNs = static_cast<uint32_t>(center + amplitude * sinf(phase) + 0.5f);
|
||||
if (pulseNs == pwmOutputTestPulseNs) return;
|
||||
|
||||
ActualPwm actual = {};
|
||||
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, duty, actual)) {
|
||||
pwmOutputTestDuty = duty;
|
||||
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, pulseNs, actual)) {
|
||||
pwmOutputTestPulseNs = pulseNs;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "Core.h"
|
||||
|
||||
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 8;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 12;
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
|
||||
@@ -21,24 +21,25 @@ struct ProtocolPacket {
|
||||
uint16_t stageCount;
|
||||
uint16_t sequence;
|
||||
uint32_t requestedHz;
|
||||
uint32_t requestedPulseNs;
|
||||
uint32_t actualHz;
|
||||
uint16_t actualDutyX100;
|
||||
uint32_t actualPulseNs;
|
||||
uint32_t testTimeMs;
|
||||
uint16_t accuracyX100;
|
||||
uint8_t settleCycles;
|
||||
uint8_t lightCode;
|
||||
uint8_t progressStep;
|
||||
uint8_t passed;
|
||||
uint8_t reason;
|
||||
uint32_t periods;
|
||||
uint32_t measuredHzX10;
|
||||
uint16_t measuredDutyX10;
|
||||
uint32_t measuredPulseNs;
|
||||
uint32_t minPeriodTicks;
|
||||
uint32_t maxPeriodTicks;
|
||||
uint16_t crc;
|
||||
};
|
||||
#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);
|
||||
void finalizePacket(ProtocolPacket &packet);
|
||||
|
||||
@@ -12,20 +12,20 @@ namespace {
|
||||
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
|
||||
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_ll_timer_pause(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_ls_timer_update(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;
|
||||
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
|
||||
mcpwm_timer_handle_t mcpwmTimer = nullptr;
|
||||
@@ -94,38 +94,49 @@ void PwmGenerator::begin() {
|
||||
timerConfig.period_ticks / 2U) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
|
||||
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH)) == ESP_OK;
|
||||
MCPWM_TIMER_EVENT_EMPTY, TX_LIGHT_ON_GPIO_LEVEL == HIGH ?
|
||||
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
|
||||
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
mcpwmComparator, MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
mcpwmComparator, TX_LIGHT_OFF_GPIO_LEVEL == HIGH ?
|
||||
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
|
||||
if (!ok) {
|
||||
releaseMcpwm();
|
||||
pinMode(GPIO_PWM, OUTPUT);
|
||||
digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
|
||||
return;
|
||||
}
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
|
||||
const uint8_t activeLevel = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
|
||||
TX_LIGHT_OFF_GPIO_LEVEL;
|
||||
const uint8_t inactiveLevel = activeLevel == HIGH ? LOW : HIGH;
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
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 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) {
|
||||
stop();
|
||||
const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL);
|
||||
if (attached) {
|
||||
// Arduino's LEDC API normally chooses an 8-bit fractional divider.
|
||||
// Force the fractional byte to zero so every PWM period contains the
|
||||
// same integer number of 40 MHz source-clock ticks.
|
||||
setIntegerDivider(config.divider);
|
||||
// Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when
|
||||
// the configured active pulse level is LOW.
|
||||
if (!ledcOutputInvert(GPIO_PWM, activeLevel == LOW)) {
|
||||
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 a timer edge. Reading immediately can therefore return zero.
|
||||
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);
|
||||
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
@@ -141,31 +153,45 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
if (attached) ledcDetach(GPIO_PWM);
|
||||
delay(2);
|
||||
}
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
|
||||
return false;
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || dutyPct > 100U ||
|
||||
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs ||
|
||||
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<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 >= periodTicks) activeTicks = periodTicks - 1U;
|
||||
|
||||
stop();
|
||||
bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK;
|
||||
ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
|
||||
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
MCPWM_TIMER_EVENT_EMPTY, activeLevel == HIGH ?
|
||||
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
|
||||
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
mcpwmComparator, inactiveLevel == HIGH ?
|
||||
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
// stop() applies a continuous force level (hold_on=true). Remove that same
|
||||
// continuous-force action; hold_on=false addresses a different, one-shot
|
||||
// force mechanism and would leave the safe level permanently active.
|
||||
ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, true) == ESP_OK;
|
||||
ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK;
|
||||
if (!ok) {
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
|
||||
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;
|
||||
running_ = true;
|
||||
return true;
|
||||
@@ -175,9 +201,10 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
void PwmGenerator::stop() {
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
if (running_) ledcDetach(GPIO_PWM);
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
if (mcpwmGenerator) mcpwm_generator_set_force_level(
|
||||
mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
|
||||
if (running_ && mcpwmTimer) {
|
||||
mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY);
|
||||
const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U;
|
||||
@@ -189,16 +216,33 @@ void PwmGenerator::stop() {
|
||||
}
|
||||
|
||||
void PwmGenerator::active() {
|
||||
// First detach/stop the PWM peripheral, then select the independently
|
||||
// configured active level. The active and safe levels may be equal.
|
||||
// First detach/stop the PWM peripheral, then apply the same active level
|
||||
// that denotes the pulse during a running test.
|
||||
stop();
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL);
|
||||
digitalWrite(GPIO_PWM, activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
|
||||
TX_LIGHT_OFF_GPIO_LEVEL);
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, PWM_ACTIVE_LEVEL, true);
|
||||
const uint8_t level = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
|
||||
TX_LIGHT_OFF_GPIO_LEVEL;
|
||||
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, level, true);
|
||||
else {
|
||||
pinMode(GPIO_PWM, OUTPUT);
|
||||
digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL);
|
||||
digitalWrite(GPIO_PWM, level);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PwmGenerator::lightOn() {
|
||||
stop();
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (mcpwmGenerator)
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_ON_GPIO_LEVEL, true);
|
||||
else {
|
||||
pinMode(GPIO_PWM, OUTPUT);
|
||||
digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
#pragma once
|
||||
#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 {
|
||||
public:
|
||||
void begin();
|
||||
bool start(uint32_t frequencyHz, uint8_t dutyPct, ActualPwm &actual);
|
||||
void configureActiveLight(bool lightOn) { activeLightOn_ = lightOn; }
|
||||
bool start(uint32_t frequencyHz, uint32_t pulseNs, ActualPwm &actual);
|
||||
// Hold the optical level selected as the active TX pulse.
|
||||
void active();
|
||||
// Hold actual optical light ON, independently of HH/HL/LH/LL.
|
||||
void lightOn();
|
||||
void stop();
|
||||
bool running() const { return running_; }
|
||||
private:
|
||||
bool running_ = false;
|
||||
bool activeLightOn_ = true;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "Receiver.h"
|
||||
#include "Config.h"
|
||||
#include <string.h>
|
||||
#if !OPTICAL_USE_RMT_DMA
|
||||
#include "Log.h"
|
||||
#include <math.h>
|
||||
#if !OPTICAL_USE_MCPWM_CAPTURE
|
||||
#include <esp_cpu.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#endif
|
||||
|
||||
uint32_t PulseReceiver::tickHz() const {
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
return captureResolutionHz_;
|
||||
#else
|
||||
return cpuTickHz_;
|
||||
@@ -15,31 +15,57 @@ uint32_t PulseReceiver::tickHz() 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 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];
|
||||
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) return 0;
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
return captureResolutionHz_ ? captureResolutionHz_ :
|
||||
MCPWM_CAPTURE_RESOLUTION_HZ;
|
||||
#else
|
||||
(void)expectedHz; (void)expectedDutyPct;
|
||||
return cpuTickHz_;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PulseReceiver::begin() {
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
|
||||
return queue_ && configureRmt(CAPTURE_RESOLUTION_OPTIONS_HZ[0]);
|
||||
#else
|
||||
queue_ = xQueueCreate(256, sizeof(Edge));
|
||||
queue_ = xQueueCreate(512, sizeof(Edge));
|
||||
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;
|
||||
// ACK pulses are sub-microsecond and consecutive acknowledgements can be
|
||||
// only 1 us apart. A low-priority capture interrupt can leave the channel
|
||||
// status pending long enough for the next timestamp to overwrite it.
|
||||
channelConfig.intr_priority = 3;
|
||||
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;
|
||||
if (mcpwm_capture_channel_register_event_callbacks(
|
||||
fallingChannel_, &callbacks, this) != ESP_OK) return false;
|
||||
|
||||
// The TX channel is created immediately before a driver test. Only the end
|
||||
// of the active PWM pulse is armed; handling its start here would occupy the
|
||||
// shared MCPWM ISR during the RX acknowledgement only ~300 ns later.
|
||||
return true;
|
||||
#else
|
||||
pinMode(GPIO_RX, INPUT);
|
||||
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
|
||||
@@ -47,178 +73,402 @@ bool PulseReceiver::begin() {
|
||||
#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
|
||||
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
|
||||
bool activeLightOn) {
|
||||
(void)activeLightOn;
|
||||
if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
|
||||
expectedHz_ = expectedHz;
|
||||
expectedDutyPct_ = expectedDutyPct;
|
||||
#if !OPTICAL_USE_MCPWM_CAPTURE
|
||||
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||
if (!cpuTickHz_) return false;
|
||||
#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;
|
||||
resetStream();
|
||||
Log::event("CAPTURE", "RX optical polarity will be detected automatically");
|
||||
return startCapture(false);
|
||||
}
|
||||
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
bool PulseReceiver::configureDriverTxCapture(bool risingEdge) {
|
||||
if (running_) return false;
|
||||
if (txChannel_) {
|
||||
if (mcpwm_del_capture_channel(txChannel_) != ESP_OK) return false;
|
||||
txChannel_ = nullptr;
|
||||
}
|
||||
captureResolutionHz_ = resolutionHz;
|
||||
return true;
|
||||
mcpwm_capture_channel_config_t config = {};
|
||||
config.gpio_num = GPIO_PWM;
|
||||
config.intr_priority = 3;
|
||||
config.prescale = 1;
|
||||
config.flags.pos_edge = risingEdge;
|
||||
config.flags.neg_edge = !risingEdge;
|
||||
config.flags.io_loop_back = true;
|
||||
if (mcpwm_new_capture_channel(captureTimer_, &config, &txChannel_) != ESP_OK)
|
||||
return false;
|
||||
mcpwm_capture_event_callbacks_t callbacks = {};
|
||||
callbacks.on_cap = onCapture;
|
||||
return mcpwm_capture_channel_register_event_callbacks(
|
||||
txChannel_, &callbacks, this) == ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
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
|
||||
bool PulseReceiver::startDriver(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
bool activeTxLightOn) {
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
if (!frequencyHz || !pulseNs || !tickHz() || running_) return false;
|
||||
resetStream();
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
// In partial RX mode the callback is delivered when this user buffer fills.
|
||||
// Keep chunks near 5 ms so low-frequency input is reported before NO SIGNAL.
|
||||
uint64_t symbols = (static_cast<uint64_t>(expectedHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL;
|
||||
if (symbols < RMT_MIN_RECEIVE_SYMBOLS) symbols = RMT_MIN_RECEIVE_SYMBOLS;
|
||||
if (symbols > RMT_MAX_RECEIVE_SYMBOLS) symbols = RMT_MAX_RECEIVE_SYMBOLS;
|
||||
receiveChunkSymbols_ = static_cast<uint16_t>(symbols);
|
||||
if (rmt_enable(channel_) != ESP_OK) 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;
|
||||
}
|
||||
const uint64_t pulseTicks =
|
||||
(static_cast<uint64_t>(pulseNs) * tickHz() + 500000000ULL) /
|
||||
1000000000ULL;
|
||||
const uint32_t periodTicks = tickHz() / frequencyHz;
|
||||
if (!pulseTicks || pulseTicks >= periodTicks || pulseTicks > UINT32_MAX)
|
||||
return false;
|
||||
driverPulseTicks_ = static_cast<uint32_t>(pulseTicks);
|
||||
driverReleaseSlackTicks_ = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(DRIVER_RESPONSE_TIMEOUT_NS) * tickHz() +
|
||||
999999999ULL) / 1000000000ULL);
|
||||
const uint8_t activeRawLevel = activeTxLightOn ?
|
||||
TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL;
|
||||
const bool pulseEndIsRising = activeRawLevel == LOW;
|
||||
if (!driverReleaseSlackTicks_ ||
|
||||
!configureDriverTxCapture(pulseEndIsRising)) return false;
|
||||
return startCapture(true);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
running_ = true; return true;
|
||||
}
|
||||
|
||||
bool PulseReceiver::startCapture(bool withTx) {
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
// Progress updates keep one capture session alive. Pulse-width stages stop
|
||||
// capture only after PWM is quiet, so resetStream never races the ISR.
|
||||
if (running_) return true;
|
||||
if (mcpwm_capture_timer_enable(captureTimer_) != ESP_OK) return false;
|
||||
if (mcpwm_capture_channel_enable(risingChannel_) != ESP_OK) {
|
||||
mcpwm_capture_timer_disable(captureTimer_);
|
||||
return false;
|
||||
}
|
||||
if (mcpwm_capture_channel_enable(fallingChannel_) != ESP_OK) {
|
||||
mcpwm_capture_channel_disable(risingChannel_);
|
||||
mcpwm_capture_timer_disable(captureTimer_);
|
||||
return false;
|
||||
}
|
||||
if (withTx && mcpwm_capture_channel_enable(txChannel_) != ESP_OK) {
|
||||
mcpwm_capture_channel_disable(fallingChannel_);
|
||||
mcpwm_capture_channel_disable(risingChannel_);
|
||||
mcpwm_capture_timer_disable(captureTimer_);
|
||||
return false;
|
||||
}
|
||||
txCaptureEnabled_ = withTx;
|
||||
running_ = true;
|
||||
if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) {
|
||||
running_ = false;
|
||||
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
|
||||
txCaptureEnabled_ = false;
|
||||
mcpwm_capture_channel_disable(fallingChannel_);
|
||||
mcpwm_capture_channel_disable(risingChannel_);
|
||||
mcpwm_capture_timer_disable(captureTimer_);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (withTx) return false;
|
||||
running_ = true;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void PulseReceiver::stop() {
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
if (running_) rmt_disable(channel_);
|
||||
#endif
|
||||
const bool wasRunning = running_;
|
||||
running_ = false;
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
if (wasRunning) {
|
||||
// Mask capture interrupts before stopping the shared capture timer.
|
||||
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
|
||||
txCaptureEnabled_ = false;
|
||||
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() {
|
||||
if (queue_) xQueueReset(queue_);
|
||||
overflow_ = false; droppedItems_ = 0; haveRise_ = haveFall_ = haveRawTick_ = false;
|
||||
lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0;
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0;
|
||||
#endif
|
||||
haveReorderEdge_ = false;
|
||||
__atomic_store_n(&driverRingWrite_, 0U, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&driverRingRead_, 0U, __ATOMIC_RELEASE);
|
||||
haveLastDriverEdge_ = false;
|
||||
lastDriverEdge_ = {};
|
||||
driverPulseTicks_ = 0;
|
||||
driverReleaseSlackTicks_ = 0;
|
||||
droppedItems_ = 0;
|
||||
polarityKnown_ = false;
|
||||
activeStartRising_ = false;
|
||||
polarityEdgeCount_ = 0;
|
||||
memset(polarityEdges_, 0, sizeof(polarityEdges_));
|
||||
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)
|
||||
tickEpoch_ += 0x100000000ULL;
|
||||
haveRawTick_ = true; lastRawTick_ = e.tick;
|
||||
const uint64_t tick = tickEpoch_ + e.tick;
|
||||
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;
|
||||
return {tickEpoch_ + e.tick, e.rising != 0};
|
||||
}
|
||||
|
||||
bool PulseReceiver::overflowed() {
|
||||
const bool value = overflow_; overflow_ = false; return value;
|
||||
bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Optical mode does not use the DRIVER level setting. Compare the first two
|
||||
// alternating intervals with the configured active duration and select the
|
||||
// level that is actually present at RX. Three edges are enough to determine
|
||||
// polarity and, when the first interval is active, produce the first period.
|
||||
polarityEdges_[polarityEdgeCount_++] = edge;
|
||||
if (polarityEdgeCount_ < 3U) return false;
|
||||
|
||||
const uint64_t firstInterval =
|
||||
polarityEdges_[1].tick - polarityEdges_[0].tick;
|
||||
const uint64_t secondInterval =
|
||||
polarityEdges_[2].tick - polarityEdges_[1].tick;
|
||||
const uint64_t expectedPeriod = tickHz() / expectedHz_;
|
||||
const uint64_t expectedActive = static_cast<uint64_t>(
|
||||
expectedPeriod * expectedDutyPct_ / 100.0f + 0.5f);
|
||||
const uint64_t firstError = firstInterval > expectedActive ?
|
||||
firstInterval - expectedActive : expectedActive - firstInterval;
|
||||
const uint64_t secondError = secondInterval > expectedActive ?
|
||||
secondInterval - expectedActive : expectedActive - secondInterval;
|
||||
const bool firstIntervalIsActive = firstError <= secondError;
|
||||
activeStartRising_ = firstIntervalIsActive ? polarityEdges_[0].rising :
|
||||
polarityEdges_[1].rising;
|
||||
polarityKnown_ = true;
|
||||
polarityEdgeCount_ = 0;
|
||||
|
||||
if (firstIntervalIsActive) {
|
||||
out = {polarityEdges_[0].tick,
|
||||
static_cast<uint32_t>(polarityEdges_[2].tick - polarityEdges_[0].tick),
|
||||
static_cast<uint32_t>(firstInterval), tickHz()};
|
||||
activeStart_ = polarityEdges_[2].tick;
|
||||
waitingForActiveEnd_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
activeStart_ = polarityEdges_[1].tick;
|
||||
activeEnd_ = polarityEdges_[2].tick;
|
||||
waitingForActiveEnd_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t PulseReceiver::takeDroppedItems() {
|
||||
return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) {
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
|
||||
const mcpwm_capture_event_data_t *data,
|
||||
void *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),
|
||||
static_cast<uint8_t>(channel == self->txChannel_ ? CaptureSource::TX :
|
||||
CaptureSource::RX)};
|
||||
if (self->txCaptureEnabled_) {
|
||||
// All channels in one MCPWM group are dispatched serially by the same
|
||||
// group ISR. Keep this callback shorter than the minimum interval between
|
||||
// equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic
|
||||
// RMW operations here can leave the channel pending until its capture
|
||||
// register is overwritten by the next edge.
|
||||
if (self->haveLastDriverEdge_ &&
|
||||
self->lastDriverEdge_.tick == edge.tick &&
|
||||
self->lastDriverEdge_.rising == edge.rising &&
|
||||
self->lastDriverEdge_.source == edge.source) {
|
||||
// The same channel callback can be delivered twice while several MCPWM
|
||||
// capture status bits are pending. Two physical edges cannot have the
|
||||
// same source, direction and 12.5 ns hardware timestamp.
|
||||
return false;
|
||||
}
|
||||
const uint16_t write = self->driverRingWrite_;
|
||||
const uint16_t next = static_cast<uint16_t>(
|
||||
(write + 1U) & (DRIVER_RING_CAPACITY - 1U));
|
||||
if (next == self->driverRingRead_) {
|
||||
++self->droppedItems_;
|
||||
} else {
|
||||
self->driverRing_[write] = edge;
|
||||
self->lastDriverEdge_ = edge;
|
||||
self->haveLastDriverEdge_ = true;
|
||||
asm volatile("memw" ::: "memory");
|
||||
self->driverRingWrite_ = next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
BaseType_t wake = pdFALSE;
|
||||
size_t offset = 0;
|
||||
while (offset < data->num_symbols) {
|
||||
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;
|
||||
}
|
||||
if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
|
||||
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
|
||||
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
|
||||
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
|
||||
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
|
||||
bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
|
||||
if (RX_ACTIVE_LEVEL == LOW) level = !level;
|
||||
Edge e = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)};
|
||||
if (!self->running_) return;
|
||||
const bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
|
||||
const Edge edge = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level),
|
||||
static_cast<uint8_t>(CaptureSource::RX)};
|
||||
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);
|
||||
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;
|
||||
Edge e;
|
||||
while (count < capacity && xQueueReceive(queue_, &e, count ? 0 : waitTicks) == pdTRUE)
|
||||
if (consumeEdge(e, periods[count])) ++count;
|
||||
Edge edge = {};
|
||||
while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) {
|
||||
if (consumeEdge(edge, periods[count])) {
|
||||
periods[count].activeTickHz = tickHz();
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
||||
TickType_t waitTicks) {
|
||||
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
|
||||
!driverReleaseSlackTicks_) return 0;
|
||||
|
||||
constexpr size_t MAX_BATCH = 64;
|
||||
const size_t limit = capacity < MAX_BATCH ? capacity : MAX_BATCH;
|
||||
Edge ordered[MAX_BATCH] = {};
|
||||
|
||||
uint16_t read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED);
|
||||
if (read == __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
|
||||
vTaskDelay(waitTicks);
|
||||
read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED);
|
||||
}
|
||||
// Work on one immutable producer snapshot. RX belonging to a pulse start is
|
||||
// deliberately retained until that pulse's captured end arrives: only then
|
||||
// can the missing start interrupt be reconstructed and sorted before RX.
|
||||
const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE);
|
||||
uint16_t scan = read;
|
||||
bool haveLatestTxEnd = false;
|
||||
bool haveReleaseTxEnd = false;
|
||||
uint32_t latestTxEnd = 0;
|
||||
uint32_t releaseTxEnd = 0;
|
||||
size_t projectedCount = 0;
|
||||
while (scan != write) {
|
||||
const Edge &edge = driverRing_[scan];
|
||||
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
|
||||
? 2U : 1U;
|
||||
// Reserve one output slot for WINDOW_END.
|
||||
if (projectedCount + needed + 1U > limit) break;
|
||||
projectedCount += needed;
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
|
||||
if (haveLatestTxEnd) {
|
||||
releaseTxEnd = latestTxEnd;
|
||||
haveReleaseTxEnd = true;
|
||||
}
|
||||
latestTxEnd = edge.tick;
|
||||
haveLatestTxEnd = true;
|
||||
}
|
||||
scan = static_cast<uint16_t>((scan + 1U) % DRIVER_RING_CAPACITY);
|
||||
}
|
||||
// Keep the newest TX period in the ring. Arrival of the following TX end
|
||||
// proves that the previous end's ACK/fault window has completely elapsed.
|
||||
if (!haveReleaseTxEnd) {
|
||||
if (waitTicks) vTaskDelay(waitTicks);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_;
|
||||
size_t count = 0;
|
||||
while (read != write) {
|
||||
const Edge edge = driverRing_[read];
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::TX) &&
|
||||
static_cast<int32_t>(edge.tick - releaseTxEnd) > 0)
|
||||
break;
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::RX) &&
|
||||
static_cast<int32_t>(edge.tick - releaseThrough) > 0)
|
||||
break;
|
||||
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
|
||||
? 2U : 1U;
|
||||
if (count + needed > limit) break;
|
||||
read = static_cast<uint16_t>((read + 1U) % DRIVER_RING_CAPACITY);
|
||||
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
|
||||
Edge pulseStart = edge;
|
||||
pulseStart.tick -= driverPulseTicks_;
|
||||
pulseStart.rising = !edge.rising;
|
||||
ordered[count++] = pulseStart;
|
||||
}
|
||||
ordered[count++] = edge;
|
||||
}
|
||||
__atomic_store_n(&driverRingRead_, read, __ATOMIC_RELEASE);
|
||||
|
||||
// MCPWM channels share one timer but their callbacks can be dispatched in
|
||||
// channel order when several interrupts are pending. Restore the hardware
|
||||
// order inside the captured batch using the common timestamp.
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
const Edge key = ordered[i];
|
||||
size_t j = i;
|
||||
while (j && static_cast<int32_t>(ordered[j - 1].tick - key.tick) > 0) {
|
||||
ordered[j] = ordered[j - 1];
|
||||
--j;
|
||||
}
|
||||
ordered[j] = key;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const TimedEdge timed = extendEdge(ordered[i]);
|
||||
events[i] = {timed.tick, timed.rising,
|
||||
static_cast<CaptureSource>(ordered[i].source)};
|
||||
}
|
||||
const Edge marker = {releaseThrough, 0,
|
||||
static_cast<uint8_t>(CaptureSource::WINDOW_END)};
|
||||
const TimedEdge timedMarker = extendEdge(marker);
|
||||
events[count++] = {timedMarker.tick, false, CaptureSource::WINDOW_END};
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,66 +1,90 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <driver/gpio.h>
|
||||
#include "Config.h"
|
||||
#include "Core.h"
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
#define OPTICAL_USE_RMT_DMA 1
|
||||
#include <driver/rmt_rx.h>
|
||||
#if CONFIG_IDF_TARGET_ESP32S3 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
#define OPTICAL_USE_MCPWM_CAPTURE 1
|
||||
#include <driver/mcpwm_cap.h>
|
||||
#else
|
||||
#define OPTICAL_USE_RMT_DMA 0
|
||||
#include <driver/gpio.h>
|
||||
#define OPTICAL_USE_MCPWM_CAPTURE 0
|
||||
#endif
|
||||
|
||||
enum class CaptureSource : uint8_t { RX, TX, WINDOW_END };
|
||||
|
||||
struct CaptureEvent {
|
||||
uint64_t tick;
|
||||
bool rising;
|
||||
CaptureSource source;
|
||||
};
|
||||
|
||||
class PulseReceiver {
|
||||
public:
|
||||
bool begin();
|
||||
bool start(uint32_t expectedHz, float expectedDutyPct);
|
||||
bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
|
||||
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
|
||||
bool activeTxLightOn);
|
||||
void stop();
|
||||
void resetStream();
|
||||
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
|
||||
bool overflowed();
|
||||
size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
|
||||
uint32_t takeDroppedItems();
|
||||
uint32_t tickHz() const;
|
||||
uint32_t pulseTickHz() const { return tickHz(); }
|
||||
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
|
||||
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
||||
bool highRateBackend() const {
|
||||
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
uint32_t plannedPulseTickHz(uint32_t expectedHz, float expectedDutyPct) const {
|
||||
return plannedTickHz(expectedHz, expectedDutyPct);
|
||||
}
|
||||
private:
|
||||
struct Edge { uint32_t tick; uint8_t rising; };
|
||||
bool consumeEdge(const Edge &edge, PulsePeriod &period);
|
||||
uint16_t receiveChunkSymbols() const { return 1; }
|
||||
bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; }
|
||||
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_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 *);
|
||||
bool configureRmt(uint32_t resolutionHz);
|
||||
bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
|
||||
rmt_channel_handle_t channel_ = nullptr;
|
||||
private:
|
||||
struct Edge { uint32_t tick; uint8_t rising; uint8_t source; };
|
||||
static constexpr uint16_t DRIVER_RING_CAPACITY = 512;
|
||||
static_assert((DRIVER_RING_CAPACITY & (DRIVER_RING_CAPACITY - 1U)) == 0,
|
||||
"driver capture ring must be a power of two");
|
||||
struct TimedEdge { uint64_t tick; bool rising; };
|
||||
bool startCapture(bool withTx);
|
||||
bool consumeEdge(const Edge &edge, PulsePeriod &period);
|
||||
bool nextOrderedEdge(Edge &edge, TickType_t waitTicks);
|
||||
TimedEdge extendEdge(const Edge &edge);
|
||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||
bool configureDriverTxCapture(bool risingEdge);
|
||||
static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t,
|
||||
const mcpwm_capture_event_data_t *, void *);
|
||||
mcpwm_cap_timer_handle_t captureTimer_ = nullptr;
|
||||
mcpwm_cap_channel_handle_t risingChannel_ = nullptr;
|
||||
mcpwm_cap_channel_handle_t fallingChannel_ = nullptr;
|
||||
mcpwm_cap_channel_handle_t txChannel_ = nullptr;
|
||||
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
|
||||
static void IRAM_ATTR onGpio(void *ctx);
|
||||
uint32_t cpuTickHz_ = 0;
|
||||
#endif
|
||||
QueueHandle_t queue_ = nullptr;
|
||||
volatile bool overflow_ = false;
|
||||
Edge driverRing_[DRIVER_RING_CAPACITY] = {};
|
||||
volatile uint16_t driverRingWrite_ = 0;
|
||||
volatile uint16_t driverRingRead_ = 0;
|
||||
portMUX_TYPE driverRingMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
Edge lastDriverEdge_ = {};
|
||||
bool haveLastDriverEdge_ = false;
|
||||
uint32_t driverPulseTicks_ = 0;
|
||||
uint32_t driverReleaseSlackTicks_ = 0;
|
||||
Edge reorderEdge_ = {};
|
||||
bool haveReorderEdge_ = false;
|
||||
volatile uint32_t droppedItems_ = 0;
|
||||
bool running_ = false;
|
||||
bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false;
|
||||
volatile bool running_ = false;
|
||||
volatile bool txCaptureEnabled_ = false;
|
||||
uint32_t expectedHz_ = 0;
|
||||
float expectedDutyPct_ = 50.0f;
|
||||
bool polarityKnown_ = false, activeStartRising_ = false;
|
||||
TimedEdge polarityEdges_[3] = {};
|
||||
uint8_t polarityEdgeCount_ = 0;
|
||||
bool waitingForActiveEnd_ = true;
|
||||
uint64_t activeStart_ = 0, activeEnd_ = 0;
|
||||
bool haveRawTick_ = false;
|
||||
uint32_t lastRawTick_ = 0;
|
||||
uint64_t tickEpoch_ = 0, rise_ = 0, fall_ = 0;
|
||||
uint64_t tickEpoch_ = 0;
|
||||
};
|
||||
|
||||
@@ -3,20 +3,31 @@
|
||||
#include "Log.h"
|
||||
#include <Preferences.h>
|
||||
|
||||
namespace { constexpr uint16_t SETTINGS_VERSION = 4; constexpr char NAMESPACE[] = "opt-test"; }
|
||||
namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
|
||||
|
||||
void SettingsStore::defaults(Settings &s) const {
|
||||
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 2, 3, 2, 0};
|
||||
// 2 kHz, 200 us .. 2 us, 5%, 1 s.
|
||||
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO),
|
||||
static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH),
|
||||
2, 6, 3, 2, 3, 0, 0};
|
||||
s.checksum = settingsChecksum(s);
|
||||
}
|
||||
|
||||
bool SettingsStore::valid(const Settings &s) const {
|
||||
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.testKind <= static_cast<uint8_t>(TestKind::DRIVER) &&
|
||||
s.lightCode <= static_cast<uint8_t>(LightCode::LL) &&
|
||||
(s.testKind != static_cast<uint8_t>(TestKind::DRIVER) ||
|
||||
s.role == static_cast<uint8_t>(Role::SOLO)) &&
|
||||
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.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) &&
|
||||
s.checksum == settingsChecksum(s) &&
|
||||
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
|
||||
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) &&
|
||||
s.checksum == settingsChecksum(s);
|
||||
}
|
||||
|
||||
bool SettingsStore::load(Settings &s) {
|
||||
@@ -39,7 +50,7 @@ bool SettingsStore::save(Settings &s) {
|
||||
}
|
||||
|
||||
TestParams SettingsStore::params(const Settings &s) const {
|
||||
return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex],
|
||||
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex],
|
||||
DUTY_OPTIONS_PCT[s.dutyIndex]};
|
||||
return {PWM_FREQUENCY_OPTIONS_HZ[s.frequencyIndex],
|
||||
MAX_PULSE_OPTIONS_NS[s.maxPulseIndex], MIN_PULSE_OPTIONS_NS[s.minPulseIndex],
|
||||
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex]};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user