Adding a popular 5Volt 4 channel relay board to a 3V3 processor (beginners)

Chinese webshops sell a 5Volt 4 channel relay board that is quite popular amongst hobbyist. Sometimes I get the question if it can be used with a 3V3 microprocessor. This ofcourse is very basic Ohms law, so just skip this article if you already know, but for those who still have questions: read on.

Well in short: any relay can be made to work with any microprocessor, but lets look at how this can be done in an efficient way.

Normally, if you have a bare 5Volt relay, it would only require a transistor or a FET to drive the relay with any voltage sufficient to open the transistor. The relay board in question though already has some electronics around it, so lets have a look at the circuit:

It is quite clear that this is not the standard one transistor relay board: the Transistor driving the relay in itself is driven by an optocoupler, that also inverts the signal: It is a LOW on the INx input that will  bring the optocoupler to open, thus feeding the base of the transistor, that will subsequently open and activate the relay. The circuit also shows that the optocoupler can be fed from the same  voltage source as the  relay, or from a different voltage source.

First question we have to ask ourselves: is a 3v3 circuit enough to drive the board?
Well, suppose we feed the board entirely with 5Volt. If we then would connect a 3V3 processor pin such as from a raspberry or ESP8266 or  a modern Arduino, a LOW would indeed activate the relay, as described above, but what happens if we do a HIGH?
Well, with a HIGH there would be a voltage of (5-3.3=) 1.7 volt over the series resistor, the optocoupler and the LED. Given the fact that the  forward voltage of the  optocoupler is about 1.5 Volt and the forward voltage over the red LED is on average 2.2 Volt, that basically absorbs the 1.7 Volt making it safe to say there will be no current flowing through the optocoupler when the IO pin is made HIGH (3V3).
Factually, even if the ‘HIGH’  would only be 2.5 Volt, chances are slim that there would be any current flowing through the optocoupler.

Now ofcourse there are people who shrug at the idea of  having any 5Volt source connected to their 3V3 pins, so what happens if we  take away the Voltage jumper, feed the relay with 5Volt and the optocoupler with 3V3.

Well, suppose we again make the IO pin LOW, then there will be 3V3 over the optocoupler, the LED and the resistor.
The LED and the optocoupler will  have a voltage drop of 1.5+2.2=3.7 Volts. Ergo, there will not be any current flowing through the optocoupler and the relay will not be activated.

Therefore: yes, the relay board can be used with a 3V3 processor, but  you will have to feed the entire board with 5Volt.

But, what if I do not want the signal to be inverted, if I want a LOW to deactivate the relay and a HIGH to activate it?

Well, that is fairly easy: do that with an inverter. One can either use an inverter IC such as a 7404, 7414 or a 4069, but ofcourse then you’d have to wonder whether 3V3 would be enough input for those IC’s  (it is).
Another possibility is just a transistor with a base resistor 470-1000 Ohm (1x for every channel).



JSON, MQTT, PubSub and OpenHAB

Usually I communicate between my various nodes (arduino, esp8266) with MQTT and OpenHAB being the interface. Simple, just one string command per message. Now there comes a moment that it seems better or more convenient, to send more commands in one MQTT message, and that is where JSON comes in. JSON messages are basically an organized string and MQTT is good in sending strings.
Author of the Pubsub library Nick O’Leary’s advice on sending JSON’s is as follows:

However, you need a few tricks before you can send a string variable.

I started off by using the ArduinoJson library to make my JSON’s, but I was not quite happy with that. There are various ways to do it and you end up with some sort of data/char array.

Supposedly you could send that with "client.publish(topic, (char*) payload.c_str())". But for various reasons that didnt suit me so I used another method.

As I was a bit unhappy about adding the ArduinoJson library just to make a JSON string, I thought it would be  best to simply compile the string myself and send that. I am not saying there is no better way… and if there is, please do tell, but this worked for me.

Anyway, let’s start with something simple, I have a LiPo battery feeding an ESP8266 and in openHAB I like to keep track of the voltage of the battery, but I also like to display the Percentage of the battery charged. Ofcourse I can calculate the latter in OpenHAB, but i prefer to have the ESP8266 do that.
Note: the below calculation is for a Wemos D1, with a 100k  resistor  in series with A0. Together with the  residing voltage divider it makes a 100k/(100k+220k+100k) = 1/4.2 voltage divider,  reducing a 4.2Volt voltage to 1 Volt. Therefore a reading of 1023 (=1V) on the A0 port correlates with 4.2 Volt on the LiPo battery)

