I2C IO Module with 4 Relay Outputs and 4 Galvanic Isolated Inputs

Sometimes we need extra Inputs or Outputs on a device, or for use with a project. To implement it properly we also need a lot of additional electronic components to properly isolate these inputs and outputs, with the signals they switch, from our own project, because, let us be real, electronics and electrical devices in the real world do not all work with Arduino or ESP32/ESP8266 save voltages ( 5v and 3.3v ).

I will also tell you about a very special deal to get PCBs of your own made for only one (1) USD ( Including shipping with DHL )! No, I am not joking, and I am not crazy either… More on that later in the post…


It is thus extremely important to have a module that can effectively interface with inputs of 5.5v up to 32v DC ( optically Isolated up to 3000v ), and relay outputs, also optically isolated at 3000v. ( Note that the optical isolation voltage does not mean you can input that voltage level into the chip! It means that it can isolate the electronics on the safe side of the isolator from a voltage spike of up to that voltage!).

I also love using I2C, as it allows me to add modules onto an existing data bus while using only 2 GPIO lines on the MCU!

The module I am presenting to you today was designed to be operated from 5v DC. That includes the I2C data lines (SDA and SCL). If you need to interface to a 3.3v microprocessor, like an ESP32 or ESP8266, or even the new RP2040 or an STM32, you need to use a logic level converter.

The PCB uses the popular PCF8574 8 channel IO expander, which is extremely easy to use, and where you can connect up to 8 devices in a chain ( 16 if you use the PCF8574AT variant as well.. Meaning eight of each variant) This surely adds up to quite a lot of IO lines at a cost of only 2 GPIO on your MCU!

The Circuit diagram is below, and I will discuss each part briefly.

Schematic – Page 1

This is the Galvanic Isolated Input schematic. Each input operates at a voltage of 5.5v to 32v DC. Complete Galvanic Isolation between the Module and the remote input is in effect. Please note that you have to supply a remote ground from the device that provides the input. DO NOT connect the PCB Module ground to an isolated ground pin. This may still work but renders the galvanic Isolation for that input completely useless.


Relay Driver Schematic

This is the Relay driver schematic. Each relay output is driven through an optocoupler, as well as a transistor. Although this arrangement does not provide complete galvanic Isolation of the relay coil, it does protect your MCU from any voltage spikes caused by back-emf when the relay is de-energised. The Relay contacts themselves, being magnetically actuated by the coil, are in themselves Galvanically Isolated from the rest of the PCB.

I2C Control Schematic

Finally, we have the I2C IO Expander schematic, with a 5v LDO regulator, capable of providing up to 600mA of current to the PCB. The PCF8574 Chip’s address is selectable with DipSwitch SW1 so that you can use multiple PCBs at the same time if you should choose to do so. The only note on that is that you should not connect the 5v lines of each individual PCB together. You should also only connect the GND and SDA, SCL lines back to the MCU.

Raw PCB Layout

Earlier on in the post, I promised to tell you about a very special deal…

Well, here it is, as well as some details about the sponsor of this very exciting deal…

PCBPartner.com is owned and operated by Kinji Group, which was established in 1997. We have over 20 years of experience in PCB manufacturing, PCB design, component manufacturing and distribution, PCB assembly and PCB cam software development.

While Kinji Group has 3 PCB factories in China, we have also developed strategic partnerships with more than 15 other factories around Asia. We, therefore, have a large group of specialists in PCB manufacturing, quality control, technical support and part sourcing to support your innovative ideas and products.

Our over 500 employees are spread across 8 branches in Mainland China (Shenzhen, Dongguan, Shanghai, Wuxi, Chengdu, Xiamen), Hong Kong SAR, and Taiwan. And we’re still growing!


We’re confident once you try us out, we’ll become your PCB Partner. And if not? Well, you’ll have scored some free PCB! So why not take us for a spin, you’ve got nothing to lose.

We, MakerIoT2020.com, have decided to give it a go and send this particular PCB to PCBPartner.com for manufacturing. So far, while we are still waiting to receive the PCB, ( Weekends happen 🙂 ), We are very happy with the ease of use of the online ordering system provided.

We would also like to point out that this special order will only be available until the end of March 2022,
as well as that there are a few conditions:

Promotion ends  March 31st 2022
Each new customer can enjoy free PCB on their first order
This promotion applies to
1-2 layers of FR4 PCB, up to 100x100mm, 10pcs, with Green Solder Mask
4 layer of FR4 PCB, up to 50x50mm, 10pcs, with Green Solder Mask
1 layer Aluminum PCB, up to 100x100mm, 10pcs
This PCB promotion cannot be used with other discounts or other promotional activities



For a full list of conditions, and countries that may participate in this offer, please click on the link here

Let us have a look at the entire ordering process..

Once you click on the PCBPartner.com link, you will be taken to their website, where you should sign up, which is free and easy… We used our Google.com account details and were ready to order in seconds…

PCBPartner Start Page

You can now Login with your new credentials ( after registering using this special link ). Then click on the FR4 button to start the order process…

FR4 PBC Quote Form – Before uploading your Gerber Files

Enter the specific details for the manufacturing of your PCB, and upload your Gerber files.

After uploading your Gerber Files.

Continue selecting options for your PCB order…
Make sure to select DHL shipping, to take advantage of the special 1USD option, and click on the ADD to Cart Button…

Quote added to your shopping cart.

You will now get a message that your enquiry has been submitted successfully.

Click on the “Under review” button, to see your quote status… In my case, it took about 5 minutes for the review to pass, and be able to checkout and pay for the order…

PCB order under review

Once the review has passed, you will see a pending payment,

Payment Pending

You may now click on the “Proceed to Payment” option

Add your shipping address, and choose your payment option.

At this moment in time, only two payment options are supported, Paypal ( as well as Debit and Credit cards) and Direct Bank Transfer. I believe more options will be made available in future..
Checkout with Paypal

In my case, I chose Paypal and paid by Debit card.

Enter your card details
After Payment.

After payment was made successfully, you can also check on the status of your order…

Review your order status

You can also review your order at any stage before or after payment, as well as get progress reports of the manufacturing process.

PCB Order Status.

In conclusion, I would like to say that it was quite easy to order and make payment. The Website is easy to use, and everything is clear and easy to understand. The PCB was well manufactured and seems to be quite good quality

Port Extender Card for the MCU-8266-12E

Port Extender Card for the MCU-8266-12E IoT Controller

Introduction

After quite a few experiments, and playing with a lot of ideas, we have finally produced and tested an almost final prototype for the MCU-8266-12E IoT Controller Port Extender Card. While the baseboard already has quite a lot of free GPIO pins for additional sensors and devices, It did however have quite a few issues, namely a lack of sufficient Power outputs, difficulty access to the I2C bus, as well as only 2 relay outputs. Granted that you do have access to unused pins on the PCF8574 Port Extender, We nonetheless decided that an add-on card would definitely make sense to allow this device to really be more useable.

While looking at various ideas for this card, the most flexible seemed to be the APE Protocol device as documented in ESPHome. They used a standard Arduino board for that, but we decided that, after testing it with an Arduino Nano, since it seems to work well, let us just design a dedicated PCB. It also looks much better as well 🙂

Some Features (Aside from being a fully functional Arduino clone as well)

1). Dedicated LDO Regulators for 5v and 3.3v (800mA each), with jumpers to switch them on or off (receive power only from the IoT Motherboard).
2.) Dedicated Logic Level Converter on the I2C Bus ( This is sort of very much needed 🙂 The Atmega 328P-AU is running at 5v on this device, to enable it to run at 16Mhz.. and the ESP8266 on the Motherboard is a 3.3v device..

There are also 3x 3.3V I2C Headers, complete with 3.3v and Ground, as well as a single 5v I2C header
3). 8 Analog Inputs ( While practically you can only use 6 of these if you use I2C )
4). Voltage Divider provided on A0 to measure VIN ( to be safe, we calculated the resistors for 22v)
5). 100R current limiting resistor on A1 and A2, to measure 5v and 3.3v as well…
Analog inputs A0, A1 and A2 can be switched back to normal operation by changing the jumper at J2,J3 or J4 from On to Off.
6). 12 Digital Inputs/Outputs (14 if you use D0 and D1 as well), as well as a Jumper to remove the LED on D13.
7). Full access to the PCF8574 and ESP8266 Pins from the motherboard below.

Pictures of the PCB

Circuit Diagram

ATMega328P-AU Circuit diagram with LDO Regulators, headers and supporting circuitry.
Analog measuring circuitry, level converters and supporting circuitry and headers

Uploading Code to the ATMega328P

Uploading code to the device requires the use of either an ISCP programmer ( Arduino as ISP works well ) or in the case of a pre-boot loaded chip, a USB-to-Serial converter. We did not find it necessary to add a dedicated USB-to-Serial converter onto the PCB. It is quite easy enough to do any flashing with the tools mentioned above.


