Skip to content
Atelier · 1987
Journal de l'Atelier · depuis 1987

How to display a graph on a 2.76 inch 480x480 round display?

Par admin
To display a graph on a 2.76 inch 480x480 round display, you need to connect the panel to a microcontroller or single-board computer that supports the MIPI RGB interface, then use a graphics library to render data in a circular coordinate system. This specific display, the 2.76 inch 480x480 round tft display, has a pixel density of about 174 PPI (pixels per inch), which is enough for crisp line graphs, bar charts, or even real-time sensor plots. The round shape introduces a challenge: you can’t just map Cartesian coordinates directly because the active area is a circle with a diameter of 2.76 inches, roughly 70.1 mm. So, you have to clip or mask the graph to fit within the circular boundary, and you might want to use polar coordinates for radial graphs like speedometers or radar charts. The interface is MIPI DSI (Display Serial Interface) with RGB data format, typically using 4 data lanes plus a clock lane, running at speeds up to 500 Mbps per lane. That gives you a total bandwidth of 2 Gbps, which is enough to refresh the 480x480 resolution at 60 Hz with 24-bit color (16.7 million colors). To drive this, you’ll need a controller like the STM32F429 or a Raspberry Pi with a MIPI adapter, because the display doesn’t have a built-in frame buffer—it relies on the host to stream pixel data continuously.

Hardware Setup for Graph Rendering

First, let’s talk about the physical connection. The 2.76 inch 480x480 round tft display uses a 40-pin FPC connector with a 0.5 mm pitch. The pinout includes MIPI_D0P, MIPI_D0N, MIPI_D1P, MIPI_D1N, MIPI_D2P, MIPI_D2N, MIPI_D3P, MIPI_D3N, MIPI_CLKP, MIPI_CLKN, plus power (3.3V for I/O, 2.8V for analog), reset, and backlight control. The backlight is a white LED array with a typical forward voltage of 3.0V and current of 20 mA per string (usually 4 strings in parallel, so 80 mA total). For brightness control, you can use a PWM signal at 1 kHz to 10 kHz. The display’s typical power consumption is around 150 mW for the panel plus 250 mW for the backlight at full brightness, so total about 400 mW. That’s important if you’re running on battery—you might want to dim the backlight to 50% duty cycle, which drops power to about 200 mW. For the controller, I recommend the Raspberry Pi 4 or 5 with a MIPI DSI adapter like the official Raspberry Pi 7-inch touchscreen adapter, but you’ll need to modify the cable because that adapter is for a 15-pin connector. Alternatively, use an STM32H743 board, which has a built-in MIPI DSI controller with 2 data lanes, but you’ll need to run at reduced resolution or refresh rate—480x480 at 30 Hz is feasible with 2 lanes. The STM32H743 has 1 MB of SRAM, which can hold a full frame buffer (480x480x3 bytes = 691,200 bytes, about 675 KB), so you can render the graph off-screen and then copy to the display. For the Raspberry Pi, the GPU handles the frame buffer, and you can use the fbcp (frame buffer copy) utility to map the circular area. The key here is to set up a custom framebuffer that only updates the circular region, because the display’s active area is a circle, not a square. The panel has a square glass substrate, but the circular mask is applied during manufacturing, so pixels outside the circle are physically blocked. You can still write to them, but they won’t be visible. To save bandwidth, you should only write to pixels within the circle, which is about 78.5% of the square area (π * (240^2) / (480^2) ≈ 0.785). That means you’re effectively rendering 180,864 pixels per frame instead of 230,400, saving about 21% of the data transfer.

Graphics Library and Coordinate Mapping

