Easy to Use RA-02 Breakout Module

Original RA-02 breakout Module, next to improved RA-02 breakout Module

Most Makers and electronics enthusiasts may already know of the RA-02 LoRa Module. Many of them might own an RA-02 Breakout module or two… For those who do, they will surely know about the problems encountered with using this particular breakout module…

The RA-02 module, in itself, is a great piece of kit, and when used on a custom PCB, which was designed with all the little secrets of this module taken into consideration, is a pleasure. Using the RA-02 breakout module, in its existing form factor, does however present quite a few unique challenges, which, if you are unaware of them, can cause quite a few frustrating moments, or even result in permanent damage to the module…

In this post, we will focus on :
1) The Challenges of the existing commercially available RA-02 Breakout Module
2) My Solution to above mentioned Challenges
3)Testing the Module
Maker Uno – An Arduino Uno Clone
Maker Nano RP2040
Maker Pi Pico – Raspberry Pi Pico breakout module


What are these challenges:

1) The module is based on the SX1278 chip from Semtech and is a 3v device. The IO pins are NOT 5v compatible but seem to work for a few hours or so when used with 5v… This causes many people, especially on Youtube, to assume that it is ok to send 5v logic signals to this module…

I have still not seen any Youtube video telling viewers to at least use a resister divider or logic converter… People just don’t know, and those that know seem to be keeping quiet!

Adding logic converters is in fact specified by the datasheet.

2) Adding logic converters means adding additional wiring, and for a breadboard based project, that adds to the complexity.

3) You have a total of 4 ground pins that need to be connected. not connecting all of them, causes funny things to happen, from overheating down to failure… ( My personal experience while researching this project)

4) The existing breakout module is not breadboarding compatible, resulting in a floating assembly with wires going everywhere, which results in unstable connections etc…

Basically something similar to the picture below:

RA-02 breakout Module (original) with Maker Uno and Level converter module

In this picture, I have an existing RA-02 Breakout Module, with an 8 channel Logic converter and an Arduino Uno clone, along with all the needed wiring to make this setup possible… Quite a lot of wires indeed…

My solution:

I design and use quite a few LoRa PCBs and on all of them, I implement logic conversion using the BSS138 N-MOS Mosfet and 10k resistors. It is a cheap and reliable solution, but it can take up quite a lot of space on a PCB, as this means 11 Mosfets and 22 10k resistors if I were to provide level conversion to all of the RA-02’s GPIO and IO pins…

I also have the constant problem of many unnecessary wires, many of which sometimes fail straight out of the box, when prototyping something. I partly solved that by designing a few dedicated PCB solutions, but that is not always ideal,

Using a dedicated Logic Converter IC, and Mosfet based converters to make up the difference, on a breadboard compatible module, seemed like a good idea, so I went ahead and designed the following solution:

RA-02 breakout Module on a breadboard

The breakout board module is breadboard compatible, and also has clearly marked pins to indicate the 3v and 5v sides of the module.

Testing the Module:

Using a 5v device ( Cytron’s Maker Uno )

For my first test, I decided to test with an Arduino Uno Clone, since that is what most Makers and students will have access to. I used Cytron’s Maker Uno platform, which is equipped with some added goodies, in the form of diagnostic LED etc to make prototyping a lot easier.

RA-02 breakout Module, connected to Maker Uno

As we can clearly see, It is only necessary to connect to the 5v logic side of the module, as well as provide 3v and 5v + GND to the module

In this test, I used Sandeep Mistry’s LoRa Library, with the Arduino IDE to do a quick test sketch.

Connections are as follows:

RA-02 Module Maker Uno

MISO D12

MOSI D11

SCK D13

NSS D10

RST D9

DIO0 D2

OE D8

Full code download

Let us look at some important sections though, to thoroughly understand how to use the module:

Pin Declaration

#include <SPI.h>       // include libraries

#include <LoRa.h> // I used Sandeep Mistry’s LoRa Library, as it is easy to use and understand

const int csPin = 10;     // LoRa radio chip select

const int resetPin = 9;    // LoRa radio reset

const int irqPin = 2;     // change for your board; must be a hardware interrupt pin

const int OEPin = 8;     // Output Enable Pin, to enable the Logic Converter

In the Setup function, we need to do a bit of extra work, since our Maker Uno ( or your Arduino Uno ) is a 5v device…

void setup() {

 Serial.begin(115200); // initialize serial

 pinMode(OEPin,OUTPUT); // Setup the OE pin as an Outout

 digitalWrite(OEPin,HIGH); // and Pull it High to enable the logic converter

 while (!Serial);

 Serial.println(“LoRa Duplex – Set spreading factor”);

 // override the default CS, reset, and IRQ pins (optional)

 LoRa.setPins(csPin, resetPin, irqPin); // set CS, reset, IRQ pin

 if (!LoRa.begin(433E6)) {       // initialize ratio at 433 MHz

  Serial.println(“LoRa init failed. Check your connections.”);

  while (true);            // if failed, do nothing

 }

 LoRa.setSpreadingFactor(8);      // ranges from 6-12,default 7 see API docs

 Serial.println(“LoRa init succeeded.”);

}

A comparison, using the standard RS-02 Breakout module, together with one of my own “Arduino type PCB”

ATMEGA328P with 8 Channel Logic Converter.

Original RA-02 Breakout Module, connected to an ATMEGA328P PCB with onboard Level converters

As we can see, you need quite a lot more wires to make this work. It is also worth noting that we have only 8 level converters on this ATMEGA328P PCB, in order to use all of the RA-02’s GPIO, we will need to add an additional external logic converter as well.

Using a 3v Device:

Cytron’s Maker Nano RP2040

For my second test, I decided to be a bit brave, and try to use the new Raspberry Pi Pico ( RP2040 Microprocessor ). I have quite a few of them lying around and have never really done a lot with them, due to the fact that I do not really like using MicroPython or CircuitPython, and also because the recently released Arduino Core for the RP2040 still being quite new… I decided to use a development board that I recently bought from Cytron, the Maker Nano RP2040, as it has all the added diagnostic features to make my life a bit easier, I will also include a test with an original Pi Pico board, to make it more accessible to everyone out there.

RA-02 Breakout Module, connected to Maker Nano RP2040

Once again, I used Sandeep Mistry’s LoRa Library, with the exact same Arduino sketch, used for the Maker Uno test. (I obviously needed to change the pin numbers though, as the RP2040 uses different pins for its SPI interface).

Maker Nano RP2040 RA-02 Breakout Module

NSS 17

MOSI 19

MISO 16

SCK 18

RST 9

DIO0 8

In this case, we DO NOT need the OE pin, as the RP2040 is a native 3v device. The level converter can thus stay disabled, with its pins in tri-state ( high impedance ) mode.

If we look at the code, it is similar to the Maker Uno’s code, with only the Pin declarations needing a change

#include <SPI.h>       // include libraries

#include <LoRa.h>

const int csPin = 17;     // LoRa radio chip select

const int resetPin = 9;    // LoRa radio reset

const int irqPin = 8;     // change for your board; must be a hardware interrupt pin

byte msgCount = 0;      // count of outgoing messages

int interval = 2000;     // interval between sends

long lastSendTime = 0;    // time of last packet send

// Note that SPI has different names on the RP2040, and it has 2 SPI ports. We used port 0

// CIPO (Miso) is on pin 16

// COPI (Mosi) is on pin 19

// SCK is on pin 18

// CE/SS is on pin 17, as already declared above

I did not use a breadboard, in order to make things as easy as possible.

