Skip to main content

LCD display

Welcome to the most exciting part of this kit. LCDs are wonderful tools to display: text or data, simple shapes, BMP images or fancy interfaces, etc. In this tutorial, you will learn how to work with the LCD. Let's start!

Learning goals

  • Get a general idea of SPI protocol.
  • Understand the coordinate system of LCD.
  • Know more about color: RGB color model, color depth.
  • Learn about vector and raster image.
  • Learn to draw simple graphics on LCD.

🔸Background

What is SPI?

Serial Peripheral Interface, SPI for short, is another synchronous communication protocol between devices. It supports one master with multiple devices, but its speed is much faster than the I2C.

In most cases, SPI needs four wires:

  • SCK (serial clock): it carries the clock signal generated by the master (the SwiftIO Feather board), which is used to ensure synchronous data transmission.
  • SDI (serial data in): also called MISO (master in slave out). The master device receives data from slave devices through this wire.
  • SDO (serial data out): also called MOSI (master out slave in). The master device sends data to slave devices through this wire.
  • CS (chip select): master device controls this signal to select the specified slave device. Each slave device needs a CS that connects to the master. At the beginning of an SPI communication, the master device will set the CS line to wake up the corresponding device and keep others unchanged. In this way, the desired device gets prepared for the following communication.
SPI

SPI has 4 modes: 0, 1, 2, 3. They are decided by CPOL and CPHA.

  • CPOL (clock polarity): it depends on the state of SCK when the line is idle. 1 for high and 0 for low.
  • CPHA (clock phase): it descibes the sampling phase relative to the clock signal. If the data is sampled at the first edge of clock pulse, it is 0. If it's sampled at second edge, it's 1.
SPI mode

As for data transmission, there are two modes which tells how data is sent: most-significant bit (MSB) first, or least-significant bit (LSB) first. In a byte, MSB is the left-most bit and LSB is the right-most bit.

All in all, you could regard SPI communication as a combination of input and output following some established transmission rules. Its main work is to read (receive) and write (send) data, all other details such as mode define how the data is sent and interpreted.

🔸New component

LCD

There are so many kinds of screens to display characters or graphics. The one you are going to use is a TFT LCD screen (Thin-film-transistor liquid crystal display). It provides a great view from all angles.

This 1.54 inches LCD on your kit has 240x240 pixels. One pixel means one point on the LCD. To display some stuff on the LCD, you need to set the pixels. Each pixel needs a 16-bit color.

The coordinate system on screens is quite different from what you get used to before. The origin (0,0) is in the upper left corner. The values in the x-axis and y-axis gradually increase from 0 to maximum. So all the coordinates for this LCD are from (0,0) to (239,239).

LCD

If you want to display some graphics on a screen, you need to set the corresponding pixels: the coordinates and the colors data. All data is sent to the LCD via SPI wires.

🔸New concept

RGB color model

RGB color model is a common way to describe colors in many displays, for example, on your computer monitor.

RGB represents red, green, and blue. They are the basis of colors. The combination of three colors forms more colors: red and green form yellow, green and blue form cyan, blue and red form magenta, the combination of rgb colors form white. By mixing three colors with different intensities, you will get a broad range of colors.

RGB color model

Color depth

Color depth, or bit depth, describes the number of bits to represent a pixel on the screen. It tells the count of colors that each pixel can display. For each bit, there are two values: 0 or 1. So you could easily get the total number of colors: 2 to the power of the number of bits. A 1-bit color depth has only two colors, such as white and black.

The color depth you will always see nowadays is 16-bit, 24-bit, and 32 bit.

  • For 16-bit color, also called high color, there are 65536 colors in total. The values are divided into 5, 6, 5 bits, representing red, green, and blue.
  • 24-bit color is called true color. There are about 16 million colors. The bits are evenly divided to represent RGB colors: 8 for red, 8 for green, and 8 for blue.
  • 32-bit color, quite like 24-bit color, uses 8-bit for red, 8-bit for green, and 8-bit for blue. Besides, there is an 8-bit alpha channel to represent the degree of transparency.

The color code normally uses 8 bits to represent each color in hexadecimal. For example, 0xFFFFFF represents white, 0x000000 represents black, 0xFF0000 represents red.

24-bit RGB color

However, the LCD module in this kit supports 16-bit color. So if you are going to define the color for this screen, you need to convert 24-bit color to 16-bit color. Usually, you will take the bits of each color from the most significant bits as below, because they have more effect on the color. So red is represented as 0xF800. You could use the online converters to get 16-bit color values from 24-bit.

16-bit RGB color

Image format

