Is replacing an LCD with an OLED display possible?

Hi David, thanks for your message.

I have opted for the OLED 0.96 display principally, because I have a few of them lying around and cost is a major consideration. I also need to use an IC2 display as I have used all digital pins for the keyboard and rotary encoder.

The Adafruit library does not appear to work correctly with this display, whereas the u8g2 does.

As requested, please find herewith the original 2004 LCD sketch

/*
  Mark Fox's DCC++ 'Bodnar Throttle'. 
  Rewritten and customised from Dave Bodnar's code from June 16th, 2016, his version 2.6a
  Version 1.00 uses an Arduino MEGA2560 (devboard) with a 4x4 keypad, 20x4 I2C LCD Display, and uses digital debouncing on the KY-040 rotary encoder without an interrupt.
  This version is 1.03, a heavy edit streamlining all the function comms.
  Date 6th August 2020.
*/

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

#define RE_CLK 2 // Encoder on digital pins 2 & 3
#define RE_DATA 3
#define RE_Button 4 // Pushbutton on digital pin 4. Use 10k external pullup resistor to Vcc

#define I2C_ADDR    0x27
#define BACKLIGHT_PIN     3
#define En_pin  2
#define Rw_pin  1
#define Rs_pin  0
#define D4_pin  4
#define D5_pin  5
#define D6_pin  6
#define D7_pin  7

LiquidCrystal_I2C lcd(I2C_ADDR, En_pin, Rw_pin, Rs_pin, D4_pin, D5_pin, D6_pin, D7_pin);

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}; // Character for function not selected
uint8_t onedot[8] = {0x0, 0xe, 0x1f, 0x1b, 0x1f, 0xe, 0x0}; // Character for function selected

// Setup Keypad variables
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'}
};

// #=35; *=42; 0-9=48to57; A-D=65to68

byte rowPins[ROWS] = {5, 6, 7, 8 }; //{8,7,6,5 }; //connect to the row pinouts of the keypad
byte colPins[COLS] = {9, 10, 11, 12}; // {12,11,10,9}; //connect to the column pinouts of the keypad

// Now setup the hardware

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";

void setup() {

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

  lcd.begin (20, 4);
  lcd.setBacklightPin(BACKLIGHT_PIN, POSITIVE);
  lcd.setBacklight(HIGH);

  lcd.createChar(0, zerodot); // Using glyph generated above
  lcd.createChar(1, onedot);

  lcd.home (); // go home

  Serial.begin (115200); // Output to be transmitted to serial monitor and TX pin on Nano

  getAddresses();  // read loco IDs from eeprom
  lcd.print("DCC ++ Throttle");
  lcd.setCursor(0, 1);
  lcd.print("March 2022 v");
  for (int i = 0; i < 4; i++) {
    lcd.print(VersionNum[i]);
  }

  Serial.print("March 2022 Version ");
  for (int i = 0; i < 4; i++) {
    Serial.print(VersionNum[i]);
  }
  if (debug == 1) Serial.println("");
  Serial.print("<0>");// power off command to control unit vial serial monitor
  delay(1500);

  lcd.clear();

  InitialiseSpeedsLCD();
  InitialiseFunctionLCD();

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

}  // 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();
        InitialiseSpeedsLCD();
        getLocoAddress();
        updateSpeedsLCD();
        key = 0;
        break;

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

      case 'D':
        //Do nowt for now - menu goes in here?
        doExtendedFunction();
        updateSpeedsLCD();
        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();
    updateSpeedsLCD();
  }

  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();

      updateSpeedsLCD();

      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() {
  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 ) {
        lcd.setCursor(9 , number + 1);
        total = number * 10;
      }
      else if (counter == 2 && number < 10) {
        // Second time around... and 0-9
        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);
      }
      InitialiseFunctionLCD();

      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);
  InitialiseFunctionLCD();

  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 {
    lcd.setCursor( counter , ActiveAddress);
    key = keypad.getKey();
    if (key == '#' || key == '*' || key == 'A' || key == 'B' || key == 'C' || key == 'D' ) { //abort when either is hit

      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) {
        lcd.print(" ");
      } else {
        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 (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
  updateSpeedsLCD();
}

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 InitialiseSpeedsLCD() {
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Prints LocoID(right justified), direction arrow, and speed(right justified)
    lcd.setCursor(0, tempx);
    String temp = "   " + String(LocoAddress[tempx] , DEC);
    int tlen = temp.length() - 4;
    lcd.print(temp.substring(tlen));
    // ... direction...
    if (LocoDirection[tempx] == 1 ) {
      lcd.print(">");
    }
    else {
      lcd.print("<");
    }
    // ... speed ...
    temp = "  " + String(LocoSpeed[tempx] , DEC);
    tlen = temp.length() - 3;
    lcd.print(temp.substring(tlen));
  }
  // Return cursor to direction arrow for loco under control
  lcd.setCursor(4, ActiveAddress);
}

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

void updateSpeedsLCD() {

  int tempx = ActiveAddress;
  lcd.setCursor(0, tempx);
  String temp = "   " + String(LocoAddress[tempx] , DEC);
  int tlen = temp.length() - 4;
  lcd.print(temp.substring(tlen));
  if (LocoDirection[tempx] == 1 ) {
    lcd.print(">");
  }

  else {
    lcd.print("<");
  }

  temp = "  " + String(LocoSpeed[tempx] , DEC);
  tlen = temp.length() - 3;
  lcd.print(temp.substring(tlen));
  lcd.setCursor(4, ActiveAddress);
}

// Robust Rotary encoder reading
// 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;
}

