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

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

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;
}