Storing raw IR remote codes in small NANO EEPROM

I had intended to use a standard NANO (ATmega328) create a "teachable" multi button IR remote replacement for use with devices connected to my TV. This would replace the need for separate remotes like:

  • (UP/DOWN/MUTE volume for our sound system,
    
  • POWER and INPUT SOURCE for the TV,
    
  • POWER/ PLAY/ PAUSE for the blue-ray player).
    

Despite these devices having different protocols, I was excited when i saw the many IRremote libraries and tutorials for receiving and sending all possible IR codes. I'm proficient enough at both coding and electronics, so I thought this would be a simple and fun project. The goal would be to have one button switch between "learn" and "operate" modes. In "learn" mode I could then press any other button, point a device remote at my sensor, capture a command, and do the same with other buttons and devices. In "operate" mode, each other button would send the assigned code.

Well my problem was: I'd planned to save the remote codes in NANO using EEPROM library.

Ooops! Big issue there! In order to work with ANY remote, it seemed the best choice was to capture and save "raw" remote codes. With no decoding necessary, I hoped this would free up more program space, and make the storage requirements for the codes small. Unfortunately In all the libraries and examples I've looked at, "raw" remote capture always requires large buffers, some up to 750 bytes long. That makes it impossible to store even one code in the 256 bytes available with the built in EEPROM memory.

I've looked mostly at the "IRRemore" library, and at least briefly at the "IRMP", "IRLIB2", "IRRemoteControl" libraries, and I know there are more. But when it comes to recording and playing back "RAW" IR remote codes, all of them seem to need these large buffers. Is there another library that does a better job of compacting RAW remote codes for easier EEPROM storage? If not, these are the solutions that I can think of:

  1. Give up on using RAW IR codes, and limit my project to just the protocols my handful of devices need. That would limit EEPROM storage to knowing which sender to use, along with short address and command data. If this is the only choice, I'll still need to pick an IR library that is lightweight enough on memory use, to leave room for the EEPROM library at least.

  2. Give up on having the "learn" mode built into the remote. Just capture and copy all the RAW codes discovered using one NANO sketch, and add them to the program memory in the separate IR Remote control project. Obviously this makes the project much less useful, and impossible for anyone but me to re-program.

Advise?

It sounds like a "learning" remote control from the Far East. I have one of those, and have always assumed that they deal only in raw data, mainly because there's just no way they could keep track of all the protocols. In the end, it's just easier to track the on/off timings. But as you say, that's more memory.

I think you should look at adding an I2C EEPROM chip. Something like a 24C512 would give you 64KB of storage. But they go on up to 1Mb or even 2Mb chips. They come in an 8-pin SOIC package, and cost about a dollar at Digikey. The 2Mb version (AT24CM02) costs a bit over $3, but still just 8 pins.

https://ww1.microchip.com/downloads/en/DeviceDoc/20006161B.pdf

1 Like

For "normal" use, like TV remote you probably can encode the signals and save encoded.
Raw is the only way to go if you have to save signals from "uncommon" remotes, but you need a lot of memory.
You could use Esp8266, like D1 Mini, costs $2, it has plenty of memory and smaller than nano.

Yes, that's another possibility thanks. I wouldn't need much external EEPROM to add to store a half dozen codes. But still, before I do this, I'm not wanting to give up on the idea of working with just the lone NANO.

A couple other things. First, while 38KHz is the most commonly used carrier frequency, there are exceptions. This won't matter on transmission, because you can build that into your code, but could make a 38KHz receiver not work so well. So you need to inventory what all your devices are using, and if one has an oddball frequency, just see if it still works with a 38KHz receiver.

The other thing is the IR wavelength. 940nm is the standard, but if something else is used, you just have to see if it still works with the standard parts. In both cases, it can still work even if it's a bit off spec.

Second, I recently opened a Github repo on raw send and receive code for the Atmega328P Arduinos which may be useful to you. No library required.