Make sure that the PCB is not stacked when doing this. ( This will prevent excessive current use of other components when you supply 5v to the PE card.


Procedure to upload using ICSP

During assembly, you are required to solder a single 90-degree bend pin header on the bottom side of the PCB, in the same hole as the board side edge of the RESET push-button. This will serve as the RESET Pin for the ISCP. Other connections are as follows:

H2 Header <- > ICSP Programmer
MOSI (E11~) – MOSI ( or Pin 11 on Arduino as ISP )
MISO (E12 ) – MISO ( or Pin 12 on Arduino as ISP )
SCK (E13) – D13 (or Pin 13 on Arduino as ISP )
RESET – D10 (or Pin 10 on Arduino as ISP )

5v and Ground from Arduino as ISP or ISCP Programmer to any 5v and ground pin on the PE Card

Please note the description above for assembly of the RESET pin header



Procedure to upload using USB-to-Serial converter

H1 Header

E0/Rx <- to Tx of USB-to-Serial converter
E1/Tx -> to Rx of USB-to-Serial converter

H2 Header

DTR <-> to DTR of USB-to-Serial converter [ This connection is needed for successful uploading. Don’t leave it out ]


5v and Ground from the USB-to-Serial converter to any 5v and ground pin on the PE Card


Testing with ESPHome APE protocol and the MCU-8266-12E IoT controller

The following Arduino Sketch needs to be uploaded to the device.
It will allow the device to function as a custom I2C device. Feel free to change the I2C address in the sketch as you choose, but remember to use the same address in your ESPHome YAML configuration file

/*
Ports:
  0 0 .. 13 13
  A0: 14, A1: 15, A2: 16, A3: 17: A4: 18: A5: 19: A6: 20, A7: 21
  port bits: 5 ... 0..32
  0:   0: 00000
  1:   1: 00001
  A7: 21: 10101
*/

#include <Arduino.h>
#include <Wire.h>

//#define DEBUG // remove debug so pin 0 and 1 can be used for IO

#define I2C_ADDRESS 8

void onRequest();
void onReceive(int);

void setup()
{
#ifdef DEBUG
  Serial.begin(115200);
  Serial.println(F("Init "));
#endif

  analogReference(INTERNAL);

  Wire.begin(I2C_ADDRESS);
  Wire.onRequest(onRequest);
  Wire.onReceive(onReceive);

#ifdef DEBUG
  Serial.println(F("Wire ok"));
#endif
}

void loop()
{
  //int temp = analogRead(A1);
  //Serial.println(temp);
}

volatile byte buffer[3];
volatile byte len = 1;

#define DIGITAL_READ(b, pin, mask) \
  if (digitalRead(pin))            \
    buffer[b] |= mask;

void readDigital()
{
  len = 3;
  buffer[0] = 0;
  DIGITAL_READ(0, 0, 1);
  DIGITAL_READ(0, 1, 2);
  DIGITAL_READ(0, 2, 4);
  DIGITAL_READ(0, 3, 8);
  DIGITAL_READ(0, 4, 16);
  DIGITAL_READ(0, 5, 32);
  DIGITAL_READ(0, 6, 64);
  DIGITAL_READ(0, 7, 128);

  buffer[1] = 0;
  DIGITAL_READ(1, 8, 1);
  DIGITAL_READ(1, 9, 2);
  DIGITAL_READ(1, 10, 4);
  DIGITAL_READ(1, 11, 8);
  DIGITAL_READ(1, 12, 16);
  DIGITAL_READ(1, 13, 32);
  DIGITAL_READ(1, A0, 64);
  DIGITAL_READ(1, A1, 128);

  buffer[2] = 0;
  DIGITAL_READ(2, A2, 1);
  DIGITAL_READ(2, A3, 2);

// I2C
//DIGITAL_READ(2, A4, 4);
//DIGITAL_READ(2, A5, 8);

// DIGITAL READ not supports on A3 .. A7
#ifdef DEBUG_READ
  Serial.print(F("Read 3 bytes: "));
  Serial.print(buffer[0]);
  Serial.print(' ');
  Serial.print(buffer[1]);
  Serial.print(' ');
  Serial.println(buffer[2]);

#endif
}
void readAnalog(int pin)
{
  int val = analogRead(A0 + pin);
  len = 2;
  buffer[0] = val & 0xFF;
  buffer[1] = (val >> 8) & 0b11;
#ifdef DEBUG_READ
  Serial.print(F("Read analog pin "));
  Serial.println(pin);
#endif
}

void onRequest()
{
  Wire.write(const_cast<uint8_t *>(buffer), len);
}

#define CMD_DIGITAL_READ 0x0

#define CMD_WRITE_ANALOG 0x2
#define CMD_WRITE_DIGITAL_HIGH 0x3
#define CMD_WRITE_DIGITAL_LOW 0x4

#define CMD_SETUP_PIN_OUTPUT 0x5
#define CMD_SETUP_PIN_INPUT_PULLUP 0x6
#define CMD_SETUP_PIN_INPUT 0x7

// 8 analog registers.. A0 to A7
// A4 and A5 not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8
// ....
#define CMD_ANALOG_READ_A7 0b1111 // 0xF

#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x11

void onReceive(int numBytes)
{
#ifdef DEBUG_READ
  Serial.print("Received bytes: ");
  Serial.println(numBytes);
#endif
  int cmd = Wire.read();

  switch (cmd)
  {
  case CMD_DIGITAL_READ:
    readDigital();
    break;
  }

  if (cmd >= CMD_ANALOG_READ_A0 && cmd <= CMD_ANALOG_READ_A7)
  {
    readAnalog(cmd & 0b111);
    return;
  }

  int pin = Wire.read();

  switch (cmd)
  {
  case CMD_WRITE_DIGITAL_HIGH:
  case CMD_WRITE_DIGITAL_LOW:
  {
    bool output = cmd == CMD_WRITE_DIGITAL_HIGH;
    digitalWrite(pin, output);
#ifdef DEBUG
    Serial.print(F("Pin "));
    Serial.print(pin);
    Serial.println(output ? F(" HIGH") : F(" LOW"));
#endif
    break;
  }
  case CMD_WRITE_ANALOG:
  {
    int val = Wire.read() & (Wire.read() << 8);
    analogWrite(pin, val);
#ifdef DEBUG
    Serial.print(F("Pin "));
    Serial.print(pin);
    Serial.print(F(" Analog write "));
    Serial.println(val);
#endif
    break;
  }
  case CMD_SETUP_PIN_OUTPUT:
    pinMode(pin, OUTPUT);
#ifdef DEBUG
    Serial.print(F("Pin "));
    Serial.print(pin);
    Serial.println(F(" OUTPUT"));
#endif
    break;
  case CMD_SETUP_PIN_INPUT:
    pinMode(pin, INPUT);
#ifdef DEBUG
    Serial.print(F("Pin "));
    Serial.print(pin);
    Serial.println(F("INPUT"));
#endif
    break;
  case CMD_SETUP_PIN_INPUT_PULLUP:
    pinMode(pin, INPUT_PULLUP);
#ifdef DEBUG
    Serial.print(F("Pin "));
    Serial.print(pin);
    Serial.println(F("INPUT PULLUP"));
#endif
    break;
  case CMD_SETUP_ANALOG_INTERNAL:
    analogReference(INTERNAL);
#ifdef DEBUG
    Serial.println(F("Analog reference INTERNAL"));
#endif
    break;
  case CMD_SETUP_ANALOG_DEFAULT:
    analogReference(DEFAULT);
#ifdef DEBUG
    Serial.println(F("Analog reference DEFAULT"));
#endif
    break;
  }
}

The following C header file needs to be uploaded to your Home Assistant ESPHome folder.

// Must disable logging if using logging in main.cpp or in other custom components for the
//  __c causes a section type conflict with __c thingy
// you can enable logging and use it if you enable this in logger:
/*
logger:
  level: DEBUG
  esp8266_store_log_strings_in_flash: False
  */

//#define APE_LOGGING

// take advantage of LOG_ defines to decide which code to include
#ifdef LOG_BINARY_OUTPUT
#define APE_BINARY_OUTPUT
#endif
#ifdef LOG_BINARY_SENSOR
#define APE_BINARY_SENSOR
#endif
#ifdef LOG_SENSOR
#define APE_SENSOR
#endif

static const char *TAGape = "ape";

#define APE_CMD_DIGITAL_READ 0
#define APE_CMD_WRITE_ANALOG 2
#define APE_CMD_WRITE_DIGITAL_HIGH 3
#define APE_CMD_WRITE_DIGITAL_LOW 4
#define APE_CMD_SETUP_PIN_OUTPUT 5
#define APE_CMD_SETUP_PIN_INPUT_PULLUP 6
#define APE_CMD_SETUP_PIN_INPUT 7
// 8 analog registers.. A0 to A7
// A4 and A5 not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8
// ....
#define CMD_ANALOG_READ_A7 0b1111 // 0xF

#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x11

#define get_ape(constructor) static_cast<ArduinoPortExpander *>(constructor.get_component(0))

#define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin)
#define ape_binary_sensor(ape, pin) get_ape(ape)->get_binary_sensor(pin)
#define ape_analog_input(ape, pin) get_ape(ape)->get_analog_input(pin)

class ArduinoPortExpander;

using namespace esphome;

#ifdef APE_BINARY_OUTPUT
class ApeBinaryOutput : public output::BinaryOutput
{
public:
  ApeBinaryOutput(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->parent_ = parent;
    this->pin_ = pin;
  }
  void write_state(bool state) override;
  uint8_t get_pin() { return this->pin_; }

protected:
  ArduinoPortExpander *parent_;
  uint8_t pin_;
  // Pins are setup as output after the state is written, Arduino has no open drain outputs, after setting an output it will either sink or source thus activating outputs writen to false during a flick.
  bool setup_{true};
  bool state_{false};

  friend class ArduinoPortExpander;
};
#endif

#ifdef APE_BINARY_SENSOR
class ApeBinarySensor : public binary_sensor::BinarySensor
{
public:
  ApeBinarySensor(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->pin_ = pin;
  }
  uint8_t get_pin() { return this->pin_; }

protected:
  uint8_t pin_;
};
#endif

#ifdef APE_SENSOR
class ApeAnalogInput : public sensor::Sensor
{
public:
  ApeAnalogInput(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->pin_ = pin;
  }
  uint8_t get_pin() { return this->pin_; }

protected:
  uint8_t pin_;
};
#endif

class ArduinoPortExpander : public Component, public I2CDevice
{
public:
  ArduinoPortExpander(I2CBus *bus, uint8_t address, bool vref_default = false)
  {
    set_i2c_address(address);
    set_i2c_bus(bus);
    this->vref_default_ = vref_default;
  }
  void setup() override
  {
#ifdef APE_LOGGING
    ESP_LOGCONFIG(TAGape, "Setting up ArduinoPortExpander at %#02x ...", address_);
#endif

    /* We cannot setup as usual as arduino boots later than esp8266
            Poll i2c bus for our Arduino for a n seconds instead of failing fast,
            also this is important as pin setup (INPUT_PULLUP, OUTPUT it's done once)
        */
    this->configure_timeout_ = millis() + 5000;
  }
  void loop() override
  {
    if (millis() < this->configure_timeout_)
    {
      bool try_configure = millis() % 100 > 50;
      if (try_configure == this->configure_)
        return;
      this->configure_ = try_configure;

      if (ERROR_OK == this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 3))
      {
#ifdef APE_LOGGING
        ESP_LOGCONFIG(TAGape, "ArduinoPortExpander found at %#02x", address_);
#endif
        delay(10);
        if (this->vref_default_)
        {
          this->write_register(CMD_SETUP_ANALOG_DEFAULT, nullptr, 0); // 0: unused
        }

        // Config success
        this->configure_timeout_ = 0;
        this->status_clear_error();
#ifdef APE_BINARY_SENSOR
        for (ApeBinarySensor *pin : this->input_pins_)
        {
          App.feed_wdt();
          uint8_t pinNo = pin->get_pin();
#ifdef APE_LOGGING
          ESP_LOGCONFIG(TAGape, "Setup input pin %d", pinNo);
#endif
          this->write_register(APE_CMD_SETUP_PIN_INPUT_PULLUP, &pinNo, 1);
          delay(20);
        }
#endif
#ifdef APE_BINARY_OUTPUT
        for (ApeBinaryOutput *output : this->output_pins_)
        {
          if (!output->setup_)
          { // this output has a valid value already
            this->write_state(output->pin_, output->state_, true);
            App.feed_wdt();
            delay(20);
          }
        }
#endif
#ifdef APE_SENSOR
        for (ApeAnalogInput *sensor : this->analog_pins_)
        {
          App.feed_wdt();
          uint8_t pinNo = sensor->get_pin();
#ifdef APE_LOGGING
          ESP_LOGCONFIG(TAGape, "Setup analog input pin %d", pinNo);
#endif
          this->write_register(APE_CMD_SETUP_PIN_INPUT, &pinNo, 1);
          delay(20);
        }
#endif
        return;
      }
      // Still not answering
      return;
    }
    if (this->configure_timeout_ != 0 && millis() > this->configure_timeout_)
    {
#ifdef APE_LOGGING
      ESP_LOGE(TAGape, "ArduinoPortExpander NOT found at %#02x", address_);
#endif
      this->mark_failed();
      return;
    }

#ifdef APE_BINARY_SENSOR
    if (ERROR_OK != this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 3))
    {
#ifdef APE_LOGGING
      ESP_LOGE(TAGape, "Error reading. Reconfiguring pending.");
#endif
      this->status_set_error();
      this->configure_timeout_ = millis() + 5000;
      return;
    }
    for (ApeBinarySensor *pin : this->input_pins_)
    {
      uint8_t pinNo = pin->get_pin();

      uint8_t bit = pinNo % 8;
      uint8_t value = pinNo < 8 ? this->read_buffer_[0] : pinNo < 16 ? this->read_buffer_[1] : this->read_buffer_[2];
      bool ret = value & (1 << bit);
      if (this->initial_state_)
        pin->publish_initial_state(ret);
      else
        pin->publish_state(ret);
    }
#endif
#ifdef APE_SENSOR
    for (ApeAnalogInput *pin : this->analog_pins_)
    {
      uint8_t pinNo = pin->get_pin();
      pin->publish_state(analogRead(pinNo));
    }
#endif
    this->initial_state_ = false;
  }

#ifdef APE_SENSOR
  uint16_t analogRead(uint8_t pin)
  {
    bool ok = (ERROR_OK == this->read_register((uint8_t)(CMD_ANALOG_READ_A0 + pin), const_cast<uint8_t *>(this->read_buffer_), 2));
#ifdef APE_LOGGING
    ESP_LOGVV(TAGape, "analog read pin: %d ok: %d byte0: %d byte1: %d", pin, ok, this->read_buffer_[0], this->read_buffer_[1]);
#endif
    uint16_t value = this->read_buffer_[0] | ((uint16_t)this->read_buffer_[1] << 8);
    return value;
  }
