добавлена бета проверка драйверов

This commit is contained in:
2026-08-14 12:01:07 +03:00
parent a17e8962b4
commit 037bb37e62
20 changed files with 1796 additions and 156 deletions

1
.gitignore vendored
View File

@@ -2,5 +2,6 @@
__Previews/ __Previews/
History History
Project Logs*/ Project Logs*/
/.build/

View File

@@ -10,13 +10,14 @@
#include <driver/gpio.h> #include <driver/gpio.h>
#include <Wire.h> #include <Wire.h>
#include <math.h> #include <math.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
namespace { namespace {
const char *uiFailName(FailReason reason); const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) { const char *appStateName(AppState state) {
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER", static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START", "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"}; "SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
const uint8_t index = static_cast<uint8_t>(state); const uint8_t index = static_cast<uint8_t>(state);
@@ -54,7 +55,7 @@ void formatMeasured(float hz, uint32_t pulseNs, char *out, size_t size) {
void formatTestTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) { void formatTestTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char target[32]; char target[32];
formatTarget(hz, pulseNs, target, sizeof(target)); formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, "TEST: %s", target); snprintf(out, size, UiText::TEST_TARGET_FORMAT, target);
} }
void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs, void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
@@ -62,7 +63,16 @@ void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
(void)reason; (void)reason;
char target[32]; char target[32];
formatTarget(hz, pulseNs, target, sizeof(target)); formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, "FAIL AT %s", target); snprintf(out, size, UiText::FAIL_TARGET_FORMAT, target);
}
void formatElapsedNs(uint64_t ns, char *out, size_t size) {
// Compact form keeps `T:... D:... P:...` 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) { size_t utf8CharacterCount(const char *text) {
@@ -116,6 +126,12 @@ uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL); return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
} }
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] : "?";
}
uint8_t lastValidMaxPulseIndex(uint32_t frequencyHz) { uint8_t lastValidMaxPulseIndex(uint32_t frequencyHz) {
uint8_t last = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS) - 1U); 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) while (last && static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[last]) * frequencyHz >= 1000000000ULL)
@@ -194,9 +210,41 @@ uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) {
if (direction > 0) return value >= last ? first : static_cast<uint8_t>(value + 1U); if (direction > 0) return value >= last ? first : static_cast<uint8_t>(value + 1U);
return value <= first ? last : 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;
} }
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {} 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() { void App::begin() {
Serial.begin(SERIAL_BAUD); Serial.begin(SERIAL_BAUD);
@@ -221,6 +269,7 @@ void App::finishInitialization(bool factoryReset) {
} }
sanitizeRange(); sanitizeRange();
params_ = store_.params(settings_); params_ = store_.params(settings_);
pwm_.configureActiveLight(txActiveLightOn(settings_));
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational"); if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
initialized_ = true; initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
@@ -233,6 +282,7 @@ void App::finishInitialization(bool factoryReset) {
} }
void App::update() { void App::update() {
serviceSerialConsole();
serviceIdlePowerSave(); serviceIdlePowerSave();
const uint32_t now = millis(); const uint32_t now = millis();
const ButtonEvent startEvent = startButton_.update(now); const ButtonEvent startEvent = startButton_.update(now);
@@ -258,11 +308,14 @@ void App::update() {
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) { if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
if (modeEvent == ButtonEvent::SHORT) { 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_); params_ = store_.params(settings_);
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave(); if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle(); 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) { } else if (modeEvent == ButtonEvent::LONG) {
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu(); state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); } } else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
@@ -273,7 +326,9 @@ void App::update() {
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) { if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
radio_.end(); havePeer_ = false; radio_.end(); havePeer_ = false;
if (modeEvent == ButtonEvent::SHORT) { 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(); params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED"); Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) { } else if (modeEvent == ButtonEvent::LONG) {
@@ -283,7 +338,7 @@ void App::update() {
} }
if (state_ == AppState::MENU) { if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) { 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) { else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
@@ -316,6 +371,39 @@ void App::update() {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) showStageResult(live); if (measurement_.statsSnapshot(live)) showStageResult(live);
} }
} 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 || } else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) { state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
handleRadio(); updateMaster(); handleRadio(); updateMaster();
@@ -328,14 +416,207 @@ void App::showIdle() {
setActivePerformance(false); setActivePerformance(false);
setStandbyOpticalOutput(); setStandbyOpticalOutput();
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX, char one[64]; snprintf(one, sizeof(one), "%s: %s",
uiRoleName(static_cast<Role>(settings_.role))); uiRoleName(static_cast<Role>(settings_.role)),
uiTestName(static_cast<TestKind>(settings_.testKind)));
display_.show(one, UiText::START_RUN); 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");
}
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,
lightCodeName(static_cast<LightCode>(settings_.lightCode)),
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(txActiveLightOn(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 (!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() { void App::sanitizeRange() {
if (settings_.role > static_cast<uint8_t>(Role::SLAVE)) if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
settings_.role = static_cast<uint8_t>(Role::SOLO); 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_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS); settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS);
settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS); settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS);
@@ -346,9 +627,15 @@ void App::sanitizeRange() {
if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid; if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid;
const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]); const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin; if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin;
const uint8_t firstMin = firstMinPulseIndexAtLeast( if (settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)) {
minimumPulseForAccuracy(hz, ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), lastMin); const uint8_t driverLastMin = lastMinPulseIndexAtMost(
if (settings_.minPulseIndex < firstMin) settings_.minPulseIndex = firstMin; MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t firstDriverMin = firstMinPulseIndexAtLeast(
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
if (settings_.minPulseIndex < firstDriverMin)
settings_.minPulseIndex = firstDriverMin;
settings_.lightCode = static_cast<uint8_t>(LightCode::HL);
}
} }
void App::serviceRxPinStateLog() { void App::serviceRxPinStateLog() {
@@ -376,25 +663,26 @@ void App::changeMenu(int d) {
} else if (menuItem_ == 2) { } else if (menuItem_ == 2) {
const uint8_t last = lastMinPulseIndexAtMost( const uint8_t last = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]); MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t first = firstMinPulseIndexAtLeast( const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
minimumPulseForAccuracy(PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex], ? firstMinPulseIndexAtLeast(DRIVER_MIN_INPUT_PULSE_NS, last) : 0U;
ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), last); settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex, first, last, d);
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex,
first, last, d);
} else { } else {
uint8_t *value = nullptr; size_t count = 0; uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) { switch (menuItem_) {
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break; case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); 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 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 5: value = &settings_.lightCode; count = 4; break;
default: return; default: return;
} }
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d); *value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
} }
sanitizeRange(); params_ = store_.params(settings_); sanitizeRange(); params_ = store_.params(settings_);
Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u", pwm_.configureActiveLight(txActiveLightOn(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, menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex); settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
lightCodeName(static_cast<LightCode>(settings_.lightCode)));
showMenu(); showMenu();
} }
@@ -423,6 +711,15 @@ void App::showMenu() {
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f); snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = UiText::MENU_TEST_TIME; label = UiText::MENU_TEST_TIME;
break; break;
case 5: {
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; default: return;
} }
formatMenuLine(label, value, one, sizeof(one)); formatMenuLine(label, value, one, sizeof(one));
@@ -434,23 +731,29 @@ void App::startTest() {
leaveIdlePowerSave(); leaveIdlePowerSave();
pwm_.stop(); pwm_.stop();
setActivePerformance(true); setActivePerformance(true);
sanitizeRange();
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs); params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE; stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; } if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_); Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
lightCodeName(static_cast<LightCode>(settings_.lightCode)), stageCount_);
if (SERIAL_MINIMAL_LOG) { if (SERIAL_MINIMAL_LOG) {
Log::printf("CONFIG", "mode=%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums TX=%s RX=AUTO stages=%lu", 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)), params_.frequencyHz, roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs, params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
PWM_ACTIVE_LEVEL == HIGH ? "HIGH" : "LOW", lightCodeName(static_cast<LightCode>(settings_.lightCode)),
stageCount_); stageCount_);
} }
printConfiguration(); printConfiguration();
const Role role = static_cast<Role>(settings_.role); const Role role = static_cast<Role>(settings_.role);
if (role == Role::SOLO) { if (role == Role::SOLO) {
if (!prepareStage()) return; 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 (!radio_.begin()) finish(false, FailReason::LINK_LOST);
else if (role == Role::MASTER) startMasterDiscovery(); 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); } else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
@@ -498,24 +801,31 @@ bool App::prepareStage(bool showProgress) {
params_.accuracyPct); params_.accuracyPct);
finish(false, FailReason::RESOLUTION); return false; finish(false, FailReason::RESOLUTION); return false;
} }
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct); const bool driverMode = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz( if (!driverMode) {
actual_.actualHz, actual_.actualDutyPct); const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct, const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
plannedRxHz, plannedPulseRxHz, actual_.bits, actual_.actualHz, actual_.actualDutyPct);
MEASUREMENT_AVERAGING_PERIODS); const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct,
if (resolution != FailReason::NONE) { params_.accuracyPct, plannedRxHz, plannedPulseRxHz, actual_.bits,
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%", MEASUREMENT_AVERAGING_PERIODS);
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, plannedPulseRxHz, if (resolution != FailReason::NONE) {
effectiveTolerancePct(params_.accuracyPct)); Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
finish(false, resolution); return false; 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", Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz, stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits); actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
if (showProgress) showStageProgress(); if (showProgress) showStageProgress();
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { if (static_cast<Role>(settings_.role) == Role::SOLO) {
finish(false, FailReason::UNSUPPORTED); return false; const bool started = driverMode ? startDriverMeasurement() :
startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct);
if (!started) { finish(false, FailReason::UNSUPPORTED); return false; }
} }
return true; return true;
} }
@@ -526,7 +836,7 @@ bool App::startLocalMeasurement(float hz, float duty) {
receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty), receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs); PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES); MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, rxActiveLightOn(settings_));
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
static_cast<uint32_t>(hz + 0.5f)); static_cast<uint32_t>(hz + 0.5f));
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL; const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
@@ -536,6 +846,18 @@ bool App::startLocalMeasurement(float hz, float duty) {
return ok; 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() { void App::stagePassed() {
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_); Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
pwm_.stop(); pwm_.stop();
@@ -543,8 +865,16 @@ void App::stagePassed() {
// queue for the next pulse width. Never reset a FreeRTOS queue concurrently // queue for the next pulse width. Never reset a FreeRTOS queue concurrently
// with the capture ISR. // with the capture ISR.
if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop(); if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop();
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; } if (++stageIndex_ >= stageCount_) {
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; } 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) { else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = params_.frequencyHz; requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_); requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
@@ -561,7 +891,7 @@ void App::startMasterDiscovery() {
requestedPulseNs_ = 0; havePeer_ = false; radio_.flush(); requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
opticalWakeActive_ = true; opticalWakeActive_ = true;
lastOpticalWakeToggleMs_ = millis(); lastOpticalWakeToggleMs_ = millis();
pwm_.active(); pwm_.lightOn();
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_); pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = millis(); retries_ = 0; lastSendMs_ = millis(); retries_ = 0;
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_); state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
@@ -575,7 +905,8 @@ ProtocolPacket App::makePacket(MessageType type) const {
p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_; p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs; p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
p.testTimeMs = params_.testTimeMs; p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES; p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f);
p.lightCode = settings_.lightCode;
return p; return p;
} }
@@ -663,6 +994,8 @@ void App::handleRadio() {
state_ = AppState::SLAVE_WAIT_START; state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs; params_.testTimeMs = r.packet.testTimeMs;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; 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; requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
stageCount_ = r.packet.stageCount; stageCount_ = r.packet.stageCount;
actual_ = {}; actual_ = {};
@@ -753,7 +1086,7 @@ void App::updateMaster() {
if (state_ == AppState::MASTER_DISCOVER) { if (state_ == AppState::MASTER_DISCOVER) {
if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) { if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) {
opticalWakeActive_ = !opticalWakeActive_; opticalWakeActive_ = !opticalWakeActive_;
if (opticalWakeActive_) pwm_.active(); if (opticalWakeActive_) pwm_.lightOn();
else pwm_.stop(); else pwm_.stop();
lastOpticalWakeToggleMs_ = now; lastOpticalWakeToggleMs_ = now;
} }
@@ -836,7 +1169,10 @@ void App::sendAbort(FailReason reason) {
void App::abortTest() { void App::abortTest() {
Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM"); 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) { void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
@@ -903,17 +1239,39 @@ bool App::idlePowerSaveAllowed() const {
// Never enter blocking light sleep while the settings screen is open. A // Never enter blocking light sleep while the settings screen is open. A
// wake-up press is deliberately consumed by the button state machine, which // wake-up press is deliberately consumed by the button state machine, which
// is useful in IDLE but makes menu navigation appear frozen. // is useful in IDLE but makes menu navigation appear frozen.
return initialized_ && (state_ == AppState::IDLE || return !usbHostPresent() && initialized_ && (state_ == AppState::IDLE ||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY); 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() { 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(); else pwm_.active();
} }
void App::setActivePerformance(bool 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)) if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz); Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
} }
@@ -1020,13 +1378,18 @@ void App::printConfiguration() {
if (SERIAL_MINIMAL_LOG) return; if (SERIAL_MINIMAL_LOG) return;
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3"; const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA); 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)),
lightCodeName(static_cast<LightCode>(settings_.lightCode)));
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX, Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL); GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, RX AUTO\n", 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_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
params_.accuracyPct, params_.testTimeMs); params_.accuracyPct, params_.testTimeMs,
lightCodeName(static_cast<LightCode>(settings_.lightCode))[0],
lightCodeName(static_cast<LightCode>(settings_.lightCode))[1]);
stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
Serial.printf("Pulse widths descending (%lu): ", stageCount_); Serial.printf("Pulse widths descending (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i) for (uint32_t i = 0; i < stageCount_; ++i)
@@ -1093,6 +1456,66 @@ void App::showStageResult(const StageStats &s) {
overallProgressTotal(stageCount_)); 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] = "---", errorDelay[12] = "---", errorPulse[12] = "---";
if (s.errorElapsedTicks) {
const uint64_t elapsedNs =
(s.errorElapsedTicks * 1000000000ULL + driverTest_.tickHz() / 2U) /
driverTest_.tickHz();
formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed));
}
if (s.errorDelayValid) {
const uint64_t errorDelayNs =
(static_cast<uint64_t>(s.errorDelayTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
formatElapsedNs(errorDelayNs, errorDelay, sizeof(errorDelay));
}
if (s.errorPulseValid) {
const uint64_t errorPulseNs =
(static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
}
snprintf(two, sizeof(two), "T:%s D:%s P:%s",
elapsed, errorDelay, errorPulse);
} else {
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
delay, response);
}
display_.show(one, two,
overallProgress(stageIndex_, driverTest_.progressStep()),
overallProgressTotal(stageCount_), s.reason == FailReason::NONE ? nullptr :
roleCorner(Role::SOLO));
}
void App::showRemoteResult(const ProtocolPacket &packet) { void App::showRemoteResult(const ProtocolPacket &packet) {
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED) const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED; ? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
@@ -1135,6 +1558,15 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
} }
void App::showStageProgress() { void App::showStageProgress() {
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]; char one[64];
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one)); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0), display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),

