Is replacing an LCD with an OLED display possible?

Hi ! Newbie here, please be gentle - If I were to say that I am a complete novice, it would be an overstatement!

I am working on a project which was originally written for a 2004 LCD, unfortunately, this display is too big for my purposes and as I have a couple of non branded 128 * 64 IC2 SSD1306 displays at hand, I am trying to convert the sketch to use an OLED.

Hardware: Nano V3 clone, 4 x 4 matrix keyboard , rotary encoder and non branded 128*64 SSD1306 IC2 display. All hardware & wiring is correct and functional.

I found out through trial and error that my display is not 100% compatible with the Adafruit library, but that u8g2 works well.

I have the display configured using:

U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

I am trying to work through the sketch 1 line at a time, having commented out all references to lcd so that the sketch will compile and run and so that I can test the results.
I have used "// To be modified for u8g2 library //" to denote what still needs to be done

So far I can only get the display to print simple text and a variables, but I am stumped now and don't seem able to proceed.

Would anyone be able to help please? Don't forget I am a complete novice

uint8_t zerodot[8] = {0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0};
uint8_t onedot[8] = {0x0, 0xe, 0x1f, 0x1b, 0x1f, 0xe, 0x0};
lcd.createChar(0, zerodot);
lcd.createChar(1, onedot);

I can see that these lines generates a graphic image on the lcd, the first a simple dot, the second similar to a donut. They are used to denote if a function option has been selected or not.

and:

lcd.setCursor(4, ActiveAddress);
  lcd.blink();

This positions a cursor and blinks it. I have tried (to the best of my understanding) to follow a couple of examples found online, but I am stumped now.

Full code below - Thanks for looking

/*
  Modified Oled throttle building on the work of Mark Fox & Dave Bodnar
  Customised from Dave Bodnar's original code from 16th June 2016, his version 2.6a 
  and Mark Fox's edit & streamlining version 1.03 dated 6th August 2020.
  This version attempts to utilise a non-branded 128 * 64 IC2 Oled display which does not work with Adafruit library
  Attempting to use U8g2 library using page buffer due to memory constraints
  Using an Arduino Nano V3 clone with a 4x4 keypad, non-branded 128 * 64 Ic2 Oled display,
  Digital debouncing on the KY-040 rotary encoder without an interrupt, Copyright John Main - best-microcontroller-projects.com
  This version (2.01) is an attempt to replace previous 2004 LCD display which utilised <LiquidCrystal_I2C.h>
  Date March 2022.
*/

#include "Arduino.h"
#include <EEPROM.h>
#include <Wire.h>
#include <Keypad.h>

#include <U8g2lib.h> 

// Display Type - Unbranded OLED 128x64 (does not display correctly using Adafruit library)
// Trying to use page buffer to reduce RAM useage

U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); 

// Encoder Pin definitions
#define RE_CLK 2 // Check rotation, pins 2 & 3 may need to be swapped if cw turn is not +ve
#define RE_DATA 3 // See above
#define RE_Button 4 // Use external 10K pullup resistor

int debug = 0; // set to 1 to show debug info on serial port - assume that it will cause issues with DCC++ depending on what is sent

uint8_t zerodot[8] = {0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0}; // Graphic for when function IS NOT selected
uint8_t onedot[8] = {0x0, 0xe, 0x1f, 0x1b, 0x1f, 0xe, 0x0}; // Graphic when function IS selected

// Keypad variables - 4 x 4 Matrix (8 pins, pin 1 to 4 = C1 to C4, pin 5 to 8 = R1 to R4
const byte ROWS = 4; // four rows
const byte COLS = 4; // four columns
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = {5, 6, 7, 8 };
byte colPins[COLS] = {9, 10, 11, 12}; 

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

static uint8_t prevNextCode = 0; // statics for rotary encoder
static uint16_t store = 0;

int buttonState = 0;
int re_absolute = 0;
char key ;

// Array set for 4 Locos maximum.  Needs code tidyup to make truly flexible

int maxLocos = 4;// number of loco addresses
int LocoAddress[4] = {1111, 2222, 3333, 4444};
int LocoDirection[4] = {1, 1, 1, 1};
int LocoSpeed[4] = {0, 0, 0, 0};

// Neater way of storing Loco Function Bytes.
// Rows are Locos, Columns are the five sets of function groups
// 128 = prefix for functions 0 to 4 (last five bits are functions 04321.)
// 176 = prefix for functions 5 to 8
// 160 = prefix for functions 9 to 12
// 0 & 0 are full bitpatterns for functions 13 to 20 and 21 to 28.

byte LocoFNs[4][5] = {
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
};

int ActiveAddress = 0; // make address1 active

int i = 0;
char VersionNum[] = "2.01"; // Set version

void setup() {

// Setup Encoder 
  pinMode(RE_CLK, INPUT);
    pinMode(RE_CLK, INPUT_PULLUP);
      pinMode(RE_DATA, INPUT);
        pinMode(RE_DATA, INPUT_PULLUP);

// Setup Serial Monitor
  Serial.begin(115200);
    Serial.println("Entering Setup"); //  Trouble shooting txt remove when operational
  
// Start OLED display
  u8g2.begin();

  u8g2.setFont(u8g2_font_7x13_tf);
    u8g2.firstPage();
      do {
    u8g2.setCursor(0, 20);
      u8g2.print(("Welcome"));
        } while ( u8g2.nextPage() );
      delay(2000); 

   Serial.println("Oled print Welcome "); //  Trouble shooting txt remove when operational

// To be modified for u8g2 library //   lcd.createChar(0, zerodot);
// To be modified for u8g2 library //   lcd.createChar(1, onedot);

  getAddresses();  // read loco IDs from eeprom
 
  u8g2.firstPage();  
    do {
     u8g2.setCursor(0, 20);
    u8g2.print(("DCC++ THROTTLE"));
    u8g2.setCursor(0, 40);
    u8g2.print(("March 2022 v ")); 
   for (int i = 0; i < 4; i++) {
    u8g2.print(VersionNum[i]);
  }                     
  } while ( u8g2.nextPage() );
  delay(1000);  
          
 Serial.println(" ");
  Serial.print("March 2022 Version ");
    for (int i = 0; i < 4; i++) {
      Serial.print(VersionNum[i]);
  }
  
  Serial.println("");
  
  if (debug == 1) Serial.println("");
  Serial.print("<0>");// power off to DCC++ unit
  delay(1500);
    Serial.println("  Power off command"); //  Trouble shooting txt remove when operational
      delay(1500); //  Trouble shooting txt remove when operational
      
  u8g2.clearDisplay();
  
   InitialiseSpeedsOLED();
   InitialiseFunctionOLED();
   
// To be modified for u8g2 library //   lcd.setCursor(4, ActiveAddress);      
// To be modified for u8g2 library //   lcd.blink();
            
  Serial.println("End of setup"); //  Trouble shooting txt remove when operational

}  // END SETUP