voltage=analogRead(A0);// (mind you, this is 1 V max)
voltage=voltage/1023.0;// so 1V max renders "1" here
voltage=voltage*4.2;// We want a max of 4.2, so multiply
percent=100*voltage/4.2;// and calculate percentage

The JSON string I need to make, will look as follows

{"Voltage": voltage_value,"Percent": percent_value}

to insert a quote in a string you need to use the backslash as an escape character, so, we make the JSON string as follows:

String payload="{\"Voltage\":"+String(voltage)+ ",\"Percent\":"+String(percent)+"} ";

So now we have the Stringvariable “payload” that we have to send via PubSub.

There may be various ways to do that, I did it as follows:

payload.toCharArray(data, (payload.length() + 1));

You could probably also use: client.publish("home/battery", payload.c_str());, but I didn’t*).
So, once in openHAB it is quite easy to process the the JSON, once the proper transformation Add-on is installed.

In the items file it then looks like this:

Number battery_garden "Battery Garden [%.1f V]"  (Back_Garden, batteries) {mqtt="<[mosquitto:home/battery:state:JSONPATH($.Voltage)]"}
Number battery_garden_pct "Battery Garden [%.0f %%]"  (Back_Garden, batteries) {mqtt="<[mosquitto:home/battery:state:JSONPATH($.Percent)]"}

And on screen like this:

OK, let’s try that with the reading results of a DHT11. Sure, there might be many reasons why you would want to send the humidity reading and temperature reasing as two different MQTT strings, but for arguments sake let’ s try it as a JSON.

We will first read the DHT11 sensor,then we will create a JSON that looks like {“Temperature”: 20.0, “Humidity”: 68}  with 20.0 and 68 of course just snapshot  examples. Subsequently we will create the MQTT message and display it in openHAB

float humid_coop = dht.readHumidity();
float temp_coop = dht.readTemperature();
String payload="{\"Humidity\":"+String(humid_coop)+ ",\"Temperature\":"+String(temp_coop)+"} ";
payload.toCharArray(data, (payload.length() + 1));
client.publish("home/coop", data);

and then in the itemsfile:

Number Humidity_Coop "Humidity Coop [%.0f %%]" (coop) {mqtt="<[mosquitto:home/coop:state:JSONPATH($.Humidity)]"}
Number Temperature_coop "Temperatuur [%.2f °C]" (coop) {mqtt="<[mosquitto:home/coop:state:JSONPATH($.Temperature)]"}

Let’s now have a look at sending more than just two data  values. Suppose you have an I/O chip (or more) that lets you read 12 channels at the same time, you could ofcourse make a JSON like this: {“channel1” : value1, “channel2″:value2,  ……..”channel12”: value12}, but that seems to send a lot of unnecessary data, so we will create the following JSON:

We do that as follows

String payload="{\"data\":["+String(ana0)+","+String(ana1)+","+String(ana2)+","+String(ana3)+","+String(ana4)+","+String(ana5)+","+String(ana6)+","+String(ana7)+","+String(ana8)+","+String(ana9)+","+String(ana10)+","+String(ana11)+ "]}";
payload.toCharArray(data, (payload.length() + 1));
client.publish("home/json", data);

in the MQTT channel in the items file we will now use a slightly different JSON format. The data will now be read by index number like:


the full itemsfile then will be

Number Moisture1 "Moisture 1 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[0])]"}
Number Moisture2 "Moisture 2 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[1])]"}
Number Moisture3 "Moisture 3 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[2])]"}
Number Moisture4 "Moisture 4 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[3])]"}
Number Moisture5 "Moisture 5 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[4])]"}
Number Moisture6 "Moisture 6 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[5])]"}
Number Moisture7 "Moisture 7 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[6])]"}
Number Moisture8 "Moisture 8 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[7])]"}
Number Moisture9 "Moisture 9 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[8])]"}
Number Moisture10 "Moisture 10 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[9])]"}
Number Moisture11 "Moisture 11 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[10])]"}
Number Moisture12 "Moisture 12 [%.0f]" <plantbed> (Garden_moist) {mqtt="<[mosquitto:home/json:state:JSONPATH($.data[11])]"}

and look like:

There is nothing stopping you from including data from more than 1 sensor in a JSON and send that all in one MQTT message. This way you could for instance put all the sensor data from say one room, or the entire garden in one MQTT message and send it.
Beware though that the default packet size supported by the PubSub client is 128 bytes. You can increase this limit by editing the value of MQTT_MAX_PACKET_SIZE in PubSubClient.h.
*)There is a fork of Nick O’Leary’s PubSub library, by Imroy, that will happily send:
client.publish(topic, String (value));

