TFT Display

This commit is contained in:
ImplFerris 2025-07-19 07:51:33 +05:30
parent edd47c94f5
commit f4867e6434
9 changed files with 424 additions and 2 deletions

View File

@ -27,6 +27,7 @@
- [Fading LED](./led/index.md)
- [Code](./led/code.md)
- [External LED](./led/external-led.md)
<!-- - [Buttons](./buttons/index.md) -->
- [Embassy](./embassy/index.md)
- [Blinky with Embassy](./embassy/blinky-with-embassy.md)
- [Buzzer](./buzzer/index.md)
@ -150,5 +151,7 @@
- [API](./e-ink/weather-station/weather-api.md)
- [Dashboard](./e-ink/weather-station/dashboard.md)
- [Fun](./e-ink/weather-station/main.md)
<!-- - [TFT Display](./tft-display/index.md) -->
- [TFT Display](./tft-display/index.md)
- [Circuit](./tft-display/circuit.md)
- [Write Text](./tft-display/draw-text.md)
- [Projects](./projects.md)

2
src/buttons/index.md Normal file
View File

@ -0,0 +1,2 @@
# Buttons
//TODO

View File

@ -4,7 +4,6 @@ Async programming allows tasks to run concurrently without blocking each other.
## Embassy
So far, we have worked with code that runs in blocking mode. This means that whenever we ask the program to do something like `delay` for a while or wait for a button press, the CPU stops and waits until that task is finished before continuing. This is simple to understand and works well for small programs, but it becomes limiting when we want to handle multiple tasks at the same time, like reading a sensor, and listening for input; all without blocking each other.
This is where [Embassy](https://github.com/embassy-rs/embassy) comes in. Embassy is an async runtime designed for embedded systems. It allows us to write non-blocking code using Rust's async and await features. Instead of waiting and wasting CPU time, tasks can pause and let others run, making better use of the processor and enabling more responsive and power-efficient applications. It can be used it with ESP32, Pico and other microcontrollers.

128
src/tft-display/circuit.md Normal file
View File

@ -0,0 +1,128 @@
# Connect ESP32 with TFT Display
In this section, we will look at the pinout of the TFT display module and see how to connect it to the ESP32. If you notice carefully, it is very similar to the e-ink display module. That's because both displays use SPI (Serial Peripheral Interface) for communication, and they share common control lines like CS, DC, RESET, MOSI, and SCK. This means that once you understand how to wire and drive one SPI-based display, switching to another becomes much easier.
## TFT Display Pinout
<img style="display: block; margin: auto;" src="./images/tft-display-pinout.png" alt="tft display pinout"/>
- **VCC**: Power supply (3.3V or 5V). This is the main power supply for the entire display module. It powers the display controller (e.g., ILI9341) and other parts.
- **GND**: Ground
- **CS**: Chip Select. This tells the display when it should listen to SPI commands. Keep it low (active) when sending data.
- **RESET**: Resets the display. Useful during startup to make sure the display starts in a known state.
- **DC**: Data/Command control pin. Set high to send data, low to send commands. Used to switch between writing commands and pixel data.
- **SDI (MOSI)**: Master Out Slave In. This is the SPI data line from the microcontroller to the display. Used to send pixel data and commands.
- **SCK**: Serial Clock. SPI clock signal from the microcontroller. It synchronizes the data being sent.
- **LED**: This pin is specifically for the backlight (LED panel) of the display. It controls how bright the screen appears. If you want the backlight always on, just connect this to 3.3V. If you want to control brightness (e.g., dim or turn off), you can connect it to a PWM-capable GPIO pin from your microcontroller.
- **SDO (MISO)**: Master In Slave Out. SPI read line from the display to the microcontroller. Not always used, but some displays support reading display memory or status.
### Touch Controller Pinout
If your TFT display includes a resistive touchscreen, it likely uses a touch controller chip like the **XPT2046**. These touch pins are separate from the display pins and are also connected over SPI. You only need to connect these pins if you want to use the touch functionality.
- **T_CLK**: Touch screen SPI bus clock pin
- **T_CS**: Touch screen chip select control pin
- **T_DIN**: Touch screen SPI write data pin (MOSI)
- **T_DO**: Touch screen SPI read data pin (MISO)
- **T_IRQ**: Touch screen interrupt detection pin
If you do not need touch input or your module does not have a touch panel, you can leave these pins unconnected.
## Connecting e-Paper display with ESP32
<img style="display: block; margin: auto;" src="./images/esp32-tft-display.png" alt="tft display with esp32"/>
<br/>
<table style="margin-bottom:20px">
<thead>
<tr>
<th>TFT Display Pin</th>
<th style="width: 250px; margin: 0 auto;">Wire</th>
<th>ESP32 Pin</th>
</tr>
</thead>
<tbody>
<tr>
<td>VCC</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire red" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>Vin</td>
</tr>
<tr>
<td>GND</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire black" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GND</td>
</tr>
<tr>
<td>CS</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire white" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GPIO15</td>
</tr>
<tr>
<td>RESET</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire brown" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GPIO4</td>
</tr>
<tr>
<td>DC</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire purple" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GPIO2</td>
</tr>
<tr>
<td>SDI (MOSI)</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire green" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GPIO23</td>
</tr>
<tr>
<td>SCK (CLK)</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire blue" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>GPIO18</td>
</tr>
<tr>
<td>LED</td>
<td style="text-align: center; vertical-align: middle; padding: 0;">
<div class="wire orange" style="width: 200px; margin: 0 auto;">
<div class="male-left"></div>
<div class="male-right"></div>
</div>
</td>
<td>3.3V (or PWM pin for brightness control)</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,244 @@
# Write Text on TFT Display using ESP32
Let's create a simple program to draw text on the display module using the [ili9341](https://docs.rs/ili9341/0.6.0/ili9341/) crate.
## Generate project using esp-generate
To create the project, use the `esp-generate` command. Run the following:
```sh
esp-generate --chip esp32 tft-display-hello
```
This will open a screen asking you to select options.
- First, select the option "Enable unstable HAL features."
Just save it by pressing "s" in the keyboard.
## Update cargo.toml
```toml
embedded-hal-bus = { version = "0.1" }
display-interface-spi = "0.5"
ili9341 = "0.6.0"
embedded-graphics = "0.8.1"
profont = "0.7.0"
```
By now, you should be familiar with embedded-hal-bus and the embedded-graphics crates. The embedded-hal crate provides standardized interfaces (like SPI, I2C) for microcontroller peripherals, letting developers write reusable drivers that work across any compatible hardware. Basically, we will use this to convert the SpiBus provided by esp-hal into the SpiDevice.
However, unlike in previous chapters where we used SpiDevice directly, the ili9341 crate requires one more layer: it expects an interface that implements traits from the display-interface-spi crate. This crate defines traits and wrappers that bridge SPI bus drivers with display drivers by handling details like the data/command (DC) pin internally.
We use the profont crate to get a larger monospace font for our display, since the built-in embedded-graphics fonts are too small.
We will use embedded-graphics crate to render text, shapes, and images on the display.
## Required imports
```rust
// Usual imports
use defmt::info;
use esp_hal::clock::CpuClock;
use esp_hal::main;
use esp_hal::time::{Duration, Instant};
use esp_println as _;
// Embedded Grpahics related
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::text::{Baseline, Text};
// Larger font
use profont::{PROFONT_18_POINT, PROFONT_24_POINT};
// ESP32 SPI + Display Driver bridge
use display_interface_spi::SPIInterface;
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::delay::Delay;
use esp_hal::spi::master::Config as SpiConfig;
use esp_hal::spi::master::Spi;
use esp_hal::spi::Mode as SpiMode;
use esp_hal::time::Rate; // For specifying SPI frequency
use ili9341::{DisplaySize240x320, Ili9341, Orientation};
// For managing GPIO state
use esp_hal::gpio::{Level, Output, OutputConfig};
```
## SPI Setup
Let's initialize the SPI device for communication between the ESP32 and the display. This follows the usual setup: first, we initialize the SPI bus and then convert it into an SPI device using embedded-hal-bus.
```rust
// Initialize SPI
let spi = Spi::new(
peripherals.SPI2,
SpiConfig::default()
.with_frequency(Rate::from_mhz(4))
.with_mode(SpiMode::_0),
)
.unwrap()
//CLK
.with_sck(peripherals.GPIO18)
//DIN
.with_mosi(peripherals.GPIO23);
let cs = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default());
let dc = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
let reset = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
let spi_dev = ExclusiveDevice::new_no_delay(spi, cs);
let interface = SPIInterface::new(spi_dev, dc);
```
This time, we've added one more step: we create an SPIInterface using the display-interface-spi crate. This interface combines the SPI device and the data/command (DC) pin into a single abstraction. It simplifies communication by handling how commands and data are sent over SPI. We will pass this interface to the TFT display driver.
## Initialize the display
To initialize the display, we pass the SPI Interface, reset pin, delay, orientation, and screen size to the Ili9341 driver. This sets up everything the driver needs to start working with the display.
```rust
let reset = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
let mut display = Ili9341::new(
interface,
reset,
&mut Delay::new(),
Orientation::Portrait,
DisplaySize240x320,
)
.unwrap();
```
We set the orientation to portrait, which means the display is treated as 240 pixels wide and 320 pixels tall. The display size is set to 240 by 320 pixels to match the screen's resolution. Together, these settings help the driver draw content correctly based on the shape and size of the display.
## Clear Display
Let's clear the display by filling its background with white. Since the TFT is a color display, we use the Rgb565 color format, which represents 16-bit color values (5 bits red, 6 bits green, 5 bits blue). This is the first time we are using Rgb565; until now, we have only worked with monochrome displays.
```rust
display.clear(Rgb565::WHITE).unwrap();
```
## Write Text
Now, let's finally display the text "impl Rust for ESP32" on the screen. We will write the two parts separately using different font sizes and colors.
```rust
let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Rgb565::RED);
Text::with_baseline("impl Rust", Point::new(50, 150), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Rgb565::CSS_DIM_GRAY);
Text::with_baseline("for ESP32", Point::new(60, 180), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
```
We draw the first line, "impl Rust", using a red font, positioned 50 pixels from the left edge and 150 pixels down from the top of the screen. The second line, "for ESP32", is placed just below it, 60 pixels from the left and 180 pixels from the top. If you are writing different text, feel free to adjust the coordinates to achieve the alignment and spacing that looks best.
## Clone the existing project
You can clone (or refer) project I created and navigate to the `tft-display-hello` folder.
```sh
git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/tft-display-hello/
```
## The Full code
```rust
#![no_std]
#![no_main]
// Usual imports
use defmt::info;
use esp_hal::clock::CpuClock;
use esp_hal::main;
use esp_hal::time::{Duration, Instant};
use esp_println as _;
// Embedded Grpahics related
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::text::{Baseline, Text};
// Larger font
use profont::{PROFONT_18_POINT, PROFONT_24_POINT};
// ESP32 SPI + Display Driver bridge
use display_interface_spi::SPIInterface;
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::delay::Delay;
use esp_hal::spi::master::Config as SpiConfig;
use esp_hal::spi::master::Spi;
use esp_hal::spi::Mode as SpiMode;
use esp_hal::time::Rate; // For specifying SPI frequency
use ili9341::{DisplaySize240x320, Ili9341, Orientation};
// For managing GPIO state
use esp_hal::gpio::{Level, Output, OutputConfig};
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[main]
fn main() -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// Initialize SPI
let spi = Spi::new(
peripherals.SPI2,
SpiConfig::default()
.with_frequency(Rate::from_mhz(4))
.with_mode(SpiMode::_0),
)
.unwrap()
//CLK
.with_sck(peripherals.GPIO18)
//DIN
.with_mosi(peripherals.GPIO23);
let cs = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default());
let dc = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
let reset = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
let spi_dev = ExclusiveDevice::new_no_delay(spi, cs);
let interface = SPIInterface::new(spi_dev, dc);
let mut display = Ili9341::new(
interface,
reset,
&mut Delay::new(),
Orientation::Portrait,
DisplaySize240x320,
)
.unwrap();
display.clear(Rgb565::WHITE).unwrap();
let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Rgb565::RED);
Text::with_baseline("impl Rust", Point::new(50, 150), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Rgb565::CSS_DIM_GRAY);
Text::with_baseline("for ESP32", Point::new(60, 180), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
loop {
info!("Hello world!");
let delay_start = Instant::now();
while delay_start.elapsed() < Duration::from_millis(5000) {}
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

46
src/tft-display/index.md Normal file
View File

@ -0,0 +1,46 @@
# TFT Display
In this chapter, we will learn how to work with a TFT Display. TFT stands for Thin-Film Transistor, which is a type of LCD (Liquid Crystal Display) that gives us vibrant colors, clear images, and fast refresh rates. Compared to simple text-based displays like 16x2 LCDs or monochrome OLEDs, TFT displays allow us to show full-color graphics, shapes, images, and even touch-based user interfaces.
![2.8 Inch TFT Display](./images/tft-display-2.8-spi.png)
## What Is a TFT Display?
A TFT display is made up of thousands of tiny pixels arranged in a grid. Each pixel can show different colors, allowing us to draw anything we want on the screen. The display includes a controller chip to understand the data coming from our microcontroller and to refresh the screen accordingly.
TFT displays are very common in mobile phones, smartwatches, and handheld gadgets. In embedded systems, they are used to build dashboards, games, control panels, and more.
## Varieties of TFT Displays
TFT modules come in different sizes, resolutions, and communication types. Here are a few common differences:
- Size: Ranges from 1.3 inches to 7 inches or more.
- Resolution: From 128×160 to 800×480 and beyond.
- Interface Type: TFT displays offer different ways to connect to a microcontroller. The SPI interface is simple and uses fewer pins, but it is slower. The parallel interface is faster but needs more pins. Bigger displays often use an RGB (TTL) interface, which is very fast but needs a special controller to work.
## Our Display: 2.8-Inch SPI Touchscreen
In this project, we will use a 2.8-inch TFT display module with a resolution of 240×320 pixels. It uses the SPI interface, which allows us to connect it to a microcontroller using just a few pins.
This module also includes a resistive touchscreen, which lets us detect touches by measuring pressure on the screen. We will focus first on drawing to the display.
## Display Driver (Controller Chip)
Every TFT display has a controller chip that takes care of drawing pixels on the screen. In our case, the display uses the ILI9341 chip. We send commands to this chip, and it handles tasks like updating the screen, drawing shapes, filling colors, and more.
The ILI9341 controller is most commonly used in 2.4" and 2.8" TFT displays, and sometimes in 3.2" displays. If your screen uses the same ILI9341 chip, it will work with the same code and examples in this tutorial.
You can also use displays with other driver chips (like ST7735, ILI9225, ILI9486), but those will need different Rust crates or setup instructions. So, it's important to check the controller chip used in your display before buying, to make sure it's compatible with the tutorial.
## Touch Controller
Some TFT displays (like the one we're using here) also come with a resistive touchscreen on top of the screen. In these displays, touch functionality is handled by a separate chip called XPT2046.
This chip reads the X and Y position when you touch the screen and sends that data over SPI. It works independently from the display driver (ILI9341).
## Datasheet
- [Datasheet for the ILI9341](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf).
- [Datasheet for the XPT2046](https://grobotronics.com/images/datasheets/xpt2046-datasheet.pdf)