Experimenting with I2c on the ESP8266? I am – and I’m having a great time with it. If you’re using my code you don’t really need to think about it but if you’re hunting around for better I2c for the ESP – or maybe interfacing the ESP8266 to Arduino – then you’ll love the stuff I’m doing right now – and yes, I AM looking for I2c experts to tell me if I’m doing something wrong because it is all going rather too smoothly.
I decided to take the end from the info from the last blog entry and separate it off as I learn more about I2c (which to recall is a simple 2-wire multi-drop serial communication protocol with separate clock and data).
Why read this? Well, up to now because I spotted a howling issue in the WIRE library for Arduino, found an improvement on the ESP8266 SDK I2C code in the ESPRUINO changes to the Espressif I2c code – I think I found a mistake in it, fixed it then SAILED past the original speeds, reducing a 15ms package best case to well under 3ms while adding clock stretching into the bargain. I should clarify at this point that I am NOT an expert in I2C but hey – I can’t get this to fail so I might be onto something! Read on!
So firstly let’s recall – I’ve been trying to make an Arduino peripheral for the ESP8266 Home Control 2016 project. I started with the WIRE library in Arduino, working on a small NANO device. I discovered that there are two buffers in WIRE that are 32 bytes in length and hence limit packages to less than that – I replaced them with a couple of 128 byte buffers and solved lots of issues I’d been having – that is detailed in the last blog.
Where we left off, I’d made a test setup to send a message to the newly-created NANO peripheral which right now on my bench handles a QTECH display and some IO. The ESP8266 I2C is also talking to a BME280 temperature/pressure/humidity sensor and a PWM expander board – so there are 3 devices – some input, some output.
In the last blog entry I’d added a test command for the ESP which could be sent to the board serially or by MQTT and went like this…
{nano:9,7,”how are you today and the weather is nice – NO REALLY it certainly is.”,0}
Simple enough – send a message to device 9 (my chosen default address for the NANO peripheral), command 7, a string – and a dummy 0 on the end to tell the device not to bother sending any return info.
As you can see below, this operation takes, using the standard Espressif Ic2 Master code, 15ms in total – not stunningly fast but ok.
Flush with the success of finding that buffer issue, I decided to go into the ESP I2C library and have a tinker – changing all the delays into ‘’#defines so I could mess with them. So without any real effort I could reduce delays from 14ms to 6ms – a very worthwhile improvement. bear in mind however that the ESP code has no clock-stretching and hence no way for the peripheral to slow things down. I am told that the call-back routine within WIRE holds the clock down until it is finished, in order to slow things down – this would do nothing but cause havoc for the Espressif code.
Below is a read operation which I got to work properly after reading this video – as I had no idea what a re-start was – see the little green circle just after the third byte.
As you can see in the image above I address device 9 (18 as it is shifted left by one – i.e. 7 bit addresses but the bottom bit is reserved to indicate a read or write) – I fire command 2 to read port 4 – then with an I2s restart (start without a preceding stop), I resend to the device with the LSB set (ie 19) and get a value 0 back (from a pin) All in a matter of a millisecond.
I was at this point getting worried about lack of clock-stretching – in which a slave can hold the clock low to “stretch” things out a bit – about the only control the slave has!
It looked as if this forum might have some answers as their modification of the ESPRESSIF code certainly had clock stretching in it. It involved some changes and I decided to pick the best bits from their modification.
So they’ve simplified the code a little – while adding in clock-stretching – by replacing a simple delay with a check for clock low – easy enough when you look at it… but they also seemed to have completely messed up master_start – at least, they had it the opposite way to Espressif – and accordingly, repeated starts – as I use in getting data out of a Nano above – simply failed. The logic probe said this was all wrong.
So I reversed master_start back to the way Espressif have it –so I ended up with a mix of the old and new – and the result – well, up to now everything works – and that long 15ms string send was now reduced to under 5ms from 15ms.
I tested the code with the PWM chip I’ve been playing with and the BME280 – so that is both reading and writing and up to now both were working perfectly.
It was notable that the clock low period was now much longer than the clock high period and I wondered if there is any way to shorten that. You know when you get deeply into code and all of a sudden it all becomes clear. I realised that setting and checking ACK signals was a function – with all that entails – I changed that to a macro. I realised that delays in loops were really un-necessary as the loop itself would cause delays.. I took them out.
At this point I had backed everything up expecting my experiments to fail miserably. I tested the new code and everything worked. I got the logic probe out again.
My original test string at 15ms was now down to 3ms and according to the probe – all is well. I DID get it down to 2.5ms but the probe said I was doing restarts in the middle of the string – I’ll find another way around that one. Current state of the art – 2.9ms
As for reads – my original byte read was taking somewhat over 1ms – it now takes 0.25ms.
And of course I’ve concentrated on the read and write routines leaving the inter-byte handshaking pretty much along so maybe there’s another 10% improvement to make here without dipping into ESP assembly language.
https://bitbucket.org/scargill/esp-mqtt-dev/src
The driver and firmware libraries in the repository above contain the modified Espressif i2c_master code – and also other I2c wrappers for sending packages – they are fine – any further optimisation needs a good solid look at the one file (and it’s header) for i2c_master. Open to ideas (that don’t involve converting Arduino assembly language to work with the ESP SDK – been there, failed).
I have just noticed that in the write cycles, there is still more off-time than on for the clock – and so I just took a delay out – which SHOULD mean the off-time is way too slow but because of the time taken to call and process the function – we still end up with 0.9us on time and 1.3us off time – everything continues to work on the tests I have – yet the total time for that read drops from 0.25ms to 0.21ms and the string write time from 2.9ms to about 2.2ms – again – worthwhile improvements.
That simple change was made in the write routine…
void ICACHE_FLASH_ATTR
i2c_master_writeByte(uint8 wrdata)
{
uint8 dat;
sint8 i;for (i = 7; i >= 0; i–) {
dat = wrdata >> i;
I2C_MASTER_SET_DC(dat,0);
// i2c_master_wait(I2C_DELAY_5);
I2C_MASTER_SET_DC(dat,1);
while (!GPIO_INPUT_GET(GPIO_ID_PIN(I2C_MASTER_SCL_GPIO))) {}
I2C_MASTER_SET_DC(dat,0);
}
}
Logic analyser says yes, 3 test items say yes… how low can it go!
Great job ! Nonetheless this i2c blows !
Each millisecond at 80MHz is tens of thousands of clock cycles wasted for ESP8266. Ideally i2c should be created using timers so that it doesn’t waste CPU cycles waiting for time to pass. I don’t care how much time a transaction takes if it’s not wasting CPU time. Most i2c devices are 100 KHz anyways. I’ll try to make an implementation using timers if can’t find one and share it once done. The onboard SPI can be used to run i2c asynchronously too, but I suspect espressif have done a botched job there too and their spi doesn’t work on interrupts and wastes CPU cycles, though SPI registers’ info is available so a new better SPI lib can be created.
It’s just making me angry that espressif have done such a shoddy job with the software, I wonder how they were able to make such amazing hardware.
The I2c as used on the ESP8266 is nothing more than software – I never did find out why any hardware implementation does not work – you might be wrong – it might be they blew the original hardware implementation. In my case I don’t care – the operations are synchronous and it certainly does not stop background processes like WIFI working. Beware, if you start messing around with interrupt-driven timers – then you might open a can of worms – and other stuff may fail – in my software for example I use precise timing for RGB serial LEDS. You cannot run PWM at the same time as it messes up the timing – similarly, async I2c would do the same. Other manufacturers may have done a better job (or not – I don’t know) but you have to concede that no-body else can touch the ESP8266 for price. So – we put up with minor issues I guess.
Wrong blog entry.
Hey Pete,
I’ve used your $t $p $h to get them to the display, what i want to be able to do is send the payload of a topic to the display 🙂
My brain is starting to come to life here – this has nothing to do with the blog entry you’ve put this in – which caught me out.
So I assume you’re referring to this:
Topic: freddy/toesp
Payload: {hitachi:39,”$1MQTT test$2$i$3Time $t$4Date $d”}
It’s pretty simple – you’re sending a string to the payload …. just make up a string that comprises whatever it is you want to send.
Of COURSE, you can’t put variables into an inject – you’d do that in a function node… for example.
msg.topic=”freddy/toesp”;
msg.payload=”The temperature is ” + myvar;
return msg;
There it is – simples…
Superstar thank you Peter, I’ll have a go later on! My apologies for the wrong blog entry 🙂
Hi Again,
Hopefully you don’t mind a silly question, but clearly I’m doing something wrong –
My function node will not work with a +, so I assume I’m missing a declaration or similar? It’ll only successfully deploy with this
msg.topic=”testnode/toesp”;
msg.payload={hitachi:63,”$s$F$1Set temp is ” :”DesiredTemperature”};
return msg;
Which I assume is not right, as DesiredTemperature should currently give a figure of 15, instead is blank!
This should be in the LCD area – but I’m looking at the above and it does not make sense at all.
Could anyone offer me some pointers on how to get a global.set nodered value eg target temperature to display on the esp8266 using node red? I’ve got multiple esp8266’s, one is running the excellent firmware provided by Peter, but I’m stumped as to how to get a node red value out to it in the format it expects?
Thank you
You might want to clarify which LCD – Nextion? One of the I2c ones? The more info the better.
It’s one of the 16×2 i2c lcd ones, have it connected to the esp displaying default text, and have alternated between IP address, time, temp, humidity no problem!
What I’d like it to do, is be able to show for eg
Set temp: 16
Actual Temp:19
So it seems to me from what you say that you know how to send text to the display so this is more a programming issue than the display? If you’ve been able to send time, temperature and other sensor information I don’t understand the problem.
lol, latest video of Andreas Spiess, just published, is right on this stuff 😀
Can it just be that these delays were calibrated for 100 kHz (Standard mode) I2C speed only, and by suppressing them, you reach up to 400 kHz (Fast mode)?
See http://www.nxp.com/documents/user_manual/UM10204.pdf#page=35
The speed should be adjustable by the user and not fixed, but this may be just too fast for implementing it using bit-banging, as a simple function call is already taking too much time.
Just in case, the official I2C specs are here:
http://www.nxp.com/documents/user_manual/UM10204.pdf
If you skip the initial 5 page-long propaganda, you will find the applicable I2C-bus protocol features on page 8, just short of the RESTART condition (!?!).
As you can see, you are not missing any mandatory feature, and even implement the optional clock stretching one.
One thing I don’t like (not your change, original did it too) is that waiting for ack is just looping forever…
If the other side goes dark (disconnect/crash/…) the code hangs
I’d add a timeout to prevent this
One COULD put a timeout on both the ack AND the clock stretcher – but that would add a significant time hit..
I’m looking at the code – where is the holdup on ACK in my code?? Line # ??
So for clockstretch..
while ((!GPIO_INPUT_GET(GPIO_ID_PIN(I2C_MASTER_SCL_GPIO)))&&(–timeout)) {}
But that would add an overhead.
my mistake, I thought I saw a while() on reading SDA but they’re all on SCL
sorry for the confusion
I’ll check to see what the overhead is of timing out the stretch. Not ENTIRELY sure what to do if it times out 🙂
What confuses me is that the chip is supposed to support I2C and https://github.com/esp8266/esp8266-wiki/wiki/Memory-Map hints at I2C controller registers, yet every driver I’ve seen is just bitbanging (which you could do with any IO port)
Espressif do not support I2c in hardware.
Peter, without looking at your code (yet), what I assume is your fixes upped the clock frequency?
This will depend on the device you communicate with (some I2C devices can handle higher speeds, always mentioned in the datasheet), and on the wire length (clock skew).
See http://www.i2c-bus.org/speed/
I think Espressif themselves have confirmed there is no hardware I2c pr at least nothing that has ever been implemented using hardware registers – and certainly the software implementation could use any old port bits. Perhaps they intended hardware I2c but then discovered it didn’t work when the chips came out??
Good point – well I’m assuming short wires – I would imagine the devices sitting right next to the ESP. Worth mentioning though. Up to now the devices I have seem to be keeping up.. (crossed fingers) including the Nano and that is the one I’d expect to fail first.