The IR-egg: Voice-controlled ESP8266 I2S IR transmission!

I recently posted on ESP8266: Minimum I2S code required for use I2S to accurately control output to a GPIO pin.  I also previously used a UART hack to use serial output to control an IR led.  This approach was a bit hacky though, requiring a few transmissions of the UART IR signal to get proper reception.  I thought that using the I2S protocol would be a really simple way of implementing an accurate IR signal transmission, modulated at 38 kHz.


Ultimately I want to put this together into a wooden egg that is voice-controlled (via my Echo Dot) and use it to send IR signals to turn my home projector on and off.

Putting it all together in the IR-egg!

With the timings in hand it is relatively simple to use I2S to send the IR signal.  As before, we have to define our IR signal:
void turnon_projectorDMA(void)
{
    struct myIRstruct mycode;
    mycode.len=32;
    mycode.low = 345;
    mycode.high = 169;
    mycode.IRcode[0]=0x32;
    mycode.IRcode[1]=0xCD;
    mycode.IRcode[2]=0x81;
    mycode.IRcode[3]=0x7E;
          
   I2S_send_IRcode(&mycode);
   Serial.println("DONE");
}

In I2S_send_IRcode we have:
#include <i2s.h>
#include <i2s_reg.h>

#define I2S_IR_SENDnBYTES(n, data) {for (uint16_t j=0;j<n;j++) {while(i2s_write_sample_nb(data)==0);}}

void I2S_send_IRcode(struct myIRstruct *ircode)
{
   uint8_t byte_index, bit_index;
   byte_index = ircode->len/8; // 32 = 4, 0 remainder
   bit_index = ircode->len - (8*byte_index);

   Serial.println("i2s begin");
   i2s_begin();

   Serial.println("i2s set rate");
   i2s_set_rate(38000);

   uint8_t div1 = 10, div2 = 13; //38461
   div1 &= I2SBDM;
   div2 &= I2SCDM;

  // trans master(active low), recv master(active_low), !bits mod(==16 bits/chanel), clear clock dividers
  I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD));
  // I2SRF = Send/recv right channel first (? may be swapped form I2S spec of WS=0 => left)
  // I2SMR = MSB recv/xmit first
  // I2SRMS, I2STMS = 1-bit delay from WS to MSB (I2S format)
  // div1, div2 = Set I2S WS clock frequency.  BCLK seems to be generated from 32x this
  I2SC |= I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD);

   Serial.print("Real I2S rate:");
   Serial.println((float)160000000L/32/((I2SC>>I2SBD) & I2SBDM)/((I2SC >> I2SCD) & I2SCDM));
   Serial.println("i2s write sample IR");


   // IR code here
   // Leading sequence
   I2S_IR_SENDnBYTES(ircode->low,0xFFFF0000)
   I2S_IR_SENDnBYTES(ircode->high,0x00000000)

   for (uint8_t b=0;b<=byte_index;b++)
     {
      for (uint8_t bt=0;bt<8;bt++)
      {
        if (b<byte_index || (b==byte_index && bt<bit_index))
        {
          uint8_t v = ircode->IRcode[b]&(1<<bt);
          I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)
          if (v) I2S_IR_SENDnBYTES(ircode->ht1, 0x00000000)
          else I2S_IR_SENDnBYTES(ircode->ht0, 0x00000000)
        }
      }
     }
    I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)
    delay(100);
   // END IR send
   Serial.println("i2s end");
   i2s_end();
}
The code is straightforward.  First i2s_rate is set to 38000.  In fact I have read that 38.4 kHz was a better speed and I set that manually (or the closest I can to it), by setting div1 and div2 myself.  I don't know if the i2s_set_rate code would choose the same divisors but in any case, I think it's instructive to see how to do that and how to calculate the actual rate.  Remember the rate we set is the rate of sending double words, i.e. 32 bits.  So if we simply send 16 bits on followed by 16 bits off (i.e. 0xFFFF0000) we will generate the precise the modulated signal we're after: 11111111111111110000000000000000, every 26 usecs! Once we understand this principal, the rest is straightforward.  The leading sequence of 9 ms high and 4.5 ms low is sent, followed by decoding the HEX encoded binary signal.  Finally we mustn't forget to send a final '0' to finish the transmission.  The delay(100) is there because we don't want to end the i2s transmission until the TX data buffers are empty - without this here only part of the signal gets transmitted.  It would be better to go into a while loop and check the buffer status...

