Files
OptoTest/OpticalChannelTester/Receiver.cpp

431 lines
16 KiB
C++

#include "Receiver.h"
#include "Log.h"
#include <math.h>
#if !OPTICAL_USE_MCPWM_CAPTURE
#include <esp_cpu.h>
#include <esp32-hal-cpu.h>
#endif
uint32_t PulseReceiver::tickHz() const {
#if OPTICAL_USE_MCPWM_CAPTURE
return captureResolutionHz_;
#else
return cpuTickHz_;
#endif
}
uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const {
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) return 0;
#if OPTICAL_USE_MCPWM_CAPTURE
return captureResolutionHz_ ? captureResolutionHz_ :
MCPWM_CAPTURE_RESOLUTION_HZ;
#else
return cpuTickHz_;
#endif
}
bool PulseReceiver::begin() {
queue_ = xQueueCreate(512, sizeof(Edge));
if (!queue_) return false;
#if OPTICAL_USE_MCPWM_CAPTURE
// PWM generation uses MCPWM group 0. Group 1 is dedicated to input capture,
// so RX cannot exhaust or conflict with the generator's resources.
mcpwm_capture_timer_config_t timerConfig = {};
timerConfig.group_id = 1;
timerConfig.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT;
timerConfig.resolution_hz = MCPWM_CAPTURE_RESOLUTION_HZ;
if (mcpwm_new_capture_timer(&timerConfig, &captureTimer_) != ESP_OK) return false;
if (mcpwm_capture_timer_get_resolution(captureTimer_, &captureResolutionHz_) != ESP_OK ||
!captureResolutionHz_) return false;
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;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &risingChannel_) != ESP_OK)
return false;
mcpwm_capture_event_callbacks_t callbacks = {};
callbacks.on_cap = onCapture;
if (mcpwm_capture_channel_register_event_callbacks(
risingChannel_, &callbacks, this) != ESP_OK) return false;
channelConfig.flags.pos_edge = false;
channelConfig.flags.neg_edge = true;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK)
return false;
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;
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
return cpuTickHz_ != 0;
#endif
}
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
bool activeLightOn) {
if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
expectedHz_ = expectedHz;
expectedDutyPct_ = expectedDutyPct;
#if !OPTICAL_USE_MCPWM_CAPTURE
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
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.
if (running_) return true;
if (mcpwm_capture_timer_enable(captureTimer_) != ESP_OK) return false;
if (mcpwm_capture_channel_enable(risingChannel_) != ESP_OK) {
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
if (mcpwm_capture_channel_enable(fallingChannel_) != ESP_OK) {
mcpwm_capture_channel_disable(risingChannel_);
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;
}
void PulseReceiver::stop() {
const bool wasRunning = running_;
running_ = false;
#if OPTICAL_USE_MCPWM_CAPTURE
if (wasRunning) {
// 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_);
mcpwm_capture_timer_disable(captureTimer_);
}
#else
(void)wasRunning;
#endif
}
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;
waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0;
haveRawTick_ = false;
lastRawTick_ = 0;
tickEpoch_ = 0;
}
PulseReceiver::TimedEdge PulseReceiver::extendEdge(const Edge &e) {
if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL)
tickEpoch_ += 0x100000000ULL;
haveRawTick_ = true; lastRawTick_ = e.tick;
return {tickEpoch_ + e.tick, e.rising != 0};
}
bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
const TimedEdge edge = extendEdge(rawEdge);
if (polarityKnown_) {
// Deliberately ignore edge type after synchronization. A PWM waveform is
// just alternating intervals: active, inactive, active, inactive. An
// extra or missing edge therefore becomes a concrete wrong pulse/period
// instead of an ambiguous GLITCH state.
if (waitingForActiveEnd_) {
activeEnd_ = edge.tick;
waitingForActiveEnd_ = false;
return false;
}
const uint64_t periodTicks = edge.tick - activeStart_;
const uint64_t activeTicks = activeEnd_ - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeTicks), tickHz()};
activeStart_ = edge.tick;
waitingForActiveEnd_ = true;
return true;
}
// 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;
activeStart_ = edge.tick;
waitingForActiveEnd_ = true;
return false;
}
uint32_t PulseReceiver::takeDroppedItems() {
return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED);
}
#if OPTICAL_USE_MCPWM_CAPTURE
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),
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);
return wake == pdTRUE;
}
#else
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return;
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);
if (wake) portYIELD_FROM_ISR();
}
#endif
bool PulseReceiver::nextOrderedEdge(Edge &edge, TickType_t waitTicks) {
if (!haveReorderEdge_) {
if (xQueueReceive(queue_, &reorderEdge_, waitTicks) != pdTRUE) return false;
haveReorderEdge_ = true;
}
Edge next = {};
// Keep one-event look-ahead. If both channel interrupts were pending while
// OLED/I2C ran, the MCPWM driver may dispatch them by channel number rather
// than timestamp. The signed modular comparison restores their real order.
if (xQueueReceive(queue_, &next, waitTicks) != pdTRUE) return false;
if (static_cast<int32_t>(next.tick - reorderEdge_.tick) < 0) {
edge = next;
} else {
edge = reorderEdge_;
reorderEdge_ = next;
}
return true;
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
TickType_t waitTicks) {
size_t count = 0;
Edge edge = {};
while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) {
if (consumeEdge(edge, periods[count])) {
periods[count].activeTickHz = tickHz();
++count;
}
}
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;
}