Raspberry Pi, Python and TFT 2.4″ – wiring, running and painting

So this is it. A start of a long road to the main control node. It needs output (display) and input(touch). I was thinking about using a ready-solution, hook shield to a RPi and do some web based GUI. But the web it is my daily work – so no! It must be something different. And I want to use Python!

So lets get to work and do some first drawing. My screen is 2.4″ TFT 240×320 v2.1 – this display has an ILI9325 controller for an LCD and AD7843 for touch. And it is GPIO only version without SPI (ok it has SPI for touch) but it may work in 8-bit mode (it has 16 data pins – we will use only 8).

We will start with dirty code and one goal in mind. Display something 🙂 Later we will do the same thing as with CharLCD – separate driver logic and wiring from the main class.

A what?

So why we are doing this? Currently, I gather some data and have a problem with clear displaying useful information. So I thought that I need one central unit for this.
Using char LCDs was nice but they have much less space than this TFT. And as a side effect, we can have a touch control. Imagine that we have icons (like windows mobile :P) with data and additionally we can click on them. Icon with the weather, click, and we have all required weather info. Icon with light to toggle in this room but also ability to control other rooms (not that I have so any :D).
In summer temperature control with fans and maybe auto-opening windows. (this one later :P)
But as lots of things we need to start from small one. Like to display something! Anything 🙂


So many pins but we are lucky, all are described on the board. I’m using Raspberry Pi 2B because it has lots of pins – and we need lots of them 😀 How many? 8 bits for data, DB8 – DB15, +3.3V, GND, data/cmd, write, read, cs, rest, led a. Signal read to 3.3 because we won’t need it, cs to gnd and we are left with 12 important pins.

TFT                          Raspberry Pi 2B

GND   ------------------------ GND
Vcc   ------------------------ 3.3
RS    ------------------------ G27 (data[H]/cmd[L])
WR    ------------------------ G17 
RD    ------------------------ 3.3 (never read from screen)
DB8   ------------------------ G22
DB9   ------------------------ G23
DB10  ------------------------ G24
DB11  ------------------------ G5
DB12  ------------------------ G12
DB13  ------------------------ G16
DB14  ------------------------ G20
DB15  ------------------------ G21
CS    ------------------------ GND (always selected)
REST  ------------------------ G25
LED_A ------------------------ G6 (yes G6 not 3.3, we already used both 3.3v pins:P)


We wired LED_A to G6 because we have no 3.3v left. But in this a way, we can control backlight and it may be good for us in future (yes I have a plan :)).
Wiring takes lots of wires but is relatively straightforward.


Okey, we wired it what now? We need to initialize display… quick look at documentation and here: http://www.elecfreaks.com/1228.html and seems it is not as easy as with char LCD. We need much more commands and data.

But before we can init screen we need to init/assign pins and all this stuff. Lets start a TFT class:

import RPi.GPIO as GPIO
import time