Adding an MCP23017 16 port IO expander to Arduino or Esp8266 or Attiny85 or……..

After I made this expander module, a ready made module
with this chip has become available. So I actually would advice anybody needing a 16 bit expander, to buy that one rather than build it. The module will cost you abt 1.50 euro, while the individual chip may set you back a euro or so.

I am not claiming that what I am describing here is earth shattering or trailblazing, because in fact it is very simple and no doubt has been done by many already. But sometimes what is simple for the one, is still a question mark for the other, so here is quick ‘how-to’ of adding 16 I/O ports to your microprocessor. This is especially handy when working with a chip like the ESP8266 that has only limited I/O
The MCP23017 is an I2C enabled 16 I/O port chip. That means that you only need 2 pins (yes with Vcc and ground it makes 4) to control the chip and the added advantage is that you can share I2C with various other devices as well.

The 16 I/O lines are divided into an 8 I/O PORT A and an 8 I/O PORT B. Both can be used as input as well as output. The chip also has 2 configurable interrupts (that I will not be using). The physical layout of the chip makes it quite easy to use it on a piece of strip board.

The circuit (at right) is rather simple. At a last moment I decided to leave out the pull up resistors so it would be more flexible to use together with other I/O devices. The 3 Address pins A0-A2 determine the I2C address that ranges from 0x20 (all pins on ground) to 0x27 (all pins on Vcc).
The chip  can take a Vcc from 2.7V to 5V and this is perfect for 3.3 Volt devices as  the modern arduino’s and the ESP8266 range of boards.

Using the chip in a program is fairly easy. There are good libraries available, but it might help if you know how to program the chip without a library.
In my case I have all  address lines tied to ground and therefore my I2C address is 0x20. Suppose I want to use all PORT A lines as outputs. I do that  as follows:

Wire.write(0x00); // IODIRA register
Wire.write(0x00); // set entire PORT A to output

For PORT B that  is rather similar:

Wire.write(0x01); // IODIRB register
Wire.write(0x00); // set entire PORT B to output

If we then want to send a specific value ‘X’ to that PORT A, we do that as follows

Wire.write(0x12); // address port A
Wire.write(X);  // value to send

‘X’ ofcourse is a byte value that determines whether we set a specific port HIGH or LOW.
If for instance ‘X’is ‘0’ that means we write a LOW to all PORT A outputs. If it is 255 that means we write a HIGH to all PORT A outputs.
To determine what value to send, consider the 8 I/O lines of PORT A as a byte in which the individual bits determine HIGH or LOW.
So if we only want to make PORTA.0 HIGH and the rest LOW, we write a binary value of 0b00000001 =1 to the A register. If we want to make PORTA.0 and PORTA.2 HIGH and the rest LOW we write a binary value of 0b00000101 = 5.
For PORT B it is similar:

Wire.write(0x13); // address PORT B
Wire.write(X);  // value to send

If we want to use PORT B (or PORT A for that matter) as input, we do that as follows:

Wire.write(0x13); // address PORT B
Wire.requestFrom(0x20, 1); // request one byte of data
byte; // store incoming byte into "input"

The byte “input” will vary between 0 and 255, in which the individual bits determine the input on the corresponding IO line. So if ‘input’  reads ‘3’  which in binary is 0b00000011, that means that both IO line 0 and 1  were HIGH and the rest LOW

#include <Wire.h> // Wire.h
byte input=0;
void setup()
  Wire.begin(); // wake up I2C bus
  Wire.write(0x00); // IODIRA register
  Wire.write(0x00); // set entire PORT A as output
void loop()
  // read the inputs of bank B
  Wire.requestFrom(0x20, 1);;
  // now send the input data to bank A
  Wire.write(0x12); // address PORT A
  Wire.write(input);    // PORT A
  delay(100); // for debounce

That’s basically it if you want to do the adressing yourself. Using a library, such as the one from Adafruit, makes it much easier though as it has commands to write and read from individual IO lines. One of the example programs to read a single button, looks  for instance like this:

#include <Wire.h> // Wire.h
#include "Adafruit_MCP23017.h"

// Basic pin reading and pullup test for the MCP23017 I/O expander
// public domain!
// Connect pin #12 of the expander to Analog 5 (i2c clock)
// Connect pin #13 of the expander to Analog 4 (i2c data)
// Connect pins #15, 16 and 17 of the expander to ground (address selection)
// Connect pin #9 of the expander to 5V (power)
// Connect pin #10 of the expander to ground (common ground)
// Connect pin #18 through a ~10kohm resistor to 5V (reset pin, active low)
// Input #0 is on pin 21 so connect a button or switch from there to ground

