11 Commits

Author SHA1 Message Date
5759658d42 Merge pull request #12 from Show-maket/claude/rx-terminal-api
RX по состоянию, airtime формулами, DMA по ранам, гвард декодера после обрыва (для Car v4.3.11)
2026-09-09 17:02:16 +03:00
1b408b0de6 Merge branch 'STM32' into claude/rx-terminal-api 2026-09-09 17:01:57 +03:00
baac9fbf46 fix(rx): после обрыва кадра не требовать 30 мс тишины перед новым кандидатом преамбулы (releasePreambleGuard)
prevRise (последний декодированный фронт) не сбрасывался при abortFrame, а отброшенные гвардом фронты его не двигают —
валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика. Стенд 09.09: КУ теряла
пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга (V-серия 75–83 %, после правки 100 %).
Хост-тест (настоящий IR_DecoderRaw.cpp + шим Arduino): после abort кадр через 5…28 мс — терялся, теперь принимается;
чистый конец кадра не тронут (мин. пауза 26.25 мс), хвост оборванного кадра ложных захватов не даёт, 200/200 валидных
среди мусора, стресс 166→176/200.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-09 16:09:55 +03:00
5b220dadd8 Revert "refactor(config): IR_ResponseDelay перенесён в IR_config.h"
This reverts commit b375aa169e.
2026-09-08 14:13:14 +03:00
b375aa169e refactor(config): IR_ResponseDelay перенесён в IR_config.h
Зависит только от конфигурационных констант (bitTime, tolerance, syncBits) — это таймаут байта
(irRxByteTimeoutUs) × 2.7735 = 42 мс; значение не изменилось. Потребителям достаточно IR_config.h.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-08 14:09:38 +03:00
883c0b00cf perf(tx): заполнение DMA-буфера по ранам вместо пословного автомата
IrTxBsrrWave::fill: пауза (gate=0) — один цикл записи resetWord, несущая — копия шаблона периода
(powerN слов setWord + остальные resetWord); состояние переносится через границы порций как у
nextWord(). На 12 МГц предзаполнение 4096 слов перед стартом DMA и ISR-дозаполнение половин
(2048 слов каждые ~8.9 мс кадра — было ~88% CPU) укорачиваются в разы: старт несущей после
вызова send 15.7 → ~5 мс (с построением ранов). Эквивалентность проверена на хосте: 17856
случаев (len 1..31, multiply 2..8, powerN 1..multiply/2, порции 2048/случайные/по 1 слову),
0 расхождений на 719 млн слов.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:56:23 +03:00
d103d2a3ae perf(tx): таблица ранов кадра строится по границам ранов, а не по тактам несущей
buildGateRuns/buildPhysicalGateRuns крутили автомат передачи по одному такту (пинг 10 байт =
8728 тактов) — на 12 МГц это ~30 мс между вызовом send и стартом DMA, на каждой передаче
(задний ~20 мс). Обход по границам (txWalkRuns: ран = toggleCounter+1 тактов уровня state)
даёт ту же последовательность за число шагов = число ранов (225). Эквивалентность проверена на
хосте: 12480 случаев, len 1..38, множитель 2..8, логические и физические раны совпадают
бит-в-бит (len=39 — чтение за буфером в sync-фазе автомата у обеих версий, на эфире ≤31).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:33:20 +03:00
86956bcf99 fix(rx): таймаут кандидата преамбулы 3×→1× IR_timeout
Кандидат без фронтов дольше таймаута байта преамбулой быть не может. При 3× (45 мс)
rxLineActive() держал линию «занятой» после паразитных фронтов (засветка своим
дальномером) и откладывал пинг машинки на ~30 мс сверх заданного гэпа.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:07:22 +03:00
5b9f73ec7c feat(config): irRxByteTimeoutUs/irRxAbortSilenceUs — таймаут байта и тишина обрыва приёма формулой
Константы использует Car (IrCycle/Executer) вместо макроса IR_timeout,
который завязан на член класса декодера.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 16:30:44 +03:00
8f45f6e214 feat(rx): наблюдаемость приёма по состоянию + airtime кадра формулами
- IR_config.h: irFrameAirtimeUs/irFrameDecodeEndUs/irLockToDecodeEndUs/irLockLatencyUs,
  irMaxPackSize (31), irDataPackSize/irBackPackSize — все из констант FSM передатчика
  (преамбула 6×98 тактов, байт 11 бит × 74 такта, такт = полпериода несущей).
