Skip to content

Networking and Communication with OLED Display and Rotary Encoder

wiring

Embedded networking communication is the enabling of data exchange between devices, within a system or between systems, over a network. In the context of embedded systems, this often refers to communication between microcontrollers, sensors, and other devices. In order to communicate, this components/devices have to follow the same protocol. In other words, you need to speak the same language in order to be able to communicate communicate.

Key concepts:

  • Networking Protocols

    Protocols define rules and conventions for communication between network devices. Examples of networking protocols that are familiar to me are like HTTP/HTTPS and Bluetooth, for short-range wireless communication standard. But apparently, there are also TCP/IP (Transmission Control Protocol/Internet Protocol), the foundational protocols for the internet and most local networks, MQTT (Message Queuing Telemetry Transport), a lightweight protocol often used in IoT (Internet of Things) applications, etc.

  • Bus Protocols

    These protocols are used for communication between components within a device, including:

    • I2C (Inter-Integrated Circuit)
    • SPI (Serial Peripheral Interface)
    • UART (Universal Asynchronous Receiver/Transmitter) –> We’ve explored a bit about UART communication Week 6 Embedded Programming

    If you want to understand more of the differences between these 3 serial protocols, I recommend you to watch this great youtube series:

For my individual assignment, I worked with the XIAO RP2040 microcontroller and an OLED I2C display, because I will use this as my output device for my final project. I’ve actually explain the protocol on how to work with an OLED I2C on my Week 9: Output Devices assignment. But here, I will use OLED I2C 1.3” (SH1106) which uses a different driver and thus requires different library (SH110X)

I²C (Inter-Integrated Circuit)

I²C is a widely adopted serial communication protocol used in electronics to facilitate communication between integrated circuits. Serial communication bus means it sends data one bit at a time, like a line of people passing notes. Here’s some key characteristics of an I2C communication.

Basics of I2C

  • Bus Configuration:

    • It operates using only two wires: SDA (Serial Data Line) for data transfer and SCL (Serial Clock Line) for timing synchronization.
    • Pull-Up Resistors Apparently both lines are pulled up to a positive supply voltage with resistors. Devices only pull the line low, allowing multiple devices to communicate on the same bus without collision
  • Communication roles:

    • Sender device initiates communication and generates clock signals
    • Receiver device responds to the sender. Each receiver has a unique address.
  • Data Transfer: packet-switched approach

    • Data is sent in packets or frames consisting of 7-bit or 10-bit addresses followed by the data payload
    • Start and stop conditions: A sender initiates communication with a start condition and terminates it with a stop condition. The sender generates a start condition by pulling the SDA line low while SCL is high.
    • Acknowledgement or negative acknowledgement system: the sender transmits or receives data bytes sequentially, and each byte is acknowledged by the receiving device, ensuring data integrity and reliable communication
  • Multi-Senders, Multi-Receivers:

    One of the key features of I²C is its ability to support multiple senders and multiple receivers on the same bus. This means that several microcontrollers or other controlling devices can initiate communication and interact with various peripherals simultaneously. Each device connected to the bus is assigned a unique address, allowing the sender device to select which specific device it wants to communicate with during any given transaction.

  • Disadvantages:

    • Slower compared to others: while supports various speed modes, ranging from standard mode (up to 100 kbit/s) to high-speed mode (up to 3.4 Mbit/s), I2C typically operates at lower speeds compared to other protocols like SPI (Serial Peripheral Interface)
    • Distance: might be limited in distance between devices (bus length) due to signal integrity issues

Interacting Rotary Encoder to OLED I2C

Since for my final project I’m going to use Rotary Encoder as one of the input devices. So here I’m going to show you how to control OLED that uses I2C communication protocol based on the input from Rotary Encoder.

Hardware Setup

Components

  • XIAO RP2040
  • OLED I2C SSH10X 1.3” (128x64 pixels)
  • Rotary Encoder
  • Jumper wires
  • Breadboard (optional)

Specifications

