From the Advisory Desk — Al-Ershaad Consultancy
How to display sensor data on a 2.4 inch 240x320 screen?
You connect a 2.4 inch 240x320 ips display to a microcontroller like an ESP32 or STM32, then write firmware that reads sensor data from I2C or SPI buses and updates the screen buffer at a refresh rate that matches your application. The display itself uses a parallel 8-bit interface or SPI, with the ILI9341 or ST7789 driver being the most common chipsets. For a typical temperature and humidity sensor like the DHT22, you poll the sensor every 2 seconds, convert the raw 16-bit data into Celsius and percentage, then draw the text using a font library like Adafruit GFX. The screen’s 240x320 resolution gives you 76,800 pixels, which at 16-bit color depth means 153,600 bytes of frame buffer if you use double buffering. On an ESP32 with 520KB SRAM, that’s fine, but on an Arduino Uno with 2KB, you’d need to use the display’s internal memory and write directly to it. The refresh rate over SPI at 40MHz clock is about 30 frames per second for full-screen updates, but if you only update text regions, you can push 60 fps. For real-time data like accelerometer readings from an MPU6050, you’d use the FIFO buffer to batch 6-axis data at 1kHz, then plot it as a scrolling waveform using a 320-pixel-wide canvas. The display’s backlight is typically driven by a PWM pin, so you can dim it to 50% duty cycle to save power in battery applications. The 2.4 inch 240x320 ips display module we’re talking about uses a 4-wire SPI interface, which means you only need four GPIO pins plus a chip select and reset, leaving the rest of your microcontroller pins for sensors. The IPS technology gives you 178-degree viewing angles, which matters if the display is mounted in a moving device like a drone or a wearable. For sensor data that changes slowly, like soil moisture, you can use partial updates to redraw only the numeric value, reducing SPI traffic by 90%. The display’s controller supports windowed addressing, so you can define a 50x20 pixel rectangle for the temperature value and update that region in 1.2 milliseconds at 40MHz SPI. If you’re using a Raspberry Pi, you’d use the hardware SPI with a 64MHz clock and a Python library like luma.lcd, which gives you 60 fps for a 240x320 frame buffer. The key is to match the sensor’s output rate to the display’s update rate. For a BME280 sensor, the maximum sampling rate is 3.4Hz for humidity, 1.3Hz for pressure, and 0.5Hz for temperature in ultra-high resolution mode, so you can update the display every 300 milliseconds without data loss. The display’s power consumption at full brightness is about 80mA at 3.3V, so a 1200mAh LiPo battery gives you 15 hours of continuous operation. If you use a deep sleep mode on the ESP32 and wake every 10 seconds to read the sensor and update the display, you can extend battery life to 30 days. The SPI bus must be shared carefully between the display and the sensor if they use the same bus. For example, the ILI9341 uses SPI mode 0, while the MPU6050 uses I2C, so they don’t conflict. But if you have an SPI sensor like the MAX31855 thermocouple amplifier, you need separate chip select lines and ensure the display’s CS is pulled high during sensor reads. The display’s resolution is 240 pixels wide, which is perfect for a 12-character font at 20 pixels per character, giving you 20 characters per line. With 320 pixels tall, you can fit 16 lines of 20-pixel-tall text, or 10 lines of 32-pixel-tall text. For a dashboard showing temperature, humidity, pressure, and altitude, you can use four lines of 32-pixel text, with the remaining space for a graph. The graph can be a 240x128 pixel area, which at 16-bit color uses 61,440 bytes of frame buffer. If you draw a scrolling line graph of the last 240 samples, each sample is one pixel wide, so you can store 240 data points in a circular buffer. For a temperature sensor with 0.1°C resolution, you map the range -10°C to 50°C to 128 pixels, giving you 0.47°C per pixel, which is good enough for visual trends. The display controller supports hardware acceleration for drawing rectangles, lines, and circles, so you can use the ILI9341’s built-in drawing commands to draw a gauge without sending pixel data. For a circular gauge, you use the drawCircle command with a radius of 100 pixels, then draw a filled arc using the fillRect command in a wedge pattern. The gauge’s needle can be a line drawn from the center to the edge, rotated by the sensor value. The rotation calculation uses sin() and cos() in floating point, which on an ESP32 takes about 2 microseconds, so you can update the gauge at 500 Hz if needed. The display’s SPI interface is 3.3V logic, so if you’re using a 5V Arduino, you need a level shifter. The 2.4 inch 240x320 ips display module we’re using has a built-in 3.3V regulator, but the logic pins are still 3.3V tolerant. For the backlight, you can use a transistor to switch it on and off, or use a PWM pin for brightness control. The backlight’s forward voltage is 3.2V at 20mA, so a 100 ohm resistor in series with a 3.3V supply gives 20mA, which is the maximum brightness. If you dim it to 50% PWM, the average current drops to 10mA, saving power. The display’s response time is 25 milliseconds, so you won’t see ghosting even at 30 fps. For sensor data that updates faster than 30 fps, like a magnetometer running at 100 Hz, you can use a double buffer to avoid tearing. The double buffer uses two 153,600-byte buffers in the microcontroller’s RAM, which is fine on an ESP32 with 520KB, but on a Teensy 4.0 with 1024KB, you have plenty of room. The double buffer allows you to write the next frame while the display is showing the current frame, then swap buffers at the vertical blanking interval. The ILI9341 doesn’t have a hardware V-sync pin, so you need to use a timer interrupt to time the swap. On an ESP32, you can use the LEDC PWM timer to generate a 60 Hz interrupt, then swap buffers in the ISR. The interrupt service routine should be short, just swapping a pointer and setting a flag, so it doesn’t interfere with sensor reading. The sensor reading itself can be done in the main loop, using a non-blocking state machine. For example, for a DHT22, you send a start signal, wait 1 second, then read 40 bits of data. The 40 bits include 16 bits for humidity, 16 bits for temperature, and 8 bits for checksum. The checksum is the sum of the first four bytes modulo 256, so you can verify the data integrity. If the checksum fails, you discard the data and try again. The DHT22’s accuracy is ±0.5°C and ±2% RH, so you can display the temperature to one decimal place and humidity to one decimal place. The display’s font library, like Adafruit GFX, supports 5x7 pixel fonts, which are 7 pixels tall and 5 pixels wide, so you can fit 48 characters per line. But for readability, you’d use a 12x16 pixel font, which gives 20 characters per line and 20 lines per screen. The font data is stored in the microcontroller’s flash memory, which on an ESP32 is 4MB, so you can include multiple fonts. For a numeric display, you can use a 24x32 pixel font, which gives 10 characters per line and 10 lines per screen. The font rendering uses the drawChar function, which writes the pixel data to the frame buffer. The pixel data is 16-bit color, so each character takes 24x32x2 = 1536 bytes of frame buffer. If you update the entire screen with 10 lines of 10 characters, that’s 100 characters, or 153,600 bytes, which is the full frame buffer. So a full-screen update of text takes the same time as a full-screen image update. But if you only update the numeric value, you can use the setAddrWindow function to define a rectangle around the number, then write only the pixels in that rectangle. For a 24x32 pixel number, the rectangle is 24x32 pixels, which is 768 pixels, or 1536 bytes. At 40MHz SPI, that takes 1536 x 8 / 40e6 = 307 microseconds, plus the overhead of the SPI transaction, which is about 10 microseconds. So you can update the number at 3 kHz, which is much faster than the sensor’s update rate. The display’s controller also supports hardware scrolling, which is useful for a terminal-style display of sensor logs. The vertical scrolling uses the scroll address register, which you can set to start at a specific row. As you add new lines, the display scrolls the old lines up. The scrolling is done in hardware, so it doesn’t require any CPU time. The scroll area is defined by the top and bottom fixed lines, which you can set to zero for full-screen scrolling. The scroll speed is determined by how often you add new lines. For a sensor log, you add a new line every 10 seconds, so the scroll is smooth. The display’s resolution is 320 pixels tall, so with a 12x16 pixel font, you can fit 20 lines. After 20 lines, the oldest line scrolls off the top. The scroll register is 9 bits, so it can address 320 rows. The scroll value is the row number that becomes the first row of the display. So if you set the scroll value to 16, the display shows rows 16 to 319, then rows 0 to 15 at the bottom. This creates a seamless scrolling effect. For sensor data that needs to be displayed in a graph, you can use the hardware rectangle fill to draw a bar chart. The bar chart uses 240 pixels wide, so you can fit 24 bars of 10 pixels each, or 12 bars of 20 pixels each. Each bar’s height is proportional to the sensor value. For a temperature range of 0°C to 50°C, you map the value to a height of 0 to 200 pixels. The bar’s color can be green for normal, yellow for warning, and red for alarm. The color change is done by checking the value against thresholds. For example, if temperature > 35°C, the bar is red. The bar chart update is done by drawing a filled rectangle for the new value and erasing the old value with a background color rectangle. The background color is the same as the display’s background, which is typically black or white. The display’s contrast ratio is 1000:1, so black is very dark and white is very bright. For a black background, you use 0x0000 for black and 0xFFFF for white. The bar chart’s background is black, so you draw the bar in white, yellow, or red. The red color is 0xF800, which is 5 bits of red, 6 bits of green, and 5 bits of blue. The yellow color is 0xFFE0, which is maximum red and green. The green color is 0x07E0. The bar chart update takes about 2 milliseconds for a 10-pixel-wide bar, so you can update all 24 bars in 48 milliseconds, which is 20 fps. For a real-time graph, you can use the hardware line drawing to draw a line between consecutive data points. The line drawing uses the Bresenham algorithm, which is implemented in the ILI9341’s controller. The drawLine command takes two endpoints and a color. For a scrolling line graph, you shift the entire graph left by one pixel, then draw a new line from the previous point to the new point. The shifting is done by reading the frame buffer, shifting it, and writing it back, which is slow. Instead, you can use a circular buffer of pixel columns. You store the last 240 columns of pixel data in RAM, then write them to the display as a block. The block write uses the setAddrWindow function to define the entire graph area, then write 240x128 pixels in one SPI transaction. This takes 240x128x2 = 61,440 bytes, which at 40MHz SPI takes 12.3 milliseconds. So you can update the graph at 80 fps. The graph’s y-axis range is mapped to the sensor’s range. For a temperature sensor, you map 0°C to 50°C to 128 pixels, so each pixel is 0.39°C. The graph’s x-axis is time, with each pixel representing one sample. If you sample every second, the graph shows 240 seconds of data. If you sample every 10 seconds, it shows 2400 seconds, or 40 minutes. The graph’s grid lines can be drawn once at initialization, then never redrawn. The grid lines are horizontal lines at 10°C intervals, drawn in dark gray (0x8410). The grid lines don’t change, so they are part of the background. The graph’s data line is drawn in white (0xFFFF). The graph’s update is done by writing the new pixel column to the rightmost position, then shifting the column buffer. The column buffer is an array of 128 16-bit values, one for each pixel row. The new column is calculated by mapping the sensor value to a row index. For example, if the sensor value is 25°C, the row index is (25 / 50) * 128 = 64. The pixel at row 64 is set to white, and all other pixels in the column are set to black. The column buffer is then written to the display using the setAddrWindow function with the x coordinate being the current position. The current position increments each time, wrapping around after 240 columns. The wrapping is done by resetting the x coordinate to 0 after 240. This creates a scrolling effect where the oldest data is overwritten by the newest data. The display’s controller supports this kind of windowed write natively, so it’s very efficient. For sensor data that is binary, like a motion detector, you can display a simple icon. The icon is a 16x16 pixel bitmap stored in flash. The bitmap is drawn using the drawBitmap function, which writes the pixel data to the frame buffer. The bitmap is 16x16x2 = 512 bytes. The icon can be a person walking for motion detected, or a person standing for no motion. The icon update is done by drawing the new icon over the old one. The old icon is erased by drawing a rectangle of the background color. The rectangle is 16x16 pixels, so it takes 512 bytes of SPI data. The icon update takes about 100 microseconds. The display’s touch interface, if it has one, is a resistive touch panel with an XPT2046 controller. The touch controller uses SPI with a separate chip select. The touch data is 12-bit for x and y, giving 4096x4096 resolution. The touch coordinates are mapped to the display’s 240x320 resolution by scaling. The scaling factor is 240/4096 = 0.0586 for x, and 320/4096 = 0.0781 for y. The touch data is read by sending a command byte, then reading two bytes for x and two bytes for y. The touch read takes about 1 millisecond. The touch data can be used to select different sensor displays. For example, you can have a button that switches between temperature and humidity. The button is a rectangle drawn on the display. The touch coordinates are compared to the button’s rectangle. If the touch is inside the rectangle, the display mode changes. The button’s rectangle is 80x40 pixels, so it’s easy to tap. The button’s color changes when pressed, using a different color for the pressed state. The button’s text is drawn using the font library. The button’s update is done by redrawing the rectangle and text. The redraw takes about 2 milliseconds. The touch controller’s pressure measurement can also be used to detect a long press or a double tap. The pressure is measured by the z-axis of the touch controller. The z-axis value is the resistance between the two touch layers. A light touch gives a high resistance, while a hard touch gives a low resistance. The pressure threshold is set to 1000 for a light touch, and 500 for a hard touch. The touch data is filtered with a moving average of 4 samples to reduce noise. The filtered touch coordinates are used for the button detection. The display’s backlight can be controlled by the touch as well. For example, a double tap turns the backlight on or off. The backlight is controlled by a PWM pin on the microcontroller. The PWM frequency is 1kHz, which is above the audible range. The duty cycle is set to 0 for off, and 255 for full brightness. The backlight on/off is done by setting the duty cycle to 0 or 255. The display’s power consumption with the backlight off is 10mA, so it’s useful for battery conservation. The sensor data display can be combined with a real-time clock (RTC) to show the timestamp of the data. The RTC is a DS3231 module with I2C interface. The RTC provides seconds, minutes, hours, day, month, and year. The time is read from the RTC every second. The time is displayed in a 24-hour format, like 14:30:45. The time display uses a 24x32 pixel font, so it takes 24x32x6 = 4608 bytes for the six digits. The time update is done every second, so it’s not a heavy load. The time is displayed at the top of the screen, with the sensor data below. The sensor data is displayed in a 12x16 pixel font, so it’s smaller. The layout is: top row for time, second row for temperature, third row for humidity, fourth row for pressure. The pressure is displayed in hPa, with one decimal place. The temperature is in Celsius, with one decimal place. The humidity is in percent, with one decimal place. The
Confidential — By Appointment
Bring a Shariah-sensitive question to a Senior Advisor.
A 45-minute consultation with a member of our Advisory Board — held under privilege, in Arabic or English, with full discretion.