Skip to content

Commit 160b843

Browse files
zehnmdanielwallner
andauthored
refactor: support higher modulation frequencies and better precision (#4)
Support higher modulation frequencies and get better precision without the need for calibration. --------- Co-authored-by: Daniel Wallner <[email protected]>
1 parent bb1e5df commit 160b843

File tree

6 files changed

+117
-53
lines changed

6 files changed

+117
-53
lines changed

src/IRsend.cpp

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
/// i.e. If not, assume a 100% duty cycle. Ignore attempts to change the
2727
/// duty cycle etc.
2828
IRsend::IRsend(uint16_t IRsendPin, bool inverted, bool use_modulation)
29-
: IRpin(IRsendPin), periodOffset(kPeriodOffset) {
29+
: IRpin(IRsendPin) {
3030
if (inverted) {
3131
outputOn = LOW;
3232
outputOff = HIGH;
@@ -54,7 +54,7 @@ IRsend::IRsend(uint16_t IRsendPin, bool inverted, bool use_modulation)
5454
/// @param[in] w1tc_mask mask for clearing GPIOs for a logical 1 output.
5555
/// Required if some outputs are inverted.
5656
IRsend::IRsend(bool use_modulation, int64_t w1ts_mask, uint64_t w1tc_mask) :
57-
periodOffset(kPeriodOffset), _irPinMaskEnabled(true) {
57+
_irPinMaskEnabled(true) {
5858
IRpin = static_cast<int32_t>(w1ts_mask);
5959
_w1ts_mask_upper = static_cast<int32_t>(w1ts_mask >> 32);
6060
_w1tc_mask_lower = static_cast<int32_t>(w1tc_mask);
@@ -152,18 +152,14 @@ void IRsend::ledOn() {
152152

153153
/// Calculate the period for a given frequency.
154154
/// @param[in] hz Frequency in Hz.
155-
/// @param[in] use_offset Should we use the calculated offset or not?
156155
/// @return nr. of uSeconds.
157156
/// @note (T = 1/f)
158-
uint32_t IRsend::calcUSecPeriod(uint32_t hz, bool use_offset) {
157+
uint32_t IRsend::calcUSecPeriod(uint32_t hz) {
159158
if (hz == 0) hz = 1; // Avoid Zero hz. Divide by Zero is nasty.
160159
uint32_t period =
161160
(1000000UL + hz / 2) / hz; // The equiv of round(1000000/hz).
162-
// Apply the offset and ensure we don't result in a <= 0 value.
163-
if (use_offset)
164-
return std::max(static_cast<uint32_t>(1), period + periodOffset);
165-
else
166-
return std::max(static_cast<uint32_t>(1), period);
161+
// Ensure we don't result in a <= 0 value.
162+
return std::max(static_cast<uint32_t>(1), period);
167163
}
168164

169165
/// Set the output frequency modulation and duty cycle.
@@ -188,11 +184,34 @@ void IRsend::enableIROut(uint32_t freq, uint8_t duty) {
188184
#ifdef UNIT_TEST
189185
_freq_unittest = freq;
190186
#endif // UNIT_TEST
187+
188+
#ifndef UNIT_TEST
189+
_fractionalBits = 14;
190+
191+
// Maximum signed value that fits.
192+
uint32_t maxValue = 0x7FFF >> _fractionalBits;
193+
uint32_t period = calcUSecPeriod(freq);
194+
195+
// Decrement the number of fractional bits until the period fits.
196+
while (maxValue < period) {
197+
--_fractionalBits;
198+
maxValue = 0x7FFF >> _fractionalBits;
199+
}
200+
201+
uint32_t fixedPointPeriod = ((1000000ULL << _fractionalBits) + freq / 2)
202+
/ freq;
203+
204+
// Nr. of uSeconds the LED will be on per pulse.
205+
onTimePeriod = (fixedPointPeriod * _dutycycle) / kDutyMax;
206+
// Nr. of uSeconds the LED will be off per pulse.
207+
offTimePeriod = fixedPointPeriod - onTimePeriod;
208+
#else
191209
uint32_t period = calcUSecPeriod(freq);
192210
// Nr. of uSeconds the LED will be on per pulse.
193211
onTimePeriod = (period * _dutycycle) / kDutyMax;
194212
// Nr. of uSeconds the LED will be off per pulse.
195213
offTimePeriod = period - onTimePeriod;
214+
#endif
196215
}
197216

198217
#if ALLOW_DELAY_CALLS
@@ -253,6 +272,67 @@ uint16_t IRsend::mark(uint16_t usec) {
253272
// Not simple, so do it assuming frequency modulation.
254273
uint16_t counter = 0;
255274
IRtimer usecTimer = IRtimer();
275+
#ifndef UNIT_TEST
276+
#if SEND_BANG_OLUFSEN && ESP8266 && F_CPU < 160000000L
277+
// Free running loop to attempt to get close to the 455 kHz required by
278+
// Bang & Olufsen.
279+
// Define BANG_OLUFSEN_CHECK_MODULATION temporarily to test frequency and
280+
// time.
281+
// Runs at ~300 kHz on an 80 MHz ESP8266.
282+
// This is far from ideal but works if the transmitter is close enough.
283+
uint32_t periodUInt = (onTimePeriod + offTimePeriod) >> _fractionalBits;
284+
periodUInt = std::max(static_cast<uint32_t>(1), periodUInt);
285+
if (periodUInt <= 5) {
286+
// Assume we can at least run for this number of periods.
287+
uint32_t nextCheck = usec / periodUInt / 2;
288+
for (;;) { // nextStop is not updated in this loop.
289+
ledOn();
290+
ledOff();
291+
counter++;
292+
if (counter >= nextCheck) {
293+
uint32_t now = usecTimer.elapsed();
294+
int32_t timeLeft = usec - now;
295+
if (timeLeft <= 1) {
296+
return counter;
297+
}
298+
uint32_t periodsToEnd = counter * timeLeft / now;
299+
// Check again when we are half way closer to the end.
300+
nextCheck = (periodsToEnd >> 2) + counter;
301+
}
302+
}
303+
}
304+
#endif
305+
306+
// Use absolute time for zero drift (but slightly uneven period).
307+
// Using IRtimer.elapsed() instead of _delayMicroseconds is better for short
308+
// period times.
309+
// Maxed out at ~190 kHz on an 80 MHz ESP8266.
310+
// Maxed out at ~460 kHz on ESP32.
311+
// Must be 32 bits to not overflow when usec is near max.
312+
uint32_t nextStop = 0;
313+
// Loop until we've met/exceeded our required time.
314+
while ((nextStop >> _fractionalBits) < usec) {
315+
ledOn();
316+
nextStop += onTimePeriod;
317+
uint32_t nextStopUInt = std::min(nextStop >>
318+
_fractionalBits, static_cast<uint32_t>(usec));
319+
while (usecTimer.elapsed() < nextStopUInt) {}
320+
ledOff();
321+
counter++;
322+
nextStop += offTimePeriod;
323+
nextStopUInt = std::min(nextStop >>
324+
_fractionalBits, static_cast<uint32_t>(usec));
325+
uint32_t now = usecTimer.elapsed();
326+
int32_t delay = nextStopUInt - now;
327+
if (delay > 0) {
328+
while (usecTimer.elapsed() < nextStopUInt) {}
329+
} else {
330+
// This means we ran past nextStop and need to reset to actual time to
331+
// avoid playing catch-up with a far too short period.
332+
nextStop = (now << _fractionalBits) + (offTimePeriod >> 1);
333+
}
334+
}
335+
#else
256336
// Cache the time taken so far. This saves us calling time, and we can be
257337
// assured that we can't have odd math problems. i.e. unsigned under/overflow.
258338
uint32_t elapsed = usecTimer.elapsed();
@@ -273,6 +353,7 @@ uint16_t IRsend::mark(uint16_t usec) {
273353
static_cast<uint32_t>(offTimePeriod)));
274354
elapsed = usecTimer.elapsed(); // Update & recache the actual elapsed time.
275355
}
356+
#endif
276357
return counter;
277358
}
278359

@@ -296,7 +377,7 @@ void IRsend::space(uint32_t time) {
296377
int8_t IRsend::calibrate(uint16_t hz) {
297378
if (hz < 1000) // Were we given kHz? Supports the old call usage.
298379
hz *= 1000;
299-
periodOffset = 0; // Turn off any existing offset while we calibrate.
380+
int8_t periodOffset = 0;
300381
enableIROut(hz);
301382
IRtimer usecTimer = IRtimer(); // Start a timer *just* before we do the call.
302383
uint16_t pulses = mark(UINT16_MAX); // Generate a PWM of 65,535 us. (Max.)

src/IRsend.h

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,6 @@
2222
// Constants
2323
// Offset (in microseconds) to use in Period time calculations to account for
2424
// code excution time in producing the software PWM signal.
25-
#if defined(ESP32)
26-
// Calculated on a generic ESP-WROOM-32 board with v3.2-18 SDK @ 240MHz
27-
const int8_t kPeriodOffset = -2;
28-
#elif (defined(ESP8266) && F_CPU == 160000000L) // NOLINT(whitespace/parens)
29-
// Calculated on an ESP8266 NodeMCU v2 board using:
30-
// v2.6.0 with v2.5.2 ESP core @ 160MHz
31-
const int8_t kPeriodOffset = -2;
32-
#else // (defined(ESP8266) && F_CPU == 160000000L)
33-
// Calculated on ESP8266 Wemos D1 mini using v2.4.1 with v2.4.0 ESP core @ 40MHz
34-
const int8_t kPeriodOffset = -5;
35-
#endif // (defined(ESP8266) && F_CPU == 160000000L)
3625
const uint8_t kDutyDefault = 50; // Percentage
3726
const uint8_t kDutyMax = 100; // Percentage
3827
// delayMicroseconds() is only accurate to 16383us.
@@ -940,13 +929,13 @@ class IRsend {
940929
#else
941930
uint32_t _freq_unittest;
942931
#endif // UNIT_TEST
943-
uint16_t onTimePeriod;
944-
uint16_t offTimePeriod;
932+
uint16_t onTimePeriod; // Fixed point.
933+
uint16_t offTimePeriod; // Fixed point.
934+
uint8_t _fractionalBits; // Number of fractional bits in on/offTimePeriod.
945935
uint32_t IRpin;
946-
int8_t periodOffset;
947936
uint8_t _dutycycle;
948937
bool modulation;
949-
uint32_t calcUSecPeriod(uint32_t hz, bool use_offset = true);
938+
uint32_t calcUSecPeriod(uint32_t hz);
950939
#if SEND_SONY
951940
void _sendSony(const uint64_t data, const uint16_t nbits,
952941
const uint16_t repeat, const uint16_t freq);

src/IRtimer.cpp

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ uint32_t IRtimer::elapsed() {
3131
#else
3232
uint32_t now = _IRtimer_unittest_now;
3333
#endif
34-
if (start <= now) // Check if the system timer has wrapped.
35-
return now - start; // No wrap.
36-
else
37-
return UINT32_MAX - start + now; // Has wrapped.
34+
return now - start; // Wrap safe.
3835
}
3936

4037
/// Add time to the timer to simulate elapsed time.
@@ -64,10 +61,7 @@ uint32_t TimerMs::elapsed() {
6461
#else
6562
uint32_t now = _TimerMs_unittest_now;
6663
#endif
67-
if (start <= now) // Check if the system timer has wrapped.
68-
return now - start; // No wrap.
69-
else
70-
return UINT32_MAX - start + now; // Has wrapped.
64+
return now - start; // Wrap safe.
7165
}
7266

7367
/// Add time to the timer to simulate elapsed time.

src/ir_GlobalCache.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const uint8_t kGlobalCacheStartIndex = kGlobalCacheRptStartIndex + 1;
3535
void IRsend::sendGC(uint16_t buf[], uint16_t len) {
3636
uint16_t hz = buf[kGlobalCacheFreqIndex]; // GC frequency is in Hz.
3737
enableIROut(hz);
38-
uint32_t periodic_time = calcUSecPeriod(hz, false);
38+
uint32_t periodic_time = calcUSecPeriod(hz);
3939
uint8_t emits =
4040
std::min(buf[kGlobalCacheRptIndex],
4141
static_cast<uint16_t>(kGlobalCacheMaxRepeat));

src/ir_Pronto.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ bool IRsend::sendPronto(uint16_t data[], uint16_t len, uint16_t repeat) {
7373
uint16_t seq_1_start = kProntoDataOffset;
7474
uint16_t seq_2_start = kProntoDataOffset + seq_1_len;
7575

76-
uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10, false);
76+
uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10);
7777

7878
// Normal (1st sequence) case.
7979
// Is there a first (normal) sequence to send?

test/IRsend_test.cpp

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -239,18 +239,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt38kHz) {
239239

240240
irsend.reset();
241241
irsend.enableIROut(38000, 50);
242-
EXPECT_EQ(5, irsend.mark(100));
242+
EXPECT_EQ(4, irsend.mark(100));
243243
EXPECT_EQ(
244-
"[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs"
245-
"[On]10usecs[Off]11usecs[On]10usecs[Off]6usecs",
244+
"[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs"
245+
"[On]13usecs[Off]9usecs",
246246
irsend.low_level_sequence);
247247

248248
irsend.reset();
249249
irsend.enableIROut(38000, 33);
250-
EXPECT_EQ(5, irsend.mark(100));
250+
EXPECT_EQ(4, irsend.mark(100));
251251
EXPECT_EQ(
252-
"[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs"
253-
"[On]6usecs[Off]15usecs[On]6usecs[Off]10usecs",
252+
"[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs"
253+
"[On]8usecs[Off]14usecs",
254254
irsend.low_level_sequence);
255255

256256
irsend.reset();
@@ -266,18 +266,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt36_7kHz) {
266266

267267
irsend.reset();
268268
irsend.enableIROut(36700, 50);
269-
EXPECT_EQ(5, irsend.mark(100));
269+
EXPECT_EQ(4, irsend.mark(100));
270270
EXPECT_EQ(
271-
"[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs"
272-
"[On]11usecs[Off]11usecs[On]11usecs[Off]1usecs",
271+
"[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs"
272+
"[On]13usecs[Off]6usecs",
273273
irsend.low_level_sequence);
274274

275275
irsend.reset();
276276
irsend.enableIROut(36700, 33);
277-
EXPECT_EQ(5, irsend.mark(100));
277+
EXPECT_EQ(4, irsend.mark(100));
278278
EXPECT_EQ(
279-
"[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs"
280-
"[On]7usecs[Off]15usecs[On]7usecs[Off]5usecs",
279+
"[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs"
280+
"[On]8usecs[Off]11usecs",
281281
irsend.low_level_sequence);
282282

283283
irsend.reset();
@@ -293,18 +293,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt40kHz) {
293293

294294
irsend.reset();
295295
irsend.enableIROut(40000, 50);
296-
EXPECT_EQ(5, irsend.mark(100));
296+
EXPECT_EQ(4, irsend.mark(100));
297297
EXPECT_EQ(
298-
"[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs"
299-
"[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs",
298+
"[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs"
299+
"[On]12usecs[Off]13usecs",
300300
irsend.low_level_sequence);
301301

302302
irsend.reset();
303303
irsend.enableIROut(40000, 33);
304-
EXPECT_EQ(5, irsend.mark(100));
304+
EXPECT_EQ(4, irsend.mark(100));
305305
EXPECT_EQ(
306-
"[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs"
307-
"[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs",
306+
"[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs"
307+
"[On]8usecs[Off]17usecs",
308308
irsend.low_level_sequence);
309309

310310
irsend.reset();

0 commit comments

Comments
 (0)