mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 20:39:35 +00:00
Compare commits
11 Commits
archive/co
...
STM32
| Author | SHA1 | Date | |
|---|---|---|---|
| 5759658d42 | |||
| 1b408b0de6 | |||
| baac9fbf46 | |||
| 5b220dadd8 | |||
| b375aa169e | |||
| 883c0b00cf | |||
| d103d2a3ae | |||
| 86956bcf99 | |||
| 5b9f73ec7c | |||
| 8f45f6e214 | |||
| b1d7016147 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
bin/*
|
bin/*
|
||||||
tests/*.exe
|
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
log/*
|
log/*
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|||||||
@ -374,45 +374,15 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
|
|||||||
return busy;
|
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()
|
void IR_DecoderRaw::listenStart()
|
||||||
{
|
{
|
||||||
if (rxTimeoutPipelineBusy())
|
if (rxTimeoutPipelineBusy())
|
||||||
return;
|
return;
|
||||||
const uint32_t nowUs = micros();
|
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
||||||
if (isReciveRaw && ((nowUs - lastEdgeTime) > IR_timeout * 2U))
|
|
||||||
{
|
{
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceOnTimeoutOrAbort(true);
|
packTraceOnTimeoutOrAbort(true);
|
||||||
#endif
|
#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;
|
isReciveRaw = false;
|
||||||
firstRX();
|
firstRX();
|
||||||
}
|
}
|
||||||
@ -433,7 +403,7 @@ inline void IR_DecoderRaw::checkTimeout()
|
|||||||
#endif
|
#endif
|
||||||
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
||||||
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
||||||
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
|
noteRxEnd(RxEndReason::Timeout, micros());
|
||||||
isRecive = false; // приём завершён
|
isRecive = false; // приём завершён
|
||||||
msgTypeReceive = 0;
|
msgTypeReceive = 0;
|
||||||
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
||||||
@ -448,6 +418,62 @@ inline void IR_DecoderRaw::checkTimeout()
|
|||||||
}
|
}
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
|
void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs)
|
||||||
|
{
|
||||||
|
rxEnd.seq++;
|
||||||
|
rxEnd.reason = reason;
|
||||||
|
rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF;
|
||||||
|
rxEnd.packSize = (uint8_t)packSize;
|
||||||
|
rxEnd.tUs = tUs;
|
||||||
|
rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes)
|
||||||
|
? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks)
|
||||||
|
: 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_DecoderRaw::abortFrame(uint32_t tUs)
|
||||||
|
{
|
||||||
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
|
packTraceOnTimeoutOrAbort(false);
|
||||||
|
#endif
|
||||||
|
noteRxEnd(RxEndReason::Abort, tUs);
|
||||||
|
isRecive = false;
|
||||||
|
isReciveRaw = false;
|
||||||
|
msgTypeReceive = 0;
|
||||||
|
firstRX();
|
||||||
|
releasePreambleGuard(tUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется.
|
||||||
|
// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают,
|
||||||
|
// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика
|
||||||
|
// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга).
|
||||||
|
// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит
|
||||||
|
// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout.
|
||||||
|
// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов.
|
||||||
|
void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs)
|
||||||
|
{
|
||||||
|
prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_DecoderRaw::expirePreambleCandidate()
|
||||||
|
{
|
||||||
|
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
|
||||||
|
return;
|
||||||
|
if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT)
|
||||||
|
{
|
||||||
|
if (preambleGoodPeriods)
|
||||||
|
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros());
|
||||||
|
preambleResetToIdle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t IR_DecoderRaw::rxExpectedEndUs() const
|
||||||
|
{
|
||||||
|
if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes)
|
||||||
|
return 0;
|
||||||
|
return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize);
|
||||||
|
}
|
||||||
|
|
||||||
void IR_DecoderRaw::tick()
|
void IR_DecoderRaw::tick()
|
||||||
{
|
{
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
@ -502,17 +528,16 @@ void IR_DecoderRaw::tick()
|
|||||||
if (!processedFront)
|
if (!processedFront)
|
||||||
{
|
{
|
||||||
isSubBufferOverflow = false;
|
isSubBufferOverflow = false;
|
||||||
listenStart();
|
|
||||||
checkTimeout();
|
checkTimeout();
|
||||||
expirePreambleCandidateIfIdle(micros());
|
listenStart();
|
||||||
|
expirePreambleCandidate();
|
||||||
#if defined(IR_EDGE_TRACE)
|
#if defined(IR_EDGE_TRACE)
|
||||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
} // Если данных нет - ничего не делаем
|
} // Если данных нет - ничего не делаем
|
||||||
listenStart();
|
|
||||||
checkTimeout();
|
checkTimeout();
|
||||||
expirePreambleCandidateIfIdle(micros());
|
listenStart();
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
rxBriefFlushDeferredIsrLogs();
|
rxBriefFlushDeferredIsrLogs();
|
||||||
#endif
|
#endif
|
||||||
@ -811,19 +836,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
}
|
}
|
||||||
if (isBufferOverflow || isPreamb || isWrongPack)
|
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() сбрасывает буфер битов, преамбулу и
|
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
|
||||||
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
||||||
isRecive = false;
|
abortFrame(micros());
|
||||||
isReciveRaw = false;
|
|
||||||
msgTypeReceive = 0;
|
|
||||||
firstRX();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,6 +906,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
||||||
#endif
|
#endif
|
||||||
|
abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -918,8 +935,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
|
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
|
||||||
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
|
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
|
||||||
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
||||||
if (packSize != 0 && packSize < msgBytes + crcBytes)
|
if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё
|
||||||
|
{
|
||||||
isWrongPack = true;
|
isWrongPack = true;
|
||||||
|
abortFrame(micros());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
|
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
|
||||||
@ -943,6 +964,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
preambleResetToIdle();
|
preambleResetToIdle();
|
||||||
msgTypeReceive = 0;
|
msgTypeReceive = 0;
|
||||||
isAvailable = crcCheck(packSize - crcBytes, crcValue);
|
isAvailable = crcCheck(packSize - crcBytes, crcValue);
|
||||||
|
noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros());
|
||||||
|
|
||||||
#ifdef BRUTEFORCE_CHECK
|
#ifdef BRUTEFORCE_CHECK
|
||||||
{
|
{
|
||||||
@ -981,9 +1003,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
noteRxTerminal(isAvailable ? IR_RxTerminalReason::FrameOk
|
|
||||||
: IR_RxTerminalReason::FrameCrcError,
|
|
||||||
currentRxMsgType(), true);
|
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
if (isAvailable)
|
if (isAvailable)
|
||||||
packTraceEmitEndOk(static_cast<uint8_t>(packSize));
|
packTraceEmitEndOk(static_cast<uint8_t>(packSize));
|
||||||
@ -1626,7 +1645,6 @@ void IR_DecoderRaw::preambleResetToIdle()
|
|||||||
{
|
{
|
||||||
preambleState = PreambleState::Idle;
|
preambleState = PreambleState::Idle;
|
||||||
preambleGoodPeriods = 0;
|
preambleGoodPeriods = 0;
|
||||||
preambleWasObservable = false;
|
|
||||||
preambleMeanPeriod = 0;
|
preambleMeanPeriod = 0;
|
||||||
preambleCandidateLastEdgeTime = 0;
|
preambleCandidateLastEdgeTime = 0;
|
||||||
preambleCandidateFirstRiseTime = 0;
|
preambleCandidateFirstRiseTime = 0;
|
||||||
@ -1641,10 +1659,6 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
|||||||
{
|
{
|
||||||
preambleState = PreambleState::Candidate;
|
preambleState = PreambleState::Candidate;
|
||||||
preambleGoodPeriods = 0;
|
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;
|
preambleMeanPeriod = 0;
|
||||||
preambleCandidateLastEdgeTime = front.time;
|
preambleCandidateLastEdgeTime = front.time;
|
||||||
preambleCandidateFirstRiseTime = front.time;
|
preambleCandidateFirstRiseTime = front.time;
|
||||||
@ -1655,24 +1669,6 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
|||||||
isReciveRaw = false;
|
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)
|
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||||
{
|
{
|
||||||
const uint32_t longSilence = IR_timeout * 2U;
|
const uint32_t longSilence = IR_timeout * 2U;
|
||||||
@ -1699,10 +1695,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
|
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
|
||||||
{
|
{
|
||||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
|
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
|
||||||
if (preambleWasObservable)
|
|
||||||
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
|
|
||||||
preambleStartCandidate(front);
|
preambleStartCandidate(front);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preambleCandidateLastEdgeTime = front.time;
|
preambleCandidateLastEdgeTime = front.time;
|
||||||
@ -1720,20 +1713,15 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
preambleCandidateFirstRiseTime = front.time;
|
preambleCandidateFirstRiseTime = front.time;
|
||||||
if (!preambleRisePeriodCoarseOk(period))
|
if (!preambleRisePeriodCoarseOk(period))
|
||||||
{
|
{
|
||||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods,
|
|
||||||
irClampU16(period), front.time);
|
|
||||||
preambleGoodPeriods = 0;
|
preambleGoodPeriods = 0;
|
||||||
preambleMeanPeriod = 0;
|
preambleMeanPeriod = 0;
|
||||||
// Keep preambleWasObservable sticky: this edge proves the medium is
|
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
|
||||||
// still active, but not that a possible physical frame has ended.
|
|
||||||
// Only silence timeout or a real locked terminal releases it.
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preambleGoodPeriods == 0)
|
if (preambleGoodPeriods == 0)
|
||||||
{
|
{
|
||||||
preambleGoodPeriods = 1;
|
preambleGoodPeriods = 1;
|
||||||
preambleWasObservable = true;
|
|
||||||
preambleMeanPeriod = (uint16_t)period;
|
preambleMeanPeriod = (uint16_t)period;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -1750,7 +1738,6 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
{
|
{
|
||||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
|
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
|
||||||
preambleGoodPeriods = 1;
|
preambleGoodPeriods = 1;
|
||||||
preambleWasObservable = true;
|
|
||||||
preambleMeanPeriod = (uint16_t)period;
|
preambleMeanPeriod = (uint16_t)period;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1781,6 +1768,8 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
isRecive = true;
|
isRecive = true;
|
||||||
isReciveRaw = true;
|
isReciveRaw = true;
|
||||||
risePeriod = preambleMeanPeriod;
|
risePeriod = preambleMeanPeriod;
|
||||||
|
rxLockSeqCnt++;
|
||||||
|
rxLockTimeUsVal = front.time;
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceResetFrame();
|
packTraceResetFrame();
|
||||||
packTraceOpen = true;
|
packTraceOpen = true;
|
||||||
|
|||||||
@ -25,30 +25,7 @@ class Print;
|
|||||||
#define riseTimeMin (riseTime - riseTolerance)
|
#define riseTimeMin (riseTime - riseTolerance)
|
||||||
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
||||||
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
|
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
|
||||||
constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
|
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000;
|
||||||
|
|
||||||
/** 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_Encoder;
|
||||||
class IR_DecoderRaw : virtual public IR_FOX
|
class IR_DecoderRaw : virtual public IR_FOX
|
||||||
@ -75,28 +52,14 @@ public:
|
|||||||
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||||
bool isSubOverflow();
|
bool isSubOverflow();
|
||||||
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
||||||
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
|
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется
|
||||||
// Candidate после первого post-silence rise. Даже сильно искажённый ответ может не дать ни
|
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного
|
||||||
// одного coarse-valid периода, поэтому Candidate остаётся активным до lock/terminal либо
|
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего:
|
||||||
// доказанной тишины по candidate timeout. Для гейта заднего: «не стрелять, пока на линии
|
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const.
|
||||||
// идёт/формируется потенциальный кадр (напр. ответ точки)». Аддитивно, const.
|
|
||||||
inline bool rxLineActive() const {
|
inline bool rxLineActive() const {
|
||||||
return isRecive ||
|
return isRecive ||
|
||||||
(preambleState == PreambleState::Candidate && preambleWasObservable);
|
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 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
|
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
|
||||||
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
|
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
|
||||||
@ -104,9 +67,25 @@ public:
|
|||||||
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
|
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
|
||||||
}
|
}
|
||||||
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
|
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
|
||||||
static constexpr uint16_t rxMaxPackSize() {
|
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; }
|
||||||
return static_cast<uint16_t>(irproto::kMaxWireFrameBytes);
|
|
||||||
}
|
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
|
||||||
|
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
|
||||||
|
struct RxEndInfo {
|
||||||
|
uint16_t seq = 0; // номер завершения (растёт на каждом терминале)
|
||||||
|
RxEndReason reason = RxEndReason::None;
|
||||||
|
uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят
|
||||||
|
uint8_t packSize = 0; // объявленная длина (0 = неизвестна)
|
||||||
|
uint32_t tUs = 0; // micros() терминала
|
||||||
|
uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен)
|
||||||
|
};
|
||||||
|
uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы
|
||||||
|
uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время)
|
||||||
|
/// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт.
|
||||||
|
uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; }
|
||||||
|
const RxEndInfo &rxLastEnd() const { return rxEnd; }
|
||||||
|
/// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна.
|
||||||
|
uint32_t rxExpectedEndUs() const;
|
||||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
||||||
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
||||||
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
||||||
@ -166,7 +145,6 @@ private:
|
|||||||
volatile bool isSubBufferOverflow = false;
|
volatile bool isSubBufferOverflow = false;
|
||||||
bool isBufferOverflow = false; // Флаг переполнения буффера данных
|
bool isBufferOverflow = false; // Флаг переполнения буффера данных
|
||||||
bool isWrongPack = false; // Флаг битого пакета
|
bool isWrongPack = false; // Флаг битого пакета
|
||||||
IR_RxTerminalInfo rxTerminalInfo;
|
|
||||||
|
|
||||||
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
|
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
|
||||||
|
|
||||||
@ -203,11 +181,14 @@ private:
|
|||||||
Locked = 2
|
Locked = 2
|
||||||
};
|
};
|
||||||
PreambleState preambleState = PreambleState::Idle;
|
PreambleState preambleState = PreambleState::Idle;
|
||||||
|
uint16_t rxLockSeqCnt = 0;
|
||||||
|
uint32_t rxLockTimeUsVal = 0;
|
||||||
|
RxEndInfo rxEnd;
|
||||||
|
void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец
|
||||||
|
void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow)
|
||||||
|
void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
|
||||||
|
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
|
||||||
uint8_t preambleGoodPeriods = 0;
|
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;
|
uint16_t preambleMeanPeriod = 0;
|
||||||
uint32_t preambleCandidateLastEdgeTime = 0;
|
uint32_t preambleCandidateLastEdgeTime = 0;
|
||||||
uint32_t preambleCandidateFirstRiseTime = 0;
|
uint32_t preambleCandidateFirstRiseTime = 0;
|
||||||
@ -276,9 +257,6 @@ bool isReciveRaw = false;
|
|||||||
void preambleResetToIdle();
|
void preambleResetToIdle();
|
||||||
void preambleStartCandidate(const FrontStorage &front);
|
void preambleStartCandidate(const FrontStorage &front);
|
||||||
bool preambleProcessEdge(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, пришедшим в пакете
|
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
|
||||||
/// @param len Длина в байтах проверяемых данных
|
/// @param len Длина в байтах проверяемых данных
|
||||||
|
|||||||
@ -352,6 +352,29 @@ bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool
|
|||||||
return txAdvanceAfterOutput(st, sendBufferLocal);
|
return txAdvanceAfterOutput(st, sendBufferLocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обход кадра по границам ранов: между границами автомат выдаёт st.toggleCounter+1 тактов
|
||||||
|
// уровня st.state (txAdvanceAfterOutput считает toggleCounter до нуля, затем txAdvanceBoundary
|
||||||
|
// открывает следующий ран). Даёт ту же последовательность тактов, что потиковый обход, но за
|
||||||
|
// число шагов = число ранов (пинг: ~230 вместо ~8700 тактов — на 12 МГц это ~30 мс перед стартом DMA).
|
||||||
|
template <typename Emit>
|
||||||
|
bool IR_Encoder::txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit)
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
const bool gate = st.state;
|
||||||
|
const uint32_t lenTicks = (uint32_t)st.toggleCounter + 1U;
|
||||||
|
if (!emit(gate, lenTicks))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
st.toggleCounter = 0;
|
||||||
|
if (!txAdvanceBoundary(st, sendBufferLocal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const
|
void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const
|
||||||
{
|
{
|
||||||
st.sendLen = sendLen;
|
st.sendLen = sendLen;
|
||||||
@ -476,28 +499,27 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
|
|||||||
st.currentBitSequence = bitHigh;
|
st.currentBitSequence = bitHigh;
|
||||||
|
|
||||||
size_t runCount = 0;
|
size_t runCount = 0;
|
||||||
bool isActive = true;
|
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool {
|
||||||
while (isActive)
|
|
||||||
{
|
|
||||||
bool gate = false;
|
|
||||||
isActive = txEmitTick(st, sendBufferLocal, gate);
|
|
||||||
|
|
||||||
if (runCount > 0 && outRuns[runCount - 1].gate == gate)
|
if (runCount > 0 && outRuns[runCount - 1].gate == gate)
|
||||||
{
|
{
|
||||||
outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + 1U);
|
const uint32_t merged = (uint32_t)outRuns[runCount - 1].lenTicks + lenTicks;
|
||||||
}
|
if (merged > 65535U)
|
||||||
else
|
|
||||||
{
|
|
||||||
if (runCount >= maxRuns)
|
|
||||||
{
|
{
|
||||||
return 0;
|
return false;
|
||||||
}
|
}
|
||||||
outRuns[runCount].gate = gate;
|
outRuns[runCount - 1].lenTicks = (uint16_t)merged;
|
||||||
outRuns[runCount].lenTicks = 1U;
|
return true;
|
||||||
runCount++;
|
|
||||||
}
|
}
|
||||||
}
|
if (runCount >= maxRuns || lenTicks > 65535U)
|
||||||
return runCount;
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outRuns[runCount].gate = gate;
|
||||||
|
outRuns[runCount].lenTicks = (uint16_t)lenTicks;
|
||||||
|
runCount++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return ok ? runCount : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply)
|
size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply)
|
||||||
@ -564,40 +586,29 @@ size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_
|
|||||||
bool currentGate = false;
|
bool currentGate = false;
|
||||||
uint32_t currentLogicalLen = 0;
|
uint32_t currentLogicalLen = 0;
|
||||||
bool havePendingRun = false;
|
bool havePendingRun = false;
|
||||||
bool isActive = true;
|
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool {
|
||||||
while (isActive)
|
if (havePendingRun && currentGate == gate)
|
||||||
{
|
|
||||||
bool gate = false;
|
|
||||||
isActive = txEmitTick(st, sendBufferLocal, gate);
|
|
||||||
|
|
||||||
if (!havePendingRun)
|
|
||||||
{
|
{
|
||||||
currentGate = gate;
|
currentLogicalLen += lenTicks;
|
||||||
currentLogicalLen = 1U;
|
return true;
|
||||||
havePendingRun = true;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
||||||
if (currentGate == gate)
|
|
||||||
{
|
{
|
||||||
currentLogicalLen++;
|
return false;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGate = gate;
|
currentGate = gate;
|
||||||
currentLogicalLen = 1U;
|
currentLogicalLen = lenTicks;
|
||||||
|
havePendingRun = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCount;
|
return runCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1183,9 +1194,8 @@ uint8_t IR_Encoder::bitLow[2] = {
|
|||||||
|
|
||||||
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
|
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
|
||||||
{
|
{
|
||||||
// The TX FSM emits syncBits after every wire byte (including the last)
|
// Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс.
|
||||||
// and its preamble runs are preambToggle+1 logical ticks long.
|
return (irFrameAirtimeUs(packSize) + 999U) / 1000U;
|
||||||
return irproto::wireAirtimeMsCeil(packSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции для тестирования времени отправки без фактической отправки
|
// Функции для тестирования времени отправки без фактической отправки
|
||||||
|
|||||||
@ -214,6 +214,8 @@ private:
|
|||||||
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||||
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||||
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
||||||
|
template <typename Emit>
|
||||||
|
static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit);
|
||||||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||||
void storeTxFsmToMembers(const TxFsmState &st);
|
void storeTxFsmToMembers(const TxFsmState &st);
|
||||||
bool shouldUseBufferedIsr() const;
|
bool shouldUseBufferedIsr() const;
|
||||||
|
|||||||
218
IR_config.h
218
IR_config.h
@ -231,9 +231,11 @@ typedef uint16_t crc_t;
|
|||||||
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
||||||
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
||||||
#endif
|
#endif
|
||||||
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
|
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта
|
||||||
|
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
|
||||||
|
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
|
||||||
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
||||||
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
|
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define preambPulse 3
|
#define preambPulse 3
|
||||||
@ -264,193 +266,31 @@ typedef uint16_t crc_t;
|
|||||||
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
||||||
#define tolerance 300U
|
#define tolerance 300U
|
||||||
|
|
||||||
namespace irproto
|
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ----
|
||||||
{
|
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по
|
||||||
/** Maximum complete frame length representable by the five header bits. */
|
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта.
|
||||||
constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
|
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U);
|
||||||
|
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
|
||||||
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U);
|
||||||
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
|
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U;
|
||||||
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks;
|
||||||
constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
|
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); }
|
||||||
constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс.
|
||||||
|
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
|
||||||
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
|
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс.
|
||||||
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
|
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
|
||||||
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
|
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс.
|
||||||
|
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
|
||||||
/** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
|
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс.
|
||||||
constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
|
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks);
|
||||||
{
|
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём.
|
||||||
return payloadBytes <= kMaxDataPayloadBytes
|
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U);
|
||||||
? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
|
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs;
|
||||||
: 0U;
|
/// Протокольный максимум длины кадра (5-битное поле длины).
|
||||||
}
|
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO;
|
||||||
|
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
|
||||||
/** Complete non-addressed BACK frame size, or zero when it cannot fit. */
|
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); }
|
||||||
constexpr uint8_t backWireBytes(uint8_t payloadBytes)
|
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); }
|
||||||
{
|
|
||||||
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_Time = bitTime;
|
||||||
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
||||||
|
|||||||
@ -65,13 +65,63 @@ public:
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Заполнение по ранам, а не по словам: тот же поток слов, что даёт nextWord() (состояние
|
||||||
|
// runIndex_/ticksLeftInRun_/slotInPeriod_ переносится через границы порций), но пауза (gate=0)
|
||||||
|
// пишется одним циклом записи, а несущая — копией готового шаблона периода. На 12 МГц это
|
||||||
|
// ~1 мс на 4096 слов вместо ~16 (пословный автомат) — и в предзаполнении перед стартом DMA,
|
||||||
|
// и в ISR-дозаполнении половин буфера во время передачи.
|
||||||
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
|
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||||
if (dst == nullptr || count == 0) {
|
if (dst == nullptr || count == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
do {
|
while (count != 0) {
|
||||||
*dst++ = nextWord();
|
if (runIndex_ >= runCount) {
|
||||||
} while (--count != 0);
|
do { *dst++ = resetWord; } while (--count != 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool gate = runs[runIndex_].gate;
|
||||||
|
uint16_t n = ticksLeftInRun_; // слов до конца текущего рана
|
||||||
|
if (n == 0) n = 1; // ран нулевой длины: nextWord() выдаёт одно слово и переходит дальше
|
||||||
|
if (n > count) n = count;
|
||||||
|
if (!gate) {
|
||||||
|
slotInPeriod_ = 0;
|
||||||
|
uint16_t k = n;
|
||||||
|
do { *dst++ = resetWord; } while (--k != 0);
|
||||||
|
} else {
|
||||||
|
uint16_t k = n;
|
||||||
|
// добить текущий период до слота 0 (если ран начался посреди периода на границе порции)
|
||||||
|
while (k != 0 && slotInPeriod_ != 0) {
|
||||||
|
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
||||||
|
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
|
||||||
|
k--;
|
||||||
|
}
|
||||||
|
// целые периоды: powerN_ слов setWord, остальные resetWord
|
||||||
|
while (k >= multiply_) {
|
||||||
|
uint16_t i = 0;
|
||||||
|
for (; i < powerN_; ++i) *dst++ = setWord;
|
||||||
|
for (; i < multiply_; ++i) *dst++ = resetWord;
|
||||||
|
k = (uint16_t)(k - multiply_);
|
||||||
|
}
|
||||||
|
// хвост неполного периода
|
||||||
|
while (k != 0) {
|
||||||
|
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
||||||
|
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
|
||||||
|
k--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count = (uint16_t)(count - n);
|
||||||
|
if (ticksLeftInRun_ > n) {
|
||||||
|
ticksLeftInRun_ = (uint16_t)(ticksLeftInRun_ - n);
|
||||||
|
} else {
|
||||||
|
ticksLeftInRun_ = 0;
|
||||||
|
}
|
||||||
|
if (ticksLeftInRun_ == 0) {
|
||||||
|
runIndex_++;
|
||||||
|
if (runIndex_ < runCount) {
|
||||||
|
ticksLeftInRun_ = runs[runIndex_].lenTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
#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;
|
|
||||||
@ -1,386 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
#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