View File

@@ -1,13 +1,14 @@
#pragma once #pragma once
#include "Buttons.h" #include "Buttons.h"
#include "Display.h" #include "Display.h"
#include "DriverTest.h"
#include "Measurement.h" #include "Measurement.h"
#include "Pwm.h" #include "Pwm.h"
#include "Radio.h" #include "Radio.h"
#include "SettingsStore.h" #include "SettingsStore.h"
enum class AppState : uint8_t { 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, MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
SLAVE_WAIT_ACK, FINISHED SLAVE_WAIT_ACK, FINISHED
}; };
@@ -22,11 +23,13 @@ class App {
void finishInitialization(bool factoryReset); void finishInitialization(bool factoryReset);
void showMenu(); void showMenu();
void changeMenu(int direction); void changeMenu(int direction);
void cycleRunMode();
void sanitizeRange(); void sanitizeRange();
void startTest(); void startTest();
bool armSlave(bool preserveDisplay = false); bool armSlave(bool preserveDisplay = false);
bool prepareStage(bool showProgress = true); bool prepareStage(bool showProgress = true);
bool startLocalMeasurement(float hz, float duty); bool startLocalMeasurement(float hz, float duty);
bool startDriverMeasurement();
void startMasterDiscovery(); void startMasterDiscovery();
void handleRadio(); void handleRadio();
void updateMaster(); void updateMaster();
@@ -38,6 +41,7 @@ class App {
void printConfiguration(); void printConfiguration();
void printStageStats(const StageStats &s, uint32_t hz); void printStageStats(const StageStats &s, uint32_t hz);
void showStageResult(const StageStats &s); void showStageResult(const StageStats &s);
void showDriverResult(const DriverStats &s, bool testPassed = false);
void showRemoteResult(const ProtocolPacket &packet); void showRemoteResult(const ProtocolPacket &packet);
void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const; void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const;
void showStageProgress(); void showStageProgress();
@@ -49,8 +53,15 @@ class App {
bool packetForCurrent(const ProtocolPacket &p) const; bool packetForCurrent(const ProtocolPacket &p) const;
void serviceIdlePowerSave(); void serviceIdlePowerSave();
void serviceRxPinStateLog(); void serviceRxPinStateLog();
void serviceSerialConsole();
void handleSerialCommand(char *line);
void printSerialHelp();
void printSerialStatus();
bool serialSettingsMutable() const;
void finishSerialSettingsChange();
void leaveIdlePowerSave(bool wakeDisplay = true); void leaveIdlePowerSave(bool wakeDisplay = true);
bool idlePowerSaveAllowed() const; bool idlePowerSaveAllowed() const;
bool usbHostPresent() const;
void setStandbyOpticalOutput(); void setStandbyOpticalOutput();
void setActivePerformance(bool active); void setActivePerformance(bool active);
@@ -62,6 +73,7 @@ class App {
PwmGenerator pwm_; PwmGenerator pwm_;
PulseReceiver receiver_; PulseReceiver receiver_;
Measurement measurement_; Measurement measurement_;
DriverTest driverTest_;
Radio radio_; Radio radio_;
AppState state_ = AppState::IDLE; AppState state_ = AppState::IDLE;
uint8_t menuItem_ = 0; uint8_t menuItem_ = 0;
@@ -88,4 +100,8 @@ class App {
uint32_t lastOpticalWakeToggleMs_ = 0; uint32_t lastOpticalWakeToggleMs_ = 0;
bool opticalWakeActive_ = false; bool opticalWakeActive_ = false;
bool rxPinStateKnown_ = false, rxPinState_ = false; bool rxPinStateKnown_ = false, rxPinState_ = false;
mutable uint32_t lastUsbHostSeenMs_ = 0;
char serialLine_[96] = {};
uint8_t serialLineLength_ = 0;
bool serialLineOverflow_ = false;
}; };

View File

@@ -74,15 +74,12 @@ constexpr bool SERIAL_LOG_TIMESTAMPS = true;
constexpr bool SERIAL_MINIMAL_LOG = true; constexpr bool SERIAL_MINIMAL_LOG = true;
#define BUTTON_ACTIVE_LEVEL LOW #define BUTTON_ACTIVE_LEVEL LOW
// Raw GPIO_RX level that means the optical receiver is active. // Fixed PCB conversion between electrical GPIO levels and actual optical
#define RX_ACTIVE_LEVEL HIGH // light. User settings HH/HL/LH/LL operate only in the optical domain and
// PWM_ACTIVE_LEVEL is the electrical level of the active test pulse and is // never change these hardware facts.
// also used for the constant active output while awake outside a test. During #define TX_LIGHT_ON_GPIO_LEVEL LOW
// the remainder of a running PWM period the output is !PWM_ACTIVE_LEVEL. #define RX_LIGHT_ON_GPIO_LEVEL LOW
// PWM_SAFE_LEVEL is used only while PWM is stopped and during sleep; it is #define TX_LIGHT_OFF_GPIO_LEVEL (TX_LIGHT_ON_GPIO_LEVEL == HIGH ? LOW : HIGH)
// independent of the PWM inactive level and may equal PWM_ACTIVE_LEVEL.
#define PWM_SAFE_LEVEL HIGH
#define PWM_ACTIVE_LEVEL LOW
#define PWM_SETTLE_CYCLES 5U #define PWM_SETTLE_CYCLES 5U
constexpr uint32_t BUTTON_DEBOUNCE_MS = 30; constexpr uint32_t BUTTON_DEBOUNCE_MS = 30;
@@ -95,8 +92,8 @@ constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
constexpr uint8_t LINK_PACKET_RETRIES = 10; constexpr uint8_t LINK_PACKET_RETRIES = 10;
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000; constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20; constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20;
// During discovery Master alternates PWM_ACTIVE_LEVEL and PWM_SAFE_LEVEL to // During discovery Master alternates actual optical light ON and OFF to wake
// wake a sleeping Slave through the optical channel. // a sleeping Slave through the optical channel.
constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50; constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50;
constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500; constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500; constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
@@ -114,6 +111,9 @@ constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15; constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000; 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_INTERVAL_MS = 100;
constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20; constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20;
static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS, static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS,
@@ -139,6 +139,22 @@ constexpr uint8_t LEDC_MAX_BITS = 14;
constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000; constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000;
constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535; constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535;
// 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;
// Any response this long is a fault, not a normal acknowledgement.
constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500;
// A short circuit is about 9 us. Gate-monitoring may be stretched by an
// overlapping turn-off ACK, but remains shorter on the tested driver.
constexpr uint32_t DRIVER_SHORT_CIRCUIT_MIN_NS = 6000;
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;
// -------------------------- Menu value arrays ----------------------------- // -------------------------- Menu value arrays -----------------------------
// The test uses one selected PWM frequency and walks the pulse-width list from // 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 // the selected maximum down to the selected minimum. Widths are stored in
@@ -147,13 +163,13 @@ constexpr uint32_t PWM_FREQUENCY_OPTIONS_HZ[] = {
500, 1000, 2000, 5000, 10000, 25000, 500, 1000, 2000, 5000, 10000, 25000,
}; };
constexpr uint32_t MAX_PULSE_OPTIONS_NS[] = { constexpr uint32_t MAX_PULSE_OPTIONS_NS[] = {
20000, 50000, 100000, 200000, 500000 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000
}; };
constexpr uint32_t MIN_PULSE_OPTIONS_NS[] = { constexpr uint32_t MIN_PULSE_OPTIONS_NS[] = {
250, 500, 1000, 2000, 5000, 10000 250, 500, 1000, 2000, 5000, 10000, 50000
}; };
constexpr uint32_t TEST_PULSE_WIDTHS_NS[] = { constexpr uint32_t TEST_PULSE_WIDTHS_NS[] = {
250, 500, 1000, 2000, 5000, 10000, 20000, 50000, 50, 100, 150, 200, 250, 500, 1000, 2000, 5000, 10000, 20000, 50000,
100000, 200000, 500000, 1000000 100000, 200000, 500000, 1000000
}; };
constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f}; constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f};

