I2C Conundrums

Resolved  - working though still don't know why Arduino single write was causing problems - however - got a way around it.

Firstly why am I doing this i2c thing? Well, the little ESP8266 is a wonderful thing but there are some chips out there that are really neat and work on I2c. More’s the point there are some REALLY cheap and powerful Arduino-compatible boards out there which benefit from the vast wealth of Arduino libraries – but don’t have WIFI..

So it occurred to me to add an I2c interface to my software – to talk to the likes of the Arduino.

And that it does – kind of – but I’m hitting a brick wall on return data. That is, return data over one byte – well, kind of… let me explain.

The ESP i2c is of course bit-banged because the chip itself does not do I2c – but it is fast at 80Mbps so you would expect it would work without issue. I am using the i2c master software that comes in the Espressif SDK – I’m using version 2.00. I’m programming in C.

The Arduino code is  using the latest Arduino IDE 1.69 and straight forward WIRE code. This is the SLAVE.

I can send multiple bytes to the Arduino - no problem - and I can get a byte back into the ESP no problem.

Now bearing in mind that [a] my experience with i2c is minimal [b] the Arduino I2c looks completely different to the ESP I2c....

When I first wrote this – I was getting reboot problems with the ESP when trying to return multiple bytes. Not every time – just every now and then.

I checked my code and I am initialising the I2c every time I use it – for various reasons.  When I put a logic analyser on the 2 i2c lines, I could see what looked like good i2c – but at the start, a whole load of clock pulses. 48 of them in fact. I went into the Espressif code only to find a loop in the initialisation – (it is in i2c.c).

void ICACHE_FLASH_ATTR
i2c_master_init(void)
{
/*
uint8 i;

    i2c_master_setDC(1, 0);
i2c_master_wait(5);

    // when SCL = 0, toggle SDA to clear up
i2c_master_setDC(0, 0) ;
i2c_master_wait(5);
i2c_master_setDC(1, 0) ;
i2c_master_wait(5);

    // set data_cnt to max value
for (i = 0; i < 28; i++) {
i2c_master_setDC(1, 0);
i2c_master_wait(5);    // sda 1, scl 0
i2c_master_setDC(1, 1);
i2c_master_wait(5);    // sda 1, scl 1
}

    // reset all
i2c_master_stop();
return;
*/

i2c_master_setDC(1, 1);
}

I have absolutely no idea why that is in there but it does not appear to be part of the I2c specification – so I cut it out – all that’s needed at the beginning as far as I can see is for data and clock to be high. The main init routine which calls this one (i2c_master_gpio_init()) sets the pins up the right way (pins 4 and 5 by default).

Well, this did not seem to have a lot of effect – except that now I can’t crash the ESP8266.

So – I can send data to an Arduino with I2c slave running – and I can receive a byte back – wheeeeee.

If I return one byte to the ESP - all is fine - works perfectly – but then the rot….
So --- Arduino...

//function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
Wire.write(retParam); // *****
}

That works – and now the ESP

i2c_master_gpio_init();
i2c_master_start();
i2c_master_writeByte((arg1<<1)|1);
if (!i2c_master_checkAck())
{
i2c_master_stop();
os_sprintf(strValue, "duff i2c");
}
else
{
uint8_t a;
a=i2c_master_readByte(); //*******
i2c_master_stop();
os_sprintf(strValue, "%d",a);
}

That also works... but add another write for the Arduino where I've put stars *** and another read in the ESP code where I've put stars – even with a i2c_master_send_ack() in between - it does not work – the second byte is returned (not the first) and then a  255  is returned.

So here is the multi-byte return on the ESP side..

      a=i2c_master_readByte();
i2c_master_send_ack();
b=i2c_master_readByte();

      i2c_master_stop();
os_sprintf(strValue, "%d %d",a,b);

Sending 2 bytes from the Arduino…

void requestEvent() {
Wire.write(33);
Wire.write(45);
}

FAILS!!!

This however SEEMS to succeed and since my other change – seems to work reliably.

Wire.write("This is it you know",X);
// where X is how many chars to send out - I've tested 19 chars repeatedly.

In the above I’m just returning the first 2 bytes of course and this can be turned into byte arrays  or whatever..

I2c logic

But the question is – WHY does the first version screw up?

