From 8699f8e4eb0a02f99d66573ac3f6d01399b36b11 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Sun, 23 Aug 2026 18:32:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=BE=D1=89=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B0=D1=8E=D1=89=D0=B5=D0=B9=20=D0=BE=D0=BF=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpticalChannelTester/App.cpp | 75 +++++++++++++++++++++---- OpticalChannelTester/App.h | 7 +++ OpticalChannelTester/Config.h | 35 +++++++++++- OpticalChannelTester/Config_Text.h | 2 + OpticalChannelTester/OpticalCurrent.cpp | 74 ++++++++++++++++++++++++ OpticalChannelTester/OpticalCurrent.h | 18 ++++++ README.md | 25 +++++++++ 7 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 OpticalChannelTester/OpticalCurrent.cpp create mode 100644 OpticalChannelTester/OpticalCurrent.h diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index d273212..755e9e7 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -14,6 +14,8 @@ #include namespace { +constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7; + const char *uiFailName(FailReason reason); const char *appStateName(AppState state) { @@ -248,6 +250,13 @@ uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) { return value <= first ? last : static_cast(value - 1U); } +uint8_t nextMenuItem(uint8_t current, TestGroup group) { + if (group == TestGroup::BOARD) + return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U; + return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U + : static_cast(current + 1U); +} + bool parseUnsigned(const char *text, uint32_t &value) { if (!text || !*text || *text == '-') return false; char *end = nullptr; @@ -388,18 +397,25 @@ void App::update() { } if (state_ == AppState::MENU) { if (modeEvent == ButtonEvent::SHORT) { - const uint8_t menuCount = static_cast(settings_.testGroup) == TestGroup::BOARD ? 1U : 7U; - menuItem_ = (menuItem_ + 1U) % menuCount; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); + leaveOpticalCalibration(); + menuItem_ = nextMenuItem(menuItem_, + static_cast(settings_.testGroup)); + Log::printf("ACTION", "menu item selected index=%u", menuItem_); + showMenu(); } else if (modeEvent == ButtonEvent::LONG) { + leaveOpticalCalibration(); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED"); state_ = AppState::IDLE; printConfiguration(); if (static_cast(settings_.testGroup) == TestGroup::OPTICS && static_cast(settings_.role) == Role::SLAVE) armSlave(); else showIdle(); - } else if (startEvent == ButtonEvent::SHORT) changeMenu(+1); - else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1); + } else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM && + startEvent == ButtonEvent::SHORT) changeMenu(+1); + else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM && + (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT)) changeMenu(-1); + updateOpticalCalibration(now); return; } if (state_ == AppState::SOLO_MEASURE) { @@ -544,6 +560,7 @@ bool App::serialSettingsMutable() const { void App::finishSerialSettingsChange() { if (state_ == AppState::SLAVE_READY) radio_.end(); + leaveOpticalCalibration(); state_ = AppState::IDLE; sanitizeRange(); params_ = store_.params(settings_); @@ -589,6 +606,7 @@ void App::handleSerialCommand(char *line) { Serial.println("OK board test stopped"); stopBoardTest(); } else if (state_ == AppState::MENU) { + leaveOpticalCalibration(); 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 { @@ -823,6 +841,9 @@ void App::showMenu() { display_.show(one, total); return; } + case MENU_OPTICAL_CALIBRATION_ITEM: + enterOpticalCalibration(millis()); + return; default: return; } formatMenuLine(label, value, one, sizeof(one)); @@ -833,6 +854,43 @@ void App::showMenu() { display_.show(one, total); } +void App::enterOpticalCalibration(uint32_t now) { + if (opticalCalibrationActive_) return; + optical_current_begin(); + pwm_.lightOn(); + opticalCalibrationActive_ = true; + opticalCalibrationStartedMs_ = now; + opticalCalibrationUpdatedMs_ = 0; + display_.show("Idiode: --.- mA", "Vcc: --.-- V"); + Log::printf("CALIB", "optical LED ON; sense GPIO=%u, VCC GPIO=%u, settling %lums", + GPIO_OPTICAL_CURRENT, GPIO_OPTICAL_VCC, OPTICAL_CURRENT_SETTLE_MS); +} + +void App::updateOpticalCalibration(uint32_t now, bool force) { + if (!opticalCalibrationActive_ || + now - opticalCalibrationStartedMs_ < OPTICAL_CURRENT_SETTLE_MS) return; + (void)force; + OpticalCurrentMeasurement reading = {}; + if (!optical_current_poll(reading)) return; + opticalCalibrationUpdatedMs_ = now; + char current[32], vcc[24]; + snprintf(current, sizeof(current), "Idiode: %.1f mA", reading.currentMa); + snprintf(vcc, sizeof(vcc), "Vcc: %.2f V", reading.vccVoltage); + display_.show(current, vcc); + Log::printf("CALIB", + "raw sense=%u vcc=%u | ADC sense=%.3fV vcc=%.3fV | Usense=%.3fV Vcc=%.3fV | Idiode=%.1fmA", + reading.senseRaw, reading.vccRaw, reading.senseAdcVoltage, + reading.vccAdcVoltage, reading.senseVoltage, reading.vccVoltage, + reading.currentMa); +} + +void App::leaveOpticalCalibration() { + if (!opticalCalibrationActive_) return; + pwm_.stop(); + opticalCalibrationActive_ = false; + Log::event("CALIB", "optical LED OFF; calibration menu left"); +} + void App::startTest() { leaveIdlePowerSave(); pwm_.stop(); @@ -1463,12 +1521,9 @@ bool App::usbHostPresent() const { } void App::setStandbyOpticalOutput() { - // 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(settings_.testGroup) == TestGroup::BOARD || - static_cast(settings_.role) == Role::SLAVE || - static_cast(settings_.testKind) == TestKind::DRIVER) pwm_.stop(); - else pwm_.active(); + // Calibration and tests may hold the transmitter active. Idle/result + // screens must always detach PWM and restore the physical light-OFF level. + pwm_.stop(); } void App::setActivePerformance(bool active) { diff --git a/OpticalChannelTester/App.h b/OpticalChannelTester/App.h index 674d1ac..504a21c 100644 --- a/OpticalChannelTester/App.h +++ b/OpticalChannelTester/App.h @@ -3,6 +3,7 @@ #include "Display.h" #include "DriverTest.h" #include "Measurement.h" +#include "OpticalCurrent.h" #include "Pwm.h" #include "Radio.h" #include "SettingsStore.h" @@ -23,6 +24,9 @@ class App { void finishInitialization(bool factoryReset); void showMenu(); void changeMenu(int direction); + void enterOpticalCalibration(uint32_t now); + void updateOpticalCalibration(uint32_t now, bool force = false); + void leaveOpticalCalibration(); void cycleRunMode(); void sanitizeRange(); void startTest(); @@ -105,6 +109,9 @@ class App { uint32_t boardTestUpdatedMs_ = 0; uint32_t boardDisplayUpdatedMs_ = 0; bool boardPwmLightOn_ = false; + bool opticalCalibrationActive_ = false; + uint32_t opticalCalibrationStartedMs_ = 0; + uint32_t opticalCalibrationUpdatedMs_ = 0; mutable uint32_t lastUsbHostSeenMs_ = 0; char serialLine_[96] = {}; uint8_t serialLineLength_ = 0; diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 63fb912..38a3abc 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -5,13 +5,15 @@ // ------------------------- Hardware configuration ------------------------- // 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. -//#define MAKETKA +#define MAKETKA // Select exactly one populated receiver circuit. Use // BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX. #define BOARD_RX_INTERFACE_ADC 1 #define BOARD_RX_INTERFACE_DIGITAL 2 -#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC +#ifndef BOARD_RX_INTERFACE +#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL +#endif #if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \ BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_DIGITAL @@ -38,12 +40,16 @@ constexpr uint8_t GPIO_RX = 4; constexpr bool BOARD_ADC_AVAILABLE = false; constexpr uint8_t GPIO_BUTTON_MODE = 0; constexpr uint8_t GPIO_BUTTON_START = 1; +constexpr uint8_t GPIO_OPTICAL_CURRENT = 2; +constexpr uint8_t GPIO_OPTICAL_VCC = 5; constexpr uint8_t GPIO_VBAT = UINT8_MAX; constexpr uint8_t GPIO_ANALOG_RX = UINT8_MAX; #else constexpr bool BOARD_ADC_AVAILABLE = true; constexpr uint8_t GPIO_BUTTON_MODE = 20; constexpr uint8_t GPIO_BUTTON_START = 10; +constexpr uint8_t GPIO_OPTICAL_CURRENT = 1; +constexpr uint8_t GPIO_OPTICAL_VCC = 5; constexpr uint8_t GPIO_VBAT = 2; constexpr uint8_t GPIO_ANALOG_RX = 0; #endif @@ -58,6 +64,8 @@ constexpr uint8_t GPIO_PWM = 12; constexpr uint8_t GPIO_RX = 13; constexpr uint8_t GPIO_BUTTON_MODE = 9; constexpr uint8_t GPIO_BUTTON_START = 10; +constexpr uint8_t GPIO_OPTICAL_CURRENT = 2; +constexpr uint8_t GPIO_OPTICAL_VCC = 6; constexpr uint8_t GPIO_SDA = 44; constexpr uint8_t GPIO_SCL = 1; constexpr uint8_t GPIO_VBAT = UINT8_MAX; @@ -70,6 +78,8 @@ constexpr uint8_t GPIO_PWM = 12; constexpr uint8_t GPIO_RX = 13; constexpr uint8_t GPIO_BUTTON_MODE = 5; constexpr uint8_t GPIO_BUTTON_START = 4; +constexpr uint8_t GPIO_OPTICAL_CURRENT = 10; +constexpr uint8_t GPIO_OPTICAL_VCC = 6; constexpr uint8_t GPIO_SDA = 44; constexpr uint8_t GPIO_SCL = 1; constexpr uint8_t GPIO_VBAT = 11; @@ -79,6 +89,22 @@ constexpr uint8_t GPIO_ANALOG_RX = 9; #error "Only ESP32-C3 and ESP32-S3 are supported" #endif +static_assert(GPIO_OPTICAL_CURRENT != GPIO_PWM && + GPIO_OPTICAL_CURRENT != GPIO_RX && + GPIO_OPTICAL_CURRENT != GPIO_BUTTON_MODE && + GPIO_OPTICAL_CURRENT != GPIO_BUTTON_START && + GPIO_OPTICAL_CURRENT != GPIO_SDA && + GPIO_OPTICAL_CURRENT != GPIO_SCL, + "Optical-current ADC GPIO conflicts with another board signal"); +static_assert(GPIO_OPTICAL_VCC != GPIO_OPTICAL_CURRENT && + GPIO_OPTICAL_VCC != GPIO_PWM && + GPIO_OPTICAL_VCC != GPIO_RX && + GPIO_OPTICAL_VCC != GPIO_BUTTON_MODE && + GPIO_OPTICAL_VCC != GPIO_BUTTON_START && + GPIO_OPTICAL_VCC != GPIO_SDA && + GPIO_OPTICAL_VCC != GPIO_SCL, + "Optical-VCC ADC GPIO conflicts with another board signal"); + constexpr uint8_t OLED_ROTATION = 0; constexpr uint8_t OLED_ADDRESS = 0x3C; @@ -89,6 +115,11 @@ constexpr uint32_t SERIAL_BAUD = 115200; constexpr uint32_t SERIAL_TX_TIMEOUT_MS = 2; constexpr uint32_t BOARD_ADC_PRINT_INTERVAL_MS = 100; constexpr uint32_t BOARD_TEST_DISPLAY_INTERVAL_MS = 250; +constexpr float OPTICAL_VCC = 5.1f; +constexpr float OPTICAL_SENSE_R = 47.0f; +constexpr float OPTICAL_DIVIDER_RATIO = 2.0f; +constexpr uint32_t OPTICAL_CURRENT_SETTLE_MS = 20; +constexpr uint32_t OPTICAL_CURRENT_AVERAGING_MS = 200; constexpr bool SERIAL_ACTION_LOG = true; constexpr bool SERIAL_LOG_TIMESTAMPS = true; constexpr bool SERIAL_MINIMAL_LOG = true; diff --git a/OpticalChannelTester/Config_Text.h b/OpticalChannelTester/Config_Text.h index 7bd5b15..687b082 100644 --- a/OpticalChannelTester/Config_Text.h +++ b/OpticalChannelTester/Config_Text.h @@ -58,6 +58,7 @@ 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 *MENU_OPTICAL_CALIBRATION = "КАЛИБР. ОПТИКИ"; constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; constexpr const char *LIGHT_AUTO = "АВТО"; constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: АВТО"; @@ -136,6 +137,7 @@ 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 *MENU_OPTICAL_CALIBRATION = "OPTICAL CALIBRATION"; constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c"; constexpr const char *LIGHT_AUTO = "AUTO"; constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: AUTO"; diff --git a/OpticalChannelTester/OpticalCurrent.cpp b/OpticalChannelTester/OpticalCurrent.cpp new file mode 100644 index 0000000..077eab7 --- /dev/null +++ b/OpticalChannelTester/OpticalCurrent.cpp @@ -0,0 +1,74 @@ +#include "OpticalCurrent.h" +#include "Config.h" + +namespace { +struct AdcAccumulator { + uint64_t senseRaw = 0; + uint64_t vccRaw = 0; + uint64_t senseMillivolts = 0; + uint64_t vccMillivolts = 0; + uint32_t samples = 0; + uint32_t startedMs = 0; +}; + +AdcAccumulator accumulator; + +void resetAccumulator(uint32_t now) { + accumulator = {}; + accumulator.startedMs = now; +} + +OpticalCurrentMeasurement finishMeasurement() { + const float senseAdcVoltage = + accumulator.senseMillivolts / (1000.0f * accumulator.samples); + const float vccAdcVoltage = + accumulator.vccMillivolts / (1000.0f * accumulator.samples); + const float senseVoltage = senseAdcVoltage * OPTICAL_DIVIDER_RATIO; + const float vccVoltage = vccAdcVoltage * OPTICAL_DIVIDER_RATIO; + const float resistorVoltage = vccVoltage - senseVoltage; + const float currentMa = resistorVoltage * 1000.0f / OPTICAL_SENSE_R; + return { + static_cast(accumulator.senseRaw / accumulator.samples), + static_cast(accumulator.vccRaw / accumulator.samples), + senseAdcVoltage, vccAdcVoltage, senseVoltage, vccVoltage, + currentMa > 0.0f ? currentMa : 0.0f + }; +} +} + +void optical_current_begin(void) { + analogReadResolution(12); + pinMode(GPIO_OPTICAL_CURRENT, INPUT); + pinMode(GPIO_OPTICAL_VCC, INPUT); + // Both 10k/10k dividers can present about 2.5 V to their ADC inputs. + analogSetPinAttenuation(GPIO_OPTICAL_CURRENT, ADC_11db); + analogSetPinAttenuation(GPIO_OPTICAL_VCC, ADC_11db); + resetAccumulator(0); +} + +bool optical_current_poll(OpticalCurrentMeasurement &measurement) { + const uint32_t now = millis(); + if (!accumulator.startedMs) resetAccumulator(now ? now : 1U); + + accumulator.senseRaw += analogRead(GPIO_OPTICAL_CURRENT); + accumulator.senseMillivolts += analogReadMilliVolts(GPIO_OPTICAL_CURRENT); + accumulator.vccRaw += analogRead(GPIO_OPTICAL_VCC); + accumulator.vccMillivolts += analogReadMilliVolts(GPIO_OPTICAL_VCC); + ++accumulator.samples; + + if (now - accumulator.startedMs < OPTICAL_CURRENT_AVERAGING_MS) return false; + measurement = finishMeasurement(); + resetAccumulator(now); + return true; +} + +OpticalCurrentMeasurement optical_get_led_measurement(void) { + OpticalCurrentMeasurement measurement = {}; + resetAccumulator(millis()); + while (!optical_current_poll(measurement)) delay(1); + return measurement; +} + +float optical_get_led_current_ma(void) { + return optical_get_led_measurement().currentMa; +} diff --git a/OpticalChannelTester/OpticalCurrent.h b/OpticalChannelTester/OpticalCurrent.h new file mode 100644 index 0000000..26cd376 --- /dev/null +++ b/OpticalChannelTester/OpticalCurrent.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +struct OpticalCurrentMeasurement { + uint16_t senseRaw; + uint16_t vccRaw; + float senseAdcVoltage; + float vccAdcVoltage; + float senseVoltage; + float vccVoltage; + float currentMa; +}; + +void optical_current_begin(void); +bool optical_current_poll(OpticalCurrentMeasurement &measurement); +OpticalCurrentMeasurement optical_get_led_measurement(void); +float optical_get_led_current_ma(void); diff --git a/README.md b/README.md index baaf0d9..ef2aa23 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Arduino sketch находится в каталоге `OpticalChannelTester`: | Optical RX input | GPIO 4 | GPIO 13 | | START | GPIO 1 | GPIO 10 | | MODE | GPIO 0 | GPIO 9 | +| Optical LED current | GPIO 2 | GPIO 2 | +| Optical VCC measurement | GPIO 5 | GPIO 6 | | OLED SDA | GPIO 6 | GPIO 44 | | OLED SCL | GPIO 7 | GPIO 1 | @@ -81,11 +83,34 @@ Arduino sketch находится в каталоге `OpticalChannelTester`: | MODE | GPIO 20 | GPIO 5 | | VBAT | GPIO 2 | GPIO 11 | | Analog RX | GPIO 0 | GPIO 9 | +| Optical LED current | GPIO 1 | GPIO 10 | +| Optical VCC measurement | GPIO 5 | GPIO 6 | | OLED SDA | GPIO 6 | GPIO 44 | | OLED SCL | GPIO 7 | GPIO 1 | Оба профиля рассчитаны на установку S3 с совмещением контактов `5V` и `GND` с модулем C3. Номера GPIO S3 выбраны по тому же физическому ряду разъёма, а не по совпадению номера GPIO. +Для измерения тока HFBR-1528Z подключите выход (среднюю точку) делителя +`10 кОм / 10 кОм` к `GPIO 2` на макетке ESP32-S3 или к `GPIO 10` на итоговой +плате ESP32-S3. Оба вывода относятся к ADC1 (`ADC1_CH1` и `ADC1_CH9` +соответственно) и не конфликтуют с ESP-NOW. Верх делителя подключается к +нижнему выводу штатного резистора 47 Ом, низ — к GND; прямые 5 В на GPIO +подавать нельзя. + +Второй делитель `10 кОм / 10 кОм` измеряет фактическое питание оптической +цепи: верх делителя подключается к `+5 В`, низ — к GND, а его средняя точка — +к `GPIO 6` ESP32-S3 (`ADC1_CH5`) в обоих профилях. Ток рассчитывается как +`(Vcc_измеренное - Usense_измеренное) / 47 Ом`, поэтому отклонение питания от +5.000 В не вносит систематическую ошибку. + +В меню настроек пункт `КАЛИБР. ОПТИКИ` включает передатчик постоянным светом, +выдерживает 20 мс и показывает `Idiode` в мА и измеренное `Vcc`. Значения +обновляются примерно 10 раз в секунду по непрерывному среднему за 100 мс для +каждого ADC. При переходе к другому пункту или выходе +из меню передатчик выключается. Пересчёт сосредоточен в +`optical_get_led_current_ma()`; коэффициенты `OPTICAL_VCC`, +`OPTICAL_SENSE_R` и `OPTICAL_DIVIDER_RATIO` находятся в `Config.h`. + На ESP32-C3 PWM формируется через LEDC с аппаратным дробным делителем Q10.8. На ESP32-S3 используется отдельный MCPWM со счётчиком 20 МГц: это позволяет формировать 500 Гц в пределах 16-битного счётчика и сохраняет шаг импульса 50 нс. `TX_LIGHT_ON_GPIO_LEVEL` задаёт постоянное свойство схемы: какой физический уровень `PIN_TX` включает свет. Для приведённой схемы с SN75451 это `HIGH`. Пользовательский код света не изменяет это соответствие. При остановке PWM прошивка выдаёт `TX_LIGHT_OFF_GPIO_LEVEL`. Перед началом точки проверяется, что фактические частота и длительность укладываются в выбранную точность. ### OLED