#endif

#ifdef APE_BINARY_OUTPUT
  output::BinaryOutput *get_binary_output(uint8_t pin)
  {
    ApeBinaryOutput *output = new ApeBinaryOutput(this, pin);
    output_pins_.push_back(output);
    return output;
  }
#endif
#ifdef APE_BINARY_SENSOR
  binary_sensor::BinarySensor *get_binary_sensor(uint8_t pin)
  {
    ApeBinarySensor *binarySensor = new ApeBinarySensor(this, pin);
    input_pins_.push_back(binarySensor);
    return binarySensor;
  }
#endif
#ifdef APE_SENSOR
  sensor::Sensor *get_analog_input(uint8_t pin)
  {
    ApeAnalogInput *input = new ApeAnalogInput(this, pin);
    analog_pins_.push_back(input);
    return input;
  }
#endif
  void write_state(uint8_t pin, bool state, bool setup = false)
  {
    if (this->configure_timeout_ != 0)
      return;
#ifdef APE_LOGGING
    ESP_LOGD(TAGape, "Writing %d to pin %d", state, pin);
#endif
    this->write_register(state ? APE_CMD_WRITE_DIGITAL_HIGH : APE_CMD_WRITE_DIGITAL_LOW, &pin, 1);
    if (setup)
    {
      App.feed_wdt();
      delay(20);
#ifdef APE_LOGGING
      ESP_LOGI(TAGape, "Setup output pin %d", pin);
#endif
      this->write_register(APE_CMD_SETUP_PIN_OUTPUT, &pin, 1);
    }
  }

protected:
  bool configure_{true};
  bool initial_state_{true};
  uint8_t read_buffer_[3]{0, 0, 0};
  unsigned long configure_timeout_{5000};
  bool vref_default_{false};

#ifdef APE_BINARY_OUTPUT
  std::vector<ApeBinaryOutput *> output_pins_;
#endif
#ifdef APE_BINARY_SENSOR
  std::vector<ApeBinarySensor *> input_pins_;
#endif
#ifdef APE_SENSOR
  std::vector<ApeAnalogInput *> analog_pins_;
#endif
};

#ifdef APE_BINARY_OUTPUT
void ApeBinaryOutput::write_state(bool state)
{
  this->state_ = state;
  this->parent_->write_state(this->pin_, state, this->setup_);
  this->setup_ = false;
}
#endif

The file should be named “arduino_port_expander.h”

Make the following changes to your ESPHome YAML configuration file for the MCU-8266-12E device

esphome:
  name: mcu-8266-12e-01
  platform: ESP8266
  board: nodemcuv2
  includes:
      - arduino_port_expander.h
# Note the include file - This loads the APE Header

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "<your password will be different - dont change it>"

wifi:
  ssid: <your ssid>
  password: <your password>

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "MCU-8266-Hotspot"
    password: "password"

captive_portal:

i2c:
# PCB Prototype
  sda: GPIO5
  scl: GPIO4
# PCB Rev 1.5 or higher, comment the above 2 lines
# and uncomment
#sda: GPIO4
#scl: GPIO5
#################### - IMPORTANT ###########
  scan: true
  id: i2c_bus_a
  
pcf8574:
  - id: 'pcf8574_hub'
    address: 0x22 # Set at 0x22, feel free to change to your liking, Remember to set the chip to the address you choose as well
    pcf8575: false

time:
  - platform: sntp
    id: ha_time
    timezone: "Etc/GMT+7"

status_led:
  pin:
    number: GPIO16
    inverted: true

#Define the APE as a custom component, taking care to ensure that:
#1). The I2C Bust ID is the same as the one you have defined in the I2C: Section
#2). The address of the APE is the same as the one you set in the sketch
    
custom_component:
  - id: ape
    lambda: |-
      auto ape_component = new ArduinoPortExpander(i2c_bus_a, 0x08,true);
      return {ape_component};
    

sensor:

  - platform: custom
    lambda: |-
      return {ape_analog_input(ape, 0),  // 1 = A1
             ape_analog_input(ape, 1),
             ape_analog_input(ape, 2)};
   
#We define 3 analog inputs (A0,A1,A2) to monitor voltages on the card
#Note that you MUST define them in the sensors section below as well AND
#THAT THEY MUST BE IN THE SAME SEQUENCE THAT YOU DEFINED THEM IN ABOVE HERE
#
#ALSO NOTE THAT YOU CAN "NOT" use A4 and A5, as they are used for I2C !
#
# As an example, of adding another 3 analog inputs, your definition above will change to:
#
#    return {ape_analog_input(ape, 0),
#           ape_analog_input(ape, 1),
#           ape_analog_input(ape, 2),
#           ape_analog_input(ape, 3),
#           ape_analog_input(ape, 6),
#           ape_analog_input(ape, 7)};
#
#
# Now define the sensors connected to these analogs below:

             
    sensors:
      - name: "PE Card VIN"
        id: analog_a0
        device_class: "voltage"
        unit_of_measurement: "v"
        accuracy_decimals: 2
        filters:
          - lambda: return x * (22.00/1023.0);
          - throttle: 60s

# We use a lambda to scale the value of VIN - Our Voltage divider was designed around 22 volt
# thus we need 22 volt here in the calculation as well to make it accurate
#
      - name: "PE Card 5v"
        id: analog_a1
        device_class: "voltage"
        unit_of_measurement: "v"
        accuracy_decimals: 2
        filters:
          - lambda: return x * (5.02/1023.0); 
          - throttle: 60s
      - name: "PE Card 3v"
        id: analog_a2
        device_class: "voltage"
        unit_of_measurement: "v"
        accuracy_decimals: 2
        filters:
          - lambda: return x * (5.02/1023.0);
          - throttle: 60s
 
# The ATMega328P 's Analog Reference is set to 5v internally, thus we need to also scale the 
# 3v input with a maximum of 5v ... 

# In case you enabled the other 3 Analog Inputs above, you need to add the following
#
#    - name: "Analog 3"
#      id: analog_a3
#      filters:
#        - throttle: 60s
#    - name: "Analog 6"
#      id: analog_a6
#      filters:
#        - throttle: 60s
#    - name: "Analog 7"
#      id: analog_a7
#      filters:
#        - throttle: 60s
#
#
             
             
             
  - platform: adc
    pin: VCC
    name: "ESP8266 Chip Voltage"
    id: mcu_voltage
    unit_of_measurement: "V"
    device_class: "voltage"
    accuracy_decimals: 2
    update_interval: 60s
    
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    id: wifi_strength
    device_class: "signal_strength"
    unit_of_measurement: "dBm"
    update_interval: 240s
    

#Digital outputs function the same

output:
- platform: custom
  type: binary
  lambda: |-
    return {ape_binary_output(ape, 2),
            ape_binary_output(ape, 3),
            ape_binary_output(ape, 4),
            ape_binary_output(ape, 5),
            ape_binary_output(ape, 6),
            ape_binary_output(ape, 7)};
  outputs:
    - id: ape_output_p2
      inverted: false
    - id: ape_output_p3
      inverted: false
    - id: ape_output_p4
      inverted: false
    - id: ape_output_p5
      inverted: false
    - id: ape_output_p6
      inverted: false
    - id: ape_output_p7
      inverted: false
      
- platform: gpio
  id: relay_1
  pin:
    pcf8574: pcf8574_hub
    number: 0
    mode: OUTPUT
    inverted: true
- platform: gpio
  id: relay_2
  pin:
    pcf8574: pcf8574_hub
    number: 1
    mode: OUTPUT
    inverted: true
- platform: gpio
  id: led_status_1
  pin:
    pcf8574: pcf8574_hub
    number: 2
    mode: OUTPUT
    inverted: true
- platform: gpio
  id: led_status_2
  pin:
    pcf8574: pcf8574_hub
    number: 3
    mode: OUTPUT
    inverted: true
      
binary_sensor:
  - platform: gpio
    id: push_button_1
    name: 'Relay1 Pushbutton'
    device_class: ''
    pin: 
      pcf8574: pcf8574_hub
      number: 4
      mode: INPUT
      inverted: true
    on_press:
      then:
        - switch.toggle: switch_relay1
        
  - platform: gpio
    id: push_button_2
    name: 'Relay2 Pushbutton'
    device_class: ''
    pin:
      pcf8574: pcf8574_hub
      number: 5
      mode: INPUT
      inverted: true
    on_press:
      #min_length: 50ms
      #max_length: 500ms
      then:
        - switch.toggle: switch_relay2
    filters:
        - delayed_on_off: 50ms
      
  

switch:
  - platform: output
    id: switch_relay1
    name: "Relay No. 1 (#0)"
    output: relay_1
    on_turn_on:
      - output.turn_on: led_status_1
    on_turn_off:
      - output.turn_off: led_status_1
      
  - platform: output
    id: switch_relay2
    name: "Relay No. 2 (#1)"
    output: relay_2
    on_turn_on:
      - output.turn_on: led_status_2
    on_turn_off:
      - output.turn_off: led_status_2
  - platform: restart
    id: reboot_switch
    name: "Reboot Me"

Detailed information on the APE is available here

Manufacturing the PCB

This PCB was manufactured at PCBWAY. The Gerber files and BOM, as well as all the schematics, will soon be available as a shared project on their website. If you would like to have PCBWAY manufacture one of your own, designs, or even this particular PCB, you need to do the following…
1) Click on this link
2) Create an account if you have not already got one of your own.
If you use the link above, you will also instantly receive a $5USD coupon, which you can use on your first or any other order later. (Disclaimer: I will earn a small referral fee from PCBWay. This referral fee will not affect the cost of your order, nor will you pay any part thereof.)
3) Once you have gone to their website, and created an account, or login with your existing account,

4) Click on PCB Instant Quote

5) If you do not have any very special requirements for your PCB, click on Quick-order PCB

6) Click on Add Gerber File, and select your Gerber file(s) from your computer. Most of your PCB details will now be automatically selected, leaving you to only select the solder mask and silk-screen colour, as well as to remove the order number or not. You can of course fine-tune everything exactly as you want as well.

7) You can also select whether you want an SMD stencil, or have the board assembled after manufacturing. Please note that the assembly service, as well as the cost of your components, ARE NOT included in the initial quoted price. ( The quote will update depending on what options you select ).

8) When you are happy with the options that you have selected, you can click on the Save to Cart Button. From here on, you can go to the top of the screen, click on Cart, make any payment(s) or use any coupons that you have in your account.

Then just sit back and wait for your new PCB to be delivered to your door via the shipping company that you have selected during checkout.

Design and build an ESP8266 IoT Controller, Part 2

In my previous post, available here, I have shown you the initial stages of designing and developing our own IoT controller. Today, while the PCB has just arrived from PCBWay, and while we wait for the rest of the components to arrive, we will continue with the software development of the device. I will also show you the beautiful PCB that I got from PCBWay!

For today’s post, we will focus on configuring ESPHome, get it to work together with Home Assistant, as well as have a look at those PCB’s!

Table of Contents
Configuring ESPHome
The YAML configuration file
Updating the prototype device OTA
Integrating it all with Home Assistant
Taking a look at the PCB’s
Conclusion of Part 2

Configuring ESPHome

In the last post, we left off here…

After Flashing ESPHome to the device

Our next step will be to write the YAML configuration file…

YAML configuration

ESPHome uses the YAML language to define IO and automation. These files are then parsed, converted into C/C++ and compiled. The resulting Binary File is the uploaded OTA to the device via WiFi.

We will still be using our Virtual Machine Image of Home Assistant, running inside VirtualBox on our PC.
Go ahead and start that, and then open Home Assistant in your Web Browser. Then click on ESPHome.
You will see a screen similar to the one above.

