Simple Text Menu for ST7920 Graphic LCD

 Author:   Posted on:   Updated on:  2018-03-25T12:43:32Z
Graphic LCD displays are a good addition for any project where you want to display some data. They look better than the old fashioned 7 segment displays and even alphanumeric LCDs, but more than that, you can use them to build user interfaces and menus. In a previous post, I wrote code for displaying text on a ST7920 128x64 graphic LCD. To save space, I wrote my code from scratch, instead of using a library that draws text in graphic mode and takes up a lot of memory on small microcontrollers. This time, I will continue to add features to the initial Arduino sketch in order to create a simple menu. This menu still uses text mode for displaying items. In this way, you are forced to display a maximum of four items at a time (the display has 4 rows of 16 characters). For highlighting menu items, we'll have to switch to graphics mode and draw rectangles on the screen. I will show you how the graphics RAM of ST7920 is organized and how you can set any pixel you want. The nice thing about ST7920 is that text pixels and graphics pixels are never at the same state. Therefore, if you have written text on a row and afterwards you fill the entire pixels on that row, the text pixels will be cleared ("RGB Controller" is written in text mode, then all pixels from that row are filled - see photo below).

Simple Text Menu for ST7920 Graphic LCD
I created a simple RGB Controller that uses a rotary encoder to select and adjust duty cycle of each diode of an RGB LED. By pushing the encoder button you cycle through menu items and by rotating the knob you adjust PWM signal duty cycle.

I've written the basic functions for communicating with ST7920 in the previous article. Now, let's add what's missing. First of all, to start drawing on screen, we need to switch to graphic mode. To do this, there's the Function Set register (see datasheet, page 16-17). First, we set RE bit to 1 to switch to extended function mode and then we set G bit to 1. To revert back to text mode, RE bit should be cleared. This could be an implementation:
void ST7920_setGraphicMode(boolean enabled = true) {
  ST7920_Write(LCD_COMMAND, enabled ? LCD_EXTEND_FUNCTION : LCD_BASIC_FUNCTION);
  if (enabled) ST7920_Write(LCD_COMMAND, LCD_EXTEND_FUNCTION | 0x02); // Graphic ON
}
Now, you have access to graphic RAM. But, after reset, this memory is filled with random data. We need to clear it before drawing something relevant on screen. Before writing a function to clear the memory, let's see how it is organized.

ST7920 graphic memory organization
ST7920 graphic memory organization
Similar to text RAM, ST7920 seems to be built for a 256x32 display. Imagine that display being perfectly "cut" in two pieces that are then stacked one on top of the other. We have a top half and a bottom half of display.

In graphic mode, you get access to 16 horizontal pixels at a time. First, you select the row on Y axis (0 to 31) then one of the 16 pixel groups (first 8 on top half, second 8 on bottom half). 0x80 is added to coordinates because this is the RAM base address. To clear RAM we have to write 0x00 to all pixel groups. We don't care about display organization, we just treat the display as 256x32.
void ST7920_ClearGraphicMem() {
  for (byte x = 0; x < 16; x++)
    for (byte y = 0; y < 32; y++) {
      ST7920_Write(LCD_COMMAND, 0x80 | y);
      ST7920_Write(LCD_COMMAND, 0x80 | x);
      ST7920_Write(LCD_DATA, 0x00);
      ST7920_Write(LCD_DATA, 0x00);
    }
}
The RAM is cleared progressively, row by row.

Now we need to highlight the title "RGB Controller". All pixels from Y = 0 to 14 and X = 0 to 7 need to be filled. The rectangle is 15 pixels height, not 16 as it would fill the entire row and be in contact with the selection rectangle on first menu item.
  ST7920_setGraphicMode(true);
  ST7920_ClearGraphicMem();

  // highlight title
  for (byte y = 0; y < 15; y++)
    for (byte x = 0; x < 8; x++) {
      ST7920_Write(LCD_COMMAND, 0x80 | y);
      ST7920_Write(LCD_COMMAND, 0x80 | x);
      ST7920_Write(LCD_DATA, 0xFF);
      ST7920_Write(LCD_DATA, 0xFF);
    }

  ST7920_setGraphicMode(false);
First, enable graphic mode, clear RAM then start drawing. At the end, revert to text mode. You can put text before or after drawing the rectangle, the result is the same.

