Simple Infrared PWM on Arduino, Part 2- RAW IR Signals

In Part 1 of this series, we demonstrated how to send signals using simple Infrared PWM on Arduino. In this Part 2 post we look at sending RAW IR signals – specifically a RAW NEC signal and a longer RAW Mitsubishi Air Conditioner signal. We have also improved the method shown in Part 1 due to some issues we identified when sending ‘real’ signals versus the ‘test’ signal we used before. (More on that later). In Part 3, we will take the signals from this post and show how to send them using their binary (or Hex) representation, which saves lots of SRAM.

Original NEC 32-bit and Mitsubishi 88-bit Signals displayed using AnalysIR
Original NEC 32-bit and Mitsubishi 88-bit Signals displayed using AnalysIR

The image above shows the 2 signals we will be using. The first is an NEC 32 bit signal from an LG TV, and includes one repeat header. The second is a signal from a Mitsubishi Air Conditioner with 88 bits of data.

Rationale
The reason for publishing this series includes:

  • Providing a learning opportunity for those interested in IR remote control on Arduinos and other MCUs.
  • Demonstrates a mechanism to send IR signals, without using off-the-shelf libraries which often use up valuable resources and peripherals on the Arduino. For example, many of the support requests on the Arduino forum, relating to IR remote control, are due to timer pin conflicts between libraries. Using this method you can use any available digital or analogue pin, without using any additional Timers or peripherals.
  • Although, we would always recommend using IRremote or IRLib wherever possible, the methods shown is this series offer yet another option for makers and hobbyists.
  • This series of posts is targeted at makers and hobbyists. Experienced or professional user, will likely already be familiar with this or better approaches.
  • We will post a link to the complete Arduino sketch at the bottom of this post.

Definitions Setup
Both definitions and Setup remain largely the same as in Part 1. However, we have now defined the 2 RAW signals in buffers as below:

unsigned long sigTime = 0; //use in mark & space functions to keep track of time

//RAW NEC signal -32 bit with 1 repeat - make sure buffer starts with a Mark
unsigned int NEC_RAW[] = {9000, 4500, 560, 560, 560, 560, 560, 1690, 560, 560, 560, 560, 560, 560, 560, 560, 560, 560, 560, 1690, 560, 1690, 560, 560, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 560, 560, 560, 560, 1690, 560, 560, 560, 560, 560, 560, 560, 1690, 560, 560, 560, 1690, 560, 1690, 560, 560, 560, 1690, 560, 1690, 560, 1690, 560, 560, 560, 1690, 560, 39980, 9000, 2232, 560}; //AnalysIR Batch Export (IRremote) - RAW

//RAW Mitsubishi 88 bit signal - make sure buffer starts with a Mark
unsigned int Mitsubishi_RAW[] = {3172, 1586, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 1182, 394, 1182, 394, 394, 394, 1182, 394, 394, 394, 1182, 394, 1182, 394, 1182, 394, 394, 394, 394, 394, 394, 394, 394, 394, 1182, 394, 1182, 394, 394, 394, 1182, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 1182, 394, 394, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 1182, 394, 394, 394, 394, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 1182, 394, 394, 394, 1182, 394, 1182, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 394, 1182, 394, 394, 394}; //AnalysIR Batch Export (IRremote) - RAW

You should now get a sense that storing signals in buffers can result in running out of SRAM quickly, which is why we will show a better approach in Part 3. Both of these signals were recorded with AnalysIR and automatically exported in the C language format above. You will also notice the sigTime variable defined. SigTime is used, in the mark space functions described below, to keep a track of elapsed time throughout the signal and greatly improved the fidelity of the transmitted signal.

Loop
For demonstration purposes, we continually send the 2 IR signals with a 5 second gap in-between each one. The first one is an NEC signal from an LG TV and the second a Mitsubishi AC signal.