void loop() {
  static int8_t re_val;

// First check the keypad
  key = keypad.getKey();

  if (key) {
    if (debug == 1) {
      Serial.println(" ");
        Serial.println(key);
    }
    switch (key) {
      case '*':
        all2ZeroSpeed();
          InitialiseSpeedsOLED();
            getLocoAddress();
              updateSpeedsOLED();
                key = 0;
        break;
        
      case '#':
        ActiveAddress++;
          if (ActiveAddress >= maxLocos) ActiveAddress = 0;
            updateSpeedsOLED();
              InitialiseFunctionOLED();
        delay(200);
          key = 0;
            re_absolute = LocoSpeed[ActiveAddress];
              doDCCspeed();
        break;
        
      case 'D':
        doExtendedFunction();
          updateSpeedsOLED();
        break;
      
      default:
// It's 0 - 9 or A - C so perform a loco function
        key = key - 48;
        if (key > 10) key = key - 7;
        doFunction(key);
        break;
    }
  }

// Read encoder
  if ( re_val = read_rotary() ) {
    re_absolute += re_val;
      re_absolute = constrain(re_absolute, 0, 126);
        LocoSpeed[ActiveAddress] = re_absolute;
          doDCCspeed();
            updateSpeedsOLED();
  }

  buttonState = digitalRead(RE_Button);
    if (buttonState == LOW) {
      delay(50);
        buttonState = digitalRead(RE_Button); // check a 2nd time to be sure
    if (buttonState == LOW) {// check a 2nd time to be sure
      
// Reverse direction...
      LocoDirection[ActiveAddress] = !LocoDirection[ActiveAddress];
      
// ... and set speed to zero (saves loco running away on slow decel/accel set in decoder.)
      LocoSpeed[ActiveAddress] = 0;
        re_absolute = 0;
          doDCCspeed();
      
  updateSpeedsOLED();

      do {  // routine to stay here till button released & not toggle direction
        buttonState = digitalRead(RE_Button);
      }      while (buttonState == LOW);
    }
  }
}  //END LOOP

//START DO FUNCTION BUTTONS

void doExtendedFunction() {
  
// To be modified for u8g2 library //  lcd.setCursor(9 , 0);
 
  int counter = 0;
  int total = 0;
  do {
    key = keypad.getKey();
    if (key) {
      counter++;
// Abort if # or *
      if (key < 48) return;
      // otherwise...
      int number =  key - 48;
// if it 3-9 or A-D, and this is the first key...
      if (number > 2 && counter == 1) {
        if (debug == 1) Serial.print("First Time, 3 to D");
          return;
      }
// else we can assume it's 0,1,2...
      else if ( counter == 1 ) {
        
// To be modified for u8g2 library //       lcd.setCursor(9 , number + 1);
  
        total = number * 10;
      }
      else if (counter == 2 && number < 10) {
        
// Second time around... and 0-9
        
// To be modified for u8g2 library //        lcd.setCursor(number + 10 , total / 10 + 1);
        
        total = total + number;
      }
      else if (counter == 2 && number > 9) {
        if (debug == 1) Serial.print("Second Time, A-D");
          return;
      }
    }
  }   while (counter <= 1); //  collect exactly 2 digits
        if (debug == 1) Serial.print(total);

  doFunction(total);
}

void doFunction(int FunctoFlip) {
  // Will be passed a number from 0 to 28.
  int FuncSet;
  int FuncLoc;
  if (debug == 1) {
    Serial.print("doFunction - passed:");
    Serial.println(FunctoFlip);
  }
  switch (FunctoFlip)
  {
    case (0) ... (4):
      if (debug == 1) Serial.print("0-4");
      FuncSet = 0;
      FuncLoc = FunctoFlip - 1;
      if (FuncLoc == -1) FuncLoc = 4;
      break;
    case (5) ... (8):
      if (debug == 1) Serial.print("5-8");
      FuncSet = 1;
      FuncLoc = FunctoFlip - 5;
      break;
    case (9) ... (12):
      if (debug == 1) Serial.print("9-12");
      FuncSet = 2;
      FuncLoc = FunctoFlip - 9;
      break;
    case (13) ... (20):
      if (debug == 1) Serial.print("13-20");
      FuncSet = 3;
      FuncLoc = FunctoFlip - 13;
      break;
    case (21) ... (28):
      if (debug == 1) Serial.print("21-28");
      FuncSet = 4;
      FuncLoc = FunctoFlip - 21;
      break;
    case (29):
      if (debug == 1) Serial.print("29");
      // Maybe set all the LocoFNs to zero?
      // Like this!  :)
      LocoFNs[ActiveAddress][0] = 128;
      LocoFNs[ActiveAddress][1] = 176;
      LocoFNs[ActiveAddress][2] = 160;
      LocoFNs[ActiveAddress][3] = 0;
      LocoFNs[ActiveAddress][4] = 0;
      for (int ddf = 0; ddf < 4; ddf++) {
        doDCCfunction(ddf);
      }
      InitialiseFunctionOLED();

      return;
      break;
  }
  // What we now have is the number of which bit we'd like to change in that specific function's bitpattern
  // The next command is effectively 2^number, thus giving us a bitpattern of the number...
  
  FuncLoc = 1 << FuncLoc ;
  
  // ... which we can then simply XOR onto the existing bitpattern to flip the bit.
  
  LocoFNs[ActiveAddress][FuncSet] ^= FuncLoc;
  doDCCfunction(FuncSet);
  
 InitialiseFunctionOLED();
  
  if (debug == 1) {
    Serial.println("**");
      Serial.print(LocoFNs[ActiveAddress][FuncSet], BIN);
        Serial.print(" - ");
          Serial.println(LocoFNs[ActiveAddress][FuncSet], DEC);
            Serial.println(ActiveAddress);
              Serial.println(FuncSet);
                Serial.println("**");
  }
}

