Friday, August 28, 2015

ESP8266 - NodeMCU - I2C - Programming considerations







The story behind:

  Many people are complaining about I2C communications problems, mostly related with the NodeMCU LUA.

  I found out that many of the related problems are generated from a easy to fall in trap, generated by the way how code is executed in case of a step-by-step debug run or using a interpreter console, as in our above NodeMCU LUA case.

   Let's take a very simple example code and with the help of a Oscilloscope let's see what's happening exactly. I have choosen a LM75 I2C temperature sensor for our example, as been widely used and also people heavily complained about communication problems with. For more details about the code take a look at the LM75 Temperature sensor Article.

address = 0x48  -- LM75 I2C Address
temp_reg = 0     -- temp reg

bus = 0              -- I2C bus
sda, scl = 2, 1   -- Used pins for SDA and SCL
 

-- init I2C Bus  
   i2c.setup(bus, sda, scl, i2c.SLOW)
 

-- LM 75 Communication
  i2c.start(bus)     
  i2c.address(bus, address, i2c.TRANSMITTER)
  i2c.write(bus, temp_reg)
  i2c.stop(bus)


  i2c.start(bus)
  i2c.address(bus, address, i2c.RECEIVER)
  c=i2c.read(bus, 2)
  i2c.stop(bus)

  h,l = string.byte(c,1,2)
  if h > 127 then h = h - 255 end       
  if l > 127 then l = 5 else l = 0 end  
  temp=string.format("%d.%d", h,l)
  print(temp)


If you take the code from above, save it on ESP8266 module as lm75_test.lua and run it after that as
dofile("lm75_test.lua")  you will see that the result is OK, typing the right temperature, 29C in our case.

LM75 - I2C Communication - 500 us timebase

  If we zoom in a bit, we will be able to identify also our code snippets:
  
   i2c.start(bus)     
  i2c.address(bus, address, i2c.TRANSMITTER)
  i2c.write(bus, temp_reg)
  i2c.stop(bus)


-> address    = 0x48 
-> temp_reg = 0x00 

LM75 - I2C Communication - zoom in - 1


  i2c.start(bus)
  i2c.address(bus, address, i2c.RECEIVER)
  c=i2c.read(bus, 2)
  i2c.stop(bus)

  h,l = string.byte(c,1,2)

 - > h = 29 (0x1D)  
 - >  l = 0 (0x00)

LM75 - I2C Communication - zoom in - 2

 So, as you can see above, all OK and confirmed also by decoding the I2C communication on the Oscilloscope.

Now, let's see what's happening when running the same code  from the console (just select the code and press "Execute Selection" button.

-> h = 255
-> l = 255
-> temp = 0.5

WHAT?! WHY?!

Let's take a look on the Osciloscope also for this Run, using the same timebase, 500us :

LM 75 - I2C Communication - Run from console - 500us



Confused?  Let's increase the time base to 500ms and let's see what we can found about:

LM 75 - I2C Communication - Run from console - 500ms

What is telling us the last picture? The TIMING now is totally wrong !!

 Why?
 Because of the way that the code is sended to the interpreter and the way that is run, line by line!!

And please remember , these nasty things might happen in some conditions, even in case of a compiled code, not interpreted, when the code is in a step-by-step debugger mode run!

Might look silly and trivial, but imagine a way more complex code, all working OK, adding some extra modules, start debugging for them and suddenly starting having problems with previous tested modules, some nice working ones!
If you fall in such a trap, you can end up to spin around in circles for weeks.

What can be the solution? Easy. Just use the Rule number one: keep sensitive things like communication/timing and stuff at "atomic" level! Reading carefully the Datasheets can help you to quickly identify the "atomic granulation".

How can be done that in a easy way? Simple. Just use a FUNCTION!

Rememer, using functions in you code will give you not only the advantage of high readability, a modular and structural programming, easy debugging, etc,  but also, if functions are designed properly, can help you to avoid a potential pitfall as the one above.

Corrected code:

address = 0x48  -- LM75 I2C Address
temp_reg = 0     -- temp reg

bus = 0              -- I2C bus
sda, scl = 2, 1   -- Used pins for SDA and SCL
 

-- init I2C Bus  
   i2c.setup(bus, sda, scl, i2c.SLOW)
 

-- LM 75 Communication
function read_temp()
  i2c.start(bus)     
  i2c.address(bus, address, i2c.TRANSMITTER)
  i2c.write(bus, temp_reg)
  i2c.stop(bus)


  i2c.start(bus)
  i2c.address(bus, address, i2c.RECEIVER)
  c=i2c.read(bus, 2)
  i2c.stop(bus)

  h,l = string.byte(c,1,2)
  if h > 127 then h = h - 255 end       
  if l > 127 then l = 5 else l = 0 end  
  temp=string.format("%d.%d", h,l)
  print(temp)

end

read_temp()

And Yes, you can refine it even better than that :)

function init_I2C(sda, scl)
    i2c.setup(bus, sda, scl, i2c.SLOW)
end

function read_temp()
  i2c.start(bus)     
  i2c.address(bus, address, i2c.TRANSMITTER)
  i2c.write(bus, temp_reg)
  i2c.stop(bus)


  i2c.start(bus)
  i2c.address(bus, address, i2c.RECEIVER)
  c=i2c.read(bus, 2)
  i2c.stop(bus)
end 


function print_temp(c)
  h,l = string.byte(c,1,2)
  if h > 127 then h = h - 255 end       
  if l > 127 then l = 5 else l = 0 end  
  temp=string.format("%d.%d", h,l)
  print(temp)

end

-->>
init_I2C(sda, scl)
read_temp()
 print_temp(c)
 29.0






4 comments:

Rocco said...

Can you help me figure out how to get the temperature (and others value) from the MPU-6050 accelerometer? I'm having issue with I2C. :/

Tracker J said...

Hi Rocco,
Unfortunatelly I don't have such a sensor :(. What's more exaclty your problem with? Are you able to receive any response from it?

Rocco said...

I'm able to receive a response from the mpu. I can even read the WHO_AM_I register. But when i try to read the X-Y-Z-axis registers or the temperature register i got value 0. :/ I've also open a new topic on the esp8266 forum, take a look: http://www.esp8266.com/viewtopic.php?f=19&t=5838&p=30611#p30611

I don't know why they returns me value 0.

Tracker J said...

For MPU-6050 you can take a look here: http://www.esp8266-projects.com/2015/12/mailbag-mpu6050-module-i2c-driver-init.html

Post a Comment