Learn

Pong on a color TFT: an ILI9341 on an Arduino Uno

Advanced

Or see every lesson.

  • 261parts on the bench
  • 31boards running now
  • 1.00xreal time, on every board
Arduino Uno: Pong on an ILI9341live0.000 s 0.00x
Press the thumb stick to serve, then push it left and right
The Pong console example on a simulated ATmega328P. Press the stick to serve.

The ILI9341 is the chip behind the common 2.4 and 2.8 inch color TFT screens, 240 by 320 pixels. It is a big step up from a small OLED. You get 65,536 colors and room for a real game or a dashboard. It talks to the Arduino over SPI with a few extra control pins.

The circuit above is an Arduino Uno playing Pong on an ILI9341, with a thumb stick to move the paddle and a piezo for the beeps. Press the stick to serve. It runs in your browser on the sketch below, and the way it draws teaches the most important thing about these screens: only redraw what moved.

What you need

  • An Arduino Uno
  • A 2.4 inch ILI9341 SPI TFT module (240x320)
  • A thumb stick module and a piezo buzzer, for the game
  • A full-size breadboard and jumper wires

Wiring

Part and pinGoes toNote
TFT CSPin 10Chip select
TFT DCPin 9LOW for a command, HIGH for data
TFT SCKPin 13SPI clock
TFT MOSIPin 11SPI data to the screen
TFT RST5 VTied high here; or wire it to a pin and name it in the sketch
TFT LED5 VThe backlight; without it the screen stays dark
TFT VCC and GND5 V and GNDCheck your module: many want 3.3 V logic
Stick VRX, VRYA0, A1The two axes
Stick SWPin 2The press
PiezoPin 6 and GNDThe beeps

DC: command or data

The ILI9341 has one extra pin that makes it different from a plain SPI chip: DC, for data or command. When DC is LOW, the byte you send is a command, like "set the window" or "start writing pixels". When DC is HIGH, the bytes are the data for that command, including the pixels themselves.

Drawing a rectangle is three steps. Set the column range, set the row range, then stream the right number of pixel colors. Every library does exactly this under the hood.

Why the game only redraws what moved

Each pixel is 16 bits, and the whole screen is 76,800 pixels. That is a lot of data for an Uno. In Mokxi the Uno model has no SPI hardware, so every bit is sent by hand, and clearing the whole screen takes about a second and a half. A real Uno’s hardware SPI is faster, but a full redraw is still too slow for a smooth game.

So the sketch draws the frame, the labels and the score once. After that, each frame only repaints the ball and the strip of paddle that moved: under two thousand pixels, which keeps the game at 30 frames a second. The game also lives in a 160 by 200 window in the middle of the screen, which makes the one-time clear shorter.

This habit, draw once and then update only what changed, is the key to any fast display project, on any board.

Drawing the fixed parts once, from the Pong console example
static void drawFrame() {
  tft.fillRect(FX, FY, FW, FH, BG);
  tft.rect(FX - 2, FY - 2, FW + 4, FH + 4, EDGE, 2);
  tft.text(FX, FY - 26, "PONG", ILI9341_WHITE, ILI9341_BLACK, 3);
  tft.text(FX, FY + FH + 10, "SCORE", ILI9341_WHITE, ILI9341_BLACK, 2);
  tft.text(FX + 110, FY + FH + 10, "LIVES", ILI9341_WHITE, ILI9341_BLACK, 2);
}

Using the Adafruit library

The example uses mokxi_ili9341.h, a small driver that ships with Mokxi. On a real Uno, most tutorials use the Adafruit_ILI9341 and Adafruit_GFX libraries, and Mokxi ships headers with those names and their usual calls too. The hello world below is written the way those tutorials write it.

Mokxi’s version has a few differences. The built-in font is Mokxi’s own 5x8, and the Fonts/ headers are Mokxi’s letters at each font’s size. Nothing is read back from the screen. And the bus is software SPI, so draw what moved rather than filling the whole screen often.

Hello world with the Adafruit ILI9341 library
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>

Adafruit_ILI9341 tft(10, 9);  // CS, DC; add a third pin for RST if it is wired

void setup() {
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextColor(ILI9341_WHITE);
  tft.setCursor(10, 10);
  tft.print("Hello");
}

void loop() {
}

Try it in the editor

Change BALL_COLOUR to ILI9341_RED and the ball turns red. Colors are 16-bit RGB565 numbers, so you can also write your own, like 0xF81F for magenta.

Make the paddle wider by raising PADDLE_W from 36 to 60, and the game gets easier. Make the window bigger by changing FW and FH, and watch the first clear take longer.

Common mistakes

A white or dark screen. Check the LED pin: it is the backlight, and the screen stays dark without it. A white screen with the backlight on usually means the init never ran: check CS, DC and RST.

5 V logic on a 3.3 V screen. Many ILI9341 modules are 3.3 V parts. Some have a level shifter on the board and some do not. Check yours before you wire it to an Uno, and use level shifters if needed.

Clearing the whole screen every frame. It flickers and crawls. Draw the background once and repaint only what changed.

Mixing up DC and CS in the constructor. The order is CS first, then DC.

Questions

Which pins does an ILI9341 use on an Arduino Uno?

SCK to 13 and MOSI to 11 (the Uno’s SPI pins), CS and DC to any two pins you name in the sketch, RST to a pin or to the supply, and LED to the supply for the backlight.

Why is my ILI9341 so slow on an Uno?

A full screen is 76,800 pixels of 16 bits each. Redraw only what changes. In Mokxi the Uno’s SPI is done in software, so it is slower still, and the Pong example shows how to work within that.

Does Mokxi support the touch version?

Yes. There is an ILI9341 with a touch panel in the parts bin, read with the XPT2046_Touchscreen library, and a Touch paint project that uses it.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.