Skip to content

Commit

Permalink
Support higher modulation frequencies and get better precision withou…
Browse files Browse the repository at this point in the history
…t the need for calibration.
  • Loading branch information
danielwallner committed Feb 25, 2024
1 parent 48d7433 commit 3853364
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 51 deletions.
89 changes: 81 additions & 8 deletions src/IRsend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand All @@ -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.)
Expand Down
19 changes: 4 additions & 15 deletions src/IRsend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 2 additions & 8 deletions src/IRtimer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/ir_GlobalCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/ir_Pronto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
36 changes: 18 additions & 18 deletions test/IRsend_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down

0 comments on commit 3853364

Please sign in to comment.