I built your program to see how much SRAM was used. Then replaced all the print("text") and println("text") calls with F() versions. There is probably enough SRAM for a 128x64 buffer. You can possibly move some of your data arrays to PROGMEM too.

backintime_orig.h

Using library EEPROM at version 2.0 in folder: C:\Program Files (x86)\Arduino-1.8.19\hardware\arduino\avr\libraries\EEPROM 
Using library Wire at version 1.0 in folder: C:\Program Files (x86)\Arduino-1.8.19\hardware\arduino\avr\libraries\Wire 
Using library LiquidCrystal in folder: C:\Users\David Prentice\Documents\Arduino\libraries\LiquidCrystal (legacy)
Using library Keypad at version 3.1.0 in folder: C:\Users\David Prentice\Documents\Arduino\libraries\Keypad 
"C:\\Users\\David Prentice\\AppData\\Local\\Arduino15\\packages\\arduino\\tools\\avr-gcc\\7.3.0-atmel3.6.1-arduino7/bin/avr-size" -A "C:\\Users\\DAVIDP~1\\AppData\\Local\\Temp\\arduino_build_772611/backintime.ino.elf"
Sketch uses 11992 bytes (37%) of program storage space. Maximum is 32256 bytes.
Global variables use 800 bytes (39%) of dynamic memory, leaving 1248 bytes for local variables. Maximum is 2048 bytes.

backintime_F.h with F()

Using library EEPROM at version 2.0 in folder: C:\Program Files (x86)\Arduino-1.8.19\hardware\arduino\avr\libraries\EEPROM 
Using library Wire at version 1.0 in folder: C:\Program Files (x86)\Arduino-1.8.19\hardware\arduino\avr\libraries\Wire 
Using library LiquidCrystal in folder: C:\Users\David Prentice\Documents\Arduino\libraries\LiquidCrystal (legacy)
Using library Keypad at version 3.1.0 in folder: C:\Users\David Prentice\Documents\Arduino\libraries\Keypad 
"C:\\Users\\David Prentice\\AppData\\Local\\Arduino15\\packages\\arduino\\tools\\avr-gcc\\7.3.0-atmel3.6.1-arduino7/bin/avr-size" -A "C:\\Users\\DAVIDP~1\\AppData\\Local\\Temp\\arduino_build_772611/backintime.ino.elf"
Sketch uses 12092 bytes (37%) of program storage space. Maximum is 32256 bytes.
Global variables use 708 bytes (34%) of dynamic memory, leaving 1340 bytes for local variables. Maximum is 2048 bytes.

