работает с перемычкой на 1МГц 2%

This commit is contained in:
2026-08-06 15:53:32 +03:00
parent 1edc420b77
commit f52f65686d
11 changed files with 207 additions and 55 deletions

View File

@@ -92,12 +92,9 @@ void App::update() {
}
if (state_ == AppState::SOLO_MEASURE) {
const MeasureState ms = measurement_.update();
if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), requestedHz_); finish(false, measurement_.reason()); }
if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), actual_.actualHz); finish(false, measurement_.reason()); }
else if (ms == MeasureState::PASS) {
printStageStats(measurement_.stats(), requestedHz_);
if (measurement_.reason() == FailReason::DATA_LOST) {
sweepHadDataLoss_ = true; if (!firstDataLossHz_) firstDataLossHz_ = requestedHz_;
}
printStageStats(measurement_.stats(), actual_.actualHz);
stagePassed();
}
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
@@ -156,13 +153,18 @@ void App::showMenu() {
void App::startTest() {
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
stageIndex_ = 0; pendingReason_ = FailReason::NONE; sweepHadDataLoss_ = false; firstDataLossHz_ = 0;
stageIndex_ = 0; pendingReason_ = FailReason::NONE;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
if (SERIAL_MINIMAL_LOG)
Log::printf("CONFIG", "mode=%s range=%lu..%luHz step=%luHz accuracy=%.2f%% time=%lums repeats=%u duty=%u%% stages=%lu",
roleName(static_cast<Role>(settings_.role)), params_.startHz, params_.endHz, params_.stepHz,
if (SERIAL_MINIMAL_LOG) {
char startText[12], endText[12], stepText[12];
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText));
Log::printf("CONFIG", "mode=%s range=%s..%s step=%s accuracy=%.2f%% time=%lums repeats=%u duty=%u%% stages=%lu",
roleName(static_cast<Role>(settings_.role)), startText, endText, stepText,
params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_);
}
printConfiguration();
const Role role = static_cast<Role>(settings_.role);
if (role == Role::SOLO) {
@@ -214,11 +216,7 @@ bool App::startLocalMeasurement(float hz, float duty) {
void App::stagePassed() {
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
pwm_.stop();
if (++stageIndex_ >= stageCount_) {
if (sweepHadDataLoss_) { requestedHz_ = firstDataLossHz_; finish(false, FailReason::DATA_LOST); }
else finish(true, FailReason::NONE);
return;
}
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
@@ -329,7 +327,7 @@ void App::updateSlave() {
if (state_ == AppState::SLAVE_MEASURE) {
const MeasureState ms = measurement_.update();
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return;
printStageStats(measurement_.stats(), requestedHz_);
printStageStats(measurement_.stats(), actual_.actualHz);
pendingPacket_ = makePacket(MessageType::RESULT);
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
@@ -357,8 +355,11 @@ void App::finish(bool pass, FailReason reason) {
if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end();
state_ = AppState::FINISHED; pendingReason_ = reason;
char one[24];
if (pass) { snprintf(one, sizeof(one), "PASS %luHz-%lu", params_.startHz, params_.endHz); display_.show(one, "START=REPEAT"); }
else { snprintf(one, sizeof(one), "FAIL AT %lu", requestedHz_); display_.show(one, failName(reason)); }
if (pass) display_.show("PASS", "REPEAT");
else {
char frequency[12]; Display::formatFrequency(requestedHz_, frequency, sizeof(frequency));
snprintf(one, sizeof(one), "FAIL %s", frequency); display_.show(one, failName(reason));
}
}
void App::printConfiguration() {
@@ -388,9 +389,11 @@ void App::printStageStats(const StageStats &s, uint32_t hz) {
if (!s.periods) return;
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
const char *status = s.reason == FailReason::NONE ? "PASS" :
(s.reason == FailReason::DATA_LOST ? "DATA_LOST" : "FAIL");
Log::printf("RESULT", "%luHz %s periods=%lu measured=%.2fHz duty=%.2f%% lost=%lu%s%s",
hz, status, s.periods, measuredHz, measuredDuty, s.lostItems,
char requestedText[12], measuredText[12];
Display::formatFrequency(hz, requestedText, sizeof(requestedText));
Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
Log::printf("RESULT", "%s %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s",
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems,
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
}

View File

@@ -64,7 +64,5 @@ class App {
uint8_t retries_ = 0;
ProtocolPacket pendingPacket_ = {};
bool initialized_ = false, bootResetCandidate_ = false;
bool sweepHadDataLoss_ = false;
uint32_t firstDataLossHz_ = 0;
uint32_t bootCheckStartedMs_ = 0;
};

View File

@@ -11,7 +11,7 @@ const char *roleName(Role r) {
const char *failName(FailReason r) {
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED", "DATA LOST"};
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
}
@@ -72,6 +72,80 @@ uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
return bits;
}
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct) {
const uint8_t fallback = choosePwmResolution(frequencyHz, sourceClockHz, maxBits);
if (!fallback || dutyPct > 100U) return fallback;
for (uint8_t bits = fallback; bits > 0; --bits) {
const uint32_t levels = 1UL << bits;
const uint64_t denominator = static_cast<uint64_t>(frequencyHz) * levels;
if (denominator > sourceClockHz || sourceClockHz % denominator) continue;
const uint32_t divider = static_cast<uint32_t>(sourceClockHz / denominator);
if (!divider || divider > 1024U) continue;
if ((static_cast<uint32_t>(levels) * dutyPct) % 100U == 0U) return bits;
}
return fallback;
}
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct,
IntegerPwmConfig &config) {
if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false;
bool found = false;
uint64_t bestError = 0;
uint32_t bestDenominator = 1;
uint32_t bestDutyError = 0;
uint32_t bestLevels = 1;
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
const uint32_t levels = 1UL << bits;
const uint64_t requestedProduct = static_cast<uint64_t>(requestedHz) * levels;
uint32_t lowerDivider = static_cast<uint32_t>(sourceClockHz / requestedProduct);
if (lowerDivider < 1U) lowerDivider = 1U;
if (lowerDivider > 1023U) lowerDivider = 1023U;
const uint32_t candidates[] = {lowerDivider,
lowerDivider < 1023U ? lowerDivider + 1U : lowerDivider};
for (uint8_t candidate = 0; candidate < 2; ++candidate) {
const uint32_t divider = candidates[candidate];
if (candidate && divider == candidates[0]) continue;
const uint32_t denominator = levels * divider;
const uint64_t targetClock = static_cast<uint64_t>(requestedHz) * denominator;
const uint64_t error = targetClock > sourceClockHz
? targetClock - sourceClockHz : sourceClockHz - targetClock;
const uint32_t dutyCount = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U;
const uint32_t representedDuty = dutyCount * 100U;
const uint32_t requestedDuty = levels * dutyPct;
const uint32_t dutyError = representedDuty > requestedDuty
? representedDuty - requestedDuty : requestedDuty - representedDuty;
const bool frequencyBetter = !found ||
error * bestDenominator < bestError * denominator;
const bool frequencyEqual = found &&
error * bestDenominator == bestError * denominator;
const bool dutyBetter = frequencyEqual &&
static_cast<uint64_t>(dutyError) * bestLevels <
static_cast<uint64_t>(bestDutyError) * levels;
const bool dutyEqual = frequencyEqual &&
static_cast<uint64_t>(dutyError) * bestLevels ==
static_cast<uint64_t>(bestDutyError) * levels;
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue;
config.actualHz = static_cast<uint32_t>(
(static_cast<uint64_t>(sourceClockHz) + denominator / 2U) / denominator);
config.divider = static_cast<uint16_t>(divider);
config.bits = bits;
bestError = error;
bestDenominator = denominator;
bestDutyError = dutyError;
bestLevels = levels;
found = true;
}
}
return found;
}
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t captureHz, uint8_t pwmBits) {
if (!frequencyHz || !captureHz || !pwmBits) return FailReason::RESOLUTION;

View File

@@ -6,7 +6,7 @@
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED, DATA_LOST
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
};
const char *roleName(Role role);
@@ -53,7 +53,7 @@ struct StageStats {
uint8_t firstBadRepeat;
float badFrequency;
float badDuty;
uint32_t lostItems;
uint32_t droppedItems;
FailReason reason;
void reset();
};
@@ -65,6 +65,12 @@ struct PeriodLimits {
uint32_t maxDutyX100;
};
struct IntegerPwmConfig {
uint32_t actualHz;
uint16_t divider;
uint8_t bits;
};
uint32_t settingsChecksum(const Settings &s);
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz);
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index);
@@ -73,6 +79,11 @@ bool periodWithin(float measuredHz, float expectedHz, float tolerancePct);
bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct);
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits);
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct);
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct,
IntegerPwmConfig &config);
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t captureResolutionHz, uint8_t pwmBits);
FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz,