Adafruit_MCP23017 mcp;

void setup() 
mcp.begin();      // use default address 0
mcp.pinMode(0, INPUT);
mcp.pullUp(0, HIGH);  // turn on a 100K pullup internally
pinMode(13, OUTPUT);  // use the p13 LED as debugging

void loop() {
// The LED will 'echo' the button
digitalWrite(13, mcp.digitalRead(0));

If you want to use more than one MCP23017 do that as follows:

#define addr1 0 //addr1 =A2 low , A1 low , A0 low =000
#define addr2 1 //addr 2 = A2 low , A1 low , A0 high =001 

Mind you that “0” is in fact 0x20 and ‘1’ is in fact 0x21

If you are using the Adafruit library with the ESP8266, you will encounter a compilation error signaling it cannot find <avr/pgmspace.h>. The solution for this is easy:

Open the cpp file of the library.


#include <avr/pgmspace.h>


#if (defined(__AVR__))
#include <avr/pgmspace.h>
#include <pgmspace.h>

Reviewing the Wemos Battery Shield

wemosb3The Wemos battery shield seems an easy way to start battery feeding your Wemos D1 mini, but after using one for a while, it became obvious it is not for any serious battery use, mainly for two reasons: It is not efficient in using the battery power and the Wemos D1 itself is not efficient to be used with batteries.

The battery shield like all shields is to be plugged onto the Wemos D1 mini. It has two jacks: one for a battery and one for an USB plug that can be used to charge the battery. The charging circuit is built around a TP5410 Lipo charger. One therefore would expect a standard LiPo cell connector on the board but the LiPo cell connector on the board is an XH2.54 connector, whereas most LiPo cells come with a JST-PH connector. Another disappointment was that it came with male headers only. Apparently it is thought this shield should come on top of every other possible shield

Apparently he 5410 cranks up the battery voltage to 5 Volt and feeds this to the 5 Volt pin of the Wemos D1. Though this makes the Shield handy to use in combination with other shields that may rely on the voltage coming from the 5 Volt pin, it is likely a less efficient way of using the battery power as opposed to bringing it to 3.3 Volt directly via an LDO.

wemosbatteryshieldcircuitThere is not much technical information on the battery shield, but I did find a circuit that seems to almost come right out of a chinese datasheet of the TP5410, in which several configurations are shown.
Studying the circuit it seems that the 5 Volt coming from the USB, is connected to the 5 Volt pin of the shield, only through an SS32 Skottky diode. In itself that is no problem, unless one decides to use another input to that connector, e.g. a solar cell. The Wemos D1 mini has an RT9013 LDO regulator that has a max input of 5.5 Volt with an absolute Max rating of 6 Volt.
Considering that the SS32 Skottky diode has a forward voltage of 200 mV@200mA, a voltage of > 6.2 on the USB connector of the battery shield (say a 6 Volt solarpanel on a bright day) could already kill the RT9013 LDO on the Wemos D1 board, Eventhough Wemos states the shield can be supplied with 10 Volt, but maybe that is without it being connected to the Wemos D1 mini

Anyway, I hooked up a 720mAh Lipocell to the battery shield and uploades a sketch that measures the battery voltage (through a resistor on A0) and did an upload to Thingspeak every minute, being in deep sleep in between, just to see how long it would last.

My first observation was that the shield apparently does not fully charge the LiPo cell. It came to a max of 4.05 Volt before it switched to ‘Standby’. I made sure by using a Multimeter and indeed, only 4.05-4.1 volt on the cell.
shield5-6It worked pretty well after that, uploading the voltage to Thingspeak for 5 days and 5 hrs, when suddenly it stopped, with the battery voltage at 3.56 Volt, far above the 3 Volt minimum charge of a LiPo. Strange. The RT9013 has a drop of 250mV at 500mA, so even at 500mA the voltage on the ESP8266 still would have been 3.31 Volt. I have fed the 8266 with as low as 2.9 Volt and it was still working, so this was a strange finding. Resetting the Wemos did not bring it back to life… which was not so surprising because when i measured the LiPo (this was almost a day after the sketch stopped working… I was busy shopping for Newyears Eve), it was at 1.3 Volt. A quick connection to a USB port brought it back to life. A very strange finding indeed that warranted me to repeat the test (See below). But for now it doesnt seem like the battery shield is  the best solution for a serious battery dependend project.

As said before, the Wemos D1 Mini, also is not the most suitable for a battery operated project. True, the ESP8266 can be put to deepsleep and will only use around 77uA, but the Wemos board also has the CH340 FTDI to TTL chip on board. As that is directly connected to the 3.3 Volt line, it will always be active, drawing around 50uA which is sort of in the same ballparc as the ESP8266 itself. Without that CH340 it could operate 1.6 times longer.

Battery Voltage Wemos battery shield
Battery Voltage Wemos battery shield

The repeated test showed similar results: Charge to 4.05 V (should be 4.2 Volt) and apparently shuts off at 3.5 volt (should be 2.7 Volt)





batterijspaning10minA final test, in which the Wemos was put to sleep for 10 minutes, the battery lasted 18 days before it was shut off at 3.59 Volt

Apparently Harald has a more positive experience with the shield.

Connect ESP8266 with Thingspeak through LUA: Analog pin and Internal ReferenceVoltage

esp8266-thingspeakluaConnecting your ESP to Thingspeak with a program written in your Arduino IDE is quite easy and those programs come a dime a dozen.
However, when I went to look for a program that did the same in LUA I came a cross a number of programs that simply didn’t work.
So I had to give it a go myself.
The below program is more or less a skeleton for thingspeak connection. It reads two values and posts those on Thingspeak. As it was just for testing I didnt want to add a lot of sensors, so th eonly two values read are the Internal reference voltage and the analog value from ADC0.
To make the program easily adaptable, I readthe sensors in a seperate function.
If you want to add a variable resistor to test the ADC, mind you that it can only take 1 Volt input so you need a voltage divider that limits it to 1 volt instead of 3.3 Volt.
When using the program dont forget your network credentials and  Thingspeak API Key

WRITEKEY="T3I6T9YF67JE527" -- set your key
volt=0 --Internal reference voltage
ana=0 -- analog port A0 Mind you, this one has a 1 Volt max

--read sensor
function ReadSensor()
volt = node.readvdd33() -- 1V=1023
print("Internal Ref.Voltage: " ..(volt/1000).."."..(volt%1000).." Volt")
print("Analoog: "..ana)

-- send to 
function sendTS() 
conn = nil
conn = net.createConnection(net.TCP, 0)
conn:on("receive", function(conn, payload)success = true print(payload)end)
   function(conn, payload)
   conn:send('GET /update?key='..WRITEKEY..'&field1='..(volt/1000)..'.'..(volt%1000)..'&field2='..ana..'HTTP/1.1\r\n\
   Host:\r\nAccept: */*\r\nUser-Agent: Mozilla/4.0 (compatible; esp8266 Lua; Windows NT 5.1)\r\n\r\n')end)