Cytron’s Maker Pi Pico – A Pi Pico on a breakout PCB

RA-02 Breakout Module, connected to Maker Pi Pico

To make things a bit easier, without having to resort to using a breadboard, I decided to do the Original Pi Pico test using the Maker Pi Pico PCB. This PCB is basically a big breakout module, with detailed pin numbers and some diagnostic LEDs, but it also uses a native Pi Pico, soldered directly to the PCB, by means of the castellated holes… So, While technically not being a true standalone Pico, It makes my life easier and was thus used for the test, as I can be sure that the pins are labelled exactly the same as on the original Pico.

The code used for the Maker Nano RP2040 works perfectly, with no changes required.

This post is getting quite long by now, so I have decided not to include my tests of the ESP-12E ( NodeMCU ) or ESP32 development boards here as well… They also function as expected.

In Summary

When I started this project, I set out to solve a problem ( personal to me ), that could potentially help a lot of other people use the RA-02 Module for more projects and tasks. The Breakout module in its current form can also be used with the RA-01h module (915Mhz Module) without any changes. All GPIO pins are broken out, and accessible through full logic converted pins on both sides of the breakout module.

I hope that this will be useful to someone. I am also not releasing the full schematics at this stage, as I may decide to make some minor cosmetic changes in the near future.

The PCB can however be ordered from PCBWay in its current form and works 100% as expected. The BOM file is available with the ordered PCB as usual.

PCBWay Banner

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,

PCBWay Start Quotation Page

4) Click on PCB Instant Quote

PCBWay Instant Quote

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

Quick order PCB from PCBWay

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.

PCBWay PCB parameters
PCBWay PCB Parameters - Page 2

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 ).

PCBWay Stencil
PCBWay Checkout

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.

LoRa – Part 2

So many people asked me which Lora Module I use for my projects. In this part of the series, I will show you, as well as shed some light on another module, that although seemingly cheap, is, unfortunately, according to me, a complete waste of time and money.

Heltec LoRa 32 v 2 – The good stuff ( according to me at least)

Technical Specs
Electrical Specifications
Pinout

Installation in Arduino IDE:

Installation of the libraries into the Arduino IDE is quite easy, just follow the link to heltec…

The Bad ( according to me )

The following module, is, according to me, an absolute waste of time and money. Documentation is impossible to find, and that that you do find, as often incorrect. The pin-outs are wrong, with no definite standard.

I am talking about the TTGO Lora V1 or V2 or what ever ??? can seem to even find that answer reliably. I was initially attracted to this module, as it was allegedly compatible with the heltec version, and did not have the oled screen, which, to be honest, is not always needed in every project. It was also about 25% cheaper, and could be sourced locally, without enriching the greedy shipping companies 😉 (I just have to rant about this, as 25USD to ship 100g worth of stuff is a ripoff. Either that or 60 to 90 days of guess-if-it-will-arrive mail is not on ( and even that is 10 USD!)

So, having high hopes, I ordered one of these boards, hoping to use it together with my heltec boards… It arrived, and that was well the top came of.. I could immediately see that the quality of the PCB was quite bad. Documentation was missing, and even the supplier sent me to a heltec pinout, which, after a quick test were definitely not correct…

Google turned up mixed results, and eventually I found a sort of accurate pinout …

Alleged pinout for TTGO LoRa device

This pinout also turned out to be only about 50% correct, and after manually trying to map out the pins, I was sort of confident enough to test it further…

Further problems arose, LoRa does not work, I2C does not work, SPI does not work shall I continue…? 🙂 It now seems clear that the board that I bought was a clone of a clone, and a very bad one at that …
I will post a picture of the actual board below, in the interest of education, to inform others not to get duped as well. Likewise, If I am the mistaken party, and you have had success with this board, please give me a message/yell and lets share some knowledge

The Front (Top Side) of the Module

Front (Top) of the module
Back (Bottom) of the module

I hope that you found this useful and that I will see you for part 3 of the series, where I will get into the actual coding.

What is LoRa?

Introduction

When designing IoT solutions, we all encounter the problem of connecting our device(s) to each other, either directly, or through the internet. In Urban areas, it is quite easy to use WiFi or even GSM to achieve this, but these solutions often come with additional costs in the form of subscriptions. Although it is possible to run your own WiFi network free of charge, you will soon run into issues with the range…

Enter LoRa (short for Long Range) Radio communication. LoRa is a radio technology derived from chirp spread spectrum technology. It uses an ISM band, meaning it is unregulated in most countries, if you use the correct frequency for your country, that is.

It is also extremely low power, making it ideal for use with battery-powered devices.
The technology is available in Node-to-Node, as well as Node-to -Gateway modes.

In this series, I will show you how to use a few of the existing LoRa Modules available on the market.

Ai-Tinker Ra-02 (Sx1278)

Ra-02 Lora Module, with spring antenna, by Ai-Tinker

This Module is conveniently broken-out onto a breakout board. It is sort of bread-board friendly (depending on the size of your bread-board) and is nicely labelled. It is also extremely cheap ( around $USD5 each, depending on where you buy from).

Caveats

There are quite a few important things that you should know about these modules before you start using them.

Disclaimer: The caveats listed below are by no means complete, or even valid. They are the result of experimentation by myself, with the intent to destroy a few modules, to see how hardy they are. Also take into mind, that living in SE Asia, it is quite common to buy something from a shop, where the seller has no or only a very limited idea of what he or she is selling, and are thus usually quite unable to provide any technical support.

To summarise: USE YOUR HEAD. If I did leave out something, it is quite possible that I forgot, or decided not to include it on purpose. This is a general guide, and you should ideally do your own research as well. That is the best way to learn.



1) Always connect an Antenna. This may seem like a logical one, but it is extremely important. The module is capable of quite a lot of transmission power, and operating it without an antenna will quickly damage the module, permanently.

2) ONLY use 3.3v, even on the control lines (the module uses SPI). This is quite important, as it is not very clearly stated by the suppliers, and will result in very short-lived component operation 😉 If you absolutely have to use 5v, use a level converter. (There are examples available on the internet, where they use this chip directly from an Arduino Uno. I can confirm that that approach does work, BUT, not for very long. I have purposely sacrificed a pair of transceiver modules so that you don’t have to. You can also adjust the SPI frequency, in the event that your level converter is not capable of running at a high SPI frequency.

3)Make sure that you connect ALL the ground pins on the device. This is another area that is not fully explained by the user manual and does “unexplainably” result in damaged modules.

4) Use short, good quality cables, and if possible, keep the module off the breadboard.
While testing the modules, I found that the usual DuPont wires, as everyone should know by now, are quite unreliable. Combine that with a bread-board that has seen its share of use,
and it is a definite recipe for headache 🙂

5) LoRa Antennas are polarised, make sure you have your antennas in the same orientation.
Although this will not prevent it from working over short distances, it makes sense to just do it correctly. Good RF practices never hurt anybody 🙂

Connecting to Arduino

A Note on Power:
It is important to power this module from a decent dedicated 3.3v power-supply.
The Arduino Uno does sometimes have a 3.3v regulator on-board. From my tests, it is however not always up to the task, as the module may spike up to 120mA when transmitting. It is thus also recommended to have a nice fat capacitor across the power lines (decoupling cap) to soak up any spikes.

As mentioned above, a level converter is mandatory for a 5v Arduino. You may do without it if you use a 3.3v Arduino, but once again, your mileage will vary 🙂

Both the transmitter and receiver uses the same connections, which are listed below:

