Introduction: The Secrets of an Inexpensive, Ubiquitous Chinese LCD

About: I am a hobbyist with an interest in open-source software, 3D printing, science and electronics. Please visit my store or Patreon page to help support my work!

In this Instructable, I will share the secrets of an inexpensive 128x64 LCD display module.

This display is very commonly used for 3D printers, making its particular aspect ratio quite recognizable on many 3D printers, but the nice thing about its popularity is that you can obtain a bare display module for Arduino projects for only five bucks on eBay or AliExpress.

While this display module is often driven by the U8G library, in this Instructable I will walk you through the process of interfacing with it directly. This allows you to learn the full capabilities of the ST7920 controller chip while conserving resources on your Arduino board. This is useful if your project needs:

  • A simple, light-weight user interface with a large legible font.
  • You only need a few icons, bitmaps or graphical elements.
  • You want to spend the least amount of Flash and RAM on your Arduino.
  • You want the fastest possible refresh rate with the minimum amount of CPU use.

This Instructable is meant to be used along with the official datasheet (which I have attached), but it clarifies a few things, walks you through the capabilities and provides many ready to run examples.

Step 1: Where Do I Get One?

The bare display module itself can be found on eBay of AliExpress for about $5. Search for "ST7920 128x64".

Alternatively, you can use a 128x64 display built specifically for a 3D printer, such as the "RepRapDiscount Full Graphics Smart Controller", so long as it is driven by the Sitronix ST7920 chip. Not all are, for example, the MKS 12864 OLED display module, while an attractive alternative, uses a different chip and would not work for this tutorial.

The 3D RepRapDiscount Full Graphics Smart Controller will typically sell for $10-$15. It includes additional hardware, such as an rotary encoder, a push button, a buzzer and a SD card reader. None of the extra hardware will be used in this particular tutorial, so it is entirely your choice as to whether to get a bare display module or a full RepRapDiscount assembly.

One nice thing about the bare display module is that if you solder headers onto it you can plug it right into a breadboard. For the RepRapDiscount Full Graphics Smart Controller module, you will need jumper wires.

Step 2: Hooking the Display Module to Your Arduino

The ST7920 display chip can be configured to use a protocol known as SPI, or Serial Peripheral Interface. For this mode, you'll need two data pins on the Arduino.

One the face (or perhaps the rear) of the LCD module, you will see labels for the various pins. Here are the ones you need to connect:

  • VCC -> Connect to 5V
  • GND -> Connect to GND
  • RS -> Connect to 5V
  • RW -> Connect to D5 (or an available data pin on your Arduino)
  • E -> Connect to D4 (or an available data pin on your Arduino)
  • PSB -> Connect to GND
  • RST -> Connect to 5V
  • A (or BLA) -> Connect to 5V
  • K (or BLK) -> Connect to GND

If you wish, you can connect a variable resistor (trimpot) between A and 5V to adjust the LCD brightness, or a fixed value resistor if you find the display is too bright.

If you are using a RepRap Discount Full Graphics Controller, then only five connections will be needed, as shown in the photo. The pin names in parenthesis are alternative names for the signal lines.

Step 3: Sending Commands to the Display Module

Communication with the display module takes place through a series of pulses. These pulses happen on the following pins:

  • E - Serial clock (SCLK)
  • RW - Serial Input Data (SID)
  • RS - Chip Select (CS)

The serial clock signal (SCLK) tells the display that a new bit is available for reading on the serial input data (SID) signal line. The chip select (CS) is only used when we are driving multiple SPI devices at a time. Since we are only connecting one display to our Arduino, we can connect this line to 5V to make it always active.

From the datasheet, we see that the pulses happen in a particular sequence. We translate this into Arduino code like so:

#define LCD_SID  5
#define LCD_SCLK 4
void lcd_send(bool value) {
  digitalWrite(LCD_SID, value);
  digitalWrite(LCD_SCLK, HIGH);
  digitalWrite(LCD_SCLK, LOW);
}
void lcd_sync(bool rs, bool rw) {
  lcd_send(1); // Sync 1
  lcd_send(1); // Sync 2
  lcd_send(1); // Sync 3
  lcd_send(1); // Sync 4
  lcd_send(1); // Sync 5
  lcd_send(rw);
  lcd_send(rs);
  lcd_send(0);
}
void lcd_data(uint8_t data) {
  lcd_send(data & 0b10000000);
  lcd_send(data & 0b01000000);
  lcd_send(data & 0b00100000);
  lcd_send(data & 0b00010000);
  lcd_send(0);
  lcd_send(0);
  lcd_send(0);
  lcd_send(0);
  lcd_send(data & 0b00001000);
  lcd_send(data & 0b00000100);
  lcd_send(data & 0b00000010);
  lcd_send(data & 0b00000001);
  lcd_send(0);
  lcd_send(0);
  lcd_send(0);
  lcd_send(0);
}

