Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
040853e7dc |
@@ -19,7 +19,7 @@ constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7;
|
|||||||
const char *uiFailName(FailReason reason);
|
const char *uiFailName(FailReason reason);
|
||||||
|
|
||||||
const char *appStateName(AppState state) {
|
const char *appStateName(AppState state) {
|
||||||
static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
|
static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "PWM_OUTPUT", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
|
||||||
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
|
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
|
||||||
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
|
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
|
||||||
const uint8_t index = static_cast<uint8_t>(state);
|
const uint8_t index = static_cast<uint8_t>(state);
|
||||||
@@ -250,9 +250,12 @@ uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) {
|
|||||||
return value <= first ? last : static_cast<uint8_t>(value - 1U);
|
return value <= first ? last : static_cast<uint8_t>(value - 1U);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t nextMenuItem(uint8_t current, TestGroup group) {
|
uint8_t nextMenuItem(uint8_t current, TestGroup group, TestKind kind) {
|
||||||
if (group == TestGroup::BOARD)
|
if (group == TestGroup::BOARD)
|
||||||
return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U;
|
return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U;
|
||||||
|
if (kind == TestKind::PWM_OUTPUT)
|
||||||
|
return current == 0U ? 1U : current == 1U ? 2U
|
||||||
|
: current == 2U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U;
|
||||||
return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U
|
return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U
|
||||||
: static_cast<uint8_t>(current + 1U);
|
: static_cast<uint8_t>(current + 1U);
|
||||||
}
|
}
|
||||||
@@ -356,6 +359,12 @@ void App::update() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state_ == AppState::PWM_OUTPUT) {
|
||||||
|
if (startEvent == ButtonEvent::LONG) stopPwmOutput();
|
||||||
|
else if (modeEvent != ButtonEvent::NONE)
|
||||||
|
Log::event("ACTION", "MODE ignored while PWM output is active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
||||||
startEvent == ButtonEvent::LONG) { abortTest(); return; }
|
startEvent == ButtonEvent::LONG) { abortTest(); return; }
|
||||||
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
||||||
@@ -399,7 +408,8 @@ void App::update() {
|
|||||||
if (modeEvent == ButtonEvent::SHORT) {
|
if (modeEvent == ButtonEvent::SHORT) {
|
||||||
leaveOpticalCalibration();
|
leaveOpticalCalibration();
|
||||||
menuItem_ = nextMenuItem(menuItem_,
|
menuItem_ = nextMenuItem(menuItem_,
|
||||||
static_cast<TestGroup>(settings_.testGroup));
|
static_cast<TestGroup>(settings_.testGroup),
|
||||||
|
static_cast<TestKind>(settings_.testKind));
|
||||||
Log::printf("ACTION", "menu item selected index=%u", menuItem_);
|
Log::printf("ACTION", "menu item selected index=%u", menuItem_);
|
||||||
showMenu();
|
showMenu();
|
||||||
}
|
}
|
||||||
@@ -529,12 +539,12 @@ void App::printSerialHelp() {
|
|||||||
Serial.println(" set group optics|board");
|
Serial.println(" set group optics|board");
|
||||||
Serial.println(" set boardtest adc|pwm|rx");
|
Serial.println(" set boardtest adc|pwm|rx");
|
||||||
Serial.println(" set role solo|master|slave");
|
Serial.println(" set role solo|master|slave");
|
||||||
Serial.println(" set test optical|driver");
|
Serial.println(" set test optical|driver|pwm (driver/pwm: SOLO only)");
|
||||||
Serial.println(" set frequency 500|1000|2000|5000|10000|25000");
|
Serial.println(" set frequency 500|1000|2000|5000|10000");
|
||||||
Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000");
|
Serial.println(" set max 2000|5000|10000|50000|100000|500000 (PWM pulse in SOLO PWM)");
|
||||||
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
|
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
|
||||||
Serial.println(" set accuracy 1|2|5|10");
|
Serial.println(" set accuracy 1|2|5|10");
|
||||||
Serial.println(" set time 100|250|500|1000|2000|5000 (ms)");
|
Serial.println(" set time 100|250|500|1000|2000|5000|60000 (ms)");
|
||||||
Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
|
Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,6 +615,9 @@ void App::handleSerialCommand(char *line) {
|
|||||||
else if (state_ == AppState::BOARD_TEST) {
|
else if (state_ == AppState::BOARD_TEST) {
|
||||||
Serial.println("OK board test stopped");
|
Serial.println("OK board test stopped");
|
||||||
stopBoardTest();
|
stopBoardTest();
|
||||||
|
} else if (state_ == AppState::PWM_OUTPUT) {
|
||||||
|
Serial.println("OK PWM output stopped");
|
||||||
|
stopPwmOutput();
|
||||||
} else if (state_ == AppState::MENU) {
|
} else if (state_ == AppState::MENU) {
|
||||||
leaveOpticalCalibration();
|
leaveOpticalCalibration();
|
||||||
state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed");
|
state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed");
|
||||||
@@ -659,6 +672,9 @@ void App::handleSerialCommand(char *line) {
|
|||||||
else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
|
else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
|
||||||
static_cast<Role>(settings_.role) == Role::SOLO) {
|
static_cast<Role>(settings_.role) == Role::SOLO) {
|
||||||
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
|
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
|
||||||
|
} else if (!strcmp(value, "pwm") &&
|
||||||
|
static_cast<Role>(settings_.role) == Role::SOLO) {
|
||||||
|
settings_.testKind = static_cast<uint8_t>(TestKind::PWM_OUTPUT); accepted = true;
|
||||||
}
|
}
|
||||||
} else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
|
} else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
|
||||||
const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
|
const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
|
||||||
@@ -705,6 +721,8 @@ void App::cycleRunMode() {
|
|||||||
const TestKind kind = static_cast<TestKind>(settings_.testKind);
|
const TestKind kind = static_cast<TestKind>(settings_.testKind);
|
||||||
if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
|
if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
|
||||||
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
|
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
|
||||||
|
} else if (role == Role::SOLO && kind != TestKind::PWM_OUTPUT) {
|
||||||
|
settings_.testKind = static_cast<uint8_t>(TestKind::PWM_OUTPUT);
|
||||||
} else if (role == Role::SOLO) {
|
} else if (role == Role::SOLO) {
|
||||||
settings_.role = static_cast<uint8_t>(Role::MASTER);
|
settings_.role = static_cast<uint8_t>(Role::MASTER);
|
||||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||||
@@ -725,11 +743,12 @@ void App::sanitizeRange() {
|
|||||||
settings_.boardTest = static_cast<uint8_t>(defaultBoardTest());
|
settings_.boardTest = static_cast<uint8_t>(defaultBoardTest());
|
||||||
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
|
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
|
||||||
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
||||||
if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER))
|
if (settings_.testKind > static_cast<uint8_t>(TestKind::PWM_OUTPUT))
|
||||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||||
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
|
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
|
||||||
settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
|
settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
|
||||||
if (settings_.role != static_cast<uint8_t>(Role::SOLO) ||
|
if ((settings_.role != static_cast<uint8_t>(Role::SOLO) &&
|
||||||
|
settings_.testKind != static_cast<uint8_t>(TestKind::OPTICAL)) ||
|
||||||
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
|
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
|
||||||
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
|
||||||
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
|
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
|
||||||
@@ -760,8 +779,8 @@ void App::changeMenu(int d) {
|
|||||||
} else if (menuItem_ == 2) {
|
} else if (menuItem_ == 2) {
|
||||||
const uint8_t last = lastValidMaxPulseIndex(
|
const uint8_t last = lastValidMaxPulseIndex(
|
||||||
PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
|
PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
|
||||||
const uint8_t first = firstMaxPulseIndexAtLeast(
|
const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT
|
||||||
MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
|
? 0U : firstMaxPulseIndexAtLeast(MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
|
||||||
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
|
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
|
||||||
first, last, d);
|
first, last, d);
|
||||||
} else if (menuItem_ == 3) {
|
} else if (menuItem_ == 3) {
|
||||||
@@ -811,7 +830,8 @@ void App::showMenu() {
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
|
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
|
||||||
label = UiText::MENU_MAX_PULSE;
|
label = static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT
|
||||||
|
? UiText::MENU_PWM_PULSE : UiText::MENU_MAX_PULSE;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
|
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
|
||||||
@@ -847,7 +867,8 @@ void App::showMenu() {
|
|||||||
default: return;
|
default: return;
|
||||||
}
|
}
|
||||||
formatMenuLine(label, value, one, sizeof(one));
|
formatMenuLine(label, value, one, sizeof(one));
|
||||||
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD)
|
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD ||
|
||||||
|
static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT)
|
||||||
snprintf(total, sizeof(total), "%s", UiText::BOARD_READY);
|
snprintf(total, sizeof(total), "%s", UiText::BOARD_READY);
|
||||||
else
|
else
|
||||||
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
||||||
@@ -900,6 +921,10 @@ void App::startTest() {
|
|||||||
startBoardTest();
|
startBoardTest();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT) {
|
||||||
|
startPwmOutput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
|
||||||
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
|
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
|
||||||
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
|
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
|
||||||
@@ -1019,6 +1044,37 @@ void App::stopBoardTest() {
|
|||||||
showIdle();
|
showIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::startPwmOutput() {
|
||||||
|
params_ = store_.params(settings_);
|
||||||
|
requestedHz_ = params_.frequencyHz;
|
||||||
|
requestedPulseNs_ = params_.maxPulseNs;
|
||||||
|
stageIndex_ = 0;
|
||||||
|
stageCount_ = 1;
|
||||||
|
pwm_.configureActiveLight(true);
|
||||||
|
if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) {
|
||||||
|
Log::printf("PWM", "start failed: frequency=%luHz pulse=%luns",
|
||||||
|
requestedHz_, requestedPulseNs_);
|
||||||
|
finish(false, FailReason::RESOLUTION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state_ = AppState::PWM_OUTPUT;
|
||||||
|
char target[32], one[48];
|
||||||
|
formatTarget(actual_.actualHz, actual_.actualPulseNs, target, sizeof(target));
|
||||||
|
snprintf(one, sizeof(one), "%s: %s", uiTestName(TestKind::PWM_OUTPUT), target);
|
||||||
|
display_.show(one, UiText::BOARD_STOP);
|
||||||
|
Log::printf("PWM", "continuous output GPIO=%u requested=%luHz/%luns actual=%luHz/%luns duty=%.2f%%",
|
||||||
|
GPIO_PWM, requestedHz_, requestedPulseNs_, actual_.actualHz,
|
||||||
|
actual_.actualPulseNs, actual_.actualDutyPct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::stopPwmOutput() {
|
||||||
|
Log::event("PWM", "continuous output stopped");
|
||||||
|
pwm_.stop();
|
||||||
|
state_ = AppState::IDLE;
|
||||||
|
setActivePerformance(false);
|
||||||
|
showIdle();
|
||||||
|
}
|
||||||
|
|
||||||
bool App::armSlave(bool preserveDisplay) {
|
bool App::armSlave(bool preserveDisplay) {
|
||||||
setActivePerformance(false);
|
setActivePerformance(false);
|
||||||
pwm_.stop();
|
pwm_.stop();
|
||||||
@@ -1652,6 +1708,11 @@ void App::printConfiguration() {
|
|||||||
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
|
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
|
||||||
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
|
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
|
||||||
|
if (static_cast<TestKind>(settings_.testKind) == TestKind::PWM_OUTPUT) {
|
||||||
|
Serial.printf("Continuous PWM output %lu Hz, pulse %lu ns; RX unused\n",
|
||||||
|
params_.frequencyHz, params_.maxPulseNs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
|
if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
|
||||||
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
|
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
|
||||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
|
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "SettingsStore.h"
|
#include "SettingsStore.h"
|
||||||
|
|
||||||
enum class AppState : uint8_t {
|
enum class AppState : uint8_t {
|
||||||
IDLE, MENU, BOARD_TEST, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY,
|
IDLE, MENU, BOARD_TEST, PWM_OUTPUT, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY,
|
||||||
MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
|
MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
|
||||||
SLAVE_WAIT_ACK, FINISHED
|
SLAVE_WAIT_ACK, FINISHED
|
||||||
};
|
};
|
||||||
@@ -33,6 +33,8 @@ class App {
|
|||||||
void startBoardTest();
|
void startBoardTest();
|
||||||
void updateBoardTest(uint32_t now);
|
void updateBoardTest(uint32_t now);
|
||||||
void stopBoardTest();
|
void stopBoardTest();
|
||||||
|
void startPwmOutput();
|
||||||
|
void stopPwmOutput();
|
||||||
bool armSlave(bool preserveDisplay = false);
|
bool armSlave(bool preserveDisplay = false);
|
||||||
bool prepareStage(bool showProgress = true);
|
bool prepareStage(bool showProgress = true);
|
||||||
bool startLocalMeasurement(float hz, float duty);
|
bool startLocalMeasurement(float hz, float duty);
|
||||||
|
|||||||
@@ -2,18 +2,21 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Uncomment to build the standalone PWM/RX pulse probe instead of the tester UI.
|
||||||
|
#define SIGNAL_PROBE_FIRMWARE
|
||||||
|
|
||||||
// ------------------------- Hardware configuration -------------------------
|
// ------------------------- Hardware configuration -------------------------
|
||||||
// Enabled for the hand-wired prototype. Comment out for the production PCB.
|
// Enabled for the hand-wired prototype. Comment out for the production PCB.
|
||||||
// Both profiles map S3 signals by physical header position with 5V/GND aligned.
|
// Both profiles map S3 signals by physical header position with 5V/GND aligned.
|
||||||
|
|
||||||
//#define MAKETKA
|
#define MAKETKA
|
||||||
|
|
||||||
// Select exactly one populated receiver circuit. Use
|
// Select exactly one populated receiver circuit. Use
|
||||||
// BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX.
|
// BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX.
|
||||||
#define BOARD_RX_INTERFACE_ADC 1
|
#define BOARD_RX_INTERFACE_ADC 1
|
||||||
#define BOARD_RX_INTERFACE_DIGITAL 2
|
#define BOARD_RX_INTERFACE_DIGITAL 2
|
||||||
#ifndef BOARD_RX_INTERFACE
|
#ifndef BOARD_RX_INTERFACE
|
||||||
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC
|
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \
|
#if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ constexpr const char *ROLE_NAMES[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr const char *TEST_NAMES[] = {
|
constexpr const char *TEST_NAMES[] = {
|
||||||
"ОПТИКА", "ДРАЙВЕР"
|
"ОПТИКА", "ДРАЙВЕР", "ШИМ"
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr const char *TEST_GROUP_NAMES[] = {
|
constexpr const char *TEST_GROUP_NAMES[] = {
|
||||||
@@ -54,6 +54,7 @@ constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
|
|||||||
constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
|
constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
|
||||||
constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:";
|
constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:";
|
||||||
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
|
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
|
||||||
|
constexpr const char *MENU_PWM_PULSE = "ИМПУЛЬС ШИМ:";
|
||||||
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
|
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
|
||||||
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
|
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
|
||||||
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
|
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
|
||||||
@@ -97,7 +98,7 @@ constexpr const char *ROLE_NAMES[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr const char *TEST_NAMES[] = {
|
constexpr const char *TEST_NAMES[] = {
|
||||||
"OPTICAL", "DRIVER"
|
"OPTICAL", "DRIVER", "PWM"
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr const char *TEST_GROUP_NAMES[] = {
|
constexpr const char *TEST_GROUP_NAMES[] = {
|
||||||
@@ -133,6 +134,7 @@ constexpr const char *START_RUN = "READY TO START";
|
|||||||
constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
|
constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
|
||||||
constexpr const char *MENU_TEST_GROUP = "TEST GROUP:";
|
constexpr const char *MENU_TEST_GROUP = "TEST GROUP:";
|
||||||
constexpr const char *MENU_MAX_PULSE = "MAX PULSE:";
|
constexpr const char *MENU_MAX_PULSE = "MAX PULSE:";
|
||||||
|
constexpr const char *MENU_PWM_PULSE = "PWM PULSE:";
|
||||||
constexpr const char *MENU_MIN_PULSE = "MIN PULSE:";
|
constexpr const char *MENU_MIN_PULSE = "MIN PULSE:";
|
||||||
constexpr const char *MENU_ACCURACY = "ACCURACY:";
|
constexpr const char *MENU_ACCURACY = "ACCURACY:";
|
||||||
constexpr const char *MENU_TEST_TIME = "TEST TIME:";
|
constexpr const char *MENU_TEST_TIME = "TEST TIME:";
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ const char *testGroupName(TestGroup group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char *testKindName(TestKind kind) {
|
const char *testKindName(TestKind kind) {
|
||||||
static const char *names[] = {"OPTICAL", "DRIVER"};
|
static const char *names[] = {"OPTICAL", "DRIVER", "PWM OUTPUT"};
|
||||||
const uint8_t i = static_cast<uint8_t>(kind);
|
const uint8_t i = static_cast<uint8_t>(kind);
|
||||||
return i < 2 ? names[i] : "?";
|
return i < 3 ? names[i] : "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *boardTestName(BoardTest test) {
|
const char *boardTestName(BoardTest test) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
||||||
enum class TestGroup : uint8_t { OPTICS, BOARD };
|
enum class TestGroup : uint8_t { OPTICS, BOARD };
|
||||||
enum class TestKind : uint8_t { OPTICAL, DRIVER };
|
enum class TestKind : uint8_t { OPTICAL, DRIVER, PWM_OUTPUT };
|
||||||
enum class BoardTest : uint8_t { ADC, PWM_OUTPUT, RX_INPUT };
|
enum class BoardTest : uint8_t { ADC, PWM_OUTPUT, RX_INPUT };
|
||||||
enum class LightCode : uint8_t { HH, HL, LH, LL };
|
enum class LightCode : uint8_t { HH, HL, LH, LL };
|
||||||
enum class FailReason : uint8_t {
|
enum class FailReason : uint8_t {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
#include <esp32-hal-cpu.h>
|
#include <esp32-hal-cpu.h>
|
||||||
#include <soc/gpio_struct.h>
|
#include <soc/gpio_reg.h>
|
||||||
|
#include <soc/soc.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#if CONFIG_IDF_TARGET_ESP32C3
|
#if CONFIG_IDF_TARGET_ESP32C3
|
||||||
#include <riscv/rv_utils.h>
|
#include <riscv/rv_utils.h>
|
||||||
@@ -204,7 +205,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() {
|
|||||||
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
|
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
uint32_t levels = GPIO.in.val & PIN_MASK;
|
uint32_t levels = REG_READ(GPIO_IN_REG) & PIN_MASK;
|
||||||
uint32_t nextStart = 0;
|
uint32_t nextStart = 0;
|
||||||
uint32_t windowEnd = 0;
|
uint32_t windowEnd = 0;
|
||||||
uint32_t lastTxStart = 0;
|
uint32_t lastTxStart = 0;
|
||||||
@@ -216,7 +217,7 @@ void IRAM_ATTR DriverTest::pollTaskLoop() {
|
|||||||
uint32_t interruptState = 0;
|
uint32_t interruptState = 0;
|
||||||
|
|
||||||
auto sampleOnce = [&]() {
|
auto sampleOnce = [&]() {
|
||||||
const uint32_t current = GPIO.in.val & PIN_MASK;
|
const uint32_t current = REG_READ(GPIO_IN_REG) & PIN_MASK;
|
||||||
if (current == levels) return;
|
if (current == levels) return;
|
||||||
const uint32_t now = esp_cpu_get_cycle_count();
|
const uint32_t now = esp_cpu_get_cycle_count();
|
||||||
const uint32_t changed = current ^ levels;
|
const uint32_t changed = current ^ levels;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
#include "Config.h"
|
||||||
|
#ifdef SIGNAL_PROBE_FIRMWARE
|
||||||
|
#include "SignalProbe.h"
|
||||||
|
SignalProbe app;
|
||||||
|
#else
|
||||||
#include "App.h"
|
#include "App.h"
|
||||||
App app;
|
App app;
|
||||||
|
#endif
|
||||||
|
|
||||||
void setup() { app.begin(); }
|
void setup() { app.begin(); }
|
||||||
void loop() { app.update(); }
|
void loop() { app.update(); }
|
||||||
|
|||||||
@@ -88,6 +88,19 @@ bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
|
|||||||
return startCapture(false);
|
return startCapture(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PulseReceiver::startRaw() {
|
||||||
|
if (!queue_ || running_) return false;
|
||||||
|
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||||
|
if (!captureTimer_) return false;
|
||||||
|
#endif
|
||||||
|
#if !OPTICAL_USE_MCPWM_CAPTURE
|
||||||
|
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
|
||||||
|
if (!cpuTickHz_) return false;
|
||||||
|
#endif
|
||||||
|
resetStream();
|
||||||
|
return startCapture(false);
|
||||||
|
}
|
||||||
|
|
||||||
#if OPTICAL_USE_MCPWM_CAPTURE
|
#if OPTICAL_USE_MCPWM_CAPTURE
|
||||||
bool PulseReceiver::configureDriverTxCapture(bool risingEdge) {
|
bool PulseReceiver::configureDriverTxCapture(bool risingEdge) {
|
||||||
if (running_) return false;
|
if (running_) return false;
|
||||||
@@ -376,6 +389,18 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t PulseReceiver::readRawEdges(CaptureEvent *events, size_t capacity,
|
||||||
|
TickType_t waitTicks) {
|
||||||
|
if (!events || !capacity) return 0;
|
||||||
|
size_t count = 0;
|
||||||
|
Edge edge = {};
|
||||||
|
while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) {
|
||||||
|
const TimedEdge timed = extendEdge(edge);
|
||||||
|
events[count++] = {timed.tick, timed.rising, CaptureSource::RX};
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
|
||||||
TickType_t waitTicks) {
|
TickType_t waitTicks) {
|
||||||
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
|
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ class PulseReceiver {
|
|||||||
public:
|
public:
|
||||||
bool begin();
|
bool begin();
|
||||||
bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
|
bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
|
||||||
|
bool startRaw();
|
||||||
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
|
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
|
||||||
bool activeTxLightOn);
|
bool activeTxLightOn);
|
||||||
void stop();
|
void stop();
|
||||||
void resetStream();
|
void resetStream();
|
||||||
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
|
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
|
||||||
|
size_t readRawEdges(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
|
||||||
size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
|
size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
|
||||||
uint32_t takeDroppedItems();
|
uint32_t takeDroppedItems();
|
||||||
uint32_t tickHz() const;
|
uint32_t tickHz() const;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
|
namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
|
||||||
|
|
||||||
void SettingsStore::defaults(Settings &s) const {
|
void SettingsStore::defaults(Settings &s) const {
|
||||||
// 2 kHz, 200 us .. 2 us, 5%, 1 s.
|
// 2 kHz, 100 us .. 2 us, 5%, 1 s.
|
||||||
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO),
|
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO),
|
||||||
static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH),
|
static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH),
|
||||||
2, 6, 3, 2, 3, static_cast<uint8_t>(TestGroup::OPTICS),
|
2, 4, 3, 2, 3, static_cast<uint8_t>(TestGroup::OPTICS),
|
||||||
static_cast<uint8_t>(BoardTest::ADC), 0};
|
static_cast<uint8_t>(BoardTest::ADC), 0};
|
||||||
s.checksum = settingsChecksum(s);
|
s.checksum = settingsChecksum(s);
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,9 @@ bool SettingsStore::valid(const Settings &s) const {
|
|||||||
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
|
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
|
||||||
s.testGroup <= static_cast<uint8_t>(TestGroup::BOARD) &&
|
s.testGroup <= static_cast<uint8_t>(TestGroup::BOARD) &&
|
||||||
s.boardTest <= static_cast<uint8_t>(BoardTest::RX_INPUT) &&
|
s.boardTest <= static_cast<uint8_t>(BoardTest::RX_INPUT) &&
|
||||||
s.testKind <= static_cast<uint8_t>(TestKind::DRIVER) &&
|
s.testKind <= static_cast<uint8_t>(TestKind::PWM_OUTPUT) &&
|
||||||
s.lightCode <= static_cast<uint8_t>(LightCode::LL) &&
|
s.lightCode <= static_cast<uint8_t>(LightCode::LL) &&
|
||||||
(s.testKind != static_cast<uint8_t>(TestKind::DRIVER) ||
|
(s.testKind == static_cast<uint8_t>(TestKind::OPTICAL) ||
|
||||||
s.role == static_cast<uint8_t>(Role::SOLO)) &&
|
s.role == static_cast<uint8_t>(Role::SOLO)) &&
|
||||||
s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
|
s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
|
||||||
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
|
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
|
||||||
|
|||||||
236
OpticalChannelTester/SignalProbe.cpp
Normal file
236
OpticalChannelTester/SignalProbe.cpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#include "SignalProbe.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include <esp32-hal-cpu.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t DISPLAY_INTERVAL_MS = 250;
|
||||||
|
constexpr uint8_t PERIOD_TOLERANCE_PCT = 10;
|
||||||
|
|
||||||
|
bool pwmPointAvailable(uint32_t frequencyHz, uint32_t pulseNs) {
|
||||||
|
return static_cast<uint64_t>(frequencyHz) * pulseNs < 1000000000ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void formatWidth(uint64_t nanoseconds, char *out, size_t size) {
|
||||||
|
if (nanoseconds < 1000ULL)
|
||||||
|
snprintf(out, size, "%lluns", nanoseconds);
|
||||||
|
else if (nanoseconds < 1000000ULL)
|
||||||
|
snprintf(out, size, "%.2fus", nanoseconds / 1000.0);
|
||||||
|
else
|
||||||
|
snprintf(out, size, "%.2fms", nanoseconds / 1000000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void formatPulseCount(uint64_t count, char *out, size_t size) {
|
||||||
|
if (count < 1000000ULL) snprintf(out, size, "%llu", count);
|
||||||
|
else if (count < 1000000000ULL) snprintf(out, size, "%lluM", count / 1000000ULL);
|
||||||
|
else snprintf(out, size, "%lluG", count / 1000000000ULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalProbe::SignalProbe() : startButton_(GPIO_BUTTON_START),
|
||||||
|
modeButton_(GPIO_BUTTON_MODE) {}
|
||||||
|
|
||||||
|
void SignalProbe::begin() {
|
||||||
|
Serial.begin(SERIAL_BAUD);
|
||||||
|
#if ARDUINO_USB_CDC_ON_BOOT
|
||||||
|
Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
|
||||||
|
#endif
|
||||||
|
setCpuFrequencyMhz(160);
|
||||||
|
startButton_.begin();
|
||||||
|
modeButton_.begin();
|
||||||
|
pwm_.begin();
|
||||||
|
pwm_.configureActiveLight(true);
|
||||||
|
display_.begin();
|
||||||
|
receiverInitialized_ = receiver_.begin();
|
||||||
|
rxReady_ = receiverInitialized_ && captureStart();
|
||||||
|
applyPwm();
|
||||||
|
Serial.printf("Signal probe: MODE short=frequency, MODE long=pulse, START short=reset RX minimum, START long=PWM on/off, hold both=RX active level; RX=%s\n",
|
||||||
|
rxActiveHigh_ ? "HIGH" : "LOW");
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SignalProbe::captureStart() {
|
||||||
|
if (!receiver_.startRaw()) {
|
||||||
|
Serial.println("RX capture start failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
havePulseStart_ = false;
|
||||||
|
havePulseEnd_ = false;
|
||||||
|
extraEdgeInPeriod_ = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::resetMinimum() {
|
||||||
|
if (rxReady_) receiver_.stop();
|
||||||
|
rxReady_ = receiverInitialized_ && captureStart();
|
||||||
|
minTicks_ = UINT64_MAX;
|
||||||
|
pulseCount_ = 0;
|
||||||
|
edgeCount_ = 0;
|
||||||
|
rejectedPeriods_ = 0;
|
||||||
|
displayDirty_ = true;
|
||||||
|
Serial.println("RX minimum reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::updateCapture() {
|
||||||
|
if (!rxReady_) return;
|
||||||
|
CaptureEvent edges[64];
|
||||||
|
for (uint8_t batch = 0; batch < 8; ++batch) {
|
||||||
|
const size_t count = receiver_.readRawEdges(edges, countOf(edges));
|
||||||
|
if (!count) break;
|
||||||
|
uint64_t batchMinimum = minTicks_;
|
||||||
|
uint64_t batchPulses = 0;
|
||||||
|
uint64_t batchRejected = 0;
|
||||||
|
const bool activeRising = rxActiveHigh_;
|
||||||
|
const uint32_t expectedHz = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_];
|
||||||
|
const uint64_t expectedPeriod = receiver_.tickHz() / expectedHz;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
if (edges[i].rising == activeRising) {
|
||||||
|
if (havePulseStart_) {
|
||||||
|
const uint64_t period = edges[i].tick - pulseStartTick_;
|
||||||
|
const uint64_t width = havePulseEnd_
|
||||||
|
? pulseEndTick_ - pulseStartTick_ : 0;
|
||||||
|
const uint64_t error = period > expectedPeriod
|
||||||
|
? period - expectedPeriod : expectedPeriod - period;
|
||||||
|
if (havePulseEnd_ && !extraEdgeInPeriod_ && period && width && width < period &&
|
||||||
|
error * 100U <= expectedPeriod * PERIOD_TOLERANCE_PCT) {
|
||||||
|
if (width < batchMinimum) batchMinimum = width;
|
||||||
|
++batchPulses;
|
||||||
|
} else {
|
||||||
|
++batchRejected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pulseStartTick_ = edges[i].tick;
|
||||||
|
havePulseStart_ = true;
|
||||||
|
havePulseEnd_ = false;
|
||||||
|
extraEdgeInPeriod_ = false;
|
||||||
|
} else if (havePulseStart_ && !havePulseEnd_ &&
|
||||||
|
edges[i].tick > pulseStartTick_) {
|
||||||
|
pulseEndTick_ = edges[i].tick;
|
||||||
|
havePulseEnd_ = true;
|
||||||
|
} else if (havePulseStart_) {
|
||||||
|
extraEdgeInPeriod_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uint32_t dropped = receiver_.takeDroppedItems();
|
||||||
|
if (dropped) {
|
||||||
|
Serial.printf("RX capture overflow: %lu edges lost; restarting capture\n", dropped);
|
||||||
|
receiver_.stop();
|
||||||
|
rxReady_ = captureStart();
|
||||||
|
displayDirty_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
edgeCount_ += count;
|
||||||
|
pulseCount_ += batchPulses;
|
||||||
|
rejectedPeriods_ += batchRejected;
|
||||||
|
displayDirty_ = true;
|
||||||
|
if (batchMinimum < minTicks_) {
|
||||||
|
minTicks_ = batchMinimum;
|
||||||
|
displayDirty_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::applyPwm() {
|
||||||
|
pwm_.stop();
|
||||||
|
if (!pwmEnabled_) {
|
||||||
|
Serial.println("PWM off");
|
||||||
|
displayDirty_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t frequency = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_];
|
||||||
|
const uint32_t pulse = MAX_PULSE_OPTIONS_NS[pulseIndex_];
|
||||||
|
if (!pwm_.start(frequency, pulse, actual_)) {
|
||||||
|
Serial.printf("PWM start failed: %lu Hz, %lu ns\n", frequency, pulse);
|
||||||
|
pwmEnabled_ = false;
|
||||||
|
} else {
|
||||||
|
Serial.printf("PWM GPIO=%u requested=%luHz/%luns actual=%luHz/%luns\n",
|
||||||
|
GPIO_PWM, frequency, pulse, actual_.actualHz, actual_.actualPulseNs);
|
||||||
|
}
|
||||||
|
displayDirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::advanceFrequency() {
|
||||||
|
const uint8_t count = static_cast<uint8_t>(countOf(PWM_FREQUENCY_OPTIONS_HZ));
|
||||||
|
for (uint8_t step = 0; step < count; ++step) {
|
||||||
|
frequencyIndex_ = static_cast<uint8_t>((frequencyIndex_ + 1U) % count);
|
||||||
|
if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_],
|
||||||
|
MAX_PULSE_OPTIONS_NS[pulseIndex_])) break;
|
||||||
|
}
|
||||||
|
applyPwm();
|
||||||
|
resetMinimum();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::advancePulse() {
|
||||||
|
const uint8_t count = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS));
|
||||||
|
for (uint8_t step = 0; step < count; ++step) {
|
||||||
|
pulseIndex_ = static_cast<uint8_t>((pulseIndex_ + 1U) % count);
|
||||||
|
if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_],
|
||||||
|
MAX_PULSE_OPTIONS_NS[pulseIndex_])) break;
|
||||||
|
}
|
||||||
|
applyPwm();
|
||||||
|
resetMinimum();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::show() {
|
||||||
|
char frequency[16], pulse[16], first[64], second[64];
|
||||||
|
const uint32_t shownFrequency = pwm_.running() ? actual_.actualHz
|
||||||
|
: PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_];
|
||||||
|
const uint32_t shownPulse = pwm_.running() ? actual_.actualPulseNs
|
||||||
|
: MAX_PULSE_OPTIONS_NS[pulseIndex_];
|
||||||
|
Display::formatPwmFrequency(shownFrequency,
|
||||||
|
frequency, sizeof(frequency));
|
||||||
|
Display::formatPulse(shownPulse, pulse, sizeof(pulse));
|
||||||
|
snprintf(first, sizeof(first), "PWM%s %s %s", pwmEnabled_ ? "" : " OFF",
|
||||||
|
frequency, pulse);
|
||||||
|
if (!rxReady_) {
|
||||||
|
snprintf(second, sizeof(second), "RX: ERROR");
|
||||||
|
} else if (minTicks_ == UINT64_MAX) {
|
||||||
|
snprintf(second, sizeof(second), "RX E:%llu BAD:%llu", edgeCount_, rejectedPeriods_);
|
||||||
|
} else {
|
||||||
|
char width[24], count[12];
|
||||||
|
const uint64_t nanoseconds =
|
||||||
|
(minTicks_ * 1000000000ULL + receiver_.tickHz() / 2U) / receiver_.tickHz();
|
||||||
|
formatWidth(nanoseconds, width, sizeof(width));
|
||||||
|
formatPulseCount(pulseCount_, count, sizeof(count));
|
||||||
|
snprintf(second, sizeof(second), "MIN:%s N:%s", width, count);
|
||||||
|
Serial.printf("RX minimum=%lluns, periods=%llu, rejected=%llu, edges=%llu\n",
|
||||||
|
nanoseconds, pulseCount_, rejectedPeriods_, edgeCount_);
|
||||||
|
}
|
||||||
|
display_.show(first, second, 0, 0, rxActiveHigh_ ? "H" : "L");
|
||||||
|
lastDisplayMs_ = millis();
|
||||||
|
displayDirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalProbe::update() {
|
||||||
|
const uint32_t now = millis();
|
||||||
|
const ButtonEvent start = startButton_.update(now);
|
||||||
|
const ButtonEvent mode = modeButton_.update(now);
|
||||||
|
if (startButton_.pressed() && modeButton_.pressed()) {
|
||||||
|
if (!bothHeld_) {
|
||||||
|
bothHeld_ = true;
|
||||||
|
bothHeldSinceMs_ = now;
|
||||||
|
startButton_.suppressUntilRelease();
|
||||||
|
modeButton_.suppressUntilRelease();
|
||||||
|
}
|
||||||
|
if (!bothHeldHandled_ && now - bothHeldSinceMs_ >= BUTTON_LONG_PRESS_MS) {
|
||||||
|
rxActiveHigh_ = !rxActiveHigh_;
|
||||||
|
bothHeldHandled_ = true;
|
||||||
|
resetMinimum();
|
||||||
|
Serial.printf("RX active level=%s\n", rxActiveHigh_ ? "HIGH" : "LOW");
|
||||||
|
}
|
||||||
|
} else if (bothHeld_) {
|
||||||
|
if (!startButton_.pressed() && !modeButton_.pressed()) {
|
||||||
|
bothHeld_ = false;
|
||||||
|
bothHeldHandled_ = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode == ButtonEvent::SHORT) advanceFrequency();
|
||||||
|
else if (mode == ButtonEvent::LONG) advancePulse();
|
||||||
|
if (start == ButtonEvent::SHORT) resetMinimum();
|
||||||
|
else if (start == ButtonEvent::LONG) {
|
||||||
|
pwmEnabled_ = !pwmEnabled_;
|
||||||
|
applyPwm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCapture();
|
||||||
|
if (displayDirty_ && now - lastDisplayMs_ >= DISPLAY_INTERVAL_MS) show();
|
||||||
|
}
|
||||||
49
OpticalChannelTester/SignalProbe.h
Normal file
49
OpticalChannelTester/SignalProbe.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Buttons.h"
|
||||||
|
#include "Display.h"
|
||||||
|
#include "Pwm.h"
|
||||||
|
#include "Receiver.h"
|
||||||
|
|
||||||
|
class SignalProbe {
|
||||||
|
public:
|
||||||
|
SignalProbe();
|
||||||
|
void begin();
|
||||||
|
void update();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool captureStart();
|
||||||
|
void resetMinimum();
|
||||||
|
void updateCapture();
|
||||||
|
void advanceFrequency();
|
||||||
|
void advancePulse();
|
||||||
|
void applyPwm();
|
||||||
|
void show();
|
||||||
|
|
||||||
|
Button startButton_, modeButton_;
|
||||||
|
Display display_;
|
||||||
|
PwmGenerator pwm_;
|
||||||
|
PulseReceiver receiver_;
|
||||||
|
ActualPwm actual_ = {};
|
||||||
|
uint8_t frequencyIndex_ = 2;
|
||||||
|
uint8_t pulseIndex_ = 0;
|
||||||
|
bool pwmEnabled_ = true;
|
||||||
|
bool rxActiveHigh_ = RX_LIGHT_ON_GPIO_LEVEL == HIGH;
|
||||||
|
bool receiverInitialized_ = false;
|
||||||
|
bool rxReady_ = false;
|
||||||
|
bool havePulseStart_ = false;
|
||||||
|
bool havePulseEnd_ = false;
|
||||||
|
bool extraEdgeInPeriod_ = false;
|
||||||
|
bool displayDirty_ = true;
|
||||||
|
uint64_t pulseStartTick_ = 0;
|
||||||
|
uint64_t pulseEndTick_ = 0;
|
||||||
|
uint64_t minTicks_ = UINT64_MAX;
|
||||||
|
uint64_t pulseCount_ = 0;
|
||||||
|
uint64_t edgeCount_ = 0;
|
||||||
|
uint64_t rejectedPeriods_ = 0;
|
||||||
|
uint32_t lastDisplayMs_ = 0;
|
||||||
|
uint32_t bothHeldSinceMs_ = 0;
|
||||||
|
bool bothHeld_ = false;
|
||||||
|
bool bothHeldHandled_ = false;
|
||||||
|
};
|
||||||
45
README.md
45
README.md
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
- неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`;
|
- неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`;
|
||||||
- общие для оптического и драйверного тестов частота PWM и диапазон импульсов;
|
- общие для оптического и драйверного тестов частота PWM и диапазон импульсов;
|
||||||
- четыре рабочих режима: `SOLO OPTICAL`, `SOLO DRIVER` (только S3),
|
- пять рабочих режимов: `SOLO OPTICAL`, `SOLO DRIVER` (только S3),
|
||||||
`MASTER OPTICAL`, `SLAVE OPTICAL`;
|
`SOLO PWM`, `MASTER OPTICAL`, `SLAVE OPTICAL`;
|
||||||
- настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте
|
- настраиваемый для теста драйвера код `HH`, `HL`, `LH`, `LL`; в тесте
|
||||||
оптики полярность входного сигнала определяется автоматически;
|
оптики полярность входного сигнала определяется автоматически;
|
||||||
- аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса;
|
- аппаратные LEDC (C3) и MCPWM (S3) с расчётом реально получившихся частоты и длительности импульса;
|
||||||
@@ -40,7 +40,8 @@ Arduino sketch находится в каталоге `OpticalChannelTester`:
|
|||||||
- `Receiver.*` — MCPWM Capture на S3 / совместимый GPIO fallback;
|
- `Receiver.*` — MCPWM Capture на S3 / совместимый GPIO fallback;
|
||||||
- `Measurement.*` — строгая проверка периодов;
|
- `Measurement.*` — строгая проверка периодов;
|
||||||
- `Protocol.*`, `Radio.*` — ESP-NOW;
|
- `Protocol.*`, `Radio.*` — ESP-NOW;
|
||||||
- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE.
|
- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE;
|
||||||
|
- `SignalProbe.*` — тестовый вариант прошивки PWM/Rx.
|
||||||
|
|
||||||
## Требования Arduino IDE
|
## Требования Arduino IDE
|
||||||
|
|
||||||
@@ -187,11 +188,17 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE
|
|||||||
|
|
||||||
В ожидании в группе `ОПТИКА` на S3:
|
В ожидании в группе `ОПТИКА` на S3:
|
||||||
|
|
||||||
- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → MASTER ОПТИКА → SLAVE ОПТИКА`;
|
- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → SOLO ШИМ → MASTER ОПТИКА → SLAVE ОПТИКА`;
|
||||||
- MODE long: открыть настройки;
|
- MODE long: открыть настройки;
|
||||||
- START short: начать тест;
|
- START short: начать тест;
|
||||||
- START long во время теста: ABORT, PWM немедленно выключается.
|
- START long во время теста: ABORT, PWM немедленно выключается.
|
||||||
|
|
||||||
|
На C3 режим `SOLO ДРАЙВЕР` пропускается. В `SOLO ШИМ` выход непрерывно выдаёт
|
||||||
|
выбранную частоту и длительность импульса без проверки Rx. Частота задаётся
|
||||||
|
пунктом `ЧАСТОТА ШИМ`, длительность — пунктом `ИМПУЛЬС ШИМ` (общим с
|
||||||
|
`МАКС. ИМПУЛЬС` остальных режимов). Удержание START или команда `stop` в
|
||||||
|
Serial выключает выход.
|
||||||
|
|
||||||
В ожидании в группе `ПЛАТА`:
|
В ожидании в группе `ПЛАТА`:
|
||||||
|
|
||||||
- MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`;
|
- MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`;
|
||||||
@@ -256,6 +263,36 @@ START и MODE будят устройство. Первое, пробуждаю
|
|||||||
5. время выборки.
|
5. время выборки.
|
||||||
6. код света `HH`, `HL`, `LH`, `LL` — только для теста драйвера.
|
6. код света `HH`, `HL`, `LH`, `LL` — только для теста драйвера.
|
||||||
|
|
||||||
|
В `SOLO ШИМ` меню показывает только частоту и длительность одного импульса.
|
||||||
|
|
||||||
|
### Тестовая прошивка PWM/Rx
|
||||||
|
|
||||||
|
Для отдельной проверки сигнала раскомментируйте `#define SIGNAL_PROBE_FIRMWARE`
|
||||||
|
в `OpticalChannelTester/Config.h` и загрузите тот же скетч. Эта прошивка
|
||||||
|
при включённом выходе выдаёт выбранный PWM на `GPIO_PWM` и независимо измеряет на
|
||||||
|
`GPIO_RX` минимальную длительность активного импульса и число пойманных
|
||||||
|
импульсов. Начальный активный уровень входа задаёт `RX_LIGHT_ON_GPIO_LEVEL`.
|
||||||
|
Результат показывается на OLED и в Serial
|
||||||
|
115200; при отсутствии OLED достаточно Serial.
|
||||||
|
|
||||||
|
- MODE коротко: следующая частота из `PWM_FREQUENCY_OPTIONS_HZ`;
|
||||||
|
- MODE удержать: следующая длительность из `MAX_PULSE_OPTIONS_NS`;
|
||||||
|
- START коротко: сбросить найденный минимум и начать измерение заново;
|
||||||
|
- START удержать: выключить или включить PWM;
|
||||||
|
- START и MODE удержать вместе: переключить активный уровень Rx между HIGH и
|
||||||
|
LOW и сбросить накопленные измерения.
|
||||||
|
|
||||||
|
Верхняя строка OLED показывает частоту и длительность импульса PWM; `OFF`
|
||||||
|
означает выключенный выход. Буква `H` или `L` справа показывает текущий
|
||||||
|
активный уровень Rx. Нижняя строка показывает минимум и число пойманных
|
||||||
|
импульсов.
|
||||||
|
|
||||||
|
Значения, при которых импульс не помещается в период, пропускаются. В статистику
|
||||||
|
Rx попадают только полные периоды выбранной частоты; число фронтов и отброшенных
|
||||||
|
периодов видно на экране, пока подходящие импульсы не найдены. Изменение PWM
|
||||||
|
сбрасывает накопленные измерения. Для возврата к обычному тестеру закомментируйте
|
||||||
|
`SIGNAL_PROBE_FIRMWARE` и загрузите скетч снова.
|
||||||
|
|
||||||
Экран активного уровня:
|
Экран активного уровня:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
Reference in New Issue
Block a user