Skip to content

How to create a circular UI on a 2.76 inch round display?

About the author

To create a circular UI on a 2.76 inch 480x480 round tft display, you need to start by understanding that the display’s physical round shape demands a software-driven approach to clip content outside the active circle. The 2.76-inch round TFT panel typically has a 480x480 pixel resolution, which gives you a square pixel matrix but a circular active area with a diameter of about 480 pixels. The key is to use a circular clipping mask in your graphics library, such as LVGL, Squareline Studio, or even raw C with a framebuffer, to ensure UI elements like buttons, sliders, and text only render inside the circular boundary. For example, in LVGL, you can set the display’s width and height to 480, but then apply a custom drawing function that checks if each pixel falls within the circle equation: (x - 240)^2 + (y - 240)^2 <= 240^2. This is critical because the TFT controller, like the ST7701S or ILI9488, still outputs the full 480x480 square frame, so without software clipping, you’ll see artifacts in the corners. Many developers also use a circular overlay image as a mask in the framebuffer, but that consumes extra memory bandwidth. On the hardware side, the MIPI RGB interface on this display runs at 24-bit color depth, which means each pixel requires 3 bytes, so the full framebuffer is 480*480*3 = 691,200 bytes, or about 675 KB. If you’re using a microcontroller like an ESP32-S3 or an STM32H7, you’ll need at least 2 MB of external PSRAM for double buffering to avoid tearing. The display’s refresh rate is typically 60 Hz, so your UI rendering loop must complete within 16.67 milliseconds per frame to maintain smooth animations. For a circular UI, you should also consider the viewing angle and touch interface. The 2.76-inch round display often uses a capacitive touch panel with a circular sensor pattern, but the touch coordinates are still reported in a square grid. You’ll need to map the touch input to the circular area by ignoring touches outside the circle, which can be done by checking if the touch point’s distance from the center is less than 240 pixels. In practice, I’ve seen teams use a radial menu layout, where icons are placed along the circumference at angles like 0, 45, 90, 135, 180, 225, 270, and 315 degrees, with each icon occupying a 60x60 pixel area. The text rendering is trickier because most fonts are designed for rectangular blocks. You can use a custom font engine that wraps text inside a circular path, or simply use short labels that fit within the radius. For example, a 12-point font renders about 20 characters per line in a 240-pixel radius, so you can display “Temperature” and “Humidity” side by side. Data density matters here: the display’s pixel pitch is about 0.146 mm (calculated as 2.76 inches * 25.4 mm/inch / 480 pixels), which gives a crisp image at typical viewing distances of 30-50 cm. For industrial applications, you might use a circular gauge with a 270-degree arc, where the needle sweeps from 0 to 270 degrees, and the tick marks are spaced every 10 degrees, giving 28 ticks. Each tick line is 2 pixels wide and 10 pixels long, with a gap of 5 pixels from the arc. The arc itself is drawn using Bresenham’s circle algorithm, which is computationally efficient on embedded processors. If you’re using a GUI builder like Squareline Studio, you can import the display’s resolution and set the screen shape to “round,” which automatically generates the clipping code. But Squareline Studio’s output is based on LVGL, so you still need to configure the LVGL display driver to handle the MIPI RGB interface. The MIPI DSI interface on this display uses 4 lanes, each running at up to 500 Mbps, so the total bandwidth is 2 Gbps, which is enough for 60 fps at 480x480. However, the actual pixel clock is around 27 MHz, calculated as 480*480*60*1.2 (overhead) = 16.6 MHz, but with blanking intervals, it’s closer to 27 MHz. This means your microcontroller’s SPI or parallel interface might not be fast enough; you’ll need a dedicated RGB interface with at least 16-bit data lines. For the UI layout, consider using a circular progress bar, which is common in smartwatch or dashboard designs. The progress bar can be implemented as an arc that grows from 0 to 360 degrees, with a width of 10 pixels. The arc’s color can change based on the value, such as green for 0-70%, yellow for 70-90%, and red for 90-100%. The math for the arc is straightforward: for each angle from 0 to 360, calculate the x and y coordinates using sin and cos, and then draw a line from the inner radius to the outer radius. The inner radius is 220 pixels, and the outer radius is 240 pixels, so the arc is 20 pixels thick. You can also add a center circle of 40 pixels radius to display a numeric value, like “75%”. The font for the number should be at least 24 points to be readable. For touch interaction, you can implement a circular slider that follows the finger movement around the circle. The slider’s value is determined by the angle of the touch point relative to the center, which is calculated using atan2(y - 240, x - 240). The angle is then mapped to a range of 0 to 100. This requires a touch sampling rate of at least 100 Hz to feel responsive, which the capacitive touch controller on this display supports. The touch controller, such as the FT6336, reports coordinates at up to 200 Hz over I2C, so you’re fine. Power consumption is another factor: the display’s backlight typically draws 200-300 mA at 3.3V, and the TFT panel itself draws about 50 mA. So total power is around 1-1.2 watts. If you’re running on a battery, you’ll need to dim the backlight to 50% brightness to reduce power to 600 mW, which still gives readable content in indoor lighting. The UI should also handle sleep mode, where the display is turned off but the touch controller remains active to wake it up. This is done by sending a sleep command to the TFT controller via SPI, which puts it in a low-power state drawing less than 1 mA. For the circular UI, you also need to consider the bezel width. The display’s outer diameter is 2.76 inches, but the active area is about 2.5 inches, so there’s a 0.13-inch bezel on each side. This bezel can be used for mounting, but it also means that the UI should not place interactive elements too close to the edge, because the bezel might interfere with touch. A safe margin is 10 pixels from the edge of the active area, so the touchable area is within a radius of 230 pixels. In terms of color depth, the display supports 16.7 million colors, but for performance, you might want to use 16-bit color (RGB565) which reduces the framebuffer to 480*480*2 = 460,800 bytes. This is a common trade-off in embedded systems. The color palette for a circular UI should be high-contrast, like white text on a dark background, because the round shape can cause glare at certain angles. The display’s viewing angle is typically 80 degrees in all directions, so it’s fine for most use cases. For the UI framework, I recommend using LVGL 8.3 or later, which has built-in support for round displays via the `lv_disp_drv_t` structure. You set the `round_corner` flag to true, and then provide a custom `flush_cb` function that clips the output. The flush function receives a buffer of pixels, and you iterate through each pixel, checking if it’s inside the circle. If not, you set the pixel to transparent or black. This is efficient because the flush function runs in the background. Another approach is to use a DMA-based framebuffer, where the microcontroller sends the entire framebuffer to the display via RGB interface, and the display’s internal controller handles the clipping. But the ST7701S controller doesn’t support hardware clipping, so you must do it in software. For animations, like a rotating circular menu, you can use a double buffer and swap them every frame. The rotation is done by updating the angle of each element and redrawing the arc. This requires a lot of CPU time, but on an ESP32-S3 running at 240 MHz, you can achieve 30 fps with a simple UI. For more complex UIs, like a watch face with second hand, minute hand, and hour hand, you need to optimize the drawing. The second hand is drawn every second, so it’s a thin line of 2 pixels width from the center to the edge. The minute hand is thicker, 4 pixels, and the hour hand is 6 pixels. The hands are drawn using anti-aliasing to avoid jagged edges, which is done by modifying the pixel intensity based on the distance from the line. This adds computational overhead, but it’s worth it for a professional look. The display’s response time is 30 ms, so there’s no ghosting issue. For data visualization, you can use a circular bar chart, where each bar is a segment of the circle. For example, if you have 12 data points, each bar occupies 30 degrees. The bar’s height is proportional to the value, ranging from 0 to 240 pixels. The bars are drawn from the center outward, so the inner radius is 0 and the outer radius is the value. This is similar to a radar chart but in a circular format. The chart’s background is a grid of concentric circles at radii 60, 120, 180, and 240 pixels, with thin lines of 1 pixel width. The grid helps the user gauge the values. For the touch interface, you can implement a long press gesture to reset the UI, or a swipe gesture to change screens. The swipe gesture is detected by tracking the touch movement over 200 ms. If the distance moved is more than 50 pixels, it’s a swipe. The direction is determined by the angle of the movement. This is all handled in the touch event handler, which runs at 100 Hz. The event handler also debounces the touch to avoid false triggers. The debounce time is 20 ms, which is standard for capacitive touch. For the UI’s aesthetic, use a dark theme with a blue accent color, because blue LEDs are more energy-efficient on the backlight. The backlight is a white LED, but you can adjust the color temperature by using a PWM signal on the RGB backlight pins. Some round displays have a single backlight pin, but others have separate red, green, and blue pins, allowing you to create a custom color. For the 2.76-inch display, the backlight is typically a single white LED, so you can only control brightness. The brightness is controlled by a PWM signal at 1 kHz, with a duty cycle from 0 to 100%. At 100% duty cycle, the brightness is 500 nits, which is too bright for indoor use. I recommend 200 nits for indoor, which corresponds to a 40% duty cycle. The display’s contrast ratio is 1000:1, so it’s good for reading text. For the UI’s layout, use a circular notification bar at the top, which is an arc of 180 degrees from 0 to 180 degrees, showing time, battery, and signal strength. The notification bar is 20 pixels thick, with icons placed at 30, 90, and 150 degrees. The time is displayed in the center of the bar using a 16-point font. The battery icon is a small rectangle with a percentage inside, and the signal strength is a series of bars. This layout is inspired by smartwatches, and it works well on a round display. The rest of the screen is used for the main content, which can be a list of items, a map, or a control panel. For a list, you can use a circular scrolling list, where items are placed along a spiral path. The spiral starts at the center and goes outward, with each item rotated by 10 degrees. This is a complex UI, but it’s visually appealing. The list items are touchable, and when you tap one, it expands to show more details. The expansion is animated by scaling the item from 0 to 1 over 200 ms. The scaling is done by adjusting the font size and the bounding box. This requires a lot of math, but it’s manageable with a library like LVGL. For the map, you can use a circular clipping mask to show a portion of a larger map. The map is panned by touch, and the visible area is always a circle. The map tiles are loaded from a microSD card, and the display’s MIPI interface can handle the data rate. The map rendering is done in a background task, so the UI remains responsive. The map’s scale is 1:10000, which is suitable for a small area. The map’s coordinate system is converted to the display’s pixel coordinates using a projection. This is a niche use case, but it’s possible. For the control panel, you can have circular buttons that are 40 pixels in diameter, placed at the 3, 6, 9, and 12 o’clock positions. Each button has an icon and a label. The buttons are touchable, and when pressed, they change color. The feedback is immediate because the touch response time is 10 ms. The buttons are arranged in a cross pattern, which is easy to use with a thumb. The button’s hit area is 50 pixels in diameter to account for finger size. The button’s label is 10-point font, placed below the icon. The icon is a 24x24 pixel image, stored in a sprite sheet. The sprite sheet is loaded into RAM at boot time, and each icon is 24*24*2 = 1,152 bytes. For 10 icons, that’s 11,520 bytes, which is fine for most microcontrollers. The sprite sheet is drawn using a blitting function that copies the icon to the framebuffer. The blitting function is optimized for the MIPI interface, so it writes pixels in bursts. The burst size is 16 pixels, which matches the display’s data bus width. This reduces the overhead of the interface. For the UI’s performance, you should measure the frame rate using a timer. The target is 30 fps for a smooth experience. If the frame rate drops below 20 fps, you need to optimize the drawing. Common optimizations include using a smaller color depth, reducing the number of elements, and using hardware acceleration. The ESP32-S3 has a built-in JPEG decoder, which can be used for background images. But for a circular UI, it’s better to use solid colors or gradients. The gradient is drawn using a linear interpolation from the center to the edge. The gradient’s color changes from dark blue to black, which gives a depth effect. The gradient is drawn once and stored in the framebuffer, so it doesn’t affect performance. The gradient’s calculation is done using a lookup table for the color values. The lookup table has 256 entries, each with a 16-bit color. The gradient is applied by setting the pixel’s color based on its distance from the center. The distance is normalized to 0-255, and then used to index the lookup table. This is fast because it’s just a memory read. The gradient’s effect is subtle but noticeable. For the UI’s accessibility, use large fonts and high contrast. The font size should be at least 14 points for body text, and 20 points for headings. The contrast ratio should be at least 4.5:1 for normal text, and 3:1 for large text. This is based on WCAG guidelines. The display’s color gamut is 70% NTSC, so it can show a wide range of colors. The UI’s color palette should be limited to 8-10 colors to avoid a cluttered look. The colors are chosen from the display’s color space, which is sRGB. The colors are defined in the code as hex values, like 0x001F for blue, 0x07E0 for green, and 0xF800 for red. The colors are used for the background, text, and elements. The UI’s typography is also important. Use a sans-serif font for better readability on a small screen. The font is stored in a compressed format, like RLE, to save space. The font’s glyphs are 16x16 pixels for basic characters, and 24x24 for symbols. The font’s rendering is done by copying the glyph’s bitmap to the framebuffer. The bitmap is 16*16*1 = 256 bytes for a monochrome font, or 16*16*2 = 512 bytes for a grayscale font. The grayscale font is better for anti-aliasing, but it uses more memory. For a 2.76-inch display, a grayscale font is recommended because the pixel density is high enough to show the anti-aliasing. The font’s anti-aliasing is done by blending the glyph’s edge pixels with the background. The blending is done using a 4-bit alpha channel, which gives 16 levels of transparency. The alpha channel is stored in the font’s bitmap, so each pixel is 8 bits (4 bits for alpha, 4 bits for intensity). This is a common format for embedded fonts. The font’s size is 16*16*1 = 256 bytes per glyph, but with anti-aliasing, it’s 16*16*2 = 512 bytes. For a font with 100 glyphs, that’s 50 KB, which is acceptable. The font is loaded from flash memory at boot time, and cached in RAM for fast access. The cache is a simple array of 256 bytes, which stores the most recently used glyphs. The cache is flushed when the UI changes. The font’s rendering is done by the LVGL library, which handles the anti-aliasing and the clipping. The library’s font engine is optimized for the MIPI interface, so it writes pixels in

Written by

admin

Home cook, recipe developer, and editor of Anne's Kitchen Table from a 1920s farmhouse kitchen in Portland, Oregon. Triple-testing recipes since 2009.