View File

@@ -17,6 +17,10 @@ constexpr const char *ROLE_NAMES[] = {
"СОЛО", "МАСТЕР", "СЛЕЙВ" "СОЛО", "МАСТЕР", "СЛЕЙВ"
}; };
constexpr const char *TEST_NAMES[] = {
"ОПТИКА", "ДРАЙВЕР"
};
constexpr const char *FAIL_NAMES[] = { constexpr const char *FAIL_NAMES[] = {
"НЕТ ОШИБКИ", "НЕТ ОШИБКИ",
"НЕТ СИГНАЛА", "НЕТ СИГНАЛА",
@@ -29,7 +33,13 @@ constexpr const char *FAIL_NAMES[] = {
"СВЯЗЬ ПОТЕРЯНА", "СВЯЗЬ ПОТЕРЯНА",
"РЕЖИМ НЕ ПОДДЕРЖИВ.", "РЕЖИМ НЕ ПОДДЕРЖИВ.",
"НЕ ХВАТАЕТ ТОЧНОСТИ", "НЕ ХВАТАЕТ ТОЧНОСТИ",
"ТЕСТ ОСТАНОВЛЕН" "ТЕСТ ОСТАНОВЛЕН",
"НЕТ ОТВЕТА ACK",
"ТАЙМИНГ ACK",
"АВАРИЯ ДРАЙВЕРА",
"ОТВЕТЫ ACK СЛИЛИСЬ",
"ОШИБКА ЗАТВОРА",
"КОРОТКОЕ ЗАМЫКАНИЕ"
}; };
constexpr const char *MODE_PREFIX = "РЕЖИМ: "; constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
@@ -40,6 +50,8 @@ constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:"; constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:";
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:"; constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
constexpr const char *FREQUENCY_UNIT = " Гц"; constexpr const char *FREQUENCY_UNIT = " Гц";
@@ -48,7 +60,7 @@ constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА"; constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ"; constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА"; 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 *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН"; constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ"; constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
@@ -57,9 +69,13 @@ constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН"; constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
constexpr const char *FAIL_FORMAT = "СБОЙ %s"; constexpr const char *FAIL_FORMAT = "СБОЙ %s";
constexpr const char *TEST_FORMAT = "%s, %s"; constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s"; constexpr const char *TEST_TARGET_FORMAT = "ТЕСТ: %s";
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %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 *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 #elif UI_LANGUAGE == UI_LANGUAGE_EN
@@ -67,6 +83,10 @@ constexpr const char *ROLE_NAMES[] = {
"SOLO", "MASTER", "SLAVE" "SOLO", "MASTER", "SLAVE"
}; };
constexpr const char *TEST_NAMES[] = {
"OPTICAL", "DRIVER"
};
constexpr const char *FAIL_NAMES[] = { constexpr const char *FAIL_NAMES[] = {
"NONE", "NONE",
"NO SIGNAL", "NO SIGNAL",
@@ -79,7 +99,13 @@ constexpr const char *FAIL_NAMES[] = {
"LINK LOST", "LINK LOST",
"UNSUPPORTED", "UNSUPPORTED",
"RESOLUTION", "RESOLUTION",
"ABORTED" "ABORTED",
"ACK MISSING",
"ACK TIMING",
"DRIVER FAULT",
"ACK MERGED",
"GATE FAULT",
"SHORT CIRCUIT FAULT"
}; };
constexpr const char *MODE_PREFIX = "MODE: "; constexpr const char *MODE_PREFIX = "MODE: ";
@@ -90,6 +116,8 @@ constexpr const char *MENU_MAX_PULSE = "MAX PULSE:";
constexpr const char *MENU_MIN_PULSE = "MIN PULSE:"; constexpr const char *MENU_MIN_PULSE = "MIN PULSE:";
constexpr const char *MENU_ACCURACY = "ACCURACY:"; constexpr const char *MENU_ACCURACY = "ACCURACY:";
constexpr const char *MENU_TEST_TIME = "TEST TIME:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:";
constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:";
constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:"; constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
constexpr const char *FREQUENCY_UNIT = " Hz"; constexpr const char *FREQUENCY_UNIT = " Hz";
@@ -107,9 +135,13 @@ constexpr const char *TEST_FAILED = "TEST FAILED";
constexpr const char *PASS_WORD = "TEST PASS"; constexpr const char *PASS_WORD = "TEST PASS";
constexpr const char *FAIL_FORMAT = "FAIL %s"; constexpr const char *FAIL_FORMAT = "FAIL %s";
constexpr const char *TEST_FORMAT = "%s, %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 *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s"; constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *NO_MEASUREMENT = "F:---, P:---"; 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 #else
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU" #error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"

View File

@@ -9,10 +9,24 @@ const char *roleName(Role r) {
return i < 3 ? names[i] : "?"; 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) { const char *failName(FailReason r) {
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT", static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED"}; "UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING",
"DRIVER FAULT", "ACK MERGED", "GATE MONITORING FAULT",
"SHORT CIRCUIT FAULT"};
const uint8_t i = static_cast<uint8_t>(r); const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN"; return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
} }
@@ -31,6 +45,14 @@ uint32_t settingsChecksum(const Settings &s) {
return hash; return hash;
} }
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) { uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs) {
if (!minPulseNs || maxPulseNs < minPulseNs) return 0; if (!minPulseNs || maxPulseNs < minPulseNs) return 0;
uint32_t count = 0; uint32_t count = 0;

View File

@@ -4,22 +4,31 @@
#include <stddef.h> #include <stddef.h>
enum class Role : uint8_t { SOLO, MASTER, SLAVE }; 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 { enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE, NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED,
GATE_MONITOR_FAULT, SHORT_CIRCUIT_FAULT
}; };
const char *roleName(Role role); const char *roleName(Role role);
const char *testKindName(TestKind kind);
const char *lightCodeName(LightCode code);
const char *failName(FailReason reason); const char *failName(FailReason reason);
struct Settings { struct Settings {
uint16_t version; uint16_t version;
uint8_t role; uint8_t role;
uint8_t testKind;
uint8_t lightCode;
uint8_t frequencyIndex; uint8_t frequencyIndex;
uint8_t maxPulseIndex; uint8_t maxPulseIndex;
uint8_t minPulseIndex; uint8_t minPulseIndex;
uint8_t accuracyIndex; uint8_t accuracyIndex;
uint8_t timeIndex; uint8_t timeIndex;
uint16_t reserved;
uint32_t checksum; uint32_t checksum;
}; };
@@ -71,6 +80,8 @@ struct IntegerPwmConfig {
}; };
uint32_t settingsChecksum(const Settings &s); uint32_t settingsChecksum(const Settings &s);
bool txActiveLightOn(const Settings &s);
bool rxActiveLightOn(const Settings &s);
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs); uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs);
uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index); uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index);
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles); uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles);

