mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-22 04:49:37 +00:00
Compare commits
32 Commits
01a34ed3f7
...
archive/co
| Author | SHA1 | Date | |
|---|---|---|---|
| 00e101990f | |||
| 6c97d33c7c | |||
| 628c050702 | |||
| a589416cfc | |||
| e25feb6824 | |||
| 96ffb91b97 | |||
| 08ea71eb5e | |||
| 5bcaf4092a | |||
| 873277e59f | |||
| f93f4101c3 | |||
| bf14b3d7ad | |||
| a8d32bbcef | |||
| fc3510c425 | |||
| e5ed029cfd | |||
| 55935b8b92 | |||
| 7176fe5321 | |||
| 9935ac192a | |||
| 8361828c44 | |||
| 38d93edd1b | |||
| 2f5b57680b | |||
| 31ac7a3625 | |||
| b4f644c258 | |||
| a75fd0c564 | |||
| 4061a4ed10 | |||
| fc93ea41db | |||
| 0620d98e35 | |||
| 4caed06218 | |||
| 37522f974f | |||
| 6375c4eed5 | |||
| c4000d6b75 | |||
| fc02c79135 | |||
| d068a576f7 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.vscode/*
|
||||
bin/*
|
||||
tests/*.exe
|
||||
!.vscode/launch.json
|
||||
log/*
|
||||
/.vscode
|
||||
@ -10,3 +11,4 @@ Analyzer/raw/dll/*.so
|
||||
Analyzer/raw/dll/*.dylib
|
||||
/Analyzer/raw/IR_Fox/.github
|
||||
**/.build
|
||||
graphify-out/*
|
||||
|
||||
@ -162,11 +162,13 @@ void IR_Decoder::_tick()
|
||||
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
||||
isWaitingAcceptSend = true;
|
||||
}
|
||||
gotRaw.set(&packInfo, id);
|
||||
// Raw keeps the decoder's common minimum-size contract. Known packet
|
||||
// layouts are validated by their typed BasePack::set calls above.
|
||||
gotRaw.set(&packInfo, id, false);
|
||||
}
|
||||
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
|
||||
{
|
||||
encoder->sendAccept(addrAcceptSendTo, acceptCustomByte);
|
||||
if (encoder != nullptr) encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); // B2: у Car decoder без парного энкодера (nullptr) → был HardFault
|
||||
isWaitingAcceptSend = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,12 +10,12 @@ private:
|
||||
static std::list<IR_Decoder*>& get_dec_list();
|
||||
void _tick();
|
||||
|
||||
uint32_t acceptSendTimer;
|
||||
bool isWaitingAcceptSend;
|
||||
uint16_t addrAcceptSendTo;
|
||||
uint32_t acceptSendTimer = 0;
|
||||
bool isWaitingAcceptSend = false; // B4: без инициализации мусор мог дёрнуть sendAccept при старте
|
||||
uint16_t addrAcceptSendTo = 0;
|
||||
|
||||
uint16_t acceptDelay = IR_ResponseDelay;
|
||||
uint8_t acceptCustomByte;
|
||||
uint8_t acceptCustomByte = 0;
|
||||
|
||||
bool extiEnabled_ = false;
|
||||
bool rxExtiPreemptConfigured_ = false;
|
||||
|
||||
@ -74,7 +74,6 @@ bool IR_DecoderRaw::availableRaw()
|
||||
|
||||
void IR_DecoderRaw::pulseFilterResetStats()
|
||||
{
|
||||
pulseFilterDropFilteredOverflow = 0;
|
||||
pulseFilterDropHoldOverflow = 0;
|
||||
pulseFilterDropGlitchPairs = 0;
|
||||
}
|
||||
@ -146,9 +145,19 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
||||
default: return F("UNK");
|
||||
}
|
||||
}
|
||||
#endif // IR_RX_BRIEF_LOG (rxBriefReasonTag)
|
||||
|
||||
// Безусловна: always-on счёт причин RX-событий (наблюдаемость по контракту
|
||||
// живучести — счётчики живут и в проде без печати); печать — только под флагом.
|
||||
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
|
||||
{
|
||||
const uint8_t ri = (uint8_t)reason;
|
||||
if (ri < 14U)
|
||||
rxReasonCnt[ri]++;
|
||||
#if !IR_RX_BRIEF_LOG
|
||||
(void)a; (void)b; (void)tUs;
|
||||
return;
|
||||
#else
|
||||
#if IR_RX_BRIEF_LOG_REJECT_ONLY
|
||||
if (reason == RxBriefReason::Ok || reason == RxBriefReason::Preamble)
|
||||
return;
|
||||
@ -221,8 +230,26 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
|
||||
break;
|
||||
}
|
||||
Serial.println();
|
||||
#endif // IR_RX_BRIEF_LOG (печать)
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::printRxReasonStats(Print &out) const
|
||||
{
|
||||
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
|
||||
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
|
||||
"TIMEOUT", "CRC", "OK"};
|
||||
out.print(F("RXSTAT"));
|
||||
for (uint8_t i = 1; i < 14U; i++)
|
||||
{
|
||||
out.print(',');
|
||||
out.print(kTags[i]);
|
||||
out.print('=');
|
||||
out.print(rxReasonCnt[i]);
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
#if IR_RX_BRIEF_LOG
|
||||
void IR_DecoderRaw::rxBriefNoteMuteBlockedIsr(uint32_t tUs)
|
||||
{
|
||||
(void)tUs;
|
||||
@ -277,10 +304,8 @@ void IR_DecoderRaw::isr()
|
||||
// Интервалы между соседними фронтами считаются как (uint32_t)(t1 - t0) — корректно при
|
||||
// паузе < ~35 мин между фронтами; условие «тишина > longSilence» в preambleProcessEdge
|
||||
// переписано без front.time > prevRise (оно ломается при wrap).
|
||||
uint32_t t;
|
||||
noInterrupts();
|
||||
t = micros();
|
||||
interrupts();
|
||||
// B6: micros() в ISR не требует маскировки; прежний interrupts() посреди ISR снимал PRIMASK.
|
||||
const uint32_t t = micros();
|
||||
|
||||
FrontStorage edge;
|
||||
edge.dir = port->IDR & mask;
|
||||
@ -344,20 +369,50 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
|
||||
if (pulseFilterHoldCount != 0U)
|
||||
return true;
|
||||
noInterrupts();
|
||||
const bool busy = !subBuffer.isEmpty() || !filteredSubBuffer.isEmpty();
|
||||
const bool busy = !subBuffer.isEmpty();
|
||||
interrupts();
|
||||
return busy;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::rxPipelineActive() const
|
||||
{
|
||||
return rxLineActive() || rxTimeoutPipelineBusy();
|
||||
}
|
||||
|
||||
uint8_t IR_DecoderRaw::currentRxMsgType() const
|
||||
{
|
||||
if (i_dataBuffer < static_cast<uint16_t>(msgBytes) * bitPerByte)
|
||||
return 0xFFU;
|
||||
return static_cast<uint8_t>((dataBuffer[0] >> 5U) & IR_MASK_MSG_TYPE);
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock)
|
||||
{
|
||||
++rxTerminalInfo.seq;
|
||||
rxTerminalInfo.reason = reason;
|
||||
rxTerminalInfo.msgType = msgType;
|
||||
rxTerminalInfo.hadLock = hadLock;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::listenStart()
|
||||
{
|
||||
if (rxTimeoutPipelineBusy())
|
||||
return;
|
||||
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
||||
const uint32_t nowUs = micros();
|
||||
if (isReciveRaw && ((nowUs - lastEdgeTime) > IR_timeout * 2U))
|
||||
{
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(true);
|
||||
#endif
|
||||
if (isRecive)
|
||||
{
|
||||
const uint16_t expected =
|
||||
(i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
||||
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, nowUs);
|
||||
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
|
||||
isRecive = false;
|
||||
msgTypeReceive = 0;
|
||||
}
|
||||
isReciveRaw = false;
|
||||
firstRX();
|
||||
}
|
||||
@ -376,10 +431,9 @@ inline void IR_DecoderRaw::checkTimeout()
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(false);
|
||||
#endif
|
||||
#if IR_RX_BRIEF_LOG
|
||||
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
||||
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
||||
#endif
|
||||
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
|
||||
isRecive = false; // приём завершён
|
||||
msgTypeReceive = 0;
|
||||
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
||||
@ -403,56 +457,76 @@ void IR_DecoderRaw::tick()
|
||||
// Не в начале до pop: иначе после checkTimeout lastEdgeTime vs micros() расходятся
|
||||
// с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет.
|
||||
|
||||
FrontStorage currentFront;
|
||||
bool hasCurrentFront = false;
|
||||
FrontStorage rawFront;
|
||||
bool hasRawFront = false;
|
||||
noInterrupts();
|
||||
FrontStorage *rawPtr = subBuffer.pop();
|
||||
if (rawPtr != nullptr)
|
||||
bool processedFront = false;
|
||||
// Батч-выемка (IR_RX_TICK_BATCH): раньше 1 фронт/tick — приём был привязан
|
||||
// к частоте loop, буфер 250 фронтов (~120мс эфира) переполнялся на медленном
|
||||
// loop и кадры гибли молча. Семантика idle-пути сохранена: flush
|
||||
// пульс-фильтра — только когда буфер пуст (как прежняя else-ветка).
|
||||
uint8_t batchBudget = IR_RX_TICK_BATCH;
|
||||
bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида)
|
||||
while (hasRawFront)
|
||||
{
|
||||
rawFront = *rawPtr;
|
||||
hasRawFront = true;
|
||||
}
|
||||
interrupts();
|
||||
|
||||
if (IR_INPUT_MIN_PULSE_US > 0U)
|
||||
{
|
||||
if (hasRawFront)
|
||||
pulseFilterFeedOneRaw(rawFront);
|
||||
else
|
||||
pulseFilterFlushTimeout(micros());
|
||||
|
||||
noInterrupts();
|
||||
FrontStorage *flt = filteredSubBuffer.pop();
|
||||
if (flt != nullptr)
|
||||
if (IR_INPUT_MIN_PULSE_US > 0U)
|
||||
{
|
||||
currentFront = *flt;
|
||||
hasCurrentFront = true;
|
||||
pulseFilterPushRaw(rawFront);
|
||||
FrontStorage confirmedFront;
|
||||
while (pulseFilterTryTakeConfirmed(confirmedFront))
|
||||
{
|
||||
processDecodedFront(confirmedFront);
|
||||
processedFront = true;
|
||||
}
|
||||
}
|
||||
interrupts();
|
||||
else
|
||||
{
|
||||
processDecodedFront(rawFront);
|
||||
processedFront = true;
|
||||
}
|
||||
if (--batchBudget == 0U)
|
||||
{
|
||||
break;
|
||||
}
|
||||
hasRawFront = subBuffer.pop(rawFront);
|
||||
}
|
||||
else if (hasRawFront)
|
||||
if (!hasRawFront && IR_INPUT_MIN_PULSE_US > 0U)
|
||||
{
|
||||
currentFront = rawFront;
|
||||
hasCurrentFront = true;
|
||||
const uint32_t nowUs = micros();
|
||||
FrontStorage confirmedFront;
|
||||
while (pulseFilterTryFlushOne(nowUs, confirmedFront))
|
||||
{
|
||||
processDecodedFront(confirmedFront);
|
||||
processedFront = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCurrentFront)
|
||||
if (!processedFront)
|
||||
{
|
||||
isSubBufferOverflow = false;
|
||||
listenStart();
|
||||
checkTimeout();
|
||||
expirePreambleCandidateIfIdle(micros());
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||
#endif
|
||||
return;
|
||||
} // Если данных нет - ничего не делаем
|
||||
listenStart();
|
||||
checkTimeout();
|
||||
expirePreambleCandidateIfIdle(micros());
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefFlushDeferredIsrLogs();
|
||||
#endif
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||
#endif
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront)
|
||||
{
|
||||
if (preambleProcessEdge(currentFront))
|
||||
{
|
||||
lastEdgeTime = currentFront.time;
|
||||
goto END;
|
||||
return;
|
||||
}
|
||||
|
||||
lastEdgeTime = currentFront.time; // запоминаем любой фронт
|
||||
@ -472,13 +546,11 @@ void IR_DecoderRaw::tick()
|
||||
if (short_low_glitch)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
|
||||
#endif
|
||||
#if IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
|
||||
#endif
|
||||
goto END;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if IR_MICRO_GAP_RISE_REJECT
|
||||
@ -489,22 +561,18 @@ void IR_DecoderRaw::tick()
|
||||
if (micro_gap_rise)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
|
||||
#endif
|
||||
#if IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
|
||||
#endif
|
||||
goto END;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (candRp <= riseTimeMax / 4U && !highCount && !lowCount)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Timing, irClampU16(candRp), 0, currentFront.time);
|
||||
#endif
|
||||
goto END;
|
||||
return;
|
||||
}
|
||||
|
||||
if (candRp > riseTimeMax / 4 || highCount || lowCount)
|
||||
@ -557,7 +625,7 @@ void IR_DecoderRaw::tick()
|
||||
}
|
||||
}
|
||||
#ifdef IRDEBUG
|
||||
// goto END; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// return; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
#endif
|
||||
//----------------------------------------------------------------------------------
|
||||
#ifdef IRDEBUG
|
||||
@ -567,12 +635,10 @@ void IR_DecoderRaw::tick()
|
||||
if (risePeriod > IR_timeout || isBufferOverflow || risePeriod < riseTimeMin || isWrongPack)
|
||||
// ~Мы в пределах таймаута и буффер не переполнен и fix дроблёных единиц
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
if (!isBufferOverflow && !isWrongPack)
|
||||
rxBriefLog(RxBriefReason::Timing, irClampU16((uint32_t)risePeriod),
|
||||
irClampU16((uint32_t)highTime), currentFront.time);
|
||||
#endif
|
||||
goto END;
|
||||
return;
|
||||
}
|
||||
|
||||
// определить направление фронта
|
||||
@ -727,17 +793,6 @@ void IR_DecoderRaw::tick()
|
||||
else
|
||||
{ // Если ```\__ ↓
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
END:;
|
||||
listenStart();
|
||||
checkTimeout();
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefFlushDeferredIsrLogs();
|
||||
#endif
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||
#endif
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
@ -745,12 +800,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#if !defined(IRDEBUG_SERIAL_PACK)
|
||||
(void)packTraceInvertFix;
|
||||
#endif
|
||||
if (i_dataBuffer > dataByteSizeMax * 8)
|
||||
{ // проверка переполнения
|
||||
if (i_dataBuffer >= dataByteSizeMax * 8)
|
||||
{ // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3)
|
||||
isBufferOverflow = true;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
|
||||
#endif
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
if (packTraceOpen)
|
||||
packTraceEmitErrorFlash(F("ERROR: buffer overflow"));
|
||||
@ -758,6 +811,13 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
}
|
||||
if (isBufferOverflow || isPreamb || isWrongPack)
|
||||
{
|
||||
const bool hadLock =
|
||||
isRecive || isReciveRaw || preambleState == PreambleState::Locked;
|
||||
const bool wasObservable =
|
||||
hadLock ||
|
||||
(preambleState == PreambleState::Candidate && preambleWasObservable);
|
||||
if (wasObservable)
|
||||
noteRxTerminal(IR_RxTerminalReason::DecodeAbort, currentRxMsgType(), hadLock);
|
||||
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
|
||||
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
||||
isRecive = false;
|
||||
@ -827,9 +887,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#endif
|
||||
{
|
||||
isWrongPack = true;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Sync, err_syncBit, 0, micros());
|
||||
#endif
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
||||
#endif
|
||||
@ -857,6 +915,11 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
if (i_dataBuffer == 8 * msgBytes)
|
||||
{ // Ппервый байт
|
||||
packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
|
||||
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
|
||||
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
|
||||
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
||||
if (packSize != 0 && packSize < msgBytes + crcBytes)
|
||||
isWrongPack = true;
|
||||
}
|
||||
|
||||
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
|
||||
@ -867,8 +930,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000;
|
||||
}
|
||||
|
||||
if (packSize && (i_dataBuffer == packSize * bitPerByte))
|
||||
{ // Конец
|
||||
if (packSize > crcBytes && (i_dataBuffer == packSize * bitPerByte))
|
||||
{ // Конец (B1: packSize>crcBytes → crcCheck(packSize-crcBytes) не уходит в underflow/OOB)
|
||||
packInfo.buffer = dataBuffer;
|
||||
packInfo.crc = crcValue;
|
||||
packInfo.err = errors;
|
||||
@ -918,6 +981,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
noteRxTerminal(isAvailable ? IR_RxTerminalReason::FrameOk
|
||||
: IR_RxTerminalReason::FrameCrcError,
|
||||
currentRxMsgType(), true);
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
if (isAvailable)
|
||||
packTraceEmitEndOk(static_cast<uint8_t>(packSize));
|
||||
@ -926,12 +992,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#endif
|
||||
const uint16_t errSum =
|
||||
uint16_t(errors.lowSignal) + uint16_t(errors.highSignal) + uint16_t(errors.other);
|
||||
#if IR_RX_BRIEF_LOG
|
||||
if (isAvailable)
|
||||
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
|
||||
else
|
||||
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
|
||||
#endif
|
||||
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
|
||||
memcpy(rejectBuffer, dataBuffer, packSize);
|
||||
rejectPackSize = static_cast<uint8_t>(packSize);
|
||||
@ -951,7 +1015,7 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc)
|
||||
crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF);
|
||||
crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF;
|
||||
|
||||
if (dataBuffer[len] == (crc >> 8) & 0xFF &&
|
||||
if (dataBuffer[len] == ((crc >> 8) & 0xFF) && // B7: скобки — иначе (== & 0xFF) по приоритету
|
||||
dataBuffer[len + 1] == (crc & 0xFF))
|
||||
{
|
||||
crcOK = true;
|
||||
@ -974,6 +1038,7 @@ bool IR_DecoderRaw::availableReject()
|
||||
|
||||
uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider)
|
||||
{
|
||||
if (divider == 0) return 0; // B10: защита от деления на 0 (актуально при freeFrec — сейчас выкл, не включаем)
|
||||
int ret = val / divider;
|
||||
if ((val << 4) / divider - (ret << 4) >= 8)
|
||||
ret++;
|
||||
@ -1455,34 +1520,18 @@ void IR_DecoderRaw::pulseFilterShiftLeft(uint8_t n)
|
||||
pulseFilterHoldCount = newCount;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::pulseFilterEmit(const FrontStorage &e)
|
||||
{
|
||||
if (filteredSubBuffer.isFull())
|
||||
{
|
||||
pulseFilterDropFilteredOverflow++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::FilterOverflow, irClampU16(pulseFilterDropFilteredOverflow), 0, e.time);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
filteredSubBuffer.push(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterReset()
|
||||
{
|
||||
pulseFilterHoldCount = 0;
|
||||
pulseFilterLastRawValid = false;
|
||||
pulseFilterLastRawTime = 0;
|
||||
while (filteredSubBuffer.pop() != nullptr) {}
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e)
|
||||
void IR_DecoderRaw::pulseFilterPushRaw(const FrontStorage &e)
|
||||
{
|
||||
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
|
||||
if (minUs == 0U)
|
||||
{
|
||||
pulseFilterEmit(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1492,47 +1541,52 @@ void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e)
|
||||
if (pulseFilterHoldCount >= kPulseFilterHoldCap)
|
||||
{
|
||||
pulseFilterDropHoldOverflow++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::HoldOverflow, irClampU16(pulseFilterDropHoldOverflow), 0, e.time);
|
||||
#endif
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
pulseFilterShiftLeft(1);
|
||||
}
|
||||
|
||||
pulseFilterHoldEdges[pulseFilterHoldCount++] = e;
|
||||
const uint8_t holdback = irPulseFilterHoldbackEdges();
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime)
|
||||
{
|
||||
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
|
||||
if (minUs == 0U)
|
||||
return false;
|
||||
|
||||
const uint8_t holdback = irPulseFilterHoldbackEdges();
|
||||
if (logTime == 0U)
|
||||
logTime = pulseFilterLastRawTime;
|
||||
for (;;)
|
||||
{
|
||||
if (pulseFilterHoldCount < 2)
|
||||
return;
|
||||
return false;
|
||||
|
||||
const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time;
|
||||
if (dt < minUs)
|
||||
{
|
||||
pulseFilterDropGlitchPairs++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, e.time);
|
||||
#endif
|
||||
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, logTime);
|
||||
pulseFilterShiftLeft(2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pulseFilterHoldCount <= holdback)
|
||||
return;
|
||||
return false;
|
||||
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
out = pulseFilterHoldEdges[0];
|
||||
pulseFilterShiftLeft(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs)
|
||||
bool IR_DecoderRaw::pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out)
|
||||
{
|
||||
if (IR_INPUT_MIN_PULSE_US == 0U || !pulseFilterLastRawValid || pulseFilterHoldCount == 0)
|
||||
return;
|
||||
return false;
|
||||
const uint32_t waitUs = IR_INPUT_MIN_PULSE_US * (uint32_t)IR_INPUT_FILTER_TIMEOUT_MULT;
|
||||
if ((uint32_t)(nowUs - pulseFilterLastRawTime) < waitUs)
|
||||
return;
|
||||
return false;
|
||||
|
||||
while (pulseFilterHoldCount > 0)
|
||||
{
|
||||
@ -1542,16 +1596,16 @@ void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs)
|
||||
if (dt < IR_INPUT_MIN_PULSE_US)
|
||||
{
|
||||
pulseFilterDropGlitchPairs++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, nowUs);
|
||||
#endif
|
||||
pulseFilterShiftLeft(2);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
out = pulseFilterHoldEdges[0];
|
||||
pulseFilterShiftLeft(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t IR_DecoderRaw::preambleJitterTolUs(uint32_t baselineUs) const
|
||||
@ -1572,6 +1626,7 @@ void IR_DecoderRaw::preambleResetToIdle()
|
||||
{
|
||||
preambleState = PreambleState::Idle;
|
||||
preambleGoodPeriods = 0;
|
||||
preambleWasObservable = false;
|
||||
preambleMeanPeriod = 0;
|
||||
preambleCandidateLastEdgeTime = 0;
|
||||
preambleCandidateFirstRiseTime = 0;
|
||||
@ -1586,6 +1641,10 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
||||
{
|
||||
preambleState = PreambleState::Candidate;
|
||||
preambleGoodPeriods = 0;
|
||||
// The first post-silence rise already opens a potential frame epoch.
|
||||
// Keep the line busy until that epoch locks or expires after real silence:
|
||||
// even a badly distorted response may contain no coarse-valid rise period.
|
||||
preambleWasObservable = true;
|
||||
preambleMeanPeriod = 0;
|
||||
preambleCandidateLastEdgeTime = front.time;
|
||||
preambleCandidateFirstRiseTime = front.time;
|
||||
@ -1596,6 +1655,24 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
||||
isReciveRaw = false;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::expirePreambleCandidateIfIdle(uint32_t nowUs)
|
||||
{
|
||||
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
|
||||
return;
|
||||
|
||||
const uint32_t candTimeout =
|
||||
IR_timeout * static_cast<uint32_t>(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT);
|
||||
if ((uint32_t)(nowUs - preambleCandidateLastEdgeTime) <= candTimeout)
|
||||
return;
|
||||
|
||||
const uint8_t goodPeriods = preambleGoodPeriods;
|
||||
const bool wasObservable = preambleWasObservable;
|
||||
rxBriefLog(RxBriefReason::Preamble, goodPeriods, 0, nowUs);
|
||||
preambleResetToIdle();
|
||||
if (wasObservable)
|
||||
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
{
|
||||
const uint32_t longSilence = IR_timeout * 2U;
|
||||
@ -1611,17 +1688,21 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
if (!isReciveRaw && front.dir &&
|
||||
((prevRise == 0U && front.time > longSilence) ||
|
||||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
|
||||
{
|
||||
preambleStartCandidate(front);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (preambleState == PreambleState::Candidate)
|
||||
{
|
||||
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
|
||||
#endif
|
||||
if (preambleWasObservable)
|
||||
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
|
||||
preambleStartCandidate(front);
|
||||
return true;
|
||||
}
|
||||
|
||||
preambleCandidateLastEdgeTime = front.time;
|
||||
@ -1639,17 +1720,20 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
preambleCandidateFirstRiseTime = front.time;
|
||||
if (!preambleRisePeriodCoarseOk(period))
|
||||
{
|
||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods,
|
||||
irClampU16(period), front.time);
|
||||
preambleGoodPeriods = 0;
|
||||
preambleMeanPeriod = 0;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
|
||||
#endif
|
||||
// Keep preambleWasObservable sticky: this edge proves the medium is
|
||||
// still active, but not that a possible physical frame has ended.
|
||||
// Only silence timeout or a real locked terminal releases it.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preambleGoodPeriods == 0)
|
||||
{
|
||||
preambleGoodPeriods = 1;
|
||||
preambleWasObservable = true;
|
||||
preambleMeanPeriod = (uint16_t)period;
|
||||
}
|
||||
else
|
||||
@ -1664,10 +1748,9 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
}
|
||||
else
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
|
||||
#endif
|
||||
preambleGoodPeriods = 1;
|
||||
preambleWasObservable = true;
|
||||
preambleMeanPeriod = (uint16_t)period;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,30 @@ class Print;
|
||||
#define riseTimeMin (riseTime - riseTolerance)
|
||||
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
||||
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
|
||||
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000;
|
||||
constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
|
||||
|
||||
/** Why the most recent observable receive attempt reached a terminal state. */
|
||||
enum class IR_RxTerminalReason : uint8_t
|
||||
{
|
||||
None = 0,
|
||||
FrameOk,
|
||||
FrameCrcError,
|
||||
LockedTimeout,
|
||||
DecodeAbort,
|
||||
CandidateTimeout
|
||||
};
|
||||
|
||||
/**
|
||||
* Monotonic receive-completion snapshot for schedulers polling after decoder.tick().
|
||||
* seq is allowed to wrap; consumers only compare it with their previous snapshot.
|
||||
*/
|
||||
struct IR_RxTerminalInfo
|
||||
{
|
||||
uint32_t seq = 0;
|
||||
IR_RxTerminalReason reason = IR_RxTerminalReason::None;
|
||||
uint8_t msgType = 0xFFU;
|
||||
bool hadLock = false;
|
||||
};
|
||||
|
||||
class IR_Encoder;
|
||||
class IR_DecoderRaw : virtual public IR_FOX
|
||||
@ -52,7 +75,39 @@ public:
|
||||
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||
bool isSubOverflow();
|
||||
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return pulseFilterDropFilteredOverflow; }
|
||||
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
|
||||
// Candidate после первого post-silence rise. Даже сильно искажённый ответ может не дать ни
|
||||
// одного coarse-valid периода, поэтому Candidate остаётся активным до lock/terminal либо
|
||||
// доказанной тишины по candidate timeout. Для гейта заднего: «не стрелять, пока на линии
|
||||
// идёт/формируется потенциальный кадр (напр. ответ точки)». Аддитивно, const.
|
||||
inline bool rxLineActive() const {
|
||||
return isRecive ||
|
||||
(preambleState == PreambleState::Candidate && preambleWasObservable);
|
||||
}
|
||||
/**
|
||||
* True while a real frame is active or ISR/filter work is still queued.
|
||||
* This closes the one-loop ordering gap when Timer::tick() runs before
|
||||
* decoder.tick(): a transmitter must not start while an unprocessed edge
|
||||
* is already waiting in the receive pipeline.
|
||||
*/
|
||||
bool rxPipelineActive() const;
|
||||
/**
|
||||
* Last terminal RX transition. Updated from tick()/decode context, never from ISR.
|
||||
* A frame that starts and finishes within one tick is observable through seq.
|
||||
*/
|
||||
IR_RxTerminalInfo rxLastTerminal() const { return rxTerminalInfo; }
|
||||
uint32_t rxTerminalSeq() const { return rxTerminalInfo.seq; }
|
||||
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
|
||||
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
|
||||
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
|
||||
inline uint16_t rxDeclaredPackSize() const {
|
||||
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
|
||||
}
|
||||
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
|
||||
static constexpr uint16_t rxMaxPackSize() {
|
||||
return static_cast<uint16_t>(irproto::kMaxWireFrameBytes);
|
||||
}
|
||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
||||
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
||||
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
||||
void pulseFilterResetStats();
|
||||
@ -65,6 +120,14 @@ public:
|
||||
uint16_t edgeTraceFlushChunk(Print &out, uint16_t maxRec = 48);
|
||||
#endif
|
||||
|
||||
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
|
||||
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
|
||||
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
|
||||
const uint16_t *rxReasonCounters() const { return rxReasonCnt; }
|
||||
void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; }
|
||||
/// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
|
||||
void printRxReasonStats(Print &out) const;
|
||||
|
||||
/// Кадр собран по длине из заголовка, но CRC не сошёлся — один раз можно прочитать копию сырых байтов.
|
||||
bool availableReject();
|
||||
uint8_t getRejectSize() const { return rejectPackSize; }
|
||||
@ -103,6 +166,7 @@ private:
|
||||
volatile bool isSubBufferOverflow = false;
|
||||
bool isBufferOverflow = false; // Флаг переполнения буффера данных
|
||||
bool isWrongPack = false; // Флаг битого пакета
|
||||
IR_RxTerminalInfo rxTerminalInfo;
|
||||
|
||||
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
|
||||
|
||||
@ -122,8 +186,6 @@ private:
|
||||
// volatile FrontStorage subBuffer[subBufferSize]; // вспомогательный буфер для хранения необработанных фронтов/спадов
|
||||
|
||||
RingBuffer<FrontStorage, subBufferSize> subBuffer;
|
||||
/** Очередь фронтов после потокового анти-глитча; tick() читает из неё при включённом фильтре. */
|
||||
RingBuffer<FrontStorage, subBufferSize> filteredSubBuffer;
|
||||
IR_Encoder *pairMuteEncoders[IR_PAIR_MUTE_MAX_ENCODERS]{};
|
||||
uint8_t pairMuteEncoderCount = 0;
|
||||
static constexpr uint8_t kPulseFilterHoldCap = 6;
|
||||
@ -131,7 +193,6 @@ private:
|
||||
uint8_t pulseFilterHoldCount = 0;
|
||||
bool pulseFilterLastRawValid = false;
|
||||
uint32_t pulseFilterLastRawTime = 0;
|
||||
uint32_t pulseFilterDropFilteredOverflow = 0;
|
||||
uint32_t pulseFilterDropHoldOverflow = 0;
|
||||
uint32_t pulseFilterDropGlitchPairs = 0;
|
||||
static constexpr uint8_t kPreambleLockNeed = (uint8_t)IR_PREAMBLE_LOCK_RISE_PERIODS;
|
||||
@ -143,6 +204,10 @@ private:
|
||||
};
|
||||
PreambleState preambleState = PreambleState::Idle;
|
||||
uint8_t preambleGoodPeriods = 0;
|
||||
// Sticky potential-frame latch for one Candidate epoch. After one plausible
|
||||
// rise period, coarse-invalid activity remains busy until proven silence;
|
||||
// the scheduler's hard deadline may skip an optional tail under noise.
|
||||
bool preambleWasObservable = false;
|
||||
uint16_t preambleMeanPeriod = 0;
|
||||
uint32_t preambleCandidateLastEdgeTime = 0;
|
||||
uint32_t preambleCandidateFirstRiseTime = 0;
|
||||
@ -197,12 +262,12 @@ bool isReciveRaw = false;
|
||||
void checkTimeout(); //
|
||||
/** В очередях/hold фильтра ещё есть фронты — не оценивать таймаут по micros()-lastEdgeTime (ложный TIMEOUT). */
|
||||
bool rxTimeoutPipelineBusy() const;
|
||||
/** Один сырой фронт из subBuffer -> потоковый holdback-антиглитч. */
|
||||
void pulseFilterFeedOneRaw(const FrontStorage &e);
|
||||
void pulseFilterFlushTimeout(uint32_t nowUs);
|
||||
bool pulseFilterEmit(const FrontStorage &e);
|
||||
void pulseFilterPushRaw(const FrontStorage &e);
|
||||
bool pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime = 0);
|
||||
bool pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out);
|
||||
void pulseFilterShiftLeft(uint8_t n);
|
||||
void pulseFilterReset();
|
||||
void processDecodedFront(const FrontStorage ¤tFront);
|
||||
static uint32_t absDiffU32(uint32_t a, uint32_t b);
|
||||
bool registerPairMuteEncoder(IR_Encoder *enc);
|
||||
void refreshPairMuteState();
|
||||
@ -211,6 +276,9 @@ bool isReciveRaw = false;
|
||||
void preambleResetToIdle();
|
||||
void preambleStartCandidate(const FrontStorage &front);
|
||||
bool preambleProcessEdge(const FrontStorage &front);
|
||||
void expirePreambleCandidateIfIdle(uint32_t nowUs);
|
||||
uint8_t currentRxMsgType() const;
|
||||
void noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock);
|
||||
|
||||
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
|
||||
/// @param len Длина в байтах проверяемых данных
|
||||
@ -238,9 +306,13 @@ bool isReciveRaw = false;
|
||||
/// @return Результат
|
||||
uint16_t ceil_div(uint16_t val, uint16_t divider);
|
||||
|
||||
// rxBriefLog безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason]
|
||||
// (always-on наблюдаемость по контракту живучести), печать события —
|
||||
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
|
||||
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
||||
uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13
|
||||
#if IR_RX_BRIEF_LOG
|
||||
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
|
||||
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
||||
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
||||
void rxBriefNoteRawOverflowIsr(uint32_t tUs);
|
||||
void rxBriefFlushDeferredIsrLogs();
|
||||
|
||||
396
IR_Encoder.cpp
396
IR_Encoder.cpp
@ -1,7 +1,24 @@
|
||||
#include "IR_Encoder.h"
|
||||
#include "IR_DecoderRaw.h"
|
||||
#include "IrTxIsrBufferedStorage.h"
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#define IRPROTO_PRAGMA_MESSAGE(text) __pragma(message(text))
|
||||
#else
|
||||
#define IRPROTO_PRAGMA_MESSAGE(text) _Pragma(#text)
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_STM32)
|
||||
#if defined(STM32G4xx)
|
||||
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR + built-in DMA"))
|
||||
#elif defined(STM32F4xx)
|
||||
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR only"))
|
||||
#else
|
||||
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR"))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define LoopOut 12
|
||||
#define ISR_Out 10
|
||||
#define TestOut 13
|
||||
@ -14,6 +31,7 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool
|
||||
{
|
||||
setPin(pin);
|
||||
id = addr;
|
||||
txIsrMode_ = txIsrLegacyMode_ ? TxIsrMode::Legacy : TxIsrMode::Buffered;
|
||||
this->decPair = decPair;
|
||||
if (decPair != nullptr)
|
||||
{
|
||||
@ -45,9 +63,44 @@ HardwareTimer* IR_Encoder::IR_Timer = nullptr;
|
||||
IR_Encoder::ExternalTxStartFn IR_Encoder::externalTxStartFn = nullptr;
|
||||
IR_Encoder::ExternalTxBusyFn IR_Encoder::externalTxBusyFn = nullptr;
|
||||
void *IR_Encoder::externalTxCtx = nullptr;
|
||||
bool IR_Encoder::txIsrLegacyMode_ = false;
|
||||
bool IR_Encoder::txIsrLegacyMode_ = true;
|
||||
uint16_t IR_Encoder::s_carrierMultiply = 2;
|
||||
|
||||
const char* irSendStatusToString(IR_SendStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case IR_SendStatus::Success:
|
||||
return "Success";
|
||||
case IR_SendStatus::PayloadTooLarge:
|
||||
return "PayloadTooLarge";
|
||||
case IR_SendStatus::EncoderBusy:
|
||||
return "EncoderBusy";
|
||||
case IR_SendStatus::BufferTooLarge:
|
||||
return "BufferTooLarge";
|
||||
case IR_SendStatus::ExternalBackendBusy:
|
||||
return "ExternalBackendBusy";
|
||||
case IR_SendStatus::ExternalStartFailed:
|
||||
return "ExternalStartFailed";
|
||||
case IR_SendStatus::ExternalNoStream:
|
||||
return "ExternalNoStream";
|
||||
case IR_SendStatus::ExternalInvalidConfig:
|
||||
return "ExternalInvalidConfig";
|
||||
case IR_SendStatus::BuildGateRunsFailed:
|
||||
return "BuildGateRunsFailed";
|
||||
case IR_SendStatus::ScaleGateRunsFailed:
|
||||
return "ScaleGateRunsFailed";
|
||||
case IR_SendStatus::DmaStartFailed:
|
||||
return "DmaStartFailed";
|
||||
case IR_SendStatus::EncoderPinUnavailable:
|
||||
return "EncoderPinUnavailable";
|
||||
case IR_SendStatus::BufferedStorageInvalid:
|
||||
return "BufferedStorageInvalid";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void IR_Encoder::setCarrierMultiply(uint16_t multiply)
|
||||
{
|
||||
if (multiply < 2)
|
||||
@ -146,6 +199,11 @@ bool IR_Encoder::scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, si
|
||||
void IR_Encoder::setTxIsrLegacyMode(bool legacy)
|
||||
{
|
||||
txIsrLegacyMode_ = legacy;
|
||||
const TxIsrMode mode = legacy ? TxIsrMode::Legacy : TxIsrMode::Buffered;
|
||||
for (IR_Encoder *p = head; p != nullptr; p = p->next)
|
||||
{
|
||||
p->txIsrMode_ = mode;
|
||||
}
|
||||
}
|
||||
|
||||
bool IR_Encoder::txIsrLegacyMode()
|
||||
@ -153,6 +211,54 @@ bool IR_Encoder::txIsrLegacyMode()
|
||||
return txIsrLegacyMode_;
|
||||
}
|
||||
|
||||
void IR_Encoder::attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage)
|
||||
{
|
||||
txBufferedCtx_ = &storage;
|
||||
}
|
||||
|
||||
void IR_Encoder::detachBufferedIsrStorage()
|
||||
{
|
||||
txBufferedCtx_ = nullptr;
|
||||
if (!isSending)
|
||||
{
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
txUseBufferedIsr_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IR_Encoder::hasBufferedIsrStorage() const
|
||||
{
|
||||
return txBufferedCtx_ != nullptr && txBufferedCtx_->isValid();
|
||||
}
|
||||
|
||||
void IR_Encoder::enableBufferedIsr(IrTxIsrBufferedStorageBase& storage)
|
||||
{
|
||||
attachBufferedIsrStorage(storage);
|
||||
txIsrMode_ = TxIsrMode::Buffered;
|
||||
}
|
||||
|
||||
void IR_Encoder::disableBufferedIsr()
|
||||
{
|
||||
txIsrMode_ = TxIsrMode::Legacy;
|
||||
if (!isSending)
|
||||
{
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
txUseBufferedIsr_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
IR_Encoder::TxIsrMode IR_Encoder::txIsrMode() const
|
||||
{
|
||||
return txIsrMode_;
|
||||
}
|
||||
|
||||
bool IR_Encoder::shouldUseBufferedIsr() const
|
||||
{
|
||||
return txIsrMode_ == TxIsrMode::Buffered &&
|
||||
txBufferedCtx_ != nullptr &&
|
||||
txBufferedCtx_->isValid();
|
||||
}
|
||||
|
||||
bool IR_Encoder::txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal)
|
||||
{
|
||||
while (true)
|
||||
@ -336,6 +442,8 @@ void IR_Encoder::externalFinishSend()
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
refreshBlindDecoderMuteState();
|
||||
}
|
||||
|
||||
@ -392,6 +500,107 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
|
||||
return runCount;
|
||||
}
|
||||
|
||||
size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply)
|
||||
{
|
||||
if (packet == nullptr || outRuns == nullptr || maxRuns == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (len == 0 || len > dataByteSizeMax)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (multiply < 2)
|
||||
{
|
||||
multiply = 2;
|
||||
}
|
||||
|
||||
// Copy into fixed-size buffer to match original encoder behavior (safe reads past sendLen).
|
||||
uint8_t sendBufferLocal[dataByteSizeMax] = {0};
|
||||
memcpy(sendBufferLocal, packet, len);
|
||||
|
||||
TxFsmState st{};
|
||||
st.sendLen = len;
|
||||
st.toggleCounter = preambToggle;
|
||||
st.dataBitCounter = bitPerByte - 1;
|
||||
st.dataByteCounter = 0;
|
||||
st.preambFrontCounter = preambPulse * 2 - 1;
|
||||
st.dataSequenceCounter = bitPerByte * 2;
|
||||
st.syncSequenceCounter = syncBits * 2;
|
||||
st.syncLastBit = false;
|
||||
st.signal = preamb;
|
||||
st.state = HIGH;
|
||||
st.currentBitSequence = bitHigh;
|
||||
|
||||
auto appendPhysicalRun = [&](bool gate, uint32_t logicalLen, size_t& runCount) -> bool {
|
||||
if (logicalLen == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t phys = (logicalLen * (uint32_t)multiply) / 2U;
|
||||
if (logicalLen > 0 && phys == 0)
|
||||
{
|
||||
phys = 1;
|
||||
}
|
||||
|
||||
while (phys > 0)
|
||||
{
|
||||
if (runCount >= maxRuns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t chunk = phys > 65535U ? 65535U : phys;
|
||||
outRuns[runCount].gate = gate;
|
||||
outRuns[runCount].lenTicks = static_cast<uint16_t>(chunk);
|
||||
runCount++;
|
||||
phys -= chunk;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
size_t runCount = 0;
|
||||
bool currentGate = false;
|
||||
uint32_t currentLogicalLen = 0;
|
||||
bool havePendingRun = false;
|
||||
bool isActive = true;
|
||||
while (isActive)
|
||||
{
|
||||
bool gate = false;
|
||||
isActive = txEmitTick(st, sendBufferLocal, gate);
|
||||
|
||||
if (!havePendingRun)
|
||||
{
|
||||
currentGate = gate;
|
||||
currentLogicalLen = 1U;
|
||||
havePendingRun = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentGate == gate)
|
||||
{
|
||||
currentLogicalLen++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentGate = gate;
|
||||
currentLogicalLen = 1U;
|
||||
}
|
||||
|
||||
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return runCount;
|
||||
}
|
||||
|
||||
|
||||
void IR_Encoder::enable()
|
||||
{
|
||||
@ -479,10 +688,13 @@ IR_SendResult IR_Encoder::sendData(uint16_t addrTo, uint8_t *data, uint8_t len,
|
||||
|
||||
IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data, uint8_t len, bool needAccept)
|
||||
{
|
||||
if (len > bytePerPack)
|
||||
// 5-битное поле длины => ВЕСЬ кадр ≤31 байт (для Data payload ≤24). Было `len > bytePerPack(31)` —
|
||||
// неверно: packSize=7+len оборачивался в заголовке (packSize & 0x1F) при len 25..31 → кадр молча
|
||||
// терялся, а send возвращал успех. Проверяем полный packSize в широком типе (uint8_t 7+len мог переполниться).
|
||||
if (((uint16_t)msgBytes + addrBytes + addrBytes + len + crcBytes) > IR_MASK_MSG_INFO)
|
||||
{
|
||||
Serial.println("IR Pack to big");
|
||||
return IR_SendResult(false, 0);
|
||||
return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge);
|
||||
}
|
||||
constexpr uint8_t dataStart = msgBytes + addrBytes + addrBytes;
|
||||
memset(sendBuffer, 0x00, dataByteSizeMax);
|
||||
@ -532,11 +744,15 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8
|
||||
// }
|
||||
|
||||
// отправка
|
||||
rawSend(sendBuffer, packSize);
|
||||
|
||||
const IR_SendStatus status = rawSend(sendBuffer, packSize);
|
||||
if (status != IR_SendStatus::Success)
|
||||
{
|
||||
return IR_SendResult(false, 0, status);
|
||||
}
|
||||
|
||||
// Возвращаем результат отправки
|
||||
uint32_t sendTime = calculateSendTime(packSize);
|
||||
return IR_SendResult(true, sendTime);
|
||||
return IR_SendResult(true, sendTime, status);
|
||||
}
|
||||
|
||||
|
||||
@ -560,11 +776,15 @@ IR_SendResult IR_Encoder::sendAccept(uint16_t addrTo, uint8_t customByte)
|
||||
sendBuffer[4] = crc8(sendBuffer, 0, 4, poly1) & 0xFF;
|
||||
sendBuffer[5] = crc8(sendBuffer, 0, 5, poly2) & 0xFF;
|
||||
|
||||
rawSend(sendBuffer, packsize);
|
||||
|
||||
const IR_SendStatus status = rawSend(sendBuffer, packsize);
|
||||
if (status != IR_SendStatus::Success)
|
||||
{
|
||||
return IR_SendResult(false, 0, status);
|
||||
}
|
||||
|
||||
// Возвращаем результат отправки
|
||||
uint32_t sendTime = calculateSendTime(packsize);
|
||||
return IR_SendResult(true, sendTime);
|
||||
return IR_SendResult(true, sendTime, status);
|
||||
}
|
||||
|
||||
IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo)
|
||||
@ -586,11 +806,15 @@ IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo)
|
||||
sendBuffer[5] = crc8(sendBuffer, 0, 5, poly1) & 0xFF;
|
||||
sendBuffer[6] = crc8(sendBuffer, 0, 6, poly2) & 0xFF;
|
||||
|
||||
rawSend(sendBuffer, packsize);
|
||||
|
||||
const IR_SendStatus status = rawSend(sendBuffer, packsize);
|
||||
if (status != IR_SendStatus::Success)
|
||||
{
|
||||
return IR_SendResult(false, 0, status);
|
||||
}
|
||||
|
||||
// Возвращаем результат отправки
|
||||
uint32_t sendTime = calculateSendTime(packsize);
|
||||
return IR_SendResult(true, sendTime);
|
||||
return IR_SendResult(true, sendTime, status);
|
||||
}
|
||||
|
||||
IR_SendResult IR_Encoder::sendBack(uint8_t data)
|
||||
@ -610,14 +834,17 @@ IR_SendResult IR_Encoder::sendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len
|
||||
|
||||
IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len)
|
||||
{
|
||||
if (len > bytePerPack)
|
||||
// Длина = ВЕСЬ кадр в 5 битах (≤31). Проверяем полный packSize. Было `len>bytePerPack` + `min(1,len)`:
|
||||
// многобайтовый back (speed + customBackData) слался ОБРЕЗАННЫМ — packSize считал лишь 1 байт данных,
|
||||
// остальные не влезали в кадр и затирались CRC. Теперь учитываем полный len.
|
||||
if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO)
|
||||
{
|
||||
return IR_SendResult(false, 0);
|
||||
return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge);
|
||||
}
|
||||
memset(sendBuffer, 0x00, dataByteSizeMax);
|
||||
uint8_t dataStart = msgBytes + addrBytes + (isAdressed ? addrBytes : 0);
|
||||
|
||||
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes;
|
||||
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes;
|
||||
uint8_t msgType =
|
||||
((isAdressed ? IR_MSG_BACK_TO : IR_MSG_BACK) << 5) | ((packSize) & IR_MASK_MSG_INFO);
|
||||
|
||||
@ -643,11 +870,15 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d
|
||||
sendBuffer[packSize - crcBytes + 1] = crc8(sendBuffer, 0, packSize - crcBytes + 1, poly2) & 0xFF;
|
||||
|
||||
// отправка
|
||||
rawSend(sendBuffer, packSize);
|
||||
|
||||
const IR_SendStatus status = rawSend(sendBuffer, packSize);
|
||||
if (status != IR_SendStatus::Success)
|
||||
{
|
||||
return IR_SendResult(false, 0, status);
|
||||
}
|
||||
|
||||
// Возвращаем результат отправки
|
||||
uint32_t sendTime = calculateSendTime(packSize);
|
||||
return IR_SendResult(true, sendTime);
|
||||
return IR_SendResult(true, sendTime, status);
|
||||
}
|
||||
|
||||
void IR_Encoder::registerWithBlindDecoders()
|
||||
@ -674,18 +905,18 @@ void IR_Encoder::refreshBlindDecoderMuteState()
|
||||
}
|
||||
}
|
||||
|
||||
void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
{
|
||||
if (isSending)
|
||||
{
|
||||
// TODO: Обработка повторной отправки
|
||||
return;
|
||||
return IR_SendStatus::EncoderBusy;
|
||||
}
|
||||
|
||||
// Проверка на переполнение буфера
|
||||
if (len > dataByteSizeMax)
|
||||
{
|
||||
return;
|
||||
return IR_SendStatus::BufferTooLarge;
|
||||
}
|
||||
|
||||
// Serial.print("IR tx hex: ");
|
||||
@ -700,25 +931,27 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
{
|
||||
if (externalTxBusyFn != nullptr && externalTxBusyFn(externalTxCtx))
|
||||
{
|
||||
return;
|
||||
return IR_SendStatus::ExternalBackendBusy;
|
||||
}
|
||||
|
||||
sendLen = len;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
|
||||
const bool ok = externalTxStartFn(externalTxCtx, this, ptr, len);
|
||||
if (!ok)
|
||||
const IR_SendStatus status = externalTxStartFn(externalTxCtx, this, ptr, len);
|
||||
if (status != IR_SendStatus::Success)
|
||||
{
|
||||
isSending = false;
|
||||
refreshBlindDecoderMuteState();
|
||||
}
|
||||
return;
|
||||
return status;
|
||||
}
|
||||
|
||||
if (port == nullptr || mask == 0)
|
||||
{
|
||||
return;
|
||||
return IR_SendStatus::EncoderPinUnavailable;
|
||||
}
|
||||
|
||||
if (ptr != sendBuffer)
|
||||
@ -727,7 +960,11 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
}
|
||||
sendLen = len;
|
||||
|
||||
if (txIsrLegacyMode_)
|
||||
const bool useBufferedIsr = shouldUseBufferedIsr();
|
||||
txUseBufferedIsr_ = useBufferedIsr;
|
||||
txActiveBufferedCtx_ = useBufferedIsr ? txBufferedCtx_ : nullptr;
|
||||
|
||||
if (!useBufferedIsr)
|
||||
{
|
||||
toggleCounter = preambToggle;
|
||||
dataBitCounter = bitPerByte - 1;
|
||||
@ -753,40 +990,43 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
IR_Encoder::carrierResume();
|
||||
return;
|
||||
return IR_SendStatus::Success;
|
||||
}
|
||||
|
||||
size_t nRuns = buildGateRuns(sendBuffer, len, txGateRuns_, irproto::kIsrTxMaxGateRuns);
|
||||
IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_;
|
||||
if (buf == nullptr || !buf->isValid())
|
||||
{
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
return IR_SendStatus::BufferedStorageInvalid;
|
||||
}
|
||||
|
||||
buf->resetRuntimeState();
|
||||
|
||||
txMultiplySnap_ = carrierMultiply();
|
||||
size_t nRuns = buildPhysicalGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns, txMultiplySnap_);
|
||||
if (nRuns == 0U)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!scaleGateRunsToPhysical(txGateRuns_, &nRuns, irproto::kIsrTxMaxGateRuns, carrierMultiply()))
|
||||
{
|
||||
return;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
return IR_SendStatus::BuildGateRunsFailed;
|
||||
}
|
||||
|
||||
uint32_t total = 0;
|
||||
for (size_t i = 0; i < nRuns; i++)
|
||||
{
|
||||
total += txGateRuns_[i].lenTicks;
|
||||
total += buf->gateRuns[i].lenTicks;
|
||||
}
|
||||
txBsrrTotalTicks_ = total;
|
||||
buf->totalTicks = total;
|
||||
|
||||
const uint32_t setW = (uint32_t)mask;
|
||||
const uint32_t resetW = ((uint32_t)mask) << 16U;
|
||||
txMultiplySnap_ = carrierMultiply();
|
||||
{
|
||||
const uint16_t cap = maxPowerNumerator();
|
||||
txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_;
|
||||
}
|
||||
txBsrrWave_.configure(setW, resetW, txGateRuns_, nRuns, txMultiplySnap_, txPowerSnap_);
|
||||
|
||||
txBsrrHalfLen_ = (uint16_t)(irproto::kIsrTxBsrrWordCount / 2U);
|
||||
txBsrrWave_.fill(txBsrrWords_, irproto::kIsrTxBsrrWordCount);
|
||||
|
||||
txBsrrReadIdx_ = 0;
|
||||
txBsrrTicksSent_ = 0;
|
||||
buf->wave.configure(setW, resetW, buf->gateRuns, nRuns, txMultiplySnap_, txPowerSnap_);
|
||||
buf->wave.fill(buf->bsrrWords, buf->wordCount);
|
||||
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
@ -795,6 +1035,7 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
port->BSRR = resetW;
|
||||
}
|
||||
IR_Encoder::carrierResume();
|
||||
return IR_SendStatus::Success;
|
||||
}
|
||||
|
||||
void IR_Encoder::isr()
|
||||
@ -815,7 +1056,7 @@ void IR_Encoder::_isr()
|
||||
if (port == nullptr)
|
||||
return;
|
||||
|
||||
if (txIsrLegacyMode_)
|
||||
if (!txUseBufferedIsr_)
|
||||
{
|
||||
const uint32_t setW = (uint32_t)mask;
|
||||
const uint32_t resetW = ((uint32_t)mask) << 16U;
|
||||
@ -850,33 +1091,49 @@ void IR_Encoder::_isr()
|
||||
{
|
||||
port->BSRR = resetW;
|
||||
isSending = false;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
refreshBlindDecoderMuteState();
|
||||
carrierStopPending = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
port->BSRR = txBsrrWords_[txBsrrReadIdx_];
|
||||
txBsrrReadIdx_++;
|
||||
txBsrrTicksSent_++;
|
||||
|
||||
if (txBsrrTicksSent_ >= txBsrrTotalTicks_)
|
||||
IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_;
|
||||
if (buf == nullptr || !buf->isValid())
|
||||
{
|
||||
port->BSRR = ((uint32_t)mask) << 16U;
|
||||
isSending = false;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
refreshBlindDecoderMuteState();
|
||||
carrierStopPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (txBsrrReadIdx_ == txBsrrHalfLen_)
|
||||
port->BSRR = buf->bsrrWords[buf->readIdx];
|
||||
buf->readIdx++;
|
||||
buf->ticksSent++;
|
||||
|
||||
if (buf->ticksSent >= buf->totalTicks)
|
||||
{
|
||||
txBsrrWave_.fill(&txBsrrWords_[0], txBsrrHalfLen_);
|
||||
port->BSRR = ((uint32_t)mask) << 16U;
|
||||
isSending = false;
|
||||
txUseBufferedIsr_ = false;
|
||||
txActiveBufferedCtx_ = nullptr;
|
||||
refreshBlindDecoderMuteState();
|
||||
carrierStopPending = true;
|
||||
return;
|
||||
}
|
||||
else if (txBsrrReadIdx_ >= irproto::kIsrTxBsrrWordCount)
|
||||
|
||||
if (buf->readIdx == buf->halfLen)
|
||||
{
|
||||
txBsrrReadIdx_ = 0;
|
||||
txBsrrWave_.fill(&txBsrrWords_[txBsrrHalfLen_], txBsrrHalfLen_);
|
||||
buf->wave.fill(&buf->bsrrWords[0], buf->halfLen);
|
||||
}
|
||||
else if (buf->readIdx >= buf->wordCount)
|
||||
{
|
||||
buf->readIdx = 0;
|
||||
buf->wave.fill(&buf->bsrrWords[buf->halfLen], buf->halfLen);
|
||||
}
|
||||
}
|
||||
|
||||
@ -926,26 +1183,9 @@ uint8_t IR_Encoder::bitLow[2] = {
|
||||
|
||||
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
|
||||
{
|
||||
// Расчет времени отправки пакета в миллисекундах
|
||||
|
||||
// Время преамбулы: preambPulse * 2 фронта * bitTakts тактов
|
||||
uint32_t preambTime = preambPulse * 2 * bitTakts;
|
||||
|
||||
// Время данных: количество бит * bitTakts тактов
|
||||
uint32_t dataTime = packSize * 8 * bitTakts;
|
||||
|
||||
// Время синхронизации: syncBits * 2 фронта * bitTakts тактов
|
||||
uint32_t syncTime = syncBits * 2 * bitTakts;
|
||||
|
||||
// Общее время в тактах
|
||||
uint32_t totalTakts = preambTime + dataTime + syncTime;
|
||||
|
||||
// Конвертируем в миллисекунды
|
||||
// carrierPeriod - период несущей в микросекундах
|
||||
// totalTakts * carrierPeriod / 1000 = время в миллисекундах
|
||||
uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000;
|
||||
|
||||
return sendTimeMs;
|
||||
// The TX FSM emits syncBits after every wire byte (including the last)
|
||||
// and its preamble runs are preambToggle+1 logical ticks long.
|
||||
return irproto::wireAirtimeMsCeil(packSize);
|
||||
}
|
||||
|
||||
// Функции для тестирования времени отправки без фактической отправки
|
||||
@ -1000,12 +1240,12 @@ uint32_t IR_Encoder::testSendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len)
|
||||
|
||||
uint32_t IR_Encoder::testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const
|
||||
{
|
||||
if (len > bytePerPack)
|
||||
if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO)
|
||||
{
|
||||
return 0; // Возвращаем 0 для недопустимого размера
|
||||
}
|
||||
|
||||
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes;
|
||||
|
||||
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes;
|
||||
return calculateSendTime(packSize);
|
||||
}
|
||||
|
||||
|
||||
78
IR_Encoder.h
78
IR_Encoder.h
@ -1,19 +1,41 @@
|
||||
#pragma once
|
||||
#include "IR_config.h"
|
||||
#include "IrTxBsrrWave.h"
|
||||
#include "IrTxGateTypes.h"
|
||||
|
||||
// TODO: Отложенная передача после завершения приема
|
||||
|
||||
enum class IR_SendStatus : uint8_t {
|
||||
Success = 0,
|
||||
PayloadTooLarge,
|
||||
EncoderBusy,
|
||||
BufferTooLarge,
|
||||
ExternalBackendBusy,
|
||||
ExternalStartFailed,
|
||||
ExternalNoStream,
|
||||
ExternalInvalidConfig,
|
||||
BuildGateRunsFailed,
|
||||
ScaleGateRunsFailed,
|
||||
DmaStartFailed,
|
||||
EncoderPinUnavailable,
|
||||
BufferedStorageInvalid,
|
||||
};
|
||||
|
||||
const char* irSendStatusToString(IR_SendStatus status);
|
||||
|
||||
// Структура для возврата результата отправки
|
||||
struct IR_SendResult {
|
||||
bool success; // Флаг успешности отправки
|
||||
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
|
||||
|
||||
IR_SendResult(bool success = false, uint32_t sendTimeMs = 0)
|
||||
: success(success), sendTimeMs(sendTimeMs) {}
|
||||
bool success; // Флаг успешности отправки
|
||||
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
|
||||
IR_SendStatus status; // Детализированный статус старта передачи
|
||||
|
||||
IR_SendResult(bool success = false,
|
||||
uint32_t sendTimeMs = 0,
|
||||
IR_SendStatus status = IR_SendStatus::ExternalStartFailed)
|
||||
: success(success), sendTimeMs(sendTimeMs), status(status) {}
|
||||
};
|
||||
|
||||
class IR_DecoderRaw;
|
||||
class IrTxIsrBufferedStorageBase;
|
||||
class IR_Encoder : public IR_FOX
|
||||
{
|
||||
friend IR_DecoderRaw;
|
||||
@ -24,9 +46,13 @@ public:
|
||||
static HardwareTimer* IR_Timer;
|
||||
|
||||
using IR_TxGateRun = IrTxGateRun;
|
||||
enum class TxIsrMode : uint8_t {
|
||||
Legacy = 0,
|
||||
Buffered = 1
|
||||
};
|
||||
|
||||
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||||
using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||
private:
|
||||
// uint16_t id; /// @brief Адрес передатчика
|
||||
public:
|
||||
@ -36,6 +62,10 @@ public:
|
||||
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||||
/// (аналог setBlindDecoders() для одного RX).
|
||||
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
||||
/// Публичная оценка airtime кадра (мс) по его полной длине в байтах (packSize). Чистая функция
|
||||
/// протокольных констант — подходит и для приёма (напр. бюджет удержания заднего по объявленному
|
||||
/// в 1-м байте размеру принимаемого ответа). БЕЗ +30% компенсации занижения sync — добавляет потребитель.
|
||||
uint32_t packAirtimeMs(uint8_t packSize) const { return calculateSendTime(packSize); }
|
||||
static void isr();
|
||||
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
||||
/**
|
||||
@ -57,7 +87,7 @@ public:
|
||||
/** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */
|
||||
void setPowerPercent(uint8_t p);
|
||||
|
||||
/** После buildGateRuns: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
|
||||
/** Legacy helper: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
|
||||
static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply);
|
||||
|
||||
/** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */
|
||||
@ -67,12 +97,21 @@ public:
|
||||
static void tick();
|
||||
|
||||
/**
|
||||
* Режим внутреннего TX без DMA: false — BSRR + кольцо (buildGateRuns + scaleGateRunsToPhysical);
|
||||
* Режим внутреннего TX без DMA: false — BSRR + кольцо (direct physical gate-runs builder);
|
||||
* true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM).
|
||||
* Выставить до begin/rawSend (глобально на все IR_Encoder). Игнорируется при externalTxStartFn.
|
||||
* По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет
|
||||
* все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder
|
||||
* привязан storage через attachBufferedIsrStorage()/enableBufferedIsr().
|
||||
* Выставить до begin/rawSend. Игнорируется при externalTxStartFn.
|
||||
*/
|
||||
static void setTxIsrLegacyMode(bool legacy);
|
||||
static bool txIsrLegacyMode();
|
||||
void attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage);
|
||||
void detachBufferedIsrStorage();
|
||||
bool hasBufferedIsrStorage() const;
|
||||
void enableBufferedIsr(IrTxIsrBufferedStorageBase& storage);
|
||||
void disableBufferedIsr();
|
||||
TxIsrMode txIsrMode() const;
|
||||
|
||||
/** Optional: register external TX backend (e.g. DMA driver). */
|
||||
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||
@ -80,8 +119,10 @@ public:
|
||||
/** Called by external TX backend on actual end of transmission. */
|
||||
void externalFinishSend();
|
||||
|
||||
/** Build RLE runs of carrier gate for a packet (no HW access). */
|
||||
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
||||
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
||||
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
||||
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
@ -94,7 +135,7 @@ public:
|
||||
"IR_Encoder::setBlindDecoders: array size exceeds IR_PAIR_MUTE_MAX_ENCODERS");
|
||||
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||||
}
|
||||
void rawSend(uint8_t *ptr, uint8_t len);
|
||||
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
||||
|
||||
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||||
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||||
@ -175,14 +216,7 @@ private:
|
||||
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
||||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||
void storeTxFsmToMembers(const TxFsmState &st);
|
||||
|
||||
IrTxBsrrWave txBsrrWave_{};
|
||||
IR_TxGateRun txGateRuns_[irproto::kIsrTxMaxGateRuns]{};
|
||||
uint32_t txBsrrWords_[irproto::kIsrTxBsrrWordCount]{};
|
||||
uint16_t txBsrrReadIdx_ = 0;
|
||||
uint16_t txBsrrHalfLen_ = 0;
|
||||
uint32_t txBsrrTotalTicks_ = 0;
|
||||
uint32_t txBsrrTicksSent_ = 0;
|
||||
bool shouldUseBufferedIsr() const;
|
||||
|
||||
/** Снимок на старт TX (буферный и legacy путь). */
|
||||
uint16_t txPowerSnap_ = 1;
|
||||
@ -194,6 +228,10 @@ private:
|
||||
uint16_t legacySlotInPeriod_ = 0;
|
||||
|
||||
volatile uint16_t powerNumerator_ = 1;
|
||||
IrTxIsrBufferedStorageBase* txBufferedCtx_ = nullptr;
|
||||
IrTxIsrBufferedStorageBase* txActiveBufferedCtx_ = nullptr;
|
||||
TxIsrMode txIsrMode_ = TxIsrMode::Legacy;
|
||||
bool txUseBufferedIsr_ = false;
|
||||
|
||||
IR_DecoderRaw *decPair = nullptr;
|
||||
IR_DecoderRaw *singleBlindDecoder = nullptr;
|
||||
|
||||
204
IR_config.h
204
IR_config.h
@ -6,6 +6,13 @@
|
||||
/** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */
|
||||
namespace irproto {
|
||||
constexpr size_t kDefaultDmaTxMaxStreams = 4U;
|
||||
/** NVIC-приоритет IRQ DMA-каналов TX (refill кольца BSRR-слов). Refill длинный (полубуфер
|
||||
* 2048 слов = миллисекунды на небыстрых SYSCLK), а его дедлайн мягкий (~9 мс при 38кГц×6),
|
||||
* поэтому он обязан сидеть НИЖЕ SysTick (0) и EXTI приёмника (6): с приоритетом по умолчанию
|
||||
* (0, HAL_NVIC_EnableIRQ без SetPriority) он не вытеснялся SysTick'ом, тики слипались и
|
||||
* millis() замедлялся в 1.6×/2.2× (один/два потока) на всё время передачи — мерено на стенде
|
||||
* TofSickHunt 2026-08-11. Меньше число = выше приоритет; 8 = ниже EXTI, выше самого дна. */
|
||||
constexpr uint32_t kDmaTxIrqPriority = 8U;
|
||||
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
|
||||
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
|
||||
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
|
||||
@ -21,7 +28,7 @@ static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be eve
|
||||
// Краткий лог причин, почему физический сигнал не дошёл до распознанного пакета.
|
||||
// Формат и коды: ref/IR_RX_BRIEF_LOG.md
|
||||
#ifndef IR_RX_BRIEF_LOG
|
||||
#define IR_RX_BRIEF_LOG 1
|
||||
#define IR_RX_BRIEF_LOG 0
|
||||
#endif
|
||||
// 1: печатать только отклонённые/ошибочные события; успехи и шумовые PREAMB скрыть.
|
||||
#ifndef IR_RX_BRIEF_LOG_REJECT_ONLY
|
||||
@ -179,6 +186,13 @@ typedef uint16_t crc_t;
|
||||
#ifndef IR_INPUT_MIN_PULSE_US
|
||||
#define IR_INPUT_MIN_PULSE_US 0
|
||||
#endif
|
||||
/** Сколько сырых фронтов забирать из ISR-буфера за один tick() (батч-выемка).
|
||||
* 1 фронт/tick привязывал RX-пропускную способность к частоте loop: медленный
|
||||
* loop (телеметрия/дисплей) переполнял буфер 250 фронтов (~120мс эфира) и
|
||||
* кадры терялись молча. Батч развязывает приём от темпа loop. */
|
||||
#ifndef IR_RX_TICK_BATCH
|
||||
#define IR_RX_TICK_BATCH 16U
|
||||
#endif
|
||||
/** Сколько подтверждённых фронтов держать перед выпуском в декодер (потоковая задержка). */
|
||||
#ifndef IR_INPUT_FILTER_HOLDBACK_EDGES
|
||||
#define IR_INPUT_FILTER_HOLDBACK_EDGES 3U
|
||||
@ -250,6 +264,194 @@ typedef uint16_t crc_t;
|
||||
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
||||
#define tolerance 300U
|
||||
|
||||
namespace irproto
|
||||
{
|
||||
/** Maximum complete frame length representable by the five header bits. */
|
||||
constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
|
||||
|
||||
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
|
||||
constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||
|
||||
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
|
||||
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
|
||||
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
|
||||
|
||||
/** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
|
||||
constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
|
||||
{
|
||||
return payloadBytes <= kMaxDataPayloadBytes
|
||||
? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
|
||||
: 0U;
|
||||
}
|
||||
|
||||
/** Complete non-addressed BACK frame size, or zero when it cannot fit. */
|
||||
constexpr uint8_t backWireBytes(uint8_t payloadBytes)
|
||||
{
|
||||
return payloadBytes <= kMaxBackPayloadBytes
|
||||
? static_cast<uint8_t>(kBackFrameOverheadBytes + payloadBytes)
|
||||
: 0U;
|
||||
}
|
||||
|
||||
/** Complete addressed BACK_TO frame size, or zero when it cannot fit. */
|
||||
constexpr uint8_t backToWireBytes(uint8_t payloadBytes)
|
||||
{
|
||||
return payloadBytes <= kMaxBackToPayloadBytes
|
||||
? static_cast<uint8_t>(kBackToFrameOverheadBytes + payloadBytes)
|
||||
: 0U;
|
||||
}
|
||||
|
||||
/** Minimum complete frame size for a known message type; zero means reserved/unknown. */
|
||||
constexpr uint8_t minimumWireBytes(uint8_t msgType)
|
||||
{
|
||||
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT)
|
||||
? kDataFrameOverheadBytes
|
||||
: msgType == IR_MSG_BACK
|
||||
? kBackFrameOverheadBytes
|
||||
: (msgType == IR_MSG_BACK_TO || msgType == IR_MSG_REQUEST)
|
||||
? kRequestFrameBytes
|
||||
: msgType == IR_MSG_ACCEPT
|
||||
? kAcceptFrameBytes
|
||||
: 0U;
|
||||
}
|
||||
|
||||
constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
|
||||
{
|
||||
return minimumWireBytes(msgType) != 0U &&
|
||||
wireBytes >= minimumWireBytes(msgType) &&
|
||||
wireBytes <= kMaxWireFrameBytes;
|
||||
}
|
||||
|
||||
/*
|
||||
* TX FSM timing contract.
|
||||
*
|
||||
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
|
||||
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
|
||||
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
|
||||
*/
|
||||
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
|
||||
constexpr uint32_t kPreambleLogicalTicks =
|
||||
static_cast<uint32_t>(preambPulse * 2U) * static_cast<uint32_t>(preambToggle + 1U);
|
||||
constexpr uint32_t kEncodedBitLogicalTicks = static_cast<uint32_t>(bitTakts * 2U);
|
||||
constexpr uint32_t kWireByteLogicalTicks =
|
||||
static_cast<uint32_t>(bitPerByte + syncBits) * kEncodedBitLogicalTicks;
|
||||
|
||||
constexpr uint32_t wireLogicalTicks(uint8_t wireBytes)
|
||||
{
|
||||
return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes
|
||||
? kPreambleLogicalTicks + static_cast<uint32_t>(wireBytes) * kWireByteLogicalTicks
|
||||
: 0U;
|
||||
}
|
||||
|
||||
constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks)
|
||||
{
|
||||
return logicalTicks == 0U
|
||||
? 0U
|
||||
: static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(logicalTicks) * 1000000ULL +
|
||||
static_cast<uint64_t>(kTxLogicalClockHz) - 1ULL) /
|
||||
static_cast<uint64_t>(kTxLogicalClockHz));
|
||||
}
|
||||
|
||||
constexpr uint32_t preambleAirtimeUsCeil()
|
||||
{
|
||||
return logicalTicksToUsCeil(kPreambleLogicalTicks);
|
||||
}
|
||||
|
||||
// Decoder completion is published after the final data bit, while the TX FSM
|
||||
// still emits the last byte's sync bits. Callers that schedule a following
|
||||
// packet from a decoder terminal must include this physical tail.
|
||||
constexpr uint32_t kTrailingByteSyncLogicalTicks =
|
||||
static_cast<uint32_t>(syncBits) * kEncodedBitLogicalTicks;
|
||||
|
||||
constexpr uint32_t trailingByteSyncAirtimeUsCeil()
|
||||
{
|
||||
return logicalTicksToUsCeil(kTrailingByteSyncLogicalTicks);
|
||||
}
|
||||
|
||||
/** Complete nominal on-air duration, rounded up to a whole microsecond. */
|
||||
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
|
||||
{
|
||||
return logicalTicksToUsCeil(wireLogicalTicks(wireBytes));
|
||||
}
|
||||
|
||||
constexpr uint32_t wireAirtimeMsCeil(uint8_t wireBytes)
|
||||
{
|
||||
return wireAirtimeUsCeil(wireBytes) == 0U
|
||||
? 0U
|
||||
: (wireAirtimeUsCeil(wireBytes) + 999U) / 1000U;
|
||||
}
|
||||
|
||||
/* Preserve the deployed library turn-around policy, but expose it by name. */
|
||||
constexpr uint16_t kMandatoryInterPacketQuietMs =
|
||||
static_cast<uint16_t>(
|
||||
static_cast<uint16_t>(
|
||||
(static_cast<uint32_t>(bitTime + tolerance) *
|
||||
static_cast<uint32_t>(bitPerByte + syncBits + 1U)) *
|
||||
2.7735) /
|
||||
1000U);
|
||||
constexpr uint32_t kMandatoryInterPacketQuietUs =
|
||||
static_cast<uint32_t>(kMandatoryInterPacketQuietMs) * 1000U;
|
||||
|
||||
constexpr uint32_t completedFrameTerminalToNextPacketGuardUs(
|
||||
uint16_t requestedQuietMs)
|
||||
{
|
||||
const uint16_t quietMs = requestedQuietMs > kMandatoryInterPacketQuietMs
|
||||
? requestedQuietMs
|
||||
: kMandatoryInterPacketQuietMs;
|
||||
return trailingByteSyncAirtimeUsCeil() +
|
||||
static_cast<uint32_t>(quietMs) * 1000U;
|
||||
}
|
||||
|
||||
constexpr uint32_t completedFrameTerminalToNextPacketGuardMsCeil(
|
||||
uint16_t requestedQuietMs)
|
||||
{
|
||||
return (completedFrameTerminalToNextPacketGuardUs(requestedQuietMs) + 999U) /
|
||||
1000U;
|
||||
}
|
||||
|
||||
constexpr uint16_t kDefaultTimingGuardPermille = 1150U;
|
||||
|
||||
constexpr uint32_t addTimingGuardUs(
|
||||
uint32_t durationUs,
|
||||
uint16_t marginPermille = kDefaultTimingGuardPermille)
|
||||
{
|
||||
return marginPermille == 0U
|
||||
? 0U
|
||||
: static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(durationUs) * marginPermille + 999ULL) / 1000ULL);
|
||||
}
|
||||
|
||||
/** Deadline for seeing enough preamble to know that a response has started. */
|
||||
constexpr uint32_t responseStartGuardUs(
|
||||
uint16_t marginPermille = kDefaultTimingGuardPermille)
|
||||
{
|
||||
return addTimingGuardUs(kMandatoryInterPacketQuietUs + preambleAirtimeUsCeil(),
|
||||
marginPermille);
|
||||
}
|
||||
|
||||
/** Conservative deadline for receiving a complete response of maxWireBytes. */
|
||||
constexpr uint32_t responseFrameGuardUs(
|
||||
uint8_t maxWireBytes,
|
||||
uint16_t marginPermille = kDefaultTimingGuardPermille)
|
||||
{
|
||||
return wireAirtimeUsCeil(maxWireBytes) == 0U
|
||||
? 0U
|
||||
: addTimingGuardUs(kMandatoryInterPacketQuietUs +
|
||||
wireAirtimeUsCeil(maxWireBytes),
|
||||
marginPermille);
|
||||
}
|
||||
|
||||
static_assert(kMaxDataPayloadBytes == 24U, "DATA payload contract changed");
|
||||
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
|
||||
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
|
||||
static_assert(kWireByteLogicalTicks == 814U, "wire-byte timing contract changed");
|
||||
static_assert(kTrailingByteSyncLogicalTicks == 222U, "trailing sync timing changed");
|
||||
static_assert(kMandatoryInterPacketQuietMs == 42U, "inter-packet quiet policy changed");
|
||||
}
|
||||
|
||||
constexpr uint16_t test_all_Time = bitTime;
|
||||
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
||||
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);
|
||||
|
||||
155
IrDmaTxStm32.h
155
IrDmaTxStm32.h
@ -5,6 +5,14 @@
|
||||
|
||||
#if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx)
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) __pragma(message(text))
|
||||
#else
|
||||
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) _Pragma(#text)
|
||||
#endif
|
||||
|
||||
IRPROTO_DMA_PRAGMA_MESSAGE(message("[IR-protocol] TX path available: built-in DMA"))
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HardwareTimer.h>
|
||||
|
||||
@ -77,9 +85,12 @@ public:
|
||||
}
|
||||
|
||||
s_instance = this;
|
||||
activeCount_ = 0;
|
||||
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
// Приоритет ЯВНО и ДО включения: без SetPriority канал остаётся на 0 (уровень
|
||||
// SysTick), длинный refill не вытесняется и глотает тики millis. Обоснование
|
||||
// и измерения — у irproto::kDmaTxIrqPriority (IR_config.h).
|
||||
HAL_NVIC_SetPriority(streams_[i].dmaIrq, irproto::kDmaTxIrqPriority, 0);
|
||||
HAL_NVIC_EnableIRQ(streams_[i].dmaIrq);
|
||||
}
|
||||
|
||||
@ -90,22 +101,29 @@ public:
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
// Fix E (контракт busy): "занят" = АКТИВЕН ХОТЯ БЫ ОДИН поток (OR).
|
||||
// Раньше было AND (все потоки сразу) — почти всегда false, не отражало занятости и было
|
||||
// избыточно (per-stream s.active в startStream + per-encoder isSending уже не дают повторный
|
||||
// старт того же потока). OR нужен, чтобы irAnyTxBusy()/applyCarrierMultiply реально дожидались
|
||||
// простоя ВСЕХ эмиттеров перед ретюном несущей.
|
||||
// ВНИМАНИЕ: это НЕ сериализует независимые front/rear — путь отправки гейтится per-stream;
|
||||
// внешний busy()-гейт в IR_Encoder::rawSend намеренно нейтрализован в прошивке (txBusy()=false).
|
||||
// Чтобы ВКЛЮЧИТЬ сериализацию front/rear — вернуть txBusy()=busy() в Car/src/IR/IR.cpp.
|
||||
bool busy() const {
|
||||
if (streamCount_ == 0) return false;
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
if (!streams_[i].active) return false;
|
||||
if (streams_[i].active) return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
|
||||
if (enc == nullptr) return false;
|
||||
IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
|
||||
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
if (streams_[i].enc == enc) {
|
||||
return startStream(streams_[i], packet, len);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return IR_SendStatus::ExternalNoStream;
|
||||
}
|
||||
|
||||
void irqForStream(size_t streamIndex) {
|
||||
@ -118,6 +136,37 @@ public:
|
||||
return &streams_[streamIndex].hdma;
|
||||
}
|
||||
|
||||
// Fix D: не-IRQ страховка завершения. Вызывать из главного цикла (IR_Module::tick()).
|
||||
// Штатно isSending сбрасывает только DMA HT/CPLT IRQ; если он не сработает (пропуск/слипание
|
||||
// флагов, Transfer-Error, последствие любой гонки) — поток «залипает» навсегда. Здесь ловим
|
||||
// застой прогресса ticksOutput по стенным часам и форсим stopStream как аварийное
|
||||
// восстановление. Это recovery (не prevention): фикс C убирает саму гонку, а poll() страхует
|
||||
// структурный SPOF завершения и делает редкий баг наблюдаемым (recoveryCount()).
|
||||
void poll() {
|
||||
const uint32_t now = millis();
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
TxStream& s = streams_[i];
|
||||
if (!s.active) continue; // atomic: bool
|
||||
const uint32_t t = s.ticksOutput; // atomic: выровненный u32 на Cortex-M
|
||||
if (t != s.lastTicks) {
|
||||
s.lastTicks = t;
|
||||
s.lastProgressMs = now;
|
||||
continue;
|
||||
}
|
||||
if ((uint32_t)(now - s.lastProgressMs) >= stallTimeoutMs_) {
|
||||
forceStop(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Порог застоя для watchdog (мс). По умолчанию 100; должен быть заметно больше времени
|
||||
вывода одной половины DMA-буфера на самой медленной несущей. 0 игнорируется. */
|
||||
void setStallTimeoutMs(uint32_t ms) { if (ms != 0U) stallTimeoutMs_ = ms; }
|
||||
/** Сколько раз watchdog аварийно восстановил залипший поток (наблюдаемость/логи). */
|
||||
uint32_t recoveryCount() const { return recoveries_; }
|
||||
/** Сколько раз DMA-передача завершилась по Transfer-Error (наблюдаемость/логи). */
|
||||
uint32_t errorCount() const { return errors_; }
|
||||
|
||||
private:
|
||||
struct TxStream {
|
||||
DMA_HandleTypeDef hdma{};
|
||||
@ -144,7 +193,11 @@ private:
|
||||
uint32_t totalTicks = 0;
|
||||
volatile uint32_t ticksOutput = 0;
|
||||
|
||||
bool active = false;
|
||||
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
||||
uint32_t lastTicks = 0;
|
||||
uint32_t lastProgressMs = 0;
|
||||
|
||||
volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором
|
||||
|
||||
void resetWave() {
|
||||
wave.configure(setWord, resetWord, nullptr, 0, 2, 1);
|
||||
@ -160,11 +213,13 @@ private:
|
||||
void onHalf() {
|
||||
ticksOutput += halfLen;
|
||||
fill(&dmaBuf[0], halfLen);
|
||||
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
|
||||
}
|
||||
|
||||
void onComplete() {
|
||||
ticksOutput += halfLen;
|
||||
fill(&dmaBuf[halfLen], halfLen);
|
||||
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
|
||||
}
|
||||
|
||||
void onError() {}
|
||||
@ -178,29 +233,39 @@ private:
|
||||
TxStream streams_[MaxStreams]{};
|
||||
uint8_t streamCount_ = 0;
|
||||
|
||||
volatile uint8_t activeCount_ = 0;
|
||||
// Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR.
|
||||
uint32_t stallTimeoutMs_ = 100; // Fix D
|
||||
uint32_t recoveries_ = 0; // Fix D (только контекст потока)
|
||||
volatile uint32_t errors_ = 0; // Fix #5: счётчик Transfer-Error (пишется в ISR dmaErrorCb, читается из потока)
|
||||
|
||||
static uint32_t u32ptr(const volatile void* p) {
|
||||
return (uint32_t)(uintptr_t)p;
|
||||
}
|
||||
|
||||
void startTimerIfNeeded() {
|
||||
// Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не
|
||||
// останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена).
|
||||
// Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей
|
||||
// фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM
|
||||
// генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается.
|
||||
void ensureTimerRunning() {
|
||||
if (htim_ == nullptr) return;
|
||||
if (activeCount_ != 1) return;
|
||||
|
||||
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
|
||||
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
|
||||
__HAL_TIM_SET_COUNTER(htim_, 0);
|
||||
__HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE);
|
||||
HAL_TIM_Base_Start(htim_);
|
||||
if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) {
|
||||
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
|
||||
HAL_TIM_Base_Start(htim_);
|
||||
}
|
||||
}
|
||||
|
||||
void stopTimerIfIdle() {
|
||||
if (htim_ == nullptr) return;
|
||||
if (activeCount_ != 0) return;
|
||||
|
||||
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
|
||||
HAL_TIM_Base_Stop(htim_);
|
||||
// Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ
|
||||
// этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream
|
||||
// (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем.
|
||||
void forceStop(TxStream& s) {
|
||||
HAL_NVIC_DisableIRQ(s.dmaIrq);
|
||||
if (s.active) {
|
||||
stopStream(s);
|
||||
recoveries_++;
|
||||
}
|
||||
HAL_NVIC_EnableIRQ(s.dmaIrq);
|
||||
}
|
||||
|
||||
static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) {
|
||||
@ -233,9 +298,10 @@ private:
|
||||
|
||||
static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
|
||||
auto* s = streamFromDma(hdma);
|
||||
if (s == nullptr) return;
|
||||
if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish
|
||||
s->onError();
|
||||
if (s_instance != nullptr) {
|
||||
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
|
||||
s_instance->stopStream(*s);
|
||||
}
|
||||
}
|
||||
@ -282,28 +348,22 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
|
||||
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return false;
|
||||
if (s.active) return false;
|
||||
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return false;
|
||||
if (s.runs == nullptr || s.maxRuns == 0) return false;
|
||||
IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
|
||||
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
if (s.active) return IR_SendStatus::EncoderBusy;
|
||||
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
|
||||
s.resetWave();
|
||||
|
||||
s.runCount = IR_Encoder::buildGateRuns(packet, len, s.runs, s.maxRuns);
|
||||
if (s.runCount == 0) return false;
|
||||
|
||||
size_t rc = s.runCount;
|
||||
if (!IR_Encoder::scaleGateRunsToPhysical(s.runs, &rc, s.maxRuns, IR_Encoder::carrierMultiply())) {
|
||||
return false;
|
||||
}
|
||||
s.runCount = rc;
|
||||
const uint16_t mult = IR_Encoder::carrierMultiply();
|
||||
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
|
||||
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
|
||||
|
||||
uint32_t total = 0;
|
||||
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
|
||||
s.totalTicks = total;
|
||||
|
||||
const uint16_t mult = IR_Encoder::carrierMultiply();
|
||||
uint16_t pwr = mult / 2U;
|
||||
if (s.enc != nullptr) {
|
||||
const uint16_t want = s.enc->powerNumerator();
|
||||
@ -317,15 +377,22 @@ private:
|
||||
|
||||
s.port->BSRR = s.resetWord;
|
||||
|
||||
// Fix C/D: помечаем active и инициализируем watchdog ДО арминга DMA — иначе ранний
|
||||
// HT/CPLT IRQ при active==false выйдет из callback'а и пропустит refill → недосчёт
|
||||
// ticksOutput → латч. TIM уже (или станет) free-running, старт по счётчику не нужен.
|
||||
s.lastTicks = 0;
|
||||
s.lastProgressMs = millis();
|
||||
s.active = true;
|
||||
|
||||
__DSB(); // Fix #8: завершить запись dmaBuf (fill) и BSRR до арминга DMA
|
||||
const uint32_t dst = u32ptr(&s.port->BSRR);
|
||||
if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
|
||||
return false;
|
||||
s.active = false;
|
||||
return IR_SendStatus::DmaStartFailed;
|
||||
}
|
||||
|
||||
s.active = true;
|
||||
activeCount_++;
|
||||
startTimerIfNeeded();
|
||||
return true;
|
||||
ensureTimerRunning();
|
||||
return IR_SendStatus::Success;
|
||||
}
|
||||
|
||||
void stopStream(TxStream& s) {
|
||||
@ -341,9 +408,7 @@ private:
|
||||
if (s.enc != nullptr) {
|
||||
s.enc->externalFinishSend();
|
||||
}
|
||||
|
||||
if (activeCount_ > 0) activeCount_--;
|
||||
stopTimerIfIdle();
|
||||
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
|
||||
/**
|
||||
* Один RLE-сегмент огибающей несущей.
|
||||
* В buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec (как раньше).
|
||||
* После IR_Encoder::scaleGateRunsToPhysical — в физических тиках carrierFrec×multiply.
|
||||
* В legacy buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec.
|
||||
* В современном DMA/buffered ISR пути buildPhysicalGateRuns строит lenTicks сразу в физических тиках carrierFrec×multiply.
|
||||
*/
|
||||
struct IrTxGateRun {
|
||||
uint16_t lenTicks;
|
||||
|
||||
64
IrTxIsrBufferedStorage.h
Normal file
64
IrTxIsrBufferedStorage.h
Normal file
@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "IR_config.h"
|
||||
#include "IrTxBsrrWave.h"
|
||||
|
||||
class IrTxIsrBufferedStorageBase {
|
||||
public:
|
||||
IrTxGateRun* gateRuns = nullptr;
|
||||
size_t maxGateRuns = 0;
|
||||
uint32_t* bsrrWords = nullptr;
|
||||
uint16_t wordCount = 0;
|
||||
|
||||
IrTxBsrrWave wave{};
|
||||
uint16_t readIdx = 0;
|
||||
uint16_t halfLen = 0;
|
||||
uint32_t totalTicks = 0;
|
||||
uint32_t ticksSent = 0;
|
||||
|
||||
bool isValid() const {
|
||||
return gateRuns != nullptr &&
|
||||
maxGateRuns != 0U &&
|
||||
bsrrWords != nullptr &&
|
||||
wordCount >= 2U &&
|
||||
(wordCount & 1U) == 0U;
|
||||
}
|
||||
|
||||
void resetRuntimeState() {
|
||||
readIdx = 0;
|
||||
halfLen = static_cast<uint16_t>(wordCount / 2U);
|
||||
totalTicks = 0;
|
||||
ticksSent = 0;
|
||||
}
|
||||
};
|
||||
|
||||
class IrTxIsrBufferedStorageView : public IrTxIsrBufferedStorageBase {
|
||||
public:
|
||||
IrTxIsrBufferedStorageView(IrTxGateRun* runs, size_t runCount, uint32_t* words, uint16_t wordsCount) {
|
||||
gateRuns = runs;
|
||||
maxGateRuns = runCount;
|
||||
bsrrWords = words;
|
||||
wordCount = wordsCount;
|
||||
resetRuntimeState();
|
||||
}
|
||||
};
|
||||
|
||||
template<size_t MaxGateRuns = irproto::kIsrTxMaxGateRuns, uint16_t WordCount = irproto::kIsrTxBsrrWordCount>
|
||||
class IrTxIsrBufferedStorage : public IrTxIsrBufferedStorageBase {
|
||||
static_assert(MaxGateRuns > 0U, "IrTxIsrBufferedStorage: MaxGateRuns > 0");
|
||||
static_assert(WordCount >= 2U, "IrTxIsrBufferedStorage: WordCount >= 2");
|
||||
static_assert((WordCount & 1U) == 0U, "IrTxIsrBufferedStorage: WordCount must be even");
|
||||
|
||||
public:
|
||||
IrTxIsrBufferedStorage() {
|
||||
gateRuns = gateRunsStorage_;
|
||||
maxGateRuns = MaxGateRuns;
|
||||
bsrrWords = bsrrWordsStorage_;
|
||||
wordCount = WordCount;
|
||||
resetRuntimeState();
|
||||
}
|
||||
|
||||
private:
|
||||
IrTxGateRun gateRunsStorage_[MaxGateRuns]{};
|
||||
uint32_t bsrrWordsStorage_[WordCount]{};
|
||||
};
|
||||
@ -2,12 +2,49 @@
|
||||
|
||||
namespace PacketTypes
|
||||
{
|
||||
bool BasePack::checkAddress() { return true; };
|
||||
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
|
||||
uint8_t minimumPacketSize(uint8_t msgType)
|
||||
{
|
||||
switch (msgType)
|
||||
{
|
||||
case IR_MSG_DATA_ACCEPT:
|
||||
case IR_MSG_DATA_NOACCEPT:
|
||||
case IR_MSG_BACK_TO:
|
||||
case IR_MSG_REQUEST:
|
||||
return uint8_t(msgBytes + addrBytes + addrBytes + crcBytes);
|
||||
case IR_MSG_BACK:
|
||||
return uint8_t(msgBytes + addrBytes + crcBytes);
|
||||
case IR_MSG_ACCEPT:
|
||||
return uint8_t(msgBytes + addrBytes + 1U + crcBytes);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize)
|
||||
{
|
||||
const uint8_t minimum = minimumPacketSize(msgType);
|
||||
return minimum != 0 && packSize >= minimum;
|
||||
}
|
||||
|
||||
bool BasePack::checkAddress() { return true; }
|
||||
bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize)
|
||||
{
|
||||
isAvailable = false;
|
||||
isRawAvailable = false;
|
||||
this->packInfo = packInfo;
|
||||
this->id = id;
|
||||
|
||||
if (packInfo == nullptr || packInfo->buffer == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
|
||||
if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkAddress())
|
||||
{
|
||||
isAvailable = true;
|
||||
@ -23,29 +60,65 @@ namespace PacketTypes
|
||||
Serial.print(" NOT-OK ");
|
||||
#endif
|
||||
}
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
uint16_t BasePack::_getAddrFrom(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes ||
|
||||
uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
|
||||
};
|
||||
}
|
||||
uint16_t BasePack::_getAddrTo(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes ||
|
||||
uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
|
||||
};
|
||||
}
|
||||
|
||||
uint8_t BasePack::_getDataSize(BasePack *obj)
|
||||
{
|
||||
return obj->packInfo->packSize - crcBytes - obj->DataOffset;
|
||||
};
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset);
|
||||
if (uint16_t(obj->packInfo->packSize) <= frameOverhead)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead);
|
||||
}
|
||||
uint8_t *BasePack::_getDataPrt(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes);
|
||||
if (uint16_t(obj->DataOffset) > dataEnd)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return obj->packInfo->buffer + obj->DataOffset;
|
||||
};
|
||||
}
|
||||
uint8_t BasePack::_getDataRawSize(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return obj->packInfo->packSize;
|
||||
};
|
||||
}
|
||||
|
||||
bool BasePack::available()
|
||||
{
|
||||
@ -59,7 +132,7 @@ namespace PacketTypes
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
bool BasePack::availableRaw()
|
||||
{
|
||||
if (isRawAvailable)
|
||||
@ -71,7 +144,7 @@ namespace PacketTypes
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bool Data::checkAddress()
|
||||
{
|
||||
@ -104,4 +177,4 @@ namespace PacketTypes
|
||||
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,25 +4,34 @@
|
||||
class IR_Decoder;
|
||||
namespace PacketTypes
|
||||
{
|
||||
/**
|
||||
* Minimum complete frame size (header, addresses/data required by the type,
|
||||
* and CRC). Unknown/reserved message types return 0.
|
||||
*/
|
||||
uint8_t minimumPacketSize(uint8_t msgType);
|
||||
|
||||
/** True only for a known typed packet whose complete frame is long enough. */
|
||||
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize);
|
||||
|
||||
class BasePack
|
||||
{
|
||||
friend IR_Decoder;
|
||||
|
||||
protected:
|
||||
bool isAvailable;
|
||||
bool isRawAvailable;
|
||||
bool isNeedAccept;
|
||||
bool isAvailable = false;
|
||||
bool isRawAvailable = false;
|
||||
bool isNeedAccept = false;
|
||||
|
||||
uint8_t msgOffset;
|
||||
uint8_t addressFromOffset;
|
||||
uint8_t addressToOffset;
|
||||
uint8_t DataOffset;
|
||||
uint8_t msgOffset = 0;
|
||||
uint8_t addressFromOffset = 0;
|
||||
uint8_t addressToOffset = 0;
|
||||
uint8_t DataOffset = 0;
|
||||
|
||||
IR_FOX::PackInfo *packInfo;
|
||||
uint16_t id;
|
||||
IR_FOX::PackInfo *packInfo = nullptr;
|
||||
uint16_t id = 0;
|
||||
|
||||
virtual bool checkAddress();
|
||||
void set(IR_FOX::PackInfo *packInfo, uint16_t id);
|
||||
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
|
||||
|
||||
static uint16_t _getAddrFrom(BasePack *obj);
|
||||
static uint16_t _getAddrTo(BasePack *obj);
|
||||
|
||||
15
RingBuffer.h
15
RingBuffer.h
@ -36,6 +36,21 @@ public:
|
||||
return value;
|
||||
}
|
||||
|
||||
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
|
||||
// (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю
|
||||
// защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.)
|
||||
bool pop(T &out) {
|
||||
bool popped = false;
|
||||
noInterrupts();
|
||||
if (!isEmpty()) {
|
||||
out = data[start];
|
||||
start = (start + 1) % BufferSize;
|
||||
popped = true;
|
||||
}
|
||||
interrupts();
|
||||
return popped;
|
||||
}
|
||||
|
||||
private:
|
||||
T data[BufferSize];
|
||||
unsigned int start, end;
|
||||
|
||||
142
docs_analysis/IR_protocol_analysis.md
Normal file
142
docs_analysis/IR_protocol_analysis.md
Normal file
@ -0,0 +1,142 @@
|
||||
# IR-protocol — глубокий анализ (RX-производительность, обработка ошибок, протокол, мины)
|
||||
|
||||
> Дата: 2026-07-01. Только чтение исходников. Цель: после переноса TX на DMA приём (RX) стал узким местом — разобрать RX-конвейер, обработку ошибок сигнала, дизайн протокола и найти баги/мины. Тайминги: `carrierFrec=38000`, `bitTakts=37` → `carrierPeriod=26µs`, `bitTime≈962µs` (комментарий про 1100 в `IR_DecoderRaw.h:107` устарел), `riseTime=962`, окно ±300µs, `IR_timeout≈15144µs`, `IR_ResponseDelay≈42мс`. Числа длительностей — оценки (замеров в коде нет).
|
||||
|
||||
## 0. Резюме (главное)
|
||||
1. **Найдены реальные баги, триггеримые ШУМОМ (не только валидным трафиком):**
|
||||
- **CRITICAL — OOB-чтение** в `crcCheck` при `packSize==1` (`IR_DecoderRaw.cpp:887`): `packSize - crcBytes` = `1-2` → `uint8_t len=255` → `crc8` читает `dataBuffer[0..254]` при массиве 38 → OOB ~217 байт + `dataBuffer[255/256]`. Мусорный CRC (редко — ложно-валидный кадр) или HardFault у границы SRAM. Нижняя граница `packSize` нигде не проверяется.
|
||||
- **HIGH — реальный крэш на Car: null-deref** `encoder->sendAccept()` без проверки (`IR_Decoder.cpp:169`). У Car декодер создан с `encoder==nullptr` (`Car/src/IR/IR.cpp:31`). Приходит валидный кадр `IR_MSG_DATA_ACCEPT`, адресованный машинке (addrTo==id, addrFrom≠0 и <broadcast) → через `acceptDelay` → `sendAccept` по nullptr → HardFault. Детерминированно, если кто-то шлёт машинке DATA_ACCEPT; плюс шумом (1/65536 на ложный кадр нужного типа).
|
||||
- **HIGH — OOB-запись** `dataBuffer[38]` (off-by-one) при `packSize==0` (`IR_DecoderRaw.cpp:753` — `>` вместо `>=`): при `packSize==0` конец кадра не наступает, `i_dataBuffer` растёт до 304, запись `dataBuffer[304/8=38]` затирает соседнее поле `prevRise`.
|
||||
- **MEDIUM — неинициализированные** `isWaitingAcceptSend`/`acceptSendTimer`/… (`IR_Decoder.cpp:72-79`): мусор при старте → возможен спонтанный `sendAccept` (и крэш по null-deref) без приёма чего-либо.
|
||||
2. **RX-bottleneck — НЕ ISR и НЕ тяжёлый декод в прерывании** (декода в ISR нет, ISR лёгкий ~2-4µs). Узкое место структурное: **`tick()` вынимает РОВНО один сырой фронт за вызов** → скорость слива кольца привязана к частоте `loop()`; при медленном loop 250-элементный буфер (~120мс запаса) переполняется и рвёт кадры. Плюс битовый CRC ×2 на конец кадра и маленький hold-фильтр.
|
||||
3. **Коррекции ошибок по сути нет** — detect-and-drop: 2×CRC8 (~16 бит), FEC/повтор отсутствует, однобитный bruteforce выключен («зависает»). Нет seq/дедупа/ретраев; ACK задуман, но отправитель его игнорирует (мёртвый механизм). 5-битная длина молча обрезает payload >24 байт (send возвращает Success, кадр теряется).
|
||||
|
||||
---
|
||||
|
||||
## 1. RX-конвейер и производительность
|
||||
|
||||
### Стадии (кто где исполняется)
|
||||
- **A. EXTI-фронт → ISR** (`IR_DecoderRaw::isr`, `.cpp:274-308`; `attachInterrupt(pin, CHANGE)` в `IR_Decoder::enable :88-89`). Приёмник демодулирующий (TSOP), EXTI по фронтам огибающей. В ISR: `micros()` (в `noInterrupts/interrupts`), `dir = port->IDR&mask` (пост-фактум чтение уровня — при коротком импульсе возможна неверная dir), mute-гейт (`isPairSending` → фронт только в edgeTrace и return), `subBuffer.push(edge)`.
|
||||
- **B. Сырое кольцо** `RingBuffer<FrontStorage,250>` (`.h:124`, `subBufferSize=250`, `IR_config.h:169`), ~8 байт/фронт ≈ 2КБ/декодер. `push/pop` под своей критсекцией. **`pop()` — один элемент за вызов.**
|
||||
- **C. Фильтр импульсов** (`tick :417-444`) — по умолчанию **выключен** (`IR_INPUT_MIN_PULSE_US=0`), сырой фронт идёт прямо в декод. Включённый: hold-массив `[6]`, отсев пар <minUs как глитч.
|
||||
- **D. FSM преамбулы** (`preambleProcessEdge :1599-1723`): Idle→Candidate→Locked, нужно 2 согласованных периода rise→rise в окне 220-340% bitTime после «тишины» >`IR_timeout*2`.
|
||||
- **E. Декод бита** (`processDecodedFront :466-746`): инлайн-отсев глитчей, окна таймингов, реконструкция пропущенных бит через `ceil_div`.
|
||||
- **F. Сборка/sync/CRC** (`writeToBuffer :748-949`): биты в `dataBuffer[38]`, sync 3 бита/байт, длина из первого байта, на конце — `crcCheck`.
|
||||
- **G. Диспетчеризация** (`IR_Decoder::_tick :130-172`): разбор msgType → gotData/gotBack/gotAccept/gotRequest/gotRaw; авто-ACK для DATA_ACCEPT.
|
||||
|
||||
**Весь декод (C–G) — в `tick()`/loop, не в ISR.** Это осознанная разгрузка прерывания.
|
||||
|
||||
### Узкое место
|
||||
- ISR лёгкий (~2-4µs, оценка), даже при 5-6к фронтов/с ~1.5-2% CPU. Не он предел.
|
||||
- **Структурный предел: 1 сырой pop/tick** (`.cpp:409-415`) → RX-throughput = частота `loop()`. Полный кадр ≈ 836 фронтов ≈ 836 tick'ов. Медленный loop (Serial-отладка, моторы, дисплей) < частоты фронтов → кольцо копится → overflow → рваные кадры → CRC/timeout reject.
|
||||
- Буфер 250 = ~120мс запаса (250/2080 фр/с). Хватает от коротких стопоров, не от устойчивого отставания.
|
||||
- Финальный CRC — битовый `crc8` дважды по всей длине (~5-10µs, оценка). `BRUTEFORCE_CHECK` (выкл, «зависает») при включении — O(len²·64), отдельная мина.
|
||||
|
||||
### Переполнение/backpressure
|
||||
- `subBuffer` полон → `push`=false, `isSubBufferOverflow=true` (транзиентный, сбрасывается в tick), счётчик `rxBriefRawOverflowDrops` (u16, насыщение), лог `QRAW`.
|
||||
- hold-фильтр(6) переполнение → `pulseFilterDropHoldOverflow++` (стойкий), лог `HOLD`.
|
||||
- Битовый буфер кадра → `isBufferOverflow`, лог `BUF`, сброс.
|
||||
- `QFLT`/`pulseFilterDroppedByFilteredOverflow()` — **мёртвый код**, всегда 0.
|
||||
- **Стойкого счётчика потерянных ПАКЕТОВ нет** — потери видны косвенно (CRC/TIMEOUT/SYNC в RX-brief-логе), последний битый кадр — в `rejectBuffer`.
|
||||
|
||||
### RX во время своего TX (mute)
|
||||
- `isPairSending` (число busy-энкодеров, пересчёт `refreshPairMuteState` под критсекцией) → в ISR фронт отбрасывается на входе (`:293-299`). Глушится **любой** чужой кадр на всё время своей передачи (`isSending`). Для DMA-TX снятие mute зависит от `externalFinishSend()` — задержка удлиняет «глухоту» на хвост.
|
||||
- NVIC: библиотека приоритеты почти не трогает; RX-EXTI — дефолт ядра, поднять `setReceiveExtiPreemptPriority` (`IR_Decoder.cpp:44-62`). **Нужно: preempt RX-EXTI срочнее DMA-TX**, иначе чужой DMA/IRQ сдвигает метку фронта → искажение `risePeriod` (окно всего ±300µs).
|
||||
|
||||
### Идеи ускорения RX (trade-off)
|
||||
1. **Таймер input-capture + DMA вместо софт-EXTI** — аппаратный штамп фронтов, убирает per-edge ISR и джиттер метки. Дорого (канал таймера, wrap, восстановление dir), но чистейшие тайминги.
|
||||
2. **Батч-pop сырых фронтов за tick** (с лимитом на итерацию) — снимает привязку к частоте loop. Главный дешёвый выигрыш.
|
||||
3. **subBuffer 250→512/1024** — больше запаса (RAM: 1024≈8КБ).
|
||||
4. **Табличный CRC** вместо битового ×2 (~8× быстрее, +~512Б flash).
|
||||
5. **Поднять preempt RX-EXTI выше DMA-TX** (механизм есть).
|
||||
6. **Дешёвая метка** DWT->CYCCNT вместо `micros()`+критсекции в ISR.
|
||||
|
||||
---
|
||||
|
||||
## 2. Обработка ошибок сигнала и протокол
|
||||
|
||||
### CRC — это НЕ CRC16-CCITT, а два сцепленных CRC8
|
||||
`crc8()` (`IR_config.cpp:17-33`) битовый MSB-first, init=0xFF. Полиномы `poly1=0x31`, `poly2=0x8C` (=битовое зеркало 0x31, но алгоритм тот же → фактически два разных CRC8). Сборка (`crcCheck :951-970`): `crc = crc8(0..len,poly1)<<8 | crc8(0..len+1,poly2)`. Асимметрия: байт2 покрывает данные+байт1, байт1 — только данные. Суммарно ~16 бит, вероятность пропуска ~1/65536, но гарантированной хэмминг-дистанции реального CRC16 нет. Мелочь `:959`: `dataBuffer[len] == (crc>>8) & 0xFF` парсится как `(==)&0xFF` — работает случайно (хрупко при смене `crc_t`).
|
||||
|
||||
### Фильтры/устойчивость
|
||||
- Pulse-filter по длительности — **по умолчанию выкл** (`IR_INPUT_MIN_PULSE_US=0`).
|
||||
- Инлайн-отсев глитчей в декодере — вкл: `IR_SHORT_LOW_GLITCH_REJECT`, `IR_MICRO_GAP_RISE_REJECT` (`:484-516`), «подтяжка фазы».
|
||||
- Преамбула устойчива к случайному шуму (2 согласованных периода в узком окне). Sync 3 бита/байт — ловит расстройку сетки (до 3 ошибок → reject), но это не коррекция.
|
||||
|
||||
### Коррекции нет
|
||||
- FEC отсутствует. Есть эвристическая реконструкция бит по времени (`:636-741`) — угадывание, финально валидируется CRC.
|
||||
- `BRUTEFORCE_CHECK` (однобитная коррекция перебором) — **выключен, `//TODO: зависает`** (`IR_config.h:162`). При 16-битном контроле ещё и рискует ложной коррекцией.
|
||||
- `rejectBuffer` — не коррекция, а «detect-and-expose» битого кадра приложению.
|
||||
|
||||
### Подтверждения (Accept) — механизм есть, но мёртвый
|
||||
Приёмник на `IR_MSG_DATA_ACCEPT` планирует `sendAccept(from, crc8(данные))` через `acceptDelay` (`IR_Decoder::_tick :157-171`). **Отправитель Accept игнорирует** — логика ожидания/ретрая закомментирована (`IR_Encoder.cpp:736-741`, `rawSend:906 «TODO»`). Ретраев/дедупа/seq нет. Гарантий доставки нет.
|
||||
|
||||
### Форматы кадров
|
||||
Первый байт `(msgType<<5)|(packSize&0x1F)`, `packSize` — полный размер кадра, длина 5 бит (0..31). Адреса big-endian.
|
||||
|
||||
| Тип | Класс | Поля | data offset | CRC |
|
||||
|---|---|---|---|---|
|
||||
| BACK 0 | DataBack | msg, addrFrom(1-2) | 3 | 2 |
|
||||
| ACCEPT 1 | Accept | msg, addrFrom, customByte(3) | — | 2 |
|
||||
| REQUEST 2 | Request | msg, addrFrom, addrTo | — | 2 |
|
||||
| BACK_TO 4 | DataBack | msg, addrFrom, addrTo | 5 | 2 |
|
||||
| DATA_NOACCEPT 6 | Data | msg, addrFrom, addrTo | 5 | 2 |
|
||||
| DATA_ACCEPT 7 | Data (нужен Accept) | как 6 | 5 | 2 |
|
||||
|
||||
Адресация (`IR_config`): Broadcast=65000+; id==0 = promiscuous (принимает всё); машинки 1..31999, КТ 32000..63999, пульты 64000..64999.
|
||||
|
||||
### Что теряется/портится молча
|
||||
1. **Переполнение 5-битной длины (серьёзно):** накладные Data = 7 байт → payload ≤24, но send разрешает `len` до `bytePerPack=31` (`IR_Encoder.cpp:691`). При len 25..31 `packSize` пишется как `&0x1F` (оборот) → декодер читает короткий кадр → CRC не сойдётся → пакет теряется, **а send вернул Success**. (Комментарий `config:96` сам предупреждает про «31 vs 24».)
|
||||
2. **Нет seq** → дубликаты/пропуски незаметны (любой будущий ретрай = повтор действия).
|
||||
3. **Коллизии не детектируются** (half-duplex без CSMA/CD; `isPairSending` глушит только свой RX). TODO «отложить TX после приёма» (`IR_Encoder.cpp:5`) не реализован.
|
||||
4. Тайминговая реконструкция может собрать «правдоподобный» буфер, редко проходящий CRC на неверных данных (~1/65536).
|
||||
5. Accept-customByte — 1 байт CRC8 (1/256 ложного подтверждения), но т.к. игнорируется — без эффекта.
|
||||
|
||||
---
|
||||
|
||||
## 3. Мины/баги (severity, подтверждено по коду)
|
||||
|
||||
| # | Severity | Файл:line | Суть | Триггер |
|
||||
|---|----------|-----------|------|---------|
|
||||
| B1 | **CRITICAL** | `IR_DecoderRaw.cpp:887,951-970` | `crcCheck(packSize-crcBytes)` при `packSize==1` → len=255 → OOB-чтение `dataBuffer[0..256]` (массив 38) | шум: первый байт с младш. 5 бит =1 (0x01,0x21,…) после лока преамбулы |
|
||||
| B2 | **HIGH** | `IR_Decoder.cpp:167-169` | `encoder->sendAccept()` без null-check; у Car `encoder==nullptr` (`IR.cpp:31`) → HardFault | валидный DATA_ACCEPT машинке (детерминированно) или шум (1/65536) |
|
||||
| B3 | **HIGH** | `IR_DecoderRaw.cpp:753` | off-by-one `>` вместо `>=` → OOB-запись `dataBuffer[38]` (затирает `prevRise`) при `packSize==0` | шум: первый байт с младш. 5 бит =0, ~38 «дата»-байт подряд |
|
||||
| B4 | **MEDIUM** | `IR_Decoder.cpp:72-79` (конструктор) | `isWaitingAcceptSend`/`acceptSendTimer`/`addrAcceptSendTo`/`acceptCustomByte` не инициализированы → спонтанный `sendAccept` (+ B2 крэш) | мусор в RAM при старте |
|
||||
| B5 | **MEDIUM** | `RingBuffer.h:28-37` + `IR_DecoderRaw.cpp:408-415` | вложенные критсекции не вкладываются (`interrupts()` в `pop()` снимает внешний `noInterrupts()` из `tick()`) → торн-рид `*rawPtr`, слот уже свободен для ISR | заполненное кольцо в узком окне |
|
||||
| B6 | MEDIUM | `IR_Decoder.cpp:112-115`, `IR_DecoderRaw.cpp:280-282` | джиттер метки фронта: `std::function` indirect-call + лишний `noInterrupts/interrupts` вокруг `micros()` в ISR → разброс `risePeriod` | нагрузка/параллельный DMA |
|
||||
| B7 | LOW | `IR_DecoderRaw.cpp:959` | приоритет операторов в сравнении CRC — работает случайно | смена `crc_t` |
|
||||
| B8 | LOW | `IR_DecoderRaw.cpp:448` | `isSubBufferOverflow=false` без критсекции параллельно ISR `=true` → потеря флага (диагностика) | — |
|
||||
| B9 | LOW | `IR_DecoderRaw.cpp:1612` | sentinel `prevRise==0` при `micros()==0` (раз в ~71.6 мин) → разовый глитч преамбулы | редко |
|
||||
| B10 | LOW (спит) | `IR_DecoderRaw.cpp:980-986`, `:1676` | `ceil_div` /`riseTime`; при `freeFrec==true` `riseSyncTime` может уйти <300 → `riseTimeMin` overflow / деление на 0 | только при `freeFrec` (по умолч. выкл) |
|
||||
|
||||
**Опровергнуто (REFUTED):** wrap `micros()` в таймаутах (wrap-safe, `(uint32_t)(a-b)`); переполнение subBuffer за границы (push проверяет isFull); `packSize>dataByteSizeMax` (5 бит → max 31 < 38, опасна только НИЖНЯЯ граница); overflow `highCount/lowCount` (гейт по IR_timeout); аллокации в ISR (bind SBO на attachInterrupt, в ISR только indirect-call); реентерабельность одной EXTI (M4 не вытесняет сам себя).
|
||||
|
||||
**Требует железной проверки:** джиттер метки фронта под параллельным DMA-TX; NVIC preempt RX>TX; воспроизведение B1/B3 (подать первый байт 0x01 / 0x00 под отладчиком); RAM-бюджет subBuffer (2КБ/декодер) на F4-пульте с несколькими декодерами.
|
||||
|
||||
---
|
||||
|
||||
## 4. Приоритетный список доработок
|
||||
|
||||
### Сначала — баги (кандидаты на фикс, все в общей библиотеке → blast radius на все проекты)
|
||||
1. **B1 CRITICAL:** валидировать `packSize >= crcBytes` (и разумную нижнюю границу) ДО `crcCheck`; отбрасывать кадр иначе. Однострочная защита от OOB.
|
||||
2. **B2 HIGH:** `if (encoder) encoder->sendAccept(...)` (null-check). Спасает Car от HardFault.
|
||||
3. **B3 HIGH:** `>=` вместо `>` в границе `writeToBuffer:753` (+ обрабатывать `packSize==0` как невалидный).
|
||||
4. **B4 MEDIUM:** инициализировать accept-поля в конструкторе (`isWaitingAcceptSend=false` и т.д.).
|
||||
5. **B5 MEDIUM:** `pop()` отдавать по значению (out-параметр) под одной критсекцией, не указателем во внутренний слот.
|
||||
|
||||
### Затем — производительность RX (эффект/сложность)
|
||||
- Батч-pop за tick (высокий эффект / низкая сложность) → снять привязку к частоте loop.
|
||||
- Табличный CRC; больше subBuffer; preempt RX>TX; (крупно) input-capture+DMA.
|
||||
|
||||
### Протокол (эффект/сложность)
|
||||
1. **Валидация полной длины в send** (высокий/низкий): проверять `packSize<=31` (Data ≤24, BACK ≤26), возвращать `PayloadTooLarge` — убирает тихую потерю.
|
||||
2. **Seq + дедуп** (высокий/средний): 1 байт seq, дедуп по (addrFrom, seq). Предпосылка для любого ретрая.
|
||||
3. **Реальный ACK+ретрай на отправителе** (высокий/средне-высокий): механизм генерации Accept уже есть, замкнуть петлю (нужен seq).
|
||||
4. **Listen-before-talk** (средний/низкий): отложить TX пока `isReciving()` — TODO уже помечен.
|
||||
5. **Заголовочный CRC/дублирование длины** (средний/низкий): битая длина не должна ломать границу кадра.
|
||||
6. Кадровый повтор как лёгкий FEC (в паре с seq); CRC16-CCITT вместо 2×CRC8 (маргинально, ломает совместимость); фрагментация >31 байт с ACK (если реально нужны большие payload); починить/включить bruteforce (сомнительно).
|
||||
|
||||
---
|
||||
|
||||
## Ключевые файлы
|
||||
`IR-protocol/`: `IR_DecoderRaw.{h,cpp}` (ISR, декод, CRC, reject, преамбула, mute), `IR_Decoder.{h,cpp}` (диспетчеризация, Accept-петля, EXTI/NVIC), `IR_Encoder.{h,cpp}` (сборка кадра, CRC, send, isSending), `PacketTypes.{h,cpp}` (форматы, адресация), `IR_config.{h,cpp}` (тайминги, framing, crc8), `RingBuffer.h`, `IrTxIsrBufferedStorage.h`.
|
||||
72
docs_analysis/Signal_physics_analysis.md
Normal file
72
docs_analysis/Signal_physics_analysis.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Анализ физики сигнала: TX-форма против RX-коррекций
|
||||
|
||||
## 1. Резюме
|
||||
|
||||
Система в основном **не борется сама с собой**: и TX, и RX выведены из одних констант `IR_config.h`, форма на передаче при `multiply=2` (дефолт, чётный) — **бит-точная, без округлений**, а подавляющая часть RX-эвристик ловит деформации, которые реально вносит ИК-канал через демодулятор (TSOP-АРУ, отражения, фон, расхождение кварцев). Различение 0/1 идёт по скважности (`highTime>lowTime`, IR_DecoderRaw.cpp:620-633) внутри периода и **не зависит** от широкого окна периода — это архитектурно здоровое решение.
|
||||
|
||||
Но есть **три очага самопорождённых ошибок**, которые RX молча компенсирует:
|
||||
|
||||
1. **Главный (чистый баг, не канал):** RX строит всю временную базу на усечённой константе `carrierPeriod = 1e6/38000 = 26` (bitTime=962 µs), тогда как TX физически излучает на реальной частоте таймера — `26.316 µs`, bitTime≈973.7 µs. Систематический **односторонний сдвиг ~11.7 µs/бит (1.2%)**, который целиком съедается допуском `tolerance=300`. Это TX/модель кривит — RX терпит.
|
||||
2. **Конфиг-зависимый:** ISR-путь вносит программный джиттер фронтов (латентность/вытеснение, IR_Encoder.cpp:1037-1042), которого нет у DMA-пути. Часть glitch/reconstruction-логики RX страхует именно этот самопорождённый джиттер.
|
||||
3. **Спровоцированный дизайном модуляции:** протокол использует **длинные непрерывные MARK** (преамбула 49 периодов ≈1290 µs, бит '1' = 25 периодов ≈658 µs). Длинные непрерывные burst'ы — классический триггер АРУ TSOP-демодулятора (просадка огибающей в середине долгого mark) → «рваная единица», которую чинит реконструкция E/G. Деформацию вносит канал (TSOP), но провоцирует её выбор формы на TX.
|
||||
|
||||
Остальное — стартовая фаза (≤26 µs), дрейф периода, дребезг фронтов — это то, что **обязан** обрабатывать любой ИК-приёмник; здесь самолечения нет.
|
||||
|
||||
## 2. Карта механизмов RX
|
||||
|
||||
| Механизм RX (file:line) | Какую деформацию ловит/чинит | Детект/Коррекция | Источник деформации | Вердикт |
|
||||
|---|---|---|---|---|
|
||||
| **A.** pulse-filter, минимум ширины (DecoderRaw 1493-1555) | иголка/дребезг компаратора, пара «↑↓» короче минимума | детект | реальный канал (front-end, дребезг) | оправданно (но дефолт `MIN_PULSE_US=0` — выключен) |
|
||||
| **B.** short-LOW glitch reject + phase nudge (484-499, 8-16) | короткий ложный ↑ после микро-провала огибающей | детект + коррекция фазы | реальный канал (TSOP-АРУ, просадка) | оправданно |
|
||||
| **C.** micro-gap rise reject + nudge (500-516) | распад фронта на два близких | детект + коррекция фазы | реальный канал; **частично** ISR-джиттер | в основном оправданно |
|
||||
| **D.** отсев короткого стартового периода / спада (517-524, 566-573) | сжатый ↑→↑ без контекста, дребезг спада | детект | реальный канал (шум/дребезг) | оправданно |
|
||||
| **E.** сбор рваной единицы (526-557) | длинный HIGH-бит, распавшийся из-за пропадания несущей | подготовка к коррекции | канал (TSOP-АРУ), **спровоцировано длинными MARK на TX** | оправданно, но триггер — дизайн формы |
|
||||
| **F.** окно валидности периода (h:24-27; 583-592) | грубо растянутый/сжатый период | детект/брак | реальный канал (потеря кадра) | оправданно |
|
||||
| **G.** реконструкция ceil_div, invertErr, «лишний ноль/единица» (617-741, 980-986) | пропущенные ↑-фронты, N слипшихся бит, дроблёный/короткий бит | **коррекция (ядро)** | канал (дропаут несущей); косвенно ISR-джиттер | оправданно под канал; часть работы — следствие TX-дизайна |
|
||||
| **H.** восстановление сетки по sync (804-853) | сдвиг сетки data↔sync из-за лишнего/недостающего бита | детект срыва + локальная коррекция на 1 бит | канал (потеря бита); маркер `{!L,L,!M}` от TX детерминирован | оправданно (детект реальной потери) |
|
||||
| **I.** преамбула Idle→Candidate→Locked, EMA периода, якорь фазы, freeFrec (1557-1723) | дрейф периода, разброс кварцев, старт-фаза, чужая частота | коррекция базы/фазы | **смешанный:** реальный (кварцы/дрейф) + самопорождённый (старт-фаза TIM ≤26 µs, сдвиг 1.2% из carrierPeriod=26) | необходимо всегда; но частично адаптируется к TX-багу |
|
||||
|
||||
## 3. Где Даша сама порождает ошибку формы и потом её чинит
|
||||
|
||||
**(а) Усечённый `carrierPeriod=26` — самое чистое «самолечение».**
|
||||
Физическая генерация НЕ использует эту константу: `setOverflow(carrierFrec*multiply, HERTZ_FORMAT)` (IR_Encoder.cpp:411) даёт реальные ~26.316 µs. А вся сетка RX (`riseSyncTime=962`, `riseTimeMin/Max`, `aroundRise`, `IR_timeout`) выведена из `carrierPeriod=26`. Результат: TX-биты систематически **на 1.2% длиннее**, чем ждёт RX. Сдвиг **односторонний** (всегда в плюс), поэтому запас `aroundRise` со стороны `riseTimeMax=1262` меньше, чем со стороны `riseTimeMin=662`. На бит это 11.7 µs, на преамбулу — ~15 µs (1274 vs 1289.5 µs). Это не канальная ошибка — это несогласованность двух представлений одной частоты внутри одного кода. **RX-допуск `tolerance=300` существует в т.ч. чтобы это не всплыло.** Исправляется тривиально (общий реальный период), после чего часть запаса окна освобождается под настоящий канал.
|
||||
|
||||
**(б) ISR-путь как источник джиттера, который чинит glitch-логика.**
|
||||
DMA-путь детерминирован (аппаратная пересылка BSRR по TIM_UP, IrDmaTxStm32.h:246-253). ISR-путь плавает по времени из-за латентности прерывания и обхода списка энкодеров (IR_Encoder.cpp:1037-1042). Плавающие фронты — ровно то, что подхватывают B/C (nudge) и частично G. То есть при работе через ISR приёмник страхует **собственный** TX-джиттер, а не канал. Лечение: всегда использовать DMA-путь (или признать ISR как деградированный режим).
|
||||
|
||||
**(в) Длинные непрерывные MARK провоцируют TSOP-АРУ → «рваная единица».**
|
||||
Кодирование pulse-width с MARK до 25 периодов и преамбулой 49 периодов непрерывной несущей — это осознанный выбор формы на TX. TSOP-демодуляторы имеют АРУ и штатно «проседают» на длинных непрерывных burst'ах, порождая ложные провалы огибающей в середине mark. Реконструкция E + G (ceil_div, `highCount==0 && highTime>riseTime/3 → highCount++`, 641-648) существует во многом чтобы это дособрать. Деформацию физически вносит канал (TSOP), но её вероятность задаёт форма на TX. Это не баг, но это «дизайн, требующий костыля на приёме».
|
||||
|
||||
**(г) Что НЕ является самолечением (важно не перепутать):**
|
||||
- Стартовая фаза кадра ≤1 период несущей (free-running TIM, IrDmaTxStm32.h:241-253) — компенсируется якорем фазы преамбулы (I, `prevRise=front.time+mean/2`, 1709). Но приёмник **в принципе** не знает абсолютную фазу передатчика и обязан выводить её из преамбулы — это необходимая функция, а не борьба с собой.
|
||||
- Маркер sync `{!L, L, !M}` детерминирован (IR_Encoder.cpp:305-331), буфер зануляется — H ловит **реальную** потерю/вставку бита в канале, не TX-артефакт.
|
||||
- Квантование фронтов сеткой 13.16 µs (DMA/BSRR) — тонет в допуске TSOP (десятки µs) и погоды не делает.
|
||||
|
||||
## 4. Широкие ли допуски и что они маскируют
|
||||
|
||||
`aroundRise` = 662..1262 µs при бите 962 → **±31%**. По логике подсчёта бит это **не** широко и путаницы 1-бит/2-бита не создаёт: 2 бита ≈1924 µs, порог `ceil_div` на 2 бита ≈1443 µs; между концом окна одного бита (1262) и порогом двух (1443) есть зазор ~180 µs — перекрытия нет. Значение бита определяется по duty, а не по длине периода, поэтому широкое окно значения не портит.
|
||||
|
||||
Куда реально уходит ±300 µs:
|
||||
- **Легитимно:** асимметрия leading/trailing фронтов TSOP-демодулятора (типично десятки–сотни µs), разброс кварцев RX/TX, дрожание огибающей, задержка АРУ. Для настоящего ИК-канала запас в этом порядке нужен.
|
||||
- **Маскирует баг:** самопорождённый односторонний сдвиг 1.2% (пункт 3а) полностью прячется в этом окне. Именно поэтому баг `carrierPeriod=26` не проявляется как отказ — допуск его глотает. Это и есть «система не замечает, что борется сама с собой».
|
||||
|
||||
Слабое место (согласен с RX-разбором): при `freeFrec=false` база `riseTime` жёстко=962 и **не** подстраивается по ходу кадра (медиана — «TODO», IR_DecoderRaw.h:22). Односторонний TX-сдвиг + систематическое смещение периода передатчика съедают запас именно со стороны `riseTimeMax`; на краю окна шум легче перекидывает интервал в ветку реконструкции G, накручивая `errors.*`. Также джиттер-полки преамбулы (18%, но не менее 80 µs) **жёстче** рабочего окна декодера (31%) — залочиться труднее, чем потом удержать биты; это перекос порогов, а не форма.
|
||||
|
||||
## 5. Рекомендации
|
||||
|
||||
**Чинить на TX (чтобы соответствующие RX-эвристики стали не нужны):**
|
||||
|
||||
1. **Согласовать период несущей между TX и RX.** Убрать усечение `carrierPeriod=26` из временной модели RX: выводить `bitTime`/`riseSyncTime` из той же реальной частоты, что даёт `setOverflow(carrierFrec*multiply,...)` (дробно, напр. фиксированная точка). Устраняет односторонний сдвиг 1.2% и освобождает ~12 µs/бит запаса окна. *Trade-off:* дробная арифметика на MCU; но можно один раз посчитать реальный период при инициализации.
|
||||
2. **Всегда использовать DMA-путь; ISR — только как явный деградированный fallback.** Снимает программный джиттер фронтов и часть нагрузки с B/C/G. *Trade-off:* требует свободного канала DMA+таймера; на платах без ресурса — остаётся ISR с его джиттером.
|
||||
3. **Пересмотреть длину непрерывных MARK/преамбулы под конкретный TSOP.** Если приёмник — демодулирующий TSOP с АРУ, длинные burst'ы (658/1290 µs) провоцируют «рваную единицу». Сократить максимальный непрерывный mark или разбить преамбулу на более частые фронты в пределах спецификации TSOP по burst-length. *Trade-off:* меняет протокол/энергетику кадра, требует ревизии окон преамбулы RX; выигрыш — можно ослабить E/G.
|
||||
4. **(Опционально) фазовое выравнивание старта кадра** (сброс счётчика TIM при старте потока). Уберёт стартовый сдвиг ≤26 µs. *Trade-off:* минимальный выигрыш (преамбула всё равно якорит фазу) — низкий приоритет.
|
||||
|
||||
**Что из «коррекций» оставить под настоящий канал (даже после идеального TX):**
|
||||
|
||||
- **G (ceil_div реконструкция) и E (сбор рваной единицы)** — оставить: дропаут несущей и просадка АРУ на реальной дистанции/отражениях никуда не денутся. Это ядро устойчивости к каналу.
|
||||
- **B/C (glitch reject + phase nudge)** и **A (pulse-filter)** — оставить: дребезг/иголки front-end реальны; A стоит **включить** (`IR_INPUT_MIN_PULSE_US>0`) на STM32, где сейчас фронты идут в декодер напрямую (tick 440-444).
|
||||
- **H (sync-grid recovery)** — оставить: детектирует реальную потерю/вставку бита; маркер детерминирован, ложных срабатываний от TX не будет.
|
||||
- **I (преамбула: EMA периода, якорь фазы)** — оставить: адаптация под кварцы/дрейф и вывод фазы обязательны для любого RX. Рекомендуется **выровнять пороги**: сделать джиттер-полку преамбулы не жёстче рабочего окна декодера, иначе лок труднее удержания.
|
||||
- **Кандидаты на сужение после фикса TX:** окно `aroundRise` можно сузить в сторону `riseTimeMax` (убрать заложенный под 1.2% односторонний запас) и реализовать бегущую медиану периода (IR_DecoderRaw.h:22 TODO) — тогда допуск начнёт отражать реальный канал, а не прятать несогласованность констант.
|
||||
|
||||
**Итог-вердикт:** коррекции RX в массе **оправданны под реальный ИК-канал**, но система содержит одно явное самолечение (усечённый `carrierPeriod` → 1.2% сдвиг, спрятанный в `tolerance`) и два условных (ISR-джиттер; длинные MARK, провоцирующие TSOP-АРУ). Широкий допуск ±31% сам по себе безопасен для подсчёта бит, но именно он маскирует баг с частотой — то есть система действительно частично борется сама с собой, просто настолько тихо, что это не видно в `errors.*`.
|
||||
36
docs_analysis/crc_combined_sim.py
Normal file
36
docs_analysis/crc_combined_sim.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Точное сравнение CRC-схем на бит-слипе. crc8 как в IR_config.cpp.
|
||||
# ВАЖНО: CRC в протоколе считается над БАЙТАМИ данных (sync НЕ входит — подтверждено кодом),
|
||||
# поэтому здесь sync в CRC-математику не включён (это корректно для оценки самой CRC).
|
||||
# Второй ниббл — СО СЦЕПКОЙ: poly2 над (data + crc1_байт), как в оригинале crcCheck.
|
||||
# Сквозной анализ с sync-фильтром требует модели FSM декодера (resync/‘исправление лишнего нуля’) — см. заметку.
|
||||
import random
|
||||
random.seed(3)
|
||||
POLY1=0x31; POLY2=0x8C
|
||||
def crc8(d,poly):
|
||||
c=0xFF
|
||||
for b in d:
|
||||
c^=b
|
||||
for _ in range(8): c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF
|
||||
return c
|
||||
def fold4(x): return (x^(x>>4))&0x0F
|
||||
def single(d): return (crc8(d,POLY1),)
|
||||
def double(d): c1=crc8(d,POLY1); return (c1, crc8(d+[c1],POLY2)) # текущая 2 байта, сцепка
|
||||
def combo_indep(d): return ((fold4(crc8(d,POLY1))<<4)|fold4(crc8(d,POLY2)),) # 1 байт, БЕЗ сцепки
|
||||
def combo_chain(d): c1=crc8(d,POLY1); return ((fold4(c1)<<4)|fold4(crc8(d+[c1],POLY2)),) # 1 байт, СО сцепкой (как ориг.)
|
||||
def bitsof(d): return [x for b in d for x in [(b>>(7-j))&1 for j in range(8)]]
|
||||
def bytesof(b): return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)]
|
||||
def slip(d,p): b=bitsof(d); return bytesof(b[:p]+b[p+1:]+[0])
|
||||
S=[("single 1б",single),("double 2б (текущая, сцепка)",double),
|
||||
("combo 1б БЕЗ сцепки",combo_indep),("combo 1б СО сцепкой (как ориг.)",combo_chain)]
|
||||
for L in (4,6,8):
|
||||
und={n:0 for n,_ in S}; ch=0
|
||||
for _ in range(300000):
|
||||
d=[random.randint(0,255) for _ in range(L)]; p=random.randint(L*2,L*6)
|
||||
c=slip(d,p)
|
||||
if c==d: continue
|
||||
ch+=1
|
||||
for n,f in S:
|
||||
if f(c)==f(d): und[n]+=1
|
||||
print(f"данные {L}б, {ch} слипов:")
|
||||
for n,_ in S: print(f" {n:34s}: {und[n]/ch*100:.4f}% необнаружено")
|
||||
33
docs_analysis/crc_shift_findings.md
Normal file
33
docs_analysis/crc_shift_findings.md
Normal file
@ -0,0 +1,33 @@
|
||||
# CRC при сдвиге битов — симуляция (воспроизведение и оценка сжатия)
|
||||
|
||||
> Скрипт: `crc_shift_sim.py` (рядом). `crc8` точно как в `IR_config.cpp` (MSB-first, init=0xFF, без final-xor). `poly1=0x31`, `poly2=0x8C` (битовое зеркало 0x31). Модель ошибки: сдвиг битов в СЕРЕДИНЕ данных (бит-слип / циклический сдвиг), длина кадра сохраняется; «чек» сверяется как `check(искажённые данные) == check(оригинал)`. 200000 попыток на модель, данные 3/5/8 байт.
|
||||
|
||||
## 1. Твой исторический баг ВОСПРОИЗВЕДён
|
||||
Одиночный CRC8 (один полином) реально пропускает сдвиг в середине пакета. Примеры (данные → сдвиг → искажённые):
|
||||
- `26 0b 05` → `26 16 0a`: `crc1=0x78` совпал → **single ПРОШЁЛ**; двойная схема: `crc2 0xd8≠0x5c` → **поймала**.
|
||||
- `cc4fb8f633` → `cc5f71ec66`: `crc1=0x20` совпал → single прошёл; double поймала (`crc2 0xfc≠0x90`).
|
||||
- `b983315b416440c3` → `b983315e82c88186`: `crc1=0x3a` совпал → single прошёл; double поймала.
|
||||
|
||||
## 2. Частота пропуска (доля НЕобнаруженных сдвигов)
|
||||
| Схема | slip-delete | rotate-all | rotate-mid-window |
|
||||
|---|---|---|---|
|
||||
| **single** (1×CRC8, 8 бит) | 0.29–0.41 % | ~0.78 % | 0.55–0.78 % |
|
||||
| **double** (2×CRC8, 16 бит) — текущая | **0.007–0.015 %** | **0.025–0.03 %** | **0.008–0.026 %** |
|
||||
| nibble-fold (4+4 бит) | 0.76–0.80 % | ~1.58 % | 1.18–1.55 % |
|
||||
| nibble-trunc (4+4 бит) | 1.37–1.57 % | ~1.53 % | 1.22–1.57 % |
|
||||
|
||||
Вывод: **double примерно в 20–40× надёжнее single** и ловит практически все сдвиги, которые single пропускает. Это подтверждает, зачем добавляли второй полином (0x8C = зеркало 0x31 ловит «зеркальные» сдвиговые ошибки, слепые для 0x31).
|
||||
|
||||
## 3. Идея «склеить 2 байта в 1» (полбайта на полином) — ХУЖЕ, чем есть
|
||||
- **nibble-fold** (свернуть каждый CRC8 в 4 бита xor'ом) и **nibble-trunc** (взять по 4 бита) дают **0.6–1.6 % пропусков — это ХУЖЕ даже одиночного полного CRC8** и в ~50–150× хуже текущей двойной схемы.
|
||||
- Burst-ошибки (подряд искажённые биты — типичный IR-сбой: бит-слипы/всплески). Гарантия обнаружения = ширине контроля:
|
||||
- single 8 бит и double — надёжны на коротких burst (≥8 бит), эмпирически в выборке не пропускали и длиннее;
|
||||
- **nibble-fold** — гарантия ~4 бита (эмпирически до 7);
|
||||
- **nibble-trunc** — **пропускает даже ОДИНОЧНЫЙ бит** (усечение теряет старший ниббл) → так делать нельзя.
|
||||
|
||||
## 4. Рекомендация
|
||||
- **CRC — не то место, где стоит экономить байт.** Сжатие контроля до 1 байта повышает пропуск сдвигов/burst в ~50–150× (с ~0.01 % до ~1 %) — прямо в том классе ошибок, ради которого второй полином и вводили.
|
||||
- Если байт очень нужен — забирать его **не из CRC**, а из уже выявленного резерва: свободные `msgType` (3,5), «пустая» зона длины (компактные кадры), или переупаковка адресов/полей. 16-битный контроль (2×CRC8) сохранить.
|
||||
- Если всё же сжимать CRC до 1 байта — только **fold (xor нибблов)**, никогда не truncate; и принять ~1 % пропуска на сдвигах (в ~50–100× хуже текущего). Как отдельный компромисс — обсуждать вместе с обратной совместимостью.
|
||||
|
||||
(Обратную совместимость версий протокола обсудим отдельно — по запросу Даши.)
|
||||
103
docs_analysis/crc_shift_sim.py
Normal file
103
docs_analysis/crc_shift_sim.py
Normal file
@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Симуляция CRC-схем IR-protocol на "сдвиговых" ошибках.
|
||||
# crc8 воспроизводит IR_config.cpp: MSB-first, init=0xFF, без final-xor.
|
||||
import random
|
||||
random.seed(1)
|
||||
POLY1 = 0x31
|
||||
POLY2 = 0x8C # битовое зеркало 0x31
|
||||
|
||||
def crc8(data, poly):
|
||||
crc = 0xFF
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
crc = ((crc << 1) ^ poly) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF
|
||||
return crc
|
||||
|
||||
def fold4(x): # свернуть 8-бит CRC в 4 бита (xor старший^младший ниббл)
|
||||
return (x ^ (x >> 4)) & 0x0F
|
||||
|
||||
# --- схемы контроля: считают "чек" от данных и сверяют ---
|
||||
def single_chk(d): return (crc8(d, POLY1),) # 8 бит, 1 полином
|
||||
def double_chk(d): c1 = crc8(d, POLY1); return (c1, crc8(d + [c1], POLY2)) # текущая: 16 бит
|
||||
def nib_fold_chk(d):return ((fold4(crc8(d, POLY1)) << 4) | fold4(crc8(d, POLY2)),) # предложенная: 4+4 бит
|
||||
def nib_trunc_chk(d):return (((crc8(d, POLY1) & 0x0F) << 4) | (crc8(d, POLY2) & 0x0F),)
|
||||
|
||||
def bits_of(data):
|
||||
return [(b >> (7 - j)) & 1 for b in data for j in range(8)]
|
||||
def bytes_of(bits):
|
||||
return [sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8)]
|
||||
|
||||
# --- модели "сдвига" в середине пакета (над битами данных, длина сохраняется) ---
|
||||
def slip_delete(data, p): # выкинуть бит p, сдвинуть хвост влево, добить 0 в конец
|
||||
b = bits_of(data); b = b[:p] + b[p+1:] + [0]; return bytes_of(b)
|
||||
def rotate_all_right(data, _): # циклический сдвиг ВСЕХ бит вправо на 1
|
||||
b = bits_of(data); b = [b[-1]] + b[:-1]; return bytes_of(b)
|
||||
def rotate_mid_window(data, p): # циклический сдвиг окна [p, p+16) на 1 (локальный сдвиг в середине)
|
||||
b = bits_of(data); q = min(p+16, len(b))
|
||||
w = b[p:q]; w = [w[-1]] + w[:-1]; b = b[:p] + w + b[q:]; return bytes_of(b)
|
||||
|
||||
MODELS = [("slip-delete (бит-слип)", slip_delete),
|
||||
("rotate-all (сдвиг всего на 1)", rotate_all_right),
|
||||
("rotate-mid-window (сдвиг окна в середине)", rotate_mid_window)]
|
||||
SCHEMES = [("single (1×CRC8, 8бит)", single_chk),
|
||||
("double (2×CRC8, 16бит) — текущая", double_chk),
|
||||
("nibble-fold (4+4бит)", nib_fold_chk),
|
||||
("nibble-trunc (4+4бит)", nib_trunc_chk)]
|
||||
|
||||
def run(datalen, trials):
|
||||
print(f"\n===== данные {datalen} байт, {trials} попыток на модель =====")
|
||||
for mname, model in MODELS:
|
||||
# для каждой схемы: сколько ошибок НЕ обнаружено (чек совпал, а данные изменились)
|
||||
undetected = {s[0]: 0 for s in SCHEMES}
|
||||
changed = 0
|
||||
first_single_miss = None
|
||||
for _ in range(trials):
|
||||
d = [random.randint(0, 255) for _ in range(datalen)]
|
||||
p = random.randint(datalen*8//4, datalen*8*3//4) # позиция в середине
|
||||
c = model(d, p)
|
||||
if c == d:
|
||||
continue
|
||||
changed += 1
|
||||
for sname, chk in SCHEMES:
|
||||
if chk(c) == chk(d):
|
||||
undetected[sname] += 1
|
||||
if sname.startswith("single") and first_single_miss is None:
|
||||
first_single_miss = (d[:], c[:], p)
|
||||
print(f" модель: {mname} (изменённых кадров: {changed})")
|
||||
for sname, _ in SCHEMES:
|
||||
u = undetected[sname]
|
||||
rate = u / changed if changed else 0
|
||||
print(f" {sname:38s}: пропущено {u:6d} ({rate*100:.4f}%)")
|
||||
if first_single_miss:
|
||||
d, c, p = first_single_miss
|
||||
print(f" ПРИМЕР пропуска single (poly1): data={bytes(d).hex()} -> shift@{p} -> {bytes(c).hex()}")
|
||||
print(f" single: crc1(orig)={crc8(d,POLY1):02x} == crc1(shift)={crc8(c,POLY1):02x} -> ПРОШЁЛ")
|
||||
print(f" double: crc2(orig)={double_chk(d)[1]:02x} vs crc2(shift)={double_chk(c)[1]:02x} -> {'ПРОШЁЛ (не поймал!)' if double_chk(d)==double_chk(c) else 'ПОЙМАЛ'}")
|
||||
|
||||
for L in (3, 5, 8):
|
||||
run(L, 200000)
|
||||
|
||||
# Гарантированное покрытие burst-ошибок (подряд идущие искажённые биты) — ключ для IR (бит-слипы/всплески)
|
||||
print("\n===== Гарантия обнаружения BURST-ошибок (100% всех burst данной длины?) =====")
|
||||
def burst_guaranteed(chk, datalen, blen):
|
||||
# перебор всех burst длины blen по всем позициям на нескольких случайных базах; ищем ХОТЬ ОДИН пропуск
|
||||
for _ in range(300):
|
||||
d = [random.randint(0,255) for _ in range(datalen)]
|
||||
base = bits_of(d)
|
||||
for start in range(len(base)-blen+1):
|
||||
b2 = base[:]
|
||||
for k in range(blen):
|
||||
b2[start+k] ^= 1
|
||||
c = bytes_of(b2)
|
||||
if c != d and chk(c) == chk(d):
|
||||
return False # найден необнаруженный burst
|
||||
return True
|
||||
for sname, chk in SCHEMES:
|
||||
maxb = 0
|
||||
for blen in range(1, 25):
|
||||
if burst_guaranteed(chk, 5, blen):
|
||||
maxb = blen
|
||||
else:
|
||||
break
|
||||
print(f" {sname:38s}: 100%% ловит burst до {maxb} бит подряд")
|
||||
97
docs_analysis/crc_shift_visualize.py
Normal file
97
docs_analysis/crc_shift_visualize.py
Normal file
@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Визуализация кадров и сдвигов битов (с синхробитами и подписями).
|
||||
# crc8 как в IR_config.cpp. Синхро (syncBits=3) логически = [~last, last, ~next] (addSync, send_HIGH=1).
|
||||
POLY1=0x31; POLY2=0x8C
|
||||
def crc8(data,poly):
|
||||
c=0xFF
|
||||
for b in data:
|
||||
c^=b
|
||||
for _ in range(8):
|
||||
c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF
|
||||
return c
|
||||
def bits(b): return [(b>>(7-j))&1 for j in range(8)]
|
||||
def bstr(bl): return ''.join(str(x) for x in bl)
|
||||
|
||||
def onair(frame_bytes, labels):
|
||||
"""строка вида: dddddddd sss dddddddd sss ... + строка подписей под байтами"""
|
||||
rows=[]
|
||||
line=''; lab=''
|
||||
for i,by in enumerate(frame_bytes):
|
||||
db=bstr(bits(by))
|
||||
# синхрогруппа ПОСЛЕ байта (между байтами), кроме последнего
|
||||
if i < len(frame_bytes)-1:
|
||||
last=bits(by)[7]; nxt=bits(frame_bytes[i+1])[0]
|
||||
sy=f"{1-last}{last}{1-nxt}"
|
||||
piece=db+' ['+sy+'] '
|
||||
else:
|
||||
piece=db
|
||||
# подпись байта, центрируем под 8 бит
|
||||
L=labels[i] if i<len(labels) else ''
|
||||
lab+=(L[:8]).center(8)+ (' s ' if i<len(frame_bytes)-1 else '')
|
||||
line+=piece
|
||||
return line, lab
|
||||
|
||||
def show_frame(title, frame_bytes, labels):
|
||||
print(f"\n### {title}")
|
||||
print(" байты (hex):", ' '.join(f"{b:02x}" for b in frame_bytes))
|
||||
print(" поля :", ' '.join((labels[i] if i<len(labels) else '?')[:2].ljust(2) for i in range(len(frame_bytes))),
|
||||
" (по 1 байту)")
|
||||
line,lab=onair(frame_bytes,labels)
|
||||
print(" эфир (data 8 бит + [sync 3]) и подписи:")
|
||||
print(" "+line)
|
||||
print(" "+lab)
|
||||
|
||||
def slip_delete_databits(data, p):
|
||||
b=[x for by in data for x in bits(by)]
|
||||
b=b[:p]+b[p+1:]+[0]
|
||||
return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)]
|
||||
|
||||
def show_shift(title, data, p):
|
||||
print(f"\n### {title}")
|
||||
shifted=slip_delete_databits(data,p)
|
||||
b0=[x for by in data for x in bits(by)]
|
||||
b1=[x for by in shifted for x in bits(by)]
|
||||
# выравненные строки битов по байтам
|
||||
def grp(bl): return ' '.join(bstr(bl[i:i+8]) for i in range(0,len(bl),8))
|
||||
print(" ДО :", ' '.join(f"{x:02x}" for x in data), " crc1(poly1)=%02x"%crc8(data,POLY1),
|
||||
" crc2(poly2 over data+crc1)=%02x"%crc8(data+[crc8(data,POLY1)],POLY2))
|
||||
print(" ПОСЛЕ :", ' '.join(f"{x:02x}" for x in shifted), " crc1(poly1)=%02x"%crc8(shifted,POLY1),
|
||||
" crc2=%02x"%crc8(shifted+[crc8(shifted,POLY1)],POLY2))
|
||||
print(" биты ДО :", grp(b0))
|
||||
# маркер позиции удалённого бита p
|
||||
mk=[' ']*len(b0);
|
||||
pos = p + p//8 # +пробелы между байтами
|
||||
mk_line=''
|
||||
# построим маркерную строку под "биты ДО"
|
||||
s=grp(b0)
|
||||
# индекс символа для бита p:
|
||||
char_idx = p + (p//8) # каждый байт(8) + 1 пробел
|
||||
mk_line=' '*char_idx+'^'
|
||||
print(" ", mk_line, f"<- бит #{p} удалён, всё правее сдвинуто влево на 1")
|
||||
print(" биты ПОСЛЕ:", grp(b1))
|
||||
# различия побитно
|
||||
diff=''.join('X' if (i<len(b1) and b0[i]!=b1[i]) else '.' for i in range(len(b0)))
|
||||
print(" дельта :", ' '.join(diff[i:i+8] for i in range(0,len(diff),8)), " (X=бит изменился)")
|
||||
same1 = crc8(data,POLY1)==crc8(shifted,POLY1)
|
||||
c2o=crc8(data+[crc8(data,POLY1)],POLY2); c2s=crc8(shifted+[crc8(shifted,POLY1)],POLY2)
|
||||
print(f" ВЕРДИКТ: single(poly1) {'ПРОШЁЛ (не заметил сдвиг!)' if same1 else 'поймал'};"
|
||||
f" double(+poly2) {'НЕ поймал' if (same1 and c2o==c2s) else 'ПОЙМАЛ'} (crc2 {c2o:02x}{'=='if c2o==c2s else '≠'}{c2s:02x})")
|
||||
|
||||
print("="*70)
|
||||
print("ЧАСТЬ 1. Реалистичный кадр на эфире (структура + синхробиты)")
|
||||
print("="*70)
|
||||
# BACK-кадр: msgType=0(BACK), packSize=5 → [hdr][addrFrom_hi][addrFrom_lo][crc1][crc2]
|
||||
# hdr = (0<<5)|5 = 0x05 ; addrFrom = 300 = 0x012C
|
||||
data_back=[0x05, 0x01, 0x2C]
|
||||
c1=crc8(data_back,POLY1); c2=crc8(data_back+[c1],POLY2)
|
||||
frame_back=data_back+[c1,c2]
|
||||
show_frame("BACK, addrFrom=300, packSize=5", frame_back,
|
||||
["HDR","AfHi","AfLo","CRC1","CRC2"])
|
||||
print(" (HDR = 000 00101: msgType=0(BACK), len=5=весь пакет; sync между байтами = [¬last,last,¬next])")
|
||||
|
||||
print("\n"+"="*70)
|
||||
print("ЧАСТЬ 2. Сдвиги, где ОДИНОЧНЫЙ CRC8 слеп (из симуляции), в битах")
|
||||
print("="*70)
|
||||
show_shift("Пример A: data 26 0b 05, слип бита #10", [0x26,0x0b,0x05], 10)
|
||||
show_shift("Пример B: data 3b 7c 43, слип бита #14 (окно в середине)", [0x3b,0x7c,0x43], 14)
|
||||
show_shift("Пример C: data cc 4f b8 f6 33, слип бита #11", [0xcc,0x4f,0xb8,0xf6,0x33], 11)
|
||||
@ -1,5 +1,7 @@
|
||||
# Контракт бэкенда DMA-TX ИК (`IrDmaTxStm32`)
|
||||
|
||||
См. также: [IR_TX_MODES.md](IR_TX_MODES.md) — общая схема выбора `legacy ISR`, `buffered ISR` и `external backend`.
|
||||
|
||||
Платформа: **STM32G4**, Arduino STM32. Передача: **DMA memory → GPIO BSRR**, запрос от **TIM UPDATE** (частота `carrierFrec×2` из `IR_Encoder::beginClockOnly`).
|
||||
|
||||
### Число потоков (шаблон)
|
||||
|
||||
@ -55,7 +55,7 @@ IRRX t=1234988 rsn=MUTE_END cnt=42
|
||||
## Когда смотреть подробный debug
|
||||
|
||||
- `listenStart` / `checkTimeout` — в конце обработки фронта (`END:`) и во ветке «нет фронта» в `tick()`; не в начале до `pop`, иначе после таймаута `lastEdgeTime` расходится с метками ISR из очереди → ложные `TIMEOUT` (`bits=0`).
|
||||
- Пока в `subBuffer` / `filteredSubBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
|
||||
- Пока в `subBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
|
||||
- Если нужен полный поток битов и sync: включать `IRDEBUG_SERIAL_PACK`
|
||||
- Если нужно понять, какие именно фронты пришли в ISR: включать `IR_EDGE_TRACE`
|
||||
- `IR_RX_BRIEF_LOG` нужен как короткий always-on-ish индикатор сути проблемы, без длинного дампа
|
||||
|
||||
146
ref/IR_TX_MODES.md
Normal file
146
ref/IR_TX_MODES.md
Normal file
@ -0,0 +1,146 @@
|
||||
# Режимы IR TX в библиотеке
|
||||
|
||||
Этот документ описывает, как в библиотеке выбирается путь передачи IR и как его правильно использовать в проектах Arduino STM32.
|
||||
|
||||
## Кратко
|
||||
|
||||
У библиотеки есть три варианта TX:
|
||||
|
||||
- `legacy ISR` — внутренний ISR-путь без внешнего backend. Это путь по умолчанию для обратной совместимости.
|
||||
- `buffered ISR` — внутренний ISR-путь с предварительной подготовкой BSRR-слов и кольцевым буфером.
|
||||
- `external backend` — передача делегируется проекту через `IR_Encoder::setExternalTxBackend(...)`, например в DMA backend.
|
||||
|
||||
Порядок выбора такой:
|
||||
|
||||
1. Если зарегистрирован `external backend`, используется он.
|
||||
2. Иначе используется внутренний ISR библиотеки.
|
||||
3. Для внутреннего ISR:
|
||||
- по умолчанию включён `legacy ISR`
|
||||
- `buffered ISR` включается явно: нужно привязать storage к encoder и переключить режим
|
||||
|
||||
## 1. Legacy ISR
|
||||
|
||||
Это режим по умолчанию. Старые проекты могут ничего не менять:
|
||||
|
||||
```cpp
|
||||
static HardwareTimer timer(TIM11);
|
||||
static IR_Encoder enc(PA9, 42, &dec);
|
||||
|
||||
void setup() {
|
||||
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||
enc.enable();
|
||||
}
|
||||
```
|
||||
|
||||
Если проект не регистрирует внешний backend и не переключает режим явно, библиотека работает в `legacy ISR`.
|
||||
|
||||
Для явного выбора можно написать:
|
||||
|
||||
```cpp
|
||||
IR_Encoder::setTxIsrLegacyMode(true);
|
||||
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||
```
|
||||
|
||||
## 2. Buffered ISR
|
||||
|
||||
Этот режим использует внутренний буферный ISR-путь библиотеки. Он включается только явно:
|
||||
|
||||
```cpp
|
||||
#include <IrTxIsrBufferedStorage.h>
|
||||
|
||||
static IrTxIsrBufferedStorage<> txStorage;
|
||||
|
||||
enc.enableBufferedIsr(txStorage);
|
||||
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||
```
|
||||
|
||||
Нижнеуровневый вариант API — отдельно привязать storage через `attachBufferedIsrStorage(...)`, но в обычном проекте удобнее использовать `enableBufferedIsr(...)`.
|
||||
|
||||
Смысл режима:
|
||||
|
||||
- пакет сначала превращается в `gate runs`
|
||||
- затем в поток слов `GPIO->BSRR`
|
||||
- ISR выдаёт готовые слова из кольцевого буфера
|
||||
|
||||
### Важно про RAM
|
||||
|
||||
В текущей реализации память под буферный ISR вынесена из `IR_Encoder` в отдельный storage-объект.
|
||||
|
||||
То есть:
|
||||
|
||||
- `legacy ISR` не тянет buffered-буферы в RAM самого `IR_Encoder`
|
||||
- память под `gate runs` и `BSRR words` появляется только там, где проект сам создал `IrTxIsrBufferedStorage<>`
|
||||
|
||||
Это важно для STM32 с небольшим объёмом RAM, например для `STM32F401`.
|
||||
|
||||
## 3. External backend
|
||||
|
||||
Если проект хочет полностью взять TX на себя, библиотека позволяет зарегистрировать внешний backend:
|
||||
|
||||
```cpp
|
||||
static bool txBusy(void* ctx);
|
||||
static bool txStart(void* ctx, IR_Encoder* enc, const uint8_t* packet, uint8_t len);
|
||||
|
||||
void setup() {
|
||||
IR_Encoder::beginClockOnly(&timer);
|
||||
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
||||
enc.enable();
|
||||
}
|
||||
```
|
||||
|
||||
После вызова `setExternalTxBackend(...)` библиотека больше не использует свои внутренние ISR-пути для фактической передачи.
|
||||
|
||||
В этом режиме:
|
||||
|
||||
- `setTxIsrLegacyMode(true/false)` игнорируется
|
||||
- завершение передачи должен сигнализировать сам backend через `enc->externalFinishSend()`
|
||||
|
||||
Подробности по встроенному DMA backend для `STM32G4xx`: см. [IR_DMA_TX_backend.md](IR_DMA_TX_backend.md).
|
||||
|
||||
## Когда какой режим использовать
|
||||
|
||||
### Старый проект, который ничего не настраивает
|
||||
|
||||
Использовать как есть:
|
||||
|
||||
```cpp
|
||||
IR_Encoder::begin(...);
|
||||
```
|
||||
|
||||
Итог: `legacy ISR`
|
||||
|
||||
### Нужен новый внутренний буферный ISR
|
||||
|
||||
Включить явно:
|
||||
|
||||
```cpp
|
||||
#include <IrTxIsrBufferedStorage.h>
|
||||
|
||||
static IrTxIsrBufferedStorage<> txStorage;
|
||||
|
||||
enc.enableBufferedIsr(txStorage);
|
||||
IR_Encoder::begin(...);
|
||||
```
|
||||
|
||||
Итог: `buffered ISR`
|
||||
|
||||
### Нужен проектный DMA или другой свой транспорт
|
||||
|
||||
Подключить внешний backend:
|
||||
|
||||
```cpp
|
||||
IR_Encoder::beginClockOnly(...);
|
||||
IR_Encoder::setExternalTxBackend(...);
|
||||
```
|
||||
|
||||
Итог: `external backend`
|
||||
|
||||
## Рекомендация для совместимости
|
||||
|
||||
Для старых проектов безопаснее не вызывать `setTxIsrLegacyMode(false)`, если нет явной причины переходить на buffered ISR.
|
||||
|
||||
Если задача — сохранить старое поведение без неожиданного роста нагрузки на TX-логику, оставляйте default `legacy ISR` или задавайте его явно:
|
||||
|
||||
```cpp
|
||||
IR_Encoder::setTxIsrLegacyMode(true);
|
||||
```
|
||||
@ -132,6 +132,7 @@ void setup() {
|
||||
void loop() {
|
||||
#if LONGDATA_USE_DMA
|
||||
IR_Encoder::tick();
|
||||
dmaBackend.poll(); // Fix D: watchdog завершения DMA-передачи (как в Car/src/IR/IR.cpp)
|
||||
#endif
|
||||
|
||||
const uint32_t now = millis();
|
||||
|
||||
60
tests/arduino_stubs/Arduino.h
Normal file
60
tests/arduino_stubs/Arduino.h
Normal file
@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
class __FlashStringHelper;
|
||||
#define F(value) reinterpret_cast<const __FlashStringHelper *>(value)
|
||||
|
||||
struct GPIO_TypeDef
|
||||
{
|
||||
uint32_t BSRR = 0U;
|
||||
uint32_t IDR = 0U;
|
||||
};
|
||||
|
||||
using IRQn_Type = int;
|
||||
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
|
||||
|
||||
constexpr uint8_t LOW = 0U;
|
||||
constexpr uint8_t HIGH = 1U;
|
||||
constexpr uint8_t INPUT = 0U;
|
||||
constexpr uint8_t OUTPUT = 1U;
|
||||
|
||||
class HardwareTimer
|
||||
{
|
||||
public:
|
||||
void pause() {}
|
||||
void resume() {}
|
||||
void setOverflow(uint32_t value, TimerFormat_t = TICK_FORMAT) { overflow_ = value; }
|
||||
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
|
||||
uint32_t getPrescaleFactor() { return 1U; }
|
||||
uint32_t getTimerClkFreq() { return 12000000U; }
|
||||
void attachInterrupt(uint8_t, void (*)()) {}
|
||||
|
||||
private:
|
||||
uint32_t overflow_ = 1U;
|
||||
};
|
||||
|
||||
inline GPIO_TypeDef arduinoStubPort;
|
||||
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return &arduinoStubPort; }
|
||||
inline uint16_t digitalPinToBitMask(uint8_t) { return 1U; }
|
||||
inline void pinMode(uint8_t, uint8_t) {}
|
||||
inline void digitalWrite(uint8_t, uint8_t) {}
|
||||
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
|
||||
inline void noInterrupts() {}
|
||||
inline void interrupts() {}
|
||||
|
||||
inline uint32_t arduinoStubMicros = 0U;
|
||||
inline uint32_t micros() { return arduinoStubMicros; }
|
||||
inline uint32_t millis() { return arduinoStubMicros / 1000U; }
|
||||
|
||||
class Print
|
||||
{
|
||||
public:
|
||||
template <typename T> void print(const T &) {}
|
||||
template <typename T> void println(const T &) {}
|
||||
void println() {}
|
||||
};
|
||||
|
||||
using ArduinoSerialStub = Print;
|
||||
inline ArduinoSerialStub Serial;
|
||||
386
tests/test_rx_terminal.cpp
Normal file
386
tests/test_rx_terminal.cpp
Normal file
@ -0,0 +1,386 @@
|
||||
#include "IR_config.h"
|
||||
#include "RingBuffer.h"
|
||||
|
||||
// Test only: inspect the decoder state machine without adding production hooks.
|
||||
#define private public
|
||||
#include "IR_DecoderRaw.h"
|
||||
#undef private
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
uint32_t decoderTimeoutUs(const IR_DecoderRaw &decoder)
|
||||
{
|
||||
return static_cast<uint32_t>(decoder.riseSyncTime + tolerance) *
|
||||
static_cast<uint32_t>(bitPerByte + syncBits + 1U);
|
||||
}
|
||||
|
||||
uint32_t candidateTimeoutUs(const IR_DecoderRaw &decoder)
|
||||
{
|
||||
return decoderTimeoutUs(decoder) *
|
||||
static_cast<uint32_t>(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT);
|
||||
}
|
||||
|
||||
uint8_t crc8Local(const uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
|
||||
{
|
||||
uint8_t crc = 0xFFU;
|
||||
for (uint8_t i = start; i < end; ++i)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8U; ++bit)
|
||||
crc = (crc & 0x80U) != 0U
|
||||
? static_cast<uint8_t>((crc << 1U) ^ poly)
|
||||
: static_cast<uint8_t>(crc << 1U);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
void primeObservableCandidate(IR_DecoderRaw &decoder, uint32_t lastEdgeUs)
|
||||
{
|
||||
decoder.preambleState = IR_DecoderRaw::PreambleState::Candidate;
|
||||
decoder.preambleGoodPeriods = 1U;
|
||||
decoder.preambleWasObservable = true;
|
||||
decoder.preambleMeanPeriod = bitTime;
|
||||
decoder.preambleCandidateLastEdgeTime = lastEdgeUs;
|
||||
decoder.preambleCandidateFirstRiseTime = lastEdgeUs;
|
||||
decoder.preambleCandidateFirstRiseValid = true;
|
||||
decoder.isPreamb = true;
|
||||
decoder.isRecive = false;
|
||||
decoder.isReciveRaw = false;
|
||||
}
|
||||
|
||||
void primeLocked(IR_DecoderRaw &decoder, uint8_t msgType, uint8_t wireBytes)
|
||||
{
|
||||
decoder.preambleState = IR_DecoderRaw::PreambleState::Locked;
|
||||
decoder.isPreamb = false;
|
||||
decoder.isRecive = true;
|
||||
decoder.isReciveRaw = true;
|
||||
decoder.isWrongPack = false;
|
||||
decoder.isBufferOverflow = false;
|
||||
decoder.isAvailable = false;
|
||||
decoder.packSize = wireBytes;
|
||||
decoder.dataBuffer[0] =
|
||||
static_cast<uint8_t>((msgType << 5U) | (wireBytes & IR_MASK_MSG_INFO));
|
||||
decoder.i_dataBuffer = 8U;
|
||||
}
|
||||
|
||||
void verifyInitialSnapshot()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(terminal.seq == 0U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::None);
|
||||
assert(terminal.msgType == 0xFFU);
|
||||
assert(!terminal.hadLock);
|
||||
}
|
||||
|
||||
void verifyCandidateExpiresOnIdleTick()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t lastEdgeUs = 1000U;
|
||||
primeObservableCandidate(decoder, lastEdgeUs);
|
||||
assert(decoder.rxLineActive());
|
||||
|
||||
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
|
||||
decoder.tick();
|
||||
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(!decoder.rxLineActive());
|
||||
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Idle);
|
||||
assert(terminal.seq == 1U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(terminal.msgType == 0xFFU);
|
||||
assert(!terminal.hadLock);
|
||||
|
||||
++arduinoStubMicros;
|
||||
decoder.tick();
|
||||
assert(decoder.rxTerminalSeq() == terminal.seq);
|
||||
}
|
||||
|
||||
void emitEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high)
|
||||
{
|
||||
arduinoStubMicros = timeUs;
|
||||
arduinoStubPort.IDR = high ? 1U : 0U;
|
||||
decoder.isr();
|
||||
decoder.tick();
|
||||
}
|
||||
|
||||
void queueEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high)
|
||||
{
|
||||
arduinoStubMicros = timeUs;
|
||||
arduinoStubPort.IDR = high ? 1U : 0U;
|
||||
decoder.isr();
|
||||
}
|
||||
|
||||
void verifyCandidateIdleExpiryThroughPublicPipeline()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
|
||||
const uint32_t risePeriodUs = static_cast<uint32_t>(bitTime) * 5U / 2U;
|
||||
|
||||
emitEdge(decoder, firstRiseUs, true);
|
||||
emitEdge(decoder, firstRiseUs + risePeriodUs / 2U, false);
|
||||
emitEdge(decoder, firstRiseUs + risePeriodUs, true);
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
|
||||
arduinoStubMicros = firstRiseUs + risePeriodUs + candidateTimeoutUs(decoder) + 1U;
|
||||
decoder.tick();
|
||||
assert(!decoder.rxLineActive());
|
||||
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(decoder.rxTerminalSeq() == 1U);
|
||||
}
|
||||
|
||||
void verifyCoarseResetPublishesThroughBatchedPublicPipeline()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
|
||||
const uint32_t goodPeriodUs = static_cast<uint32_t>(bitTime) * 5U / 2U;
|
||||
const uint32_t badPeriodUs = static_cast<uint32_t>(bitTime) * 4U;
|
||||
|
||||
queueEdge(decoder, firstRiseUs, true);
|
||||
queueEdge(decoder, firstRiseUs + goodPeriodUs / 2U, false);
|
||||
queueEdge(decoder, firstRiseUs + goodPeriodUs, true);
|
||||
queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs / 2U, false);
|
||||
queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs, true);
|
||||
decoder.tick();
|
||||
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
assert(decoder.rxLineActive());
|
||||
|
||||
// Continuing coarse-invalid edges below the timeout keep the potential
|
||||
// frame busy. They manufacture no terminal; a Car gate reaches its bounded
|
||||
// hard deadline and skips the optional tail instead of transmitting here.
|
||||
const uint32_t nextRiseUs =
|
||||
firstRiseUs + goodPeriodUs + badPeriodUs + badPeriodUs;
|
||||
queueEdge(decoder, nextRiseUs - badPeriodUs / 2U, false);
|
||||
queueEdge(decoder, nextRiseUs, true);
|
||||
decoder.tick();
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
assert(decoder.rxLineActive());
|
||||
|
||||
arduinoStubMicros = nextRiseUs + candidateTimeoutUs(decoder) + 1U;
|
||||
decoder.tick();
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(terminal.seq == 1U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(!terminal.hadLock);
|
||||
assert(!decoder.rxLineActive());
|
||||
}
|
||||
|
||||
void verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
|
||||
const uint32_t badPeriodUs = static_cast<uint32_t>(bitTime) * 4U;
|
||||
const uint32_t startDeadlineUs = firstRiseUs + 58000U;
|
||||
const uint32_t hardDeadlineUs = firstRiseUs + 78000U;
|
||||
|
||||
emitEdge(decoder, firstRiseUs, true);
|
||||
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate);
|
||||
assert(decoder.preambleGoodPeriods == 0U);
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
|
||||
uint32_t riseUs = firstRiseUs;
|
||||
while (riseUs + badPeriodUs <= startDeadlineUs)
|
||||
{
|
||||
emitEdge(decoder, riseUs + badPeriodUs / 2U, false);
|
||||
riseUs += badPeriodUs;
|
||||
emitEdge(decoder, riseUs, true);
|
||||
assert(decoder.preambleGoodPeriods == 0U);
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
}
|
||||
|
||||
arduinoStubMicros = startDeadlineUs;
|
||||
decoder.tick();
|
||||
assert(decoder.rxLineActive());
|
||||
|
||||
while (riseUs + badPeriodUs <= hardDeadlineUs)
|
||||
{
|
||||
emitEdge(decoder, riseUs + badPeriodUs / 2U, false);
|
||||
riseUs += badPeriodUs;
|
||||
emitEdge(decoder, riseUs, true);
|
||||
assert(decoder.preambleGoodPeriods == 0U);
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
}
|
||||
|
||||
arduinoStubMicros = hardDeadlineUs;
|
||||
decoder.tick();
|
||||
assert(decoder.rxLineActive());
|
||||
|
||||
arduinoStubMicros = riseUs + candidateTimeoutUs(decoder);
|
||||
decoder.tick();
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
|
||||
++arduinoStubMicros;
|
||||
decoder.tick();
|
||||
assert(!decoder.rxLineActive());
|
||||
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(decoder.rxTerminalSeq() == 1U);
|
||||
}
|
||||
|
||||
void verifyCandidateExpiryWaitsForPipelineDrain()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t lastEdgeUs = 2000U;
|
||||
primeObservableCandidate(decoder, lastEdgeUs);
|
||||
decoder.pulseFilterHoldCount = 1U;
|
||||
|
||||
const uint32_t expiredAt = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
|
||||
decoder.expirePreambleCandidateIfIdle(expiredAt);
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
|
||||
decoder.pulseFilterHoldCount = 0U;
|
||||
decoder.expirePreambleCandidateIfIdle(expiredAt);
|
||||
assert(!decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 1U);
|
||||
}
|
||||
|
||||
void verifyCandidateExpiryAcrossMicrosWrap()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t lastEdgeUs = std::numeric_limits<uint32_t>::max() - 1000U;
|
||||
primeObservableCandidate(decoder, lastEdgeUs);
|
||||
|
||||
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
|
||||
decoder.tick();
|
||||
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(!decoder.rxLineActive());
|
||||
}
|
||||
|
||||
void verifyCandidateTimeoutBoundary()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t lastEdgeUs = 2500U;
|
||||
primeObservableCandidate(decoder, lastEdgeUs);
|
||||
|
||||
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder);
|
||||
decoder.tick();
|
||||
assert(decoder.rxLineActive());
|
||||
assert(decoder.rxTerminalSeq() == 0U);
|
||||
|
||||
++arduinoStubMicros;
|
||||
decoder.tick();
|
||||
assert(!decoder.rxLineActive());
|
||||
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
}
|
||||
|
||||
void verifyTimedOutCandidateRestartIsTerminal()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
const uint32_t lastEdgeUs = 3000U;
|
||||
primeObservableCandidate(decoder, lastEdgeUs);
|
||||
|
||||
IR_DecoderRaw::FrontStorage nextEdge;
|
||||
nextEdge.time = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
|
||||
nextEdge.dir = true;
|
||||
decoder.preambleProcessEdge(nextEdge);
|
||||
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(terminal.seq == 1U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
|
||||
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate);
|
||||
assert(decoder.preambleGoodPeriods == 0U);
|
||||
assert(decoder.rxReasonCounters()[
|
||||
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Preamble)] == 1U);
|
||||
}
|
||||
|
||||
void verifyLockedTimeoutPublishesHeaderTypeOnce()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
primeLocked(decoder, IR_MSG_DATA_NOACCEPT, 10U);
|
||||
decoder.lastEdgeTime = 5000U;
|
||||
|
||||
arduinoStubMicros = decoder.lastEdgeTime + decoderTimeoutUs(decoder) * 2U + 1U;
|
||||
decoder.tick();
|
||||
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(terminal.seq == 1U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::LockedTimeout);
|
||||
assert(terminal.msgType == IR_MSG_DATA_NOACCEPT);
|
||||
assert(terminal.hadLock);
|
||||
assert(!decoder.rxLineActive());
|
||||
|
||||
++arduinoStubMicros;
|
||||
decoder.tick();
|
||||
assert(decoder.rxTerminalSeq() == terminal.seq);
|
||||
}
|
||||
|
||||
void verifyDecodeAbortPublishesTerminal()
|
||||
{
|
||||
IR_DecoderRaw decoder(1U, 42U, nullptr);
|
||||
primeLocked(decoder, IR_MSG_REQUEST, 7U);
|
||||
decoder.isWrongPack = true;
|
||||
decoder.writeToBuffer(false);
|
||||
|
||||
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
|
||||
assert(terminal.seq == 1U);
|
||||
assert(terminal.reason == IR_RxTerminalReason::DecodeAbort);
|
||||
assert(terminal.msgType == IR_MSG_REQUEST);
|
||||
assert(terminal.hadLock);
|
||||
}
|
||||
|
||||
void finishBackFrame(IR_DecoderRaw &decoder, bool corruptCrc)
|
||||
{
|
||||
constexpr uint8_t wireBytes = 5U;
|
||||
primeLocked(decoder, IR_MSG_BACK, wireBytes);
|
||||
|
||||
decoder.dataBuffer[1] = 0x12U;
|
||||
decoder.dataBuffer[2] = 0x34U;
|
||||
decoder.dataBuffer[3] = crc8Local(decoder.dataBuffer, 0U, 3U, poly1);
|
||||
const uint8_t crcLow = crc8Local(decoder.dataBuffer, 0U, 4U, poly2);
|
||||
const uint8_t finalBit = static_cast<uint8_t>((crcLow & 1U) ^ (corruptCrc ? 1U : 0U));
|
||||
decoder.dataBuffer[4] = static_cast<uint8_t>(crcLow & 0xFEU);
|
||||
decoder.i_dataBuffer = wireBytes * bitPerByte - 1U;
|
||||
decoder.bufBitPos = static_cast<int16_t>(decoder.i_dataBuffer);
|
||||
decoder.nextControlBit = 0xFFFFU;
|
||||
decoder.isData = true;
|
||||
decoder.writeToBuffer(finalBit != 0U);
|
||||
}
|
||||
|
||||
void verifyCompleteFrameTerminalReasons()
|
||||
{
|
||||
IR_DecoderRaw good(1U, 42U, nullptr);
|
||||
finishBackFrame(good, false);
|
||||
const IR_RxTerminalInfo ok = good.rxLastTerminal();
|
||||
assert(ok.seq == 1U);
|
||||
assert(ok.reason == IR_RxTerminalReason::FrameOk);
|
||||
assert(ok.msgType == IR_MSG_BACK);
|
||||
assert(ok.hadLock);
|
||||
|
||||
IR_DecoderRaw bad(1U, 42U, nullptr);
|
||||
finishBackFrame(bad, true);
|
||||
const IR_RxTerminalInfo crc = bad.rxLastTerminal();
|
||||
assert(crc.seq == 1U);
|
||||
assert(crc.reason == IR_RxTerminalReason::FrameCrcError);
|
||||
assert(crc.msgType == IR_MSG_BACK);
|
||||
assert(crc.hadLock);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
verifyInitialSnapshot();
|
||||
verifyCandidateExpiresOnIdleTick();
|
||||
verifyCandidateIdleExpiryThroughPublicPipeline();
|
||||
verifyCoarseResetPublishesThroughBatchedPublicPipeline();
|
||||
verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive();
|
||||
verifyCandidateExpiryWaitsForPipelineDrain();
|
||||
verifyCandidateExpiryAcrossMicrosWrap();
|
||||
verifyCandidateTimeoutBoundary();
|
||||
verifyTimedOutCandidateRestartIsTerminal();
|
||||
verifyLockedTimeoutPublishesHeaderTypeOnce();
|
||||
verifyDecodeAbortPublishesTerminal();
|
||||
verifyCompleteFrameTerminalReasons();
|
||||
std::cout << "IR RX terminal tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
97
tests/test_timing_contract.cpp
Normal file
97
tests/test_timing_contract.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "IR_Encoder.h"
|
||||
#include "IR_DecoderRaw.h"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
// Link seams: these paths are not exercised by the pure host timing test.
|
||||
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; }
|
||||
void IR_DecoderRaw::refreshPairMuteState() {}
|
||||
void IR_Encoder::send_HIGH(bool) {}
|
||||
void IR_Encoder::send_LOW() {}
|
||||
void IR_Encoder::send_EMPTY(uint8_t) {}
|
||||
|
||||
namespace
|
||||
{
|
||||
static_assert(irproto::dataWireBytes(0U) == 7U, "empty DATA wire size changed");
|
||||
static_assert(irproto::dataWireBytes(3U) == 10U, "DATA wire size changed");
|
||||
static_assert(irproto::dataWireBytes(24U) == 31U, "maximum DATA wire size changed");
|
||||
static_assert(irproto::dataWireBytes(25U) == 0U, "oversized DATA must be rejected");
|
||||
static_assert(irproto::backWireBytes(1U) == 6U, "BACK wire size changed");
|
||||
static_assert(irproto::backWireBytes(26U) == 31U, "maximum BACK wire size changed");
|
||||
static_assert(irproto::backToWireBytes(24U) == 31U, "maximum BACK_TO wire size changed");
|
||||
|
||||
static_assert(irproto::wireLogicalTicks(6U) == 5472U, "6-byte tick count changed");
|
||||
static_assert(irproto::wireLogicalTicks(10U) == 8728U, "10-byte tick count changed");
|
||||
static_assert(irproto::wireLogicalTicks(31U) == 25822U, "31-byte tick count changed");
|
||||
static_assert(irproto::preambleAirtimeUsCeil() == 7737U, "preamble airtime changed");
|
||||
static_assert(irproto::trailingByteSyncAirtimeUsCeil() == 2922U,
|
||||
"trailing sync airtime changed");
|
||||
static_assert(irproto::wireAirtimeUsCeil(6U) == 72000U, "6-byte airtime changed");
|
||||
static_assert(irproto::wireAirtimeUsCeil(10U) == 114843U, "10-byte airtime changed");
|
||||
static_assert(irproto::wireAirtimeUsCeil(31U) == 339764U, "31-byte airtime changed");
|
||||
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
|
||||
static_assert(irproto::responseFrameGuardUs(6U) == 131100U, "response-frame guard changed");
|
||||
static_assert(irproto::completedFrameTerminalToNextPacketGuardUs(0U) == 44922U,
|
||||
"mandatory physical post-terminal quiet changed");
|
||||
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(0U) == 45U,
|
||||
"mandatory post-terminal guard rounding changed");
|
||||
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(60U) == 63U,
|
||||
"configured post-terminal guard changed");
|
||||
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(65535U) ==
|
||||
65538U,
|
||||
"large guard must not wrap uint16");
|
||||
static_assert(IR_DecoderRaw::rxMaxPackSize() == 31U, "RX max must be the wire max");
|
||||
|
||||
uint32_t sumLogicalTicks(const IrTxGateRun *runs, size_t count)
|
||||
{
|
||||
uint32_t total = 0U;
|
||||
for (size_t i = 0U; i < count; ++i)
|
||||
total += runs[i].lenTicks;
|
||||
return total;
|
||||
}
|
||||
|
||||
void verifyFormulaAgainstTxFsm()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
std::array<IrTxGateRun, 1024U> runs{};
|
||||
|
||||
for (uint8_t wireBytes = 1U; wireBytes <= irproto::kMaxWireFrameBytes; ++wireBytes)
|
||||
{
|
||||
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
|
||||
{
|
||||
for (uint8_t i = 0U; i < wireBytes; ++i)
|
||||
{
|
||||
frame[i] = pattern == 0U ? 0x00U
|
||||
: pattern == 1U ? 0xFFU
|
||||
: pattern == 2U ? static_cast<uint8_t>((i & 1U) ? 0x55U : 0xAAU)
|
||||
: static_cast<uint8_t>(i * 73U + 19U);
|
||||
}
|
||||
const size_t count = IR_Encoder::buildGateRuns(
|
||||
frame.data(), wireBytes, runs.data(), runs.size());
|
||||
assert(count != 0U);
|
||||
assert(sumLogicalTicks(runs.data(), count) == irproto::wireLogicalTicks(wireBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void verifyPublicSendTimeResults()
|
||||
{
|
||||
IR_Encoder encoder(1U, 42U, nullptr, false);
|
||||
uint8_t payload[26]{};
|
||||
|
||||
assert(encoder.testSendAccept(1U) == 72U);
|
||||
assert(encoder.testSendTime(1U, payload, 3U) == 115U);
|
||||
assert(encoder.testSendBack(payload, 26U) == 340U);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
verifyFormulaAgainstTxFsm();
|
||||
verifyPublicSendTimeResults();
|
||||
std::cout << "IR timing contract tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user