While we are at this point, I believe it is a good time to clarify why we do the development in a virtual PC, and not directly on our Home Assistant instance running on a Raspberry Pi. The reason is actually very simple. Home Assistant seems to have issues with completely removing unused entities. This is not a problem, but you could potentially end up with a lot of stale entries in your production Home Assistant Server. The VM solution allows us to test everything away from the actual server, and then, when we are done, recreate the working device on the actual server. That way, there is almost no chance of damaging your existing Home Assistant Server, which you may already have spent some time on to set up just the way you want it…

Continue by clicking on the EDIT link, in the iot-con-prototype device that we created in the last tutorial.
You will see a file similar to this …

esphome:
  name: iot-con-prototype
  platform: ESP8266
  board: nodemcuv2

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "3f132fc270315361e4d2393a50c2bac5"

wifi:
  ssid: "<your ssid here>"
  password: "<your password here>"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Iot-Con-Prototype"
    password: "4uSytyPTx1TO"

captive_portal:

This is the default configuration file generated by ESPHome. We will build on this file… Note that your OTA Password will be different. DO NOT CHANGE OR MODIFY IT IN ANY WAY!

When we edit YAML, indentations are extremely important, so try to follow exactly what I am doing.

Our IoT Controller uses I2C to communicate with the PCF8574 chip. Let us assume that you have set your address to 0x22h by using the dip switch on the PCB, you could of course set any other address, just make sure that you know what it is.

We now need to tell ESPHome that we have an I2C Bus and that it needs to scan this bus for devices. That way, we can see in the logs if it actually detects our device or not.

Add the following 5 lines to your file:

i2c:
  sda: GPIO4
  scl: GPIO5
  scan: true
  id: i2c_bus_a

They mean that we will have an I2C bus with SDA on GPIO4, and SCL on GPIO5.
The bus will have an ID of i2c_bus_a and the bus should be scanned

The next section will be the actual PCF8574 Io Expander
Add the following lines to the file:

pcf8574:
  - id: 'pcf8574_hub'
    address: 0x22
    pcf8575: false

We give this device the id of pcf8574_hub and specify its address as 0x22. We also NEED to specify that it is NOT the 16 port pcf8575 variant.

Now we can start configuring our outputs. for our purposes, we will have two relay outputs, as well as two status LEDs. In the actual circuit, the pcf8574 will sink the pins connected to these, as the chip can sink more current than it can safely source. Refer to the relay driver schematic for more information on that.

Go ahead, and add the following to your file.

output:
 - platform: gpio
   id: relay_1
   pin: 
      pcf8574: pcf8574_hub
      number: 0
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: relay_2
   pin: 
      pcf8574: pcf8574_hub
      number: 1
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: led_status_1
   pin: 
      pcf8574: pcf8574_hub
      number: 2
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: led_status_2
   pin: 
      pcf8574: pcf8574_hub
      number: 3
      mode: OUTPUT
      inverted: true

The syntax should be straightforward, every output will have a platform, in this case, gpio.
Then we will need a unique ID, let us say relay_1
Now, we need to specify the physical pin to use. Now here, you could also specify a native pin on the actual NodeMCU device that we are using for testing, but we will specify a pin on the pcf8574 instead.
This is done with the pin: directive
pin:
pcf8574: pcf8574_hub // This tells the parser to use the device at address 0x22 that we specified before.
number: 0 // use GPIO 0
mode: OUTPUT // this can be either INPUT or OUTPUT, for our case, it should be OUTPUT.
inverted: true // Invert the logic of the pin.

Once again, it is needed, as we are sinking current into the pin, and the circuit was designed that way…
You can however use it noninverted if you really want, it will just look a bit odd on Home Assistant, if your On state, actually meant Off

The next section will be called binary inputs and may seem a bit confusing at first. But, trust me, it is not. This is just the way that we define our physical push buttons on the device. We need those, in the event that our Home Assistant Server is offline, or when we need to physically press a button on the device.

What? Why? Are we not building an IoT Controller? Why should we press any physical buttons on it?
Well, the answer to that is quite obvious. There will definitely be times that you want to control an attached device by pressing a physical button. It does not make sense to scurry around, swiping through apps on your smartphone, while you are standing right next to the device in question. To leave out this basic functionality, would be plain silly, and in my view, bad engineering.

binary_sensor:
  -  platform: gpio
     id: push_button_1
     name: 'Relay1 Pushbutton'
     device_class: ''
     pin:
        pcf8574:  pcf8574_hub
        number: 4
        mode: INPUT
        inverted: true
     on_press:
      then:
        - switch.toggle: switch_relay1
     filters:
       -  delayed_on_off: 50ms
       
  -  platform: gpio
     id: push_button_2
     name: 'Relay2 Pushbutton'
     device_class: ''
     pin:
        pcf8574:  pcf8574_hub
        number: 5
        mode: INPUT
        inverted: true
     on_press:
      then:
        - switch.toggle: switch_relay2
     filters:
       -  delayed_on_off: 50ms

The syntax is almost the same as the outputs above, but we are also adding automation:
We want the relay to be toggled each time the button is pressed. This will allow us to use only one button per relay to switch it on or off.
We are also adding a filter, in this case, a bit of debouncing, of 50ms. This will prevent the chattering of the contacts on the switch from generating more than one event for each button press.

The final section of the file will be a switch: section. This will allow you to control the relays from inside Home Assistant. ( The outputs are considered an internal to ESPHome function, and will thus not be exposed to Home Assistant.
Add the following lines to the file

switch:
  - platform: output 
    id: switch_relay1
    name: "Relay No. 1 (#0)"
    output: relay_1
    on_turn_on:
    - output.turn_on: led_status_1
    on_turn_off:
    - output.turn_off: led_status_1
  - platform: output 
    id: switch_relay2
    name: "Relay No. 2 (#1)"
    output: relay_2
    on_turn_on:
    - output.turn_on: led_status_2
    on_turn_off:
    - output.turn_off: led_status_2

You can see that the syntax is once again very easy to understand. We also specify that the status LED for the relevant relay channel be switched on or off with the relay. This also happens when you press the physical switch.
The name element will be the Name of the Output that will be displayed in Home Assistant. You can change it to your liking here, or you can also change it inside Home Assistant itself.

The completed file should now look like this. NOTE that if you decide to copy-paste the file from here, as I would recommend, you should only copy from the i2c section to the end of the file. Do not copy the WiFi and other sections. Your own file already has your own settings.

esphome:
  name: iot-con-prototype
  platform: ESP8266
  board: nodemcuv2

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "3f132fc270315361e4d2393a50c2bac5"

wifi:
  ssid: "<your ssid here>"
  password: "<your password here>"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Iot-Con-Prototype"
    password: "4uSytyPTx1TO"

captive_portal:
#DO NOT COPY ANYTHING ABOVE THIS LINE -------- 
i2c:
  sda: GPIO4
  scl: GPIO5
  scan: true
  id: i2c_bus_a
  
pcf8574:
  - id: 'pcf8574_hub'
    address: 0x22
    pcf8575: false


output:
 - platform: gpio
   id: relay_1
   pin: 
      pcf8574: pcf8574_hub
      number: 0
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: relay_2
   pin: 
      pcf8574: pcf8574_hub
      number: 1
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: led_status_1
   pin: 
      pcf8574: pcf8574_hub
      number: 2
      mode: OUTPUT
      inverted: true
 - platform: gpio
   id: led_status_2
   pin: 
      pcf8574: pcf8574_hub
      number: 3
      mode: OUTPUT
      inverted: true

binary_sensor:
  -  platform: gpio
     id: push_button_1
     name: 'Relay1 Pushbutton'
     device_class: ''
     pin:
        pcf8574:  pcf8574_hub
        number: 4
        mode: INPUT
        inverted: true
     on_press:
      then:
        - switch.toggle: switch_relay1
     filters:
       -  delayed_on_off: 50ms
       
  -  platform: gpio
     id: push_button_2
     name: 'Relay2 Pushbutton'
     device_class: ''
     pin:
        pcf8574:  pcf8574_hub
        number: 5
        mode: INPUT
        inverted: true
     on_press:
      #min_length: 50ms
      #max_length: 500ms
      then:
        - switch.toggle: switch_relay2
     filters:
       -  delayed_on_off: 50ms    
switch:
  - platform: output 
    id: switch_relay1
    name: "Relay No. 1 (#0)"
    output: relay_1
    on_turn_on:
    - output.turn_on: led_status_1
    on_turn_off:
    - output.turn_off: led_status_1
  - platform: output 
    id: switch_relay2
    name: "Relay No. 2 (#1)"
    output: relay_2
    on_turn_on:
    - output.turn_on: led_status_2
    on_turn_off:
    - output.turn_off: led_status_2
  

Well done, That was quite easy.
Copy and paste the file into the edit screen that you have opened in ESPHome.

If everything went well. you should see an option to save or install as in the picture below.

YAML configuration file. ESPHome

If you can see both Save and Install, it means that your file syntax is correct. If not, go back and carefully check your indentations, as well as the spelling of the commands. It should not be a problem if you copied it from above…

Now click on save. You will get a confirmation.
Make sure that the prototype is connected to the USB port of the computer, or to another power source. Wait a few seconds to make sure that it is connected to the network, and then click Install.

ESPHome will now parse your YAML file, generate the needed code, and compile it. After that, it will upload the new firmware to your device. Wait until you can see the device restart and connect to the network in the log window.

The Firmware is compiled and the uploaded OTA
The device had booted, note that the PCF8574 has been detected and that we can see our configuration data (in purple text)

Configuring Home Assistant

We can now start to add this all to Home Assistant. It is also very likely that you will be prompted to add the device by Home Assistant itself. If this did not happen, or like with my testing image on the VM, you have a lot of old stuff floating around as well, we can do it manually …

Go to the Configuration Menu, and click on Integrations.
Click on the esp8266-nodemcu option on ESPHome
Click on the entities, next to the device …
The available entities ( selected with a blue checkmark ). Entities with a red warning are stale, meaning old, unavailable or offline… You can remove them, but they tend to show up again on their own…

We can now select the two relays, as well as the Pushbuttons. Then click on Enable selected.

When you now go back to the Home Assistant Home screen, you will see your device with its controls, on the screen. You can now control the device from Home Assistant. You will note that the two push-buttons are also present in Home Assistant, but, they are only indicators. This means that while they will change state briefly when you press the physical switches, they can not be controlled directly from Home Assistant. They can however be used in automation, or at least I believe so. The debouncing times are however rather short, so I would recommend that you base any automation on the actual relay states instead.

A sneak preview of the PCB

As promised above, I will now give you a quick preview of the PCB’s. They were manufactured by PCBWay

I am once again awed by the precision of these bords. They are absolutely flawless and exactly true to my design.
The irregular isolation cut-outs are exactly to speck and size, the silkscreen is crisp and not blurred. All component footprints are correct, and there seem to be no shorts or open circuits on initial testing with a multimeter.

You can get your own version of these for only $USD 5.00 excluding shipping from here I believe you will be just as impressed as I am.

So let us look at these boards…

My biggest frustration now will be to wait for the rest of the components to arrive. I already have some of them in stock, and can repurpose some others, but I think that I will rather wait for everything to arrive so that I don’t have to
assemble them in stages. I plan to assemble 2 of the 10 boards and make some of the remaining 8 available to interested people. Contact me on messenger if you are interested.

Conclusion

This concludes part 2 of this series. In part 3, we will look at the assembled PCB’s, as well as take a more detailed look at using them together with Home Assistant. I will also attempt to do the Tasmota integration, this will involve compiling my own special version of Tasmota.

I am also planning to release the assembly process as a series of pictures or maybe a video on Patreon.

Become a Patron!

If you like what I do, I want to ask you to consider becoming a Patron. With some assistance from generous people like yourself, I can create some more interesting projects.

Thank you, and Good-Bye until next time.

Design and Build an ESP8266 Based IoT Controller – Part 1

Table of contents

Introduction
My Requirements
My Solution
Expandability
Prototyping
–> Initial Software Installation
—–> Start Home Assistant
—–> Install ESPHome
—–> Flashing ESPHome
–> Designing the Hardware
—–> The Relay Driver Circuit(s)
——–> Relay Driver Schematic
——–> Status LED and User Input Schematic
——–> The USB to Serial Schematic
——–> The ESP-12E schematic with strapping pins, as well as the power supply circuit(s)
—–> Designing the PCB
——–> PCB Layout (Raw layers and Silkscreen, not Rendered)
——–> Top Layer
——–> Bottom Layer
——–> 3D Render ( Top Layer from left )
—–> Manufacturing – PCBWay
—–> What to expect in Part 2

Introduction

The Internet of Things (IoT), as well as Home Automation, are steadily gaining popularity all the time. You can already buy quite a lot of commercial products or do your own D.I.Y implementation. Many different companies offer various devices and modules to help you do your project easily. But many of us will know that these modules always come with a lot of wires and connections, which can be very unreliable, and also unsightly to look at.

Most of these solutions are also relying on you placing some stuff on an electronics-breadboard, or strip-board for more permanent installations. You can also decide to design and manufacture your own custom PCB.

On the software side of this problem, there are many commercial and open-source solutions available, and most of them work with almost anything on the market (With various degrees of complexity and a varying learning curve).

Go back to the Table of Contents

My Requirement(s)

Having access to a few ( four to be exact ) Raspberry Pi Computers, as well as a huge number of ESP8266, ESP32, Arduino and STM32 Development boards that are lying around in my working area, as well as being quite lazy to do repetitive tasks, I have been playing with the idea for quite a while to automate some or maybe all of the lights in my house. I have also built quite a few device prototypes, only to tear them down again after testing them.

The reason for this, and this is only my own opinion, is that an electronics breadboard is meant for prototyping only.
They are not reliable in the long term, and they look extremely messy. Using a certain development board, and connecting all of the supporting components and modules to it, also leaves quite a mess. So,

1) The project must be contained on a single PCB.
2) It must have WiFi and or Bluetooth support (ESP8266 does not have this [ Bluetooth ])
3) It must be able to be powered from 220v mains directly, OR, through a single Plug Type AC to DC converter, preferably something already available on the market.
4) The power supply unit must supply adequate power to the unit.
5) I want to make some actual use of a Raspberry Pi, as they were not meant to lie around gathering dust 🙂

