feat(protocol): derive wire size and airtime at compile time

This commit is contained in:
2026-08-28 14:53:55 +03:00
parent 96ffb91b97
commit 36f234739a
5 changed files with 298 additions and 21 deletions

View File

@ -25,7 +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 = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
class IR_Encoder; class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX

View File

@ -1183,26 +1183,10 @@ 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)
// and its preamble runs are preambToggle+1 logical ticks long. The old
// Время преамбулы: preambPulse * 2 фронта * bitTakts тактов // approximation omitted the per-byte sync and shortened the preamble.
uint32_t preambTime = preambPulse * 2 * bitTakts; return irproto::wireAirtimeMsCeil(packSize);
// Время данных: количество бит * 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

@ -264,6 +264,164 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
namespace irproto
{
/** Maximum complete frame length representable by the five header bits. */
constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
/** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxDataPayloadBytes
? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete non-addressed BACK frame size, or zero when it cannot fit. */
constexpr uint8_t backWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackPayloadBytes
? static_cast<uint8_t>(kBackFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete addressed BACK_TO frame size, or zero when it cannot fit. */
constexpr uint8_t backToWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackToPayloadBytes
? static_cast<uint8_t>(kBackToFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Minimum complete frame size for a known message type; zero means reserved/unknown. */
constexpr uint8_t minimumWireBytes(uint8_t msgType)
{
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT)
? kDataFrameOverheadBytes
: msgType == IR_MSG_BACK
? kBackFrameOverheadBytes
: (msgType == IR_MSG_BACK_TO || msgType == IR_MSG_REQUEST)
? kRequestFrameBytes
: msgType == IR_MSG_ACCEPT
? kAcceptFrameBytes
: 0U;
}
constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
{
return minimumWireBytes(msgType) != 0U &&
wireBytes >= minimumWireBytes(msgType) &&
wireBytes <= kMaxWireFrameBytes;
}
/*
* TX FSM timing contract.
*
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
*/
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
constexpr uint32_t kPreambleLogicalTicks =
static_cast<uint32_t>(preambPulse * 2U) * static_cast<uint32_t>(preambToggle + 1U);
constexpr uint32_t kEncodedBitLogicalTicks = static_cast<uint32_t>(bitTakts * 2U);
constexpr uint32_t kWireByteLogicalTicks =
static_cast<uint32_t>(bitPerByte + syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t wireLogicalTicks(uint8_t wireBytes)
{
return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes
? kPreambleLogicalTicks + static_cast<uint32_t>(wireBytes) * kWireByteLogicalTicks
: 0U;
}
constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks)
{
return logicalTicks == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(logicalTicks) * 1000000ULL +
static_cast<uint64_t>(kTxLogicalClockHz) - 1ULL) /
static_cast<uint64_t>(kTxLogicalClockHz));
}
constexpr uint32_t preambleAirtimeUsCeil()
{
return logicalTicksToUsCeil(kPreambleLogicalTicks);
}
/** 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 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(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;
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);

View File

@ -0,0 +1,50 @@
#pragma once
#include <cstddef>
#include <cstdint>
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 *digitalPinToPort(uint8_t) { return nullptr; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 0U; }
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() {}
struct ArduinoSerialStub
{
template <typename T> void print(const T &) {}
template <typename T> void println(const T &) {}
void println() {}
};
inline ArduinoSerialStub Serial;

View File

@ -0,0 +1,85 @@
#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::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");
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); // six-byte wire frame
assert(encoder.testSendTime(1U, payload, 3U) == 115U); // ten-byte wire frame
assert(encoder.testSendBack(payload, 26U) == 340U); // 31-byte wire frame
}
}
int main()
{
verifyFormulaAgainstTxFsm();
verifyPublicSendTimeResults();
std::cout << "IR timing contract tests: OK\n";
return 0;
}