View File

@@ -0,0 +1,635 @@
#include "DriverTest.h"
#include "Config.h"
#include "Log.h"
#include <driver/gpio.h>
#include <esp_cpu.h>
#include <esp32-hal-cpu.h>
#include <soc/gpio_struct.h>
#include <string.h>
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)activeRxLightOn;
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;
captureHz_ = getCpuFrequencyMhz() * 1000000UL;
if (!captureHz_ || captureHz_ % frequencyHz) return false;
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 == LOW; // ACK/fault = light OFF
ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
shortCircuitTicks_ = nsToTicks(DRIVER_SHORT_CIRCUIT_MIN_NS);
stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
subsampleTicks_ = testTicks_ / SUBSAMPLE_COUNT;
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ || !ackStartMaxTicks_ ||
!faultLongTicks_ || !shortCircuitTicks_ || !stuckTicks_ ||
!testTicks_ || !subsampleTicks_)
return false;
clearCapture();
stats_.reset();
publishStats();
pendingCount_ = 0;
response_ = {};
measurementStartTick_ = deadlineTick_ = 0;
pointOriginTick_ = lastEventTick_ = 0;
settleCycles_ = settleCycles;
settledCycles_ = 0;
completedSubsamples_ = 0;
measurementClosed_ = false;
havePointOrigin_ = 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() {
Serial.flush();
if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) {
const bool disabled = disableCore0WDT();
__atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE);
if (!disabled) {
state_ = DriverState::IDLE;
return false;
}
}
__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;
}
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;
settledCycles_ = 0;
measurementClosed_ = 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 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;
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;
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();
portEXIT_CRITICAL(&pollMux_);
critical = false;
flushHot();
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;
windowEnd = nextStart + pollWindowAfterCycles_;
}
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_);
}
}
}
}
void DriverTest::processEvent(const TimedEvent &event) {
lastEventTick_ = event.tick;
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) return;
if (settledCycles_ < settleCycles_) {
++settledCycles_;
return;
}
if (rxActive_) return;
state_ = DriverState::RUNNING;
measurementStartTick_ = event.tick;
const uint64_t measuredBefore =
static_cast<uint64_t>(completedSubsamples_) * subsampleTicks_;
const uint64_t thisSubsampleTicks =
completedSubsamples_ + 1U == SUBSAMPLE_COUNT ?
testTicks_ - measuredBefore : subsampleTicks_;
deadlineTick_ = event.tick + thisSubsampleTicks;
processTx(event, true);
}
void DriverTest::processRunning(const TimedEvent &event) {
expirePending(event.tick);
if (state_ != DriverState::RUNNING) return;
if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
const uint64_t delay = response_.associated ?
response_.startTick - response_.tx.tick : 0;
fail(FailReason::SHORT_CIRCUIT_FAULT, event.tick, delay,
event.tick - response_.startTick);
return;
}
if (event.source == Source::TX) {
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 processRx(event, event.rising == rxActiveRawHigh_);
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;
++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) {
response_ = {};
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
FailReason::GATE_MONITOR_FAULT,
event.tick, 0, width);
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;
response_ = {};
fail(FailReason::ACK_MERGED, event.tick, delay, width);
return;
}
}
if (width >= faultLongTicks_) {
const uint64_t delay = response_.startTick - response_.tx.tick;
response_ = {};
fail(width >= shortCircuitTicks_ ? FailReason::SHORT_CIRCUIT_FAULT :
FailReason::GATE_MONITOR_FAULT,
event.tick, delay, width);
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;
fail(FailReason::ACK_MISSING, now, now - pending_[i].tick, 0);
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_ >= SUBSAMPLE_COUNT) {
__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) {
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_;
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) {
Log::printf("DRIVER",
"error timing: T=%lluns D=%s%lluns P=%s%lluns",
static_cast<unsigned long long>(
ticksToNs(publishedStats_.errorElapsedTicks)),
publishedStats_.errorDelayValid ? "" : "N/A/",
static_cast<unsigned long long>(
ticksToNs(publishedStats_.errorDelayTicks)),
publishedStats_.errorPulseValid ? "" : "N/A/",
static_cast<unsigned long long>(
ticksToNs(publishedStats_.errorPulseTicks)));
}
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);
}
}