When you scale up images several times, you may notice some of the images are just the same and don't lose any quality. These images are called vector images. They use points and lines to create paths based on mathematical formulas. The fonts used on all websites now are usually vector format. It will never be blurry as you zoom it in.

While for other images, multiple grids begin to appear as they are zoomed in to a certain degree. These images are called raster images, also known as bitmaps. The whole image is a combination of the smallest units called pixels. More pixels of the same size means the image is of higher quality.

Raster and vector image

In this tutorial, the stuff displayed on the LCD are all raster graphics. There are two commonly used file formats of bitmap.

  • BMP stands for bitmap. BMP image is uncompressed and stores the color data of all pixels. Therefore it usually has a large file size.
  • JPG file uses compression algorithms to deal with similar pixels to reduce the file size. So it needs to be decoded as you open the file. It is a widely used format, so the image viewers on your computer can open it successfully.

🔸Circuit - LCD module

The LCD is connected to SPI0 for communication and D9 for CS signal. The other wires connected to the digital pins are used to configure the LCD.

LCD circuit.pngLCD circuit diagram.png
note

The circuits above are simplified versions for your reference.

🔸Preparation

Class

SPI - this class is used to send and receive data using SPI protocol.

MethodExplanation
init(_:speed:csPin:
CPOL:CPHA:bitOrder:)
Initialize an SPI interface.
Parameter:
- idName: spi pin id that the device connects to.
- speed: the communication speed, 5MHz by default. The maximum speed is about 30MHz.
- csPin: a DigitalOut pin as a CS pin. It is nil by default. If you don’t set it, you need to control it manually when communicating with devices.
- CPOL: the state of clock signal when it's idle, false by default.
- CPHA: the edge of clock signal to sample data, false for first edge and true for second edge. false by default.
- bitOrder: the bit order during data transmission, .MSB by default.

ST7789 - it configures the LCD and allows you to draw pixels on it.

MethodExplanation
init(spi:cs:dc:rst:bl:
width:height:rotation:)
Initialize an LCD.
Parameter:
- spi: the SPI interface that the LCD connects to.
- cs: the cs pin for SPI communication. The cs pin is controlled in this class, so the csPin in SPI class should be nil.
- dc: a DigitalOut pin used for data and command selection.
- rst: a DigitalOut pin used to reset the LCD.
- bl: a DigitalOut pin used for backlight control.
- width: the width of the LCD, 240 by default.
- height: the height of the LCD, 240 by default.
- rotation: the degree of LCD rotation, .angle0 by default. It is used to set the position of origin.
writePixel(x:y:color:)Set a single pixel on the screen.
Parameter:
- x, y: the coordinate of the pixel.
- color: a UInt16 color value.
writeBitmap(x:y:
width:height:data:)
Set an area of pixels on the screen.
Parameter:
- x, y: the coordinate of the starting point, that is, the left upper corner of the area.
- width: the width of the area.
- height: the height of the area.
- data: the color data stored in an UInt8 array. Please note the data is stored in UInt8, so two values are used for one pixel.
clearScreen(_:)Fill the whole screen with one color.
Parameter:
- color: a color data in UInt16.

These methods to draw on the screen are quite similar:

  • The method writeBitmap fills a block of pixels on the screen. You need to tell the position of the pixels decided by the starting point, width, and height. So the area of all pixels forms a rectangle. The pixels of the LCD need 16-bit colors, the color values stored in the array are 8-bit, so the count of the color array equals width x height x 2.

    For example, the color of the rectangle below is represented as 0xF483. So the color passed in should be [0xF4, 0x83, 0xF4, 0x83, 0xF4, 0x83, 0xF4, 0x83, 0xF4, 0x83, 0xF4, 0x83].

    Write a bitmap on screen
  • The method writePixel has a default value of 1 for both width and height to draw a single pixel, so it needs only one color data. It is the most basic operation. If you want to display whatever graphic, like a circle, rectangle, triangle, etc, it is just about the calculation of the coordinates of necessary pixels.

  • The method clearScreen sets the bitmap to screen size. It needs a UInt16 color value to fill all pixels with same color.

🔸Projects

  1. LCD display

1. LCD display

You are going to use the library ST7789 to paint the screen and show some simple animation on it.

Example code

// Import SwiftIO to set the communication and MadBoard to use pin id. 
import SwiftIO
import MadBoard
// Import the library to configure the LCD and write pixels on it.
import ST7789

// Initialize the SPI pin and the digital pins for the LCD.
let spi = SPI(Id.SPI0, speed: 30_000_000)
let cs = DigitalOut(Id.D9)
let dc = DigitalOut(Id.D10)
let rst = DigitalOut(Id.D14)
let bl = DigitalOut(Id.D2)

// Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left.
let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90)

// Store some color values for easier reference later.
let black: UInt16 = 0x0000
let red: UInt16 = 0xF800
let green: UInt16 = 0x07E0
let blue: UInt16 = 0x001F
let white: UInt16 = 0xFFFF

// Fill the whole screen with red, green, blue, white, and black in turns.
// The color changes every second.
screen.clearScreen(red)
sleep(ms: 1000)

screen.clearScreen(green)
sleep(ms: 1000)

screen.clearScreen(blue)
sleep(ms: 1000)

screen.clearScreen(white)
sleep(ms: 1000)

screen.clearScreen(black)
sleep(ms: 1000)

// Draw red horizontal lines every 10 rows, so the lines will be on rows 0, 10, 20, ..., 230.
for y in stride(from: 0, to: screen.height, by: 10) {
for x in 0..<screen.width {
screen.writePixel(x: x, y: y, color: red)
}
}
sleep(ms: 1000)

// Draw blue vertical lines every 10 columns, so the lines will be on columns 0, 10, 20, ..., 230.
for x in stride(from: 0, to: screen.width, by: 10) {
for y in 0..<screen.height {
screen.writePixel(x: x, y: y, color: blue)
}
}
sleep(ms: 1000)

// Paint the screen black to erase all stuff on it.
screen.clearScreen(black)
sleep(ms: 1000)

// Store the colors of four squares for later use.
let colors = [red, green, blue, white]

// Set the size of the square.
let width = 80

// First, draw a 80x80 red square from the origin.
// After one second, draw a green one from the centre of the red square.
// The blue and white ones are similar.
while true {
var x = 0, y = 0

for color in colors {
fillSquare(x: x, y: y, width: width, color: color)
x += width / 2
y += width / 2
sleep(ms: 1000)
}
}

// This function allows you to draw a square on the LCD from the point (x,y).
func fillSquare(x: Int, y: Int, width: Int, color: UInt16) {
for px in y..<(y + width) {
for py in x..<(x + width) {
screen.writePixel(x: px, y: py, color: color)
}
}
}

Code analysis

let spi = SPI(Id.SPI0, speed: 30_000_000)

The LCD needs quite a lot of data for display, so the SPI speed needs to be faster to get a better view. And the Feather board support 30MHz SPI speed at most.

let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90)

This statement initializes the LCD. It needs several parameters:

  • at first, the pins used for SPI communication: an SPI interface and a cs pin.

    There are two occasions to set a digital pin as cs: initializing an SPI pin or initializing a device. Here, the pin is passed in when initializing the LCD as the driver has configured the pin during communication. So the parameter csPin of SPI should be nil.

  • some digital pins reserved for LCD configuration.

  • then the size of the screen: width and height. The chip ST7789 supports screens of different sizes. The default size is 240x240, and it is what you are using, so there's no need to change them.

  • at last, the rotation of the screen. The screen rotates by 90 degrees when designing the circuit. So if you don’t set it, the origin will be at the lower-left corner. To have a better view, you could set it to .angle90, so the origin will be at the upper left corner.

LCD Rotation
for y in stride(from: 0, to: screen.height, by: 10) {
for x in 0..<screen.width {
screen.writePixel(x: x, y: y, color: red)
}
}

The function stride(from:to:by:) makes y start from 0, increased by 10 each time. The maximum value is 240 but is not included, so y is 230 at most. The value for x is 0, 1 ... 238, 239.

Here two for-in loops are used to set all corresponding pixels in red one by one. At first, the pixels from (0,0) to (239,0) are set, they form a horizontal line. Then the pixels (0, 10) to (239, 10)... The speed used to write one pixel is so fast that the lines appear on the screen extremely fast.

func fillSquare(x: Int, y: Int, width: Int, color: UInt16) {
for px in y..<(y + width) {
for py in x..<(x + width) {
screen.writePixel(x: px, y: py, color: color)
}
}
}

This function fills an area (rectangle or square) of pixels with the same color. For example, in the image below, the pixels are set in order: (4,3), (5,3), (4,4), (5,4), (4,5), (5,5).

If you change the order of two loops, the pixel order will change accordingly: (4,3), (4,4), (4,5), (5,3), (5,4), (5,5). However, you cannot perceive this change as the speed is so fast.

Write a bitmap on screen
for color in colors {
fillSquare(x: x, y: y, width: width, color: color)
x += width / 2
y += width / 2
sleep(ms: 1000)
}

In the loop, squares of the same size will show on the screen: red square starts from (0,0), green one from (40,40), blue one from (80,80), white one from (120,120).

After this loop is finished, x and y equal 0 again, and then the animation repeats.

🔸More info