C Code for Text Mode on ST7920 Graphic LCD

 Posted by:   Posted on:   Updated on:  2018-11-17T20:43:38Z

SImple C code, Arduino compatible, for setting up and writing text on ST7920 based graphic 128x64 LCD.

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. If you interface a graphic LCD with an Arduino or compatible AVR development board, you probably heard about u8g2 library. This is a monochrome graphics display library which supports a lot of LCD controllers and screens of different sizes. It is very easy to use and comes with a lot of functions and display fonts. But this comes with a price. Text is drawn on LCD in graphics mode (this is how it renders different fonts). Combine this with the fact that in serial mode, some LCD controllers are write only. Therefore, the library must keep a part or the entire display data in RAM. This is not a bad thing, but unless you are developing some application where graphics is generated programmatically (something like a game), rather static user interfaces can be written to LCD from a ROM memory and don't need to be kept in RAM all time. And if you're creating a hardware project, you don't usually need to support different LCD controllers, as you'll not replace the LCD.

With this in mind and wanting to learn how to control a graphic LCD, I started to develop my own code. It turned out to be simpler than I thought. Simple code also means simple porting to other platforms. So I started this project with a ST7920 128x64 graphic LCD. I chose ST7920 because it supports serial protocol (SPI) and is 3.3V and 5V compatible. When I bought it I thought I could directly interface it with an OpenWRT router.

C Code for Text Mode on ST7920 Graphic LCD
When you start writing your own code/library for a device, the first place to look is the device datasheet. ST7920 is a chip manufactured by Sitronix and has support for Chinese alphabet. Don't worry, it can display also the English alphabet with 8x16 characters.

ST7920 supported English characters
ST7920 supported English characters (source: datasheet)
This is enough for most users and keep in mind that while you are in graphic mode, you can draw any character you want, anywhere on the display. In text mode, the LCD has 4 rows of 16 characters.

Connecting the display in serial mode is easy. You need to set PSB pin to 0 (connect it to GND) to inform the controller that you will be sending serial data. Then, RS becomes CS, RW becomes MOSI and EN becomes SCK. It is also recommended to connect the reset pin. Look on the back of your LCD board. If it has a preset trimmer resistor, that can be used for contrast adjustment and you don't need to connect anything to VO pin.You should also note that some manufacturers hardwire the LCD to either parallel or serial mode. Use a continuity tester (ohmmeter) to check if PSB is connected to VCC or GND.

Connect ST7920 LCD to Arduino Nano SPI pins
Connect ST7920 LCD to Arduino Nano SPI pins
I connected the LCD to Arduino SPI pins, because I want to use hardware SPI. The RST pin can connect to any digital output pin. it is good practice to connect D12 (MISO) to GND because it is an input pin left floating.

According to datasheet, SPI clock cycle should be at least 600ns. This means a maximum frequency of 1.66 MHz. However, I couldn't get output on display with frequencies higher than 400 kHz. I don't know if it's ATmega328 or ST7920 fault. Let's have a look at the datasheet and see how a single byte is sent in serial mode.

ST7920 SPI transfer
ST7920 SPI transfer (source: datasheet)
To send one useful bye to ST7920, you need to transfer three bytes. The first one holds the state for RS bit and therefore toggles the controller between instruction and data mode. The rest of two bytes contain one half of the useful byte. The Arduino code for this transfer would look like:
void ST7920_Write(boolean command, byte lcdData) {
  SPI.beginTransaction(SPISettings(200000UL, MSBFIRST, SPI_MODE3));
  digitalWrite(10, HIGH);
  SPI.transfer(command ? 0xFA : 0xF8);
  SPI.transfer(lcdData & 0xF0);
  SPI.transfer((lcdData << 4) & 0xF0);
  digitalWrite(10, LOW);
  SPI.endTransaction();
}
SPI bus is configured for mode 3 (data read on clock falling edge), with MSB first. Wikipedia has a good article on SPI modes. Since we know how to transfer data to LCD, let's perform its initialization. To improve code readability, Zhongxu used some definitions for ST7920 registers.
#define LCD_DATA                1       // Data bit
#define LCD_COMMAND             0       // Command bit
#define LCD_CLEAR_SCREEN        0x01    // Clear screen
#define LCD_ADDRESS_RESET       0x02    // The address counter is reset
#define LCD_BASIC_FUNCTION      0x30    // Basic instruction set
#define LCD_EXTEND_FUNCTION     0x34    // Extended instruction set
With this, the initialization sequence can look like this:
void ST7920_Init() {
  digitalWrite(LCD_RST, LOW);
  delay(100);
  digitalWrite(LCD_RST, HIGH);

  ST7920_Write(LCD_COMMAND, LCD_BASIC_FUNCTION); // Function set
  ST7920_Write(LCD_COMMAND, LCD_CLEAR_SCREEN);   // Display clear
  ST7920_Write(LCD_COMMAND, 0x06); // Entry mode set
  ST7920_Write(LCD_COMMAND, 0x0C); // Display control
}
LCD_RST is assigned to pin 8 and it is configured as output. It is active low. After reset, the display is configured by writing four instructions (refer to datasheet, page 16):
  1. Function set clears RE bit (switches to basic instruction).
  2. Display clear fills character ram with spaces and resets address counter.
  3. Entry mode set configures cursor direction to right (I/D=1) and disables display shift.
  4. Display control turns display on (D=1) and disables cursor display and blink.