For convenience, we break the task up into smaller functions: "lcd_send" sends a single bit, while "lcd_sync" and "lcd_data" together send the full sequence specified in the data sheet's diagram. I chose to make "lcd_data" a separate function as it allows me to send multiple data bytes after a single sync, which is faster than sending one at a time.

At the top of the program, I define constants for LCD_SID and LCD_SCLK, as this makes it easy to adjust the pins used for communicating with the LCD module.

This particular mode of communications is called "bit-banging", as it does not use the hardware SPI port on the Arduino. The advantage of "bit-banging" is that it works on any data port.

Step 4: Initalizing the Display

The datasheet defined various commands that we will need to translate into code.

In order to properly initialize the display, we will need to define the following commands:

  • EXTENDED FUNCTION SET
  • DISPLAY STATUS
  • CLEAR

Since all the commands are similar, we define a generic function for sending a command. This function incorporates a delay of 72us, as the datasheet specifies this as the required wait time after each command:

void lcd_cmd(uint8_t cmd) {
  lcd_sync(0, 0); // All commands have rs=0 and rw=0
  lcd_data(cmd);
  delayMicroseconds(72); // The datasheet specifies that commands take 72u to execute
}

We now write code for the three initialization instructions:

void lcd_extended_function_set(bool extended, bool graphics) {<br>  lcd_cmd(  0b00100000 | 
    (extended   ? 0b00000100 : 0) |
    (graphics   ? 0b00000010 : 0)
  );
}
void lcd_display_status(bool display_on, bool cursor_on, bool blink_on) {
  lcd_cmd(0b00001000 |
    (display_on ? 0b0100 : 0) |
    (cursor_on  ? 0b0010 : 0) |
    (blink_on   ? 0b0001 : 0)
  );
}
void lcd_clear() {
  lcd_cmd(0b00000001);
  delayMicroseconds(1600); // The datasheet specifies that CLEAR requires 1.6ms
}

Some functions have different options which are passed in as arguments. We use the binary OR operator ("|") and an inline "if a then b else c" statement ("a ? b : c") to assemble the required command byte, as specified in the datasheet.

Step 5: Writing Data to Memory

The ST7920 has several memory areas. The primary one is called the data display RAM (DDRAM) and it is used for showing text.

Each memory area has a corresponding command that sets an internal address counter (AC) to that region. For the DDRAM, the command is called SET DDRAM ADDRESS.

Here is what it looks like in code:

void lcd_set_ddram_address(uint8_t addr) {
  lcd_cmd(0b10000000 | (addr & 0b00111111));
}

In order to print something on the display, we must initialize the AC to the position we want to write and then we send one or more data bytes to be written to that location. Bytes must always be sent in pairs. The AC automatically increments after the second byte is sent.

Sending data bytes is similar to sending commands, except that a single bit (rs) is different and we are able to send multiple bytes in a row, as opposed to just one. Furthermore, there is no need to wait 72u, as we have to do for commands.

We implement this in code like this:

void lcd_write_begin() {
  lcd_sync(1,0);
}
void lcd_write_byte(uint8_t w) {
  lcd_data(w);
}

For convenience, we also write a function that allows us to copy entire strings from Flash memory:

void lcd_write_str(const char *str) {
  char c = pgm_read_byte_near(str++);
  while(c) {
     lcd_write_byte(c);
     c = pgm_read_byte_near(str++);
  }
}

This function loops through all the characters in the string using "pgm_read_byte_near" and writes it out using "lcd_write_byte". "pgm_read_byte_near" is a special Arduino function for reading bytes from Flash storage.

Using these functions, the procedure for writing text to the LCD is this:

lcd_set_ddram_address(0x00);
lcd_write_begin();
lcd_write_str(PSTR("Write this!"));