Anyway here is the ESP code that works with the above where X is 19...

i2c_master_gpio_init();
                  i2c_master_start();
                  i2c_master_writeByte((arg1<<1)|1); // send a read command
                  if (!i2c_master_checkAck())
                      {
                          i2c_master_stop();
                          os_sprintf(strValue, "duff i2c");
                      }
                  else
                      {
                            uint8_t buff[40],a;
#define I2CINB 19
                            for (a=0;a<I2CINB;a++) {
                                buff[a]=i2c_master_readByte();
                                if (a<(I2CINB-1)) i2c_master_send_ack();
                            }
                            buff[a]=0;
                          i2c_master_send_nack();
                            i2c_master_stop();
                            os_sprintf(strValue, "%s",buff);
                      }

Lovely – now I need to find some i2c chips to experiment with. I have an ATMEGA1284 setup to respond to i2c for port input, output with PWM and analog in where appropriate – the 1284 you may recall has a lot more pins than the Arduino.  I have a few variations of miniature boards on the way to test. When I really go to town on this I’ll no doubt blog it.

Facebooktwittergoogle_pluspinterestlinkedin

52 thoughts on “I2C Conundrums

  1. Peter whatabout trying this with the arduino ide on a esp8266 as the master to start with and then see if that works before.you go to raw c ??

    1. Thanks craig but as all my ESP code is already in plain C I would rather plough on - I'm sure I'm just missing maybe some handshake or something due to lack of examples... I find it amazing that in all the scope of Google - I cannot find examples of people doing comms using the Espressif i2c code - surely SOMEONE has to be using it...

      1. I think you are at the point they have all been before - the SDK being so poor on documentation of this that everyone falls by the wayside.

        I personally would try the Arduino IDE and make sure you can get multiple bytes back - once you know that is all working OK then you can look at going back to handcrafted code.

        If you use the IDE for both the Arduino and the ESP8266 and get it working - you will at least then know the Arduino side is working OK when you go back to concentrating on the ESP.

        I have just spent about 1/2 hour reading around - if you want to persevere with C then maybe look around for i2c with multi byte return to start with - such as some of the Clock or Temp Sensors with RAM - do you have a DS3231 RTC or a 24LC256 RAM module you could try to see what happens with "simpler" devices on the end of the ESP ?

        Craig

        1. Peter,

          If you have a look at this one (referred to in the comments of my first post) - he has done some nice i2c work with inline assembly and C - targeted at the Arduino toolchain but specifically tells you what to change and look for with the native sdk - might be worth trying ??

          https://github.com/pasko-zh/brzo_i2c/wiki

          Craig

  2. For a second it looked so simple - Craig pointed me to Richard Burton's blog - all I had to do was change the receive stuff on the ESP to..

    uint8_t a,b;
    a=i2c_master_readByte();
    i2c_master_send_ack();
    b=i2c_master_readByte();
    i2c_master_send_nack();
    i2c_master_stop();
    os_sprintf(strValue, "%d %d ",a,b);

    But no.... If I send out 34, then 45 I get back..

    ard/fromesp/i2c=45 255

    So depressing...

  3. Ok, so here's a thing - back at the Arduino end..

    // Wire.write(34); // respond with message of 1 bytes as expected by master
    // Wire.write(45); // test
    Wire.write("ab");

    See how I sent two byte writes - the second one got through - the first one was lost (with the second received byte being 255)....

    I just swapped that to the line you see above - and sure enough - 2 bytes sent out in the correct order and received perfectly by the ESP8266 - The question... why ?

    1. From looking at the Wire libarary docs it seems that the write() call needs to be "in-between calls to beginTransmission() and endTransmission()"

      Your first example didn't meet that requirement as you had two consecutive write() calls. Whereas I imagine the write(string) call contains built in capability to push the whole string over the wire adding in whatever start/end transmission steps are required.

      https://www.arduino.cc/en/Reference/WireWrite

      1. Thanks for that Andy - that makes sense. I managed to send a complete sentence across to the ESP - sadly at the end of it, the ESP crashes every several messages so something else is up. My code on the ESP never crashes so I need to understand this better before we can really use it.

        1. Pete,

          Have a read of this post - where he talks about issues he found with Richard Burtons Code - he links to another two guys - Nathan Chantrelle and Zarya and kludges something together.

          Do you have a "dumber" i2c device like a DS3231 or RAM chip - you can try this on on the ESP side and remove the Arduino from the equation temporarily ?

          https://esp8266hints.wordpress.com/2015/06/04/sdk-i2c-code-todays-duh-story/

          It SHOULD be fairly straight forward to get a couple of bytes from a DS3231 as as starting point

          Craig

          1. Thanks for this Craig will check tomorrow. That's not a bad idea - find out which side is most likely duff - mind you - with the Espressif code crashing the ESP I think I know... no matter what it should not crash the thing!!

            I do have an I2c expander and that works but of course only returns one byte.... I'll either get a DS or Maureen is going over to the UK this week and stopping with my friend Aidan - worse case he'll have one.

          2. You know, I'd completely forgotten this. I gave up on using the plain C toolchain shortly after this and moved on to the Arduino-IDE version (and now on to PlatformIO) just because there was so much less pain involved.
            An excerpt from the CVS log for that same project says it all:-

            revision 1.8
            date: 2015/07/12 13:47:00; author: anoncvs; state: Exp; lines: +3 -2
            Yay! Success! (These moments are few and far between with the
            ESP8266!!).

            Anyway, if you want the code for the DS3231 project Pete (or Craig, or anyone else), I can certainly make it available.

  4. You.know puce i think Peter is secretly a bit of a masochist and will hang in there until he cracks this !!

    1. Hah, well, to my credit, I stuck with the Node-Red Firmata node issue I had (which turned out to be more than one) and the end result is the world can now enjoy a working Firmata node 🙂 In the background on another machine, I have an FTDI flashing a LED once a second and it's been doing that for over a week now, rock solid. The I2c version lasts about 15 minutes before having a stroke... 🙂

      1. Indeed you did Peter !! But i am pretty sure you had more hair when you started that little adventure. ! Keep going like this and it will be a distant memory !! Need to pick your battles 🙂

        Craig

  5. Given all the libraries available in the Arduino ESP8266 environment then for me that's the way to code on ESP8266 🙂

  6. I'm a bit late with offering a suggestion but if all else fails Bit-Bang it.

    I2C isn't as easy to bit-bang as SPI but I've done it past.

    1. Craig - you could be my best friend of the week - I'm putting this to one side as I have to help a friend this afternoon with his PC.... I'm going to look at that and I'll report back if it works - I was getting REALLY frustrated with this.....

      So many uses for this but it has to work 100% for me...

    2. No - the guy who did this responded to say that the fix was only temporary and it is no longer working for him - and it was in the Arduino ESP core - whereas for the slave I'm using an actual Arduino (well, an Atmega1284 anyway). Good try though.

  7. Pete,
    I made a capacitive humidity sensor read by Arduino and send 2bytes data to ESP with I2C.
    (I changed the I"C speed on Arduino side once but as I remember it works with the original speed.)
    Anyway the key line is:
    Wire.write(cut_val, 2); // send val as 2 separated bytes
    George
    This is the short working program:

    #include
    #include
    #include

    int val=0;
    bool sent = false;
    byte cut_val[2];

    void setup() {

    Serial.begin(9600); // setup serial

    Wire.begin(8); // join i2c bus with address #8
    Wire.onRequest(requestEvent); // register event

    pinMode(3, OUTPUT);
    // pinMode(11, OUTPUT);
    TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
    TCCR2B = _BV(WGM22)| 1;
    OCR2A = 15;
    OCR2B = 1;

    int i;
    #define COUNT 1
    for (i=1;i> 8) & 0xFF; // 256
    cut_val[1] = val & 0xFF;
    //
    Serial.println(val);

    }

    void loop() {
    // put your main code here, to run repeatedly:
    if (sent) {
    // Serial.println("Number sent");
    sent=false;
    }
    }
    void requestEvent() {
    sent=true;
    Wire.write(cut_val, 2); // send val as 2 separated bytes
    }

      1. You know, a light is slowly coming on here.

        wire.write(33);

        I'm just wondering if this is actually sending 1 byte or two - if the latter that would explain a lot.

        I just sent a byte array exactly as you describe - I had 2 characters running when your response came in - I've just tried a full string - absolutely works a treat - why...

        1. I copied (CTRL/C) the whole program and I realize now that whole lines are missing.
          The I2C part is there.

          ( In the i2c_master_init() you cleared the cycle. If it doesn't cause serious problem then it's worth to keep.)

          This is the ESP I2C routine reading the Arduino:
          int ICACHE_FLASH_ATTR
          read_moist()
          {
          #define I2C_ADDR 8 // address of Arduino
          uint8 ack;
          int i;
          uint8 a;
          // INFO("\r\n Read i2c\n");

          i2c_master_start();
          i2c_master_writeByte( (I2C_ADDR << 1) | 1); // read operation=1
          ack = i2c_master_getAck();
          if (ack) {
          INFO("\r\nI2C addr not ack \n");
          i2c_master_stop();
          return -1;
          }
          else {
          // INFO("\r\nRDT ack ok \n");
          os_delay_us(10);
          a=i2c_master_readByte();
          i2c_master_setAck(0);
          i=a<<8;
          a=i2c_master_readByte();
          i2c_master_setAck(1);
          i2c_master_stop();
          i=1024 -(a|i);
          // INFO("\r\nI2C 1st byte:%d\r\n",a);
          // INFO("\r\nI2C 2nd byte:%d",b);
          // INFO("\r\nI2C int:%d\r\n",i);
          return i;
          }

          }

    1. Between removing that loop at the start (which may be ok if it is used once only on powerup but WAS causing occasional glitches when run repeatedly) and NOT using the Arduino write(34) version but using the version sending an array and a number, I now appear to be getting perfectly reliable I2c - longest I've sent repeatedly is a 19 byte string.

      RIGHT - and now to actually do something with I2c apart from a port extender !!!!

  8. So right now I've updated the software to include a dummy command {oled:0} for the SSD1306 based displays (cheap - AliExpress etc). should put some text up but my display has not yet turned up - thanks to George for the code for this..

    It needs some optimising as it it using function RAM but I don't want to touch it until I know it actually works - so if anyone out there is using the code - and has one of these displays they can slap onto 3v3 and GPIO4 and 5 - the serial will return an error message if you get the signal wires the wrong way around - and just "OK" if not. Once I know it works - 2 more stages - optimise the routines to get rid of the extra RAM use -then I'll make something like the Hitachi string.

  9. Tested both formats and working correctly, but the $x & $X are transposed in the manual.
    Thanks for adding these commands and code tweaking - I like the extra debug info you have added, makes life easier when experimenting. Just a thought, but would it be possible to store the setup commands for LCD/OLED in flash, so it survives a reboot - as the display would normally be unchanged.
    Last thing ... I noticed all the timestamps on this site are gmt + 2, is that intentional ?

  10. Hi Pete!

    I am the author of Brzo I2C, which is a fast implementation of an i2c Master written in inline Assembly and C for the esp8266. The Code is here https://github.com/pasko-zh/brzo_i2c, and I've also included a Wiki https://github.com/pasko-zh/brzo_i2c/wiki with a lot of i2c stuff. It works quite nice and very fast 🙂
    FWeinb uses my library for an OLED display and has posted a video comparing wire-library vs. brzo_i2c speed: http://www.electronics-lab.com/esp8266-ssd1306-oled-library-release/

    The library is written for the arduino toolchain, however, you would have to slightly modify it and it will run with the native toolchain, too. For instance, I am using if (F_CPU == 160000000) {...}, which is defined by the arduino toolchain, so you would have to determine CPU Clock Speed via SDK call " system_update_cpu_freq" .

    Cheers
    Paško

      1. :-/
        I had once the toolchain (http://www.esp8266.com/wiki/doku.php?id=toolchain) installed. But had it removed because I switched to the arduino toolchain. So, at the moment I cannot test the compiler...

        I am bit confused: Both toolchains use the same gcc compiler, xtensa-lx106-elf-gcc. So, you mean that using it with the native toolchain, it does not accept inline assembly? I.e. starting with asm (...)? Even not something simple as asm ("NOP;"); ?

        1. I can reduce errors by getting rid of the RAM directive - the functions will need to go into FLASH anyway as I have very little IRAM left... and if I reduce the ASM down to plain ASM - without volatile - that works -except toward the end and I must admit I don't know what it's problem is...

          Around line 329 we fo from individual quoted lines of assembly which the asm( directive seems happy with - to something that means nothing to me....

          and clearly nothing to the compiler./..

          : [r_set] "+r" (a_set), [r_repeated] "+r" (a_repeated), [r_temp1] "+r" (a_temp1), [r_in_value] "+r" (a_in_value), [r_error] "+r" (i2c_error), [r_bit_index] "+r" (a_bit_index), [r_adr_array_element] "+r" (&data[0]), [r_byte_to_send] "+r" (byte_to_send), [r_no_of_bytes] "+r" (no_of_bytes)
          : [r_sda_bitmask] "r" (sda_bitmask), [r_scl_bitmask] "r" (scl_bitmask), [r_iteration_scl_halfcycle] "r" (iteration_scl_halfcycle), [r_iteration_minimize_spike] "r" (iteration_remove_spike), [r_iteration_scl_clock_stretch] "r" (iteration_scl_clock_stretch)
          : "memory"
          );

          1. Hmm, strange! ICACHE_RAM_ATTR is officially referenced in esspressif's documentation. So, this should (sic!) work 😮 ?!
            But then you would have to place the ..._FLASH directive, otherwise, per default, all functions are loaded into IRAM! (In the Arduino Toolchain it is the other way round)

            What do you mean by "plain ASM"?

            Line 329: OK, this is a bit tricky 🙂 First, I want the compiler to chose the registers, that's why I've used for instance "[r_iteration_scl_clock_stretch] “r” (iteration_scl_clock_stretch)" . The compiler will then find a suitable register for "r_iteration_scl_clock_stretch", say register A4. You may need to check if such a mapping is working, i.e. just do some easy ASM with just one variable, then you should know.

            Second, maybe it don't like passing the address "&data[0]" ?

            Third, maybe it don't like the keyword "memory"?

            => This is the hard part of (inline) assembly, because the compiler warnings/error message are, say, hard to understand, and also not very precise in the sense that, it won't tell you "Line 329 not possible to map variable name to register, ....".
            So, you have to do a step by step, meaning try/compile/error, phases.

  11. "Plain asm" - as in asm without volatile - I had to remove the volatile directive even to get this to go past the asm keyword - too many changes for it to have a chance of working I think - a shame as I like your transaction model... I wonder if this could be compiled to an object file and somehow linked into the Espressif C project....

    1. Sad to hear that 🙁

      I thought (I really did) that it could be easily compiled with the native toolchain, too.

      One more thought: Maybe some compiler switches could do the trick, i.e. that asm(...) compiles better? I mean, it's the same compiler, so maybe compare those switches in the two make files for the arduino vs. native toolchain.

      PS: Yes, the i2c transactions 🙂 I thought of doing it different than the "classical" approaches, where sending bytes and then ACKs etc. are seperated functions (or methods).

  12. Anyone more up to speed with the GCC compiler up to helping out with this??? Would be nice as I plan a few I2c additions - and we may as well be using the fastest I2c out there...

      1. Hi

        As it happens I revisited the Original code from Espressif and now having running at around 1meg clock - I may revisit this at some other time.

        What I'd REALLY like to do is to be able to take all of the Espressif code and existing C code but be able to code C++ in my projects as C is a little limiting. Some day I will sit down and figure this out.

          1. Hi there

            Only one tiny issue for me..... If I change from -std=c99 to -std=gnu89 and -Os to -O2 (not sure what the latter means).... my code will not even begin to compile..../

            Things like (for int i=0; a<50; i++) get thrown out because of the initialiser....

            I'd imagine others will have the same issue.... these are the standard settings used for example in the TUANPM MQTT example which was the initial basis for my code many moons ago.

            1. Just for the sake of it I went through the automatic initialisations - and removed them - my code then compiles with the new settings - however I am dangerously short of TEXT RAM (where functions run when pulled out of FLASH) - and this simple action (no new code added) took this from 7F2C to 7FAC which is too near the limit for my liking. the move from -std=gnu99 to -std=gnu89 does not seem to have done any harm (seems like a backward move??) but the move from -Os to -O2 has definitely harmed my TEXT RAM.

              Possible to make it work with the other settings??

Leave a Reply

Your email address will not be published. Required fields are marked *