https://github.com/gbhug5a/SimpleIRRaw

Thank you. Yes, I have a feeling that saving encoded values might be best if "raw" can't be somehow condensed. Now I see that NEC is the most common IR protocol, and all the devices I'd like to control at this point are identified either as NEC or NEC2. I know I could go to a more memory capable device, which would open up even more possibilities and protocols. Since discovering the Arduino NANOs though, I've always found it a fun challenge to see how complex a project I could make fit its resources.

If they're all NEC or NEC2, then the other approach would be to decode all the possible key presses in advance as address/command entries, and store those in either EEPROM or flash memory.

I'm sure IR libraries can handle NEC.

Of course they can. Lets say you receive:
4400 4500 450 600 350 650 350 600 400 600 400 1600 400 1600 350 600 400 600 400 1600....
It can be condensed to
Header(4500,4500)
0 (400,600)
1(400,1600)
so the binary data is: 000011001....

When I use the "Simple Receiver" from the "IRremote" library, and disable decoding for the remotes I'd expect to use, the "raw" dump is like:

Received noise or an unknown (or not yet enabled) protocol
rawData[68]: 
 -3276750
 +9000,-4400
 + 650,- 450 + 650,- 500 + 600,-1600 + 650,- 500
 + 600,- 500 + 600,- 500 + 600,- 550 + 600,- 500
 + 600,-1650 + 600,-1600 + 650,- 500 + 600,-1650
 + 600,-1600 + 650,-1600 + 600,-1650 + 600,-1650
 + 600,-1600 + 650,-1600 + 650,-1600 + 600,-1650
 + 650,- 450 + 600,-1650 + 550,- 550 + 650,- 450
 + 650,- 500 + 550,- 550 + 650,- 450 + 650,- 450
 + 650,-1600 + 650,- 500 + 600,-1600 + 650,-1600
 + 600

So assuming 16 bit uints, that would be 136 bytes. But in any case, if I could condense that to a short binary message, how would I do that (hopefully programmatically to be less error prone), and then how would I reverse the process and send it?

So it is: 0010 0000 1101 1111 1111 0100 0000 1011
20DFF40B in hex.

If you want to learn, study how libraries are dealing with signals.
Have a look at the pulse_distance protocol from IRremote:

Thanks again, and yes I have studied a bit but tell me if I'm wrong here: As I understand, all methods of "raw" IR "capturing are all basically sampling the IR signal, and capturing the timing between marks and spaces. Whether the captured data represents durations or time and distance, they can still be decoded and simplified down to binary or "address/command" pairs. But That seems to be just for human interpretation. In order to resend the message as "raw", the sender is going to either need the entire original array of all timing data captured, or its going to have to know the expected protocol. All the protocols seem to have different representations for what kind of modulation pattern constitutes a mark or space. Even the interpretation of inverted or non-inverted 1s and 0s can change.

So that said, I think my best bet would be to forget about "raw". Maybe I'll just store decoded data along with a number to tell me which sender to use. Since each sender is protocol aware, each will know how to best convert the commands to raw timing on the fly, right?

I think this method might only take at most a dozen bytes per command. So I'm thinking maybe just abandon the idea of using raw protocol, and just stick to the limited handful of protocols I'll really need. At least if I'm going to stick with using just and original NANO, this might be best.

Now I just need to figure out which library can do that and leave me the most program memory for my actual project code. The IRremote library doesn't seem to leave much.

You didn't have a look at the link I posted above (yet).
The signal can be sent in hex with a timing for header/0/1.

uint32_t tRawData[] = { 0xB02002, 0xA010 };
IrSender.sendPulseDistance(38, 3450, 1700, 450, 1250, 450, 400, &tRawData[0], 48, false, 0, 0);

If you go with board like D1 mini, raw is still an option.

But if you are only interested on NEC, just find the library that can decode that with smallest resources.

