Skip to content

Programming Interface and Application

Okay, so if last week we already learn how to interacting an input device (Rotary Encoder) to our OLED I2C screen. This week, we’re going to learn how to design a case that interfaces a user with these two.

Since I will be using a Rotary Encoder and an OLED I2C Screen for my final project, I will utilize this week to make an interface application for the user to select menu options for measurement in OLED on my devices.

Design Inspiration

So, I want to make 4 menu options to display my sensors data, and I want the rotary encoder to be able to navigate / switching menu options and if clicked, the page will change to display the value of the sensors selected.

At first, I want to follow this tutorial with amazing UI design for the OLED. But I think for the first spiral, I will go with the simple one first. For that, I refer to this menu selection with OLED tutorial and alsomenu selection with Nokia lCD.

Hardware Setup & Initialization

Components Used

  • Rotary Encoder HW-040
  • OLED I2C SH1106 1.3” (White)
  • SeeedStucio XIAO RP2040 (on my custom devboard for FP)
  • Jumper cables

Circuit Connection

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.

The wiring connection looks like this on my final project board

wiring

Initialization

If you have never connected/ setup communication between OLED and MCU before, you might want to first setup the communication protocol between your OLED and MCU by folllowing the step bt step process mentioned in Week 9: Output Devices and Week 13: Embedded Networking and Communication .

Programming Process: Menu Selection (2x2 Grid)