6) Once completed, the project should be able to be updated OTA or with as little fuss as possible.
7) The freedom to code in whatever language I want, as well as be easy to use with prebuilt firmware like ESPHome and or Tasmota and the likes.

Go back to the Table of Contents

My Solution (Hardware and Software)

Taking into consideration cost, as well as flexibility, I decided on using the ESP8266 and in later versions ESP32.
These have WiFi and Bluetooth already built-in but have a nasty breadboard form factor. The popular NodeMCU is a great starting point, but it has many small design flaws, most of which were fixed in later versions, but, as I live in SE Asia, and electronics are super cheap, you never know what you get.

With this in mind, I have decided to design my own controller, based on the ESP-8266 12E Module. This will give me the flexibility I need while allowing me to add custom components and features as needed, which is not possible with a stock version.

As far as Software is concerned, Home Assistant, running on a Raspberry Pi 4B with 4Gb Ram will be sufficient to control all of this. Firmware on the ESP12-E can be either ESPHome, Tasmota, or something that I write on my own, connecting to the internet and using MQTT for communication. (The options here are also almost too many to mention, but, IFTTT, Adafruit IO, and Blynx come to mind… Alexa and Google Assistant support are also possible, but definitely not required in my application at this moment.

Go back to the Table of Contents

Addon Hardware and Expandibility

Any permanent project grows on you, and over time, you will want to add features and functions. Keeping this in mind when you start designing saves a lot of headaches later. I will thus definitely make sure to provide access to the I2S, SPI and Serial Peripherals.

Prototyping and Initial Testing

Software
It is quite impossible to design something without building a prototype, and doing some very rigorous testing.
To do this, I have started with a copy of Home Assistant running in a VM on my HP ML350G Debian server computer. This way, I can quickly test Home Assistant, and make sure everything works as I want it to, without having to do it on the PI. This will mean that I will have to re-do most of the software configuration again on the PI,
but it gives me the flexibility to take snapshots and restore them if something goes wrong.

The procedure that I followed, for VirtualBox, can be found here. I am sure that it will be very similar for Windows users.

Start Home Assistant

When you have completed the installation, you can go ahead and start Home Assistant.

You may now open Home Assistant in your browser by going to http://<your-ip-address>:8123 and pressing enter.
You will see something similar to this:

Install ESPHome

You may now scroll down to the Supervisor Menu Item, Click on the Addon Store link, and Install ESPHome.

Please make sure that you select “start on boot”, “watchdog”, “auto-update” and “show in the sidebar”
Also, make sure to start the add-on

Go back to the Table of Contents

Flashing ESPHome to your device

You are now ready to flash ESPHome onto your development board. Please note that you will have to repeat this step on the actual PCB device that we will design and build later.
For now, we will however use a standard NodeMCU v3 module, as it contains the ESP-12E module that we require.

As we have not configured HTTPS in our Home Assistant installation, we can only flash the initial firmware using an external application. This application is called ESPHome Flasher, and you can download it here.

Prepare to Flash

We need to create an initial device in ESPHome. to do this, you need to click on the ESPHome Menu Item in the Home Assistant sidebar. Then click on the Green and White + sign at the bottom right corner…

Give your device a descriptive name, I went with iot-con-prototype. Also, add your WiFi network credentials (SSID and Password). Then click on NEXT.

You now have to select your ESP board. I went with NodeMCU.

Click on NEXT when you are done.
You will now be alerted that a configuration has been created. Click on the Close button.

You will now see your device in the list. The red line means that the device is OFFLINE.

If you have not installed ESPHome Flasher yet, now is a good time to do it. When you are done, connect the NodeMCU module to the computer’s USB port and start ESPHome Flasher.
Note: On Linux, you dont have to install it. you can just start it from the terminal using
./ESPHome-Flasher-1.3.0-Ubuntu-x64.exec ( this example is for Debian ) yours may differ … sudo may be required

Select the communications port from the dropdown menu.
Now go back to Home Assistant, ESPHome Screen.
Click on the Install link (in yellow) of the device that we added previously.
Then click on Manual Download

ESPHome will now compile your initial firmware and prompt you to save the .bin file it has generated.

Take note of the location of this file, we will need it for the next step.

Now you can go back to the ESPHome Flasher window, and browse to the .bin file that you have just generated and downloaded.

When this is done, you can click on Flash ESP. The firmware will now be flashed onto the NodeMCU device that we will use as our prototype.

You can close the Flasher application when you see predominantly purple text in the console window. This means that the device has connected successfully with your WiFi network, and by extension, also with ESPHome and Home Assistant.

If you now go back to ESPHome, you will see that the device has a green line above its name, which means that it is online. Please note that at this stage, the device does not do anything, as we have not yet configured it. That will be our next task, but before we do that, we will have to start playing with some of the physical hardware yet.



Designing the Hardware

I have decided to base this controller on the popular NodeMCU v3 development board. This little board makes use of the ESP12-E module, designed by AITinker, from whom I have bought many well-designed modules before. The module (NodeMCU) is in a BreadBoard form factor, and thus clearly meant for prototyping, or use on a base-board of some kind. I do however have some issues with this module, namely:

1. Many of these units are in fact clones. Some of these does not have proper protection between the board and the USB port to power them. This makes it dangerous to connect power to the VIN pin.
2. The board does not have a built-in 5v regulator. The 5v output (VIN) is thus taken directly from the USB port that powers it. This limits the total available current to the board and all peripherals to the 500mA available from the USB port.

All of these issues are however easy to overcome, as Espressif has made available excellent documentation on using their modules. I will thus be using this documentation as a reference design for my own device.

Another issue is expandability. The module provides many broken out GPIO pins (D0 to D8), but in order to remain super flexible for future expansion, I would have to make sure that I keep the built-in peripherals, like i2c, spi, and uart free for connection to external addon components in future. To solve this, I have decided on using the PCF8574 I2C IO Expander. This means that…

D0 (GPIO16) needs to be kept free for Wake up from deep sleep mode
D1 (GPIO5) SCL line of I2C Bus
D2 (GPIO4) SDA line of I2C Bus
D3 (GPIO0) connected to Flash Button, and also a strapping pin
D4 (GPIO2) Tx of UART 1
D5 (GPIO14) HSCLK – SPI Clock
D6 (GPIO12) HMISO – SPI MISO
D7 (GPIO13) Rx UART 2 / HMOSI – SPI MOSI
D8 (GPIO15) Tx UART 2 / HCS – SPI CS as well as a strapping pin
D9 ([Rx] GPIO03) Rx UART 0
D10 ([Tx] GPIO01) Tx UART 0

Looking at this, it is thus very clear that I2C is the way to go, as it will use only two pins, leaving the other GPIO’s free for future expansion.

Go back to the Table of Contents

Relay Driver Circuit, status led(s) and user control


The PCF8574 has very weak current sourcing capabilities, but it can sink 25mA per IO pin. As I will be driving the chip from 3v, this will however not be enough to drive a 5v relay. I will thus be making use of an optocoupler to drive the relay from 5v, using a general-purpose NPN transistor. That way, I can make sure that I do not overload the IO line on the PCF8574, as well as keep the 3v and 5v lines isolated from each other. (They will however still share a common ground).

Relay Driver Schematic

As you can see from the schematic above, the relay driver(s) will be connected to P0 and P1 of the PCF8574. A 200-ohm resistor will limit the current to about 16.5mA @ 3.3v. This is well within the tolerance of 25mA for the PCF8574 as well as the 50mA limit of the EL357N Optocoupler chip.

On the output side, I made use of the S8050 general purpose NPN transistor, capable of a 25v collector-emitter voltage, ant a continuous current of 500mA. Move than sufficient to drive the +/- 70mA to 80mA of current required by the relay coil. The current through the phototransistor side of the optocoupler is limited by a 1k resistor to the base of the transistor, to 5mA.

PCF8574 IO Expander Status LED(s) and User Input Schematic

Go back to the Table of Contents

Provision is made for up to four (4) pushbuttons, by pulling pins P4 to P7 down to ground, through an external push-button connected to a 2-way pin header. The design allows for the pin to be used for another function as well, with an additional breakout pin provided on the edge of the board.

Likewise, the two status LED’s on P2 and P3 is connected via a 470-ohm resistor each, driving them at approximately 7mA each. This is once again within the 25mA sink limit of the PCF8574. These pins can also be used to connect to other hardware instead via the breakout header at the edge of the board.

The user input(s) and status led circuit was purposefully kept as plain as possible, as it is very likely that the additional 6 ports taken up by these will be used for other purposes instead.

USB to Serial Interface

The USB-to-Serial interface is done by using a CH340G. I have not modified the reference design from the original NodeMCU v3 too much, as it works well, and thus need no changes. I did however make sure that there is a protection diode between the USB 5v line and the VIN line on the PCB. Most of the existing NodeMCU boards does not do this, or when they do have the protection diode, that line is directly connected to the VIN pin. in my view, that is not ideal, and can cause unnecessary damage to your computer’s USB port or NodeMCU board in the event that you power it from an external source, and also use USB power to upload code… Not that it should be done that way anyway, but rather safe than sorry later.

USB-to-Serial Schematic

The two transistors (VT1, and VT2) is used to do very cleverly put the board into programming mode, as well as reset it after flashing, without the end-user having to press any of the flash or reset buttons. This is also part of the reference design on some of the NodeMCU boards. I can not comment if it is standard. I found it useful, so decided to include it as well.

ESP-12E with strapping pins and power supply

The ESP-12E module, as manufactured by AITinker, seems to be quite stable and easy to use. It does however have a couple of caveats, to enable it to function as intended. The strapping pins (discussed later) should be in a certain state at bootup time, and failure to adhere to that will definitely cause a failed boot or wrong startup mode.

GPIO0, GPIO2, EN and RST should be pulled HIGH for a NORMAL boot, while GPIO15 should be pulled LOW.
Pulling GPIO0 LOW at boot, will put the board into FLASH mode.

Another change that I made is that I did not break out the raw Analog input pin, as is done on some of the boards ( as a VV pin, or sometimes as an unlabeled, or reserved pin). The Analog input pin on the stock module is designed for around 1.0v input. This is fed from a resistor divider, to effectively scale your 5v input down to valid levels.

ESP-12E and Power Supply Schematic

GPIO16 (D0) is used internally by the ESP-12E to wake the module from deep sleep. This is done by connecting GPIO16 to the reset pin. To wake up the module, GPIO16 is internally pulled LOW, thereby resetting the module.
I have decided to add a user-selectable jumper that can be enabled to connect GPIO16 to RESET, that enabling this functionality. It is however to be noted that the jumper must ONLY be set AFTER the relevant deep sleep mode has been enabled in the firmware for the module.

The standard NodeMCU has only a single 3.3v voltage regulator installed. I added a dedicated 5v voltage regulator as well and powered it directly from the VIN pin. This regulator will only function when the device is powered from an external power source, with an upper voltage limit of 15v DC. Protection for the USB port is provided by a diode between VCC_USB and VCC5V.

Go back to the Table of Contents

Designing the PCB

As I have mentioned at the start of the article, this is a show and tell, of how I solved a particular problem that I had.
I do not believe in re-inventing the wheel, but to adapt and improve as and where I see fit, to my own purpose. This project will by no means be unique, or better or worse than other similar devices on the market today. It is however designed to suit my particular needs for my particular project.

The design of the PCB and Schematics was done in EasyEDA, which I believe to be quick and easy to use, as well as Linux friendly. (There does not seem to be many of those around, KiCAD being the exception to that rule. I have however not been too successful in using that before, to no fault of KiCAD at all. I am just not prepared to learn a new EDA CAD package just for the sake of doing it. Time is precious, and I can get everything I need to do done with EasyEDA).

PCB Layout

The PCB is dual-layer 99.06mm x 83.058mm. Six mounting holes are provided. The ESP-12E is placed at the top,
roughly in the left-hand corner. As per Espressif design recommendations, no tracks are routed underneath the antenna area. An approximate 15mm x Board width area is also kept clear of any tracks or ground plane(s) to also prevent interference with the RF signal generated by the device. All components are mounted on the top payer of the PCB and are as far as possible grouped together by their function and purpose in the circuit. As the two onboard relay modules will very likely be used to switch mains power, they have also been excluded from the ground plane(s). Cutouts around all possible mains power carrying pads and tracks were also added to prevent tracking.

A DC Power Socket, and USB port, as well as access to the single Analog Input, Enable, Reset Pin and Button and VIN pin is provided on the left-hand side of the PCB. All other IO pins, as well as the six remaining ports on the PCF8574 IO expander, is accessible on the right. Note that GPIO4 and GPIO5 ( D1 and D2 ) were hard-wired as I2C lines. They should thus not be used for any other purpose. All other pins are accessible and broken out, clearly labelled with their GPIO numbers, as well as NodeMCU style Dx numbers. There are also an additional 6 IO pins broken out at the bottom of the ESP-12E module, to provide access to the seldom-used SDCard interface.

PCB Top Layer ( Rendered )
PCB Bottom Layer (Rendered)
3D Render PCB from Left

Manufacturing the PCB

This PCB will be manufactured at PCBWAY. The Gerber files and BOM, as well as all the schematics, will soon be available as a shared project on their website. If you would like to have PCBWAY manufacture one of your own, designs, or even this particular PCB, you need to do the following…
1) Click on this link
2) Create an account if you have not already got one of your own.
If you use the link above, you will also instantly receive a $5USD coupon, which you can use on your first or any other order later. (Disclaimer: I will earn a small referral fee from PCBWay. This referral fee will not affect the cost of your order, nor will you pay any part thereof.)
3) Once you have gone to their website, and created an account, or login with your existing account,

