NodeMcu and I2C: How to start and using the PCF8574A I2C port expander on ESP-01 board

Many of us bought a few ESP-01 boards when they came out, before other boards that have more general purpose input/output pins (GPIO) exposed had become available, like the ESP-12.

The problem with the ESP-01 board is that it only has two GPIO pins available for doing something: GPIO0 and GPIO2. If we want more pins we need to buy another ESP board with more exposed pins, or use an I2C port expander. This is possible because the ESP8266 can create and use an I2C bus on those two pins, using either native programming or the NodeMcu firmware and the Lua language. On this post I’m using the NodeMcu and Lua language.

Setting up the hardware:

I’m using the PCF8574AP chips that I’ve bought on ebay (Original, fakes, who knows, they work..). The data sheet is here: PCF8574 datasheet. The important parts of this document are the I2C addresses that we need to used, and the way the quasi-bidirectional 8574 pins are used. The chip is quite simple to use because it only has one register to control the expander pins. We will get to that. Also these chips work fine with 3.3V and/or 5V, so we can connect them directly to the ESP8266 board pins.

On the ESP-01 board we need to connect the GPIO pins to the I2C bus pins of the PCF8574 chip. In my case I’ve defined the following:

  • SDA -> GPIO2 to PCF8574A pin 15
  • SCL -> GPIO0 to PCF8574A pin 14

We also need to add pull up resistors from these pins to VCC. I’m using 4K7 resistor. Regarding the address pins (PCF8574A pins 1,2,3), I’ve grounded them all, so the I2C address is the base address for the chip. In my case the chip is the PCF8574A variant which means that the base address is 0x38. If I pull up A0 to VCC, then the address becomes 0x39 for example.

Regarding the address it’s important to note that NodeMcu uses 7 bit addresses and the I2C functions themselves set the LSB (least significant bit) for reading or writing data. So the I2 addresses to use on the I2C functions are the 7 MSB as the addresses as we can see on the PCF8574 datasheet.

The final schema is the following:

PCF8574 Schematics

PCF8574 Schematics

After powering up, all PCF8574 pins should be at level high (VCC).

Checking the hardware:

There is an I2C scanner available on the web that allows to check if there are I2C devices present on the I2C bus. For example on this thread there are some Lua script examples:

I’ve used this variation of the script: i2_scan.lua

-- Based on work by sancho and zeroday among many other open source authors
-- This code is public domain, attribution to appreciated.

id=0  -- need this to identify (software) IC2 bus?
gpio_pin= {4,3,0,1,2} -- this array maps internal IO references to GPIO numbers

-- user defined function: see if device responds with ACK to i2c start
function find_dev(i2c_id, dev_addr)
     c=i2c.address(i2c_id, dev_addr ,i2c.TRANSMITTER)
     return c

print("Scanning all pins for I2C Bus device")
for scl=1,7 do
     for sda=1,7 do
          tmr.wdclr() -- call this to pat the (watch)dog!
          if sda~=scl then -- if the pins are the same then skip this round
               i2c.setup(id,sda,scl,i2c.SLOW) -- initialize i2c with our id and current pins in slow mode :-)
               for i=0,127 do -- TODO - skip invalid addresses 
                    if find_dev(id, i)==true then
                    print("Device is wired: SDA to GPIO - IO index "..sda)
                    print("Device is wired: SCL to GPIO - IO index "..scl)
                    print("Device found at address 0x"..string.format("%02X",i))

And the output is:

> dofile('i2c_scan.lua')
Scanning all pins for I2C Bus device
Device is wired: SDA to GPIO - IO index 4
Device is wired: SCL to GPIO - IO index 3
Device found at address 0x38

Nodemcu Lua I2C functions:

To use Nodemcu Lua I2C functions we need the following functions:

  • i2.setup ( bus_id , SDA_pin , SCL_pin , I2C_bus_speed) – This function should be called before any other I2C activity. The bus_id parameter is on this case always zero, and the SDA and SCL pins the Nodemcu GPIO pind definition. Check out the Github NodeMcu page for the mappings. In our case the GPIO2 is index 4 and GPIO0 is index 3. The last parameter is the I2C bus speed that always should be i2c.SLOW.

After initializing the i2c functions, we can now read or write to the device. The pattern for this is always the same:

  • i2c.start( bus_id )  -> Start the i2c transaction
  • i2c.address( bus_id , device_address, operation) -> For our case and for the PCF8574A the device_address is 0x38, and the operation is i2c.RECEIVER for reading data, or i2c.TRANSMITTER for writing data. Based on this parameter the LSB bit is added to the device address to define the operation, 1 for read, zero for writing and thus build the complete 8 bit address. This function also returns values: It returns true if the device acknowledges the operation.
  •, size) -> For the i2c.RECEIVER operation it reads size bytes into a string from the i2c bus. That is the value that is returned from the function.
  • i2c.write(bus_id, data) -> For the i2c.TRANSMITTER operation it writes the data on the i2c bus. The value returned is the number of bytes that where wrote.
  • i2c.stop(bus_id) -> Stops the i2c transaction.

For more complete information check the Nodemcu LUA api pages, for example: Nodemcu LUA api

Using the PCF8574

Now it is simple to use the I2C PCF8574 port expander from the ESP8266. We know the address and sine it only has one register it is easy to read and write into the chip. One interesting thing if something is written on the pins, the same value can be read if nothing is connected to it. Caution for not short circuiting… Please read the data sheet if wanting to use some pins as inputs.

Anyway, the simple code for blinking a led is the following: pcf8574_blinkled.lua

busid = 0  -- I2C Bus ID. Always zero
sda= 4     -- GPIO2 pin mapping is 4
scl= 3     -- GPIO0 pin mapping is 3

addr=0x38  -- the I2C address of our device
led = 0;  
-- Seting up the I2C bus.

-- Read from the pcf8574
function read_pcf8574(dev_addr)
     i2c.address(busid, dev_addr , i2c.RECEIVER)
     bdata =,1)  -- Reads one byte
     return bdata

-- Writes to the pcf8574
function write_pcf8574(dev_addr, value)
     i2c.address(busid, dev_addr, i2c.TRANSMITTER)

-- timer controller blinking led on pin 0.
function blink_led()
    write_pcf8574( addr,  led )
    if led == 0 then
      led = 0x01
      led = 0
    --print("Blink Led...")

-- Main program.

for i=1, 254 do 
    write_pcf8574( addr,  i )

tmr.alarm(2, 1000, 1, blink_led )

An that’s it. Eight new I/O pins expandable to much more on the ESP8266.

As we can see the read and write functions are quite simple since the PCF8574 chip only has one register. If we have a more complex device like an LCD or sensor, we might need first to write some command an the read the data. For example something like this:

function read_i2cdata(dev_addr, command, numbytes)
     i2c.address(busid, dev_addr , i2c.TRANSMITTER)
     i2c.write(busid,command)  -- Sends the command

     i2c.address(busid, dev_addr, i2c.RECEIVER)
     data =, numbytes)
     return data

Since there are so many I2C devices available, using I2C is really the only way of getting the most out of the ESP-01 boards.

ESP8266 NodeMcu Heap: Limitations and a small hack to graph heap usage

So I’m messing around with ESP8266 and the NodeMcu firmware that uses the Lua language.

The fact is that I’m hitting some limitations that are hindering development of something more complex than simple web servers or anything else for that matter. Limitations like it can’t be a network server and a client at the same time, no network sockets, like opening up a socket to a server and keep that connection open a use it across Lua files,  makes building things much harder when in fact this is a communication centred device… So at the end for more complex projects the native SDK and using the C language is the way to go.

Anyway, I’ve made a small hack that uses an Apache Web server with PHP installed and Dygraphs library to draw out the heap usage in a nice line graph:

Setting up the Apache Web Server: First make sure that you have Apache with PHP installed (for this hack I use PHP because it was already installed and configured). In my case, the Apache document root directory is /var/www .

Create a directory named data under /var/www and make sure that the user running the apache server process has rights to write on the directory. In my case the user is www-data:

ps -ef | grep apache
www-data 12118 697 0 Jan04 ? 00:00:00 /usr/sbin/apache2 -k start

So I needed to do chown www-data /var/www/data so that Apache can write data to that directory.
On that directory create a file named esp8266heap.csv where the first line has the following content:

Data , Heap

Just make sure that you add a new line at the end of this line.
Make sure that the rights are ok:

chmod o+w esp8266heap.csv
chown www-data:www-data esp8266heap.csv.

Setting up the code for receiving data:

Now the esp8266 will call a file named espdata.php passing through the query string the heap size: http://server_ip_name/espdata.php?heap=12345. This php file will get the heap query parameter and write it on the esp8266heap.csv file along with a time stamp. Here’s the code:

$now = new DateTime();
parse_str( html_entity_decode( $_SERVER['QUERY_STRING']) , $out);