Finally, I hooked up the code to send the IR signal by using Amazon's Alexa and fauxmoESP.  Initially I had problems with this crashing when the fauxmo callback was called.  There are some comments on forums that the callback functions must be fairly limited otherwise crashing could result.  I therefore implemented a simple state machine whereby the fauxmo callback set a new state which was detected in the main loop and the I2S_send_IRcode triggered.  Works like a charm!

I soldered together a small board that the ESP-01S board could sit in, with as small a footprint as possible:

A header on the breakout board takes the ESP-01S board and I managed to cram all the passive components on to fit all in a wooden egg! After finished the egg surface with boiled linseed oil and beeswax polish, I positioned the LEDs, added a micro-USB power point on the base, assembled the electronics inside and soldered to the power and LED lines.  It's not shown on the schematic, but I'm using a LD1117AV33 linear regulator to convert the 5V USB to 3.3V for the ESP8266 (Vcc) and also fitting that in the egg.  I decided to add a 3mm green LED which peaks out the back by the micro-USB socket - it flashes quickly when it is acquiring an IP address, switches off, and only activates again when an Echo Dot voice- command is correctly accessed.  Just a comment on the use of GPIO0, even as an output - if GPIO0 is low when the ESP8266 boots up, a firmware programming mode is entered.  So if we are using it in circuitry it's important isn't pulled low.  I found if the LED is connected to GND rather than Vcc, then the device has problems booting up, but it is okay as drawn.  A little hacky - I guess I could pull GPIO0 to Vcc with a 10k resistor just to be sure, but space is at a premium inside the egg!

I also made some cute little feet for it to balance on and it's much more steady now.  And when I plug it in, everything works as expected and I can now control my projector (and potentially any IR device with my voice!).  Plus I think it looks really cool too :).  I call it the IR egg!


Currently I have to remove the ESP-01S board to program it, to add new IR codes for example, but I want to explore updating the firmware over the network, which I'll look into in a post soon.
EXTRA UPDATE:
The full code to send an IR signal by I2S is below:
#include <i2s.h>
#include <i2s_reg.h>

#define I2S_IR_SENDnBYTES(n, data) {for (uint16_t j=0;j<n;j++) {while(i2s_write_sample_nb(data)==0);}}

#define LOW_TIME 24
#define HIGH_TIME_0 19
#define HIGH_TIME_1 62

struct myIRstruct{
  uint8_t lt=LOW_TIME, ht0=HIGH_TIME_0, ht1=HIGH_TIME_1;
  uint16_t low, high; // Leading sequence
  uint8_t IRcode[8]= {0,0,0,0,0,0,0,0};
  uint8_t len = 0;
};

void turnon_projectorDMA(void)
{
    struct myIRstruct mycode;
    mycode.len=32;
    mycode.low = 345;
    mycode.high = 169;
    mycode.IRcode[0]=0x32;
    mycode.IRcode[1]=0xCD;
    mycode.IRcode[2]=0x81;
    mycode.IRcode[3]=0x7E;
          
   I2S_send_IRcode(&mycode);
   Serial.println("DONE");
}

void I2S_send_IRcode(struct myIRstruct *ircode)
{
   uint8_t byte_index, bit_index;
   byte_index = ircode->len/8; // 32 = 4, 0 remainder
   bit_index = ircode->len - (8*byte_index);
 
   Serial.println("i2s begin");
   i2s_begin();

   Serial.println("i2s set rate");
   i2s_set_rate(38000);

   uint8_t div1 = 10, div2 = 13; //38461
   div1 &= I2SBDM;
   div2 &= I2SCDM;

  // trans master(active low), recv master(active_low), !bits mod(==16 bits/chanel), clear clock dividers
  I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD));

  // I2SRF = Send/recv right channel first (? may be swapped form I2S spec of WS=0 => left)
  // I2SMR = MSB recv/xmit first
  // I2SRMS, I2STMS = 1-bit delay from WS to MSB (I2S format)
  // div1, div2 = Set I2S WS clock frequency.  BCLK seems to be generated from 32x this
  I2SC |= I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD);

   Serial.print("Real I2S rate:");
   Serial.println((float)160000000L/32/((I2SC>>I2SBD) & I2SBDM)/((I2SC >> I2SCD) & I2SCDM));
   Serial.println("i2s write sample IR");
   
   // IR code here
   // Leading sequence
   I2S_IR_SENDnBYTES(ircode->low,0xFFFF0000)
   I2S_IR_SENDnBYTES(ircode->high,0x00000000)
   
   for (uint8_t b=0;b<=byte_index;b++)
     {
      for (uint8_t bt=0;bt<8;bt++)
      {
        if (b<byte_index || (b==byte_index && bt<bit_index))
        {
          uint8_t v = ircode->IRcode[b]&(1<<bt);
          I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)

          if (v) I2S_IR_SENDnBYTES(ircode->ht1, 0x00000000)
          else I2S_IR_SENDnBYTES(ircode->ht0, 0x00000000)
        }
      }
     }
     
    I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)

    delay(100);
   // END IR send
   
   Serial.println("i2s end");
   i2s_end();
}