4) Click on PCB Instant Quote

5) If you do not have any very special requirements for your PCB, click on Quick-order PCB

6) Click on Add Gerber File, and select your Gerber file(s) from your computer. Most of your PCB details will now be automatically selected, leaving you to only select the solder mask and silk-screen colour, as well as to remove the order number or not. You can of course fine-tune everything exactly as you want as well.

7) You can also select whether you want an SMD stencil, or have the board assembled after manufacturing. Please note that the assembly service, as well as the cost of your components, ARE NOT included in the initial quoted price. ( The quote will update depending on what options you select ).

8) When you are happy with the options that you have selected, you can click on the Save to Cart Button. From here on, you can go to the top of the screen, click on Cart, and make any payment(s) or use any coupons that you have in your account.

Then just sit back and wait for your new PCB to be delivered to your door via the shipping company that you have selected during checkout.

Conclusion of Part 1

This is the end of a very long part 1. We have started to look at the steps involved to get started with designing our own IoT Controller, did some initial software installation, and took a detailed look at the schematics and PCB…

In part 2, we will look at

1) Writing the configuration for ESPHome to control your device
2) Look at Tasmota as an alternative way to control the device
3) Integrating the device into Home Assistant
4) Have a go at writing our own firmware using the Arduino IDE to control the device using MQTT
5) Depending on how long shipping of the components and PCB takes, assembly and testing of the actual device

Thank you for your time. This is a very long article, and I appreciate your interest.

Go back to the Table of Contents

Build your own 8 DI Optically Isolated Arduino Shield – Part 3

Welcome to the final instalment of my 8 DI Optically Isolated Arduino Shield. Today I will show you some of the assembly pictures, as well as look at the coding to use this shield. I will also provide you with a link to the manufacturing files, in case you want to make your own.

These PBC’s were manufactured at PCBWAY.

You can order your own version of this board for just $5 USD if you click here

PCBWay makes it quite easy to order prototypes for your PCB’s… Just upload the Gerber files on their website, select your desired options for the PCB and order. The turn-around time is great. I received these boards, ordered together with a stencil for SMD assembly, in exactly 5 days, shipping from China to Thailand 🙂 That is super fast, as it arrived 4 days faster than the components that were ordered locally from Bangkok! Be sure to consider using their services next time you need a PBC made…

Top and bottom layout of completed Shield
Bottom of Shield
Top Layout

Some notes on assembly: The reset switch will seem misplaced, and indeed, it is 🙂 The reason for this is that I could not get any 4 pin tactile switches 🙁 So I had to either leave it unpopulated or use a two-pin tactile switch. As I will be using these shields myself, I decided that although it doesn’t look perfect, the two pin switch will still provide me with the functionality that I want.

On the bottom of the board, you can still see some blobs of flux, as the pictures were taken right after assembly, and have not been cleaned up yet. Some solder joints have also not been cleaned up yet.

The top of the unpopulated PCB
The bottom of the PCB

Testing and Coding

The testing of the board is quite straightforward. I first checked all the power rails with a multimeter to make sure there are no open circuits of shorts. Then I checked connections to all the chips and other components, yes, it takes a while to do that, but rather safe than sorry. After assembly, I repeated this process, making sure that all the components receive the correct power level, and that all switches ( like for addressing and the reset button ) actually do what I intended them to do. The next tests were the individual inputs with the optocouplers. This is done by connecting an input source (between 5.5v and 32v) to each individual input and then physically testing on the pins of the optocoupler in question, for the correct voltage input.

The shield is then powered from 5v and the input test is repeated while checking with a multimeter that the input signal does indeed get transferred by the optocoupler to the PCF8574 chip. I found that with the particular batch of PCF8574 chips that I got, that the IC would only respond reliably with a voltage between 5.5v and 32v. The original design was for 3.0v to 32v. I found that the Optocoupler EL357N seems to be unable to switch itself on at the low current allowed through the resistor divider at the input. This can be fixed by lowering the value of R1, R5, R9, R13, R17, R21, R25, R27 from 4k7 to whatever value you need. Note that that will reduce the top-level input voltage that you can safely use. For my application, however, 5.5v to 24v will be perfect, so I will leave it as is.

The shield is now connected to an Arduino with DuPont Wires, to test the I2C addressing of the PCF8574. The chip address is changed with the 3-way dip switch at SW1. All eight addresses are available. It should be noted that I have used a pull-up configuration on the address lines. That will reverse your logic.. Switching the dip switch on will pull the pin to GND, not to VCC as you would normally expect. Thus as an example, all switches off will give an address 0f 0x3f, while all on will give 0x38.

Coding

You can use the standard Arduino IDE with the Wire.h library to code the shield, or you can use one of the many PCF8574 libraries that are available. I coded my tests with the Embeetle IDE, as it gives me much better control over my code. I will show you a short, interrupt enabled sketch, in Arduino C++ below

#include <Wire.h>

byte _portStatus = 0b00000000;
boolean _readI2C = false;

void MyISR() { // Interrupt service routine
  //Serial.println("Interrupt Occured on Pin2");
  if (_readI2C != true) {
    _readI2C = true;  
  }
}

void setup() {
  // put your setup code here, to run once:
  pinMode(2,INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(2),MyISR,FALLING);
  Serial.begin(115200);
  Wire.begin();
  Wire.beginTransmission(0x20);
  Wire.write(0xFF); // set all pins to 1, needed to make them inputs
  Wire.endTransmission();
}

void loop() {
  // put your main code here, to run repeatedly:
  
  byte _data;
  if (_readI2C == true) {
    _readI2C = false;
    Wire.requestFrom(0x20,1);
    if (Wire.available()) {
      _data = Wire.read();
    }
  }
  if (_portStatus != _data) {
    Serial.print("Port Data Changed : 0xb");
    Serial.print(_portStatus,BIN);
    Serial.print(" changed to : 0xb");
    Serial.println(_data,BIN);
    _portStatus = _data;
    delay(50);
  } else {
    _portStatus = _portStatus;
  }
  
}

Conclusion

This turned out to be a very interesting and fun project to do. From designing the circuit to getting it manufactured and hand assembling it myself was a very satisfying experience. I would like to take this opportunity to thank Wendy Wu, from PCBWay‘s Marketing department, for her assistance with the manufacturing of the board. The speed and efficiency with which she handled this project were fantastic.

Build your own 8 DI Optically Isolated Arduino Shield – Part 2

In part 1 of this article, I introduced my new I2C 8DI Optically Isolated Arduino Shield. Today, I will show you how the full design, as well as the circuit diagram.

This shield was designed to allow an input of between 3.0v and 32.0v to be applied to the various inputs. This will be completely galvanic isolated from the rest of the circuitry on the shield, and thus also from your Arduino, or other micro-controller if you choose to use another one 🙂 Yes, This is possible, as long as you power the shield with 3.0v to 5.0v. You will also have to connect your I2C bus to the SCL and SDA Lines marked on the shield.

Please note that, if you decide to do that, the other Arduino specific pins, as broken out on the shield headers, will have no connections to anything else. :), an obvious fact, but it should be stated, it seems 🙂

The Circuit Diagram

Circuit Diagram, Page 1 of 2

As you can see on page 1, each optically isolated input has a voltage divider resistor network in front of the Opto Coupler. This resistor network also limits the current that can be used by the infrared LED inside of the EL357N chip to 5mA at 32v DC. ( The chip can accept up to 50mA, but it should not be driven so hard 🙂 ) A diode provides reverse-polarity protection to each input as well.

Another voltage divider on the output side limits the current to the PCF8574 Chip. This can also only source or sink 25mA per IO.

Note that there is NO common ground between the input and output sides of this circuit. That means that you have to provide another ground, usually from your external device… This ensures that galvanic isolation between the two circuits is maintained.

Circuit Diagram, Page 2 of 2