How about this solution. Might be more universal (not just NEC). I tried the "ReceiveAndSenDistanceWidth" example. It receives and gathers the data needed to send any protocol through this one call...

void sendPulseDistanceWidthData(unsigned int aOneMarkMicros, unsigned int aOneSpaceMicros, unsigned int aZeroMarkMicros,
 *            unsigned int aZeroSpaceMicros, uint32_t aData, uint8_t aNumberOfBits, bool aMSBfirst, bool aSendStopBit = false)

The only thing that seems to be missing is that sometimes a repeat is necessary to make the device respond.

But, turning all the items needed for that function into a structure like this, produces a structure who's sizeof() is only 15 bytes on my NANO...

struct pulseDistanceWidthData {
  unsigned int aOneMarkMicros; 
  unsigned int aOneSpaceMicros; 
  unsigned int aZeroMarkMicros;
  unsigned int aZeroSpaceMicros; 
  uint32_t aData; 
  uint8_t aNumberOfBits; 
  bool aMSBfirst; 
  bool aSendStopBit;
  };

So even if I added another byte for a repeat count, that would only be 16 bytes. Less if I bitmap a byte to use for the 'bool' values. So with the 256 user bytes available in the NANO EEPROM, that would make it possibe to store at least 16 buttons.

And, that example only uses 27% of program storage (8516 out of 30720 max) and only uses about half the available space for global dynamic memory. So it seems like I should be able to implement my original project idea using this method. Do you agree?

I think the Nano has 1KB of EEPROM memory.

One problem I see with this solution is that the same Mark/Space information will be saved for virtually all the entries. If it's NEC or NEC2, it uses the standard mark and space values. How many different protocols do you think you will need other than NEC? If it's no more than four, then you could assign two bits of your boolean byte to protocol selection, the details of which could be in flash memory.

But if anything other than NEC will be possible, it may be a challenge to deal with it. Take a look at section 1.5 of this excellent writeup of the various protocols used by different devices:

https://www.mikrocontroller.net/articles/IRMP_-_english

The whole point of having a library is to reduce what you need to keep track of to the protocol, the address and the command. But the cost of that is the library size.

I think it could work if you limit the library coverage to just the protocols you know you need now for the gear you will be controlling. But then if you add something else that uses a different protocol, you'll have to modify your code and re-flash.

Thanks, and I think you are right. It seems the IRCM send call for the "ReceiveAndSenDistanceWidth" when the protocol is UNKNOWN is the more complex sendPulseDistanceWidthFromArray() call. And even then I'm sure anything less than the full raw receive array of timings (which can be 64 bytes long apparently) would still be missing details.

As far as protocols I really need, I'm sure I can do everything with just NEC, NEC2 (which is what I think the original IRRemote library called the longer 48 bit message), and Panasonic. With only those three to worry about, I think even the more memory hungry IRRemote library might work, and still leave me enough room for my own project code. The eeprom would only need to save a few parameters, along with a number to tell me which protocol specific send call to use.

Oh thanks for reminding me that my lowly NANO has 1024 bytes of eeprom! I was pretty sure I read somewhere that it only had 256, but hopefully thats not the case. If I only need to learn, store, and send a dozen remote codes, 1024 bytes could easilly store a dozen raw arrays op to 85 bytes each right? So maybe there is still hope for exploring that route. I'll have to find a send/receive example that would just let me capture and/or send the raw buffer info.

Thanks also for the link to the IRMP usage. I'd learned about the IRremote library from the excellent article on https://dronebotworkshop.com/ , but up to now was assuming the IRMP examples were the same, and they don't seem to be.

It's completely up to your use case. Just NEC could work. Or NEC plus some general pulse-distance method.
But if you want to build "universal" device, it's getting trickier.
My AC remote for example sends 578 raw pulses. And you need at least 100 codes to cover even most basic functionality.
So decoding the whole protocol is the only option I have.

@ShermanP & @kmin and others.

So I believe I have a working solution! First of all, the IRremote library is able to both decode and send at least 18 protocols and variations without using excessive memory. But more important, I only need to store about 6 bytes to send a remote code to any of those protocols! I only need to store a number representing the protocol, along with an address and a command. This means I no longer have to worry about storing or compacting raw timing codes. I can easilly create an array of structures containing the data I wish to save for each button, sized to the number of buttons I want for my remote. If I want 20 buttons, the array of structures would only take up 128 bytes of EEPROM! So I'll be able to make one remote to control many devices with different protocols, though I have only tested/verified about 5 so far.

I'm going to include a sketch here to demonstrate the operation for anyone else looking for a similar solution. It is just a "work in progress" sketch, so there are unnecessary comments and prints. I'm not saving data in eeprom yet (doing so is trivia). But I think its better I post something that works rather than simply abandon the thread. For reference, I'm using a basic NANO (ATMega328p) and the IRremote library version 4.4.1. This sketch assumes you are already familiar with the use of an IR LED, and IR sensor. You will also need a momentary pushbutton, and a per board. For reference, you can use the " Arduino Hookup" diagram in the very fine article at "(IR Remotes Revisited - 2023 | DroneBot Workshop)". Just be aware the PIN numbers will likely be different! For my NANO project:

  • pin 2 is the IR receive pin (always*)
  • pin 3 is the IR Send pin (always*)
  • pin 5 is used as a digital input for my one single "send" button (the other buttons in the referenced diagram are not in my demo)
  • pin 6 is used as an output for a "USER" indicator LED (always*).

NOTE: "*always" means that the header the pins used are defined in one of the IRRemote "pinDefinitionsAndMore.h" header file, based on your board. The NANO pins will be different than what you see in the referenced diagram (which uses the UNO board). Some may be changeable, but I believe the IR Receive pin can not be changed.

Make sure to use the serial output from the sketch so you can verify what is happening. Operation is as follows:

  1. The sketch continually reads data from your IR sensor. No protocols are defined, so all 18 (and a few variations) can be decoded.
  2. Point any remote at the IR sensor and send a command . Use something you can easilly very with a nearby device. If the protocol is known, it is displayed as a name and reference number, along with an address and command. These are saved in some local variables, which I will later place in a structure as explained earlier.
  3. A button is defined in the sketch for sending. If you press the button, the receiver is temporarily disabled, and the protocol along with its address and command are transmitted through your IR LED.
  4. If you hold the button, it will keep sending the command, with a millisecond delay I specified in DELAY_BETWEEN_REPEAT
  5. when you release the button, the sketch will re-start the receiver, but the stored codes will not be erased until a new IR message is received.

Again, this is just a proof of concept sketch. I believe it verifies that a universal remote can be built within the confines of a basic NANO board. This sketch has some waste, but so far only uses 15826 bytes (51%) of program storage space and 764 bytes (37%) of dynamic memory. So there is plenty of room to add code for more buttons and EEPROM storage. More important, the amount of capture data needed to resend any remote command a small enough to store several dozen operations.

Sketch (work in progress). Hope it helps someone

/*
 * my irRemoteTest. based on SimpleSender.cpp from IRremote library
 * Using NANO ATMega328p
 * 
 * Captures command from an IR remote, and keeps only the protocol number (an enum), 
 * address, and command. That is all I need to pass to this library function
 * to send the same command...
 * IrSender.write(aProtocol, aAddress, aCommand, sRepeats);
 * With only these few args needed, I can easilly save a large number of commands to
 * associate with buttons on the remote I will build.
 */

#include <Arduino.h>

/*
 * Specify which protocol(s) should be used for decoding.
 * If no protocol is defined, all protocols (except Bang&Olufsen) are active.
 * This must be done before the #include <IRremote.hpp>
 * // NOTE: I had no trouble including ALL protocols!
 */
