Text entry on Arduino 1602 LCD display with only three push-buttons

Working on Stellarduino (an Arduino-powered telescope computer) has taught me many things about programming, especially the art of doing a lot with very little.

In this case, my Arduino sketch needed to know the date and time in order to figure out which stars are overhead, which can then be used to automatically select two ideal stars for alignment. The obvious solution was to use a Real Time Clock, which I did, and it’s great, but you still need to set it the first time, and adding an RTC to the build meant just another barrier to entry for users.

Stellarduino text entry

So, I wanted to provide a way for a user to enter the date using only a few push-buttons, and a 16 x 2 LCD display. After toying with the idea of adding numeric keypad, then writing it off as more of a barrier than just buying an RTC module, I decided to go with something akin to the way old arcade machines let you enter your initials if you get a high score.

Arduino UIs lend themselves to being implemented as a state machine, with each button press triggering a transition of states. This means you need some way of waiting for one of a number of buttons to be pressed, and to solve this I came up with probably the worst bit of code I’ve ever written:

int waitForButton()
  int button;

  while (true) {
    // Poor man's "wait for button to be pressed".
    while (digitalRead(OK_BTN) == 0 && digitalRead(UP_BTN) == 0 &&
      digitalRead(DOWN_BTN) == 0) {}

    // Poor man's "which button was pressed?".
    button = digitalRead(OK_BTN) ? OK_BTN :
      digitalRead(UP_BTN) ? UP_BTN :
      digitalRead(DOWN_BTN) ? DOWN_BTN : -1;

    // Poor man's debounce.
    return button;

It’s horrific, but it works.

The next challenge was creating the state machine that would allow a user to navigate across the LCD display, altering the number displayed at each location. Fortunately the Arduino LiquidCrystal library has pair of methods show and hide the underline cursor at the current location, and the position and current character can be stored as integers.

Because the text to be entered is a date, it needs a placeholder and some special formatting to guide the user in what to enter. To implement this, a placeholder answer is printed to the display first, and certain characters are designated to be skipped when the user is moving past them.

String lcdDatePrompt(LiquidCrystal lcd)
  char question[] = "Enter UTC Date";
  char answer[] = "YYYY-MM-DD HH:MM";
  int answerLength = 16;
  int skipPositions[] = {4, 7, 10, 13};
  int skipsCount = 4;
  char characters[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
  int charactersCount = 10;
  int cursorPosition = 0;
  int currentCharacter = 0;
  int button;

  // Print the question to the display.
  lcd.setCursor(0, 1);

  // Print the answer to the display as placeholder text.
  lcd.setCursor(0, 1);

  // Enable the cursor.

  while (true) {
    // Write current character to screen, then reset cursor on top of it.
    lcd.setCursor(cursorPosition, 1);

    button = waitForButton();

    if (button == OK_BTN) {
      // Store selected character in answer output string.
      answer[cursorPosition] = characters[currentCharacter];

      // Move cursor along, skipping cells if necessary.
      while (inArray(cursorPosition, skipPositions, skipsCount)) {
      lcd.setCursor(cursorPosition, 1);

      // Reset currentCharacter.
      // TODO: Remember char when returning to a position that's already set.
      currentCharacter = 0;

      // If at end of answer, break out of loop.
      if (cursorPosition >= answerLength) {
    } else if (button == UP_BTN) {

    } else if (button == DOWN_BTN) {

    // Prevent currentCharacter from wrapping the characters array.
    if (currentCharacter < 0) {
      currentCharacter = currentCharacter + charactersCount;
    } else if (currentCharacter >= charactersCount) {
      currentCharacter = currentCharacter % charactersCount;


  return answer;

The currentCharacter int is always wrapped to a value between 0 and charactersCount, this means selecting the next character after ‘9’ returns a ‘0’. The function inArray is just a loop through the array that returns true if the requested element is found. Very similar to the PHP equivalent.

If you’re interested, the full code to Stellarduino can be seen on GitHub. It’s very much a work-in-progress, but hopefully commented enough to be quite readable.