void getLocoAddress() {
  int saveAddress = LocoAddress[ActiveAddress];
  Serial.print("<0>");// power off to tracks
  int total = 0;
  int counter = 0;
  do {
// To be modified for u8g2 library //     lcd.setCursor( counter , ActiveAddress);
    key = keypad.getKey();
    if (key == '#' || key == '*' || key == 'A' || key == 'B' || key == 'C' || key == 'D' ) { //abort when either is hit
      //LocoAddress[ActiveAddress] = saveAddress;
      total = saveAddress;
      break;// exits the do...while loop if above buttons pressed - ABORT new address
    }
    if (key) {
      counter++;
      int number =  key - 48;
      total = total * 10 + number;
      if (key == 48 && total == 0) {
 // To be modified for u8g2 library //        lcd.print(" ");
      } else {
 // To be modified for u8g2 library //        lcd.print(key);
      }
      if (debug == 1) Serial.print("Counter = ");
      if (debug == 1) Serial.print(counter);
      if (debug == 1) Serial.print("  key = ");
      if (debug == 1) Serial.print(key);
      if (debug == 1) Serial.print("   val = ");
      if (debug == 1) Serial.println(number);
    }
  } while (counter <= 3); //  collect exactly 4 digits
  
  // If all zeroes entered, return to original address
  
  if (total == 0) total = saveAddress;
  
  LocoAddress[ActiveAddress] = total;
    if (debug == 1) Serial.print("Actually saving: ");
      if (debug == 1) Serial.println(total);
  saveAddresses();
    Serial.println("<1>");// power back on to tracks
  
 updateSpeedsOLED();
}


void doDCCspeed() {
  if (debug == 1) Serial.println(LocoDirection[ActiveAddress] );
  Serial.print("<1>");
  Serial.print("<t1 ");
  Serial.print(LocoAddress[ActiveAddress] );//locoID);
  Serial.print(" ");
  Serial.print(LocoSpeed[ActiveAddress] );
  Serial.print(" ");
  Serial.print(LocoDirection[ActiveAddress] );
  Serial.println(">");
}

void doDCCfunction(int FuncSetX) {
  Serial.write("<f ");
  Serial.print(LocoAddress[ActiveAddress] );
  Serial.print(" ");
  switch (FuncSetX) {
    case (0) ... (2):
      // First three function sets are plain single byte commands...
      break;
    case (3):
      // Last two 8-bit sets are prefixed with 222/223.
      Serial.print("222 ");
      break;
    case (4):
      Serial.print("223 ");
      break;
  }
  int fx = LocoFNs[ActiveAddress][FuncSetX];
  Serial.print(fx);
  Serial.print(" >");
}

void all2ZeroSpeed() {
  /* Loads of bugs here.
      A) tempx <= maxLocos meant five commands were sent, the fifth to a random(?) loco.
      B) LocoSpeed and Direction were set to those of loco 1.  Not good practice, although not required to be correct as <0> is sent after.
      As of 4thAugust2020, modified to do what it says it does.
  */
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Set the recorded speeds to zero...
    LocoSpeed[tempx] = 0;
    // ... then transmit the commands too.
    Serial.print("<t1 ");
    Serial.print(LocoAddress[tempx] );//locoID);
    Serial.print(" 0 ");
    Serial.print(LocoDirection[tempx] );
    Serial.write(">");
  }
}

void getAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    LocoAddress[xyz] = EEPROM.read(xyz * 2) * 256;
    LocoAddress[xyz] = LocoAddress[xyz] + EEPROM.read(xyz * 2 + 1);
    if (LocoAddress[xyz] >= 10000) LocoAddress[xyz] = 3;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz + 1);
    }
  }
  if (debug == 1) Serial.println(" ");
  maxLocos = EEPROM.read(20);
  if (debug == 1) Serial.print("EEPROM maxLocos = ");
  if (debug == 1) Serial.println(maxLocos);
  if (maxLocos >= 4) maxLocos = 4;
}

void saveAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    xxx = LocoAddress[xyz] / 256;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz);
      Serial.print(" msb ");
      Serial.print(xxx);
      Serial.print(" writing to ");
      Serial.print(xyz * 2);
      Serial.print(" and ");
      Serial.print(xyz * 2 + 1);
    } // Endif
    EEPROM.write(xyz * 2, xxx);
    xxx = LocoAddress[xyz] - (xxx * 256);
    if (debug == 1) {
      Serial.print(" lsb ");
      Serial.print(xxx);
    }
    EEPROM.write(xyz * 2 + 1, xxx);
  }
  EEPROM.write(20, maxLocos);
}

