Assignment 4 -- Libraries

More Parts With the Help of Libraries

Schematic

Treasure Maps

The images above are my schematic for assignment 4. There is a 128 * 64 OLED display and a rotary encoder module. The OLED display have its SCL(SCK) pin connected to pin A5, and SDA pin connected to pin A4 of the Arduino. The rotary encoder has its DT pin to pin 3, and CLK pin to pin 2 of the Arduino. I'm not using the SW(switch) pin, according to the data sheet, it don't have to be connected.

Why Pin 2 & Pin 3?

According to the rotary encoder's library, when using pins that have the "interrupt" ability, the encoder will have the best performance. And from the Arduino website, only pin 2 and 3 on the UNO has the "interrupt" ability. When connecting to pin 5 and 6, the encoder's response will become unstable and laggy, making the user experience sub-optimal.

Circuit

Above the blueprint

The above image shows the actual Arduino circuit build on breadboard. The red LED and the button are not used in this assignment.

Firmware

Inside the chips
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Encoder.h>

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

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

// A Honda logo to display
// 's-l500', 48x48px
const unsigned char epd_bitmap_s_l500 [] PROGMEM = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 
    0xf0, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xe3, 0x00, 0x00, 0xe3, 0x00, 0x01, 0xc3, 
    0x00, 0x00, 0xe1, 0x80, 0x01, 0x83, 0x80, 0x00, 0xe0, 0x80, 0x01, 0x83, 0x80, 0x00, 0xe0, 0xc0, 
    0x01, 0x03, 0x80, 0x01, 0xe0, 0xc0, 0x03, 0x03, 0x80, 0x01, 0xc0, 0xc0, 0x03, 0x03, 0x80, 0x01, 
    0xc0, 0xc0, 0x03, 0x03, 0xc0, 0x01, 0xc0, 0xc0, 0x03, 0x03, 0xc0, 0x01, 0xc0, 0xc0, 0x03, 0x03, 
    0xc0, 0x03, 0xc0, 0xc0, 0x03, 0x03, 0xc0, 0x03, 0xc0, 0xc0, 0x03, 0x03, 0xe0, 0x03, 0xc0, 0xc0, 
    0x03, 0x03, 0xe0, 0x03, 0xc0, 0xc0, 0x01, 0x03, 0xe0, 0x07, 0xc0, 0xc0, 0x01, 0x03, 0xf0, 0x07, 
    0xc0, 0xc0, 0x01, 0x03, 0xf8, 0x1f, 0xc0, 0xc0, 0x01, 0x03, 0xff, 0xff, 0xc0, 0xc0, 0x01, 0x83, 
    0xff, 0xff, 0xc0, 0x80, 0x01, 0x81, 0xf8, 0x1f, 0xc0, 0x80, 0x01, 0x81, 0xf8, 0x0f, 0xc1, 0x80, 
    0x00, 0x81, 0xf0, 0x0f, 0xc1, 0x80, 0x00, 0xc1, 0xf0, 0x07, 0xc1, 0x80, 0x00, 0xc1, 0xe0, 0x07, 
    0xc3, 0x00, 0x00, 0x61, 0xe0, 0x07, 0x83, 0x00, 0x00, 0x71, 0xe0, 0x03, 0x8e, 0x00, 0x00, 0x1f, 
    0x00, 0x00, 0xfc, 0x00, 0x00, 0x03, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

long oldPos = 0;
long currPos;
// Half of the total level the knob can adjust (-20 to +20)
const int KNOB_LEVEL_HALF = 20;
int knobExceed = 0;
int currExceed = 0;
// Connect to pin 2 and pin 3, only two pins with interrupt ability to ensure max performance
Encoder myEnc(2, 3);

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

    if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
    }
    // Clear the screen
    display.clearDisplay();
    // Display Logo
    display.drawBitmap(
    (display.width()  - 48) / 2,
    (display.height() - 48) / 2,
    epd_bitmap_s_l500, 48, 48, 1
    );
    display.display();
    delay(2000);
    display.clearDisplay();
    
    delay(1000);
    display.setTextSize(2);
    display.setTextColor(WHITE);
    // Set cursor to the upper left corner
    display.setCursor(0, 0);
    display.print("Value: ");

    // Move cursor
    display.setCursor(64, 0);
    // Set text to be inverted color
    display.setTextColor(BLACK, WHITE);
    display.println(String(currPos));
    drawIndicator(currPos);
    display.display();
}