conn:on("disconnection", function(conn, payload) print('Disconnected') end)


Now I haven’t been completely honest here, as it isnt really good practice to read the internal Voltage and the A0 port as it reads either the one or the other.
When using the  A0 to read internal voltage  in the Arduino IDE rather than in LUA ADC_MODE(ADC_VCC); in combination with ESP.getVcc() is now the correct way to do it. Using readvdd33 causes problems

Adding a PCF 8591 ADC/DAC to ESP8266-01

YL-40 module

The ESP8266-01 is a great WiFi enabled  microcontroller but it only has 4 I/O pins broken out. Fortunately it does support I2C protocol so in spite of  the low number of pins, there still is a lot of hardware that can be added.
As the ESP8266-01 has no analog inputs at all, adding an ADC to it is something I wanted to do. After all,  sensors as LDR or NTC are still analog.
The PCF8591 is such an ADC: it is  a single-chip, single‑supply low‑power 8‑bit CMOS data acquisition device with four analog inputs, one analog output and a serial I²C‑bus interface. Three address pins A0, A1 and A2 are used for programming the hardware address, allowing the use of up to eight devices connected to the I²C‑bus without additional hardware. I doubt whether I need more than 8 of those chips. Lets just start with one. Although one can get the individual chip, I have chosen for a module that actually already has some sensors on it:

  • AIN0 – Jumper P5 – Light Dependent Resistor (LDR)
  • AIN1 – Jumper P4 – Thermistor
  • AIN2 – Not connected
  • AIN3 – Jumper P6 – Potentiometer
YL-40 circuit

The I2C address of the PCF8591 is determined by the pins A0-A2. As they are close to the Ground pin, let’s start with grounding them (as they are on the module).
pcf8591adresThe address is 1001A2A1A0. With A2-A0 being LOW, that is 1001000=0x48.

base A2A1A0 Hex Dec
1001 000 48 72
1001 001 49 73
1001 010 4A 74
1001 011 4B 75
1001 100 4C 76
1001 101 4D 77
1001 110 4E 78
1001 111 4F 79