I have not taken time to try and understand how the sketch works, but it appears that some of the functions only update portions of the display (common technique for an LCD display). You will not be able to do this with u8g2 using a page buffer, the entire display needs to be rewritten. Using a full buffer will allow for changing a portion of the display, but you then need to erase the previous contents in that particular location, otherwise the old characters remain on the display when the new characters are written over them. Full buffer does require 1024 bytes of ram for this size display, but that does not appear to be a problem.

Thank you for your response David, I had noticed when I was trying to work with the u8x8 library that parts of the screen were just being overwritten rather than clearing the previous information.

Whether you use Adafruit_SSD1306 or U8g2 there is different behaviour for different fonts. But since you will probably use a regular 5x7 font this is not a problem. e.g. oled.setTextColor(WHITE, BLACK);

The main difference is that LiquidCrystal is written directly but the graphics-style libraries manipulate buffers. You have to send the updated buffer to the display to see the result.
e.g. you call oled.display() or the equivalent u8g2 page sequence.

David.

Here is an example of how you might port to a SSD1306.

  1. minimise SRAM by using F() e.g. oled.print(F("text"));

  2. add C helper functions to replace library methods like lcd.home() with lcd_home()

  3. write the SSD1306 equivalent code for the C helper()

  4. add call to lcd_display() to setup() and loop()

  5. you will see that several lcd_helper() functions are currently empty.

  6. Implement / improve the C helpers until you have satisfactory operation.

Note that I have tried not to touch your program logic. You need to try it on your hardware.

David.
backintime.zip (25.1 KB)

Hi David, thank you so much for your hard work, unfortunately, this is well beyond my level of understanding. I think I may have to park this idea

Did you try my code ?

Yes, it is a bit fiddly to edit the original code. LiquidCrystal has different methods to any graphics library.
If you look at my individual steps you can see that they are nothing more than Find/Replace in the Arduino Text Editor.

Which is why I suggested that you find a small 20x4 Character LCD or Character OLED if the regular 90mm LCD module is too wide and bulky.

David.

Hi Richard,

I tried to upload the sketch, but it would not compile...

with the following line highlighted:

Adafruit_SSD1306 lcd(128, 64, &Wire);

Error message:

exit status 1
no matching function for call to 'Adafruit_SSD1306::Adafruit_SSD1306(int, int, TwoWire*)'

I didn't want to keep bothering you with this as I can see you have spent quite some time on it

Andy

Do you have a link to a webpage for the project using the 20x4 LCD? Would like to see how it is suppose to operate. I originally got interested in programming arduinos when looking at an arduino based DCC controller.

If you look at the build summaries in "build.h" you can see what library versions were used.

I guess that they are reasonably "up to date".
If your <Adafruit_SSD1306.h> version is VERY OLD, it might not have this constructor.
It is wise to check via the IDE Library Manager.

Note that before the IDE Library Manager was invented, punters installed libraries from random sites on the Internet to random locations on their PC.

David.

Hi David,

Sorry, don't know why I called you Richard. This, as you have probably worked out is a DCC++ serial throttle. It is based on the work of Dave Bodnar and has been modified to use a rotary encoder to send commands to the main controller via the tx pin on the throttle to the rx pin on the Mega control station.

I got the modified code from a forum post on RMWeb which was posted a couple of years back. Unfortunately there wasn't a video to describe the operation.

It makes a very useful little throttle considering the fact that you can replace the physical tx to rx line between the throttle and command station with a wireless link such as the HC-12.

Basically you can store 4 decoder addresses in the eeprom, if you press the '*' key you can enter a new decoder address, press the * key again and you either store the new value or exit the setup. The # key allows you to cycle down the list of (up to 4) stored address and the cursor blinks on the 'active address'.

Keys 0 - 9 will operate the first 10 functions,
Keys D+1+(0 to 9) will allow the next 10 and Keys D+2+(0 to 8) the next 9

Special case D+2+9 resets all selected functions to 0

The encoder controls speed 0 - 126 if you press the switch on the encoder, the speed drops to zero and the direction is reversed ( > indicates fwd, < indicates reverse) you then turn the encoder to increase speed (in reverse)