class TFT(object):
    def __init__(self):
        self.pins = {
            'RS': 27,
            'W': 17,
            'DB8': 22,
            'DB9': 23,
            'DB10': 24,
            'DB11': 5,
            'DB12': 12,
            'DB13': 16,
            'DB14': 20,
            'DB15': 21,
            'RST': 25,
        self.data_pins = [
            'DB8', 'DB9', 'DB10', 'DB11', 'DB12', 'DB13', 'DB14', 'DB15',
        for pin in self.pins:
            GPIO.setup(self.pins[pin], GPIO.OUT)
            GPIO.output(self.pins[pin], 0)

        self.color = {
            'R': 255, 'G': 255, 'B': 255
        self.bcolor = {
            'R': 0,  'G': 0, 'B': 0,

Pins, data pins, color and background color. It is a color display so we need colors 😀

And time for harder part. For sure we need a cmd function, data function and send function. Command and data differ by setting RS pin low.

Additionally, from what I saw we need cmd_data function. Such mix is used in referred page, they send command and data for it. When we look at their init function I can imagine how ugly it would be without it.

     def cmd(self, cmd):
        GPIO.output(self.pins['RS'], 0)

    def _set_pins(self, bits):
        for i in self.data_pins:
            value = bits & 0x01
            GPIO.output(self.pins[i], value)
            bits >>= 1

    def send(self, data):
        self._set_pins(data >> 8)
        GPIO.output(self.pins['W'], 0)
        GPIO.output(self.pins['W'], 1)
        GPIO.output(self.pins['W'], 0)
        GPIO.output(self.pins['W'], 1)

    def data(self, data):
        GPIO.output(self.pins['RS'], 1)

    def cmd_data(self, cmd, data):
        GPIO.output(self.pins['RS'], 0)
        GPIO.output(self.pins['RS'], 1)

What we need to know more?

Display bus is 16bit. But we work in 8bit mode. So it is required to send 8bit twice to have a full 16bit word. This is similar to a 4-bit mode in char LCD.

But to have more fun, we need colors in 5-6-5 bit mode. Why? Lets look at this from a different perspective: an RGB with each channel from 0-255, that is 8 bits per channel. This is 8-8-8 mode. In total it is 24bits.
LCD bus is 16-bits so we must lose some:) So from 8-8-8 we drop some data to have a 5-6-5 and this is exactly 16-bits. We know why, time to write how:

   def _bcolor(self):
        """color from 8-8-8 to 5-6-5"""
        rgb = self.bcolor['R'] << 16 | self.bcolor['G'] << 8 | self.bcolor['B'] return ((rgb & 0x00f80000) >> 8) | ((rgb & 0x0000fc00) >> 5) | ((rgb & 0x000000f8) >> 3)

    def _color(self):
        """color from 8-8-8 to 5-6-5"""
        rgb = self.color['R'] << 16 | self.color['G'] << 8 | self.color['B'] return ((rgb & 0x00f80000) >> 8) | ((rgb & 0x0000fc00) >> 5) | ((rgb & 0x000000f8) >> 3)

And there is light

Back to initialization. It is a huge block of code:

    def init(self):
        GPIO.output(self.pins['RST'], 1)
        GPIO.output(self.pins['RST'], 0)
        GPIO.output(self.pins['RST'], 1)

        # ************* ILI9325C/D **********
        self.cmd_data(0x0001, 0x0100)  # set SS and SM bit
        self.cmd_data(0x0002, 0x0200)  # set 1 line inversion
        self.cmd_data(0x0003, 0x1030)  # set GRAM write direction and BGR=1.
        self.cmd_data(0x0004, 0x0000)  # Resize register
        self.cmd_data(0x0008, 0x0207)  # set the back porch and front porch
        self.cmd_data(0x0009, 0x0000)  # set non-display area refresh cycle ISC[3:0]
        self.cmd_data(0x000A, 0x0000)  # FMARK function
        self.cmd_data(0x000C, 0x0000)  # RGB interface setting
        self.cmd_data(0x000D, 0x0000)  # Frame marker Position
        self.cmd_data(0x000F, 0x0000)  # RGB interface polarity

        # ************* Power On sequence ****************
        self.cmd_data(0x0010, 0x0000)  # SAP, BT[3:0], AP, DSTB, SLP, STB
        self.cmd_data(0x0011, 0x0007)  # DC1[2:0], DC0[2:0], VC[2:0]
        self.cmd_data(0x0012, 0x0000)  # VREG1OUT voltage
        self.cmd_data(0x0013, 0x0000)  # VDV[4:0] for VCOM amplitude
        self.cmd_data(0x0007, 0x0001)
        time.sleep(0.2)  # Dis-charge capacitor power voltage
        self.cmd_data(0x0010, 0x1690)  # SAP, BT[3:0], AP, DSTB, SLP, STB
        self.cmd_data(0x0011, 0x0227)  # Set DC1[2:0], DC0[2:0], VC[2:0]
        self.cmd_data(0x0012, 0x000D)
        self.cmd_data(0x0013, 0x1200)  # VDV[4:0] for VCOM amplitude
        self.cmd_data(0x0029, 0x000A)  # 04  VCM[5:0] for VCOMH
        self.cmd_data(0x002B, 0x000D)  # Set Frame Rate
        self.cmd_data(0x0020, 0x0000)  # GRAM horizontal Address
        self.cmd_data(0x0021, 0x0000)  # GRAM Vertical Address

        # ************* Adjust the Gamma Curve *************
        self.cmd_data(0x0030, 0x0000)
        self.cmd_data(0x0031, 0x0404)
        self.cmd_data(0x0032, 0x0003)
        self.cmd_data(0x0035, 0x0405)
        self.cmd_data(0x0036, 0x0808)
        self.cmd_data(0x0037, 0x0407)
        self.cmd_data(0x0038, 0x0303)
        self.cmd_data(0x0039, 0x0707)
        self.cmd_data(0x003C, 0x0504)
        self.cmd_data(0x003D, 0x0808)

        # ************* Set GRAM area *************
        self.cmd_data(0x0050, 0x0000)  # Horizontal GRAM Start Address
        self.cmd_data(0x0051, 0x00EF)  # Horizontal GRAM End Address
        self.cmd_data(0x0052, 0x0000)  # Vertical GRAM Start Address
        self.cmd_data(0x0053, 0x013F)  # Vertical GRAM Start Address
        self.cmd_data(0x0060, 0xA700)  # Gate Scan Line
        self.cmd_data(0x0061, 0x0001)  # NDL, VLE, REV
        self.cmd_data(0x006A, 0x0000)  # set scrolling line

        # ************* Partial Display Control *************
        self.cmd_data(0x0080, 0x0000)
        self.cmd_data(0x0081, 0x0000)
        self.cmd_data(0x0082, 0x0000)
        self.cmd_data(0x0083, 0x0000)
        self.cmd_data(0x0084, 0x0000)
        self.cmd_data(0x0085, 0x0000)

        # ************* Panel Control *************
        self.cmd_data(0x0090, 0x0010)
        self.cmd_data(0x0092, 0x0000)
        self.cmd_data(0x0007, 0x0133)  # 262K color and display ON

Many settings, here from page 50 you can read more about it.

Time to test all this stuff. New file main.py with import and TFT instance:

import RPi.GPIO as GPIO
from screen import TFT


LED = 6
GPIO.output(LED, 1)

s = TFT()

Remember pin named LED_A? It is connected to G6 and we need to init it manually.
Run a script and… it works! We have a nice initialized screen:P But lets display some pixels!

Setting a pixel

To draw something we select an area to work at and send a color. An internal pointer is incremented so if we write two commands it will toggle two pixels. When we reach the end of the row in selected area it moves to next row, and so.
To display a single pixel we need to select small area, 1×1, and send a color. Another look into documentation combined with C code and we have two new functions in TFT class:

    def _set_area(self, x1, y1, x2, y2):
        """select area to work with"""
        self.cmd_data(0x0020, x1)
        self.cmd_data(0x0021, y1)
        self.cmd_data(0x0050, x1)
        self.cmd_data(0x0052, y1)
        self.cmd_data(0x0051, x2)
        self.cmd_data(0x0053, y2)

    def draw_pixel(self, x, y):
        """draw one pixel"""
        self._set_area(x, y, x, y)

Quick test:

s = TFT()

s.draw_pixel(100, 100)
s.draw_pixel(160, 100)

And here they are! Two little shiny pixels! Almost invisible 😦 we need more!

for i in range(0, 11):
    s.draw_pixel(100 + i, 100)
    s.draw_pixel(100 + i, 110)
    s.draw_pixel(100, 100 + i)
    s.draw_pixel(110, 100 + i)

    s.draw_pixel(160 + i, 100)

This is big enough!


Yey! A little success. Two pixels, one screen, zero burned boards 🙂 Initializing screen was hard, without borrowed code it would take ages for me.
Next time we will do some more drawing, filling and checking speed. I’m quite afraid about speed. Why? Because we are using python GPIO implementation, id it fast enough? If not then we can try and refresh only required area (similar to buffered CharLCD). Or write a C extension to Python? Or only C TFT driver?
Or… Stop!



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s