In some programs you will see the address as “0x90>>1” Which is 48 as well. The “0x90” counts the LSB of the 8 bit address, which is the R/W bit. With the Write Bit Low (=active) the full address is 10010000=0x90, but the rightshift 1 removes the LSB again, making it 0x48.
The module is hardwired to 0x48 as the three address lines are soldered to ground. So if you would want to use more than one module on the same I2C port you would need to do some de-soldering (or use   bare PCF8591 chips ofcourse).

Sunfounder board (expensive)

A similar but needlessly more expensive, non configurable board without the sensors is the sunfounder board.

Mini PCF8591 AD DA Shell Module

There is a (more expensive)  fully configurable module, that allows to set the  I2C address with jumpers. That module is daisy chainable with other I2C modules in the same range (there is for example a PCF8547 digital I/O module with similar connections). Making a module yrself is not hard either:

PCF8591 configured as 0x48

The control byte sets the operating mode of the PCF8591 and is described in section 7.2 of the datasheet, The upper nibble of the control register is used for enabling the analog output, and for programming the analog inputs as single-ended or differential inputs.
The lower nibble selects one of the analog input channels defined by the upper nibble. If the auto-increment flag is set the channel number is incremented automatically after each A/D conversion.
If the auto-increment mode is desired in applications where the internal oscillator is used, the analog output enable flag in the control byte (bit 6) should be set. This allows the internal oscillator to run continuously, thereby preventing conversion errors resulting from oscillator start-up delay. The analog output enable flag may be reset at other times to reduce quiescent power consumption.

As it is not my intention to explain the full innerworkings of the PCF8591, but just to show it is working with the ESP8266-01, I will skip a full technical discussion. For now it is enough to know that the PCF8591 can be read byte for byte, but it can also be read in ‘burst mode’, in which we read the 4 analog values all at once. The program I present is burstmode with autoincrement of the address. The reason we read 5 bytes instead of 4 is because the first byte contains old data. As the datasheet states in paragraph 8.4: “The first byte transmitted in a read cycle contains the conversion result code of the previous read cycle.”

#include "Wire.h"
int PCF8591=0x48; // I2C bus address
byte ana0, ana1, ana2, ana3;
void setup()
 Wire.pins(0,2);// just to make sure
 Wire.begin(0,2);// the SDA and SCL
void loop()
 Wire.beginTransmission(PCF8591); // wake up PCF8591
 Wire.write(0x04); // control byte: reads ADC0 then auto-increment
 Wire.endTransmission(); // end tranmission
 Wire.requestFrom(PCF8591, 5);;// throw this one away;;;;

Obviously when you have these values read you will need to do something with them: print them out, put them on yr own webpage or upload them to e.g. Thingspeak. As printing from the ESP8266-01  is not always easy, I will show you how to upload the values to Thingspeak:

#include  // ESP8266WiFi.h library
#include "Wire.h"
int PCF8591=0x48; // I2C bus address
byte ana0, ana1, ana2, ana3;

const char* ssid     = "YourNetworkSSID";
const char* password = "YourPassword";
const char* host = "";
const char* writeAPIKey = "YourWriteAPI";