//#define DECODE_DENON        // Includes Sharp
//#define DECODE_JVC
//#define DECODE_KASEIKYO
//#define DECODE_PANASONIC    // alias for DECODE_KASEIKYO
//#define DECODE_LG
//#define DECODE_NEC          // Includes Apple and Onkyo. To enable all protocols , just comment/disable this line.
//#define DECODE_SAMSUNG
//#define DECODE_SONY
//#define DECODE_RC5
//#define DECODE_RC6

//#define DECODE_BOSEWAVE
//#define DECODE_LEGO_PF
//#define DECODE_MAGIQUEST
//#define DECODE_WHYNTER
//#define DECODE_FAST
//#define DECODE_DISTANCE_WIDTH // Universal decoder for pulse distance width protocols
//#define DECODE_HASH         // special decoder for all protocols
//#define DECODE_BEO          // This protocol must always be enabled manually, i.e. it is NOT enabled if no protocol is defined. It prevents decoding of SONY!
//#define DEBUG               // Activate this for lots of lovely debug output from the decoders.
//#define RAW_BUFFER_LENGTH  750 // For air condition remotes it requires 750. Default is 200.

/*************************
 * to associate protocol number with proper send method, use enums in IRprotocol.h
*/

// This include defines the actual pin number for pins like IR_RECEIVE_PIN, IR_SEND_PIN for many different boards and architectures

#include "PinDefinitionsAndMore.h"
#include <IRremote.hpp> // include the library


#define SEND_BUTTON_PIN  5      // APPLICATION_PIN (should be 5) to send my captured command
#define DEFAULT_REPEATS 0       // I find it best to control repeats myself
#define DELAY_BETWEEN_REPEAT 250

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(ALTERNATIVE_IR_FEEDBACK_LED_PIN, OUTPUT);
    pinMode(SEND_BUTTON_PIN, INPUT_PULLUP);

    Serial.begin(115200);
    while (!Serial); // Wait for Serial to become available. Is optimized away for some cores.

    // show which program is running on my Arduino
    Serial.println(F("START " __FILE__ " from " __DATE__ "\r\nUsing library version " VERSION_IRREMOTE));
    Serial.print(F("Send IR signals at pin "));
    Serial.println(IR_SEND_PIN);

   // Start the receiver and if not 3 parameters specified, take 
   // LED_BUILTIN pin from the internal boards definition as default feedback LED
    IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
    Serial.print(F("Ready to receive IR signals of protocols: "));
    printActiveIRProtocols(&Serial);
    Serial.println(F("at pin " STR(IR_RECEIVE_PIN)));

  
    IrSender.begin(); // Start with IR_SEND_PIN -which is defined in PinDefinitionsAndMore.h
                      // - as send pin and enable feedback LED at default feedback LED pin

    Serial.print(F("Ready to send IR signals at pin " STR(IR_SEND_PIN) " on press of button at pin "));
    Serial.println(SEND_BUTTON_PIN);
    //disableLEDFeedback(); // Disable feedback LED at default feedback LED pin
    }

// minimum data to store to resend a captured IR remote operation

uint16_t aAddress = 0;              // 8 or 16 bit address (Sony & Denon have 5 bit adr )
uint16_t aCommand = 0;              // 8 bit command
decode_type_t aProtocol = UNKNOWN;  // just a typed enum referencing protocol 
uint8_t sRepeats =DEFAULT_REPEATS;  // I always use zero()) and manage repeats myself 

bool sendState =false;

// Handy call I created to display the captured IR data 
void showIRData() {
    Serial.print(F("\n  protocol="));
    Serial.print(aProtocol);
    Serial.print(" (");
    Serial.print(getProtocolString(aProtocol)); // translate protocol number to text name!
    Serial.print(F(")  address=0x"));
    Serial.print(aAddress, HEX);
    Serial.print(F("  command=0x"));
    Serial.print(aCommand, HEX);
    Serial.print(F(", repeats="));
    Serial.print(sRepeats);
    Serial.println();
    Serial.flush();
    }