Next, we need to draw item selection rectangle. To ease with deselecting menu items, the same function must be able to clear the rectangle too. Another way would have been to clear graphics memory, then redraw title filled rectangle and new selection rectangle. Item selection rectangle must appear on rows 2 to 4. Not on the first row which contains title. If you are using my code for a different application, without title on top row, you should know that the following function is able to draw selection rectangle on each of the 4 rows.

The selection rectangle is made up of two horizontal lines on top and bottom of row, and two vertical lines at the left and right edges. Horizontal lines are easier to draw because you fill entire fixel groups on a row (0xFF). For the left vertical line, you set only the MSB of first pixel group (0x80) on a column. For the right one, you set the LSB (0x01) of the second byte in the last pixel group. The complete function could look like this:
void ST7920_HighlightMenuItem(byte idx, boolean fill = true) {
  idx &= 0x03; // 4 rows only

  byte y = idx * 16;
  byte x_addr = 0x80;

  // adjust cooridinates and address
  if (y >= 32) {
    y -= 32;
    x_addr = 0x88;
  }

  for (byte x = 0; x < 8; x++) {
    ST7920_Write(LCD_COMMAND, 0x80 | y);
    ST7920_Write(LCD_COMMAND, x_addr | x);
    fill ? ST7920_Write(LCD_DATA, 0xFF) : ST7920_Write(LCD_DATA, 0x00);
    fill ? ST7920_Write(LCD_DATA, 0xFF) : ST7920_Write(LCD_DATA, 0x00);

    ST7920_Write(LCD_COMMAND, 0x80 | y + 15);
    ST7920_Write(LCD_COMMAND, x_addr | x);
    fill ? ST7920_Write(LCD_DATA, 0xFF) : ST7920_Write(LCD_DATA, 0x00);
    fill ? ST7920_Write(LCD_DATA, 0xFF) : ST7920_Write(LCD_DATA, 0x00);
  }

  for (byte y1 = y + 1; y1 < y + 15; y1++) {
    ST7920_Write(LCD_COMMAND, 0x80 | y1);
    ST7920_Write(LCD_COMMAND, x_addr);
    fill ? ST7920_Write(LCD_DATA, 0x80) : ST7920_Write(LCD_DATA, 0x00);
    ST7920_Write(LCD_DATA, 0x00);

    ST7920_Write(LCD_COMMAND, 0x80 | y1);
    ST7920_Write(LCD_COMMAND, x_addr + 7);
    ST7920_Write(LCD_DATA, 0x00);
    fill ? ST7920_Write(LCD_DATA, 0x01) : ST7920_Write(LCD_DATA, 0x00);
  }
}
I used a boolean variable to change the behavior of the draw function. Therefore this function can also clear a selected menu item. Therefore the following code can cycle through menu items at a push of a pulled-up button.
  if (digitalRead(ENC_BTN) == LOW) {
    ST7920_setGraphicMode(true);
    ST7920_HighlightMenuItem(selectedItem, false);

    selectedItem += 1;
    if (selectedItem > 3) selectedItem = 1;

    ST7920_HighlightMenuItem(selectedItem, true);
    ST7920_setGraphicMode(false);

    delay(200);
  }
Variable selectedItem holds current selection and it is an unsigned 8 bit number.

There's only one thing left to do. The previously written function for text display isn't too versatile. Now I need to change only the percent value, not any other text. So, I write a new function that writes the percent at the right address.
void displayValue(byte percent) {
  byte addr;
  switch (selectedItem) {
    case 1: addr = 0x90 + 4; break;
    case 2: addr = 0x88 + 4; break;
    case 3: addr = 0x98 + 4; break;
  }

  String p = String(percent * 100 / 256);
  p = p + "%  ";

  ST7920_Write(LCD_COMMAND, addr);
  for (byte i = 0; i < 4; i++) {
    ST7920_Write(LCD_DATA, p.charAt(i));
  }
}
It also uses global variable selectedItem to know on what row to change value.

Breadboard project of ST7920 "RGB Controller" with rotary encoder
Breadboard project of ST7920 "RGB Controller" with rotary encoder
The complete sketch can be found on GitHub. On ATmega328 it uses only 14% of program memory. Next time, I will load 16x16 icons from EEPROM on display, to create an even better looking menu.

No comments :

Post a Comment

Please read the comments policy before publishing your comment.