void InitialiseSpeedsOLED() {
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Prints LocoID(right justified), direction arrow, and speed(right justified)
    
 // To be modified for u8g2 library //    lcd.setCursor(0, tempx);
 
    String temp = "   " + String(LocoAddress[tempx] , DEC);
    int tlen = temp.length() - 4;
    
 // To be modified for u8g2 library //    lcd.print(temp.substring(tlen));
 
    // ... direction...
    if (LocoDirection[tempx] == 1 ) {
      
 // To be modified for u8g2 library //      lcd.print(">");
 
    }
    else {
      
// To be modified for u8g2 library //       lcd.print("<");

    }
    // ... speed ...
    temp = "  " + String(LocoSpeed[tempx] , DEC);
    tlen = temp.length() - 3;
    
 // To be modified for u8g2 library //    lcd.print(temp.substring(tlen));
 
  }
  // Return cursor to direction arrow for loco under control
  
// To be modified for u8g2 library //   lcd.setCursor(4, ActiveAddress);

}

void InitialiseFunctionOLED() {
  int funcount = 0;
  
// To be modified for u8g2 library //   lcd.setCursor(9, 0);

// To be modified for u8g2 library // lcd.print("F0123456789");

  for (int tempy = 0; tempy < 3; tempy++) {
    
// To be modified for u8g2 library //     lcd.setCursor(9, tempy + 1);

// To be modified for u8g2 library //     lcd.print(tempy);

    for (int tempx = 0; tempx < 10; tempx++) {
      
// To be modified for u8g2 library //       lcd.setCursor(tempx + 10, tempy + 1);
      funcount = tempy * 10 + tempx;
      // Funkiness to put function 0 on the fourth bit...
      if (funcount == 0) {
 // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], 4)));
      }
      // ...  and F1 through F4 on the zeroth to third bits.
      else if (funcount >= 1 && funcount <= 4) {
 // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], funcount - 1)));
      }
      else if (funcount >= 5 && funcount <= 8) {
 // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][1], funcount - 5)));
      }
      else if (funcount >= 9 && funcount <= 12) {
  // To be modified for u8g2 library //       lcd.write(byte(bitRead(LocoFNs[ActiveAddress][2], funcount - 9)));
      }
      else if (funcount >= 13 && funcount <= 20) {
        
 // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][3], funcount - 13)));
     
      }
      else if (funcount >= 21 && funcount <= 28) {
        
 // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][4], funcount - 21)));
     
      }
      else {
        // 29th location, hint that 29 will turn off all loco functions.
        
 // To be modified for u8g2 library //        lcd.print("X");
     
      }

    }
    
// To be modified for u8g2 library //     lcd.setCursor(4, ActiveAddress);

  }
}

void updateSpeedsOLED() {
  int tempx = ActiveAddress;
  
// To be modified for u8g2 library //   lcd.setCursor(0, tempx);

  String temp = "   " + String(LocoAddress[tempx] , DEC);
  int tlen = temp.length() - 4;
  
// To be modified for u8g2 library //   lcd.print(temp.substring(tlen));

  if (LocoDirection[tempx] == 1 ) {
    
// To be modified for u8g2 library //     lcd.print(">");

  }
  else {
    
 // To be modified for u8g2 library //    lcd.print("<");
 
  }
  temp = "  " + String(LocoSpeed[tempx] , DEC);
  tlen = temp.length() - 3;
  
// To be modified for u8g2 library //   lcd.print(temp.substring(tlen));

// To be modified for u8g2 library //   lcd.setCursor(4, ActiveAddress);

}

// Robust Rotary encoder reading - external pullup resistors not used
// 0.1uF cap across D2 & Gnd and D3 & Gnd
// Copyright John Main - best-microcontroller-projects.com
// A vald CW or  CCW move returns 1, invalid returns 0.