View File

@@ -0,0 +1,167 @@
#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 errorDelayTicks;
uint32_t errorPulseTicks;
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_; }
uint32_t tickHz() const { return captureHz_; }
const DriverStats &stats() const { return publishedStats_; }
private:
enum class Source : uint8_t { TX, RX };
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 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);
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 uint8_t SUBSAMPLE_COUNT = 10;
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 pollPeriodCycles_ = 0;
uint32_t pollWindowBeforeCycles_ = 0;
uint32_t pollWindowAfterCycles_ = 0;
bool pollTxStartRawHigh_ = false;
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 shortCircuitTicks_ = 0;
uint64_t stuckTicks_ = 0;
uint64_t testTicks_ = 0;
uint64_t subsampleTicks_ = 0;
uint64_t measurementStartTick_ = 0;
uint64_t deadlineTick_ = 0;
uint64_t pointOriginTick_ = 0;
uint64_t lastEventTick_ = 0;
uint8_t settleCycles_ = 0;
uint8_t settledCycles_ = 0;
uint8_t completedSubsamples_ = 0;
bool rxActiveRawHigh_ = true;
bool rxActive_ = false;
bool measurementClosed_ = false;
bool havePointOrigin_ = 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;
};

View File

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

View File

@@ -3,13 +3,14 @@
#include <string.h> #include <string.h>
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs, bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
uint16_t averagingPeriods, uint8_t settleCycles) { uint16_t averagingPeriods, uint8_t settleCycles,
bool activeRxLightOn) {
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false; if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
expectedHz_ = static_cast<uint32_t>(hz + 0.5f); expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
expectedDutyPct_ = duty; expectedDutyPct_ = duty;
tolerance = effectiveTolerancePct(tolerance); tolerance = effectiveTolerancePct(tolerance);
if (!expectedHz_ || !timeMs || !averagingPeriods || if (!expectedHz_ || !timeMs || !averagingPeriods ||
!receiver_.start(expectedHz_, expectedDutyPct_)) return false; !receiver_.start(expectedHz_, expectedDutyPct_, activeRxLightOn)) return false;
settleCycles_ = settleCycles; settleLeft_ = settleCycles; settleCycles_ = settleCycles; settleLeft_ = settleCycles;
tolerancePct_ = tolerance; tolerancePct_ = tolerance;
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS; stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;

View File

@@ -8,7 +8,7 @@ class Measurement {
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {} explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
bool start(float expectedHz, float expectedDuty, float tolerancePct, bool start(float expectedHz, float expectedDuty, float tolerancePct,
uint32_t testTimeMs, uint16_t averagingPeriods, uint32_t testTimeMs, uint16_t averagingPeriods,
uint8_t settleCycles); uint8_t settleCycles, bool activeRxLightOn);
MeasureState update(); MeasureState update();
bool takeProgressUpdate(); bool takeProgressUpdate();
void abort(); void abort();

View File

@@ -10,11 +10,11 @@ uint32_t pwmOutputTestUpdatedMs = 0;
void setup() { void setup() {
Serial.begin(SERIAL_BAUD); Serial.begin(SERIAL_BAUD);
delay(200); delay(200);
Serial.printf("\nPWM OUTPUT TEST: GPIO=%u requested=%luHz pulse=%lu..%luns 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, GPIO_PWM, PWM_OUTPUT_TEST_FREQUENCY_HZ,
PWM_OUTPUT_TEST_MIN_PULSE_NS, PWM_OUTPUT_TEST_MAX_PULSE_NS, PWM_OUTPUT_TEST_MIN_PULSE_NS, PWM_OUTPUT_TEST_MAX_PULSE_NS,
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS, PWM_OUTPUT_TEST_SWEEP_PERIOD_MS,
PWM_SAFE_LEVEL == HIGH ? "HIGH" : "LOW"); TX_LIGHT_OFF_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
pwmOutputTest.begin(); pwmOutputTest.begin();
ActualPwm actual = {}; ActualPwm actual = {};

View File

@@ -2,7 +2,7 @@
#include "Core.h" #include "Core.h"
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
constexpr uint8_t PROTOCOL_VERSION = 11; constexpr uint8_t PROTOCOL_VERSION = 12;
enum class MessageType : uint8_t { enum class MessageType : uint8_t {
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT, DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
@@ -26,7 +26,7 @@ struct ProtocolPacket {
uint32_t actualPulseNs; uint32_t actualPulseNs;
uint32_t testTimeMs; uint32_t testTimeMs;
uint16_t accuracyX100; uint16_t accuracyX100;
uint8_t settleCycles; uint8_t lightCode;
uint8_t progressStep; uint8_t progressStep;
uint8_t passed; uint8_t passed;
uint8_t reason; uint8_t reason;

View File

@@ -34,11 +34,6 @@ mcpwm_cmpr_handle_t mcpwmComparator = nullptr;
mcpwm_gen_handle_t mcpwmGenerator = nullptr; mcpwm_gen_handle_t mcpwmGenerator = nullptr;
uint32_t mcpwmFrequencyHz = 0; uint32_t mcpwmFrequencyHz = 0;
constexpr mcpwm_generator_action_t PWM_ACTIVE_ACTION =
PWM_ACTIVE_LEVEL == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW;
constexpr mcpwm_generator_action_t PWM_INACTIVE_ACTION =
PWM_ACTIVE_LEVEL == HIGH ? MCPWM_GEN_ACTION_LOW : MCPWM_GEN_ACTION_HIGH;
void releaseMcpwm() { void releaseMcpwm() {
if (mcpwmGenerator) { if (mcpwmGenerator) {
mcpwm_del_generator(mcpwmGenerator); mcpwm_del_generator(mcpwmGenerator);
@@ -99,22 +94,27 @@ void PwmGenerator::begin() {
timerConfig.period_ticks / 2U) == ESP_OK; timerConfig.period_ticks / 2U) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator, ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
MCPWM_TIMER_EVENT_EMPTY, PWM_ACTIVE_ACTION)) == 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, ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
mcpwmComparator, PWM_INACTIVE_ACTION)) == 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; ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
if (!ok) { if (!ok) {
releaseMcpwm(); releaseMcpwm();
pinMode(GPIO_PWM, OUTPUT); pinMode(GPIO_PWM, OUTPUT);
digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
return; return;
} }
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true); mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
#endif #endif
} }
bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, 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 #if CONFIG_IDF_TARGET_ESP32C3
IntegerPwmConfig config = {}; IntegerPwmConfig config = {};
if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false; if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
@@ -127,7 +127,7 @@ bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
if (attached) { if (attached) {
// Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when // Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when
// the configured active pulse level is LOW. // the configured active pulse level is LOW.
if (!ledcOutputInvert(GPIO_PWM, PWM_ACTIVE_LEVEL == LOW)) { if (!ledcOutputInvert(GPIO_PWM, activeLevel == LOW)) {
ledcDetach(GPIO_PWM); ledcDetach(GPIO_PWM);
delay(2); delay(2);
continue; continue;
@@ -153,7 +153,7 @@ bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
if (attached) ledcDetach(GPIO_PWM); if (attached) ledcDetach(GPIO_PWM);
delay(2); 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; return false;
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs || if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs ||
@@ -169,13 +169,21 @@ bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
stop(); stop();
bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK; bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK;
ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == 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 // stop() applies a continuous force level (hold_on=true). Remove that same
// continuous-force action; hold_on=false addresses a different, one-shot // continuous-force action; hold_on=false addresses a different, one-shot
// force mechanism and would leave the safe level permanently active. // 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_generator_set_force_level(mcpwmGenerator, -1, true) == ESP_OK;
ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK; ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK;
if (!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; return false;
} }
@@ -193,9 +201,10 @@ bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
void PwmGenerator::stop() { void PwmGenerator::stop() {
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
if (running_) ledcDetach(GPIO_PWM); 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 #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) { if (running_ && mcpwmTimer) {
mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY); mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY);
const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U; const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U;
@@ -211,12 +220,29 @@ void PwmGenerator::active() {
// that denotes the pulse during a running test. // that denotes the pulse during a running test.
stop(); stop();
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL); digitalWrite(GPIO_PWM, activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3 #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 { else {
pinMode(GPIO_PWM, OUTPUT); 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 #endif
} }

View File

@@ -13,10 +13,15 @@ struct ActualPwm {
class PwmGenerator { class PwmGenerator {
public: public:
void begin(); void begin();
void configureActiveLight(bool lightOn) { activeLightOn_ = lightOn; }
bool start(uint32_t frequencyHz, uint32_t pulseNs, ActualPwm &actual); bool start(uint32_t frequencyHz, uint32_t pulseNs, ActualPwm &actual);
// Hold the optical level selected as the active TX pulse.
void active(); void active();
// Hold actual optical light ON, independently of HH/HL/LH/LL.
void lightOn();
void stop(); void stop();
bool running() const { return running_; } bool running() const { return running_; }
private: private:
bool running_ = false; bool running_ = false;
bool activeLightOn_ = true;
}; };

View File

@@ -40,6 +40,10 @@ bool PulseReceiver::begin() {
mcpwm_capture_channel_config_t channelConfig = {}; mcpwm_capture_channel_config_t channelConfig = {};
channelConfig.gpio_num = GPIO_RX; 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.prescale = 1;
channelConfig.flags.pos_edge = true; channelConfig.flags.pos_edge = true;
channelConfig.flags.neg_edge = false; channelConfig.flags.neg_edge = false;
@@ -54,8 +58,13 @@ bool PulseReceiver::begin() {
channelConfig.flags.neg_edge = true; channelConfig.flags.neg_edge = true;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK) if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK)
return false; return false;
return mcpwm_capture_channel_register_event_callbacks( if (mcpwm_capture_channel_register_event_callbacks(
fallingChannel_, &callbacks, this) == ESP_OK; 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 #else
pinMode(GPIO_RX, INPUT); pinMode(GPIO_RX, INPUT);
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
@@ -64,7 +73,8 @@ bool PulseReceiver::begin() {
#endif #endif
} }
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) { bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
bool activeLightOn) {
if (!plannedTickHz(expectedHz, expectedDutyPct)) return false; if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
expectedHz_ = expectedHz; expectedHz_ = expectedHz;
expectedDutyPct_ = expectedDutyPct; expectedDutyPct_ = expectedDutyPct;
@@ -73,6 +83,64 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
if (!cpuTickHz_) return false; if (!cpuTickHz_) return false;
#endif #endif
resetStream(); resetStream();
const bool rawHighMeansLightOn = RX_LIGHT_ON_GPIO_LEVEL == HIGH;
activeStartRising_ = rawHighMeansLightOn == activeLightOn;
Log::printf("CAPTURE", "RX active optical level=%s, raw active starts on %s",
activeLightOn ? "H/light-on" : "L/light-off",
activeStartRising_ ? "RISING" : "FALLING");
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;
}
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::startDriver(uint32_t frequencyHz, uint32_t pulseNs,
bool activeTxLightOn) {
#if OPTICAL_USE_MCPWM_CAPTURE
if (!frequencyHz || !pulseNs || !tickHz() || running_) return false;
resetStream();
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
}
bool PulseReceiver::startCapture(bool withTx) {
#if OPTICAL_USE_MCPWM_CAPTURE #if OPTICAL_USE_MCPWM_CAPTURE
// Progress updates keep one capture session alive. Pulse-width stages stop // Progress updates keep one capture session alive. Pulse-width stages stop
// capture only after PWM is quiet, so resetStream never races the ISR. // capture only after PWM is quiet, so resetStream never races the ISR.
@@ -87,15 +155,25 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
mcpwm_capture_timer_disable(captureTimer_); mcpwm_capture_timer_disable(captureTimer_);
return false; 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; running_ = true;
if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) { if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) {
running_ = false; running_ = false;
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
txCaptureEnabled_ = false;
mcpwm_capture_channel_disable(fallingChannel_); mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_); mcpwm_capture_timer_disable(captureTimer_);
return false; return false;
} }
#else #else
if (withTx) return false;
running_ = true; running_ = true;
#endif #endif
return true; return true;
@@ -106,7 +184,9 @@ void PulseReceiver::stop() {
running_ = false; running_ = false;
#if OPTICAL_USE_MCPWM_CAPTURE #if OPTICAL_USE_MCPWM_CAPTURE
if (wasRunning) { if (wasRunning) {
// Mask both edge interrupts before stopping the shared capture timer. // 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(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_); mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_stop(captureTimer_); mcpwm_capture_timer_stop(captureTimer_);
@@ -120,10 +200,15 @@ void PulseReceiver::stop() {
void PulseReceiver::resetStream() { void PulseReceiver::resetStream() {
if (queue_) xQueueReset(queue_); if (queue_) xQueueReset(queue_);
haveReorderEdge_ = false; 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; droppedItems_ = 0;
polarityKnown_ = false; polarityKnown_ = false;
activeStartRising_ = false; activeStartRising_ = false;
syncEdgeCount_ = 0;
waitingForActiveEnd_ = true; waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0; activeStart_ = activeEnd_ = 0;
haveRawTick_ = false; haveRawTick_ = false;
@@ -159,39 +244,13 @@ bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
return true; return true;
} }
syncEdges_[syncEdgeCount_++] = edge; // HH/HL/LH/LL defines the active optical state explicitly. Synchronize on
if (syncEdgeCount_ < 3U) return false; // its physical starting edge instead of guessing polarity from pulse width.
if (edge.rising != activeStartRising_) return false;
const uint64_t firstTicks = syncEdges_[1].tick - syncEdges_[0].tick;
const uint64_t secondTicks = syncEdges_[2].tick - syncEdges_[1].tick;
const double expectedTicks = static_cast<double>(tickHz()) * expectedDutyPct_ /
(100.0 * expectedHz_);
const double firstError = fabs(static_cast<double>(firstTicks) - expectedTicks);
const double secondError = fabs(static_cast<double>(secondTicks) - expectedTicks);
activeStartRising_ = firstError <= secondError ? syncEdges_[0].rising : syncEdges_[1].rising;
polarityKnown_ = true; polarityKnown_ = true;
Log::printf("CAPTURE", "RX polarity auto: active starts on %s, first=%lluns second=%lluns", activeStart_ = edge.tick;
activeStartRising_ ? "RISING" : "FALLING", waitingForActiveEnd_ = true;
static_cast<unsigned long long>(firstTicks * 1000000000ULL / tickHz()), return false;
static_cast<unsigned long long>(secondTicks * 1000000000ULL / tickHz()));
bool produced = false;
if (firstError <= secondError) {
activeStart_ = syncEdges_[0].tick;
activeEnd_ = syncEdges_[1].tick;
const uint64_t periodTicks = syncEdges_[2].tick - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeEnd_ - activeStart_), tickHz()};
activeStart_ = syncEdges_[2].tick;
waitingForActiveEnd_ = true;
produced = true;
} else {
activeStart_ = syncEdges_[1].tick;
activeEnd_ = syncEdges_[2].tick;
waitingForActiveEnd_ = false;
}
syncEdgeCount_ = 0;
return produced;
} }
uint32_t PulseReceiver::takeDroppedItems() { uint32_t PulseReceiver::takeDroppedItems() {
@@ -199,13 +258,45 @@ uint32_t PulseReceiver::takeDroppedItems() {
} }
#if OPTICAL_USE_MCPWM_CAPTURE #if OPTICAL_USE_MCPWM_CAPTURE
bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t, bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
const mcpwm_capture_event_data_t *data, const mcpwm_capture_event_data_t *data,
void *ctx) { void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return false; if (!self->running_) return false;
const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS; const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS;
const Edge edge = {data->cap_value, static_cast<uint8_t>(rawRising)}; 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_) {
// Three capture channels are independent ISR producers. Serialize their
// reservation/publication of a ring slot; treating this as an SPSC ring
// loses or duplicates RX events when TX and RX interrupts overlap.
portENTER_CRITICAL_ISR(&self->driverRingMux_);
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.
portEXIT_CRITICAL_ISR(&self->driverRingMux_);
return false;
}
const uint16_t write = __atomic_load_n(
&self->driverRingWrite_, __ATOMIC_RELAXED);
const uint16_t next = static_cast<uint16_t>(
(write + 1U) % DRIVER_RING_CAPACITY);
if (next == __atomic_load_n(&self->driverRingRead_, __ATOMIC_ACQUIRE)) {
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
} else {
self->driverRing_[write] = edge;
self->lastDriverEdge_ = edge;
self->haveLastDriverEdge_ = true;
__atomic_store_n(&self->driverRingWrite_, next, __ATOMIC_RELEASE);
}
portEXIT_CRITICAL_ISR(&self->driverRingMux_);
return false;
}
BaseType_t wake = pdFALSE; BaseType_t wake = pdFALSE;
if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE) if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
@@ -215,9 +306,9 @@ bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t,
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return; if (!self->running_) return;
bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)); const bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
if (RX_ACTIVE_LEVEL == LOW) level = !level; const Edge edge = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level),
const Edge edge = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)}; static_cast<uint8_t>(CaptureSource::RX)};
BaseType_t wake = pdFALSE; BaseType_t wake = pdFALSE;
if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE) if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
@@ -256,3 +347,84 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
} }
return count; return count;
} }
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
TickType_t waitTicks) {
if (!events || capacity < 2U || !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 haveTxEnd = false;
uint32_t lastTxEnd = 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;
if (projectedCount + needed > limit) break;
projectedCount += needed;
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
lastTxEnd = edge.tick;
haveTxEnd = true;
}
scan = static_cast<uint16_t>((scan + 1U) % DRIVER_RING_CAPACITY);
}
if (!haveTxEnd) {
if (waitTicks) vTaskDelay(waitTicks);
return 0;
}
const uint32_t releaseThrough = lastTxEnd + driverReleaseSlackTicks_;
size_t count = 0;
while (read != write) {
const Edge edge = driverRing_[read];
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)};
}
return count;
}