On page 2, we can see the various net connections to the connectors, PCF8574 chip, as well as various jumper headers, to select the interrupt pin [H3] ( For Arduino, D2 or D3, other micro-controllers: you are free to select any GPIO to connect to the D2 or D3 header pin ).


You can also select to enable or disable the pull-up resistors on the I2C bus, by shorting the jumper on [H2].
This is usually only needed on the first shield, or in other words, you need one pair of pullup resistors per i2c bus, not one pair per device!

Device addressing is selected with SW1. 8 addresses are available but switching this switch as per the table on the back of the PCB. It is worth mentioning that depending on the version of the PCF8574 chip that is on the shield, there are 8 addresses available, with 0x20h to 0x27h being common on the PCF8574, and 0x38h to 0x3fh being used on the PCF8574A/T version.

Typical Connection

Typical connection of input. Note that there are no common ground between the two devices

The PCB

The shield is built on a double-sided PCB or 71.12mm x 61.72mm. This is only slightly bigger than the standard Arduino Uno. All resistors, capacitors and LED’s are of 0805 sizes. ( smaller than that is a bit hard on my eyes, although it can be done, just takes longer 🙂 ). A ground plane is provided on both sides of the PCB.

PCB design file

All Arduino pins are broken out on a double row of 2.54mm headers. This allows you to use either the outside row with stack-able male-female headers, like on most shields, or you can use dedicated male and female headers, in a zig-zag pattern to stack the shields.

I have done the same with the ICSP header, as on many other commercially available shields, there is only a single female 3×2 header on the bottom, making it quite annoying to use on another shield.

PCB Topside
PCB Bottom side

Manufacturing

This shield is currently being manufactured by PCBWay.

PCBWay provides a rapid and affordable PCB manufacturing service. They also provide PCB assembly, and even a 3D printing and CNC service. This can really help to bring new electronic prototypes to market quickly, as PCBWay can provide you with a complete turn-key solution to bring your product to market. The process to order a PCB is also completely automated, and you can easily do it online in a few minutes. Just upload your completed Gerber Files onto their system via their web interface, and you will get a quote in seconds. You can then pay and place your order immediately from the same page. They also have various shipping methods available, That really helps, as no one wants to pay for excessively expensive shipping, or be stuck having to use only one company.

The design files for this project will be made available as a project on the PCBWay website soon after the release of part 3, which will cover the assemble, testing and programming of the shield.

Build your own 8 DI Optically Isolated Arduino Shield – Part 1

All of us Makers like to tinker with stuff, and in this process, we may find ourselves thinking about how to connect device A to my Arduino… Device A may operate at a different voltage from the Arduino, and may thus damage it badly….

Many different solutions exist to do this, but, many of them, like relays, can be quite bulky, increasing the overall size of your project, as well as putting bigger demands onto your power supply unit.

Having worked in the Industrial Automation sector for a few years, I remember that we used to have dedicated hardware to protect our sensitive controllers from the harsh outside signals that we needed to monitor. These devices were called isolators, and today I will show you how to construct your own version of this essential device.

But some theory is needed first…

What does it mean to isolate a signal? In the electronics world, you might have seen that you usually have to use a common ground between all your devices to make them work together properly. While this is definitely true, let us look at another example…

Let us say you have some device, that will send you a voltage signal when it switches on, and another voltage signal when it is switched off. This device runs on 24 volts, so some of the more informed of us will immediately say you need a level converter, meaning a device that changes the 24v signal into a 5v signal… Others will try to use a relay to convert the signal ( A relay is also a type of isolation device ). A much more elegant way of doing this will be by using an Optic Isolator chip.

A simple Optic Isolator Chip

This chip provides complete isolation between your device and the Arduino or other microprocessor. It does that by using infrared light to transmit the signal. Light, as we all know, does not conduct electricity 🙂

Whereas a relay will only give you a on or an off state, the Opto-coupler or Optic Isolator can also do linear current transfer, meaning that the more IR light it transmits, the more current the photo-transistor will allow to pass as well.

A good tutorial on Opto-Couplers can be found here

Opto Isolator Circuit

In my circuit, I made use of the following circuit…

Two Optic Isolator Level converter Circuits

As we can see in the two circuits above, there is no common ground between the input and output sides of the circuit. This is ideal, as noise and other undesirable signals will not be transferred from one circuit to the other. It also allows you to use a very high input voltage, at a frequency of up to 2kHz.

I have also decided to combine this with the PCF8574 I2C Port Extender. That way, I can cascade up to 64 inputs on the I2C bus. In a later version, I will also do an Opto-Isolated Output module.

The Shield is only slightly bigger than the standard Arduino Uno, and all Arduino pins are broken out on headers.
It is important to remember that A4 and A5 should not be used for any other purpose (They provide access to the I2C bus). Likewise, the interrupt pin of the PCF8574 can be connected to either D2 or D3 with a jumper, or left disconnected by completely removing the jumper. Device addressing can be set with the 3-way DIP switch on the board.

8 DI Optically Isolated I2C Arduino Shield

This device is currently being manufactured. In Part 2 of this article, I will show you the completed PCB, as well as give you access to the Gerber design files if you want to manufacture your own. I will also make a limited amount of these boards available for sale from my website ( this site ) as well as from https://www.facebook.com/makeriot2020

MCP23017 with Adafruit Library

In a previous post, I have shown you how to use the MCP23017 16 Port I2C I/O Port extender with the standard Wire library, as supplied with the Arduino IDE. In this post,
I will have a quick look at using Adafruit’s library for this IC. I believe that this library brings a lot of ease-of-use to the part, making it possible to obscure some of the complexity of I2C.

I do however prefer to use the native Wire library myself, as it is slightly faster.

You can download the Adafruit MCP23017 Library from here..

Pin Addressing

When using single pin operations such as pinMode(pinId, dir) or digitalRead(pinId) or digitalWrite(pinId, val) then the pins are addressed using the ID’s below. For example, for set the mode of GPB0 then use pinMode(8, …).

Physical Pin #Pin NamePin ID
21GPA00
22GPA11
23GPA22
24GPA33
25GPA44
26GPA55
27GPA66
28GPA77
1GPB08
2GPB19
3GPB210
4GPB311
5GPB412
6GPB513
7GPB614
8GPB715

Some examples, directly from the library, all code belongs to Adafruit, and was not written by me.

1. A Button Example

#include <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));
}

2. An Interrupt Example

// Install the LowPower library for optional sleeping support.
// See loop() function comments for details on usage.
//#include <LowPower.h>

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

Adafruit_MCP23017 mcp;

byte ledPin=13;

// Interrupts from the MCP will be handled by this PIN
byte arduinoIntPin=3;

// ... and this interrupt vector
byte arduinoInterrupt=1;

volatile boolean awakenByInterrupt = false;

// Two pins at the MCP (Ports A/B where some buttons have been setup.)
// Buttons connect the pin to grond, and pins are pulled up.
byte mcpPinA=7;
byte mcpPinB=15;

void setup(){

  Serial.begin(9600);
  Serial.println("MCP23007 Interrupt Test");

  pinMode(arduinoIntPin,INPUT);

  mcp.begin();      // use default address 0
  
  // We mirror INTA and INTB, so that only one line is required between MCP and Arduino for int reporting
  // The INTA/B will not be Floating 
  // INTs will be signaled with a LOW
  mcp.setupInterrupts(true,false,LOW);

  // configuration for a button on port A
  // interrupt will triger when the pin is taken to ground by a pushbutton
  mcp.pinMode(mcpPinA, INPUT);
  mcp.pullUp(mcpPinA, HIGH);  // turn on a 100K pullup internally
  mcp.setupInterruptPin(mcpPinA,FALLING); 

  // similar, but on port B.
  mcp.pinMode(mcpPinB, INPUT);
  mcp.pullUp(mcpPinB, HIGH);  // turn on a 100K pullup internall
  mcp.setupInterruptPin(mcpPinB,FALLING);

  // We will setup a pin for flashing from the int routine
  pinMode(ledPin, OUTPUT);  // use the p13 LED as debugging
  
}

// The int handler will just signal that the int has happen
// we will do the work from the main loop.
void intCallBack(){
  awakenByInterrupt=true;
}

void handleInterrupt(){
  
  // Get more information from the MCP from the INT
  uint8_t pin=mcp.getLastInterruptPin();
  uint8_t val=mcp.getLastInterruptPinValue();
  
  // We will flash the led 1 or 2 times depending on the PIN that triggered the Interrupt
  // 3 and 4 flases are supposed to be impossible conditions... just for debugging.
  uint8_t flashes=4; 
  if(pin==mcpPinA) flashes=1;
  if(pin==mcpPinB) flashes=2;
  if(val!=LOW) flashes=3;

  // simulate some output associated to this
  for(int i=0;i<flashes;i++){  
    delay(100);
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
  }

  // we have to wait for the interrupt condition to finish
  // otherwise we might go to sleep with an ongoing condition and never wake up again.
  // as, an action is required to clear the INT flag, and allow it to trigger again.
  // see datasheet for datails.
  while( ! (mcp.digitalRead(mcpPinB) && mcp.digitalRead(mcpPinA) ));
  // and clean queued INT signal
  cleanInterrupts();
}

// handy for interrupts triggered by buttons
// normally signal a few due to bouncing issues
void cleanInterrupts(){
  EIFR=0x01;
  awakenByInterrupt=false;
}  

/**
 * main routine: sleep the arduino, and wake up on Interrups.
 * the LowPower library, or similar is required for sleeping, but sleep is simulated here.
 * It is actually posible to get the MCP to draw only 1uA while in standby as the datasheet claims,
 * however there is no stadndby mode. Its all down to seting up each pin in a way that current does not flow.
 * and you can wait for interrupts while waiting.
 */
void loop(){
  
  // enable interrupts before going to sleep/wait
  // And we setup a callback for the arduino INT handler.
  attachInterrupt(arduinoInterrupt,intCallBack,FALLING);
  
  // Simulate a deep sleep
  while(!awakenByInterrupt);
  // Or sleep the arduino, this lib is great, if you have it.
  //LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);
  
  // disable interrupts while handling them.
  detachInterrupt(arduinoInterrupt);
  
  if(awakenByInterrupt) handleInterrupt();
}


I hope that this shows you another way of using this versatile IC, 
In a future post, I will show you how to do interrupts, using the native Wire library, as well as point out a few things about why interrrupts sometimes does not seem to be working, as well as a workaround for that.

Using the MCP23017 to increase your GPIO’s

Today I will show you another useful IO Expander chip, The MCP23017. This chip, although similar to the PCF8475, which I have already covered in a previous article, has many additional features that may make it a very attractive solution when you need some more extra GPIO pins for a big project…

Features

Let us look at some of the features of this chip

  • 16-Bit Remote Bidirectional I/O Port:
  • I/O pins default to input
    • High-Speed I2C Interface (MCP23017):
  • 100 kHz
  • 400 kHz
  • 1.7 MHz
    • High-Speed SPI Interface (MCP23S17):
  • 10 MHz (maximum)
    • Three Hardware Address Pins to Allow Up to
    Eight Devices On the Bus
    • Configurable Interrupt Output Pins:
  • Configurable as active-high, active-low or
    open-drain
    • INTA and INTB Can Be Configured to Operate
    Independently or Together
    • Configurable Interrupt Source:
  • Interrupt-on-change from configured register
    defaults or pin changes
    • Polarity Inversion Register to Configure the
    Polarity of the Input Port Data
    • External Reset Input
    • Low Standby Current: 1 µA (max.)
    • Operating Voltage:
  • 1.8V to 5.5V @ -40°C to +85°C
  • 2.7V to 5.5V @ -40°C to +85°C
  • 4.5V to 5.5V @ -40°C to +125°C
MCP23017 Pinout Diagram