int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};

  prevNextCode <<= 2;
    if (digitalRead(RE_DATA)) prevNextCode |= 0x02;
      if (digitalRead(RE_CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

  // If valid then store as 16 bit data.
    if  (rot_enc_table[prevNextCode] ) {
      store <<= 4;
        store |= prevNextCode;
    if ((store & 0xff) == 0x2b) return -1;
     if ((store & 0xff) == 0x17) return 1;
  }
    return 0;
}

You might find it easier to use the U8X8 library, which comes bundled with the U8G2 library, uses fewer resources (ram, flash memory etc) because it can only draw text, not graphics, so is similar to your old 2004 character LCD.

There are a large number of fonts available with the U8G2/U8X8 library, including some symbol fonts. I'm sure there will be some suitable replacements for your use-defined symbols there:

Thank you Paul, I will take a look at that library as well, thank you also for suggesting the fonts, much appreciated

I have modified a much simpler project (voltmeter & ammeter to use the same display, but that was just a case of printing text and variables in a set position, this is far more complex and may be beyond me

Hi Paul,

Firstly thanks for your comments, unfortunately, I don't think the u8x8 library is going to help, I have got much of the code to run, but due to available font sizes I run out of screen real estate.

Therefore I will have to persevere with the u8g2 library

Sorry I wasn't able to help you. Good luck with your project.

Please don't apologise Paul, your assistance was very much appreciated

@backintime, I've read your post a few times now, and I can see (I think) what you've done so far and also some of your reflections on the code, but what I don't see is what exactly goes wrong or what specific problem(s) you're running into. Could you go into a bit more detail on this? Perhaps start with the first/most immediate display problem and let's see if we can get a crowbar into that one.

Well, it didn't feel like that, it felt like my suggestion was dismissed out of hand. To be honest, it was getting late and I was tired, hence my reaction.

Your old LCD was 20x4 characters. Your new oled is 128x64 pixels. So your font can't be more than 6 pixels wide but can be up to 16 pixels high. That's a pretty tall, narrow font! But I did find up to 6x13:

But you are right to say that U8X8 is going to be difficult, because whichever font you choose, you won't get more than 16 characters across the screen, but you could have 8 lines. Could you redesign the screen layout?

Sorry Paul, sometimes the written word comes across different to what was meant, I am truly grateful for your help and if my comment suggested otherwise, I am really very sorry.

To clarify last night's comment:

As you said, the LCD is 20 characters wide and 4 rows deep, the code I am trying to butcher (adapt) requires those 20 characters, the most I could squeeze in with the smallest u8x8 font I could find was 16 characters.

I did however, manage to get the required number of characters using the u8g2 library and TBH, the u8g2 font seemed to be easier to read

TBH, this is well beyond me and although I am starting to learn some basics, most of the code in this sketch is beyond my comprehension.

But, as you said, the u8x8 library was far easier to understand at my level

Going back to your original questions:

Actually, these lines don't print anything to the lcd. They are creating two custom characters ready for use later in the code. You will probably see either lcd.print(char(0));, lcd.write(0); or similar when they are printed to the screen.

A blinking cursor is a hardware/firmware feature of LCD character displays. They were designed a long time ago when microcontrollers had very limited capabilities, so it was a valuable feature back then. Newer display technology like graphic LCD and oled probably don't have anything like it, because they are expected to be used with microcontrollers will much higher capabilities, which could draw/blink a cursor without hardware support. Even the most basic Arduino is capable of that, but you will have to write some extra code to achieve it.

1 Like

Thank you Paul, I will try to do some more research this evening (chores permitting) my initial thoughts for the cursor would be to invert the background / text colours within a looped function that does not interrupt the rest of the sketch (easier for me to say than do I suspect).

Hi Koraks,

Thank you for your message, as you can see PaulRB has given me some pointers, do you mind if I come back to you when I get stuck?

BTW, crowbars and sledgehammers are something that is more familiar to me than coding

No problem; I'm reading this and if I think I have something to add I'll do so.

They say a picture paints a thousand words! well 2 pictures must be better! This is what I am trying to achieve. The LCD shows a functioning controller whilst the OLED is just a mock-up. The font chosen for the OLED is no way set in stone, I just wanted to make sure it was legible and all characters could be accommodated.


That looks pretty good. I would just find a sensible alternative for both the circle/dot icons; the way you've done it now can work indeed. And for the blinking you could also find an alternative that does not necessarily have to be animated; you could replace that one '>' with another character that denotes it's 'active'/selected. The main thing is that you understand it, right?

You can create a custom graphics symbol for the OLED display, or even modify the font to replace one of the preexisting characters with your own. The U8g2 github page has all the original font files and the programs needed to re-encode custom fonts into the format used by u8g2.

Here is the full character set for the font you are using (from the U8g2 manual pages)
u8g2_font_7x13_tf

Looks like you made some progress yesterday! Maybe time to re-post your latest code? But your indentation seems a bit wacky, so please click Tools->Auto Format in the IDE first.

Which font are you using now?

This font has some characters you could use to replace your dot0 and dot1:

Hi Paul, thank you for your continued support. TBH I haven't made much progress, other than establishing a font that will allow me to print 20 characters wide:

u8g2_font_6x10_tf

I also replaced any LCD library references with u8g2 where I could see a direct correlation, but I realise that those will not function as is, as they need to be within a function.

The plus side is that my tinkering does not appear to have interfered with the serial output of the sketch.

Also, the ' > ' symbol, which is where the cursor usually sits, is a direction indicator which will change to ' < ' when the switch on the rotary encoder is pushed, so I need to retain this character.

/*
  Modified Oled throttle building on the work of Mark Fox & Dave Bodnar
  Customised from Dave Bodnar's original code from 16th June 2016, his version 2.6a
  and Mark Fox's edit & streamlining version 1.03 dated 6th August 2020.
  This version attempts to utilise a non-branded 128 * 64 IC2 Oled display which does not work with Adafruit library
  Attempting to use U8g2 library using page buffer due to memory constraints
  Using an Arduino Nano V3 clone with a 4x4 keypad, non-branded 128 * 64 Ic2 Oled display,
  Digital debouncing on the KY-040 rotary encoder without an interrupt, Copyright John Main - best-microcontroller-projects.com
  This version (2.01) is an attempt to replace previous 2004 LCD display which utilised <LiquidCrystal_I2C.h>
  Date March 2022.
*/

#include <Arduino.h>
#include <EEPROM.h>
#include <Wire.h>
#include <Keypad.h>

#include <U8g2lib.h>

// Display Type - Unbranded OLED 128x64 (does not display correctly using Adafruit library)
// Trying to use page buffer to reduce RAM useage

U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

// Encoder Pin definitions
#define RE_CLK 2 // Check rotation, pins 2 & 3 may need to be swapped if cw turn is not +ve
#define RE_DATA 3 // See above
#define RE_Button 4 // Use external 10K pullup resistor

int debug = 0; // set to 1 to show debug info on serial port - assume that it will cause issues with DCC++ depending on what is sent

/* Will not need to used custom character, can use characters wihin font for now ill use
  // - to indicate not selected and * to indicate selected
*/
// uint8_t zerodot[8] = {0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0}; // Graphic for when function IS NOT selected
// uint8_t onedot[8] = {0x0, 0xe, 0x1f, 0x1b, 0x1f, 0xe, 0x0}; // Graphic when function IS selected

// Keypad variables - 4 x 4 Matrix (8 pins, pin 1 to 4 = C1 to C4, pin 5 to 8 = R1 to R4
const byte ROWS = 4; // four rows
const byte COLS = 4; // four columns
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = {5, 6, 7, 8 };
byte colPins[COLS] = {9, 10, 11, 12};

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

static uint8_t prevNextCode = 0; // statics for rotary encoder
static uint16_t store = 0;

int buttonState = 0;
int re_absolute = 0;
char key ;

int maxLocos = 4;// number of loco addresses
int LocoAddress[4] = {1111, 2222, 3333, 4444};
int LocoDirection[4] = {1, 1, 1, 1};
int LocoSpeed[4] = {0, 0, 0, 0};

byte LocoFNs[4][5] = {
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
};

int ActiveAddress = 0; // make address1 active

int i = 0;
char VersionNum[] = "2.01"; // Set version

void setup() {

  pinMode(RE_CLK, INPUT);
  pinMode(RE_CLK, INPUT_PULLUP);
  pinMode(RE_DATA, INPUT);
  pinMode(RE_DATA, INPUT_PULLUP);

  Serial.begin(115200);

  u8g2.begin();

  /* used to test font suitability for screen, will not be in operational programme
     suitable font:  u8g2_font_6x10_tf
  */
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.firstPage();
  do {
    u8g2.setCursor(0, 15);
    u8g2.print("0003> 126 F0123456789"); // Test text to prove text will fit on display
    u8g2.setCursor(0, 30); // Test text to prove text will fit on display
    u8g2.print("0011> 126 0-*-*-*-*-*"); // Test text to prove text will fit on display
    u8g2.setCursor(0, 45); // Test text to prove text will fit on display
    u8g2.print("0066> 126 1*-*-*-*-*-"); // Test text to prove text will fit on display
    u8g2.setCursor(0, 60); // Test text to prove text will fit on display
    u8g2.print("0088> 126 2--***---**"); // Test text to prove text will fit on display
  } while ( u8g2.nextPage() );
  delay(10000);
  u8g2.clearDisplay();
  /* above to be removed
  */

  // Welcome screen
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.firstPage();
  do {
    u8g2.setCursor(40, 35);
    u8g2.print(("Welcome"));
  } while ( u8g2.nextPage() );
  delay(2000);

  Serial.println("Oled print Welcome ");


  /* Do not need to create these 2 custom characters, can use symbols within font library
  */
  // lcd.createChar(0, zerodot);
  // lcd.createChar(1, onedot);

  getAddresses();  // read loco IDs from eeprom

  u8g2.firstPage();
  do {
    u8g2.setCursor(0, 20);
    u8g2.print(("DCC++ THROTTLE"));
    u8g2.setCursor(0, 40);
    u8g2.print(("March 2022 v "));
    for (int i = 0; i < 4; i++) {
      u8g2.print(VersionNum[i]);
    }
  } while ( u8g2.nextPage() );
  delay(1000);

  Serial.println(" ");
  Serial.print("March 2022 Version ");
  for (int i = 0; i < 4; i++) {
    Serial.print(VersionNum[i]);
  }

  Serial.println("");

  if (debug == 1) Serial.println("");
  Serial.print("<0>");
  delay(1500);

  u8g2.clearDisplay();

  InitialiseSpeedsOLED();
  InitialiseFunctionOLED();

  u8g2.setCursor(4, ActiveAddress);

  // Need to establish some means to indicate where the cursor is on screen
  // To be modified for u8g2 library //   lcd.blink();

}  // END SETUP

void loop() {
  static int8_t re_val;

  key = keypad.getKey();

  if (key) {
    if (debug == 1) {
      Serial.println(" ");
      Serial.println(key);
    }
    switch (key) {
      case '*':
        all2ZeroSpeed();
        InitialiseSpeedsOLED();
        getLocoAddress();
        updateSpeedsOLED();
        key = 0;
        break;

      case '#':
        ActiveAddress++;
        if (ActiveAddress >= maxLocos) ActiveAddress = 0;
        updateSpeedsOLED();
        InitialiseFunctionOLED();
        delay(200);
        key = 0;
        re_absolute = LocoSpeed[ActiveAddress];
        doDCCspeed();
        break;

      case 'D':
        doExtendedFunction();
        updateSpeedsOLED();
        break;

      default:
        // It's 0 - 9 or A - C so perform a loco function
        key = key - 48;
        if (key > 10) key = key - 7;
        doFunction(key);
        break;
    }
  }

  if ( re_val = read_rotary() ) {
    re_absolute += re_val;
    re_absolute = constrain(re_absolute, 0, 126);
    LocoSpeed[ActiveAddress] = re_absolute;
    doDCCspeed();
    updateSpeedsOLED();
  }

  buttonState = digitalRead(RE_Button);
  if (buttonState == LOW) {
    delay(50);
    buttonState = digitalRead(RE_Button); // check a 2nd time to be sure
    if (buttonState == LOW) {// check a 2nd time to be sure

      LocoDirection[ActiveAddress] = !LocoDirection[ActiveAddress];


      LocoSpeed[ActiveAddress] = 0;
      re_absolute = 0;
      doDCCspeed();

      updateSpeedsOLED();

      do {
        buttonState = digitalRead(RE_Button);
      }      while (buttonState == LOW);
    }
  }
}  //END LOOP

//START DO FUNCTION BUTTONS
void doExtendedFunction() {

  u8g2.setCursor(9 , 0); // To be modified for u8g2 library //

  int counter = 0;
  int total = 0;
  do {
    key = keypad.getKey();
    if (key) {
      counter++;
      // Abort if # or *
      if (key < 48) return;
      // otherwise...
      int number =  key - 48;
      // if it 3-9 or A-D, and this is the first key...
      if (number > 2 && counter == 1) {
        if (debug == 1) Serial.print("First Time, 3 to D");
        return;
      }
      // else we can assume it's 0,1,2...
      else if ( counter == 1 ) {

        u8g2.setCursor(9 , number + 1); // To be modified for u8g2 library //

        total = number * 10;
      }
      else if (counter == 2 && number < 10) {

        // Second time around... and 0-9

        u8g2.setCursor(number + 10 , total / 10 + 1); // To be modified for u8g2 library //

        total = total + number;
      }
      else if (counter == 2 && number > 9) {
        if (debug == 1) Serial.print("Second Time, A-D");
        return;
      }
    }
  }   while (counter <= 1); //  collect exactly 2 digits
  if (debug == 1) Serial.print(total);

  doFunction(total);

}

void doFunction(int FunctoFlip) {
  // Will be passed a number from 0 to 28.
  int FuncSet;
  int FuncLoc;
  if (debug == 1) {
    Serial.print("doFunction - passed:");
    Serial.println(FunctoFlip);
  }
  switch (FunctoFlip)
  {
    case (0) ... (4):
      if (debug == 1) Serial.print("0-4");
      FuncSet = 0;
      FuncLoc = FunctoFlip - 1;
      if (FuncLoc == -1) FuncLoc = 4;
      break;
    case (5) ... (8):
      if (debug == 1) Serial.print("5-8");
      FuncSet = 1;
      FuncLoc = FunctoFlip - 5;
      break;
    case (9) ... (12):
      if (debug == 1) Serial.print("9-12");
      FuncSet = 2;
      FuncLoc = FunctoFlip - 9;
      break;
    case (13) ... (20):
      if (debug == 1) Serial.print("13-20");
      FuncSet = 3;
      FuncLoc = FunctoFlip - 13;
      break;
    case (21) ... (28):
      if (debug == 1) Serial.print("21-28");
      FuncSet = 4;
      FuncLoc = FunctoFlip - 21;
      break;
    case (29):
      if (debug == 1) Serial.print("29");
      // Maybe set all the LocoFNs to zero?
      // Like this!  :)
      LocoFNs[ActiveAddress][0] = 128;
      LocoFNs[ActiveAddress][1] = 176;
      LocoFNs[ActiveAddress][2] = 160;
      LocoFNs[ActiveAddress][3] = 0;
      LocoFNs[ActiveAddress][4] = 0;
      for (int ddf = 0; ddf < 4; ddf++) {
        doDCCfunction(ddf);
      }
      InitialiseFunctionOLED();

      return;
      break;
  }

  FuncLoc = 1 << FuncLoc ;

  // ... which we can then simply XOR onto the existing bitpattern to flip the bit.
  // This may need to be incorporated within u8g2??

  LocoFNs[ActiveAddress][FuncSet] ^= FuncLoc;
  doDCCfunction(FuncSet);

  InitialiseFunctionOLED();

  if (debug == 1) {
    Serial.println("**");
    Serial.print(LocoFNs[ActiveAddress][FuncSet], BIN);
    Serial.print(" - ");
    Serial.println(LocoFNs[ActiveAddress][FuncSet], DEC);
    Serial.println(ActiveAddress);
    Serial.println(FuncSet);
    Serial.println("**");
  }
}

void getLocoAddress() {
  int saveAddress = LocoAddress[ActiveAddress];
  Serial.print("<0>");// power off to tracks
  int total = 0;
  int counter = 0;
  do {
    u8g2.setCursor( counter , ActiveAddress); // To be modified for u8g2 library //

    key = keypad.getKey();
    if (key == '#' || key == '*' || key == 'A' || key == 'B' || key == 'C' || key == 'D' ) { //abort when either is hit
      //LocoAddress[ActiveAddress] = saveAddress;
      total = saveAddress;
      break;// exits the do...while loop if above buttons pressed - ABORT new address
    }
    if (key) {
      counter++;
      int number =  key - 48;
      total = total * 10 + number;
      if (key == 48 && total == 0) {
        u8g2.print(" "); // To be modified for u8g2 library //
      } else {
        u8g2.print(key);  // To be modified for u8g2 library //
      }
      if (debug == 1) Serial.print("Counter = ");
      if (debug == 1) Serial.print(counter);
      if (debug == 1) Serial.print("  key = ");
      if (debug == 1) Serial.print(key);
      if (debug == 1) Serial.print("   val = ");
      if (debug == 1) Serial.println(number);
    }
  } while (counter <= 3); //  collect exactly 4 digits

  // If all zeroes entered, return to original address

  if (total == 0) total = saveAddress;

  LocoAddress[ActiveAddress] = total;
  if (debug == 1) Serial.print("Actually saving: ");
  if (debug == 1) Serial.println(total);
  saveAddresses();
  Serial.println("<1>");// power back on to tracks

  updateSpeedsOLED();
}


void doDCCspeed() {
  if (debug == 1) Serial.println(LocoDirection[ActiveAddress] );
  Serial.print("<1>");
  Serial.print("<t1 ");
  Serial.print(LocoAddress[ActiveAddress] );//locoID);
  Serial.print(" ");
  Serial.print(LocoSpeed[ActiveAddress] );
  Serial.print(" ");
  Serial.print(LocoDirection[ActiveAddress] );
  Serial.println(">");
}

void doDCCfunction(int FuncSetX) {
  Serial.write("<f ");
  Serial.print(LocoAddress[ActiveAddress] );
  Serial.print(" ");
  switch (FuncSetX) {
    case (0) ... (2):
      // First three function sets are plain single byte commands...
      break;
    case (3):
      // Last two 8-bit sets are prefixed with 222/223.
      Serial.print("222 ");
      break;
    case (4):
      Serial.print("223 ");
      break;
  }
  int fx = LocoFNs[ActiveAddress][FuncSetX];
  Serial.print(fx);
  Serial.print(" >");
}

void all2ZeroSpeed() {
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Set the recorded speeds to zero...
    LocoSpeed[tempx] = 0;
    // ... then transmit the commands too.
    Serial.print("<t1 ");
    Serial.print(LocoAddress[tempx] );//locoID);
    Serial.print(" 0 ");
    Serial.print(LocoDirection[tempx] );
    Serial.write(">");
  }
}

void getAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    LocoAddress[xyz] = EEPROM.read(xyz * 2) * 256;
    LocoAddress[xyz] = LocoAddress[xyz] + EEPROM.read(xyz * 2 + 1);
    if (LocoAddress[xyz] >= 10000) LocoAddress[xyz] = 3;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz + 1);
    }
  }
  if (debug == 1) Serial.println(" ");
  maxLocos = EEPROM.read(20);
  if (debug == 1) Serial.print("EEPROM maxLocos = ");
  if (debug == 1) Serial.println(maxLocos);
  if (maxLocos >= 4) maxLocos = 4;
}

void saveAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    xxx = LocoAddress[xyz] / 256;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz);
      Serial.print(" msb ");
      Serial.print(xxx);
      Serial.print(" writing to ");
      Serial.print(xyz * 2);
      Serial.print(" and ");
      Serial.print(xyz * 2 + 1);
    } // Endif
    EEPROM.write(xyz * 2, xxx);
    xxx = LocoAddress[xyz] - (xxx * 256);
    if (debug == 1) {
      Serial.print(" lsb ");
      Serial.print(xxx);
    }
    EEPROM.write(xyz * 2 + 1, xxx);
  }
  EEPROM.write(20, maxLocos);
}

void InitialiseSpeedsOLED() {
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Prints LocoID(right justified), direction arrow, and speed(right justified)

    u8g2.setCursor(0, tempx);  // To be modified for u8g2 library //

    String temp = "   " + String(LocoAddress[tempx] , DEC);
    int tlen = temp.length() - 4;

    u8g2.print(temp.substring(tlen));  // To be modified for u8g2 library //

    // ... direction...
    if (LocoDirection[tempx] == 1 ) {

      u8g2.print(">"); // To be modified for u8g2 library //

    }
    else {

      u8g2.print("<"); // To be modified for u8g2 library //

    }
    // ... speed ...
    temp = "  " + String(LocoSpeed[tempx] , DEC);
    tlen = temp.length() - 3;

    u8g2.print(temp.substring(tlen));  // To be modified for u8g2 library //

  }
  // Return cursor to direction arrow for loco under control

  u8g2.setCursor(4, ActiveAddress); // To be modified for u8g2 library //

}