For drawing graphs, you can use LVGL (Light and Versatile Graphics Library) version 8.3 or later, which has built-in support for round displays. LVGL uses a “display driver” that you configure with a custom flush callback. The callback receives a rectangular area, but for a round display, you need to modify the flush function to only write pixels that fall inside the circle. The circle’s center is at (240, 240) with a radius of 240 pixels. So, for each pixel (x, y) in the flush rectangle, check if (x-240)^2 + (y-240)^2 <= 240^2. If not, skip it. This adds a small overhead—about 2 microseconds per pixel on a 200 MHz ARM Cortex-M7, so for a full frame flush of 180,864 pixels, that’s about 360 ms, which is too slow for 60 fps. Instead, you can precompute a mask table: a 480x480 array of 1-bit values indicating whether the pixel is inside the circle. That table takes 28,800 bytes (480*480/8), which fits in flash. Then the flush function just checks the mask bit for each pixel, which is a single bit test and takes about 50 ns per pixel, so the full flush is about 9 ms, leaving plenty of time for 60 fps. For the graph itself, you have two options: Cartesian or polar. For a Cartesian line graph (like a time series), you can clip the lines to the circular boundary. For example, if you have a line from (0, 100) to (480, 380), you need to compute the intersection with the circle. The math: param t from 0 to 1, x = x0 + t*(x1-x0), y = y0 + t*(y1-y0), and solve for t such that (x-240)^2 + (y-240)^2 = 240^2. This is a quadratic equation, and you’ll get up to two solutions. For a polar graph (like a radial speedometer), you can use polar coordinates: angle θ from 0 to 2π, radius r from 0 to 240. Then x = 240 + r*cos(θ), y = 240 + r*sin(θ). This is simpler because you don’t need to clip—just ensure r ≤ 240. For a bar chart in polar form, you can draw arcs: for each bar, define a start angle and end angle, and a radius, then fill the sector. LVGL has a “lv_arc” object that can draw arcs, but it’s limited to a single arc. For multiple bars, you’ll need to use the “lv_canvas” object and draw the sectors manually using pixel-level writes. The canvas is a buffer of 480x480 pixels, but you can allocate a smaller buffer for the graph area only—say, a 240x240 buffer for the inner region, then scale it up. That reduces memory from 675 KB to 169 KB, which is useful for microcontrollers with limited SRAM.

Data Handling and Refresh Rate

Now, let’s talk about the data source. If you’re displaying real-time sensor data, you need to update the graph at a rate that matches the sensor’s output. For example, a temperature sensor like the DS18B20 outputs data at 1 Hz, so you can update the graph once per second. But if you’re using an accelerometer like the MPU6050 at 100 Hz, you’ll want to update the graph at 100 fps, but the display’s refresh rate is limited by the MIPI interface. With 4 data lanes at 500 Mbps each, the total bandwidth is 2 Gbps. For a 480x480 resolution at 24-bit color, each frame is 480*480*3 = 691,200 bytes, or 5.5296 million bits. At 2 Gbps, you can theoretically achieve 2,000,000,000 / 5,529,600 ≈ 361 fps, but the display’s timing controller (TCON) usually limits the refresh rate to 60 Hz. The TCON expects a vertical blanking interval of about 10 lines, so the actual pixel clock is around 480*480*60*1.02 ≈ 14.1 MHz, which is well within the MIPI bandwidth. So, you can update the graph at 60 Hz, but the sensor data might be slower. In that case, you can interpolate between data points to create a smooth animation. For example, if you have a new data point every 100 ms, you can linearly interpolate between the previous and current values over 10 frames (at 60 Hz, that’s 166.7 ms per frame, so 10 frames is 1.67 seconds—too slow). Instead, you can use a moving average filter: store the last 10 data points, and display the average. This smooths out noise and reduces the update rate to 10 Hz, which is still visually acceptable. For the graph’s axes, you need to draw tick marks and labels. On a round display, you can place the X-axis as a circle around the edge, with tick marks at regular angles. For example, for a 24-hour clock, you’d have 24 ticks at 15-degree intervals. The labels (like “0”, “1”, “2”, …) can be rotated to align with the radial direction. LVGL supports text rotation with the “lv_label_set_angle” function, but it only rotates by 90-degree increments. For arbitrary angles, you’ll need to use the “lv_canvas” and draw the text pixel by pixel using a font bitmap. That’s heavy on the CPU, so you might want to pre-render the labels as images and store them in flash. Each label at a 24-point font size is about 24x24 pixels, so 24 labels would be 24*24*24*3 = 41,472 bytes, which is fine for a 1 MB flash. For the Y-axis, you can use concentric circles. For example, if the data range is 0 to 100, you can draw circles at radii 48, 96, 144, 192, and 240 (corresponding to values 20, 40, 60, 80, 100). The labels for these can be placed along the 12 o’clock direction (θ = -90 degrees). You’ll need to calculate the pixel positions: x = 240 + r*cos(θ), y = 240 + r*sin(θ). For θ = -90°, cos = 0, sin = -1, so x = 240, y = 240 - r. That’s simple.

Performance Optimization Techniques

