Skip to content

Commit

Permalink
Improve the accuracy of software PWM in mark(). (#192)
Browse files Browse the repository at this point in the history
- Apply an external offset for the period calculation so we
allow for code/execution time in producing the software based PWM signal
in mark().
- Add a simple calibrate() method to IRsend().
- First check in of a routine to calculate the execution delays in mark()
  and create an offset based on that for future mark() calls.

Note: Requires a user to call IRsend.calibrate() before sending a signal.
      If not, defaults to '-3' as determined experimentally in Issue #62

WARNING: The calibrate() routine will generate a PWM pulse when called, thus
         it is left as an optional step for users who are interested.
  • Loading branch information
crankyoldgit committed May 4, 2017
1 parent a137ceb commit 178b09d
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 4 deletions.
52 changes: 49 additions & 3 deletions lib/IRremoteESP8266/IRremoteESP8266.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,50 @@ uint32_t ICACHE_FLASH_ATTR IRtimer::elapsed() {

IRsend::IRsend(uint16_t IRsendPin) {
IRpin = IRsendPin;
periodOffset = PERIOD_OFFSET;
}

void ICACHE_FLASH_ATTR IRsend::begin() {
pinMode(IRpin, OUTPUT);
}

// Calculate & set any offsets to account for execution times.
//
// Args:
// hz: The frequency to calibrate at >= 1000Hz. Default is 38000Hz.
//
// Status: ALPHA / Untested.
//
// NOTE:
// This will generate an 65535us mark() IR LED signal.
// This only needs to be called once, if at all.
void ICACHE_FLASH_ATTR 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.
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.)
uint32_t timeTaken = usecTimer.elapsed(); // Record the time it took.
// While it shouldn't be neccesary, assume at least 1 pulse, to avoid a
// divide by 0 situation.
pulses = std::max(pulses, (uint16_t) 1U);
uint32_t calcPeriod = calcUSecPeriod(hz); // e.g. @38kHz it should be 26us.
// Assuming 38kHz for the example calculations:
// In a 65535us pulse, we should have 2520.5769 pulses @ 26us periods.
// e.g. 65535.0us / 26us = 2520.5769
// This should have caused approx 2520 loops through the main loop in mark().
// The average over that many interations should give us a reasonable
// approximation at what offset we need to use to account for instruction
// execution times.
//
// Calculate the actual period from the actual time & the actual pulses
// generated.
double_t actualPeriod = (double_t) timeTaken / (double_t) pulses;
// Store the difference between the actual time per period vs. calculated.
periodOffset = (int8_t) ((double_t) calcPeriod - actualPeriod);
}

// Generic method for sending data that is common to most protocols.
// Will send leading or trailing 0's if the nbits is larger than the number
// of bits in data.
Expand Down Expand Up @@ -847,6 +885,8 @@ void ICACHE_FLASH_ATTR IRsend::sendDenon(uint64_t data, uint16_t nbits,
//
// Args:
// usec: The period of time to modulate the IR LED for, in microseconds.
// Returns:
// Nr. of pulses actually sent.
//
// Note:
// The ESP8266 has no good way to do hardware PWM, so we have to do it all
Expand All @@ -857,7 +897,8 @@ void ICACHE_FLASH_ATTR IRsend::sendDenon(uint64_t data, uint16_t nbits,
// Hence, for greater compatiblity & choice, we don't use that method.
// Ref:
// https://www.analysir.com/blog/2017/01/29/updated-esp8266-nodemcu-backdoor-upwm-hack-for-ir-signals/
void ICACHE_FLASH_ATTR IRsend::mark(uint16_t usec) {
uint16_t ICACHE_FLASH_ATTR IRsend::mark(uint16_t usec) {
uint16_t counter = 0;
IRtimer usecTimer = IRtimer();
// 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.
Expand All @@ -869,13 +910,15 @@ void ICACHE_FLASH_ATTR IRsend::mark(uint16_t usec) {
// e.g. Are we to close to the end of our requested mark time (usec)?
delayMicroseconds(std::min((uint32_t) onTimePeriod, usec - elapsed));
digitalWrite(IRpin, LOW); // Turn the LED off.
counter++;
if (elapsed + onTimePeriod >= usec)
return; // LED is now off & we've passed our allotted time. Safe to stop.
return counter; // LED is now off & we've passed our allotted time.
// Wait for the lesser of the rest of the duty cycle, or the time remaining.
delayMicroseconds(std::min(usec - elapsed - onTimePeriod,
(uint32_t) offTimePeriod));
elapsed = usecTimer.elapsed(); // Update & recache the actual elapsed time.
}
return counter;
}

void ICACHE_FLASH_ATTR IRsend::ledOff() {
Expand Down Expand Up @@ -910,7 +953,10 @@ void ICACHE_FLASH_ATTR IRsend::space(uint32_t time) {
// Returns:
// nr. of uSeconds.
uint32_t ICACHE_FLASH_ATTR IRsend::calcUSecPeriod(uint32_t hz) {
return (1000000UL + hz/2) / hz; // round(1000000/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.
return std::max((uint32_t) 1, period + periodOffset);
}

// Set the output frequency modulation and duty cycle.
Expand Down
4 changes: 3 additions & 1 deletion lib/IRremoteESP8266/IRremoteESP8266.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class IRsend {
public:
explicit IRsend(uint16_t IRsendPin);
void begin();
void calibrate(uint16_t hz = 38000U);
void send(uint16_t type, uint64_t data, uint16_t nbits) {
switch (type) {
SEND_PROTOCOL_NEC
Expand Down Expand Up @@ -271,13 +272,14 @@ class IRsend {
uint16_t repeat = SHERWOOD_MIN_REPEAT);
void sendMitsubishiAC(unsigned char data[]);
void enableIROut(uint32_t freq, uint8_t duty = 50);
VIRTUAL void mark(uint16_t usec);
VIRTUAL uint16_t mark(uint16_t usec);
VIRTUAL void space(uint32_t usec);

private:
uint16_t onTimePeriod;
uint16_t offTimePeriod;
uint16_t IRpin;
int8_t periodOffset;
uint32_t calcUSecPeriod(uint32_t hz);
void sendMitsubishiACChunk(unsigned char data);
void sendData(uint16_t onemark, uint32_t onespace, uint16_t zeromark,
Expand Down
4 changes: 4 additions & 0 deletions lib/IRremoteESP8266/IRremoteInt.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@
// when received due to sensor lag.
#define MARK_EXCESS 100U
#define TOLERANCE 25U // default percent tolerance in measurements
// Offset (in microseconds) to use in Period time calculations to account for
// code excution time in producing the software PWM signal.
// Value determined in https://github.com/markszabo/IRremoteESP8266/issues/62
#define PERIOD_OFFSET -3
// receiver states
#define STATE_IDLE 2U
#define STATE_MARK 3U
Expand Down

0 comments on commit 178b09d

Please sign in to comment.