void InitialiseFunctionOLED() {
  int funcount = 0;

  u8g2.setCursor(9, 0); // To be modified for u8g2 library //

  u8g2.print("F0123456789"); // To be modified for u8g2 library //

  for (int tempy = 0; tempy < 3; tempy++) {

    u8g2.setCursor(9, tempy + 1); // To be modified for u8g2 library //

    u8g2.print(tempy); // To be modified for u8g2 library //

    for (int tempx = 0; tempx < 10; tempx++) {

      u8g2.setCursor(tempx + 10, tempy + 1); // To be modified for u8g2 library //

      funcount = tempy * 10 + tempx;
      // Funkiness to put function 0 on the fourth bit...
      if (funcount == 0) {
        // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], 4)));
      }
      // ...  and F1 through F4 on the zeroth to third bits.
      else if (funcount >= 1 && funcount <= 4) {
        // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], funcount - 1)));
      }
      else if (funcount >= 5 && funcount <= 8) {
        // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][1], funcount - 5)));
      }
      else if (funcount >= 9 && funcount <= 12) {
        // To be modified for u8g2 library //       lcd.write(byte(bitRead(LocoFNs[ActiveAddress][2], funcount - 9)));
      }
      else if (funcount >= 13 && funcount <= 20) {

        // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][3], funcount - 13)));

      }
      else if (funcount >= 21 && funcount <= 28) {

        // To be modified for u8g2 library //        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][4], funcount - 21)));

      }
      else {
        // 29th location, hint that 29 will turn off all loco functions.

        u8g2.print("X");  // To be modified for u8g2 library //

      }

    }

    u8g2.setCursor(4, ActiveAddress); // To be modified for u8g2 library //

  }
}