The "PSTR" makes it so the string is stored in Flash memory (a.k.a. PROGMEM) which is vital for conserving the limited RAM on the Arduino.

Step 6: Drawing Some Text!

Now, we finally write our main program:

void setup() {
  // Set all the pins as output
  pinMode(LCD_SID,  OUTPUT);
  pinMode(LCD_SCLK, OUTPUT);

  // Initialize the display
  lcd_extended_function_set(false, false); // Do this twice since only one bit
  lcd_extended_function_set(false, false); // can be set at a time.
  lcd_display_status(true, false, false);
  lcd_clear();

  // Set the address to the top of the display
  // and write some text
  lcd_set_ddram_address(0x00);
  lcd_write_begin();
  lcd_write_str(PSTR("The quick brown fox jumps over the lazy dogs. 0123456789.:,;(*!?"));
}
void loop() {
}

The first thing the "setup" does is configure the pins for SID and SCLK as output pins. Then, we send a series of commands to initialize the display.

The first "True" argument to "lcd_display_status" turns the display on. Without it, the screen would remain blank. The remaining two arguments indicate whether the text cursor is shown and whether it will blink. They are currently set to "False", so no cursor will be shown. You can change this if you would like.

For your convenience, I have attached the entire Arduino sketch so you can compile it and upload it to your Arduino.

Once you run this sketch, you should see a screen full of text. However, the lines will be out of order. Let's try to understand why and learn how to fix that.

Step 7: The Display That Folds Unto Itself

As you ran your first program, you may have noticed that the lines were out of order. The reason for this is that the ST7920 can show only two lines of 32 characters at a time. To get around this limitation, the designers of the 128x64 LCD module folded the display in half, so that the first two lines are cut in half and pasted on at the bottom of the display. This is shown in the figure.

Even though this gives us four lines to work with, their arrangement in memory is non-intuitive.

Each memory location, as indexed by the address counter, has space for two 8-bit values -- that is, two characters. Increasing the AC by one moves over by two characters at a time, as indicated by the hexadecimal numbers on the figure above and below the corresponding characters.

From those numbers, we can determine the addresses corresponding to the start of the lines as they are shown on the "real" display. We define them at the start of our program:

#define DDRAM_LINE_1   0x00
#define DDRAM_LINE_2   0x10
#define DDRAM_LINE_3   0x08
#define DDRAM_LINE_4   0x18

With these constants, it is now easy to write lines in the proper order by calling "lcd_set_ddram_address" before each line and writing up to a 16 maximum of characters at a time:

lcd_set_ddram_address(DDRAM_LINE_1);
lcd_write_begin();
lcd_write_str(PSTR("The quick brown "));

lcd_set_ddram_address(DDRAM_LINE_2);
lcd_write_begin();
lcd_write_str(PSTR("fox jumps over t"));

lcd_set_ddram_address(DDRAM_LINE_3);
lcd_write_begin();
lcd_write_str(PSTR("he lazy dogs. 01"));

lcd_set_ddram_address(DDRAM_LINE_4);
lcd_write_begin();
lcd_write_str(PSTR("23456789.:,;(*!?"));

Step 8: Defining Custom Icons

The ST7290 allows you to define up to four 16x16 bitmaps. These bitmaps can be shown in any 16-bit location in the DDRAM, occupying the place of two individual characters.

The bitmaps themselves are stored in an memory region called the CGRAM (Character Generator RAM).

In order to write custom icons, we define a few new constants and implement a new SET CGRAM ADDRESS instruction:

#define CGRAM_ICON_1_ADDR 0x00
#define CGRAM_ICON_2_ADDR 0x10
#define CGRAM_ICON_3_ADDR 0x20
#define CGRAM_ICON_4_ADDR 0x30

#define CGRAM_ICON_1_WORD 0x00
#define CGRAM_ICON_2_WORD 0x02
#define CGRAM_ICON_3_WORD 0x04
#define CGRAM_ICON_4_WORD 0x06

void lcd_set_cgram_address(uint8_t addr) {
  lcd_cmd(0b01000000 | (addr & 0b00111111));
}

We also create a new function for writing 16-bit words:

void lcd_write_word(uint16_t w) {
  lcd_data((w >> 8) & 0xFF);
  lcd_data((w >> 0) & 0xFF);
}