LoRa SX1278 ModuleArduino Board
3.3V
GndGnd
En/NssD10
G0/DIO0D2
SCKD13
MISOD12
MOSID11
RSTD9
Connections to the Arduino from a LoRa RA-02 Module

Remember that you NEED a Level converter between the LoRa Module and the Arduino.

Software Library

The software library that we will use in our example is the excellent library from Sandeep Mistry. We will just include this into the Arduino IDE, and then use a slightly modified version of the examples for our experiment. It is also important to note that we will use Node-to Node communication, NOT LoRaWan. This means that all your communications will essentially be unencrypted, and not addressed. This does however allow you the flexibility to design and implement your own addressing scheme.

LORA code for Transmitting Side

#include <SPI.h>
#include <LoRa.h>

int counter = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("LoRa Sender");

  if (!LoRa.begin(433E6)) { // Set the frequency to that of your  //module. Mine uses 433Mhz, thus I have set it to 433E6
    Serial.println("Starting LoRa failed!");
    while (1);
  }

  LoRa.setTxPower(20);
  
}

void loop() {
  Serial.print("Sending packet: ");
  Serial.println(counter);

  // send packet
  LoRa.beginPacket();
  LoRa.print("hello ");
  LoRa.print(counter);
  LoRa.endPacket();

  counter++;

  delay(5000);
}
LORA code for Receiver Side

#include <SPI.h>
#include <LoRa.h>

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("LoRa Receiver");

  if (!LoRa.begin(433E6)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
}

void loop() {
  // try to parse packet
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    // received a packet
    Serial.print("Received packet '");

    // read packet
    while (LoRa.available()) {
      Serial.print((char)LoRa.read());
    }

    // print RSSI of packet
    Serial.print("' with RSSI ");
    Serial.println(LoRa.packetRssi());
  }
}

Where to from here?

If all went well, you will see packets being received in the serial monitor of the Arduino IDE, connected to the receiver module. You will also see that the data from this example is sent as a string… It is however also possible to send binary data, by using the LoRa.write() function.

In the next part of this series, I will show you how to use LoRa with the ESP32/ESP8266,
as well as a working example with binary data transmission and an addressing scheme in part 3.

Thank you

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.

ESP32/ESP8266 WiFi Config and OTA on Demand

While working on a recent project, using a custom version of the ESP32, I had a particular need to be able to update the firmware of the device, as well as allow the end-user to set various configuration options on the device. This need can best be described as follows below:

1. The device firmware needs to be updated periodically, as new features are needed, or when bugs are discovered by the end-user, that needs fixing. The problem encountered was that the physical device will be quite far away from me, so travelling, or sending the device to and fro via post was not really an option.

Sending uncompiled firmware to the customer is a possibility, but also risky and prone to errors, as everyone is not always inclined to learn how to update firmware via USB port from an IDE, and accidental changes to code can render it unable to compile and upload in the first place.

The device also needs to run on battery power, in remote areas without connectivity. Conserving battery power is thus also a very important issue, one that prevents me from having a permanent OTA server running via WiFi.

2. The second issue was that there are certain setup parameters that needed to be set by the end-user, these include addresses and others. My initial thoughts were to implement a simple UI via UART, but that I also quickly saw that that, although very useable to me, would not appeal very much to an end-user.

These two issues, OTA and Configuration, both done through WiFi, seemed mutually exclusive as far as all the research that I have done online seemed to be concerned. To complicate it even further, the WiFi HAD to stay off when not in use.

I believe that I have developed a very neat, workable solution, that I would like to share with you today, in the hope that it will solve some problems for somebody out there, that maybe having a similar problem.

My solution makes use of a “flag” set in the ESP32/ESP8266 device’s EEPROM.
This “flag” can then be set to a “Normal” Run mode, a Firmware Update Mode, and a Configuration Mode.

Please Note: The code below has been tested, and works perfectly. You will however have to make modifications to use it for your own needs.

The code is also very long, so I will explain the operation here:

When first started, the chip will run your standard code.
When you quickly press the PGM button, 5 times after each other, a flag will be set to 0x0F.
This will then activate the ESP Soft IP.
You can then connect to this AP, and telnet to 192.168.4.1 on port 23

You will be presented with a very brief menu, where you can SET Firmware Mode, Exit or see the menu again.

This can be expanded to suit your needs

On setting Firmware Mode, the Flag 0xFF is written to EEPROM, and the ESP is restarted.
upon startup, you once more connect to the generated SoftIP, and then browse to http://192.168.4.1, log in with username admin, and password admin.

You can now load and upload a .bin firmware file to the ESP.
on successful upload, the flag 0x00 is once more written to EEPROM.

After restarting, the device will once more run your standard code.


Let us look at how I have done this:

The actual code will be commented with details, please read that for more info.

#include "EEPROM.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

#define EEPROM_SIZE 64 // This can be smaller or larger, adjust to    //your needs as well as the chip capabilities
int wifi_ap_on = 0; // the address for the "flag"


// Wifi for Firmware Update and Configuration Server(s)
const char* host = "ESP32/8266";
char* ssid = "--- your desired ssid ---";
char* password = "--- your password ---";

// Define two types of Servers, they will however not run at the same // time
WebServer OTAserver(80); // OTA Server Port
WiFiServer ConfigServer(23); // Configuration Server Port

String header; // HTML requests will be stored in here

// HTML Pages for Firmware Updater
/*
 * These are the standard OTA examples that came with Arduino IDE
 * Modify and enhance as see fit, keeping in mind that my application
 * would not have access to the internet
 */

/*
 * Login page
 */
const char* loginIndex = 
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP Firmware Update Page</b></font></center><br>"
                
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<td>Username:</td>"
        "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";

const char* serverIndex = "<form method='POST' action='/update' enctype='multipart/form-data'><input type='file' name='update'><input type='submit' value='Update'></form>";
// End HTML Pages 

// Other variables
int Btn = 0; // The PGM button, Usually connected to GPIO0
int wifi_ap = 0; // The Running MODE
int buttonState;
int buttonValue = 0;
int lastButtonState = LOW;
int buttoncount = 0;

void setup()
{
   Serial.begin(115200);
   EEPROM.begin(EEPROM_SIZE); initiate EEPROM
  /*
   * Upload the sketch via USB the first time, and let chip run for
   * a few seconds. We need to set the EEPROM location to a known
   * initial value. Then comment the following 3 lines, to prevent 
   * the chip from overwriting the EEPROM location on next startup
   */
  pinMode(Btn,INPUT);
  EEPROM.write(wifi_ap_on,0x00); // comment this line after first run
  EEPROM.commit();// comment this line after first run
  delay(50);// comment this line after first run
  wifi_ap = EEPROM.read(wifi_ap_on); // read EEPROM value into "flag"
  
  if (wifi_ap == 0x00) { 
   // This will be executed on normal startup
   // Place all your normal initialisation stuff here
   // For example other pin configurations and 
   // peripheral  initialisations
  } else if (wifi_ap == 0xFF) { // Firmware Update MODE
   // Setup for OTA Webserver.
   // I have used SoftAP mode, as internet would not
   // be available anyway.

    WiFi.softAP(ssid,password);
    IPAddress IP = WiFi.softAPIP();
    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP()); 
     
    if (!MDNS.begin(host)) { //http://esp32.local
        Serial.println("Error setting up MDNS responder!");
        while (1) {
        delay(1000);
      }
    }

    
    server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress

/*
 * NOTE : We clear the EEPROM Flag after a sucessful Firmware Update
 * That way, the chip will stay in OTA mode on a failure.
 */
        EEPROM.write(wifi_ap_on,0x00);
        EEPROM.commit();
        delay(5);
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
    OTAserver.begin();
  } else if (wifi_ap == 0x0F) { // End of Mode 0xFF 

    //This is Configuration Mode
    //This code will normally not be executed,
    //As the device exits Config mode on restart, and
    //We dont write Mode 0x0F to EEPROM

  } // End of Mode 0x0F
} // End of Void Setup

