diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index a574934..2130553 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -25,7 +25,7 @@ class Print; #define riseTimeMin (riseTime - riseTolerance) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) #define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 -constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; +constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs; class IR_Encoder; class IR_DecoderRaw : virtual public IR_FOX diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index c7c96c6..13356c2 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -1183,26 +1183,10 @@ uint8_t IR_Encoder::bitLow[2] = { uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const { - // Расчет времени отправки пакета в миллисекундах - - // Время преамбулы: preambPulse * 2 фронта * bitTakts тактов - uint32_t preambTime = preambPulse * 2 * bitTakts; - - // Время данных: количество бит * bitTakts тактов - uint32_t dataTime = packSize * 8 * bitTakts; - - // Время синхронизации: syncBits * 2 фронта * bitTakts тактов - uint32_t syncTime = syncBits * 2 * bitTakts; - - // Общее время в тактах - uint32_t totalTakts = preambTime + dataTime + syncTime; - - // Конвертируем в миллисекунды - // carrierPeriod - период несущей в микросекундах - // totalTakts * carrierPeriod / 1000 = время в миллисекундах - uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000; - - return sendTimeMs; + // The TX FSM emits syncBits after every wire byte (including the last) + // and its preamble runs are preambToggle+1 logical ticks long. The old + // approximation omitted the per-byte sync and shortened the preamble. + return irproto::wireAirtimeMsCeil(packSize); } // Функции для тестирования времени отправки без фактической отправки diff --git a/IR_config.h b/IR_config.h index 31f439e..dde58cb 100644 --- a/IR_config.h +++ b/IR_config.h @@ -264,6 +264,164 @@ typedef uint16_t crc_t; #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define tolerance 300U +namespace irproto +{ +/** Maximum complete frame length representable by the five header bits. */ +constexpr uint8_t kMaxWireFrameBytes = static_cast(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(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(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(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(carrierFrec) * 2U; +constexpr uint32_t kPreambleLogicalTicks = + static_cast(preambPulse * 2U) * static_cast(preambToggle + 1U); +constexpr uint32_t kEncodedBitLogicalTicks = static_cast(bitTakts * 2U); +constexpr uint32_t kWireByteLogicalTicks = + static_cast(bitPerByte + syncBits) * kEncodedBitLogicalTicks; + +constexpr uint32_t wireLogicalTicks(uint8_t wireBytes) +{ + return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes + ? kPreambleLogicalTicks + static_cast(wireBytes) * kWireByteLogicalTicks + : 0U; +} + +constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks) +{ + return logicalTicks == 0U + ? 0U + : static_cast( + (static_cast(logicalTicks) * 1000000ULL + + static_cast(kTxLogicalClockHz) - 1ULL) / + static_cast(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( + static_cast( + (static_cast(bitTime + tolerance) * + static_cast(bitPerByte + syncBits + 1U)) * + 2.7735) / + 1000U); +constexpr uint32_t kMandatoryInterPacketQuietUs = + static_cast(kMandatoryInterPacketQuietMs) * 1000U; + +constexpr uint16_t kDefaultTimingGuardPermille = 1150U; + +constexpr uint32_t addTimingGuardUs(uint32_t durationUs, + uint16_t marginPermille = kDefaultTimingGuardPermille) +{ + return marginPermille == 0U + ? 0U + : static_cast( + (static_cast(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_Takts = bitTakts * 2; constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); diff --git a/tests/arduino_stubs/Arduino.h b/tests/arduino_stubs/Arduino.h new file mode 100644 index 0000000..a9addcf --- /dev/null +++ b/tests/arduino_stubs/Arduino.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +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 void print(const T &) {} + template void println(const T &) {} + void println() {} +}; + +inline ArduinoSerialStub Serial; diff --git a/tests/test_timing_contract.cpp b/tests/test_timing_contract.cpp new file mode 100644 index 0000000..e5b32a7 --- /dev/null +++ b/tests/test_timing_contract.cpp @@ -0,0 +1,85 @@ +#include "IR_Encoder.h" +#include "IR_DecoderRaw.h" + +#include +#include +#include +#include + +// 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 frame{}; + std::array 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((i & 1U) ? 0x55U : 0xAAU) + : static_cast(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; +}