At startup, we can now load our bitmaps into one or more of the CGRAM locations. Let's make outselves some Space Invader aliens!

void load_custom_bitmaps() {
  lcd_set_cgram_address(CGRAM_ICON_1_ADDR);
  lcd_write_begin();
  lcd_write_word(0b0000000000000000); // 0
  lcd_write_word(0b0000000000000000); // 1
  lcd_write_word(0b0000000000000000); // 2
  lcd_write_word(0b0000000000000000); // 3
  lcd_write_word(0b0000010000010000); // 4
  lcd_write_word(0b0000001000100000); // 5
  lcd_write_word(0b0000011111110000); // 6
  lcd_write_word(0b0000110111011000); // 7
  lcd_write_word(0b0001111111111100); // 8
  lcd_write_word(0b0001011111110100); // 9
  lcd_write_word(0b0001010000010100); // A
  lcd_write_word(0b0000001101100000); // B
  lcd_write_word(0b0000000000000000); // C
  lcd_write_word(0b0000000000000000); // D
  lcd_write_word(0b0000000000000000); // E
  lcd_write_word(0b0000000000000000); // F

  lcd_set_cgram_address(CGRAM_ICON_2_ADDR);
  lcd_write_begin();
  lcd_write_word(0b0000000000000000); // 0
  lcd_write_word(0b0000000000000000); // 1
  lcd_write_word(0b0000000000000000); // 2
  lcd_write_word(0b0000000000000000); // 3
  lcd_write_word(0b0000010000010000); // 4
  lcd_write_word(0b0001001000100100); // 5
  lcd_write_word(0b0001011111110100); // 6
  lcd_write_word(0b0001110111011100); // 7
  lcd_write_word(0b0001111111111100); // 8
  lcd_write_word(0b0000111111111000); // 9
  lcd_write_word(0b0000010000010000); // A
  lcd_write_word(0b0000100000001000); // B
  lcd_write_word(0b0000000000000000); // C
  lcd_write_word(0b0000000000000000); // D
  lcd_write_word(0b0000000000000000); // E
  lcd_write_word(0b0000000000000000); // F
}

We write 1's where we want a pixel lit up, and 0 where we want it dark. If you stand far away from your monitor, you may be able to make out the pictures embedded in the binary strings!

Step 9: Animating a Space Invaders Intro Screen

Suppose we are building an Intro screen for the game. The intro screen will have some text and some animated aliens. This is easy to do using what we already learned.

At startup, I draw the text for the game's title:

lcd_set_ddram_address(DDRAM_LINE_2);
lcd_write_begin(); lcd_write_str(PSTR(" SPACE ")); lcd_set_ddram_address(DDRAM_LINE_3); lcd_write_begin(); lcd_write_str(PSTR(" INVADERS "));

Now, in the loop() function, I animate the alien by writing a 16-bit word to the DDRAM locations corresponding to the space where I want the alien to appear. By alternating every second between writing CGRAM_ICON_1_WORD and CGRAM_ICON_2_WORD, the alien comes to life!

void loop() {
lcd_set_ddram_address(DDRAM_LINE_2+1); lcd_write_begin(); lcd_write_word(CGRAM_ICON_1_WORD); delay(1000); lcd_set_ddram_address(DDRAM_LINE_2+1); lcd_write_begin(); lcd_write_word(CGRAM_ICON_2_WORD); delay(1000); }

Download Example 2 to show a complete intro screen with four animated Space Invaders aliens!

Step 10: Combining Text and Larger Bitmaps Using the DDRAM and GDRAM

In addition to the text in DDRAM and the custom icons in CGRAM, the ST7920 also has one last memory region, a full graphics buffer that can store a complete, full-screen 128x64 bitmap! This last buffer is called the GDRAM (Graphics Data RAM).

So, to review, the ST7920 has three memory regions:

  • The DDRAM is a text buffer that can store 64 characters, which are folded into 4 lines of 16 characters.
  • The CGRAM is a bitmap buffer that can store four 16x16 icon-like custom text characters.
  • The GDRAM that can store a full screen 128x64 bitmap!

One of the nice things about the GDRAM is that it can be used along with the DDRAM and the CGRAM. The contents are combined using an "exclusive OR" (XOR) operation, so it is easy to make Arduino sketches that combine text, graphics and animated icons.