void loop() 
{
  CountButton(); // Count Button Presses
  if (buttoncount == 5) { // Activate Configuration Mode
    buttoncount = 0;
    StartSoftAP(); // Start a SoftAP
  }
  if (wifi_ap == 0x0F) { // Code for the Config Server (TELNET Style)
       WiFiClient client = ConfigServer.available(); // Start Config
       if (client) {
          String currentLine = "";
          client.println();
          client.println("Telnet Server - Welcome");
          client.println();
          client.println("Press <Enter> to Login");
          
    while (client.connected()) {
      if (client.available()) {
       char c = client.read();
       if (c == '\n') {
        if (currentLine.length() == 0) {
           
        } else {
          // Process Commands
       if (currentLine == "Master123abc") {
        telnetLogin == true;
        client.println();
        client.println();
        client.println("****** MENU ******");
          client.println();
          client.println("MENU -  SHOW THIS MENU");
          client.println("RUF  -  Enable Firmware Update Mode");
          client.println("EXIT -  Exit Menu and RESTART");
          client.println();
          client.print("Command> ");
          currentLine = "";
       }
       // Show MENU
       if (currentLine == "MENU") {
        client.println();
        client.println();
        client.println("****** MENU ******");
          client.println();
          client.println("MENU -  SHOW THIS MENU");
          client.println("RUF  -  Enable Firmware Update Mode");
          client.println("EXIT -  Exit Menu and RESTART");
          client.println();
          client.print("Command> ");
          
       }
 
       // EXIT AND RESTART
       if (currentLine == "EXIT") {
        client.println();
        client.println("Restarting in 5 Seconds");
        delay(5000);
        client.stop();
        ESP.restart();
      }
      // Upgrade Frmware Mode
      if (currentLine == "RUF") {
          client.println();
          client.println("Enable Firmware Update Mode");
          client.print("Connect to SoftAP :");
          client.println(ssid);
          client.println("Browse to http://192.168.4.1 ");
          client.println("Login with Username admin, Password admin");
          EEPROM.write(wifi_ap_on,0xFF); // Set Flag in EEPROM
          EEPROM.commit();
          delay(5000);
          client.stop();
          ESP.restart(); // Restart Device
      }
      
       // End Process commands 
           
          currentLine = ""; 
        }
        
       } else if (c != '\r') {
        currentLine += c;
       }  
        
      } // End if client Available
    } // End While Client Connected
    client.stop();
   } // END Client
  } // END MODE 0x0F
  if (wifi_ap == 0xFF) {
    OTAserver.handleClient(); // Handle OTA Server
    delay(1);
  }
  if (wifi_ap == 0x00 )
    // Normal code goes here , to be executed every cycle
  {

} // End of Void Loop

void StartSoftAP() // Start SoftAP, for Configuration Server
{
  WiFi.mode(WIFI_OFF);
  WiFi.softAP(ssid,password);
  IPAddress IP = WiFi.softAPIP();
  ConfigServer.begin();  
  wifi_ap = 0x0F; // Set Flag to Config Mode
                  // Note that we dont write it in EEPROM
}

void CountButton() // Counts Button Presses, to enable Config Mode
{
  //int buttonState;
  //int buttonValue = 0;
  //int lastButtonState = LOW;
  //int buttoncount = 0;
  //unsigned long lastDebounceTime = 0;
  //int debounceDelay = 50;
  buttonValue = digitalRead(Btn);
  if (buttonState != lastButtonState) {
    if (buttonValue == LOW) {
      buttoncount++;
      if (buttoncount > 10) buttoncount = 0;
    }
  }
  delay(50);
  lastButtonState = buttonValue;
  // End Debouncing
  if (buttoncount == 5) digitalWrite(LED,HIGH);
}

This concludes a very long piece of code, question are welcome.
Thank you

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…

ESP8266 and ESP32 AT Commands

NodeMCU V3, ESP8266

In this tutorial, I’ll show you some of the important and frequently used ESP8266 AT Commands or AT Instruction Set.  

ESP8266 WiFi Module offers complete networking solutions to our DIY (Do-it-yourself) and IoT (Internet of Things) projects. It provides WiFi connectivity to any microcontroller through its full TCP/IP Stack.

This means that you can use the ESP8266/ESP32 like a WiFi Modem, this is especially handy when you don’t want to reprogram an entire module for a project, or if you already have a working project on an Arduino type board, and just want to add WiFi connectivity to the project.



It is however important to tell you that it is sometimes better to write your own code to achieve exactly what you want. The AT Commands, in my opinion, is however extremely useful to quickly test something, or do a very simple integration. Your opinion and or milage will definitely vary on this one, feel free to comment and make suggestions as always 🙂

Kidbright 32, based on the ESP32 WROOM Chip

Let us get started then

Please NOTE:

The AT Command Set will ONLY function on a NEW ESP8266/ESP32 Module that you have not loaded custom firmware onto, OR on a module that you have re-flashed with the AT Command Firmware. This means that, If you have used the Arduino IDE to upload custom code to your ESP8266/ESP32 module, these commands will NOT work for you,
UNLESS you flash the module with ESPRESSIF AT Command Firmware!

The ESP8266 WiFi module and the microcontroller can be interfaced through the UART and with the help of a wide range of AT Commands, the Microcontroller can then control the ESP Module.

The AT Commands of the ESP8266 WiFi Module are responsible for controlling all the operations of the module like restarting, connecting to WiFi, changing the mode of operation and so forth.

Basically, the ESP8266 AT Commands can be classified into four types:

  • Test
  • Query
  • Set
  • Execute

In the following table, I will give you an example of the different types of AT Commands. I will use a sample command of “TEST” to demonstrate the differences between the different type of commands.

Command TypeCommand FormatCommand Function
TestAT+TEST=?Returns a value or a range of parameters
QueryAT+TEST?Returns the current value of a certain parameter
SetAT+TEST=parameter1, parameter2, …Set configuration of a certain parameter of group of parameters
ExecuteAT+TESTExecutes an action
Types of AT Commands for ESP8266 or ESP32

Test Commands: The Test AT Commands of ESP8266 WiFi Module are used to get the parameters of a command and their range.

Query Commands: The Query Commands returns the present value of the parameters of a command.

Set Commands: The Set Commands are used set the values of the parameters in the commands and also runs the commands.

Execute Commands: The Execute Commands will run the commands without parameters.  

NOTE: Not all of the ESP8266 AT Commands support all the four command types.

The ESP8266 AT Commands Set is divided into three categories. They are:

  • Basic AT Commands 
  • WiFi AT Commands 
  • TCP/IP AT Commands 

There are a total of 88 AT Commands for ESP8266 WiFi Module. We will however only look at a few of the most important ones.

If you want to know the details of all the ESP8266 AT Commands, then I suggest that you visit the official documentation page provided by Espressif Systems (the manufacturer of ESP8266EX SoC),  here.   

NOTE: The Parameters mentioned in [] are optional.

Basic ESP8266 AT Commands

As per the official documentation from Espressif Systems, there are a total of 23 Basic AT Commands.

Basic AT Commands
AT
AT+RST
AT+GMR

AT

This is the basic command that tests the AT start up i.e. if the AT System is working correctly or not. If the AT start up is successful, then the response is OK.

CommandResponse
ATOK

AT+RST

This command can be used to restart (reset) the ESP8266 WiFi Module.    

CommandResponse
AT+RSTOK

AT+GMR

This command is used to check the version information of the firmware and SDK. The response consists of three things: the AT Firmware version, the SDK version and the compilation time of the BIN file.

CommandResponse
AT+GMR<AT Version><SDK Version><Compile Time>OK

Other important Basic AT Commands: AT+GSLP, ATE and AT+UART.

WiFi AT Commands

The WiFi AT Commands are useful in controlling the WiFi features of the ESP8266 Module like setting up the WiFi Mode of operation, get the list of WiFi Networks, connect to a WiFi Network, setup the Access Point (AP), control DHCP, WPS, MAC Address, IP Address etc.

As per the official documentation, there are 40 WiFi AT Commands for ESP8266 Module. Let me introduce a few important AT Commands.

WiFi AT Commands
AT+CWMODE
AT+CWJAP
AT+CWLAP
AT+CWQAP
AT+CIPSTA
AT+CWSAP
AT+CWLIF

AT+CWMODE

This command is used to set the WiFi Mode of operation as either Station mode, Soft Access Point (AP) or a combination of Station and AP. The CWMODE command supports Test, Query and Set type commands.

The syntax, response and parameters (in Set command) of this command are given in the following table.

AT+CWMODE
Command TypeTestQuerySet
FormatAT+CWMODE=?AT+CWMODE?AT+CWMODE=<mode>
Response+CWMODE:<mode>OK+CWMODE:<mode> OKOK
Parameters<mode>1: Station2: Soft Access Point (AP)3: Station+SoftAP
Function Returns current WiFi ModeSets WiFi Mode
      

AT+CWLAP

This command lists out all the available WiFi Networks in the reach of ESP8266. It has both Set and Execute Command types.

AT+CWLAP
Command TypeSetExecute
FormatAT+CWLAP[=<ssid>,<mac>,<channel>,<scan_type>,<scan_time_min>,<scan_time_max>]AT+CWLAP
Response+CWLAP:<ecn>,<ssid>,<rssi>,<mac>,<channel>,<freq      offset>,<freq   cali>,<pairwise_cipher>,<group_cipher>,<bgn>,<wps>OK

NOTE: For more information on Parameters, please refer to the original documentation.

AT+CWJAP

This command is to connect to an Access Point (like a router).

AT+CWJAP
Command TypeQuerySet
FormatAT+CWJAP?AT+CWJAP=<ssid>,<pwd>[,<bssid>]
Response+CWJAP:<ssid>,<bssid>,<channel>,<rssi>OKOKor+CWJAP:<error>FAIL
Parameters<ssid>: SSID of the Access Point.<pwd>: Password.[<bssid>]: MAC Address of AP (usedwhen multiple APs have the same SSID.)<error>1: Connection timeout.2: Wrong password.3: Cannot find the target AP.4: Connection failed.

AT+CWQAP

This command is used to disconnect the ESP8266 from an Access Point.

CommandResponse
AT+CWQAPOK

AT+CIPSTA

This command is used to set a static IP Address to the ESP8266 WiFi Module in Station Mode. This command has both Query and Set type commands.

AT+CIPSTA
Command TypeQuerySet
FormatAT+CIPSTA?AT+CIPSTA=<ip>[,<gateway>,<netmask>]
Response+CIPSTA:<ip>+CIPSTA:<gateway>+CIPSTA:<netmask> OKOK
Parameters<ip>: IP Address<gateway>: Gateway<netmask>: Netmask
FunctionReturns the IP address, Gateway and Netmask.Sets IP Address, Gateway and Netmask.

AT+CWSAP

This command is used to configure the ESP8266 WiFi Module in Soft Access Point (AP) Mode. Both Query and Set types are available for this command.

AT+CWSAP
Command TypeQuerySet
FormatAT+CWSAP?AT+CWSAP =<ssid>,<pwd>,<chl>,<ecn>[,<maxconn>][,<ssid  hidden>]
Response+CWSAP:<ssid>,<pwd>,<chl>,<ecn>,<max conn>,<ssid hidden>OKorERROR
Parameter<ssid>: SSID of AP.<pwd>: Password.<chl>: Channel ID.<ecn>: Encryption method.0: OPEN2: WPA_PSK3: WPA2_PSK4: WPA_WPA2_PSK<max conn>: Max # of Stations<ssid hidden>:0: SSID is broadcasted. (default)1: SSID is not broadcasted.

AT+CWLIF

Using this command, you can get the IP addresses of Stations that are connected to ESP8266, which is configured in SoftAP Mode.

AT+CWLIF
Format (Execute Command)AT+CWLIF
Response<ip addr>,<mac>OK
Parameters<ip address>: IP Address of the Station<mac>: MAC Address of the station

TCP/IP AT Commands

The TCP/IP AT Commands are responsible for communication over the internet. There are a total of 25 TCP/IP AT Commands for ESP8266 WiFi Module. Some of the important ones are mentioned here.

TCP/IP Commands
AT+CIPSTATUS
AT+CIPSTART
AT+CIFSR
AT+CIPMUX
AT+CIPSERVER
AT+CIUPDATE

AT+CIPSTATUS

This TCP/IP AT Command of the ESP8266 WiFi Module get the information or status of the connection. Only the Execute type command is available.

AT+CIPSTATUS
Command TypeExecute
FormatAT+CIPSTATUS
ResponseSTATUS:<stat>+CIPSTATUS:<linkID>,<type>,<remoteIP>,<remoteport>,<localport>,<tetype>
Parameter<stat>:2: Connected to an AP and its IP is obtained.3: Created a TCP or UDP transmission.4: Disconnected.5: Does NOT connect.<linkID>: ID of the connection.<type>: “TCP” or “UDP”.<remoteIP>: Remote IP address.<remoteport>: Remote port number.<localport>: Local port number.<tetype>:0: Client.1: Server.

AT+CIPSTART

This AT Command is used to establish one of the three connections: TCP, UDP or SSL. Depending on the type of TCP Connection (single or multiple), the format of the Set command will vary.

AT+CIPSTART
Command TypeSet
FormatSingle TCP ConnectionMultiple TCP Connection
AT+CIPSTART=<type>,<remoteIP>,<remoteport>[,<TCPkeepalive>]AT+CIPSTART=<linkID>,<type>,<remoteIP>,<remoteport>[,<TCPkeepalive>]
ResponseOKorERROR(Response when TCP connection is already established:ALREADY CONNECTED)
Parameters<link    ID>: ID of connection.<type>: “TCP”, “UDP” or “SSL”.<remoteIP>: Remote IP address.<remoteport>: Remote port number.[<TCPkeepalive>]: detection time interval

NOTE: The above table shows command for only establishing the TCP Connection. For establishing UDP and SSL Connections, please refer to the official documentation.

AT+CIFSR

This AT Command is used to obtain the IP Address of the ESP8266 WiFi Module.

AT+CIFSR
Command TypeExecute
FormatAT+CIFSR
Response+CIFSR:APIP,<SoftAPIPaddress>+CIFSR:APMAC,<SoftAPMACaddress>+CIFSR:STAIP,<StationIPaddress>+CIFSR:STAMAC,<StationMACaddress>OK
Parameters<SoftAPIPaddress>: IP address of the ESP8266 SoftAP;<SoftAPMACaddress>: MAC address of the ESP8266 SoftAP<StationIPaddress>: IP address of the ESP8266 Station.<StationMACaddress>: MAC address of the ESP8266 Station

AT+CIPMUX

This AT Command is used to enable or disable multiple TCP Connections.

AT+CIPMUX
Command TypeQuerySet
FormatAT+CIPMUX?AT+CIPMUX=<mode>
Response+CIPMUX:<mode>OKOK
Parameters<mode>:0: Single connection1: Multiple connections

AT+CIPSERVER

This AT Command is used to create or delete a TCP Server.

AT+CIPSERVER
Command TypeSet
FormatAT+CIPSERVER=<mode>[,<port>]
ResponseOK
Parameters<mode>:0: Delete Server.1: Create Server.

NOTE: A TCP Server can be created only when AT+CIPMUX=1 i.e. multiple connections are enabled.

AT+CIUPDATE

This AT Command is used update the software through WiFi Connection i.e. for over the air (OTA) updates.

AT+CIUPDATE
Command TypeExecute
FormatAT+CIUPDATE
Response+CIPUPDATE:<n>OK
Parameters<n>:
1: Find the Server
2: Connect to the Server
3: Get the Software Version
4: Start Update

I have included a PDF file with the complete AT command Set for download below.

What is SPI? Serial Peripheral Interface – Part 1

Introduction

The Serial Peripheral Interface is a synchronous serial communication interface for short-distance communication, it is typically used in embedded systems. The interface was developed by Motorola in the mid 1980’s and has become a very popular standard.

It is used with many kinds of sensors, LCD’s and also SD-Cards. SPI operates in a Master-Slave model, with a possibility of multiple slave devices, each selected in turn by a SS (slave select) or CS (chip select) pin that is usually pulled low by the master.

Typical connection between two SPI devices

Typical configuration

SPI is a four-wire interface, with the different lines being
– MOSI [Master Out Slave In]
-MISO [Master In Slave Out]
-SCLK [Serial Clock OUT – generated by the master]
-SS/CS [Slave Select or Chip Select, sometimes also labelled CE – Chip Enable]

SPI is a FULL DUPLEX interface, where the master initiates the communication frames between the various slave devices. This is usually done by pulling the particular device’s SS/CS pin low. Data is then shifted simultaneously into and out of the devices by means of the MOSI and MISO lines on the bus. The frequency of the serially shifted data is controlled by the SCLK line. This clock signal is generated by the master device.

It is important to note that MOST of the slave devices have a tri-state (HIGH IMPEDANCE) mode on their MISO pins. This electrically disconnects the MISO pin from the bus when the device is not selected via the SS/CS pin.

You should also note the SPI slave devices that do not have a tri-state mode on their MISO pins, should not be used on the same bus as devices that have without using an external tri-state buffer circuit between the non-tristate device and the rest of the devices on the MISO bus.

Typical connection between an SPI Master and three Slave devices


It is possible to connect multiple SPI slave devices to on Master device if you remember that each slave device will need its own dedicated SS/CS pin on the master. This can however quickly use a lot of IO pins on a microcontroller, thus being one of the disadvantages of SPI versus I2C. SPI is however quite a bit faster than I2C.

Data Transmission

To begin communication, the bus master configures the clock, using a frequency supported by the slave device, typically up to a few MHz. The master then selects the slave device with a logic level 0 on the select line. If a waiting period is required, such as for an analog-to-digital conversion, the master must wait for at least that period of time before issuing clock cycles.

During each SPI clock cycle, full-duplex data transmission occurs. The master sends a bit on the MOSI line and the slave reads it, while the slave sends a bit on the MISO line and the master reads it. This sequence is maintained even when only one-directional data transfer is intended.

A typical hardware setup using two shift registers to form an inter-chip circular buffer

Transmissions normally involve two shift registers of some given word-size, such as eight bits, one in the master and one in the slave; they are connected in a virtual ring topology. Data is usually shifted out with the most significant bit first. On the clock edge, both master and slave shift out a bit and output it on the transmission line to the counterpart. On the next clock edge, at each receiver the bit is sampled from the transmission line and set as a new least-significant bit of the shift register. After the register bits have been shifted out and in, the master and slave have exchanged register values. If more data needs to be exchanged, the shift registers are reloaded and the process repeats. Transmission may continue for any number of clock cycles. When complete, the master stops toggling the clock signal, and typically deselects the slave.

Transmissions often consist of eight-bit words. However, other word-sizes are also common, for example, sixteen-bit words for touch-screen controllers or audio codecs, such as the TSC2101 by Texas Instruments, or twelve-bit words for many digital-to-analogue or analogue-to-digital converters.

Every slave on the bus that has not been activated using its chip select line must disregard the input clock and MOSI signals and should not drive MISO (I.E. must have a tri-state output) although some devices need external tri-state buffers to implement this.

Clock polarity and phasing

In addition to setting the clock frequency, the master must also configure the clock polarity and phase with respect to the data. Motorola SPI Block Guide names these two options as CPOL and CPHA (for clock polarity and phase) respectively, a convention most vendors have also adopted.

The timing diagram is shown below. The timing is further described below and applies to both the master and the slave device.

  • CPOL determines the polarity of the clock. The polarities can be converted with a simple inverter.
  • CPOL=0 is a clock which idles at 0, and each cycle consists of a pulse of 1. That is, the leading edge is a rising edge, and the trailing edge is a falling edge.
  • CPOL=1 is a clock which idles at 1, and each cycle consists of a pulse of 0. That is, the leading edge is a falling edge, and the trailing edge is a rising edge.
  • CPHA determines the timing (i.e. phase) of the data bits relative to the clock pulses. Conversion between these two forms is non-trivial.
  • For CPHA=0, the “out” side changes the data on the trailing edge of the preceding clock cycle, while the “in” side captures the data on (or shortly after) the leading edge of the clock cycle. The out-side holds the data valid until the trailing edge of the current clock cycle. For the first cycle, the first bit must be on the MOSI line before the leading clock edge.
  • An alternative way of considering it is to say that a CPHA=0 cycle consists of a half cycle with the clock idle, followed by a half cycle with the clock asserted.
  • For CPHA=1, the “out” side changes the data on the leading edge of the current clock cycle, while the “in” side captures the data on (or shortly after) the trailing edge of the clock cycle. The out-side holds the data valid until the leading edge of the following clock cycle. For the last cycle, the slave holds the MISO line valid until slave select is de-selected.
  • An alternative way of considering it is to say that a CPHA=1 cycle consists of a half cycle with the clock asserted, followed by a half cycle with the clock idle.
A timing diagram showing clock polarity and phase. Red lines denote clock leading edges, and blue lines, trailing edges.

The MOSI and MISO signals are usually stable (at their reception points) for the half cycle until the next clock transition. SPI master and slave devices may well sample data at different points in that half cycle.

This adds more flexibility to the communication channel between the master and slave.

Mode numbers

The combinations of polarity and phases are often referred to as modes which are commonly numbered according to the following convention, with CPOL as the high order bit and CPHA as the low order bit:

For “Microchip PIC” / “ARM-based” microcontrollers (note that NCPHA is the inversion of CPHA):

SPI modeClock polarity
(CPOL/CKP)
Clock phase
(CPHA)
Clock edge
(CKE/NCPHA)
0001
1010
2101
3110
For PIC32MX: SPI mode configure CKP, CKE and SMP bits. Set SMP bit and CKP, CKE two bits configured as above table.
ModeCPOLCPHA
000
101
210
311
For other microcontrollers:

Another commonly used notation represents the mode as a (CPOL, CPHA) tuple; e.g., the value ‘(0, 1)’ would indicate CPOL=0 and CPHA=1.

Note that in Full Duplex operation, the Master device could transmit and receive with different modes. For instance, it could transmit in Mode 0 and be receiving in Mode 1 at the same time.

Independent Slave Configuration

In the independent slave configuration, there is an independent chip select line for each slave. This is the way SPI is normally used. The master asserts only one chip select at a time.

Pull-up resistors between the power source and chip select lines are recommended for systems where the master’s chip select pins may default to an undefined state. When separate software routines initialize each chip select and communicate with its slave, pull-up resistors prevent other uninitialized slaves from responding.

Since the MISO pins of the slaves are connected together, they are required to be tri-state pins (high, low or high-impedance), where the high-impedance output must be applied when the slave is not selected. Slave devices not supporting tri-state may be used in independent slave configuration by adding a tri-state buffer chip controlled by the chip select signal. (Since only a single signal line needs to be tri-stated per slave, one typical standard logic chip that contains four tristate buffers with independent gate inputs can be used to interface up to four slave devices to an SPI bus.)

Typical SPI configuration

Daisy chain configuration

Some products that implement SPI may be connected in a daisy chain configuration, the first slave output being connected to the second slave input, etc. The SPI port of each slave is designed to send out during the second group of clock pulses an exact copy of the data it received during the first group of clock pulses. The whole chain acts as a communication shift register; daisy chaining is often done with shift registers to provide a bank of inputs or outputs through SPI. Each slave copies input to output in the next clock cycle until the active low SS line goes high. Such a feature only requires a single SS line from the master, rather than a separate SS line for each slave.

Note that not all SPI devices support this. You should thus check your datasheet before using this configuration!

SPI Daisy Chain configuration

Valid Communications

Some slave devices are designed to ignore any SPI communications in which the number of clock pulses is greater than specified. Others do not care, ignoring extra inputs and continuing to shift the same output bit. It is common for different devices to use SPI communications with different lengths, as, for example, when SPI is used to access the scan chain of a digital IC by issuing a command word of one size (perhaps 32 bits) and then getting a response of a different size (perhaps 153 bits, one for each pin in that scan chain).

Interrupts

SPI devices sometimes use another signal line to send an interrupt signal to a host CPU. Examples include pen-down interrupts from touchscreen sensors, thermal limit alerts from temperature sensors, alarms issued by real-time clock chips, SDIO, and headset jack insertions from the sound codec in a cell phone. Interrupts are not covered by the SPI standard; their usage is neither forbidden nor specified by the standard. In other words, interrupts are outside the scope of the SPI standard and are optionally implemented independently from it.

Bit Banging a SPI Master – Example code

Below is an example of bit-banging the SPI protocol as an SPI master with CPOL=0, CPHA=0, and eight bits per transfer. The example is written in the C programming language. Because this is CPOL=0 the clock must be pulled low before the chip select is activated. The chip select line must be activated, which normally means being toggled low, for the peripheral before the start of the transfer, and then deactivated afterwards. Most peripherals allow or require several transfers while the select line is low; this routine might be called several times before deselecting the chip.

/*
 * Simultaneously transmit and receive a byte on the SPI.
 *
 * Polarity and phase are assumed to be both 0, i.e.:
 *   - input data is captured on rising edge of SCLK.
 *   - output data is propagated on falling edge of SCLK.
 *
 * Returns the received byte.
 */
uint8_t SPI_transfer_byte(uint8_t byte_out)
{
    uint8_t byte_in = 0;
    uint8_t bit;

    for (bit = 0x80; bit; bit >>= 1) {
        /* Shift-out a bit to the MOSI line */
        write_MOSI((byte_out & bit) ? HIGH : LOW);

        /* Delay for at least the peer's setup time */
        delay(SPI_SCLK_LOW_TIME);

        /* Pull the clock line high */
        write_SCLK(HIGH);

        /* Shift-in a bit from the MISO line */
        if (read_MISO() == HIGH)
            byte_in |= bit;

        /* Delay for at least the peer's hold time */
        delay(SPI_SCLK_HIGH_TIME);

        /* Pull the clock line low */
        write_SCLK(LOW);
    }

    return byte_in;
}

This concludes part 1 of my series on SPI. I hope you found it interesting and useful.

The OLED Display

Introduction

Adding a display to any project can instantly increase its visual appeal, as well as make the project easier to control. Displays available to Electronic enthusiasts mostly include some sort of LCD or even TFT display. LCD displays are usually bulky and very limited in their ability to display a lot of information, whereas TFT type displays are still a bit on the expensive side, and not very easy to interface with for the beginner.

Today, I would like to introduce a different type of display, which is available in an I2C as well as SPI version. These displays are very easily readable in almost any light conditions, lightweight, and most importantly, they are extremely cheap. I am talking about the OLED display of course… Many of us may already have one of them in our mobile phones, or even TV screen…

128×32 I2C OLED Display (40mmx10mm) [0.91″] Front view

Some Technical Data

An organic light-emitting diode (OLED or Organic LED), also known as an organic EL (organic electroluminescent) diode,[1][2] is a light-emitting diode (LED) in which the emissive electroluminescent layer is a film of organic compound that emits light in response to an electric current. This organic layer is situated between two electrodes; typically, at least one of these electrodes is transparent. OLEDs are used to create digital displays in devices such as television screens, computer monitors, portable systems such as smartphoneshandheld game consoles and PDAs. A major area of research is the development of white OLED devices for use in solid-state lighting applications.[3][4][5]

There are two main families of OLED: those based on small molecules and those employing polymers. Adding mobile ions to an OLED creates a light-emitting electrochemical cell (LEC) which has a slightly different mode of operation. An OLED display can be driven with a passive-matrix (PMOLED) or active-matrix (AMOLED) control scheme. In the PMOLED scheme, each row (and line) in the display is controlled sequentially, one by one,[6] whereas AMOLED control uses a thin-film transistor backplane to directly access and switch each individual pixel on or off, allowing for higher resolution and larger display sizes.

An OLED display works without a backlight because it emits visible light. Thus, it can display deep black levels and can be thinner and lighter than a liquid crystal display (LCD). In low ambient light conditions (such as a dark room), an OLED screen can achieve a higher contrast ratio than an LCD, regardless of whether the LCD uses cold cathode fluorescent lamps or an LED backlight. OLED displays are made in the same way as LCDs, but after TFT (for active matrix displays), addressable grid (for passive matrix displays) or ITO segment (for segment displays) formation, the display is coated with hole injection, transport and blocking layers, as well with electroluminescent material after the 2 first layers, after which ITO or metal may be applied again as a cathode and later the entire stack of materials is encapsulated. The TFT layer, addressable grid or ITO segments serve as or are connected to the anode, which may be made of ITO or metal.[7][8] OLEDs can be made flexible and transparent, with transparent displays being used in smartphones with optical fingerprint scanners and flexible displays being used in foldable smartphones.

The full article is available here if you are interested.

128×32 I2C OLED Display (40mmx10mm) [0.91″] Back view

Connecting the circuit

This display is once again extremely easy to connect, as it uses the very versatile I2C protocol. (An SPI version is also available).

Connecting 128×32 OLED display to an Arduino Uno Clone

Connect the following wires to the Arduino / ESP32
+5v (red) to the VCC pin on the display
Gnd to Gnd
SDA (A4 on Uno) to SDA, and SCL (A5 on Uno) to SCL

The Software Libraries

The 128×32 OLED display that we will be using today, is based on the SSD1306. We will thus be using a library suplied by Adafruit to interface with this chip. There are various other libraries available, but I have found the Adafruit library the most stable.

To load this, start by opening the Arduino IDE, and go to the Sketch->Include Library->Manage Libraries option on the menu

The Library Manager will now open

We need to install two (2) Libraries

– Adafruit GFX ( this is for graphics)
– Adafruit SSD1306 ( to control the actual display )

Click on “Close” after installation is completed.

Using the display

We will use one of the standard Adafruit examples to show you the capabilities of the tiny little screen. The example are so straight forward to use, that I find it unnecessary to say anything else about it 🙂

Open the ssd1306_128x32_ic2 Example from the Examples menu in the Arduino IDE and upload it to your Arduino, making sure that you set the dimensions of your screen first (in my case 128×32 )

/**************************************************************************
 This is an example for our Monochrome OLEDs based on SSD1306 drivers

 Pick one up today in the adafruit shop!
 ------> http://www.adafruit.com/category/63_98

 This example is for a 128x32 pixel display using I2C to communicate
 3 pins are required to interface (two I2C and one reset).

 Adafruit invests time and resources providing this open
 source code, please support Adafruit and open-source
 hardware by purchasing products from Adafruit!

 Written by Limor Fried/Ladyada for Adafruit Industries,
 with contributions from the open source community.
 BSD license, check license.txt for more information
 All text above, and the splash screen below must be
 included in any redistribution.
 **************************************************************************/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define NUMFLAKES     10 // Number of snowflakes in the animation example

#define LOGO_HEIGHT   16
#define LOGO_WIDTH    16
static const unsigned char PROGMEM logo_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000 };