void setup() {
  // Initialize sensor
 Wire.pins(0,2);// just to make sure
 Wire.begin(0,2);// the SDA and SCL

//  Connect to WiFi network
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {

void loop() {
 Wire.beginTransmission(PCF8591); // wake up PCF8591
 Wire.write(0x04); // control byte: reads ADC0 then auto-increment
 Wire.endTransmission(); // end tranmission
 Wire.requestFrom(PCF8591, 5);;;;;;

// make TCP connections
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {

  String url = "/update?key=";
  // Request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");

The PCF8591 does not only have  4 ADC channels but also 1 DAC channel. Writing to the DAC is as follows:

#define PCF8591 (0x48) // I2C bus address
void setup()
 Wire.pins(0,2);// just to make sure
void loop()
 for (int i=0; i<255; ++i)
 Wire.beginTransmission(PCF8591); // wake up PCF8591
 Wire.write(0x40); // turn on DAC b1000000

A the board normally uses the 3.3v supply as the reference voltage:
The input voltage is determined with:
vIn = value * (3.3 / 255)
and the output voltage is:
vOut = (value / 255) * 3.3
or to find the value for a given voltage:
value = (vOut / 3.3) * 255

So if for instance I would write  the value of 50 to the DAC. The voltage would be: 0.64V
In order to test that, I hooked up Aout to the A0 of an ESP 8266-12 and found a value of 192.
To calculate that to a voltage that is  (192/1023)=0.18V.  (Remember, the ESP gives readings from 0-1023, whereas 1023 being 1 Volt.)  So that is not particularly close.
Oddly though when I hooked up the Aout to AIN2 or AIN1 that didn seem to give reliable readings. A value of 50 written to Aout should give a value of 50 on AIN, but oddly it didnt

In my version of the board the NTC channel only varied between 255 and 254, changing the temperatuur didnt seem to have any influence, but removing jumper P4 made the value go all over the place so i presume the channel is ok, and only the NTC might not be OK.

Differential Input
The PCF8591 is capable of more, it is  for instance possible to do differential measurements.
Suppose you want to  measure the difference between AIN0 and AIN1.
You do that as follows:

#include "Wire.h"
int PCF8591=0x48; // I2C bus address 
int Raw = 0;
float Voltage = 0.0;

void setup()
 Wire.pins(0,2);// just to make sure
  Serial.begin(9600); //Not on an ESP8266-01 
  Wire.beginTransmission(PCF8591); // Wake up PCF8591
  Wire.write(0x11); // control Byte for differential input mode 
  Wire.endTransmission(); //
void loop()
  Wire.requestFrom(PCF8591, 1); // Get Data from channel 1;
 Voltage = (Raw * 3.3 )/ 255.0;
 Serial.print("Raw ADC = ");
 Serial.print(" Voltage = ");

If you want to hook up this chip to a Raspbery Pi, have a look here.
There are other ways of expanding the ADC capabilities of the ESP8266:


Using the 4 pins of the ESP8266-01

esp8266_henhouseThe limited number (4) of GPIO pins on the ESP8266-01 may seem like an obstacle, for any serious application.

Yet if one uses the pins in a smart way it is very well possible to do a lot with only those 4 pins.
In some of my recent postings, I have shown the use of a DHT11 a DS18B20, an OLED, an RTC and a BMP180 with the ESP8266-01.
In this posting I set out to use 4 sensors and a display, while also uploading the acquired data to Thingspeak. It actually is expanding on a project of monitoring the atmosphere in and around my chicken coop. Yes, you could call this a weatherstation, but it is just to illustrate the use of the 4 pins, you could easily make something else this way

I will be using 2 pins for I2C (BMP180 and OLED)
1 pin for 2 DS18B20 sensors via the OneWire protocol
1 pin for the DHT11
Although the ESP8266-01 now has all its pins used, I can still add more sensors (or actuators) through the OneWire protocol and/or via the I2C protocol.

So, what do we need:

  • ESP8266-01
  • 2x DS18B20
  • 1x DHT11
  • 1x BMP180
  • OLED (optional)

and ofcourse a breadboard, a 3.3 V PSU and some breadboard wires and a Thingspeak acount

Just some remarks regarding the BOM:

  • ESP8266-01
    Obviously the project is about utilizing the limited pins of the ESP8266-01, but if you still need to buy one, you could consider an ESP8266-12 that has more pins
  • DHT11
    A cheap all purpose humidity and temperature sensor. It is not hugely accurate but it will do. If you still need to buy one, you could opt for the DHT22 that is supposedly more accurate, but you could also opt for the AMS2321. That is a sort of DHT22 that is suitable for I2C, thus freeing another pin
  • BMP180
    measures temperature and Airpressure. It is the successor of the BMP085, but it also now has some successors itself. There is the (cheaper) BMP280, but you could also opt for the BME280 that measures temperature, airpresure AND humidity. That way you can save on the DHT/AMS sensor
  • OLED
    I just used that so I quickly could see whether the sensors were read, but you could just as well check it on Thingspeak. The OLED is too small anyway to print all the read values

The circuit

weerstationThe 4 pins of the ESP8266 are not indicated as such on the PCB, and most images only clearly state GPIO0 and GPIO2.
However the ESP826-01 has a a GPIO1 pin (the Tx pin) and a GPIO3 pin (the Rx pin).
i will be using those pins as follows

  • GPIO0 -> SDA pin of the I2C port
  • GPIO1 ->DHT11
  • GPIO2-> SCL pin of the I2C port
  • GPIO3-> OneWire Bus

As my I2C modules already have pull up resistors, I will not add any I2C pullup resistors there. The DS18B20 still needs a pull up resistor for which I used a 4k7, but it is really not that critical, a 10k is also good. The DHT11 supposedly also needs a pull-up resistor but I found it to work without one as well. adding a 4k7 resistor didnt change any of the readings, so I left it out. Many of the 3 pin DHT11 modules, already have a 10 k soldered onto the module.

I just realized that I didnt draw the connections for the OLED. That is because I only hooked it up for a quick check, but should you want to add it, it is just a matter of connecting SDA to SDA and SCL to SCL… and ofcourse the ground and Vcc pins to their counterparts

The program is quite straightforward. First it sets up the libraries and the sensors.
It attaches the DHT11 to pin 1 (Tx) and the OnWire bus for the DS18B20 to pin 3 (Rx). In order to use more than 1 DS18B20 sensor on the OneWire bus, you need to know their ‘unique adress’. If you do not have that then you need a program to read those addresses. Do that on an arduino for ease.

In the program you still have to provide your WiFi credentials as well as the write API for your Thingspeak Channel


   Field 1 temp roost (DHT11)
   Field 2 humidity roost (DHT11)
   field 3 Coop temperature (DS18B20)
   field 4 soil temperature (DS18B20)
   field 5 Airpressure (bmp180)
   field 6 Outside temperature (bmp180)
 * */
#include <DHT.h>
#include  <OneWire.h>//
#include <DallasTemperature.h> //
#include <Adafruit_BMP085.h>
#include <ESP8266WiFi.h>
#include "SSD1306.h"
SSD1306  display(0x3c, 0, 2);

#define DHTPIN 1  //GPIO1 (Tx)
#define DHTTYPE  DHT11
#define ONE_WIRE_BUS 3 // GPIO3=Rx

const char* ssid     = "YourSSID";
const char* password = "YourPassword";
const char* host = "";
const char* writeAPIKey = "W367812985"; //use YOUR writeApi

//DHT11 stuff
float temperature_buiten;
float temperature_buiten2;

//DS18b20 stuff
OneWire oneWire(ONE_WIRE_BUS); //oneWire instance to communicate with any OneWire devices
DallasTemperature sensors(&oneWire);// Pass address of our oneWire instance to Dallas Temperature.
DeviceAddress Probe01 = { 0x28, 0x0F, 0x2A, 0x28, 0x00, 0x00, 0x80, 0x9F};
DeviceAddress Probe02 = { 0x28, 0x10, 0xA4, 0x57, 0x04, 0x00, 0x00, 0xA9};

// bmp180 stuff
Adafruit_BMP085 bmp;

void setup() {
  //I2C stuff
  Wire.pins(0, 2);
  Wire.begin(0, 2);
  // Initialize sensors
  //dht 11 stuff
  //ds18b20 stuff
  // set the resolution to 10 bit (Can be 9 to 12 bits .. lower is faster)
  sensors.setResolution(Probe01, 10);
  sensors.setResolution(Probe02, 10);
  //bmp180 stuff
  if (!bmp.begin()) {
    //   Serial.println("No BMP180 / BMP085");
    //   while (1) {}

  //OLED stuff

  //  Connect to WiFi network
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {

void loop() {
  sensors.requestTemperatures(); // Send the command to get temperatures
  temperature_buiten = sensors.getTempC(Probe01);//
  temperature_buiten2 = sensors.getTempC(Probe02);//
  //dht11 stuff--------------------
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature();
  if (isnan(humidity) || isnan(temperature)) {
  //bmp stuff-------------------------
    String t= String(bmp.readTemperature());
    String p=String(bmp.readPressure());
  //OLED stuff--------------------------
  display.drawString(0,10,p);//bmp pressure

  // make TCP connections
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {

  String url = "/update?key=";
  url += writeAPIKey;
  url += "&field1=";
  url += String(temperature);// roost (DHT1)
  url += "&field2=";
  url += String(humidity);// roost (DHT11)
  url += "&field3=";
  url += String(temperature_buiten);//coop temperature (DS18B20 nr 1)
  url += "&field4=";
  url += String(temperature_buiten2); //soil temperature (DS18B29 nr 2)
  url +="&field5=";
  url +=String(bmp.readTemperature());//Outside temperature (BMP180)
  url +="&field6=";
  url +=String(bmp.readPressure());// Airpressure (BMP180)
  url += "\r\n";

  // Send request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n\r\n");



Currently this program only monitors, but what is to stop you from adding a BH1750 I2C light sensor to measure if it is evening or morning or an RTC to know the time of day and to open and close the door of the coop automatically with aid of a PCF8574 I2C I/O expansion card, or as it is already in the garden, add a PCF8591 or ADS1115 AD converter to measure soil humidity and activate a pump when necessary. Or maybe switching on the water basin heater when the temperature falls below zero
if there is an I2 C chip for it, the ESP8266 can probably use it.

NOTE: the Adafruit DHT library contains an error that may show up in bigger programs on an 8266. If the majority of readings result in “failed to read”, it is time to comment out two erroneous lines in the DHT.cpp file as in the picture below: