How to display a waveform on a 2.4 inch 240x320 TFT display?
Hardware Setup and Signal Chain
The display module connects to a microcontroller like an ESP32 or STM32 via 4-wire SPI (MISO, MOSI, SCK, CS) plus a DC pin for data/command control and a RESET pin. The ILI9341 driver inside the 2.4 inch 240x320 tft display supports a 240x320 pixel resolution with 262K colors, but for waveform drawing, you’ll likely use 16-bit color (RGB565) to save RAM. The display’s pixel clock can run at 40 MHz, meaning each pixel takes 25 ns to transfer. If you’re drawing a 240-pixel-wide waveform, that’s 6 microseconds per horizontal line, ignoring overhead. The ADC on a typical ESP32 has 12-bit resolution (0-4095) and a sampling rate up to 200 ksps (kilosamples per second) in continuous mode, but for audio, you’d cap it at 40 ksps to avoid aliasing. The analog input signal, say from a function generator or microphone preamp, needs to be biased to 1.65V (half of 3.3V) for a 0-3.3V range, using a voltage divider and a capacitor for DC blocking. The signal’s amplitude should be scaled so the peak-to-peak voltage doesn’t exceed 3.3V, or you’ll clip the ADC. For a 2V p-p signal, you’d use a non-inverting op-amp with a gain of 1.65 to hit the full ADC range.
Software Architecture for Real-Time Waveform
The core loop reads the ADC, stores samples in a circular buffer of 256 or 512 bytes, and then maps each sample to a y-coordinate on the display. The display’s y-axis has 320 pixels, so you scale the ADC value (0-4095) to 0-319 using integer math: y = (sample * 320) / 4096. But you’ll want to center the waveform vertically, so offset the y by 160 and invert it (since screen y=0 is top). For a 240-pixel-wide display, you need 240 samples per frame. At a 40 kHz sample rate, that’s 6 ms per frame, giving 166 Hz refresh—well above the 60 Hz flicker threshold. The TFT_eSPI library, written by Bodmer, is optimized for the ILI9341 and can push pixels at 40 MHz SPI, with a frame buffer of 153,600 bytes (240x320x2 bytes per pixel) if you use double buffering. But double buffering eats RAM—on an ESP32 with 520 KB SRAM, it’s tight, so most projects use a single buffer and clear the old waveform by drawing a background-colored line before drawing the new one. The library’s drawPixel() function takes about 2.5 microseconds per call, so drawing 240 pixels takes 600 microseconds, leaving 5.4 ms for ADC sampling and other tasks.
Data Density: Timing and Buffer Trade-offs
Let’s table the key parameters for a typical waveform display system:
| Parameter | Value | Notes |
|---|---|---|
| Display resolution | 240 x 320 | Landscape mode uses 320 wide, 240 tall |
| ADC resolution | 12-bit (0-4095) | ESP32 internal ADC, 200 ksps max |
| Sample rate | 40 kHz | For audio, limited by ADC stability |
| Buffer size | 256 samples | Circular buffer, 512 bytes RAM |
| Frame rate | 166 Hz | 240 samples at 40 kHz |
| SPI clock | 40 MHz | ILI9341 max, 25 ns per pixel |
| Pixel draw time | 2.5 µs | TFT_eSPI drawPixel() overhead |
| Total frame draw time | 600 µs | 240 pixels, no fill |
| RAM for frame buffer | 153.6 KB | 240x320x2 bytes, 16-bit color |
The 2.4 inch 240x320 tft display’s 240x320 resolution means you have 76,800 pixels total. For a waveform, you only update 240 pixels per frame, so the rest of the screen can show a grid or static UI. The grid lines, drawn once at startup, use about 1,200 pixels (horizontal and vertical lines every 10 pixels), which takes 3 ms to draw using fillRect() for speed. The ADC sampling jitter on the ESP32 is about ±5% due to internal noise, so you’ll need to average 4 samples per point to get clean waveform data—that drops your effective sample rate to 10 kHz, but for a 1 kHz sine wave, you still get 10 samples per cycle, which is fine for visual display. If you use an external ADC like the ADS1115 (16-bit, 860 sps), you lose speed but gain precision—good for low-frequency signals like ECG.
Drawing the Waveform: Line vs. Pixel Methods
You have two main approaches: draw individual pixels or draw lines between points. For a 240-pixel-wide waveform, drawing pixels with drawPixel() gives a scatter plot, which looks noisy unless you use a thick line. The TFT_eSPI library’s drawLine() function uses Bresenham’s algorithm and takes about 4 microseconds per line, so drawing 239 lines (between 240 points) takes 956 microseconds—still under 1 ms. But if you’re drawing a fast-changing waveform, the line method can create artifacts when the signal jumps sharply. A better approach is to use a vertical scan: for each x pixel, read the ADC and draw a vertical line from the center y to the sample y. This gives a solid waveform trace and uses fillRect() for speed—fillRect() on a 1-pixel-wide column takes 2 microseconds, so 240 columns take 480 microseconds. The trade-off is that you need to clear the previous column before drawing the new one, which doubles the time to 960 microseconds per frame. At 166 Hz, that’s 15.9% of the CPU time, leaving 84% for other tasks like FFT or data logging.
Color and Contrast for Readability
The 2.4 inch 240x320 tft display uses 16-bit RGB565, which gives 65,536 colors. For waveform visibility, use a bright color like green (0x07E0) or cyan (0x07FF) on a black background (0x0000). The display’s contrast ratio is typically 500:1, and the viewing angle is 120 degrees, so you can see the waveform from the side. If you’re using a grid, make it dim gray (0x8410) so it doesn’t distract. The backlight draws 20-40 mA at 3.3V, so total power consumption is around 130-200 mW for the display alone. For a battery-powered scope, you can dim the backlight via PWM to 50% duty cycle, cutting power to 100 mW while still being readable indoors.
Handling Signal Frequency and Aliasing
If you’re displaying a 10 kHz square wave on a 40 kHz sample rate, you’ll get 4 samples per cycle, which shows the rising edge as a staircase. To avoid aliasing, you need an anti-aliasing filter—a simple RC low-pass with a cutoff at 20 kHz (half the sample rate) works. Use a 1k resistor and 8 nF capacitor for a 20 kHz cutoff. The filter’s phase shift at 10 kHz is about 45 degrees, which shifts the waveform horizontally by 1.25 pixels at 240 pixels width—negligible. For higher frequencies, like 100 kHz, you’d need a faster ADC and a display with higher refresh, but the 2.4 inch 240x320 tft display’s 166 Hz frame rate limits you to signals below 83 Hz by the Nyquist criterion if you want to show one cycle. For a 100 kHz signal, you’d need a sample-and-hold circuit and a different display technology.
Memory and Performance Optimization
The ESP32’s 520 KB SRAM is shared between the waveform buffer, the display buffer, and the stack. If you use double buffering, you need 153.6 KB for the frame buffer, plus 512 bytes for the sample buffer, plus 10 KB for the library, totaling 164 KB—leaving 356 KB for other tasks. But double buffering eliminates tearing, where the top half of the screen shows the old waveform and the bottom half shows the new one. Tearing is visible at 60 Hz, so for a 166 Hz refresh, it’s less noticeable. Single buffering with a dirty rectangle update (only redrawing the changed x columns) reduces RAM usage to 0 bytes for the frame buffer but increases CPU time. The TFT_eSPI library’s pushImage() function can update a 240x1 pixel strip in 240 microseconds, which is faster than 240 drawPixel() calls. You can store the previous waveform y values in an array of 240 ints (480 bytes) and compare them to the new values—only redraw columns where they differ. This cuts the draw time by 50% if the signal is stable.
Practical Example: Audio Waveform Display
For an audio waveform from a microphone, the signal amplitude is around 100 mV p-p, so you need a preamp with a gain of 33 to hit 3.3V. Use an LM358 op-amp with a 100k feedback resistor and 3k input resistor. The ADC on the ESP32 has a 1 dB nonlinearity at the top end, so calibrate with a 1.65V reference. The sample rate for audio is 22.05 kHz to cover 10 kHz bandwidth, giving 92 samples per frame at 240 pixels width—you’ll need to interpolate to fill the gaps. Linear interpolation between samples adds 2 microseconds per point, so total draw time is 480 microseconds plus 480 microseconds for interpolation, totaling 960 microseconds. At 22.05 kHz, the frame rate is 92 Hz, which is above the 60 Hz flicker threshold. The display’s 2.4 inch diagonal means the waveform is 1.44 inches wide (36.6 mm), and each pixel is 0.15 mm, so you can see fine details like glitches in the audio signal.
Grid and Triggering for Precision
To make the waveform useful, add a grid with 10 divisions horizontally and 8 vertically. Each division is 24 pixels wide and 40 pixels tall. The grid lines are drawn using drawFastHLine() and drawFastVLine() which take 1 microsecond per line. Draw the grid once at startup, then only update the waveform area. For triggering, you need to detect when the signal crosses a threshold voltage, say 1.65V (center of the ADC range). Use a comparator in software: when the ADC value crosses 2048 (half of 4096), start storing samples. The trigger level can be set via a potentiometer on an analog pin, read every 10 ms. The trigger hold-off time is 1 ms to avoid re-triggering on the same edge. This gives a stable waveform display even for noisy signals.
Component Selection and Cost
The 2.4 inch 240x320 tft display costs around $8-12 in single quantities, with the ILI9341 driver being the most common. The ESP32 module adds $3-5, and the op-amp and passives add $1. Total BOM cost is under $20 for a basic waveform scope. The display’s SPI interface uses 4 pins plus power, so it’s compatible with any 3.3V microcontroller. The display’s refresh rate is 60 Hz when using the internal frame buffer, but with direct SPI writes, you can push 166 Hz as shown. The only downside is the 240-pixel horizontal resolution—for a 10 MHz signal, you’d need a 10 ns sample period, which is impossible with the ESP32’s ADC. But for audio and low-frequency signals, it’s more than adequate.
Alternative Approaches and Trade-offs
If you need higher resolution, you can use the display in portrait mode (320 wide, 240 tall) to get 320 samples per frame, but the waveform becomes squished vertically. The 2.4 inch size means the pixel density is 167 PPI, so text and grids are readable. For a faster refresh, use an STM32 with a 72 MHz SPI clock and a parallel interface (8080 mode) to push pixels at 8 MHz, cutting draw time to 30 microseconds per frame. But the parallel interface uses 8-16 data pins, which eats GPIO. The SPI interface is simpler and works with any board. The TFT_eSPI library supports DMA on the ESP32, which offloads pixel transfer to the SPI controller, reducing CPU load to 0.5% during frame updates. This lets you run FFT on the signal in the background.
Real-World Testing and Calibration
In practice, the display’s color accuracy varies by 10% between units, so calibrate the background color to pure black by setting the display’s gamma curve via SPI commands. The ILI9341 has a gamma correction register that you can write to adjust the brightness of each color channel. The waveform’s vertical scale is calibrated by measuring a known voltage, say 1V from a reference, and adjusting the ADC mapping. The horizontal scale is set by the sample rate—if you sample at 40 kHz, each pixel represents 25 microseconds. Display this as a time axis label on the screen using the TFT_eSPI’s setCursor() and print() functions, which take 50 microseconds per character. A 10-character label adds 500 microseconds, so do it once per frame or less.
Got a lot that needs work before the next tenant turn?
Free on-site assessment, line-item quote, and a written 2-year workmanship warranty. Self-performed crews, no subs.