Now, the display is ON and awaiting text to be displayed. Let's see how character RAM is organised.

Character RAM of ST7920
Character RAM of ST7920
If you start sending a character array to ST7920 the address counter will increment automatically and fill the first row (Line 1 of text) with 16 characters. Then it jumps to the third row (Line 2 of text) and fills it with the next 16 characters. After that comes second row (Line 3 of text) and fourth row (Line 4 of text). Placing text at specified position requires setting RAM address. This is performed by LCD_COMMAND with an address from above table.

The RAM is optimized for Chinese 16x16 characters. Therefore, writing regular 8x16 characters is a bit different. You write two characters at a time. So, if you want to put 'A' at 0x93 position, you must also write a space before it like this:
  ST7920_Write(LCD_COMMAND, 0x93);
  ST7920_Write(LCD_DATA, ' ');
  ST7920_Write(LCD_DATA, 'A');
For 'B' character, it's enough to send only this character. The one next to it will remain unchanged unless you sent something.
  ST7920_Write(LCD_COMMAND, 0x9A);
  ST7920_Write(LCD_DATA, 'B');
You don't need to adjust address before each character. To write a string, set initial address then continue transferring the string one character by character. Like this example on Line 2:
  ST7920_Write(LCD_COMMAND, 0x88);
  const char *text = "ST7920 Graphic LCD";
  for (int i = 0; i < 18; i++)
    ST7920_Write(LCD_DATA, *text++);
It produced the following output (see how it placed text on next line, which is actually above):

Text output on ST7920 LCD
Text output on ST7920 LCD
Next time, I will switch to graphic mode and create a simple, mixed text-graphic menu with selection highlight (like you see in the top photo).

This code is adapted from Experimenting with ST7920 128×64 graphical LCD on a PIC and zhongxu/avr.ST7920. My complete Arduino sketch can be download from here.

16 comments :

  1. Thanks for the code but it doesn't work with my LCD yet the waveforms on E,R/W & RS are pretty much identical to the SPI transfer figure shown above. I might have fried my display in my testing so I will have to purchase another one. Its the external cat & mouse applied to HW/SW

    ReplyDelete
  2. Finally found the wrong resistor position R9 had the display in parallel mode...

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. hi nice tutorial. i do have a question can we use 20 pin 12c interface for this lcd..?
    i have purchased this https://www.aliexpress.com/item/32882157852.html?spm=a2g0s.9042311.0.0.27424c4dwPYRUq
    but not working

    ReplyDelete
    Replies
    1. I guess you can use that too while ST7920 is in parallel mode. But there's no need for it, since ST7920 supports SPI protocol which uses only 3 wires.

      Delete
  5. ManyTHANKS. I had absolutely same ideas this morning (forget huge u8 lib because of space 10kbytes code and write only couple of procedures with spi.h)... but I stopped my effort cause I dont know hot to do it. Now its easy!!!

    ReplyDelete
  6. I has a sketch that works using u8g2 on a mega. I have changed your references to pin 10 to pin 53, but doesn't work. In one instance changing between u8g2 and your code it displayed your data, usually it just shows a line of one or two of the pixels from the text row. Any ideas?

    ReplyDelete
    Replies
    1. Is the SPI clock speed too high?

      Delete
    2. I tried lower clock speeds (down to 100000). I also rebuilt the circuit with a nano. Exact same result. The nano worked with the GraphicsTest sample from the u8g2 library, but couldn't get your code to run.

      Delete
    3. for some reason its not working for me, but working well with u8g2(U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* CS=*/ 10, /* reset=*/ 8);
      )

      Delete
  7. Can i use 12864 lcd from transistor tester t4

    ReplyDelete
    Replies
    1. If it has ST7920 controller, then yes, you can. I don't know the controller used by that LCD.

      Delete
  8. Thank you for this, especially for spi commands

    ReplyDelete
  9. Thank you very much for the help. However I fear the SPI mode may be incorrect, since the datasheet seems to correlate with mode 0, and this is the only clock mode I had success with using the same ST9720 driver. Other than that, stuff like the data rate I would have never guessed, so really appreciate the help with that :)

    ReplyDelete
    Replies
    1. Also just a tip for other users, if you’re using crappy quality jumper cables like I was, you’ll want to lower the data rate even further for this to be reliable. Especially for when you are writing lots of data to graphic RAM. It’s slower but much much more reliable. Solid core wire though improves bandwidth greatly from my experience so that’s something to experiment with…

      Delete
  10. I had a similar issue as some comments above with it not working, the issue is that the ST7920 seems to be quite slow compared to the Arduino, so I used clock_prescale_set(clock_div_2); to slow down the Arduino's clock and therefore reduce the SPI speeds. You can also tune the SPI speed and use SPI.setClockDivider(divider); too but the clock_prescale on its own worked best for me. Hope that helps.

    ReplyDelete

Please read the comments policy before publishing your comment.