void updateSpeedsOLED() {
  int tempx = ActiveAddress;

  u8g2.setCursor(0, tempx); // To be modified for u8g2 library //

  String temp = "   " + String(LocoAddress[tempx] , DEC);
  int tlen = temp.length() - 4;

  u8g2.print(temp.substring(tlen)); // To be modified for u8g2 library //

  if (LocoDirection[tempx] == 1 ) {

    u8g2.print(">"); // To be modified for u8g2 library //

  }
  else {

    u8g2.print("<");  // To be modified for u8g2 library //

  }
  temp = "  " + String(LocoSpeed[tempx] , DEC);
  tlen = temp.length() - 3;

  u8g2.print(temp.substring(tlen)); // To be modified for u8g2 library //

  u8g2.setCursor(4, ActiveAddress); // To be modified for u8g2 library //

}

// Robust Rotary encoder reading - external pullup resistors not used
// 0.1uF cap across D2 & Gnd and D3 & Gnd
// Copyright John Main - best-microcontroller-projects.com
// A vald CW or  CCW move returns 1, invalid returns 0.

int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};

  prevNextCode <<= 2;
  if (digitalRead(RE_DATA)) prevNextCode |= 0x02;
  if (digitalRead(RE_CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

  // If valid then store as 16 bit data.
  if  (rot_enc_table[prevNextCode] ) {
    store <<= 4;
    store |= prevNextCode;
    if ((store & 0xff) == 0x2b) return -1;
    if ((store & 0xff) == 0x17) return 1;
  }
  return 0;
}

If your program works 100% for a 20x4 LCD display, just buy a smaller COG (Chip on Glass) display.

If you seriously want to use a 0.96 inch OLED it is VERY small.

Seriously, you can port any "Character LCD " program to a "Graphics LCD" program fairly easily.
All Graphics libraries can move a cursor and print text.
All Graphics libraries can "invent" some custom characters.

If you want help, post the original working 20x4 program.
Then readers can show how to adapt for a 128x64 or even a 128x32 graphics display.

Don't worry about memory. You just put text into PROGMEM memory. Which means you have plenty enough SRAM for a full 128x64 buffer. e.g. lcd.print(F("anonymous string"));

Oh, U8g2 is excellent for fancy fonts etc. And paged buffers.
But you will probably find Adafruit_SSD1306 a lot easier to use.

David.