View File

@@ -12,13 +12,24 @@
#define OPTICAL_USE_MCPWM_CAPTURE 0 #define OPTICAL_USE_MCPWM_CAPTURE 0
#endif #endif
enum class CaptureSource : uint8_t { RX, TX };
struct CaptureEvent {
uint64_t tick;
bool rising;
CaptureSource source;
};
class PulseReceiver { class PulseReceiver {
public: public:
bool begin(); 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 stop();
void resetStream(); void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
uint32_t takeDroppedItems(); uint32_t takeDroppedItems();
uint32_t tickHz() const; uint32_t tickHz() const;
uint32_t pulseTickHz() const { return tickHz(); } uint32_t pulseTickHz() const { return tickHz(); }
@@ -30,32 +41,43 @@ class PulseReceiver {
bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; } bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; }
private: private:
struct Edge { uint32_t tick; uint8_t rising; }; struct Edge { uint32_t tick; uint8_t rising; uint8_t source; };
static constexpr uint16_t DRIVER_RING_CAPACITY = 512;
struct TimedEdge { uint64_t tick; bool rising; }; struct TimedEdge { uint64_t tick; bool rising; };
bool startCapture(bool withTx);
bool consumeEdge(const Edge &edge, PulsePeriod &period); bool consumeEdge(const Edge &edge, PulsePeriod &period);
bool nextOrderedEdge(Edge &edge, TickType_t waitTicks); bool nextOrderedEdge(Edge &edge, TickType_t waitTicks);
TimedEdge extendEdge(const Edge &edge); TimedEdge extendEdge(const Edge &edge);
#if OPTICAL_USE_MCPWM_CAPTURE #if OPTICAL_USE_MCPWM_CAPTURE
bool configureDriverTxCapture(bool risingEdge);
static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t, static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t,
const mcpwm_capture_event_data_t *, void *); const mcpwm_capture_event_data_t *, void *);
mcpwm_cap_timer_handle_t captureTimer_ = nullptr; mcpwm_cap_timer_handle_t captureTimer_ = nullptr;
mcpwm_cap_channel_handle_t risingChannel_ = nullptr; mcpwm_cap_channel_handle_t risingChannel_ = nullptr;
mcpwm_cap_channel_handle_t fallingChannel_ = nullptr; mcpwm_cap_channel_handle_t fallingChannel_ = nullptr;
mcpwm_cap_channel_handle_t txChannel_ = nullptr;
uint32_t captureResolutionHz_ = 0; uint32_t captureResolutionHz_ = 0;
#else #else
static void IRAM_ATTR onGpio(void *ctx); static void IRAM_ATTR onGpio(void *ctx);
uint32_t cpuTickHz_ = 0; uint32_t cpuTickHz_ = 0;
#endif #endif
QueueHandle_t queue_ = nullptr; QueueHandle_t queue_ = nullptr;
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_ = {}; Edge reorderEdge_ = {};
bool haveReorderEdge_ = false; bool haveReorderEdge_ = false;
volatile uint32_t droppedItems_ = 0; volatile uint32_t droppedItems_ = 0;
volatile bool running_ = false; volatile bool running_ = false;
volatile bool txCaptureEnabled_ = false;
uint32_t expectedHz_ = 0; uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 50.0f; float expectedDutyPct_ = 50.0f;
bool polarityKnown_ = false, activeStartRising_ = false; bool polarityKnown_ = false, activeStartRising_ = false;
TimedEdge syncEdges_[3] = {};
uint8_t syncEdgeCount_ = 0;
bool waitingForActiveEnd_ = true; bool waitingForActiveEnd_ = true;
uint64_t activeStart_ = 0, activeEnd_ = 0; uint64_t activeStart_ = 0, activeEnd_ = 0;
bool haveRawTick_ = false; bool haveRawTick_ = false;

View File

@@ -3,16 +3,22 @@
#include "Log.h" #include "Log.h"
#include <Preferences.h> #include <Preferences.h>
namespace { constexpr uint16_t SETTINGS_VERSION = 9; constexpr char NAMESPACE[] = "opt-test"; } namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
void SettingsStore::defaults(Settings &s) const { void SettingsStore::defaults(Settings &s) const {
// 2 kHz, 200 us .. 2 us, 5%, 1 s. // 2 kHz, 200 us .. 2 us, 5%, 1 s.
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 2, 3, 3, 2, 3, 0}; s = {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); s.checksum = settingsChecksum(s);
} }
bool SettingsStore::valid(const Settings &s) const { bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) && return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.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.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) && s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
s.minPulseIndex < countOf(MIN_PULSE_OPTIONS_NS) && s.minPulseIndex < countOf(MIN_PULSE_OPTIONS_NS) &&

File diff suppressed because one or more lines are too long