View File

@@ -35,9 +35,12 @@ void Display::show(const char *a, const char *b) {
}
void Display::formatFrequency(float hz, char *out, size_t n) {
if (hz >= 1000000.0f) snprintf(out, n, "%.2fM", hz / 1000000.0f);
else if (hz >= 1000.0f) snprintf(out, n, "%.2fk", hz / 1000.0f);
else snprintf(out, n, "%.0fHz", hz);
float value = hz; const char *suffix = "Hz";
if (hz >= 1000000.0f) { value = hz / 1000000.0f; suffix = "M"; }
else if (hz >= 1000.0f) { value = hz / 1000.0f; suffix = "k"; }
if (fabsf(value - roundf(value)) < 0.005f) snprintf(out, n, "%.0f%s", value, suffix);
else if (fabsf(value * 10.0f - roundf(value * 10.0f)) < 0.005f) snprintf(out, n, "%.1f%s", value, suffix);
else snprintf(out, n, "%.2f%s", value, suffix);
}
void Display::formatDuration(uint64_t us, char *out, size_t n) {

View File

@@ -4,6 +4,7 @@
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
uint8_t repeats, uint8_t settleCycles) {
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 ||
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) ||
@@ -15,7 +16,20 @@ bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
currentRepeat_ = 0;
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
state_ = MeasureState::SETTLING; return true;
state_ = MeasureState::SETTLING;
xTaskNotifyGive(task_);
return true;
}
void Measurement::taskEntry(void *context) {
static_cast<Measurement *>(context)->taskLoop();
}
void Measurement::taskLoop() {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
while (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING) processOnce();
}
}
void Measurement::fail(FailReason reason) {
@@ -25,25 +39,25 @@ void Measurement::fail(FailReason reason) {
void Measurement::completeWindow() {
receiver_.stop();
stats_.lostItems += receiver_.takeDroppedItems();
stats_.droppedItems += receiver_.takeDroppedItems();
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
fail(FailReason::TOO_FEW_PERIODS); return;
}
if (stats_.lostItems && stats_.reason == FailReason::NONE) stats_.reason = FailReason::DATA_LOST;
state_ = MeasureState::PASS;
}
MeasureState Measurement::update() {
MeasureState Measurement::processOnce() {
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
bool receivedPeriod = false;
for (;;) {
const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE);
stats_.lostItems += receiver_.takeDroppedItems();
const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE, pdMS_TO_TICKS(2));
stats_.droppedItems += receiver_.takeDroppedItems();
if (!periodCount) break;
receivedPeriod = true;
for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) {
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
const PulsePeriod &period = periodBatch_[periodIndex];
if (state_ == MeasureState::SETTLING) {
if (settleLeft_) --settleLeft_;
@@ -86,4 +100,6 @@ MeasureState Measurement::update() {
return state_;
}
MeasureState Measurement::update() { return state_; }
void Measurement::abort() { if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING) fail(FailReason::ABORTED); }

View File

@@ -14,10 +14,14 @@ class Measurement {
FailReason reason() const { return stats_.reason; }
const StageStats &stats() const { return stats_; }
private:
static void taskEntry(void *context);
void taskLoop();
MeasureState processOnce();
void fail(FailReason reason);
void completeWindow();
PulseReceiver &receiver_;
MeasureState state_ = MeasureState::IDLE;
volatile MeasureState state_ = MeasureState::IDLE;
TaskHandle_t task_ = nullptr;
StageStats stats_ = {};
PeriodLimits limits_ = {};
uint32_t expectedHz_ = 0;

View File

@@ -1,6 +1,28 @@
#include "Pwm.h"
#include "Config.h"
#include "Core.h"
#include <hal/ledc_ll.h>
namespace {
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0;
void setIntegerDivider(uint16_t divider) {
ledc_dev_t *hardware = LEDC_LL_GET_HW();
ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER,
static_cast<uint32_t>(divider) << LEDC_LL_FRACTIONAL_BITS);
ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER);
}
bool integerDividerIsSet(uint16_t expected) {
uint32_t rawDivider = 0;
ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider);
return rawDivider == (static_cast<uint32_t>(expected) << LEDC_LL_FRACTIONAL_BITS);
}
}
void PwmGenerator::begin() {
// Match LEDC_SOURCE_CLOCK_HZ and make the timer calculation deterministic.
@@ -10,18 +32,38 @@ void PwmGenerator::begin() {
}
bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
stop();
const uint8_t bits = choosePwmResolution(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS);
if (!bits) return false;
if (!ledcAttachChannel(GPIO_PWM, hz, bits, LEDC_CHANNEL)) return false;
IntegerPwmConfig config = {};
if (!chooseIntegerPwmConfig(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, dutyPct, config)) return false;
const uint8_t bits = config.bits;
const uint32_t levels = 1UL << bits;
const uint32_t duty = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U;
if (!ledcWriteChannel(LEDC_CHANNEL, duty)) { ledcDetach(GPIO_PWM); return false; }
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
if (!actualHz) { ledcDetach(GPIO_PWM); return false; }
a = {hz, actualHz, 100.0f * duty / levels, bits};
running_ = true;
return true;
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
stop();
const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL);
if (attached) {
// Arduino's LEDC API normally chooses an 8-bit fractional divider.
// Force the fractional byte to zero so every PWM period contains the
// same integer number of 40 MHz source-clock ticks.
setIntegerDivider(config.divider);
}
if (attached && integerDividerIsSet(config.divider) && ledcWriteChannel(LEDC_CHANNEL, duty)) {
// On the first configuration after power-up the duty update is latched
// on a timer edge. Reading immediately can therefore return zero.
uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz);
if (settleUs > 2000U) settleUs = 2000U;
delayMicroseconds(settleUs);
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
if (actualHz) {
a = {hz, actualHz, 100.0f * duty / levels, bits};
running_ = true;
return true;
}
}
if (attached) ledcDetach(GPIO_PWM);
delay(2);
}
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
return false;
}
void PwmGenerator::stop() {

View File

@@ -130,11 +130,12 @@ bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_even
return wake == pdTRUE;
}
bool PulseReceiver::nextRmtEdge(Edge &edge) {
bool PulseReceiver::nextRmtEdge(Edge &edge, TickType_t waitTicks) {
for (;;) {
if (blockIndex_ >= block_.count) {
if (xQueueReceive(queue_, &block_, 0) != pdTRUE) return false;
if (xQueueReceive(queue_, &block_, waitTicks) != pdTRUE) return false;
blockIndex_ = 0; phase_ = 0;
waitTicks = 0;
}
const rmt_symbol_word_t &s = block_.symbols[blockIndex_];
const bool nextLevel = phase_ == 0 ? s.level0 : s.level1;
@@ -151,10 +152,10 @@ bool PulseReceiver::nextRmtEdge(Edge &edge) {
}
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) {
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) {
size_t count = 0;
Edge e;
while (count < capacity && nextRmtEdge(e))
while (count < capacity && nextRmtEdge(e, count ? 0 : waitTicks))
if (consumeEdge(e, periods[count])) ++count;
return count;
}
@@ -170,10 +171,10 @@ void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
if (wake) portYIELD_FROM_ISR();
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity) {
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) {
size_t count = 0;
Edge e;
while (count < capacity && xQueueReceive(queue_, &e, 0) == pdTRUE)
while (count < capacity && xQueueReceive(queue_, &e, count ? 0 : waitTicks) == pdTRUE)
if (consumeEdge(e, periods[count])) ++count;
return count;
}

View File

@@ -18,7 +18,7 @@ class PulseReceiver {
bool start(uint32_t expectedHz);
void stop();
void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity);
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
bool overflowed();
uint32_t takeDroppedItems();
uint32_t tickHz() const;
@@ -38,7 +38,7 @@ class PulseReceiver {
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS;
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
bool nextRmtEdge(Edge &edge);
bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
rmt_channel_handle_t channel_ = nullptr;
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
uint16_t receiveChunkSymbols_ = 0;

View File

@@ -202,7 +202,7 @@ PWM_SETTLE_CYCLES / actualFrequency + TEST_TIME * REPEATS
| Target | Arduino-ESP32 | Flash | RAM | Результат |
|---|---:|---:|---:|---|
| ESP32-C3 | 3.3.10 | 1,025,021 B (78%) | 45,380 B (13%) | PASS |
| ESP32-C3 | 3.3.10 | 1,026,823 B (78%) | 45,380 B (13%) | PASS |
| ESP32-S3 | 3.3.10 | 950,608 B (72%) | 48,620 B (14%) | PASS |
Локальные unit-тесты: `core tests: PASS`, `button tests: PASS`. Они покрывают неделимый диапазон, END без дубля, ALL, границы допусков, немедленный FAIL, resolution, checksum настроек, CRC протокола и отсутствие short после long.