To program menu selection options that’s controlled by Rotary Encoder, I referred to these tutorials that uses OLED and Nokia LCD referred by Elaine. Elaine also showed me her example code that is adapted to her project need. For this assignment, I adapted the programs specifically for an OLED I2C SH1106 and incorporated them with a Rotary Encoder, utilizing Eka’s debouncing program that I also tried to implemented in W13 embededded networking and communication assignment. At this stage, I’m not using any additional design software (yet), instead I’m focusing on controlling the interface directly through code adjustments.

  • Libraries and Definitions

    • Include necessary libraries for SPI, I2C communication (Wire), and for driving the OLED display (Adafruit_GFX and Adafruit_SH110X)
    • Define pins for the rotary encoder (DT, CLK) and the selection button (SW).
    • Define OLED paramaters
    #include "rotary_encoder.h"
    #include <SPI.h>
    #include <Wire.h>
    #include <Adafruit_GFX.h>
    #include <Adafruit_SH110X.h>
    
    // Define pins for rotary encoder
    #define DT D9
    #define CLK D10
    #define SW D8
    
    // Define OLED display parameters
    #define i2c_Address 0x3C // Initialize with the I2C addr 0x3C
    #define SCREEN_WIDTH 128 // OLED display width, in pixels
    #define SCREEN_HEIGHT 64 // OLED display height, in pixels
    #define OLED_RESET -1    // QT-PY / XIAO
    
  • Objects Initialization

    • Initialize a rotary encoder object with pins DT (D9) and CLK (D10).
    • Initialize an OLED display object with specified screen dimensions and reset pin configuration.
    // Initialize the OLED display object
    Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
    
  • Constants and Variables

    // Menu items
        const char* menuItems[] = {"EC", "TDS", "pH", "Cal"};
    const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);
    
    // Menu navigation variables
    int menuIndex = 0;
    bool buttonPressed = false;
    

    Menu Items:

    • Define menu options EC, TDS, pH, Cal into menuItems array
    • Calculate number of items in menuItems through menuLength

    Menu Navigation Variables:

    • menuIndex keeps track of the currently selected menu item index
    • buttonPressed flags when the selection button (SW) is pressed.
  • Setup Function

    • Initializes serial communication for debugging purposes.
    • Initializes the OLED display, clears it after 2 seconds, and sets up the button pin (SW) as input with a pull-up resistor.
    • Calls updateDisplay() to initially display the menu on the OLED.
    void setup() {
    // Initialize serial communication
    Serial.begin(9600);
    
    // Initialize the OLED display
    display.begin(i2c_Address, true); // Address 0x3C default
    display.display();
    delay(2000);
    display.clearDisplay();
    
    // Initialize button pin
    pinMode(SW, INPUT_PULLUP);
    
    // Display initial menu
    updateDisplay();
      }
    
  • Main Loop function

    void loop() {
    // Decode rotary encoder input
    rotary.decode(&countUp, &countDown);
    
    // Check if the button was pressed
    checkButtonPress();
    
    // If the button was pressed, handle the selection
    if (buttonPressed) {
        buttonPressed = false;
        selectMenuItem();
    }
        }
    
  • Custom Functions: ROtary Encoder Input Handling

    Callback function: countUp()

    To increment menuIndex to move to the next menu item, wrapping around to the first item after the last.

    // Callback function for clockwise rotation
    void countUp() {
    menuIndex++;
    if (menuIndex >= menuLength) {
        menuIndex = 0;
    }
    updateDisplay(); //refresh the display with the updated selection
    }
    

    Callback function: countDown()

    To decrements menuIndex to move to the previous menu item, wrapping around to the last item after the first.

    // Callback function for counter-clockwise rotation
        void countDown() {
    menuIndex--;
    if (menuIndex < 0) {
        menuIndex = menuLength - 1;
    }
    updateDisplay();//refresh the display with the updated selection
        }
    

    checkButtonPress() function

    • checkButtonPress() to monitor changes in the button state and sets buttonPressed flag accordingly when the button is pressed.
    // Function to check button press
    void checkButtonPress() {
    static bool lastButtonState = HIGH;
    bool currentButtonState = digitalRead(SW);
    if (lastButtonState == HIGH && currentButtonState == LOW) {
        buttonPressed = true;
    }
    lastButtonState = currentButtonState;
        }
    
  • Custom Functions: OLED Output Management

    UpdateDisplay() function

    The role of updateDisplay() function here is to manage the OLED display to present menu items in a grid layout. It uses geometric calculations to position and draw rectangles around each menu item, highlighting the selected item for clear user feedback. This method ensures that the user interface remains organized and visually appealing, enhancing user interaction with the device.

    Overview of the code steps:

    • Clear the display and updates it with the current menu items arranged in a 2x2 grid layout.
    • Each menu item is displayed with a “>” symbol as navigation next to the selected item.
    • Then, draws rectangles around each menu item and prints them on the OLED display.
    // Function to update the OLED display with the current menu item
    void updateDisplay() {
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SH110X_WHITE);
    
    // Setting box dimensions for menu item
    int boxWidth = SCREEN_WIDTH / 2 - 4;  // Width of each box
    int boxHeight = SCREEN_HEIGHT / 2 - 4; // Height of each box
    
    for (int i = 0; i < menuLength; i++) {
        int x = (i % 2) * (SCREEN_WIDTH / 2); // Calculate x position based on column
        int y = (i / 2) * (SCREEN_HEIGHT / 2); // Calculate y position based on row
    
        // Draw rectangle around the menu item
        display.drawRect(x + 2, y + 2, boxWidth, boxHeight, SH110X_WHITE);
    
        display.setCursor(x + 6, y + 8);
        if (i == menuIndex) {
        display.print(">");
        } else {
        display.print(" ");
        }
    
        display.print(menuItems[i]);
    }
    display.display();
    }
    

    How do I make the design?

    Basically, I planned since the beginning that I want to make the menu side by side in 2x2 grid-like arrangement. To do this, basically I’m using the OLED screen’s width and height as a reference as to how wide / tall the boxes that I want to make and also as the reference point for the cursor coordination. You can also customize these items below:

    • Box Dimensions for Menu Items: Calculate the dimensions for each menu item box based on the OLED screen width (SCREEN_WIDTH) and height (SCREEN_WIDTH). Each box is half the width and height of the screen minus a margin (-4), ensuring proper spacing between items.
    • Drawing Menu Items:

      • Loop through Menu Items: iterate through each item in the menuItems array (menuLength is the total number of items).
      • Calculate Position (x, y): Calculates the position (x, y) for each menu item based on its index (i);
      • x is determined by (i % 2) * (SCREEN_WIDTH / 2), which places items in two columns across the width of the screen.
      • y is determined by (i / 2) * (SCREEN_HEIGHT / 2), organizing items into rows within half the height of the screen.

      • Draw Rectangle: Uses display.drawRect() to draw a rectangle around each menu item at position (x + 2, y + 2) with dimensions boxWidth by boxHeight, using SH110X_WHITE as the color.

      • Highlight Selected Item: Checks if i equals menuIndex (the index of the currently selected item). If true, prints a “>” symbol before the menu item text to visually highlight it.

      • Display Menu Item Text: Prints the text of each menu item (menuItems[i]) starting at position (x + 6, y + 8) on the OLED display.

    selectMenuItem() function

    Function to handle menu item selection. Works by clearing the display, displays the selected menu item, waits for 2 seconds using delay(2000), and then returns to displaying the menu using updateDisplay().

    // Function to handle menu item selection
    void selectMenuItem() {
        display.clearDisplay();
        display.setTextSize(2);
        display.setTextColor(SH110X_WHITE);
        display.setCursor(0, 0);
        display.print("Selected:");
        display.setCursor(0, 20);
        display.setTextSize(2);
        display.print(menuItems[menuIndex]);
        display.display();
        delay(2000); // Display the selected menu item for 2 seconds
    

Complete code: Menu Selection Interface (2x2 Grid)

#include "rotary_encoder.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// Define pins for rotary encoder
#define DT D9
#define CLK D10
#define SW D8

// Initialize the rotary encoder object
RotaryEncoder rotary(DT, CLK);

// Define OLED display parameters
#define i2c_Address 0x3C // Initialize with the I2C addr 0x3C
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1    // QT-PY / XIAO

// Initialize the OLED display object
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Menu items
const char* menuItems[] = {"EC", "TDS", "pH", "Cal"};
const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);

// Menu navigation variables
int menuIndex = 0;
bool buttonPressed = false;

// Function prototypes for rotary encoder callbacks
void countUp();
void countDown();
void checkButtonPress();

void setup() {
  // Initialize serial communication
  Serial.begin(9600);

  // Initialize the OLED display
  display.begin(i2c_Address, true); // Address 0x3C default
  display.display();
  delay(2000);
  display.clearDisplay();

  // Initialize button pin
  pinMode(SW, INPUT_PULLUP);

  // Display initial menu
  updateDisplay();
}

