20. Intro to LCD

Scrolling text on an LCD

Background

A crash course on using an LCD will be demonstrated.

The Theory

An LCD is a type of display technology

Displays are components used to communicate information to the user. They can be as simple as LEDs and 7-segment displays or as advanced as OLED’s.

An LCD conveys information in a 2D grid of rows and columns. Each “cell” is composed of tiny liquid crystals (hence the name LCD) arranged in a grid and can display letters, numbers and sprites. However, the graphics it can render is extremely simple.

There are other display technologies such as OLED, TFT and e-paper displays. All of them vary according to: price, power consumption, interface, driver support, graphics and colour. A comparison of display technologies is tabulated below.

AttributeCharacter LCD (16×2, HD44780)OLED (0.96″ SSD1306)TFT (2.4–2.8″ ILI9341)E-Paper (1.54″ e-ink)
Typical price~$2–5~$3–8~$8–15~$15–30
Power draw~40 mA logic + ~30 mA backlight~10–20 mA (no backlight)~60–80 mA (backlight heavy)µA-level idle; power only on refresh
InterfaceI2C (via backpack) or parallelI2C or SPISPI (or parallel)SPI only
Driver / librariesHD44780 · LiquidCrystal (ubiquitous)SSD1306 · Adafruit / U8g2 (excellent)ILI9341 · Adafruit GFX / TFT_eSPI (well supported)GxEPD2 / Waveshare (fiddlier, needs waveform tables)
Graphics & resolutionText only, 16×2 characters128×64 monochrome graphics240×320, rich full graphics200×200+, high res but very slow refresh
ColorMonochrome (fixed backlight)Monochrome (or yellow/blue)Full color (65K)B/W (some 2–3 colour), slow

For every-day usage, OLED’s are a strong choice. They deliver 128×64 graphics at a small power draw at a competitive price to an LCD. For a richer, more immersive display, the TFT would be apt at the cost of a higher power consumption and price.

A TFT display would be suitable for a video game console as it delivers rich and immersive graphics. They come in various resolutions. For a smartwatch, an OLED provides high-contrast animated graphics in a compact size. An e-paper display thrives in a deployed weather station where it must run for weeks on a battery.

There are various display technologies on the market, including LCD, OLED, TFT and e-paper.

Technical Summary

The display I’ve selected is a Vishay LCD-016N002B-CFH-ET LCD module driven by an ST7066 controller. It has 2 rows and 16 columns. This is a Character 16×2 LCD. This means it can display characters. We communicate with the LCD the same way we do with sensors and microcontrollers, by writing and reading to certain registers.

There are LCD’s that are operated via I2C, however the one I selected communicates via a 8-bit data bus. If I were to redo this project, I’d chose the I2C-operated LCD to save I/O’s.

This LCD has a 1/16 duty cycle. The LCD has 2 rows and each cell has 8 rows. Thus, when rendering graphics, the controller must scan 16 rows total. 1/16 means that each row is energized for 1/16th of the time. Each row is one for 1/16’th of a frame and off for 15/16. However, due to the refresh rate (typically 60-100Hz) it appears as if each row is illuminated.

This explains why the contrast varies. Each pixel is receiving a portion of the driving voltage according to the duty cycle. Luckily, the contrast pin (V0) allows us to adjust the contrast.

This LCD has 2 rows and 16 columns. Thus, it can render 32 “icons”. Let’s call each possible position a cell. A cell is comprised of 5×8 dots. Each dot-size is 0.55mm x 0.65mm. The total cell dimension is 2.95mm x 5.55mm. (row x col). Any characters we draw is limited to 2.95mm x 5.55mm. This will be relevant for drawing custom graphics via CGRAM.

An LCD cell is composed of a 5×8 2D array of “dots”.

The image below gives you an idea of each cell when the LCD is viewed as a complete module.

The LCD must scan 16 individual rows at a certain refresh rate.

To write a graphic to a cell, we must communicate with it’s address. This involves writing to a character code to DDRAM. This maps to a glyph. The controller then renders the bitmap. The contents of DDRAM is shown below.

Register contents of DDRAM

The LCD’s view direction is optimized for 6 o’clock. This is the most optimal viewing angle/direction. It has to do with how the liquid crystals are polarized … or something. Just don’t look at it with a birds-eye view or from the side.

Functional Description

The LCD has two 8-bit registers, an Instruction Register (IR) and a Data Register (DR). The RS pin is used to select either one of these registers. Afterwards, the R/W pin is used to either read/write from the selected register.

The IR register contains the full instruction set of the LCD such as clear LCD, move cursor and shift display left and set CGRAM address. It can only be written to.

The DR register is a buffer that stores characters written or read from Display Data RAM (DDRAM) or Character Generator RAM (CGRAM).

Display Data RAM (DDRAM)

The display data is stored as 8-bit character codes. Its extended capacity is 80×8 bits or 80 characters.