Hope this makes sense?

Also, the decoder address is 4 characters long, although 0s are not displayed once stored
i.e. 0003 displays as 3

Hi David, I have deleted the 1306 library & reinstalled it! The sketch now compiles - thank you

The display looks good as does the formatting, not sure the encoder is working, but it could just be a loose DuPont cable, will have a play and get back to you!

Once again, thank you so much!

Good day David,

Firstly, thank you so much for your efforts with this project, now that I have deleted the Adafruit 1306 library and reinstalled, the sketch compiles without errors.

I hadn't realised last night that there were multiple tabs within the sketch.

Are these tabs all being used by the sketch as I can't see anything that calls another tab or are they stages in your workflow?

Obviously, my main goal is get the project working, but I would also like to try to understand what is going on within the sketch so that I can tweak without undoing your hard work.

I notice that the welcome screen in setup does not print to the display, but it is transmitted via the serial monitor.

In addition, I need to try to work out a method of highlighting where the active cursor is.

These however are secondary to the main problem that the rotary encoder is not being read correctly.

Kr

Andy

Tabs are a handy way to keep some assorted versions in one place.
You just copy-paste the required H file to the main INO tab.

Naming tabs .H means that the Arduino IDE is happy to view and edit.
You don't actually include them in the build.

But you can run the 20x4 LCD version by simply changing the #if 1 to #if 0. Assuming that both LCD and OLED are connected to the Uno.

I suggest that you debug your rotary encoder in the LCD version.
Then we can think about the empty OLED helper functions.

Regarding the missing welcome on the OLED.
It just needs an extra lcd_display(); call after the
lcd.print("DCC ++ Throttle"); sequence

David.

Thanks David, they say every day is a school day!

You could blink a character on and off using a timer, place an underline under the character (and optionally blink that), or reverse the colors at the cursor, making the background white and the character black. The underline cursor is probably the easiest, since the others would require that you know what character is displayed at a certain location. I have not looked closely at the font you are using with the Adafruit library, the u8g2 font has space underneath the characters, since it allows for descenders on letters such as y, p, etc, and that space is unused with the data you are displaying.

Reversing the display colors is also useful when editing a field, to highlight the specific item being edited.

< edit >
Thinking about it, unless you are using a full display buffer (which Adafruit's library does use), even blinking an underline would require redrawing the entire display, so the reverse colors may be the easiest to implement.

If you prefer the leading zero's that is easily done, just replace the leading spaces in the " " with "000" in this line

        String temp = "   " + String(LocoAddress[tempx] , DEC);

My personal preference is to replace all the String manipulation in the display functions with sprintf, makes it a bit easier to read and understand what is being done:

      char buff[10];
      sprintf(buff, "%04d%c%4d", LocoAddress[tempx], LocoDirection[tempx] == 1 ? '>' : '<', LocoSpeed[tempx]);
      u8g2.print(buff);

Hi David,

the ability to switch between the OLED and LCD by just changing one value and then uploading has been really helpful!

I can confirm that the encoder behaves as it should when the LCD screen is enabled, i.e CW turn of the encoder results in an increase both on the screen and on the serial monitor and -ve for CCW.

However, when the OLED screen is enabled, the encoder is more or less unresponsive, occasionally I can illicit a change, but certainly not the 100% step response when using the LCD screen. Additionally turning the encoder in either direction may produce a +ve or -ve response. The output to the screen and to the serial monitor are consistent, so it is not just a case of the display failing to update, but rather the encoder output being inhibited?

I suggest that you thoroughly test the LCD version. Make sure that the encoder has no glitches, LCD displays correctly, ...

Regarding blink. It is easier to just invert the pixels of the '>' cursor.
But it would be possible to simply rewrite the buffer every 200ms.

I would have to study the encoder library. I really don't see that the SSD1306 should interfere at all. Unless it is just "too many" lcd_display() calls i.e. every loop().

You fix that by setting a flag when you alter the buffer. i.e. any lcd.print()
Only call lcd_display() when the buffer is altered.

David.