void setup() {
  Serial.begin(9600);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds

  // Clear the buffer
  display.clearDisplay();

  // Draw a single pixel in white
  display.drawPixel(10, 10, SSD1306_WHITE);

  // Show the display buffer on the screen. You MUST call display() after
  // drawing commands to make them visible on screen!
  display.display();
  delay(2000);
  // display.display() is NOT necessary after every single drawing command,
  // unless that's what you want...rather, you can batch up a bunch of
  // drawing operations and then update the screen all at once by calling
  // display.display(). These examples demonstrate both approaches...

  testdrawline();      // Draw many lines

  testdrawrect();      // Draw rectangles (outlines)

  testfillrect();      // Draw rectangles (filled)

  testdrawcircle();    // Draw circles (outlines)

  testfillcircle();    // Draw circles (filled)

  testdrawroundrect(); // Draw rounded rectangles (outlines)

  testfillroundrect(); // Draw rounded rectangles (filled)

  testdrawtriangle();  // Draw triangles (outlines)

  testfilltriangle();  // Draw triangles (filled)

  testdrawchar();      // Draw characters of the default font

  testdrawstyles();    // Draw 'stylized' characters

  testscrolltext();    // Draw scrolling text

  testdrawbitmap();    // Draw a small bitmap image

  // Invert and restore display, pausing in-between
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);

  testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT); // Animate bitmaps
}

