diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 10e440b32..b64af91c6 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -26,7 +26,7 @@ /// i.e. If not, assume a 100% duty cycle. Ignore attempts to change the /// duty cycle etc. IRsend::IRsend(uint16_t IRsendPin, bool inverted, bool use_modulation) - : IRpin(IRsendPin), periodOffset(kPeriodOffset) { + : IRpin(IRsendPin) { if (inverted) { outputOn = LOW; outputOff = HIGH; @@ -68,15 +68,12 @@ void IRsend::ledOn() { /// @param[in] use_offset Should we use the calculated offset or not? /// @return nr. of uSeconds. /// @note (T = 1/f) -uint32_t IRsend::calcUSecPeriod(uint32_t hz, bool use_offset) { +uint32_t IRsend::calcUSecPeriod(uint32_t hz) { if (hz == 0) hz = 1; // Avoid Zero hz. Divide by Zero is nasty. uint32_t period = (1000000UL + hz / 2) / hz; // The equiv of round(1000000/hz). - // Apply the offset and ensure we don't result in a <= 0 value. - if (use_offset) - return std::max((uint32_t)1, period + periodOffset); - else - return std::max((uint32_t)1, period); + // Ensure we don't result in a <= 0 value. + return std::max((uint32_t)1, period); } /// Set the output frequency modulation and duty cycle. @@ -101,11 +98,34 @@ void IRsend::enableIROut(uint32_t freq, uint8_t duty) { #ifdef UNIT_TEST _freq_unittest = freq; #endif // UNIT_TEST + +#ifndef UNIT_TEST + _fractionalBits = 14; + + // Maximum signed value that fits. + uint32_t maxValue = 0x7FFF >> _fractionalBits; + uint32_t period = calcUSecPeriod(freq); + + // Decrement the number of fractional bits until the period fits. + while (maxValue < period) + { + --_fractionalBits; + maxValue = 0x7FFF >> _fractionalBits; + } + + uint32_t fixedPointPeriod = ((1000000ULL << _fractionalBits) + freq / 2) / freq; + + // Nr. of uSeconds the LED will be on per pulse. + onTimePeriod = (fixedPointPeriod * _dutycycle) / kDutyMax; + // Nr. of uSeconds the LED will be off per pulse. + offTimePeriod = fixedPointPeriod - onTimePeriod; +#else uint32_t period = calcUSecPeriod(freq); // Nr. of uSeconds the LED will be on per pulse. onTimePeriod = (period * _dutycycle) / kDutyMax; // Nr. of uSeconds the LED will be off per pulse. offTimePeriod = period - onTimePeriod; +#endif } #if ALLOW_DELAY_CALLS @@ -166,6 +186,58 @@ uint16_t IRsend::mark(uint16_t usec) { // Not simple, so do it assuming frequency modulation. uint16_t counter = 0; IRtimer usecTimer = IRtimer(); +#ifndef UNIT_TEST +#if SEND_BANG_OLUFSEN && ESP8266 && F_CPU < 160000000L + // Free running loop to attempt to get close to the 455 kHz required by Bang & Olufsen. + // Define BANG_OLUFSEN_CHECK_MODULATION temporarily to test frequency and time. + // Runs at ~300 kHz on an 80 MHz ESP8266. + // This is far from ideal but works if the transmitter is close enough. + uint32_t periodUInt = (onTimePeriod + offTimePeriod) >> _fractionalBits; + periodUInt = std::max(uint32_t(1), periodUInt); + if (periodUInt <= 5) { + uint32_t nextCheck = usec / periodUInt / 2; // Assume we can at least run for this number of periods. + for (;;) { // nextStop is not updated in this loop. + ledOn(); + ledOff(); + counter++; + if (counter >= nextCheck) { + uint32_t now = usecTimer.elapsed(); + int32_t timeLeft = usec - now; + if (timeLeft <= 1) { + return counter; + } + uint32_t periodsToEnd = counter * timeLeft / now; + // Check again when we are half way closer to the end. + nextCheck = (periodsToEnd >> 2) + counter; + } + } + } +#endif + + // Use absolute time for zero drift (but slightly uneven period). + // Using IRtimer.elapsed() instead of _delayMicroseconds is better for short period times. + // Maxed out at ~190 kHz on an 80 MHz ESP8266. + // Maxed out at ~460 kHz on ESP32. + uint32_t nextStop = 0; // Must be 32 bits to not overflow when usec is near max. + while ((nextStop >> _fractionalBits) < usec) { // Loop until we've met/exceeded our required time. + ledOn(); + nextStop += onTimePeriod; + uint32_t nextStopUInt = std::min(nextStop >> _fractionalBits, uint32_t(usec)); + while(usecTimer.elapsed() < nextStopUInt); + ledOff(); + counter++; + nextStop += offTimePeriod; + nextStopUInt = std::min(nextStop >> _fractionalBits, uint32_t(usec)); + uint32_t now = usecTimer.elapsed(); + int32_t delay = nextStopUInt - now; + if (delay > 0) { + while(usecTimer.elapsed() < nextStopUInt); + } else { + // This means we ran past nextStop and need to reset to actual time to avoid playing catch-up with a far too short period. + nextStop = (now << _fractionalBits) + (offTimePeriod >> 1); + } + } +#else // Cache the time taken so far. This saves us calling time, and we can be // assured that we can't have odd math problems. i.e. unsigned under/overflow. uint32_t elapsed = usecTimer.elapsed(); @@ -184,6 +256,7 @@ uint16_t IRsend::mark(uint16_t usec) { std::min(usec - elapsed - onTimePeriod, (uint32_t)offTimePeriod)); elapsed = usecTimer.elapsed(); // Update & recache the actual elapsed time. } +#endif return counter; } @@ -207,7 +280,7 @@ void IRsend::space(uint32_t time) { int8_t IRsend::calibrate(uint16_t hz) { if (hz < 1000) // Were we given kHz? Supports the old call usage. hz *= 1000; - periodOffset = 0; // Turn off any existing offset while we calibrate. + int8_t periodOffset = 0; enableIROut(hz); IRtimer usecTimer = IRtimer(); // Start a timer *just* before we do the call. uint16_t pulses = mark(UINT16_MAX); // Generate a PWM of 65,535 us. (Max.) diff --git a/src/IRsend.h b/src/IRsend.h index 38491372a..9275dc660 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -21,17 +21,6 @@ // Constants // Offset (in microseconds) to use in Period time calculations to account for // code excution time in producing the software PWM signal. -#if defined(ESP32) -// Calculated on a generic ESP-WROOM-32 board with v3.2-18 SDK @ 240MHz -const int8_t kPeriodOffset = -2; -#elif (defined(ESP8266) && F_CPU == 160000000L) // NOLINT(whitespace/parens) -// Calculated on an ESP8266 NodeMCU v2 board using: -// v2.6.0 with v2.5.2 ESP core @ 160MHz -const int8_t kPeriodOffset = -2; -#else // (defined(ESP8266) && F_CPU == 160000000L) -// Calculated on ESP8266 Wemos D1 mini using v2.4.1 with v2.4.0 ESP core @ 40MHz -const int8_t kPeriodOffset = -5; -#endif // (defined(ESP8266) && F_CPU == 160000000L) const uint8_t kDutyDefault = 50; // Percentage const uint8_t kDutyMax = 100; // Percentage // delayMicroseconds() is only accurate to 16383us. @@ -905,13 +894,13 @@ class IRsend { #else uint32_t _freq_unittest; #endif // UNIT_TEST - uint16_t onTimePeriod; - uint16_t offTimePeriod; + uint16_t onTimePeriod; // Fixed point. + uint16_t offTimePeriod; // Fixed point. + uint8_t _fractionalBits; // Number of fractional bits in on/offTimePeriod. uint16_t IRpin; - int8_t periodOffset; uint8_t _dutycycle; bool modulation; - uint32_t calcUSecPeriod(uint32_t hz, bool use_offset = true); + uint32_t calcUSecPeriod(uint32_t hz); #if SEND_SONY void _sendSony(const uint64_t data, const uint16_t nbits, const uint16_t repeat, const uint16_t freq); diff --git a/src/IRtimer.cpp b/src/IRtimer.cpp index e1f098b28..e5662aefc 100644 --- a/src/IRtimer.cpp +++ b/src/IRtimer.cpp @@ -31,10 +31,7 @@ uint32_t IRtimer::elapsed() { #else uint32_t now = _IRtimer_unittest_now; #endif - if (start <= now) // Check if the system timer has wrapped. - return now - start; // No wrap. - else - return UINT32_MAX - start + now; // Has wrapped. + return now - start; // Wrap safe. } /// Add time to the timer to simulate elapsed time. @@ -64,10 +61,7 @@ uint32_t TimerMs::elapsed() { #else uint32_t now = _TimerMs_unittest_now; #endif - if (start <= now) // Check if the system timer has wrapped. - return now - start; // No wrap. - else - return UINT32_MAX - start + now; // Has wrapped. + return now - start; // Wrap safe. } /// Add time to the timer to simulate elapsed time. diff --git a/src/ir_GlobalCache.cpp b/src/ir_GlobalCache.cpp index e8ebac4af..2f70deaf9 100644 --- a/src/ir_GlobalCache.cpp +++ b/src/ir_GlobalCache.cpp @@ -35,7 +35,7 @@ const uint8_t kGlobalCacheStartIndex = kGlobalCacheRptStartIndex + 1; void IRsend::sendGC(uint16_t buf[], uint16_t len) { uint16_t hz = buf[kGlobalCacheFreqIndex]; // GC frequency is in Hz. enableIROut(hz); - uint32_t periodic_time = calcUSecPeriod(hz, false); + uint32_t periodic_time = calcUSecPeriod(hz); uint8_t emits = std::min(buf[kGlobalCacheRptIndex], (uint16_t)kGlobalCacheMaxRepeat); // Repeat diff --git a/src/ir_Pronto.cpp b/src/ir_Pronto.cpp index 2d4ffa759..d1bda8b7f 100644 --- a/src/ir_Pronto.cpp +++ b/src/ir_Pronto.cpp @@ -72,7 +72,7 @@ void IRsend::sendPronto(uint16_t data[], uint16_t len, uint16_t repeat) { uint16_t seq_1_start = kProntoDataOffset; uint16_t seq_2_start = kProntoDataOffset + seq_1_len; - uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10, false); + uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10); // Normal (1st sequence) case. // Is there a first (normal) sequence to send? diff --git a/test/IRsend_test.cpp b/test/IRsend_test.cpp index 51fae7499..5bcf3c4d8 100644 --- a/test/IRsend_test.cpp +++ b/test/IRsend_test.cpp @@ -239,18 +239,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt38kHz) { irsend.reset(); irsend.enableIROut(38000, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs" - "[On]10usecs[Off]11usecs[On]10usecs[Off]6usecs", + "[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs" + "[On]13usecs[Off]9usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(38000, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs" - "[On]6usecs[Off]15usecs[On]6usecs[Off]10usecs", + "[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs" + "[On]8usecs[Off]14usecs", irsend.low_level_sequence); irsend.reset(); @@ -266,18 +266,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt36_7kHz) { irsend.reset(); irsend.enableIROut(36700, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs" - "[On]11usecs[Off]11usecs[On]11usecs[Off]1usecs", + "[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs" + "[On]13usecs[Off]6usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(36700, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs" - "[On]7usecs[Off]15usecs[On]7usecs[Off]5usecs", + "[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs" + "[On]8usecs[Off]11usecs", irsend.low_level_sequence); irsend.reset(); @@ -293,18 +293,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt40kHz) { irsend.reset(); irsend.enableIROut(40000, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs" - "[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs", + "[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs" + "[On]12usecs[Off]13usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(40000, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs" - "[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs", + "[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs" + "[On]8usecs[Off]17usecs", irsend.low_level_sequence); irsend.reset();