Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 91 additions & 10 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 @@ -54,7 +54,7 @@ IRsend::IRsend(uint16_t IRsendPin, bool inverted, bool use_modulation)
/// @param[in] w1tc_mask mask for clearing GPIOs for a logical 1 output.
/// Required if some outputs are inverted.
IRsend::IRsend(bool use_modulation, int64_t w1ts_mask, uint64_t w1tc_mask) :
periodOffset(kPeriodOffset), _irPinMaskEnabled(true) {
_irPinMaskEnabled(true) {
IRpin = static_cast<int32_t>(w1ts_mask);
_w1ts_mask_upper = static_cast<int32_t>(w1ts_mask >> 32);
_w1tc_mask_lower = static_cast<int32_t>(w1tc_mask);
Expand Down Expand Up @@ -152,18 +152,14 @@ void IRsend::ledOn() {

/// Calculate the period for a given frequency.
/// @param[in] hz Frequency in Hz.
/// @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(static_cast<uint32_t>(1), period + periodOffset);
else
return std::max(static_cast<uint32_t>(1), period);
// Ensure we don't result in a <= 0 value.
return std::max(static_cast<uint32_t>(1), period);
}

/// Set the output frequency modulation and duty cycle.
Expand All @@ -188,11 +184,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 @@ -253,6 +272,67 @@ 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(static_cast<uint32_t>(1), periodUInt);
if (periodUInt <= 5) {
// Assume we can at least run for this number of periods.
uint32_t nextCheck = usec / periodUInt / 2;
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.
// Must be 32 bits to not overflow when usec is near max.
uint32_t nextStop = 0;
// Loop until we've met/exceeded our required time.
while ((nextStop >> _fractionalBits) < usec) {
ledOn();
nextStop += onTimePeriod;
uint32_t nextStopUInt = std::min(nextStop >>
_fractionalBits, static_cast<uint32_t>(usec));
while (usecTimer.elapsed() < nextStopUInt) {}
ledOff();
counter++;
nextStop += offTimePeriod;
nextStopUInt = std::min(nextStop >>
_fractionalBits, static_cast<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 @@ -273,6 +353,7 @@ uint16_t IRsend::mark(uint16_t usec) {
static_cast<uint32_t>(offTimePeriod)));
elapsed = usecTimer.elapsed(); // Update & recache the actual elapsed time.
}
#endif
return counter;
}

Expand All @@ -296,7 +377,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 @@ -22,17 +22,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 @@ -940,13 +929,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.
uint32_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],
static_cast<uint16_t>(kGlobalCacheMaxRepeat));
Expand Down
2 changes: 1 addition & 1 deletion src/ir_Pronto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ bool 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