In order to access the GDRAM, we introduce the SET GDRAM ADDRESS function:

void lcd_set_gdram_address(uint8_t x, uint8_t y) {
lcd_cmd(0b10000000 | (y & 0b01111111)); lcd_cmd(0b10000000 | (x & 0b00001111)); }

We will also need a function for clearing the GDRAM:

#define BUFFER_WIDTH   256
#define BUFFER_HEIGHT 32 void clear_gdram() { for(int y = 0; y < BUFFER_HEIGHT; y++) { lcd_extended_function_set(true, false); lcd_set_gdram_address(0,y); lcd_extended_function_set(false, false); lcd_write_begin(); for(int i = 0; i < (BUFFER_WIDTH / 16); i++) { lcd_write_word(0); } } }

And another for loading a bitmap from Flash memory into GDRAM:

void write_progmem_to_lcd(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const void *data) {
x /= 16; w /= 16; for(int j = y; j < y + h; j++) { lcd_extended_function_set(true, true); // Take care of the "folding" if(j < 32) { lcd_set_gdram_address(x,j); } else { lcd_set_gdram_address(x+8,j-32); } lcd_extended_function_set(false, true); lcd_write_begin(); for(int i = 0; i < w; i++) { lcd_write_byte(pgm_read_byte_near(data++)); lcd_write_byte(pgm_read_byte_near(data++)); } } }

Like the DDRAM, the GDRAM is folded over, so this function needs to take care of that special case. Other than that, it looks somewhat like the function we saw earlier for loading strings.

Step 11: Converting GIFs to an Arduino Sketch Using Open-source Tools

In this example, I converted a Hitchhiker's Guide to the Galaxy image into C code that I could paste into my Arduino sketch.

The conversion was done using the script "gif2arduino.m" that I wrote in GNU Octave. The script handles indexed color GIFs, as well as multi-frame animated GIFs. The image is automatically dithered into black and white using the algorithm "bayer.m" by Marcelo Jo.

If you want to try converting your own GIFs, resize it so that it is a multiple of 16 in the horizontal dimension. The GIF must be indexed and contain some color information, even if it is just shades of gray -- it will automatically be dithered for you. Then install GNU Octave and save your GIF, the "gif2arduino.m" and "bayer.m" files to a directory. Start Octave and "cd" into that directory from the Octave command prompt. Then type:

gif2arduino("input.gif", "output.c")

This will read the file "input.gif" and save out C source code as "output.c". Then open the .c file in a text editor and copy the hex bytes into your Arduino sketch. From there, you can display it using the "write_progmem_to_lcd" routine we wrote earlier.

Here are highlights from the example that shows Hitchhiker's Guide to the Galaxy emblem:

PROGMEM const unsigned char dont_panic[] = {
... }; void setup() { ... lcd_set_ddram_address(DDRAM_LINE_2+4); lcd_write_begin(); lcd_write_str(PSTR(" DON'T")); lcd_set_ddram_address(DDRAM_LINE_3+4); lcd_write_begin(); lcd_write_str(PSTR(" PANIC")); write_progmem_to_lcd(0, 0, 64, 64, dont_panic); }

The entire Arduino sketch is provided for you to try out!

Step 12: And Last, But Not Least, Full-screen Video!

In my last example, I show you how to do full-screen video!

This shows a scene from the 1959 motion picture Ben Hur, starring Charlton Heston, who is maybe best known among us techies for his iconic line in the Soylent Green movie. The anachronistic video clip borrows elements from both movies! I hope you enjoy it!

The clip began as an animated GIF that I converted using my script and put into the Arduino sketch. I then used the DDRAM and CGRAM to overlay some witty dialog.

Some optimizations were done to the code, such as replacing "digitalWrite" with register accesses, but overall it uses the same tricks you have already learned in this tutorial! For the best frame rate, you can configure your board type by changing the "BOARD" variable at the start of the Sketch. Currently I have pin definitions for the Arduino Nano as well as some 3D printer boards.

Another example you may want to learn from is this one, which is a complete user interface that I am developing for 3D printers at work. It shows how to draw an animated progress bar in GDRAM and how to read a rotary encoder to navigate a menu. The main screen is drawn using the same techniques I shared in this Instructable.

Congratulations on making it this far! If you make something cool with what you learned, please share it in the comments!