if ( array_key_exists( 'heap' , $out ) ) {

echo "\n\r\n\rValor: " . $out['heap'];
$handle = fopen("/var/www/data/esp8266heap.csv", "a");

fwrite( $handle , $now->format('Y-m-d H:i:s') . " , " . $out['heap'] . "\n" );
fclose ( $handle ) ;


Changing the esp8266 code:

My ping.lua file (check my older posts), that is called every minute looks like this now:

conn=net.createConnection(net.TCP, false)
conn:on("receive", function(conn, payload) print("Get done.", payload) end )
conn:send("GET /esp8266data.php?heap=" .. node.heap() .." HTTP/1.1\r\nHost: xx.xx.xx.xx\r\n" .. "Connection: keep-alive\r\nAccept: */*\r\n\r\n")

And that’s it. Every single minute an esp8266 timer triggers and calls the above ping.lua file that makes the espdata.php store the current heap size in the esp8266heap.csv file.

Graphing the data:

For graphing the data I’m using the dygraphs javascript library called remotely from my page that I’ve called esp8266graph.html:

<script src="//"></script>
<div id="graphdiv2"
style="width:500px; height:300px;"></div>
<script type="text/javascript">
g2 = new Dygraph(
"data/esp8266heap.csv", // path to CSV file
{}          // options

And that’s it.

Seeing it all:

We can go now to http://server_ip_name/espgraph.html and see the esp8266 NodeMcu heap size variations in an interactive way. The final result show that with only the single file, heap keeps constant, then drops, and keeps constant again, then drops again, and so on, until the esp8266 reboots, and then the heap recovers and the process starts again…


We can see where it is heading… The sharp change at around 3AM was the NodeMcu reboot.

Anyway, this hack with dygraphs can be used for anything e can imagine…

ESP8266 NodeMcu and Lua language and some Arduino issues.

When using the ESP8266 chip with the original firmware, AT based command set, to perform operations, implies that something external, like an Arduino or RPi (Raspberry Pi), must be used to drive/command the chip through the serial port. This external device sends out AT commands and waits for responses.

While for some operations/applications, using the AT commands  might just work when using the Software Serial library from Arduino. In another words, using a software based serial port to communicate might work. The issue is that even at a slow rate of 9600 baud’s, Arduino has some trouble to follow the esp8266 output, due to the lack of flow control.

For example listing the available access points (AT+CWLAP) or even receiving simple web page from the internet, most of the time the Software Serial port only receives incomplete messages or garbled information. This can be worked around by increasing the software serial port buffer size, but is not a solid solution.

In fact without using the hardware port on the Arduino to connect to the ESP8266, I have doubts that some useful work can be done if we simultaneous also want to use the hardware port for debug ( connecting to the computer) and the software serial port to connect to the esp8266. We must use the hardware port for connecting to the esp8266 and use the software serial library for debugging purposes which brings other issues. This situation only arises on the Arduino Uno or Mini where only one hardware based serial port is available. The Arduino Mega has 4 hardware serial ports, so it doesn’t have this issue.

So why all about this ranting regarding Arduino, serial ports and the esp8266?

The fact is that the esp8266 is by itself a microprocessor and so it is able to do some processing on it’s own. So it’s is possible to only send or receive the already processed information needed from/for Arduino, without the Arduino being the processor that is controlling and defining the behaviour of the esp8266 chip like connecting to WIFI and so on. The AT command set is fine for some applications, but is so 80’s … 🙂

One of the solutions available is to replace the original based AT command set firmware for the ESP8266 with the NodeMcu firmware, that’s now open sourced, that allows to add some intelligence to the esp8266 chip, namely connecting, receiving, sending and waiting for data (server mode) using the Lua language running on the ESP8266 chip itself and not on an external device.

After flashing and restarting the esp8266 with the NodeMcu firmware, the esp8266 tries to load a file named init.lua.

Several words of warning regarding  when developing with this firmware:

– If init.lua file crashes, the NodeMcu restarts, and calls again init.lua that crashes again, that restarts NodeMcu and so on. You get the idea.  At initial stages do not develop using the init.lua file. The only way to recover from this crash loop is to flash the firmware again. Do your first developing in a file named testing.lua or init2.lua for example. When ready, just rename it to init.lua.

– NoceMcu is single threaded and the esp8266 has a WatchDog timer. Tight loops will trigger the watchdog timer and restart the firmware. If needed we can we use the tmr.wdclr() which resets the watchdog timer, but then other bad things happen due to the single threaded nature of the firmware, like losing wifi connection, timer triggers not activating, and so on.

A great way to develop is to use the ESPlorer IDE tool. It needs Java JDK 1.8 (Edit: It seems not anymore. JDK 7 might be fine), so make sure that version is installed in your Operation System. Then we can just do /opt/jre1.8/bin/java  -jar ESPlorer.jar to launch the IDE.

Let’s see how in Lua we can connect to WIFI.

The simplest code for init.lua is

-- This is a comment line
  print("Configuring WIFI....")   -- Print something to the serial port if connected
  wifi.setmode( wifi.STATION )    -- Client/Station mode. Other mode could be Access Point
  wifi.sta.config( SSID , APPWD)  -- Connect to SSID with password APPWD.
  -- Example: wifi.sta.config( "MYWNET" , "QuantumEncryptedPassword" )
  print( "Ip Address: " .. wifi.sta.getip() ) -- The .. is the string concatenate string operator. 
 dofile ( "RunMyCode.lua")

This code snippet will connect to WIFI, if successful print the IP, and then lauch the RunMyCode.lua file.

But what happens if the connection fails, or takes sometime? We may end lauching the RunMyCode.lua file without WIFI connectivity.

So I’ve lost a couple of hours, learning Lua and esp8266 quirks to develop my init.lua file, with a lot of help of the esp8266 forum where there are a lot of Lua code snippets and examples.

Due to the single threaded nature of the NodeMcu firmware is not possible to do this (a tight loop waiting for the wifi to connect):

isConnected = false
   -- Lets see if we are already connected by getting the IP
   ipAddr = wifi.sta.getip()
   if ( ( ipAddr ~= nil ) and  ( ipAddr ~= "" ) )then  -- Check if IP is valid
      isConnected = true

until ( isConnected == true )

This will hang the esp8266 and will kick the watch dog timer, restarting the firmware.  Also the only thread running is the the thread executing this repeat/until loop, and so this code snippet will never connect because the background functions that need to be run, so that the connection does happen, never have a chance to run and the isConnected will always be false. The fact is that after the firmware restart, on the lua prompt running the print ( wifi.sta.getip() ) shows that it connected… So the loop is in fact blocking everything else.

Another example:

isConnected = false
   -- Lets see if we are already connected by getting the IP
   ipAddr = wifi.sta.getip()
   if ( ( ipAddr ~= nil ) and  ( ipAddr ~= "" ) )then  -- Check if IP is valid
      isConnected = true
   tmr.wdclr()    -- Reset the watch dog timer.
until ( isConnected == true )

This is even worse because this code segment explicitly resets the watchdog, and so we might thing that the loop will succeed some time in the future, but it won’t. The only way to leave this is to reset by hardware. It seems that the only way that background functions can work, if we can say that, is to let the script end…

So how do we test the connection and we need to end the script? Well timers are the solution:

tmr.alarm ( Alarm_number , Number of mS , Repeat , function )

So this:  tmr.alarm ( 0 , 2500 , 1 , checkWifi )  will use timer #0 to call periodically each 2.5S the checkWifi function (NOTE the lack of () on the checkWifi parameter (checkWifi is a function)).  The Repeat parameter if zero is a one shot call, otherwise it will reset the alarm and trigger it again and again.

So we let the script end and periodically check if we have Wifi. If we have connectivity then we can lauch our code. And so this is my init.lua code:

-- Constants
APPWD   = "QuantumPassword"
CMDFILE = "ping.lua"   -- File that is executed after connection

-- Some control variables
wifiTrys     = 0      -- Counter of trys to connect to wifi
NUMWIFITRYS  = 200    -- Maximum number of WIFI Testings while waiting for connection

-- Change the code of this function that it calls your code.
function launch()
  print("Connected to WIFI!")
  print("IP Address: " .. wifi.sta.getip())
  -- Call our command file every minute.
  tmr.alarm(0, 60000, 1, function() dofile(CMDFILE) end )

function checkWIFI() 
  if ( wifiTrys > NUMWIFITRYS ) then
    print("Sorry. Not able to connect")
    ipAddr = wifi.sta.getip()
    if ( ( ipAddr ~= nil ) and  ( ipAddr ~= "" ) )then
      -- lauch()        -- Cannot call directly the function from here the timer... NodeMcu crashes...
      tmr.alarm( 1 , 500 , 0 , launch )
      -- Reset alarm again
      tmr.alarm( 0 , 2500 , 0 , checkWIFI)
      print("Checking WIFI..." .. wifiTrys)
      wifiTrys = wifiTrys + 1

print("-- Starting up! ")

-- Lets see if we are already connected by getting the IP
ipAddr = wifi.sta.getip()
if ( ( ipAddr == nil ) or  ( ipAddr == "" ) ) then
  -- We aren't connected, so let's connect
  print("Configuring WIFI....")
  wifi.setmode( wifi.STATION )
  wifi.sta.config( SSID , APPWD)
  print("Waiting for connection")
  tmr.alarm( 0 , 2500 , 0 , checkWIFI )  -- Call checkWIFI 2.5S in the future.
 -- We are connected, so just run the launch code.
-- Drop through here to let NodeMcu run

So we can see that to program the esp8266 in Lua using the NodeMcu firmware we need to change a bit our programming paradigms…