- IR_Encoder::calculateSendTime по той же формуле (раньше синхробиты считались один раз
  на кадр → занижение 21-26%, потребители компенсировали +30%).
- IR_DecoderRaw: rxLockSeq/rxLockTimeUs (лок преамбулы), rxMsgType, rxExpectedEndUs
  (по объявленной длине), rxLastEnd {seq, reason Ok/Crc/Timeout/Abort, msgType, packSize,
  tUs, expectedEndUs}. Терминалы: конец кадра, таймаут тишины, немедленный abort при
  sync-ошибке / длине <3 (в т.ч. 0) / переполнении — битый кадр больше не держит
  isReciving до конца чужой передачи + 30 мс.
- Порядок в tick: checkTimeout до listenStart (TIMEOUT-лог с реальной длиной), истечение
  кандидата преамбулы без фронтов, ложный PREAMB-инкремент на старте кандидата убран.
- rxMaxPackSize() = протокольные 31 (было 38 = размер буфера).

Совместимость: только добавления; существующие сигнатуры не тронуты. Собрано для G4 (Car)
и F4 (КУ).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 12:22:22 +03:00
b1d7016147 analyzer 2026-06-02 11:19:32 +03:00
6 changed files with 239 additions and 74 deletions

View File

@ -403,6 +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());
noteRxEnd(RxEndReason::Timeout, micros());
isRecive = false; // приём завершён isRecive = false; // приём завершён
msgTypeReceive = 0; msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -417,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
@ -471,15 +528,16 @@ void IR_DecoderRaw::tick()
if (!processedFront) if (!processedFront)
{ {
isSubBufferOverflow = false; isSubBufferOverflow = false;
listenStart();
checkTimeout(); checkTimeout();
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();
listenStart();
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs(); rxBriefFlushDeferredIsrLogs();
#endif #endif
@ -780,10 +838,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
{ {
// Как 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;
} }
@ -851,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;
} }
} }
} }
@ -878,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): выставляем сразу после первого байта, ДО проверки «Конец».
@ -903,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
{ {
@ -1622,7 +1684,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if (!isReciveRaw && front.dir && if (!isReciveRaw && front.dir &&
((prevRise == 0U && front.time > longSilence) || ((prevRise == 0U && front.time > longSilence) ||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence))) (prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
{
preambleStartCandidate(front); preambleStartCandidate(front);
return true;
}
} }
if (preambleState == PreambleState::Candidate) if (preambleState == PreambleState::Candidate)
@ -1703,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;

View File

@ -67,7 +67,25 @@ public:
return (isRecive && packSize && !isWrongPack) ? packSize : 0; return (isRecive && packSize && !isWrongPack) ? packSize : 0;
} }
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)dataByteSizeMax; } static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; }
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
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; }
@ -163,6 +181,13 @@ 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;
uint16_t preambleMeanPeriod = 0; uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0; uint32_t preambleCandidateLastEdgeTime = 0;

View File

@ -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,26 +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
{ {
// Расчет времени отправки пакета в миллисекундах // Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс.
return (irFrameAirtimeUs(packSize) + 999U) / 1000U;
// Время преамбулы: 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;
} }
// Функции для тестирования времени отправки без фактической отправки // Функции для тестирования времени отправки без фактической отправки

View File

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

View File

@ -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,6 +266,32 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ----
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта.
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U);
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U;
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks;
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); }
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс.
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс.
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс.
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс.
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks);
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём.
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U);
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs;
/// Протокольный максимум длины кадра (5-битное поле длины).
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO;
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); }
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); }
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;
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);

View File

@ -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: