Skip to content

Controlling Output Devices

Output devices are hardware components or modules that receive signals or data from a microcontroller (MCU) and convert these into a form perceivable by the user. Common output devices include LEDs, displays, motors, buzzers, and speakers. These devices enable interaction with the environment and provide feedback or perform tasks based on microcontroller commands.

The challenge of this week is learning how to control an output device. In general, this is the workflow of controlling the operation of an output device:

  1. Learn the technical specifications & requirements of the output device and the MCU you’re using.
  2. Hardware setup: connect or interface your output device to the microcontroller
  3. Program your output device

In this assignment, I’m going to try controlling OLED, but also exploring other output devices, like Neopixels and Servo by interfacing them with the custom development board that I’ve designed and produced on Week 8: Electronics Design as well as the Quentorres board that I’ve produced on Week 4: Electronics Production.

Please find my group assignment here: Week 9: Measuring Power Consumption of Output Devices

Controlling OLED Display

OLED (Organic Light-Emitting Diode) is a display hardware that uses thin films of organic molecules to emit light when an electric current is applied. It offers several advantages over traditional LCD displays, such as higher contrast ratios, better color reproduction, faster response times, and the ability to be more power-efficient in certain use cases.

  • monochrome display
  • thin, light and flexible
  • brighter than LEDs
  • Low current and power consumption
  • No backlighting required
  • can bemade very large
  • Wide field of view

For more explanations of how OLED works you can look at this youtube video.

OLED I2C SSD1306 0.96”

Technical Specifications

First of all, we need to understand the technical specifications and requirements of the components that we want to use. Understanding the technical specs of our devices is important because, as discussed in this week’s group assignment, knowing the hardware requirements—such as power needs and constraints—will determine how we interface and program the components and whether they are compatible with each other. We can do this by browsing through the datasheet of that component.

🗒️ 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.

Since we’re going to interface it with the XIAO RP2040, it is essential to understand its technical requirements. I have detailed the technical specs of the XIAO RP2040 in Week 6: Embedded Programming as well as more about the RP2040 itself on our group page, but here are some relevant details for this exercise:

🗒️ XIAO RP2040 - Technical Specs

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

Here, as you can see, 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 need to add a voltage divider to avoid damaging the MCU.

Circuit Connection

Based on the technical informations above, here’s how we should connect the OLED to the MCU

wiring diagram

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

In the board I designed and made in Week 8, actually, I have already included a connection for an OLED I2C 0.96-inch display. This means I can simply plug the OLED into my custom devboard.

DEVBOARDPINOUT devboard oled

However, the connector uses a vertical pin socket, which I designed to be later secured with a screw and bracket. Since I hadn’t added these yet at the time of the exercise, I decided to wire the OLED manually, to prevent it from overhanging and potentially damaging the pin socket. My concern was right, as I accidentally broke a solder joint on the OLED I2C pin socket while traveling with my devboard. Luckily, I provided multiple pin socket options that can be used.

wiring

Programming OLED I2C SSD1306 0.96”

To get started programming the OLED, you can follow the following step by step process:

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. Because this type of OLED is pretty common, the typical I2C address 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_SSD1306.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 SSD1306 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 SSD1306 > Select ssd1306_128x64_i2c or ssd1306_128x32_i2c (depending on your device spec)

    9-8

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

    change

  • 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: Send Hello World Message

Now, we will try to write and display some text on the OLED. There are so many tutorials for displaying text on OLED. For the exercise, I’m just going to send basic “Hello, World” message to the display. The following code is built upon this tutorial.

  • Include Libraries

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

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
  • 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.
  • 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 screen dimension to 128x32
    • Set OLED Reset pin to not used
    • Set address to 0x3c
    • Initialize OLED Display object
// Define screen dimensions
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Define OLED reset pin 
#define OLED_RESET -1 // if not used, set to -1
#define SCREEN_ADDRESS 0x3C 

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Sreen Address vs Screen Dimensions…🤔

Regarding SCREEN_ADDRESS, the I2C address for an OLED display is typically 0x3D for 128x64 and 0x3C for 128x32. This address can vary based on the configuration of your display module.

When I ran the I2C Address Scanner/Finder mentioned previously, it indicated that my OLED address is 0x3C, meaning the resolution of my OLED is 128x32. Therefore, in the code, I used the 128x32 screen dimensions. Later, I discovered that I could also use the 128x64 screen resolution, but changing the address to 0x3D caused the code to fail to upload. Therefore, I can use the 128x64 screen resolution with the 0x3C address, resulting in smaller text size yet higher, more defined resolution. So I guess, you can customize your screen resolution, but the I2C address depends on your specific device.

  • 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 0x3C
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); // SSD1306_SWITCHCAPVCC is a constant indicating the power supply method (internally generated from 3.3V)
 -->

// Initialize the OLED display with I2C address 0x3C
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
  Serial.println(F("SSD1306 allocation failed"));
  for (;;); // Loop forever if initialization fails
}

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 write our main code, which includes the message we want to display. To do this, we need to specify the text size, text color, cursor position, and finally write the message using the println command.

void loop() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(0,0);
display.println("Hello World!");
display.display();
delay(3000);
}
  • 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.
  • 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: Basic Hello World!

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

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

// Define OLED reset pin 
#define OLED_RESET -1 // if not used, set to -1
#define SCREEN_ADDRESS 0x3C 

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup()   {  

// Initialize serial communication
Serial.begin(9600);

  <!-- // Initialize the OLED display with I2C address 0x3C
  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); // SSD1306_SWITCHCAPVCC is a constant indicating the power supply method (internally generated from 3.3V)
   -->

// Initialize the OLED display with I2C address 0x3C
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
  Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Loop forever if initialization fails
}

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
}

void loop() { 
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.println("Hello World!");
  display.display();
  delay(3000);
}

5. Upload Code | Result

Next, you can upload your code.

Set Text Size: 2

hello world 1

SetText Size: 1

hello world 2

Further 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 further to display sensors data for my final prject development:

Main adjustment I did in the code below is I changed the screen resolution from 128x32 to 128x64 while keeping the address same.

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

// Tafia Sabila Khairunnisa, Fab Lab Bali, Skylab Workshop, Fab Academy 2024
// Original code based on Pierre Hertzog's (https://www.youtube.com/watch?v=-xKIczj9rVA)
// Code has been adapted to be displayed in 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 ();
}
////////////////////////////////////////////////////////////////////////////////////

I also explore other output devices, but I will not explain extensively since I will prioritize finishing my other documentation first.

Controlling Neopixels

#include <Adafruit_NeoPixel.h>

#define neopixelPin D2   // input pin Neopixel is attached to
#define pixelsNum 8 // number of neopixels in strip

Adafruit_NeoPixel tafExercise = Adafruit_NeoPixel(pixelsNum, neopixelPin, NEO_GRB + NEO_KHZ800);

int delayval = 100; // timing delay in milliseconds

int redColor = 0;
int greenColor = 0;
int blueColor = 0;

void setup() {
  tafExercise.begin(); // Initialize the NeoPixel library.
}

void loop() {
  setColor();

  for (int i=0; i < pixelsNum; i++) {

    tafExercise.setPixelColor(i, tafExercise.Color(redColor, greenColor, blueColor)); // tafExercise.Color takes RGB values, from 0,0,0 up to 255,255,255
    tafExercise.show(); // This sends the updated pixel color to the hardware.
    delay(delayval); // Delay for a period of time (in milliseconds).
  }
}

// setColor()
// picks random values to set for RGB
void setColor(){
  redColor = random(0, 255);
  greenColor = random(0,255);
  blueColor = random(0, 255);
}

Controlling Servo

9-6

#include <Servo.h>

Servo tafServo;

int val;

void setup() // 
{
  tafServo.attach(D3);
}

void loop()
{
  val = analogRead(A2); 
  val = map(val,0,1023,0,180); // map 10 bit value to 8bit value, pwm value requires value from 0-255
  tafServo.write(val);
  delay(15);
}