To get smooth graph rendering at 60 fps, you need to optimize the pixel pipeline. The biggest bottleneck is the MIPI DSI transmission. On the STM32H743, the DSI peripheral uses a dedicated DMA channel to transfer data from the frame buffer to the display. The frame buffer should be in DTCM (Data Tightly Coupled Memory) for fastest access—DTCM runs at 400 MHz with zero wait states. The DMA transfer can be double-buffered: you render the next frame in one buffer while the DMA sends the current frame. This avoids tearing. The buffer size is 480*480*3 = 691,200 bytes, and DTCM on the STM32H743 is 128 KB, so it won’t fit. You’ll need to use the AXI SRAM (512 KB) or SDRAM (if you have external memory). The AXI SRAM has a 64-bit bus and runs at 200 MHz, so it can handle the bandwidth: 691,200 bytes per frame at 60 Hz is 41.5 MB/s, well within the 1.6 GB/s theoretical bandwidth of the AXI bus. For the graphics rendering, you can use the Chrom-ART accelerator (DMA2D) on the STM32 to fill rectangles, copy images, and blend colors. The DMA2D can fill a 480x480 area with a solid color in about 0.5 ms, and copy a 480x480 image in about 1 ms. For a line graph, you can draw the background once, then draw the line as a series of small rectangles (each 1 pixel wide and the height of the line). That’s inefficient. Instead, use the “lv_line” object in LVGL, which uses Bresenham’s algorithm to draw the line pixel by pixel. On a 400 MHz Cortex-M7, Bresenham’s algorithm takes about 10 ns per pixel, so a 480-pixel line takes 4.8 µs. For a graph with 100 data points, that’s 480 µs, plus the background fill of 0.5 ms, total about 1 ms per frame. That’s well within the 16.7 ms frame budget. For the polar graph, you can draw arcs using the DMA2D’s fill function with a custom pattern. For example, to draw a sector from angle 0 to 30 degrees at radius 200, you can fill a 240x240 rectangle that covers the sector, then use a mask to clip the pixels outside the sector. The mask can be a 1-bit bitmap of the sector, precomputed and stored in flash. The sector mask for 30 degrees at 240x240 resolution is about 7,200 bytes (240*240/8). That’s fine for a few sectors, but if you have 100 bars, you’ll need 720 KB of flash, which is too much. Instead, you can compute the mask on the fly: for each pixel in the bounding rectangle, check if the angle and radius are within the sector. That’s a few arithmetic operations per pixel, which on a 400 MHz CPU takes about 100 ns per pixel, so a 240x240 rectangle (57,600 pixels) would take 5.76 ms, which is acceptable for a 60 fps update if you only update one bar per frame. But if you need to update all bars every frame, you’re looking at 576 ms, which is too slow. So, for real-time polar graphs, limit the number of bars to 20 or fewer, or update them at a lower rate like 10 fps.

Software Stack and Code Example

Let’s get into the software. You’ll need a real-time operating system (RTOS) like FreeRTOS to manage the display update and sensor reading tasks. The display task runs at 60 Hz, reading from a queue that contains the latest data points. The sensor task reads the sensor at its native rate and pushes data to the queue. The queue should be large enough to hold 10 data points to handle bursts. For the graph rendering, you can use the LVGL library with the following configuration: set the display resolution to 480x480, color depth to 24 bits, and use the custom flush function with the circular mask. Here’s a pseudo-code snippet for the flush function:

void my_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x1 = area->x1, x2 = area->x2, y1 = area->y1, y2 = area->y2;
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if ((x-240)*(x-240) + (y-240)*(y-240) <= 240*240) {
// Write pixel to display via MIPI DSI
dsi_write_pixel(x, y, color_p->full);
}
color_p++;
}
}
lv_disp_flush_ready(disp_drv);
}

This function iterates over all pixels in the flushed area, checks the circle condition, and writes only the valid pixels. The MIPI DSI write function sends a “write memory” command (0x2C) followed by the pixel data. For a 480x480 display, you can send the entire frame in one go, but the display’s TCON expects the data in a specific order: row by row, from top to bottom. So, you need to set the column address (0x2A) and page address (0x2B) before writing. For a circular display, the column address range is 0 to 479, and the page address range is 0 to 479, but you only write pixels inside the circle. The TCON doesn’t care about the mask—it just stores the pixel values in the frame buffer. So, pixels outside the circle will be written to the buffer but not displayed. That’s fine, but it wastes bandwidth. To avoid that, you can set the column and page address to the bounding box of the flushed area, then write only the pixels in that box. But the flush function in LVGL often sends small rectangles, so the overhead is minimal. For the graph itself, you can use LVGL’s “lv_chart” object, which supports line charts, bar charts, and scatter plots. However, the lv_chart object is designed for rectangular displays, so you’ll need to modify its drawing function to clip to the circle. You can do this by setting the “lv_obj_set_style_clip_corner” to true and using a custom style with a circular clip path. LVGL supports clip paths through the “lv_draw_ctx” structure, but it’s not trivial. Alternatively, you can use the “lv_canvas” object to draw the graph manually, which gives you full control. The canvas is a buffer that you can write to using LVGL’s drawing functions like “lv_canvas_draw_line” and “lv_canvas_draw


a
admin
Atelier Fabrice Requin · Paris 6ᵉ

Réserver un essayage privé à l'atelier

Sur rendez-vous uniquement — 12 rue de Seine, Paris 6ᵉ.

Réserver un essayage privé