Character Generator ROM (CGROM)

The LCD controller has 200 presets called glyphs which are stored in its ROM. You simply transmit the 8-bit character code and the controller looks up the bitmap and drives the segments.

CGROM can generate icons from a 5×8 dot or 5×10 character patterns derived from a 8-bit character code.

The datasheet contains a table which is essentially a 16×16 lookup table of all the built-in glyphs. The 8-bit character code we write to DDRAM is split. The upper nibble is on the columns, lower nibble along the rows.

By selecting a character code and a CGRAM address, you can display any one of these glyphs.

Character Generator Ram (CGRAM)

This is how the user can render custom graphics, e.g. a cherry icon or battery symbol. There are two modes: Firstly, 5×8 dots, allows you to write 8 character patterns. On the other hand, 5×10 dots, allows you to write 4 character patterns. The character pattern is the actual bitmap, a nxm representation of which dots are illuminated.

LCD Pinout

The LCD I’m using has 14 pins.

My module from AliExpress has 2 extra pins, pins 15 and 16.

The LCD has 2 registers: Command and data register. We select the register by outputting a voltage on RS. 1 = data register. 0 = command register.

PinNameDescription
1VSSGND
2VCC5V
3VEEPower supply to control contrast
4RS0 = Select command register. 1 = Select data register
5R/W0 = Write. 1 = Read
6ENEnable
7DB08-bit data bus
8DB18-bit data bus
9DB28-bit data bus
10DB38-bit data bus
11DB48-bit data bus
12DB58-bit data bus
13DB68-bit data bus
14DB78-bit data bus
15BLABacklight Anode. Connect to 5V
16BLKBacklight Cathode. Connect to GND

R/W allows us to read/write to the LCD. 1 = Read. = 0 Write

D0-D7 is the 8-bit data bus. To send characters, we do RS = 1, send the 8-bit ASCII equivalent of the character/letter/symbol we want displayed. Below are the list of possible instructions we can transmit to the LCD. Think of it as the Instruction Set (ISA).

Code (Hex)Command to LCD Instruction Register
1Clear display screen
2Return home
4Decrement cursor (shift cursor to left)
6Increment cursor (shift cursor to right)
5Shift display right
7Shift display left
8Display off, cursor off
ADisplay off, cursor on
CDisplay on, cursor off
EDisplay on, cursor blinking
FDisplay on, cursor blinking
10Shift cursor position to left
14Shift cursor position to right
18Shift the entire display to the left
1CShift the entire display to the right
80Force cursor to beginning of 1st line
C0Force cursor to beginning of 2nd line
282 lines and 5 × 7 matrix (D4–D7, 4-bit)
382 lines and 5 × 7 matrix (D0–D7, 8-bit)

Table 2: LCD Command Codes

Project 1 – Hello World

Let’s get up and running with the LCD.

Hardware Connections

The schematic is available in my GitHub repository.

Highlights:

  • Data bus on Port D
  • Command port on PC0 – PC2
  • Backlight adjustment via R1

The Software

The program consists of a single main.c and its corresponding Makefile. The program initializes the LCD and prints a message, The world is but one country.

The Makefile saves us the headache of compiling the project. This includes dependencies and files from other file paths. Also, it contains project configuration settings.

Firstly, the LCD is initialized via you guessed it lcd_init(). Numerous steps are involved during initialization. Firstly, we sett the data bus and command port HIGH. We configure it for 8-bit mode, turn on the display, turn on the cursor and shift it to the right. These instructions reach the LCD through lcdCommand(), which accepts specific hex codes for each operation — see Table 2: LCD Command Codes. The cursor is then positioned at (1,1), the upper left, and the message is printed.

To print a message, the firmware invokes lcdprint(). This function invokes lcdData() for each character. This function implements the write cycle as conveyed by the datasheet. It is reproduced below. lcdData() implements these steps in code however with one deviation. Data is placed on the bus, before the setup window. It is assumed that the bus is stable.

Timing diagram for a write operation (pg. 14 of datasheet)
  1. Send data (single char) on data bus
  2. Select data register by setting RS = 1
  3. Write to the register by pulling RW low
  4. Pull EN high
  5. Wait one second before clearing EN

Full program available in my GitHub repository.

Backlight adjustment

Project 2 – 4-Bit Mode

We learnt about and flashed a Hello World program to the LCD. Fantastic!

Although, if you examine the schematic, you’ll notice that the data bus is 8-bits long. In some applications, you might not have the luxury of that many I/O’s. Luckily, the LCD has a 4-bit mode. This cuts the number of connections from 8 to 4.

NOTE: There is a version of this LCD that is I2C-compatible. Thus, further reducing the amount of connections.

Hardware Connections

The schematic is available in my GitHub repository.

Highlights

  • Command port on PB0-PB2
  • Data bus connected to high nibble of PORTD (PD4-PD7)