🗒️ OLED I2C SSD1306 0.96 inch - Technical Specs

  • Resolution: 128x64 pixels
  • Operating Voltage: 3.3V - 5V
  • Communication Protocol: I2C
  • I2C Address: Usually 0x3C or 0x3D
  • Pin Configuration: 4 pins
    • VCC
    • GND
    • SCL (Clock Line)
    • SDA (Data Line)

For a complete information of SSD1306, you can refer to the datasheet.

🗒️ Rotary Encoder HW-040- Technical Specs

  • Working voltage: 3.3-5V
  • Mechanical angle: 360 Degrees
  • Output: 2-bit gray code
  • Positions per revolution: 30
  • Pin Configuration: 5 pins

    • CLK : Encoder Pin A (Digital Pin)
    • DT : Encoder Pin B (Digital Pin)
    • SW: switch
    • VCC(+): Voltage input(+5V)
    • GND : Ground

🗒️ XIAO RP2040 - Technical Specs

  • Operating Voltage: 3.3V
  • Communication Protocol: I2C, SPI, UART
  • I2C Pins:
    • SDA: Pin 6 / D4
    • SCL: Pin 7 / D5

Note that the OLED’s operating voltage ranges from 3.3 to 5 volts. While some says 5 volts is more ideal for OLED, the XIAO RP2040 operates at 3.3 volts. Therefore, we must connect the OLED to the 3.3V pin on the XIAO. If we still want to use the 5V pin, we might need to add a voltage divider to avoid damaging the MCU.

Circuit Connection

Based on the technical informations above, here’s how I connect the components.

wiring

  • (OLED) VCC –> 3.3V (XIAO)
  • (OLED) GND –> GND (XIAO)
  • (OLED) SCL –> D5 (XIAO)
  • (OLED) SDA –> D4 (XIAO)

  • (Rotary Encoder) CLK –> D10 (XIAO)

  • (Rotary Encoder) DT –> D9 (XIAO)
  • (Rotary Encoder) SW –> D8 (XIAO)
  • (Rotary Encoder) VCC –> 3.3V (XIAO)
  • (Rotary Encoder) GND –> GND (XIAO)

The reason I’m assigning the CLK, DT, and SW pins of Rotary Encoder to D8, D9, D10 in the XIAO is because I’m reserving all the analog pins for the sensors that I will use for my final project.

I also will be using the board that I made for my final project for this exercise. In the end, the wiring connection looks like this on my final project:

wiring

Software Setup

1. Identify I2C Address

First of all, each I2C device has a unique hexadecimal address. So, we have to know the address for our component. The typical I2C address for this OLED is usually 0x3C or 0x3D. However, in order to make sure, you can check the I2C address of your component by uploading the following code, which I found from this tutorial to the board with the device connected:

#include <Wire.h>

void setup()

{
  Serial.begin (115200);
   while (!Serial)
    {
    }

  Serial.println ();
  Serial.println ("I2C scanner. Scanning ...");
  byte count = 0;
  pinMode(13,OUTPUT); 
  digitalWrite(13,HIGH);
  Wire.begin();
  for (byte i = 1; i < 120; i++)
  {
    Wire.beginTransmission (i);
    if (Wire.endTransmission () == 0)
      {
      Serial.print ("Found address: ");
      Serial.print (i, DEC);
      Serial.print (" (0x");
      Serial.print (i, HEX);
      Serial.println (")");
      count++;
      delay (1); 
      } 
  } 
  Serial.println ("Done.");
  Serial.print ("Found ");
  Serial.print (count, DEC);
  Serial.println (" device(s).");
} 

void loop() {}

Once uploaded, the serial monitor will show the I2C address of your OLED. The result in my was that my device address is 0x3C.

2. Install Libraries

Next step is to install and include the necessary library for the OLED display. For this, we will need two additional libraries, which are Adafruit_GFX.h, that provides graphics functions for drawing shapes and text on displays, and Adafruit_SH110X.h, that specifically providing functions to control the SSD1206 display.

  • Open the Arduino IDE.
  • Go to Sketch > Include Library > Manage Libraries.
  • In the Library Manager, search for ADAFRUIT GFX and install it
  • Search for ADAFRUIT SH110X and install it

3. Testing the Display

Before writing and uploading our own code, it’s better to check if everything works as expected. We can run an example file to test the display.

  • Go to File > Examples > Adafruit SH110X > OLED_QTPY_SH1106 > Select SH1106_128x64_i2c_QTPY

    13-2

  • Set the I2C Address to the I2C address that we have identified before

    Alt text

  • Upload the code

    Once uploaded, you will see the test animation on the screen, indicating that you have successfully set up the OLED.

    From the examples code, you can also learn how the code works and what does it do / show in your OLED display.

4. Code Implementation

For the program, I’m going to display the readings of Rotary Encoder rotation in the OLED. If you want to understand in more details how to write and display some text on OLED, you can check my Week 9 documentation as I’ve already explain the programming process there. And as for the Rotary Encoder, the source code is derived from Eka’s input week documentation which explains greatly about how rotary encoder works and the code that he customizes already fixed typical debouncing issues of rotary encoders. For a more detailed steps on how to control ROtary Encoder, please visit his page.

  • Include Libraries

Open your Arduino IDE and include the 4 libraries with the order as follows

// Include Rotary Encoder library
#include "rotary_encoder.h"

// Include OLED library
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

For initializing the I2C communication:

  • SPI.h and Wire.h: Arduino libraries for SPI (Serial Peripheral Interface) and I2C communication. Wire.h is used for I2C communication, which is what the OLED display (Adafruit SSD1306) typically uses.

  • rotary_encoder.h: customized Rotary Encoder library to fix debouncing issue

For initializing OLED Display:

  • Adafruit_GFX.h: This library provides graphics functions for drawing shapes and text on displays.
  • Adafruit_SSD1306.h: This library specifically supports the SSD1306 OLED display controller, providing functions to control the display.
  • Define

    • Set pins of Rotary Encoder
    • Set screen dimension to 128x64
    • Set OLED Reset pin to not used
    • Set address to 0x3c
    • Initialize OLED Display object
// Define Rotary Encoder
#define CLK D10
#define DT D9
#define SW D8

// Define OLED
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1
#define I2C_ADDRESS 0x3C // Set the I2C address of the display 

//Initialize a rotary object
RotaryEncoder rotary(DT, CLK);
//Initialize SH1106 display object
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

int counter = 0; // declare counter for custom function of rotary encoder
  • Setup() function

Here, we will begin the display and clear it.

void setup() {
// Initialize serial communication
Serial.begin(9600);
// Initialize the OLED display with I2C address
if (!display.begin(I2C_ADDRESS, true)) {
  Serial.println(F("SH110X allocation failed"));
  for (;;) {} // Don't proceed, loop forever
}
display.clearDisplay(); // Clears the display buffer to prepare for drawing new content
delay(2000); // Pause for 2 seconds
display.display(); // this command will display all the data which is in buffer
}
  • Loop function

In the void loop, we will run a custom function for reading the rotation of the rotary encode and also for OLED to display the readings.

void loop() {
rotary.decode(&countUp, &countDown);
updateDisplay();
}
  • Custom function
// Count up function that is referenced as clockwise callback in the decoder above
void countUp() {
counter++;
updateDisplay();
}

// Count down function that is referenced as counter clockwise callback in the decoder above
void countDown() {
  counter--;
  updateDisplay();
}

// show the counter reading in display
void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.println("Counter:");
  display.setCursor(0, 20);
  display.println(counter);
  display.display();
  • Setting Text Size and Color: Using functions like setTextSize() and ‘setTextColor()` from the Adafruit SSD1306 library, you can define how large the text appears on the screen and what color it is. However, for whatever reason, set text size function is not working in this library SH1106
  • Setting Cursor Position: The setCursor(x, y) function allows you to specify where on the screen the text will start. This is useful for aligning text properly.
  • Displaying Text: Using print() or println(), you can send text to the display buffer. This text will then be rendered on the screen when the display is updated.

New things that I learn:

The OLED display uses a buffer to hold the image data before it’s displayed on the screen. The buffer is a block of memory where the display data is temporarily stored before being sent to the screen.

Clear Display: Before updating the display with new content, it’s often necessary to clear the previous content with clearDisplay() function, which resets the buffer to a blank state. This is important to ensure that old data does not overlap with new data.

Update Display: After modifying the buffer with new text or graphics, you need to send this data to the display hardware. This is done using the display() function, which refreshes the screen to show the updated content.

We need to practice efficient buffer management to ensure smooth and flicker-free updates to the display. This often means updating only the parts of the screen that change, rather than redrawing the entire screen unnecessarily.

❗ Don’t forget to write display.display() in the end, otherwide we will get a blank screen.

❗ On some tests that I did, the display was not updating correctly. Turns out it’s due to timing issues. Adding a short delay after each message transmission resolved this issue.

Complete Code

// Rotary Encoder library
#include "rotary_encoder.h"

// OLED library
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// Define Rotary Encoder
#define CLK D10
#define DT D9
#define SW D8

// Define OLED
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1
#define I2C_ADDRESS 0x3C // Set the I2C address of the display

RotaryEncoder rotary(DT, CLK);
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  // Setup display
  Serial.begin(9600);
  if (!display.begin(I2C_ADDRESS, true)) {
    Serial.println(F("SH110X allocation failed"));
    for (;;) {} // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.display(); // this command will display all the data which is in buffer
}

void loop() {
  rotary.decode(&countUp, &countDown);
  updateDisplay();
}

int counter = 0;

void countUp() {
  counter++;
  updateDisplay();
}

void countDown() {
  counter--;
  updateDisplay();
}

void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.println("Counter:");
  display.setCursor(0, 20);
  display.println(counter);
  display.display();
}

5. Upload Code | Result

Next, you can upload your code.

Other Dev (FP): Displaying Sensors Data

So based on this workflow you can further customize and update your code. This is a glimpse of how I adapt it to display sensors data in OLED I2C 0.96” (SSD1306)

Code: Displaying EC, TDS, and Temperature Measurement with OLED I2C SSD1306 0.96”

#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 64 // OLED display height, in pixels
#define OLED_RESET -1

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

int R1= 1000; // Value of resistor for EC probe
int EC_Read = A0;
int ECPower = A1;
int Temp_pin = A3;

float Temp_C; // Do not change
float Temp_F; // Do not change
float Temp1_Value = 0;
float Temp_Coef = 0.019; // You can leave as it is
/////////////////This part needs your attention during calibration only///////////////
float Calibration_PPM =380 ; //Change to PPM reading measured with a separate meter
float K= 10.18; //You must change this constant once for your probe(see video)
float PPM_Con=0.5; //You must change this only if your meter uses a different factor
/////////////////////////////////////////////////////////////////////////////////////
float CalibrationEC= (Calibration_PPM*2)/1000;
float Temperature;
float EC;
float EC_at_25;
int ppm;
float A_to_D= 0;
float Vin= 5;
float Vdrop= 0;
float R_Water;
float Value=0;
//Leave the next 2 lines in if you need help later on///////////////////////////////////
//Ask any questions that you may have in the comment section of this video
//https://youtu.be/-xKIczj9rVA

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

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x64)
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(0,15);
  display.println("Electrolyte Meter");
  display.display();
  delay(3000);
  pinMode(EC_Read,INPUT);
  pinMode(ECPower,OUTPUT);

//////////////////////////////////////////////////////////////////////////////////////////
// Calibrate (); // After calibration put two forward slashes before this line of code
//////////////////////////////////////////////////////////////////////////////////////////
}
void loop() {
  GetEC(); //Calls GetEC()
  delay(6000); //Do not make this less than 6 sec (6000)
}

void printOLED () {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);
}
////////////////////////////////////////////////////////////////////////////////////
void GetEC() {
  int val;
  double Temp;

  val = analogRead(Temp_pin);
  Temp = log(((10240000/val) - 10000));
  Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp);
  Temp_C = Temp - 273.15; // Kelvin to Celsius
  Temp_F = (Temp_C * 9.0)/ 5.0 + 32.0; // Celsius to Fahrenheit
  Temp1_Value = Temp_C;
  Temperature = Temp_C;

  digitalWrite(ECPower,HIGH);
  A_to_D= analogRead(EC_Read);
  A_to_D= analogRead(EC_Read);
  digitalWrite(ECPower,LOW);
  Vdrop= (Vin*A_to_D) / 1024.0;
  R_Water = (Vdrop*R1) / (Vin-Vdrop);
  EC = 1000/ (R_Water*K);
  EC_at_25 = EC / (1+ Temp_Coef*(Temperature-25.0));
  ppm=(EC_at_25)*(PPM_Con*1000);

  printOLED ();
  display.setCursor(0,15);
  display.println("EC: ");
  display.print(EC_at_25);
  display.print("mS/cm");
  display.display();
  delay(3000);

  printOLED ();
  display.setCursor(0,15);
  display.println("TDS: ");
  display.print(ppm);
  display.print(" ppm");
  display.display();
  delay(3000);

  printOLED ();
  display.setCursor(0,15);
  display.println("Temp: ");
  display.print(Temperature);
  display.print("C");
  display.display();
  delay(3000);

  // Serial.print(" EC: ");
  // Serial.print(EC_at_25);
  // Serial.print(" milliSiemens(mS/cm) ");
  // Serial.print(ppm);
  // Serial.print(" ppm ");
  // Serial.print(Temperature);
  // Serial.println(" *C ");
}
////////////////////////////////////////////////////////////////////////////////////
void Calibrate () {
  Serial.println("Calibration routine started");

  float Temperature_end=0;
  float Temperature_begin=0;
  int val;
  double Temp;

  val=analogRead(Temp_pin);
  Temp = log(((10240000/val) - 10000));
  Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp);
  Temp_C = Temp - 273.15; // Kelvin to Celsius
  Temp_F = (Temp_C * 9.0)/ 5.0 + 32.0; // Celsius to Fahrenheit
  Temp1_Value = Temp_C;
  Temperature_begin = Temp_C;
  Value = 0;
  int i=1;
  while(i<=10){
    digitalWrite(ECPower,HIGH);
    A_to_D= analogRead(EC_Read);
    A_to_D= analogRead(EC_Read);
    digitalWrite(ECPower,LOW);
    Value=Value+A_to_D;
    i++;
    delay(6000);
  };
  A_to_D = (Value/10);
  val = analogRead(Temp_pin);
  Temp = log(((10240000/val) - 10000));
  Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp);
  Temp_C = Temp - 273.15; // Kelvin to Celsius
  Temp_F = (Temp_C * 9.0)/ 5.0 + 32.0; // Celsius to Fahrenheit
  Temp1_Value = Temp_C;
  Temperature_end=Temp_C;
  EC =CalibrationEC*(1+(Temp_Coef*(Temperature_end-25.0)));
  Vdrop= (((Vin)*(A_to_D))/1024.0);
  R_Water=(Vdrop*R1)/(Vin-Vdrop);

  float K_cal= 1000/(R_Water*EC);

  Serial.print("Replace K value with K = ");
  Serial.println(K_cal);
  Serial.print("Temperature difference start to end were = ");
  Temp_C=Temperature_end-Temperature_begin;
  Serial.print(Temp_C);
  Serial.println("*C");
  Serial.println("Temperature difference start to end must be smaller than 0.15*C");
  Serial.println("");
  Calibrate ();
}
////////////////////////////////////////////////////////////////////////////////////

Learning & Reflection

  • I2C addressing issue: importance of correct addressing

    Understanding the correct addressing I2C communication is crucial. The I2C protocol relies on the sender and receiver devices communicating through specific addresses. If the address is incorrect, the communication fails.

  • Display not updating: timing in I2C communication

    On some tests that I did, the display was not updating correctly. Turns out it’s due to timing issues. Adding a short delay after each message transmission resolved this issue.

    Timing is vital to ensure that data is transmitted and received correctly. Delays and clock synchronization issues can lead to data corruption or loss.

Design Files