void setup()
{
 Serial.begin(115200);
  
  pinMode(3, OUTPUT); // Override default Serial initiation
  digitalWrite(3,0); // GPIO3 = I2S data out
}

void loop()
{
    static unsigned long last = millis();
    if (millis() - last > 10000) {
      turnon_projectorDMA(); // Send the IR signal every 10 seconds
      last = millis();
    }
}

Comments

  1. Hello, I converted your code to Arduino but unfortunately your code missing this function: I2S_IR_SENDnBYTES()
    Can you send to me this function please!

    #include
    #include

    void setup()
    {

    }

    void loop()
    {

    }

    /*
    void turnon_projectorDMA(void)
    {
    struct mycode{
    int len;
    int low;
    int high;
    uint8_t IRcode[4];
    };

    struct mycode code = {32,345,169,{0x32, 0xCD, 0x81, 0x7E}};
    I2S_send_IRcode(code);
    Serial.println("DONE");
    }
    */



    void I2S_send_IRcode()
    {
    int len=32;
    int low=345;
    int high=169;
    uint8_t ircode[4];
    ircode[0]=0x32;
    ircode[1]=0xCD;
    ircode[2]=0x81;
    ircode[3]=0x7E;
    uint8_t byte_index, bit_index;
    byte_index = len/8; // 32 = 4, 0 remainder
    //len = len - byte_index;
    bit_index = len - (8*byte_index);

    Serial.println("i2s begin");
    i2s_begin();

    Serial.println("i2s set rate");
    i2s_set_rate(38000);

    uint8_t div1 = 10, div2 = 13; //38461
    div1 &= I2SBDM;
    div2 &= I2SCDM;

    // trans master(active low), recv master(active_low), !bits mod(==16 bits/chanel), clear clock dividers
    I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD));
    // I2SRF = Send/recv right channel first (? may be swapped form I2S spec of WS=0 => left)
    // I2SMR = MSB recv/xmit first
    // I2SRMS, I2STMS = 1-bit delay from WS to MSB (I2S format)
    // div1, div2 = Set I2S WS clock frequency. BCLK seems to be generated from 32x this
    I2SC |= I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD);

    Serial.print("Real I2S rate:");
    Serial.println((float)160000000L/32/((I2SC>>I2SBD) & I2SBDM)/((I2SC >> I2SCD) & I2SCDM));
    Serial.println("i2s write sample IR");


    // IR code here
    // Leading sequence
    I2S_IR_SENDnBYTES(low,0xFFFF0000)
    I2S_IR_SENDnBYTES(high,0x00000000)

    for (uint8_t b=0;b<=byte_index;b++)
    {
    for (uint8_t bt=0;bt<8;bt++)
    {
    if (blt, 0xFFFF0000)
    if (v) I2S_IR_SENDnBYTES(ircode->ht1, 0x00000000)
    else I2S_IR_SENDnBYTES(ircode->ht0, 0x00000000)
    }
    }
    }
    I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)
    delay(100);
    // END IR send
    Serial.println("i2s end");
    i2s_end();
    }

    ReplyDelete
    Replies
    1. Hi Kareem,
      Whoops! Thanks for noticing the error. I have updated the code in this post, so that it is included now. The definition of I2S_IR_SENDnBYTES is:

      #define I2S_IR_SENDnBYTES(n, data) {for (uint16_t j=0;j<n;j++) {while(i2s_write_sample_nb(data)==0);}}

      I wasn't sure from your comment whether you are trying to send IR signals with an Arduino, or with the ESP8266? If the Arduino, you might want to check out my blog post on this:

      https://shepherdingelectrons.blogspot.com/2018/07/sending-ir-remote-control-signals.html

      Hope it works for what you are trying to do!

      Delete
  2. Hi Duncan,
    Thank you for your help & fast reply.

    I'm using the Arduino programming language for the ESP8266-12E.

    I converted your Struct to I can use it with the Arduino IDE but I get errors!

    My code:
    #include
    #include

    #define I2S_IR_SENDnBYTES(n, data) {for (uint16_t j=0;jlen/8; // 32 = 4, 0 remainder
    bit_index = ircode->len - (8*byte_index);

    Serial.println("i2s begin");
    i2s_begin();

    Serial.println("i2s set rate");
    i2s_set_rate(38000);

    uint8_t div1 = 10, div2 = 13; //38461
    div1 &= I2SBDM;
    div2 &= I2SCDM;

    // trans master(active low), recv master(active_low), !bits mod(==16 bits/chanel), clear clock dividers
    I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD));
    // I2SRF = Send/recv right channel first (? may be swapped form I2S spec of WS=0 => left)
    // I2SMR = MSB recv/xmit first
    // I2SRMS, I2STMS = 1-bit delay from WS to MSB (I2S format)
    // div1, div2 = Set I2S WS clock frequency. BCLK seems to be generated from 32x this
    I2SC |= I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD);

    Serial.print("Real I2S rate:");
    Serial.println((float)160000000L/32/((I2SC>>I2SBD) & I2SBDM)/((I2SC >> I2SCD) & I2SCDM));
    Serial.println("i2s write sample IR");


    // IR code here
    // Leading sequence
    I2S_IR_SENDnBYTES(ircode->low,0xFFFF0000)
    I2S_IR_SENDnBYTES(ircode->high,0x00000000)

    for (uint8_t b=0;b<=byte_index;b++)
    {
    for (uint8_t bt=0;bt<8;bt++)
    {
    if (bIRcode[b]&(1<lt, 0xFFFF0000)
    if (v) I2S_IR_SENDnBYTES(ircode->ht1, 0x00000000)
    else I2S_IR_SENDnBYTES(ircode->ht0, 0x00000000)
    }
    }
    }
    I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)
    delay(100);
    // END IR send
    Serial.println("i2s end");
    i2s_end();
    }

    The error:
    invalid use of incomplete type 'struct myIRstruct' I2S_IR_SENDnBYTES(ircode->lt, 0xFFFF0000)

    I will be thankful if you help me

    ReplyDelete
    Replies
    1. Hi Kareem,

      Ah, I know what's wrong. I have failed to include the definition of the struct myIRstruct in my code. Please note that it is included in my referenced blog post:

      https://shepherdingelectrons.blogspot.com/2018/07/sending-ir-remote-control-signals.html

      For clarity however, I have included the complete code of an example that should work at the end of the current blog post (search "EXTRA UPDATE"). I don't have a chance to check it compiles, but it looks to me like it should fix your problem.

      best wishes,
      Duncan

      Delete
  3. Hi Duncan,

    I compiled your updated code and not gave any error using the Arduino IDE.

    but there's stack error and the watchdog reset when it execute this function:
    #define I2S_IR_SENDnBYTES(n, data) {for (uint16_t j=0;j<n;j++) {while(i2s_write_sample_nb(data)==0);}}

    I noticed you wrote in another article, We should add yield(), I added yield() but the same error!

    void I2S_IR_SENDnBYTES(uint16_t n, uint8_t data)
    {
    for (uint16_t j=0;j<n;j++) {
    while(i2s_write_sample_nb(data)==0);
    if ((j % 100) == 0) yield();
    }
    }

    I'm testing on ESP8266-12E with flash size 4M.
    Also, I'm using GPIO14 (WSI), I modified to i2s_begin();
    to i2s_rxtx_begin(true, false); // Enable I2S RX

    I will be thankful if you help me.

    Best Regards,
    Kareem Elzeftawy

    ReplyDelete
    Replies
    1. Seems like i2s_write_sample_nb is never successful so yield is never called. Adding Serial.println output might help you to find out if this is so. I can't see any call to i2s_begin or what data rate you set?

      Blogger tends to mangle code pasted in comments, so please put you whole program code into:

      https://pastebin.com/

      and then paste the URL you get as a reply so I can look at the whole program!

      cheers,
      Duncan

      Delete
    2. PS I notice that you say you are using GPIO14 and that you have set RX to true and TX to false... It seems to me that you are trying to use the I2S hardware to receive data rather than send? GPIO14 is built into the chip to be the word select toggle for I2S input...

      https://www.esp8266.com/wiki/lib/exe/fetch.php?media=pin_functions.png

      That is why your code resets the chip - you are configuring the I2S as an input and then infinitely looping trying to output!

      Do you want to send output a signal with I2S or receive one? My code only covers sending out an I2S signal.

      Did you get the code working with the standard library and outputting to GPIO03? (UART0 RX)

      cheers,
      Duncan

      Delete
  4. I want to send IR signal,

    Yes, Your code worked with GPIO03

    Thank you very much

    Best Regards,
    Kareem

    ReplyDelete
  5. This comment has been removed by a blog administrator.

    ReplyDelete

Post a Comment

Popular posts from this blog

Getting started with the Pro Micro Arduino Board

Arduino and Raspberry Pi serial communciation