The Software

The program will initializes LCD and prints a message. It’s very similar to Project 1 apart from a few differences.

In lcd_init() we transmit the hex commands to initialize LCD to 4-bit mode. When transmitting data, in order to transmit 8 bits of information into 4 bits we perform a mask and bit-shift by 4 places. This behaviour is seen in lcdData(), lcdCommand().

As always, full program located in my GitHub repository.

A very factual statement is printed

Project 3 – Scrolling Text via UART

We’ve printed a simple message to the LCD. It’s time to step things up a notch! This program accepts a string via UART and prints it on LCD as scrolling text. Pot on PC0 adjusts scroll speed.

Hardware Connections

Nothing to see here. Identical to Project 2, except a potentiometer is connected to PC0. This is to adjust the scroll speed.

The schematic is available in my GitHub repository.

The Software

Two libraries are used: lcd_driver, USART.

Since this is a longer program than Project 1 and 2, it makes sense to compartmental LCD functions into a separate driver.

main.c

Macros are defined for scroll-speed and the display.

The scroll speed is connected to PC0, an ADC pin. We initialize the ADC by configuring the multiplexer, clock prescaler and disabling the input buffer. We read the ADC with a polling routine via adc_read().

Performs initialization of ADC, UART, LCD. LCD prompts user for text, confirms it’s not nonzero then scroll_display() is invoked. After it finishes, LCD clears and is ready for new text to scroll.

Reading the potentiometer — pot_delay_ms()

pot_delay_ms() is called for each time we “scroll” the display. Let’s call this a frame. It’s the implementation of the Arduino-equivalent map() except it doesn’t cost 200 cycles but rather 42. A higher voltage maps to a shorter delay. At this setting, you’ll visibly notice the individual frames scrolling by.

static uint16_t pot_delay_ms(void)
{
    uint16_t adc   = adc_read(0U);
    uint16_t range = (uint16_t)(SCROLL_MS_MAX - SCROLL_MS_MIN);
    /* invert: adc=0 → max delay, adc=1023 → min delay */
    return (uint16_t)(SCROLL_MS_MAX - ((uint32_t)adc * range >> 10U));
}
Pot positionADCdelayspeedtime to cross all 16 columns
fully CCW0600 ms1.7 chars/s9.6 s
centre~512325 ms3.1 chars/s5.2 s
fully CW102351 ms~20 chars/s0.8 s

Scrolling the display — scroll_display()

Let’s move our attention to the main workhorse of this program, scroll_display()

For scroll_display(), we render a frame that loops according to the length of the message (the argument) and a padding, the number of LCD columns (LCD_COLS). We loop through LCD_COLS and transmit a char at a time. We add a delay according to pot_delay_ms() between frames.

A frame is a redrawing of row 1. This is the inner loop with LCD_COLS as loop variable.

static void scroll_display(const char *msg)
{
    uint8_t len   = (uint8_t)strlen(msg);
    uint8_t total = len + (uint8_t)LCD_COLS;
    uint8_t step, col;

    for (step = 0U; step < total; step++)
    {
        uint16_t delay = pot_delay_ms();   /* sample pot before each step */

        Lcd_SetCursor(0U, 0U);
        for (col = 0U; col < (uint8_t)LCD_COLS; col++)
        {
            uint8_t src    = col + step;
            uint8_t offset = (uint8_t)(LCD_COLS - 1U);
            char    ch;

            if (src < offset)
                ch = ' ';
            else if ((src - offset) < len)
                ch = msg[src - offset];
            else
                ch = ' ';

            Lcd_WriteChar((uint8_t)ch);
        }

        delay_ms_var(delay);
    }
}
readString()

The main function from the USART library is readString(). The others are printString(), initUSART())

Anyways, readString() captures the user’s input. It works by appending a char of arrays according to the length of the user’s submitted sentence. This function simply delegates work to receiveByte(), transmitByte(). Very cool how the advanced functions are built from simpler ones.

void readString(char myString[], uint8_t maxLength) {
  char response;
  uint8_t i;
  i = 0;
  while (i < (maxLength - 1)) {                   /* prevent over-runs */
    response = receiveByte();
    transmitByte(response);                                    /* echo */
    if (response == '\r') {                     /* enter marks the end */
      break;
    }
    else {
      myString[i] = response;                       /* add in a letter */
      i++;
    }
  }
  myString[i] = 0;                          /* terminal NULL character */
}

Full source code located in my GitHub repository.

Verification
  1. After flashing, open a serial terminal at 9600 8N1 at the AVR’s COM port.
  2. Serial terminal should display: Enter text:
  3. Type The world is not round, but flat! and press Enter. The text should scroll right-to-left once across row 0 while row 1 shows << scrolling >>.
  4. After the scroll finishes the LCD goes blank and serial terminal displays Enter text: on a newline.

The Showcase

Video below demonstrates adjusting the scroll speed.