void loop() {
}

void testdrawline() {
  int16_t i;

  display.clearDisplay(); // Clear display buffer

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(0, display.height()-1, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=display.width()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=0; i<display.height(); i+=4) {
    display.drawLine(display.width()-1, 0, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=0; i<display.width(); i+=4) {
    display.drawLine(display.width()-1, 0, i, display.height()-1, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000); // Pause for 2 seconds
}

void testdrawrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2; i+=2) {
    display.drawRect(i, i, display.width()-2*i, display.height()-2*i, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }

  delay(2000);
}

void testfillrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2; i+=3) {
    // The INVERSE color is used so rectangles alternate white/black
    display.fillRect(i, i, display.width()-i*2, display.height()-i*2, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }

  delay(2000);
}

void testdrawcircle(void) {
  display.clearDisplay();

  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=2) {
    display.drawCircle(display.width()/2, display.height()/2, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfillcircle(void) {
  display.clearDisplay();

  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=3) {
    // The INVERSE color is used so circles alternate white/black
    display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn circle
    delay(1);
  }

  delay(2000);
}

void testdrawroundrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    display.drawRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfillroundrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    // The INVERSE color is used so round-rects alternate white/black
    display.fillRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_INVERSE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testdrawtriangle(void) {
  display.clearDisplay();

  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=5) {
    display.drawTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfilltriangle(void) {
  display.clearDisplay();

  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=5) {
    // The INVERSE color is used so triangles alternate white/black
    display.fillTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_INVERSE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testdrawchar(void) {
  display.clearDisplay();

  display.setTextSize(1);      // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE); // Draw white text
  display.setCursor(0, 0);     // Start at top-left corner
  display.cp437(true);         // Use full 256 char 'Code Page 437' font

  // Not all the characters will fit on the display. This is normal.
  // Library will draw what it can and the rest will be clipped.
  for(int16_t i=0; i<256; i++) {
    if(i == '\n') display.write(' ');
    else          display.write(i);
  }

  display.display();
  delay(2000);
}

void testdrawstyles(void) {
  display.clearDisplay();

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  display.println(F("Hello, world!"));

  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
  display.println(3.141592);

  display.setTextSize(2);             // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.print(F("0x")); display.println(0xDEADBEEF, HEX);

  display.display();
  delay(2000);
}

void testscrolltext(void) {
  display.clearDisplay();

  display.setTextSize(2); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 0);
  display.println(F("scroll"));
  display.display();      // Show initial text
  delay(100);

  // Scroll in various directions, pausing in-between:
  display.startscrollright(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrollleft(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrolldiagright(0x00, 0x07);
  delay(2000);
  display.startscrolldiagleft(0x00, 0x07);
  delay(2000);
  display.stopscroll();
  delay(1000);
}

void testdrawbitmap(void) {
  display.clearDisplay();

  display.drawBitmap(
    (display.width()  - LOGO_WIDTH ) / 2,
    (display.height() - LOGO_HEIGHT) / 2,
    logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
  display.display();
  delay(1000);
}

#define XPOS   0 // Indexes into the 'icons' array in function below
#define YPOS   1
#define DELTAY 2

void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
  int8_t f, icons[NUMFLAKES][3];

  // Initialize 'snowflake' positions
  for(f=0; f< NUMFLAKES; f++) {
    icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
    icons[f][YPOS]   = -LOGO_HEIGHT;
    icons[f][DELTAY] = random(1, 6);
    Serial.print(F("x: "));
    Serial.print(icons[f][XPOS], DEC);
    Serial.print(F(" y: "));
    Serial.print(icons[f][YPOS], DEC);
    Serial.print(F(" dy: "));
    Serial.println(icons[f][DELTAY], DEC);
  }

  for(;;) { // Loop forever...
    display.clearDisplay(); // Clear the display buffer

    // Draw each snowflake:
    for(f=0; f< NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
    }

    display.display(); // Show the display buffer on the screen
    delay(200);        // Pause for 1/10 second

    // Then update coordinates of each flake...
    for(f=0; f< NUMFLAKES; f++) {
      icons[f][YPOS] += icons[f][DELTAY];
      // If snowflake is off the bottom of the screen...
      if (icons[f][YPOS] >= display.height()) {
        // Reinitialize to a random position, just off the top
        icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
        icons[f][YPOS]   = -LOGO_HEIGHT;
        icons[f][DELTAY] = random(1, 6);
      }
    }
  }
}

I hope that you find this useful and inspiring.
Thank you