How to display a counter on a 1.77 inch SPI TFT?
How to Display a Counter on a 1.77 Inch SPI TFT
To display a counter on a 1.77 inch SPI TFT, you need to write firmware that initializes the display, clears the screen, and then repeatedly updates a numeric value in a loop or in response to an event. The core steps involve sending commands via SPI to set the display window, then writing pixel data for each digit. For a typical 128x160 resolution panel like the 1.77 inch spi mcu rgb tft display, you’ll use a driver IC such as the ST7735S or ILI9163C. These controllers support 16-bit color (RGB565) and require a 3.3V logic level. The counter itself can be a simple integer variable incremented by a button press, timer interrupt, or sensor input. The display refresh rate for static counters is trivial—just a few milliseconds per update—so you can easily achieve 60+ updates per second without visible flicker. The key is to only redraw the area around the counter digits rather than the whole screen to avoid tearing and reduce processor load.
Let’s get into the hardware specifics. The 1.77 inch TFT typically uses 8 pins for SPI: VCC (3.3V), GND, CS (chip select), RESET, DC (data/command), MOSI, SCK, and LED (backlight). The backlight pin often needs a 100-ohm resistor to limit current, pulling about 20mA at 3.3V. Total display power consumption is around 40-60mA with backlight on, which is fine for battery-powered projects if you dim the backlight. The SPI clock speed can go up to 20MHz with short wires (<10cm), but for reliability, 8-10MHz is common. If you’re using an Arduino Uno or ESP32, the SPI library handles the low-level bit banging, but for faster updates, you’ll want to use hardware SPI. On an ESP32, you can assign any GPIO pins to SPI functions, but the default VSPI pins (MOSI=23, MISO=19, SCK=18, CS=5) work well. The reset pin is active-low, and you should hold it low for at least 10ms after power-up, then release it to high. The DC pin tells the display whether you’re sending a command (low) or data (high).
For the counter display, you need a font. The simplest approach is to use a 5x7 pixel bitmap font, which fits 25 characters per row at 128 pixels wide (5 pixels per char + 1 pixel spacing). At 160 pixels tall, you can fit 20 rows. But for a counter, you’ll want larger digits—say 16x24 pixels—so you can see them clearly from a foot away. A 16x24 digit uses 384 bytes per character in 16-bit color (2 bytes per pixel). For a 3-digit counter, that’s 1152 bytes per frame. If you redraw the entire counter area (say 60x30 pixels) every update, you’re writing 3600 bytes per SPI transaction. At 10MHz SPI, that’s about 0.36ms per update, plus overhead. So you can update the counter 1000+ times per second if needed. But realistically, you’ll update it at 10-50Hz for human-readable changes.
Here’s a concrete example using an ST7735S driver. The initialization sequence is critical: you must send a series of commands to set the display orientation, color mode, and power settings. A typical init sequence for a 1.77 inch panel includes:
Command 0x11 (Sleep Out) followed by 120ms delay
Command 0x3A (Interface Pixel Format) with parameter 0x05 (16-bit RGB565)
Command 0x36 (Memory Data Access Control) with parameter 0x08 (RGB order, portrait orientation)
Command 0x21 (Display Inversion On) for better contrast
Command 0x13 (Normal Display Mode On)
Command 0x29 (Display On) with 50ms delay
After init, you set the column and page address range using commands 0x2A and 0x2B. For a counter at the top-left corner, you’d set column start=0, end=59 (for a 60-pixel-wide counter), and page start=0, end=29 (for a 30-pixel-tall counter). Then you send 0x2C (Memory Write) and stream the pixel data. The pixel order is left-to-right, top-to-bottom. Each pixel is 2 bytes: high byte is R[4:0] and G[5:3], low byte is G[2:0] and B[4:0]. For white, send 0xFF 0xFF; for black, 0x00 0x00; for red, 0xF8 0x00.
Now, the counter logic. If you’re using a microcontroller with a timer, set up a 1-second interrupt to increment the counter. The counter value can be stored as a 16-bit integer (0-65535). To display it, you need to convert each digit to its bitmap. Pre-store the bitmaps for digits 0-9 in PROGMEM (flash memory) to save RAM. For a 16x24 digit, store 16 rows of 24 bits each. But since the display is 16-bit color, you’ll need to expand each bit to a pixel color. A faster method is to store the bitmaps as 16-bit color arrays directly, but that uses 48 bytes per digit (16 rows * 24 bits / 8 bits per byte). Actually, for 16x24, you need 16 rows * 24 bits = 384 bits = 48 bytes per digit. For 10 digits, that’s 480 bytes in flash. Then, to draw a digit, you read the bitmap byte by byte, and for each bit that’s 1, write a white pixel; for 0, write a black pixel. This is a bit slow because you’re doing bit-level operations, but it works.
For a more efficient approach, use a 4-bit grayscale font or a pre-rendered font table. Some libraries like Adafruit_GFX handle this automatically, but they are heavy on RAM. If you’re on an ESP32 with 512KB RAM, that’s fine. But on an Arduino Uno (2KB RAM), you need to be careful. A 128x160 frame buffer is 40KB (128*160*2 bytes), which won’t fit in Uno’s RAM. So you must draw directly to the display without a buffer. That means you’ll send SPI data for each pixel as you compute it. For a counter, you can compute the digit bitmaps on the fly using a lookup table for each digit’s row pattern.
Let’s talk about performance. The SPI bus speed is the main bottleneck. At 10MHz, the theoretical maximum throughput is 1.25 MB/s. But protocol overhead (command bytes, CS toggling, delays) reduces that to about 1 MB/s. For a 60x30 pixel counter area (1800 pixels), each pixel is 2 bytes, so 3600 bytes per update. At 1 MB/s, that’s 3.6ms per update. Add 1ms for digit conversion and SPI command setup, and you get about 5ms per counter update. That means you can update the counter 200 times per second, which is overkill. But if you’re also updating other parts of the screen (like a graph or text), the total refresh time can add up. For a smooth counter, 10-20 updates per second is plenty.
One common issue is ghosting or flickering when the counter changes. This happens if you clear the entire counter area before drawing the new digits. Instead, only redraw the digits that changed. For example, if the counter goes from 123 to 124, only the last digit changes. You can detect this by comparing the old and new counter values digit by digit. If only the units digit changed, just redraw that one digit’s area (16x24 pixels). That reduces SPI traffic by 2/3. Another trick is to use double buffering in the microcontroller’s RAM if you have enough memory. On an ESP32, you can allocate a 40KB buffer for the whole screen, draw the counter there, then send the entire buffer to the display via SPI. This eliminates flicker entirely because the display only sees the complete frame. But for a counter, the simpler approach is to just redraw the changed digits.
Now, let’s look at some real-world data. The ST7735S datasheet specifies a minimum SPI clock cycle time of 50ns (20MHz), but typical panels have longer traces and capacitance, so 10MHz is safe. The display’s response time is about 10ms for pixel transitions, so even if you update faster, the human eye won’t see it. For a counter, you’re updating at most once per second, so response time is irrelevant. The backlight LED forward voltage is 3.2V typical, so with a 3.3V supply, you need a current-limiting resistor. For 20mA, R = (3.3 - 3.2) / 0.02 = 5 ohms, but a 10-ohm resistor is fine. The backlight consumes about 60mW, and the display controller about 10mW, so total power is 70mW. That’s trivial for a 3.7V LiPo battery with 1000mAh capacity—you can run it for 50+ hours continuously.
For the counter input, you can use a physical button with a debounce circuit. A simple RC filter with 10k resistor and 100nF capacitor gives a 1ms time constant, which is enough for mechanical switches. In software, you can debounce by checking the button state every 10ms and only incrementing if the state is stable for 50ms. Alternatively, use a rotary encoder for up/down counting. The encoder outputs two quadrature signals; you can decode them with a state machine or interrupts. For a 20-pulse-per-revolution encoder, you can count 20 steps per rotation. The display update rate for encoder input should be at least 20Hz to feel responsive.
Here’s a table of typical SPI pin connections for common microcontrollers:
| Microcontroller | MOSI | SCK | CS | DC | RST |
|---|---|---|---|---|---|
| Arduino Uno | 11 | 13 | 10 | 9 | 8 |
| ESP32 | 23 | 18 | 5 | 2 | 4 |
| STM32F103 | PA7 | PA5 | PA4 | PB0 | PB1 |
| Raspberry Pi Pico | GP19 | GP18 | GP17 | GP16 | GP15 |
Note that the backlight pin is usually connected to a PWM-capable pin for brightness control. On the ESP32, you can use LEDC PWM at 1kHz frequency. Set the duty cycle to 0-255 for 0-100% brightness. At 50% duty, the backlight current drops to about 10mA, saving power.
For the counter display, you can also add a background color. A common choice is dark blue (0x001F) or black (0x0000) for high contrast. The digits can be white (0xFFFF) or yellow (0xFFE0). If you want a retro look, use green (0x07E0) on black. The color depth is 16-bit, so you have 65536 colors to choose from. But avoid using colors that are too similar to the background, as the display’s contrast ratio is about 300:1, so subtle differences may be hard to read.
One more thing: the 1.77 inch TFT’s viewing angle is limited. The ST7735S is a TN panel, so the best viewing angle is straight on. Off-angle, the colors invert and contrast drops. If you need wide viewing angles, consider an IPS display, but those are rarer in this size. For a simple counter, TN is fine.
Let’s talk about the firmware structure. In your main loop, you’ll have a variable `counter` that increments. You’ll also have a variable `last_counter` to track the previous value. On each loop iteration, compare them. If different, update the display. Here’s a pseudo-code snippet:
```c
void loop() {
static uint16_t last_counter = 0;
uint16_t counter = get_counter_value(); // from button, timer, etc.
if (counter != last_counter) {
display_counter(counter, last_counter);
last_counter = counter;
}
delay(10); // 100Hz loop
}
```
The `display_counter` function should calculate which digits changed. For example, if the counter is 3 digits (0-999), you can extract hundreds, tens, and units digits. Compare each digit to the previous one. If only the units digit changed, set the column range to the rightmost digit’s position. If the tens digit changed, also update that. This minimizes SPI traffic. The digit positions can be pre-calculated: for a 3-digit counter with 16-pixel-wide digits and 2-pixel spacing, the total width is 16*3 + 2*2 = 52 pixels. Center it on the 128-pixel-wide screen: start at (128-52)/2 = 38 pixels from left. The height is 24 pixels, so start at (160-24)/2 = 68 pixels from top. So the counter area is from column 38 to 89, and page 68 to 91.
For the bitmap, you can store the digit images in a 2D array: `const uint8_t digits[10][48]` where each digit is 48 bytes (16 rows * 24 bits / 8 bits per byte). To draw a digit at position (x, y), you loop through the rows, and for each row, loop through the bytes. For each byte, you check each bit. If the bit is 1, you send a white pixel; if 0, send a black pixel. This is slow but deterministic. Alternatively, you can pre-render the digits as 16-bit color arrays: `const uint16_t digits[10][384]` where each digit is 384 pixels (16*24). That takes 10*384*2 = 7680 bytes in flash, which is fine for an ESP32 but tight for an Uno. On an Uno, you have 32KB flash, so 7.5KB is okay, but you also need the program code. So it’s borderline.
For a more memory-efficient approach, use a 1-bit per pixel font and expand on the fly. The expansion can be done with a lookup table for 8-bit patterns: for each byte, you can precompute the 8 pixels as 16-bit colors. But that table is 256 entries * 16 bytes = 4KB, which is still large. A simpler method is to use a 4-bit per pixel font (16 gray levels) and map to 16-bit color. That reduces the bitmap size by half. But for a counter, black and white is fine.
Let’s get into the driver IC specifics. The ST7735S supports 12-bit, 16-bit, and 18-bit color modes. 16-bit is the sweet spot for speed and quality. The display also supports hardware windowing, which is what we use for partial updates. The windowing commands are 0x2A (Column Address Set) and 0x2B (Page Address Set). You send 4 bytes for each: start high, start low, end high, end low. For example, to set the column range from 38 to 89, you send 0x2A, then 0x00, 0x26, 0x00, 0x59. Then 0x2C to start writing pixels. The display will automatically wrap to the next row when you send more than the column count. This is standard.
One common mistake is forgetting to set the display orientation. The memory data access control command (0x36) controls the scan direction. The default is landscape mode, but for a counter, portrait is usually better. The parameter 0x08 sets portrait mode with RGB order. If you want mirroring, you can set bits 5 and 6. For example, 0x60 mirrors both axes. Experiment to get the orientation right.
Another issue is the display’s reset timing. The ST7735S requires a hardware reset pulse of at least 10ms low, then 120ms high before sending commands. Some modules have a built-in power-on reset circuit, but it’s safer to control it from the MCU. Also, the CS pin must be held low during the entire SPI transaction. Some libraries toggle CS for each byte, which adds overhead. Instead, keep CS low for the entire command+data sequence.
Now, let’s talk about the counter value source. If you’re using a timer, the timer interrupt should be as short as possible. On an ESP32, you can use the ESP32 timer group with a 1ms resolution. Set the timer to 1 second, and in the ISR, increment a volatile counter variable. The main loop reads this variable and updates the display. Be careful about race conditions: use `volatile` and possibly disable
Why Trust This Review
Every figure below comes from AutoMototrke's 14,000-HP chassis dyno or the 1,247-truck Long-Term Index — never a manufacturer press kit. See our methodology.
Subscribe to The Garage
612,000 truck buyers read it weekly. Spec sheets, dyno plots, owner telemetry — every Friday.