void loop() {
    // Read from the rotary encoder
    currPos = posConv(myEnc.read());
    // Enter if converted position changed or Exceeding status changed
    if (currPos != oldPos || currExceed != knobExceed) {
    oldPos = currPos;
    // Set currExceed same as knobExceed
    currExceed = knobExceed;
    Serial.println(currPos);
    
    // Clear previous content
    display.clearDisplay();
    // Set text size
    display.setTextSize(2);
    display.setTextColor(WHITE);
    // Set cursor to the upper left corner
    display.setCursor(0, 0);
    // Display content
    display.print("Value: ");

    // Move cursor
    display.setCursor(64, 0);
    // Set text to be inverted color
    display.setTextColor(BLACK, WHITE);
    display.println(String(currPos));

    drawIndicator(currPos);

    display.display();
    }

}

int posConv(long inputPos) {
    // Serial.println(inputPos);
    int result;
    // Result is position divided by four since the knob clicks every four turns
    result = (int)(round((inputPos) / 4.0));
    // Serial.println(result);
    if (result > KNOB_LEVEL_HALF) {
    // Knob value exceeds max value
    result = KNOB_LEVEL_HALF;
    // Set knob value to max to prevent further increase 
    myEnc.write(KNOB_LEVEL_HALF * 4 + 2);
    knobExceed = 1;
    }
    else if (result < -KNOB_LEVEL_HALF) {
    // Knob value exceeds min value
    result = -KNOB_LEVEL_HALF;
    // Set knob value to min to prevent further decrease 
    myEnc.write(-KNOB_LEVEL_HALF * 4 - 2);
    knobExceed = -1;
    }
    else {
    knobExceed = 0;
    }
    Serial.println(knobExceed);
    return result;
}

void drawIndicator(int inputPos) {
    int _width = 2;
    int _height = 16;
    int _x;
    int _y = 40;
    int padding = 8;

    // calculate indicator's position from input position value
    _x = map(inputPos, -KNOB_LEVEL_HALF, KNOB_LEVEL_HALF, padding, SCREEN_WIDTH - padding);
    _x = floor(_x - _width / 2);

    // Draw a straight line as the slider
    display.drawLine(padding, _y, SCREEN_WIDTH - padding, _y, WHITE);

    // Draw a rectangle as the indicator
    display.fillRoundRect(_x, floor(_y - (_height / 2)), _width, _height, 1, WHITE);

    // Draw three dashes
    if (knobExceed == 1) {
    // Knob is turning too high
    display.drawLine(SCREEN_WIDTH - padding + 2, floor(_y - _height / 4), SCREEN_WIDTH - padding + 4, floor(_y - _height / 4), WHITE);
    display.drawLine(SCREEN_WIDTH - padding + 2, _y, SCREEN_WIDTH - padding + 4, _y, WHITE);
    display.drawLine(SCREEN_WIDTH - padding + 2, floor(_y + _height / 4), SCREEN_WIDTH - padding + 4, floor(_y + _height / 4), WHITE);
    }
    else if (knobExceed == -1) {
    // Knob is turning too low
    display.drawLine(padding - 2, floor(_y - _height / 4), padding - 4, floor(_y - _height / 4), WHITE);
    display.drawLine(padding - 2, _y, padding - 4, _y, WHITE);
    display.drawLine(padding - 2, floor(_y + _height / 4), padding - 4, floor(_y + _height / 4), WHITE);
    }
}

Actual Operation

Coming alive

The above gif shows how this program run. The screen will first display a Honda logo for two seconds. Then it will display the current value of the rotary encoder, starting from 0. The user can rotate the knob clockwise to increase the value or counter-clockwise to decrease. Values won't change when reaching the -20 ~ +20 limit. Below the value is a small indicator that will also change its position based on the value. When reached the limit, if the user still rotates in that direction, small dashes appear beside the indicator to remind the users to stop.