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

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/
History
Project Logs*/
/.build/

View File

@@ -10,13 +10,14 @@
#include <driver/gpio.h>
#include <Wire.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
namespace {
const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) {
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER",
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
const uint8_t index = static_cast<uint8_t>(state);
@@ -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) {
char target[32];
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,
@@ -62,7 +63,16 @@ void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
(void)reason;
char target[32];
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) {
@@ -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);
}
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 last = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS) - 1U);
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);
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() {
Serial.begin(SERIAL_BAUD);
@@ -221,6 +269,7 @@ void App::finishInitialization(bool factoryReset) {
}
sanitizeRange();
params_ = store_.params(settings_);
pwm_.configureActiveLight(txActiveLightOn(settings_));
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
@@ -233,6 +282,7 @@ void App::finishInitialization(bool factoryReset) {
}
void App::update() {
serviceSerialConsole();
serviceIdlePowerSave();
const uint32_t now = millis();
const ButtonEvent startEvent = startButton_.update(now);
@@ -258,11 +308,14 @@ void App::update() {
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
if (modeEvent == ButtonEvent::SHORT) {
settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_);
cycleRunMode(); sanitizeRange(); const bool saved = store_.save(settings_);
params_ = store_.params(settings_);
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
Log::printf("ACTION", "role changed to %s, NVS=%s", roleName(static_cast<Role>(settings_.role)), saved ? "OK" : "FAILED");
Log::printf("ACTION", "mode changed to %s/%s, NVS=%s",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) {
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
@@ -273,7 +326,9 @@ void App::update() {
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
radio_.end(); havePeer_ = false;
if (modeEvent == ButtonEvent::SHORT) {
settings_.role = static_cast<uint8_t>(Role::SOLO); const bool saved = store_.save(settings_);
settings_.role = static_cast<uint8_t>(Role::SOLO);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
const bool saved = store_.save(settings_);
params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) {
@@ -283,7 +338,7 @@ void App::update() {
}
if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
menuItem_ = (menuItem_ + 1U) % 6U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
}
else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
@@ -316,6 +371,39 @@ void App::update() {
StageStats 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 ||
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
handleRadio(); updateMaster();
@@ -328,14 +416,207 @@ void App::showIdle() {
setActivePerformance(false);
setStandbyOpticalOutput();
lastUserActivityMs_ = millis();
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX,
uiRoleName(static_cast<Role>(settings_.role)));
char one[64]; snprintf(one, sizeof(one), "%s: %s",
uiRoleName(static_cast<Role>(settings_.role)),
uiTestName(static_cast<TestKind>(settings_.testKind)));
display_.show(one, UiText::START_RUN);
}
void App::serviceSerialConsole() {
while (Serial.available() > 0) {
const int raw = Serial.read();
if (raw < 0) break;
const char c = static_cast<char>(raw);
lastUserActivityMs_ = millis();
leaveIdlePowerSave();
if (c == '\r') continue;
if (c == '\n') {
if (serialLineOverflow_) Serial.println("ERR command too long");
else if (serialLineLength_) {
serialLine_[serialLineLength_] = '\0';
handleSerialCommand(serialLine_);
}
serialLineLength_ = 0;
serialLineOverflow_ = false;
continue;
}
if (c < ' ' || c > '~') continue;
if (serialLineLength_ + 1U < sizeof(serialLine_))
serialLine_[serialLineLength_++] = c;
else serialLineOverflow_ = true;
}
}
void App::printSerialHelp() {
Serial.println("COMMANDS (send with newline):");
Serial.println(" help | status | start | stop | defaults");
Serial.println(" set role solo|master|slave");
Serial.println(" set test optical|driver");
Serial.println(" set frequency 500|1000|2000|5000|10000|25000");
Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000");
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
Serial.println(" set accuracy 1|2|5|10");
Serial.println(" set time 100|250|500|1000|2000|5000 (ms)");
Serial.println(" set light HH|HL|LH|LL");
}
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() {
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
settings_.role = static_cast<uint8_t>(Role::SOLO);
if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
if (settings_.role != static_cast<uint8_t>(Role::SOLO) ||
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS);
settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS);
@@ -346,9 +627,15 @@ void App::sanitizeRange() {
if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid;
const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin;
const uint8_t firstMin = firstMinPulseIndexAtLeast(
minimumPulseForAccuracy(hz, ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), lastMin);
if (settings_.minPulseIndex < firstMin) settings_.minPulseIndex = firstMin;
if (settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)) {
const uint8_t driverLastMin = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t firstDriverMin = firstMinPulseIndexAtLeast(
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
if (settings_.minPulseIndex < firstDriverMin)
settings_.minPulseIndex = firstDriverMin;
settings_.lightCode = static_cast<uint8_t>(LightCode::HL);
}
}
void App::serviceRxPinStateLog() {
@@ -376,25 +663,26 @@ void App::changeMenu(int d) {
} else if (menuItem_ == 2) {
const uint8_t last = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t first = firstMinPulseIndexAtLeast(
minimumPulseForAccuracy(PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex],
ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), last);
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex,
first, last, d);
const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
? firstMinPulseIndexAtLeast(DRIVER_MIN_INPUT_PULSE_NS, last) : 0U;
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex, first, last, d);
} else {
uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) {
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 5: value = &settings_.lightCode; count = 4; break;
default: return;
}
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
}
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,
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex);
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
lightCodeName(static_cast<LightCode>(settings_.lightCode)));
showMenu();
}
@@ -423,6 +711,15 @@ void App::showMenu() {
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = UiText::MENU_TEST_TIME;
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;
}
formatMenuLine(label, value, one, sizeof(one));
@@ -434,23 +731,29 @@ void App::startTest() {
leaveIdlePowerSave();
pwm_.stop();
setActivePerformance(true);
sanitizeRange();
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
lightCodeName(static_cast<LightCode>(settings_.lightCode)), stageCount_);
if (SERIAL_MINIMAL_LOG) {
Log::printf("CONFIG", "mode=%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums TX=%s RX=AUTO stages=%lu",
roleName(static_cast<Role>(settings_.role)), params_.frequencyHz,
Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
PWM_ACTIVE_LEVEL == HIGH ? "HIGH" : "LOW",
lightCodeName(static_cast<LightCode>(settings_.lightCode)),
stageCount_);
}
printConfiguration();
const Role role = static_cast<Role>(settings_.role);
if (role == Role::SOLO) {
if (!prepareStage()) return;
state_ = AppState::SOLO_MEASURE;
state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
} else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
else if (role == Role::MASTER) startMasterDiscovery();
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
@@ -498,24 +801,31 @@ bool App::prepareStage(bool showProgress) {
params_.accuracyPct);
finish(false, FailReason::RESOLUTION); return false;
}
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct,
plannedRxHz, plannedPulseRxHz, actual_.bits,
MEASUREMENT_AVERAGING_PERIODS);
if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, plannedPulseRxHz,
effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false;
const bool driverMode = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
if (!driverMode) {
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct,
params_.accuracyPct, plannedRxHz, plannedPulseRxHz, actual_.bits,
MEASUREMENT_AVERAGING_PERIODS);
if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
plannedPulseRxHz, effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false;
}
} else if (!receiver_.highRateBackend()) {
finish(false, FailReason::UNSUPPORTED); return false;
}
Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
if (showProgress) showStageProgress();
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
finish(false, FailReason::UNSUPPORTED); return false;
if (static_cast<Role>(settings_.role) == Role::SOLO) {
const bool started = driverMode ? startDriverMeasurement() :
startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct);
if (!started) { finish(false, FailReason::UNSUPPORTED); return false; }
}
return true;
}
@@ -526,7 +836,7 @@ bool App::startLocalMeasurement(float hz, float duty) {
receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES);
MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, rxActiveLightOn(settings_));
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
static_cast<uint32_t>(hz + 0.5f));
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
@@ -536,6 +846,18 @@ bool App::startLocalMeasurement(float hz, float duty) {
return ok;
}
bool App::startDriverMeasurement() {
const bool ok = driverTest_.start(actual_.actualHz, actual_.actualPulseNs,
params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES,
txActiveLightOn(settings_), rxActiveLightOn(settings_));
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, actual_.actualHz);
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
if (!ok) Log::event("DRIVER", "response test start FAILED");
return ok;
}
void App::stagePassed() {
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
pwm_.stop();
@@ -543,8 +865,16 @@ void App::stagePassed() {
// queue for the next pulse width. Never reset a FreeRTOS queue concurrently
// with the capture ISR.
if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop();
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
if (++stageIndex_ >= stageCount_) {
const bool preserveDriverMeasurements =
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
finish(true, FailReason::NONE, preserveDriverMeasurements);
return;
}
if (static_cast<Role>(settings_.role) == Role::SOLO) {
if (prepareStage()) state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
}
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
@@ -561,7 +891,7 @@ void App::startMasterDiscovery() {
requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
opticalWakeActive_ = true;
lastOpticalWakeToggleMs_ = millis();
pwm_.active();
pwm_.lightOn();
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = millis(); retries_ = 0;
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
@@ -575,7 +905,8 @@ ProtocolPacket App::makePacket(MessageType type) const {
p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f);
p.lightCode = settings_.lightCode;
return p;
}
@@ -663,6 +994,8 @@ void App::handleRadio() {
state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
if (r.packet.lightCode <= static_cast<uint8_t>(LightCode::LL))
settings_.lightCode = r.packet.lightCode;
requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
stageCount_ = r.packet.stageCount;
actual_ = {};
@@ -753,7 +1086,7 @@ void App::updateMaster() {
if (state_ == AppState::MASTER_DISCOVER) {
if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) {
opticalWakeActive_ = !opticalWakeActive_;
if (opticalWakeActive_) pwm_.active();
if (opticalWakeActive_) pwm_.lightOn();
else pwm_.stop();
lastOpticalWakeToggleMs_ = now;
}
@@ -836,7 +1169,10 @@ void App::sendAbort(FailReason reason) {
void App::abortTest() {
Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM");
sendAbort(FailReason::ABORTED); measurement_.abort(); finish(false, FailReason::ABORTED);
sendAbort(FailReason::ABORTED);
measurement_.abort();
driverTest_.abort();
finish(false, FailReason::ABORTED);
}
void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
@@ -903,17 +1239,39 @@ bool App::idlePowerSaveAllowed() const {
// Never enter blocking light sleep while the settings screen is open. A
// wake-up press is deliberately consumed by the button state machine, which
// is useful in IDLE but makes menu navigation appear frozen.
return initialized_ && (state_ == AppState::IDLE ||
return !usbHostPresent() && initialized_ && (state_ == AppState::IDLE ||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
}
bool App::usbHostPresent() const {
#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT && SOC_USB_SERIAL_JTAG_SUPPORTED
// This is driven by USB SOF packets, not by CDC traffic: an enumerated host
// keeps the board awake even if COM is closed and no bytes are exchanged.
// Retain the state across short SOF/driver glitches.
const uint32_t now = millis();
if (Serial.isPlugged()) {
lastUsbHostSeenMs_ = now ? now : 1U;
return true;
}
return lastUsbHostSeenMs_ &&
now - lastUsbHostSeenMs_ <= USB_HOST_DISCONNECT_GRACE_MS;
#else
return false;
#endif
}
void App::setStandbyOpticalOutput() {
if (static_cast<Role>(settings_.role) == Role::SLAVE) pwm_.stop();
// A gate driver must never be held enabled while the tester is idle or
// showing a result. DRIVER is SOLO-only, so force real light OFF here.
if (static_cast<Role>(settings_.role) == Role::SLAVE ||
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) pwm_.stop();
else pwm_.active();
}
void App::setActivePerformance(bool active) {
const uint32_t targetMhz = active ? 160U : 80U;
const bool driverMode = initialized_ &&
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
const uint32_t targetMhz = active ? (driverMode ? 240U : 160U) : 80U;
if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
}
@@ -1020,13 +1378,18 @@ void App::printConfiguration() {
if (SERIAL_MINIMAL_LOG) return;
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
Serial.printf("\nOptical Channel Tester | %s | mode=%s\n", board, roleName(static_cast<Role>(settings_.role)));
Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board,
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
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("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
Serial.printf("Test %lu 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_.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);
Serial.printf("Pulse widths descending (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i)
@@ -1093,6 +1456,66 @@ void App::showStageResult(const StageStats &s) {
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) {
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
@@ -1135,6 +1558,15 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
}
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];
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),

View File

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

View File

@@ -74,15 +74,12 @@ constexpr bool SERIAL_LOG_TIMESTAMPS = true;
constexpr bool SERIAL_MINIMAL_LOG = true;
#define BUTTON_ACTIVE_LEVEL LOW
// Raw GPIO_RX level that means the optical receiver is active.
#define RX_ACTIVE_LEVEL HIGH
// PWM_ACTIVE_LEVEL is the electrical level of the active test pulse and is
// also used for the constant active output while awake outside a test. During
// the remainder of a running PWM period the output is !PWM_ACTIVE_LEVEL.
// PWM_SAFE_LEVEL is used only while PWM is stopped and during sleep; it is
// independent of the PWM inactive level and may equal PWM_ACTIVE_LEVEL.
#define PWM_SAFE_LEVEL HIGH
#define PWM_ACTIVE_LEVEL LOW
// Fixed PCB conversion between electrical GPIO levels and actual optical
// light. User settings HH/HL/LH/LL operate only in the optical domain and
// never change these hardware facts.
#define TX_LIGHT_ON_GPIO_LEVEL LOW
#define RX_LIGHT_ON_GPIO_LEVEL LOW
#define TX_LIGHT_OFF_GPIO_LEVEL (TX_LIGHT_ON_GPIO_LEVEL == HIGH ? LOW : HIGH)
#define PWM_SETTLE_CYCLES 5U
constexpr uint32_t BUTTON_DEBOUNCE_MS = 30;
@@ -95,8 +92,8 @@ constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
constexpr uint8_t LINK_PACKET_RETRIES = 10;
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20;
// During discovery Master alternates PWM_ACTIVE_LEVEL and PWM_SAFE_LEVEL to
// wake a sleeping Slave through the optical channel.
// During discovery Master alternates actual optical light ON and OFF to wake
// a sleeping Slave through the optical channel.
constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50;
constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
@@ -114,6 +111,9 @@ constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000;
// usb_serial_jtag_is_connected() needs no open COM port or CDC traffic, but a
// short SOF detection gap must not send the board to sleep.
constexpr uint32_t USB_HOST_DISCONNECT_GRACE_MS = 5000;
constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100;
constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20;
static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS,
@@ -139,6 +139,22 @@ constexpr uint8_t LEDC_MAX_BITS = 14;
constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000;
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 -----------------------------
// 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
@@ -147,13 +163,13 @@ constexpr uint32_t PWM_FREQUENCY_OPTIONS_HZ[] = {
500, 1000, 2000, 5000, 10000, 25000,
};
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[] = {
250, 500, 1000, 2000, 5000, 10000
250, 500, 1000, 2000, 5000, 10000, 50000
};
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
};
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[] = {
"НЕТ ОШИБКИ",
"НЕТ СИГНАЛА",
@@ -29,7 +33,13 @@ constexpr const char *FAIL_NAMES[] = {
"СВЯЗЬ ПОТЕРЯНА",
"РЕЖИМ НЕ ПОДДЕРЖИВ.",
"НЕ ХВАТАЕТ ТОЧНОСТИ",
"ТЕСТ ОСТАНОВЛЕН"
"ТЕСТ ОСТАНОВЛЕН",
"НЕТ ОТВЕТА ACK",
"ТАЙМИНГ ACK",
"АВАРИЯ ДРАЙВЕРА",
"ОТВЕТЫ ACK СЛИЛИСЬ",
"ОШИБКА ЗАТВОРА",
"КОРОТКОЕ ЗАМЫКАНИЕ"
};
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_ACCURACY = "ТОЧНОСТЬ:";
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 *FREQUENCY_UNIT = " Гц";
@@ -48,7 +60,7 @@ constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА";
constexpr const char *HOLD_START_STOP = "УДЕРЖ. START ДЛЯ СТОП";
constexpr const char *HOLD_START_STOP = "УДЕРЖ. ПУСК ДЛЯ СТОП";
constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
@@ -57,9 +69,13 @@ constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
constexpr const char *FAIL_FORMAT = "СБОЙ %s";
constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *TEST_TARGET_FORMAT = "ТЕСТ: %s";
constexpr const char *FAIL_TARGET_FORMAT = "СБОЙ: %s";
constexpr const char *PERIOD_OUT_FORMAT = "ЧАСТОТА: %s";
constexpr const char *DUTY_OUT_FORMAT = "ИМПУЛЬС: %s";
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
#elif UI_LANGUAGE == UI_LANGUAGE_EN
@@ -67,6 +83,10 @@ constexpr const char *ROLE_NAMES[] = {
"SOLO", "MASTER", "SLAVE"
};
constexpr const char *TEST_NAMES[] = {
"OPTICAL", "DRIVER"
};
constexpr const char *FAIL_NAMES[] = {
"NONE",
"NO SIGNAL",
@@ -79,7 +99,13 @@ constexpr const char *FAIL_NAMES[] = {
"LINK LOST",
"UNSUPPORTED",
"RESOLUTION",
"ABORTED"
"ABORTED",
"ACK MISSING",
"ACK TIMING",
"DRIVER FAULT",
"ACK MERGED",
"GATE FAULT",
"SHORT CIRCUIT FAULT"
};
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_ACCURACY = "ACCURACY:";
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 *FREQUENCY_UNIT = " Hz";
@@ -107,9 +135,13 @@ constexpr const char *TEST_FAILED = "TEST FAILED";
constexpr const char *PASS_WORD = "TEST PASS";
constexpr const char *FAIL_FORMAT = "FAIL %s";
constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *TEST_TARGET_FORMAT = "TEST: %s";
constexpr const char *FAIL_TARGET_FORMAT = "FAIL AT %s";
constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
#else
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"

View File

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

View File

@@ -4,22 +4,31 @@
#include <stddef.h>
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
enum class TestKind : uint8_t { OPTICAL, DRIVER };
enum class LightCode : uint8_t { HH, HL, LH, LL };
enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED,
GATE_MONITOR_FAULT, SHORT_CIRCUIT_FAULT
};
const char *roleName(Role role);
const char *testKindName(TestKind kind);
const char *lightCodeName(LightCode code);
const char *failName(FailReason reason);
struct Settings {
uint16_t version;
uint8_t role;
uint8_t testKind;
uint8_t lightCode;
uint8_t frequencyIndex;
uint8_t maxPulseIndex;
uint8_t minPulseIndex;
uint8_t accuracyIndex;
uint8_t timeIndex;
uint16_t reserved;
uint32_t checksum;
};
@@ -71,6 +80,8 @@ struct IntegerPwmConfig {
};
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 pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index);
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_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") &&
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);
else Serial.printf("[%-8s] %s\n", component, message);
}

View File

@@ -3,13 +3,14 @@
#include <string.h>
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;
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
expectedDutyPct_ = duty;
tolerance = effectiveTolerancePct(tolerance);
if (!expectedHz_ || !timeMs || !averagingPeriods ||
!receiver_.start(expectedHz_, expectedDutyPct_)) return false;
!receiver_.start(expectedHz_, expectedDutyPct_, activeRxLightOn)) return false;
settleCycles_ = settleCycles; settleLeft_ = settleCycles;
tolerancePct_ = tolerance;
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;

View File

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

View File

@@ -10,11 +10,11 @@ uint32_t pwmOutputTestUpdatedMs = 0;
void setup() {
Serial.begin(SERIAL_BAUD);
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,
PWM_OUTPUT_TEST_MIN_PULSE_NS, PWM_OUTPUT_TEST_MAX_PULSE_NS,
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS,
PWM_SAFE_LEVEL == HIGH ? "HIGH" : "LOW");
TX_LIGHT_OFF_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
pwmOutputTest.begin();
ActualPwm actual = {};

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,10 @@ bool PulseReceiver::begin() {
mcpwm_capture_channel_config_t channelConfig = {};
channelConfig.gpio_num = GPIO_RX;
// ACK pulses are sub-microsecond and consecutive acknowledgements can be
// only 1 us apart. A low-priority capture interrupt can leave the channel
// status pending long enough for the next timestamp to overwrite it.
channelConfig.intr_priority = 3;
channelConfig.prescale = 1;
channelConfig.flags.pos_edge = true;
channelConfig.flags.neg_edge = false;
@@ -54,8 +58,13 @@ bool PulseReceiver::begin() {
channelConfig.flags.neg_edge = true;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK)
return false;
return mcpwm_capture_channel_register_event_callbacks(
fallingChannel_, &callbacks, this) == ESP_OK;
if (mcpwm_capture_channel_register_event_callbacks(
fallingChannel_, &callbacks, this) != ESP_OK) return false;
// The TX channel is created immediately before a driver test. Only the end
// of the active PWM pulse is armed; handling its start here would occupy the
// shared MCPWM ISR during the RX acknowledgement only ~300 ns later.
return true;
#else
pinMode(GPIO_RX, INPUT);
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
@@ -64,7 +73,8 @@ bool PulseReceiver::begin() {
#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;
expectedHz_ = expectedHz;
expectedDutyPct_ = expectedDutyPct;
@@ -73,6 +83,64 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
if (!cpuTickHz_) return false;
#endif
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
// Progress updates keep one capture session alive. Pulse-width stages stop
// 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_);
return false;
}
if (withTx && mcpwm_capture_channel_enable(txChannel_) != ESP_OK) {
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
txCaptureEnabled_ = withTx;
running_ = true;
if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) {
running_ = false;
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
txCaptureEnabled_ = false;
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
#else
if (withTx) return false;
running_ = true;
#endif
return true;
@@ -106,7 +184,9 @@ void PulseReceiver::stop() {
running_ = false;
#if OPTICAL_USE_MCPWM_CAPTURE
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(risingChannel_);
mcpwm_capture_timer_stop(captureTimer_);
@@ -120,10 +200,15 @@ void PulseReceiver::stop() {
void PulseReceiver::resetStream() {
if (queue_) xQueueReset(queue_);
haveReorderEdge_ = false;
__atomic_store_n(&driverRingWrite_, 0U, __ATOMIC_RELEASE);
__atomic_store_n(&driverRingRead_, 0U, __ATOMIC_RELEASE);
haveLastDriverEdge_ = false;
lastDriverEdge_ = {};
driverPulseTicks_ = 0;
driverReleaseSlackTicks_ = 0;
droppedItems_ = 0;
polarityKnown_ = false;
activeStartRising_ = false;
syncEdgeCount_ = 0;
waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0;
haveRawTick_ = false;
@@ -159,39 +244,13 @@ bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
return true;
}
syncEdges_[syncEdgeCount_++] = edge;
if (syncEdgeCount_ < 3U) return false;
const uint64_t firstTicks = syncEdges_[1].tick - syncEdges_[0].tick;
const uint64_t secondTicks = syncEdges_[2].tick - syncEdges_[1].tick;
const double expectedTicks = static_cast<double>(tickHz()) * expectedDutyPct_ /
(100.0 * expectedHz_);
const double firstError = fabs(static_cast<double>(firstTicks) - expectedTicks);
const double secondError = fabs(static_cast<double>(secondTicks) - expectedTicks);
activeStartRising_ = firstError <= secondError ? syncEdges_[0].rising : syncEdges_[1].rising;
// HH/HL/LH/LL defines the active optical state explicitly. Synchronize on
// its physical starting edge instead of guessing polarity from pulse width.
if (edge.rising != activeStartRising_) return false;
polarityKnown_ = true;
Log::printf("CAPTURE", "RX polarity auto: active starts on %s, first=%lluns second=%lluns",
activeStartRising_ ? "RISING" : "FALLING",
static_cast<unsigned long long>(firstTicks * 1000000000ULL / tickHz()),
static_cast<unsigned long long>(secondTicks * 1000000000ULL / tickHz()));
bool produced = false;
if (firstError <= secondError) {
activeStart_ = syncEdges_[0].tick;
activeEnd_ = syncEdges_[1].tick;
const uint64_t periodTicks = syncEdges_[2].tick - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeEnd_ - activeStart_), tickHz()};
activeStart_ = syncEdges_[2].tick;
waitingForActiveEnd_ = true;
produced = true;
} else {
activeStart_ = syncEdges_[1].tick;
activeEnd_ = syncEdges_[2].tick;
waitingForActiveEnd_ = false;
}
syncEdgeCount_ = 0;
return produced;
activeStart_ = edge.tick;
waitingForActiveEnd_ = true;
return false;
}
uint32_t PulseReceiver::takeDroppedItems() {
@@ -199,13 +258,45 @@ uint32_t PulseReceiver::takeDroppedItems() {
}
#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,
void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return false;
const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS;
const Edge edge = {data->cap_value, static_cast<uint8_t>(rawRising)};
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;
if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__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) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return;
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 bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
const Edge edge = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level),
static_cast<uint8_t>(CaptureSource::RX)};
BaseType_t wake = pdFALSE;
if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
@@ -256,3 +347,84 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
}
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
#endif
enum class CaptureSource : uint8_t { RX, TX };
struct CaptureEvent {
uint64_t tick;
bool rising;
CaptureSource source;
};
class PulseReceiver {
public:
bool begin();
bool start(uint32_t expectedHz, float expectedDutyPct);
bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
bool activeTxLightOn);
void stop();
void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
uint32_t takeDroppedItems();
uint32_t tickHz() const;
uint32_t pulseTickHz() const { return tickHz(); }
@@ -30,32 +41,43 @@ class PulseReceiver {
bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; }
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; };
bool startCapture(bool withTx);
bool consumeEdge(const Edge &edge, PulsePeriod &period);
bool nextOrderedEdge(Edge &edge, TickType_t waitTicks);
TimedEdge extendEdge(const Edge &edge);
#if OPTICAL_USE_MCPWM_CAPTURE
bool configureDriverTxCapture(bool risingEdge);
static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t,
const mcpwm_capture_event_data_t *, void *);
mcpwm_cap_timer_handle_t captureTimer_ = nullptr;
mcpwm_cap_channel_handle_t risingChannel_ = nullptr;
mcpwm_cap_channel_handle_t fallingChannel_ = nullptr;
mcpwm_cap_channel_handle_t txChannel_ = nullptr;
uint32_t captureResolutionHz_ = 0;
#else
static void IRAM_ATTR onGpio(void *ctx);
uint32_t cpuTickHz_ = 0;
#endif
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_ = {};
bool haveReorderEdge_ = false;
volatile uint32_t droppedItems_ = 0;
volatile bool running_ = false;
volatile bool txCaptureEnabled_ = false;
uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 50.0f;
bool polarityKnown_ = false, activeStartRising_ = false;
TimedEdge syncEdges_[3] = {};
uint8_t syncEdgeCount_ = 0;
bool waitingForActiveEnd_ = true;
uint64_t activeStart_ = 0, activeEnd_ = 0;
bool haveRawTick_ = false;

View File

@@ -3,16 +3,22 @@
#include "Log.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 {
// 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);
}
bool SettingsStore::valid(const Settings &s) const {
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.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
s.minPulseIndex < countOf(MIN_PULSE_OPTIONS_NS) &&

File diff suppressed because one or more lines are too long