The sixteen I/O ports are separated into two ‘ports’ – A (on the right) and B (on the left. Pin 9 connects to 5V, 10 to GND, 11 isn’t used, 12 is the I2C bus clock line (Arduino Uno/Duemilanove analogue pin 5, Mega pin  21), and 13 is the I2C bus data line (Arduino Uno/Duemailnove analogue pin 4, Mega pin 20).

External pull-up resistors should be used on the I2C bus – in our examples we use 4.7k ohm values. Pin 14 is unused, and we won’t be looking at interrupts, so ignore pins 19 and 20. Pin 18 is the reset pin, which is normally high – therefore you ground it to reset the IC. So connect it to 5V!

Finally we have the three hardware address pins 15~17. These are used to determine the I2C bus address for the chip. If you connect them all to GND, the address is 0x20. If you have other devices with that address or need to use multiple MCP23017s, see figure 1-2 in the datasheet.

You can alter the address by connecting a combination of pins 15~17 to 5V (1) or GND (0). For example, if you connect 15~17 all to 5V, the control byte becomes 0100111 in binary, or 0x27 in hexadecimal.

It is also available on a convenient breakout PCB, for about $USD0.80 from AliExpress

MCP23017 on Breakout PCB – Back
MCP23017 on Breakout PCB – Front

Please Note: THIS BREAKOUT PCB IS NOT SUITED FOR USE ON A BREADBOARD. YOU WILL SHORT OUT VCC AND GROUND AS WELL AS ALL THE IO PINS IF YOU TRY TO USE IT ON A BREADBOARD.

As you can see, the pins are however very clearly labelled, and thus easy to use. I have also purposely soldered my header pins “the wrong way round” to prevent using it on a breadboard, as this will short out Vcc to Ground!

Having interrupt outputs is one of the most important features of the MCP23017, since the microcontroller does not have to continuously poll the device to detect an input change. Instead an interrupt service routine can be used to react quickly to an input change such a key press…

To make life even easier each GPIO input pin can be configured with an internal pullup (~100k) and that means you won’t have to wire up external pull up resistors for keyboard input. You can also mix and match inputs and outputs the same as any standard microcontroller 8 bit port.

Addressing

The 23017 has three input pins to allow you to set a different address for each attached MCP23017.

The above corresponds to a hardware address for the three lines A0, A1, A2 corresponding to the input pin values at the IC. You must set the value of these hardware inputs as 0V or (high) volts and not leave them floating otherwise they will get random values from electrical noise and the chip will do nothing!

The four left most bits are fixed a 0100 (specified by a consortium who doles out address ranges to manufacturers).

So the MCP23017 I2C address range is 32 decimal to 37 decimal or 0x20 to 0x27 for the MCP23017.

Please note: The addresses are the same as those for the PCF8475. You must thus be careful if you use these two devices on the same i2c bus!

MCP23017 Non interrupt registers

IODIR I/O direction register

For controlling I/O direction of each pin, register IODIR (A/B) lets you set the pin to an output when a zero is written and to an input when a ‘1’ is written to the register bit. This is the same scheme for most microcontrollers – the key is to remember that zero (‘0’) equates to the ‘O’ in Output.

GPPU Pullup register

Setting a bit high sets the pullup active for the corresponding I/O pin.

OLAT Output Latch register

This is exactly the same as the I/O port in 18F series PIC chips where you can read back the “desired” output of a port pin whether or not the actual state of that pin is reached. i.e. consider a strong current LED attached to the pin – it is easily possible to pull down the output voltage at the pin to below the logic threshold i.e. you would read back a zero if reading from the pin itself when in fact it should be a one. Reading the OLAT register bit returns a ‘one’ as you would expect from a software engineering point of view.

IPOL pin inversion register

The IPOL(A/B) register allows you to selectively invert any input pin. This reduces the glue logic needed to interface other devices to the MCP23017 since you won’t need to add inverter logic chips to get the correct signal polarity into the MCP23017.

It is also very handy for getting the signals the right way up e.g. it is common to use a pull up resistor for an input so when a user presses an input key the voltage input is zero, so in software you have to remember to test for zero.

Using the MCP23017 you could invert that input and test for a 1 (in my mind a key press is more equivalent to an on state i.e. a ‘1’) however I use pullups all the time (and uCs in general use internal pullups when enabled) so have to put up with a zero as ‘pressed’. Using this device would allow you to correct this easily.Note: The reason that active low signals are used everywhere is a historical one: TTL (Transistor Transistor Logic) devices draw more power in the active low state due to the internal circuitry, and it was important to reduce unnecessary power consumption – therefore signals that are inactive most of the time e.g. a chip select signal – were defined to be high. With CMOS devices either state causes the same power usage so it now does not matter – however active low is used because everyone uses it now and used it in the past.

SEQOP polling mode : register bit : (Within IOCON register)

If you have a design that has critical interrupt code e.g. for performing a timing critical measurement you may not want non critical inputs to generate an interrupt i.e. you reserve the interrupt for the most important input data.

In this case, it may make more sense to allow polling of some of the device inputs. To facilitate this “Byte mode” is provided. In this mode, you can read the same set of GPIOs using clocks but not needling to provide other control information. i.e. it stays on the same set of GPIO bits, and you can continuously read it without the register-address updating itself. In non-byte mode, you either have to set the address you read from (A or B bank) as control input data.

Now to examine how to use the IC in our sketches.

As you should know by now most I2C devices have several registers that can be addressed. Each address holds one byte of data that determines various options. So before using we need to set whether each port is an input or an output. First, we’ll examine setting them as outputs. So to set port A to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x00); // IODIRA register
Wire.write(0x00); // set all of port A to outputs
Wire.endTransmission();

Then to set port B to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x01); // IODIRB register
Wire.write(0x00); // set all of port B to outputs
Wire.endTransmission();

So now we are in void loop()  or a function of your own creation and want to control some output pins. To control port A, we use:

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(??);  // value to send
Wire.endTransmission();

To control port B, we use:

Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B
Wire.write(??);  // value to send
Wire.endTransmission();

… replacing ?? with the binary or equivalent hexadecimal or decimal value to send to the register.

To calculate the required number, consider each I/O pin from 7 to 0 matches one bit of a binary number – 1 for on, 0 for off. So you can insert a binary number representing the status of each output pin. Or if binary does your head in, convert it to hexadecimal. Or a decimal number.

So for example, you want pins 7 and 1 on. In binary that would be 10000010, in hexadecimal that is 0x82, or 130 decimal. (Using decimals is convenient if you want to display values from an incrementing value or function result).

For example, we want port A to be 11001100 and port B to be 10001000 – so we send the following (note we converted the binary values to decimal):

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(204); // value to send
Wire.endTransmission();
Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B 
Wire.write(136);     // value to send
Wire.endTransmission();

A complete Example

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
void setup()
{
 Wire.begin(); // wake up I2C bus
// set I/O pins to outputs
 Wire.beginTransmission(0x20);
 Wire.write(0x00); // IODIRA register
 Wire.write(0x00); // set all of port A to outputs
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x01); // IODIRB register
 Wire.write(0x00); // set all of port B to outputs
 Wire.endTransmission();
}
void binaryCount()
{
 for (byte a=0; a<256; a++)
 {
 Wire.beginTransmission(0x20);
 Wire.write(0x12); // GPIOA
 Wire.write(a); // port A
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x13); // GPIOB
 Wire.write(a); // port B
 Wire.endTransmission();
 }
}
void loop()
{
 binaryCount();
 delay(500);
}

Using the pins as inputs

Although that may have seemed like a simple demonstration, it was created show how the outputs can be used. So now you know how to control the I/O pins set as outputs. Note that you can’t source more than 25 mA of current from each pin, so if switching higher current loads use a transistor and an external power supply and so on.

Now let’s turn the tables and work on using the I/O pins as digital inputs. The MCP23017 I/O pins default to input mode, so we just need to initiate the I2C bus. Then in the void loop() or other function all we do is set the address of the register to read and receive one byte of data.

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;
void setup()
{
 Serial.begin(9600);
 Wire.begin(); // wake up I2C bus
}
void loop()
{
 Wire.beginTransmission(0x20);
 Wire.write(0x13); // set MCP23017 memory pointer to GPIOB address
 Wire.endTransmission();
 Wire.requestFrom(0x20, 1); // request one byte of data from MCP20317
 inputs=Wire.read(); // store the incoming byte into "inputs"
 if (inputs>0) // if a button was pressed
 {
 Serial.println(inputs, BIN); // display the contents of the GPIOB register in binary
 delay(200); // for debounce
 }
}

Other Libraries

You can also download and install the MCP23017 Library from Adafruit for the Arduino IDE.
This library will make using this chip even easier… I will discuss this library in another post

I hope this will be useful to somebody.

Using I2C with a 4×4 Matrix Keypad

Using a matrix keypad is a very easy way to add multiple control buttons to a project, be it to enter a password, or to control different devices. These keypads do unfortunately have some serious flaws (in my view anyway)

1) They are usually of extremely low quality ( especially some of the membrane types from China). This means they dont last very long.
2) A typical 4×4 Matrix keypad will require 8 of your precious IO pins for itself.

These two flaws can however easily be solved, if we use a bit of technology, and are willing to to a bit of simple circuit construction by ourselves.

What does this mean ? Most of us makers will inevitably have a piece of proto-board or strip-board lying around, as well as a few momentary push-button switches. These can easily be used to make out own, much more reliable keypad. Let us look at the circuit

Circuit diagram for a 4×4 Matrix Keypad

As we can see, to build a 4×4 matrix keypad, we will need 16 momentary switches. These are connected together as shown above. You can then interface it with your favourite micro-controller to read the key(s) pressed…

This definitely solves the first of my problems, but we still need 8 pins to control this keypad… or do we? No, we don’t, we need only 2 pins. That is to say if we use one of those PCF8574 I2C IO port expander modules. They are much more reliable, as well as quite cheap as well. all depending on where you buy them from, and how long you are willing to wait for shipping 🙂

Let us see how to connect the keypad to the I2C Module

a 4×4 Membrane Matrix Keypad with PCF8574 I2C port expander module
Connecting the two together, note that we do not connect the INT pin
Connect Power (VCC, GND and I2C lines
Connect to Arduino or your preferred microcontroller. We have used Arduino Uno, Note that you can also connect the I2C to A4 (SDA) and A5(SCL) if you prefer.

Now, we need to install some libraries

The first one is the actual Keypad library, you can download it from the link below

The second library that we will need, is the keypad_i2c library, once again, download it from the link below.

Coding the keypad



#include <Key.h>
#include <Keypad.h>
#include <Keypad_I2C.h>

#define I2CADDR 0x26 // Set the Address of the PCF8574

const byte ROWS = 4; // Set the number of Rows
const byte COLS = 4; // Set the number of Columns

// Set the Key at Use (4x4)
char keys [ROWS] [COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

// define active Pin (4x4)
byte rowPins [ROWS] = {0, 1, 2, 3}; // Connect to Keyboard Row Pin
byte colPins [COLS] = {4, 5, 6, 7}; // Connect to Pin column of keypad.

// makeKeymap (keys): Define Keymap
// rowPins:Set Pin to Keyboard Row
// colPins: Set Pin Column of Keypad
// ROWS: Set Number of Rows.
// COLS: Set the number of Columns
// I2CADDR: Set the Address for i2C
// PCF8574: Set the number IC
Keypad_I2C keypad (makeKeymap (keys), rowPins, colPins, ROWS, COLS, I2CADDR, PCF8574);

void setup () {
  Wire .begin (); // Call the connection Wire
  keypad.begin (makeKeymap (keys)); // Call the connection
  Serial.begin (9600);

}
void loop () {
 
  char key = keypad.getKey (); // Create a variable named key of type char to hold the characters pressed
 
  if (key) {// if the key variable contains
    Serial.println (key); // output characters from Serial Monitor
  }
}

Upload this to your Arduino device and enjoy. This sketch can also be adapted for 1×4, and 4×3 keypads, and with a little modification, will also work perfectly on ESP32 or ESP8266 as well…