// I use this to clear captured values before storing new ones
void resetIRvalues() {
    Serial.println(F("\nIR data clear")); 
    aAddress=aCommand=sRepeats=0; aProtocol=UNKNOWN;
    }


//////////////// main loop /////////////////////
void loop() {
    
    // if send not active, and we receive data, decode it.
    if (sendState == false ) if (IrReceiver.decode()) {
        resetIRvalues();  // my private copy of key vars
  
        aProtocol = IrReceiver.decodedIRData.protocol; 
        if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
            Serial.println(F("\nReceived noise or an unknown (or not yet enabled) protocol"));
            // We have an unknown protocol here, print extended info
            IrReceiver.printIRResultRawFormatted(&Serial, true);
            IrReceiver.resume(); // Do it here, to preserve raw data for printing with printIRResultRawFormatted()
            } else {
           //  aProtocol = IrReceiver.decodedIRData.protocol;
            aCommand = IrReceiver.decodedIRData.command;
            aAddress = IrReceiver.decodedIRData.address;
            sRepeats = DEFAULT_REPEATS; // or NO_REPEATS or SEND_REPEAT_COMMAND 
            IrReceiver.resume(); // Early enable receiving of the next IR frame
            IrReceiver.printIRResultShort(&Serial);
            IrReceiver.printIRSendUsage(&Serial);
            }
        Serial.print(F("\n********************\nReceived: \n"));
        showIRData();
       // some delay greater than 5 ms (RECORD_GAP_MICROS), otherwise the receiver 
       // may see remote repeats as one long signal
        delay(1000); 
       }
  
    // If button pressed, and we have data, wait for release, then send it
    sendState = !digitalRead(SEND_BUTTON_PIN);
    if (sendState ) {
       if (aAddress ==0 || aCommand == 0 || aProtocol == UNKNOWN) {
            Serial.println(F("\nNo valid IR data to send"));
            }
       if (aAddress !=0 && aCommand !=0) { // send IR msg received
            Serial.println();
            Serial.print(F("\nSend now:"));
            showIRData();
            Serial.flush();
            IrReceiver.stop();  // stop receiving  disableIRIn();
                        
            // from IRSend.hpp... IrSender.write() will switch/case using the protocol 
            // number to find and use the corresponding send call (sendNec(), sendPanasonic() etc.)
  
            while (digitalRead(SEND_BUTTON_PIN) == LOW) {
                digitalWrite (ALTERNATIVE_IR_FEEDBACK_LED_PIN, HIGH);
                IrSender.write(aProtocol, aAddress, aCommand, sRepeats);
                digitalWrite (ALTERNATIVE_IR_FEEDBACK_LED_PIN, LOW);
                delay (DELAY_BETWEEN_REPEAT);
                }
            // resume receiving when I stop sending
            Serial.println(F("Button released -> start receiving"));
            IrReceiver.start(); //begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
            }
  
        sendState=false;
        }   
    
    }

Thanks for posting the sketch. It all looks very good.

So then you would need a way to calculate the eeprom address of each button's data. And what about the drive circuit for the IRLED? I'm partial to mosfets and high currents myself. :slight_smile:

As well, Vishay 100mA emitter can handle short pulse at 1A without problems.

Well the one transistor drive circuit for the LED in the link I posted seems to work well. I've even added a second LED and 33 ohm resistor between the transistor collector and 5V and it had no trouble driving two of them, in case I need it. As for the addressing, my favorite way to do that kind of thing is to just settle on a structure good for any use (protocol and command), and create an array of them. So if you imagine buttons I arbitrarily designate as buttons 0 through 15 (for 16 buttons), then all I need to do is consider the button number as in index into the array. So that's one less piece of info I need to store.