void loop() {
//First send the NEC RAW signal above
sigTime = micros();
for (int i = 0; i < sizeof(NEC_RAW) / sizeof(NEC_RAW[0]); i++) {
    mark(NEC_RAW[i++]);
    if (i < sizeof(NEC_RAW) / sizeof(NEC_RAW[0])) space(NEC_RAW[i]);
}
delay(5000);

//Next send the Mitsubishi AC RAW signal above
sigTime = micros(); //keeps rolling track of signal time to avoid impact of loop & code execution delays
for (int i = 0; i < sizeof(Mitsubishi_RAW) / sizeof(Mitsubishi_RAW[0]); i++) {
    mark(Mitsubishi_RAW[i++]);
    if (i < sizeof(Mitsubishi_RAW) / sizeof(Mitsubishi_RAW[0])) space(Mitsubishi_RAW[i]);
}
delay(5000);
}

Both signals use the same format for sending. We just iterate through the buffer and use the mark space functions to send the signal. Before we start sending a signal, we reset ‘sigTime’ to the starting time of the signal, so we can use accurate timing for each mark or space instead of the less accurate approach shown in Part 1 (elapsed time + code delays). This new approach effectively eliminates any delays introduced by code execution time, as we send the signal.

Mark

void mark(unsigned int mLen) { //uses sigTime as end parameter
  sigTime+= mLen; //mark ends at new sigTime
  unsigned long now = micros();
  unsigned long dur = sigTime - now; //allows for rolling time adjustment due to code execution delays
  if (dur == 0) return;
  while ((micros() - now) < dur) { //just wait here until time is up
    digitalWrite(txPinIR, HIGH);
    delayMicroseconds(HIGHTIME - 5);
    digitalWrite(txPinIR, LOW);
    delayMicroseconds(LOWTIME - 6);
  }
}

This method for sending a mark is very similar to that introduced in Part 1. Here we add mLen to sigTime to get the end time for the mark. By continually taking this approach we essentially, eliminate any additional delays (as in Part 1 approach) due to:

  • Calling and returning from the mark function
  • while loop overheads
  • the remaining code in the mark function

The reason for introducing this new approach in Part 2, is because when reviewing the signal using the approach from Part 1, we noticed that the 9000 uSec header mark of the NEC signal was being sent as a 9,380 uSec mark. That’s a whopping 380 uSecs overhead – enough to turn the signal into a bad NEC signal. In contrast, the new approach presented in this Part 2 delivers an almost perfect signal every time.
Space

void space(unsigned int sLen) { //uses sigTime as end parameter
  sigTime+= sLen; //space ends at new sigTime
  unsigned long now = micros();
  unsigned long dur = sigTime - now; //allows for rolling time adjustment due to code execution delays
  if (dur == 0) return;
  while ((micros() - now) < dur) ; //just wait here until time is up
}

To maintain consistency with the new mark function above and eliminate any code execution overheads, the new space function operates almost identically to the mark function. The only exception is that we do not need to generate any carrier for the space.

Example Images

Original and re-transmitted NEC 32-bit signals
Original and re-transmitted NEC 32-bit signals as captured by AnalysIR
Original and re-transmitted Mitsubishi 88-bit signals
Original and re-transmitted Mitsubishi 88-bit signals as captured by AnalysIR

Conclusion – Part 2 of 3
In this part we have taken real world signals and played them back using the code above and recorded that signal using AnalysIR. The images above are screenshots of the actual signal recorded using AnalysIR. A valuable lesson learned from part one, is that using ‘test’ signals did not expose a weakness in the approach and this became very clear when using real-world captured signals. The slight overall timing differences visible in the screenshots above, may be accounted for by the clocks (crystals) on the emitter (Arduino) and IR recording device and is not material.

Part 3 will show how to send the same NEC Mitsubishi Infrared signals from the binary or HEX value.

What about other MCU platforms? This same method will work on any platform, with only a few minor adjustments.

The complete source code for the sketch can be downloaded here:
Simple_infrared_PWM_Send_RAW.ino (has some minor modifications to work on Photon)

Update for Photon
We have also included a sketch version for the Photon here:
Photon Version of this sketch

Please feel free to use the code in this blog post without restriction. If you gain any benefit please link back to this article credit the Author where possible.

Part 1 of this 3 part series can be found here.
Part 3 of this 3 part series can be
found here.

Get your own copy of AnalysIR here.

One thought on “Simple Infrared PWM on Arduino, Part 2- RAW IR Signals

Leave a Reply