void loop() {
  // Decode rotary encoder input
  rotary.decode(&countUp, &countDown);

  // Check if the button was pressed
  checkButtonPress();

  // If the button was pressed, handle the selection
  if (buttonPressed) {
    buttonPressed = false;
    selectMenuItem();
  }
}

// Callback function for clockwise rotation
void countUp() {
  menuIndex++;
  if (menuIndex >= menuLength) {
    menuIndex = 0;
  }
  updateDisplay();
}

// Callback function for counter-clockwise rotation
void countDown() {
  menuIndex--;
  if (menuIndex < 0) {
    menuIndex = menuLength - 1;
  }
  updateDisplay();
}

// Function to check button press
void checkButtonPress() {
  static bool lastButtonState = HIGH;
  bool currentButtonState = digitalRead(SW);
  if (lastButtonState == HIGH && currentButtonState == LOW) {
    buttonPressed = true;
  }
  lastButtonState = currentButtonState;
}

// Function to update the OLED display with the current menu item
void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);

  int boxWidth = SCREEN_WIDTH / 2 - 4;  // Width of each box
  int boxHeight = 26;                   // Height of each box

  for (int i = 0; i < menuLength; i++) {
    int x = (i % 2) * (SCREEN_WIDTH / 2); // 0 or SCREEN_WIDTH / 2
    int y = (i / 2) * (SCREEN_HEIGHT / 2); // 0 or SCREEN_HEIGHT / 2

    // Draw rectangle around the menu item
    display.drawRect(x + 2, y + 2, boxWidth, boxHeight, SH110X_WHITE);

    display.setCursor(x + 6, y + 8);
    if (i == menuIndex) {
      display.print(">");
    } else {
      display.print(" ");
    }

    display.print(menuItems[i]);
  }
  display.display();
}

// Function to handle menu item selection
void selectMenuItem() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.print("Selected:");
  display.setCursor(0, 20);
  display.setTextSize(2);
  display.print(menuItems[menuIndex]);
  display.display();
  delay(2000); // Display the selected menu item for 2 seconds

  // Return to the menu
  updateDisplay();
}

Result | Uploaded Code

Next step is to just upload the code to our board.

Other Tests and Trials & Errors

I played with different combinations and variations before setlling into the final design. I had to play and adjust the sizing, settings, orders, etc to get here.. Here you can witness the evolvement of my interface designing programming process

Initial Menu Selection v1

#include "rotary_encoder.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// Define pins for rotary encoder
#define DT D9
#define CLK D10
#define SW D8

// Initialize the rotary encoder object
RotaryEncoder rotary(DT, CLK);

// Define OLED display parameters
#define i2c_Address 0x3C // Initialize with the I2C addr 0x3C
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1    // QT-PY / XIAO

// Initialize the OLED display object
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Menu items
const char* menuItems[] = {"Option 1", "Option 2", "Option 3", "Option 4"};
const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);

// Menu navigation variables
int menuIndex = 0;
bool buttonPressed = false;

// Function prototypes for rotary encoder callbacks
void countUp();
void countDown();
void checkButtonPress();

void setup() {
// Initialize serial communication
Serial.begin(9600);

// Initialize the OLED display
display.begin(i2c_Address, true); // Address 0x3C default
display.display();
delay(2000);
display.clearDisplay();

// Initialize button pin
pinMode(SW, INPUT_PULLUP);

// Display initial menu
updateDisplay();
}

void loop() {
// Decode rotary encoder input
rotary.decode(&countUp, &countDown);

// Check if the button was pressed
checkButtonPress();

// If the button was pressed, handle the selection
if (buttonPressed) {
    buttonPressed = false;
    selectMenuItem();
    }
}

// Callback function for clockwise rotation
void countUp() {
menuIndex++;
if (menuIndex >= menuLength) {
    menuIndex = 0;
    }
updateDisplay();
}

// Callback function for counter-clockwise rotation
void countDown() {
menuIndex--;
if (menuIndex < 0) {
    menuIndex = menuLength - 1;
    }
updateDisplay();
}

// Function to check button press
void checkButtonPress() {
static bool lastButtonState = HIGH;
bool currentButtonState = digitalRead(SW);
if (lastButtonState == HIGH && currentButtonState == LOW) {
    buttonPressed = true;
    }
lastButtonState = currentButtonState;
}

// Function to update the OLED display with the current menu item
void updateDisplay() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Menu:");
display.setTextSize(1);
for (int i = 0; i < menuLength; i++) {
    display.setCursor(0, 16 + i * 10);
    if (i == menuIndex) {
    display.print("> ");
    } else {
    display.print("  ");
    }
    display.print(menuItems[i]);
    }
display.display();
}

// Function to handle menu item selection
void selectMenuItem() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Selected:");
display.setCursor(0, 20);
display.setTextSize(2);
display.print(menuItems[menuIndex]);
display.display();
delay(2000); // Display the selected menu item for 2 seconds

// Return to the menu
updateDisplay();
}