How to draw shapes on a 2.42 inch OLED?
You draw shapes on a 2.42 inch 128x64 oled display by sending pixel data via SPI or I2C to a controller like the SSD1309 or SH1106, then using a graphics library (like Adafruit GFX or U8g2) to call functions for lines, rectangles, circles, and triangles. The display has a resolution of 128x64 pixels, meaning you have 8,192 individual pixels to work with. Each pixel is either on (white) or off (black) in monochrome mode, but some drivers support grayscale via PWM. The physical size is 2.42 inches diagonally, with a pixel pitch around 0.43mm, giving you a crisp image for text and basic shapes. To get started, you need a microcontroller like an Arduino Uno, ESP32, or STM32, wired to the OLED’s VCC, GND, SCK, SDA, and optional DC and RST pins. The SPI interface runs at up to 10 MHz, so you can update the full frame in about 1.2 ms if you’re blasting raw data. But for drawing shapes, you rely on the library to handle the framebuffer—a 1KB chunk of RAM (128x64/8) that maps each bit to a pixel. Let’s break down the hardware specifics first, then the software techniques, with real numbers and code snippets.
Hardware wiring and constraints
The 2.42 inch 128x64 oled display typically uses the SSD1309 controller, which is a step up from the common SSD1306. It supports both SPI and I2C, but SPI is faster—128x64 at 60 Hz refresh means you’re pushing 8,192 bytes per frame (if you’re dumb about it), but with page addressing, you only update changed bytes. For shapes, you’ll want a framebuffer in RAM (1KB on the display itself, but you can use a local buffer too). The default SPI pins on an Arduino Uno are: SCK (pin 13), MOSI (pin 11), and you need a CS pin (any digital, say pin 10), DC (pin 9), and RST (pin 8). Power draw is about 20 mA at 3.3V, so a 3.3V regulator is mandatory if you’re using a 5V board—feeding 5V to the OLED kills it. The display’s operating temperature is -40°C to +85°C, so it’s fine for outdoor projects. The pixel layout is column-major: columns 0-127, rows 0-63, but the controller organizes memory into 8 pages of 8 rows each. So to draw a pixel at (x, y), you calculate the page = y/8, and bit position = y%8. This is why libraries abstract it—you don’t want to manually set bits every time.
Software setup: libraries and initialization
You have two main library choices: Adafruit_SSD1306 (with Adafruit GFX) or U8g2. Adafruit’s library uses a 1KB framebuffer in RAM, which is fine for an Uno (2KB total SRAM) but tight. U8g2 can use a smaller buffer (like 128 bytes) for page-by-page rendering, saving memory. For the 2.42 inch display, you need to specify the correct controller: in Adafruit’s library, use `SSD1306_128_64` or `SSD1306_128_64_ALT` for the SH1106 variant. The initialization sequence is: reset the display (pull RST low for 10 ms), send a set of commands via SPI (like 0xAF for display on, 0xA8 for multiplex ratio, 0xD3 for display offset). The typical init takes about 50 ms. After that, you can draw. Here’s a quick example in Arduino C++:
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define OLED_DC 9
#define OLED_CS 10
#define OLED_RST 8
Adafruit_SSD1306 display(128, 64, &SPI, OLED_DC, OLED_RST, OLED_CS);
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // I2C address, but SPI ignores it
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
}
Drawing lines: the math behind the pixels
Lines are the simplest shape. The library uses Bresenham’s line algorithm, which only uses integer arithmetic—no floating point. For a line from (x0, y0) to (x1, y1), it calculates the error term and steps through pixels. On a 128x64 display, a diagonal line from (0,0) to (127,63) takes 128 steps (since the slope is 0.5). The function `display.drawLine(x0, y0, x1, y1, SSD1306_WHITE)` does this. But you can also do it manually: iterate x from 0 to 127, compute y = (63 * x) / 127, and call `drawPixel(x, y, WHITE)`. That’s slower because each pixel call involves a framebuffer write. The library’s line function is optimized—it writes directly to the framebuffer array, modifying bits in the appropriate page. For a horizontal line, it’s even faster: you can set a whole byte (8 pixels) at once. For example, a horizontal line from (0, y) to (127, y) fills 16 bytes (128/8) in the framebuffer. The library does this internally, but if you’re writing custom code, you can use `display.drawFastHLine(x, y, w, color)` which is about 10x faster than a loop of drawPixel calls. Data: drawing a horizontal line of 128 pixels takes about 0.2 ms at 16 MHz, while a diagonal line takes 0.5 ms due to the stepping.
Rectangles: filled vs. outlined
Rectangles are just four lines, but the library has dedicated functions: `drawRect(x, y, w, h, color)` for an outline, and `fillRect(x, y, w, h, color)` for a filled version. The outline version draws four lines using the fast horizontal/vertical line functions. For a 100x50 rectangle at (10, 10), it draws two horizontal lines (y=10 and y=60) of length 100, and two vertical lines (x=10 and x=110) of length 50. That’s 300 pixels total, but the library does it in about 0.8 ms. The filled version uses a loop: for each row from y to y+h-1, it draws a horizontal line of width w. For a 100x50 filled rectangle, that’s 50 horizontal lines, each 100 pixels—5,000 pixels total. But because it uses drawFastHLine, it’s still fast: about 1.5 ms. You can also draw rounded rectangles with `drawRoundRect` and `fillRoundRect`, which add corner arcs. The corner radius is specified in pixels—say, radius 5. The arc uses Bresenham’s circle algorithm for the corners, so it’s a bit heavier. A filled rounded rectangle of 100x50 with radius 5 takes about 2.1 ms. If you’re drawing multiple shapes per frame, say 10 rectangles, that’s 21 ms, which leaves you 39 ms for other stuff at 60 fps. But the OLED’s refresh rate is typically 60 Hz, so you have 16.6 ms per frame. If you’re drawing too many shapes, you’ll need to use double buffering or only update changed regions.
Circles: pixel-perfect arcs
Circles use the midpoint circle algorithm, which also avoids floating point. The function `drawCircle(x0, y0, r, color)` draws an outline circle of radius r at center (x0, y0). For a radius of 20 pixels, the circumference is about 126 pixels (2*pi*20). The algorithm steps through octants, so it’s efficient. A radius 20 circle takes about 0.6 ms to draw. Filled circles (`fillCircle`) use the same algorithm but fill horizontal lines between the top and bottom of each octant. For a radius 20 circle, it fills about 1,257 pixels (pi*20^2), taking about 1.2 ms. You can also draw circles with arbitrary thickness by drawing multiple concentric circles, but that’s manual. The display’s pixel density is about 74 DPI (128 pixels / 1.72 inches width), so a circle of radius 20 is about 0.54 inches across—big enough for a UI button. If you want anti-aliasing, you’re out of luck on monochrome; you’d need a grayscale OLED or use dithering. Dithering involves turning neighboring pixels on/off to simulate gray, but it’s complex and slow. For a 2.42 inch OLED, you’re better off sticking to solid shapes.
Triangles and polygons: custom shapes
Triangles are drawn with `drawTriangle(x0, y0, x1, y1, x2, y2, color)` and `fillTriangle`. The outline version draws three lines. The filled version uses a scanline algorithm: it sorts the three vertices by y, then for each scanline between the top and bottom, it calculates the x intersections and draws horizontal lines. For a triangle with vertices (10,10), (100,10), (55,60), the area is about 2,250 pixels (0.5 * base * height = 0.5 * 90 * 50). Filling it takes about 1.0 ms. You can draw any polygon by breaking it into triangles (triangulation) or by using a custom function that draws lines between vertices. But the library doesn’t have a generic polygon function—you’d need to write a loop that calls drawLine for each edge. For a pentagon, that’s 5 lines. For a complex shape like a star, you’d compute the vertices and connect them. The display’s resolution limits detail: a star with 10 points might have points only 2 pixels wide, so it’s blocky. You can also use bitmaps—pre-render the shape as a monochrome bitmap array and call `drawBitmap(x, y, bitmap, w, h, color)`. A bitmap of a 32x32 star takes 128 bytes (32*32/8) and draws in about 0.4 ms.
Performance considerations and optimization
The SSD1309 controller has a 1KB GDDRAM (graphic display data RAM) that’s mapped to the pixels. When you call `display.display()`, it sends the entire framebuffer over SPI. At 10 MHz SPI clock, sending 1KB takes about 0.8 ms (1,024 bytes * 8 bits / 10,000,000 bits/s = 0.0008192 s). But the library might send it in chunks (page by page), which adds overhead. If you’re drawing shapes frequently, you can reduce SPI traffic by only updating changed regions. For example, if you draw a circle at (10,10) and later move it to (20,10), you can clear the old circle and draw the new one, then send only the affected pages. The display’s page height is 8 pixels, so a circle of radius 20 spans 40 rows, which is 5 pages (40/8). Each page has 128 bytes, so you’d send 640 bytes instead of 1,024—a 37% reduction. You can implement this by tracking dirty rectangles. But the library doesn’t do this automatically—you’d need to modify the library or use a custom framebuffer. Another optimization: use the display’s hardware scrolling. The SSD1309 supports vertical and horizontal scrolling by setting registers. You can scroll a shape across the screen without redrawing it—just set the scroll parameters. For example, to scroll a rectangle from left to right, you set the horizontal scroll offset and increment it. The display handles the pixel shifting in hardware, so it’s free in terms of CPU time. But scrolling is limited to the entire screen or a fixed region, not arbitrary shapes.
Common pitfalls and debugging
One big issue: the 2.42 inch OLED often comes with the SH1106 controller instead of SSD1306. The SH1106 has a 132x64 pixel RAM, but the display only shows 128x64. So you need to offset the columns by 2. In Adafruit’s library, you can set `display.begin(SH1106_SWITCHCAPVCC, 0x3C)` and it handles the offset. If you use the wrong init, shapes will be shifted by 2 pixels. Another pitfall: the SPI pins are shared with the ICSP header on Arduino, so if you’re using other SPI devices, you need separate CS pins. The display’s CS pin is active low—pulling it low selects the display. If you leave it floating, the display might ignore commands. Also, the display’s VCC is 3.3V, but the logic pins are 5V tolerant on some versions—check the datasheet. The SSD1309’s absolute maximum for logic pins is 5.5V, so 5V logic is okay, but the VCC must be 3.3V. If you feed 5V to VCC, the display draws 30 mA and might overheat—the maximum is 50 mA, so it’s borderline. Use a 3.3V regulator like the AMS1117-3.3. For current, the display draws 20 mA typical, 25 mA max with all pixels on. So a 3.3V rail from an Arduino’s built-in regulator (which can supply 150 mA) is fine.
Real-world example: drawing a gauge
Let’s say you want to draw a speedometer gauge on the 2.42 inch OLED. You’d draw a large circle (radius 30) for the outer rim, a smaller circle (radius 25) for the inner face, and lines for tick marks. The outer circle takes 0.8 ms, the inner circle 0.6 ms, and 10 tick marks (each a line from radius 25 to 30) take 0.2 ms total. Then you draw a needle (a line from center to radius 20) which takes 0.1 ms. Total draw time: 1.7 ms. Plus the display update (0.8 ms), you’re at 2.5 ms per frame, leaving 14.1 ms for sensor reading and logic. That’s plenty for a 60 Hz update. If you want to animate the needle, you redraw the gauge background once (static) and only update the needle. You can store the background in a separate framebuffer and copy it to the main buffer before drawing the needle. That avoids clearing the entire screen. The library’s `display.drawBitmap` can copy the background from a pre-rendered buffer. For a 128x64 background, that’s 1KB, copied in about 0.4 ms using memcpy. Then the needle takes 0.1 ms, total 0.5 ms per frame—much faster. You can also use the display’s hardware to store the background in the GDDRAM and just overwrite the needle region, but that requires careful page management.
Data on memory and speed
Here’s a table of typical draw times for common shapes on a 2.42 inch OLED with an Arduino Uno at 16 MHz, using Adafruit GFX and SPI at 10 MHz:
| Shape | Size | Draw Time (ms) | Pixels Affected |
|---|---|---|---|
| Pixel | 1x1 | 0.002 | 1 |
| Horizontal line | 128x1 | 0.2 | 128 |
| Vertical line | 1x64 | 0.15 | 64 |
| Diagonal line | 128x64 | 0.5 | 128 |
| Rectangle outline | 100x50 | 0.8 | 300 |
| Filled rectangle | 100x50 | 1.5 | 5,000 |
| Circle outline | radius 20 | 0.6 | 126 |
| Filled circle | radius 20 | 1.2 | 1,257 |
| Triangle outline | 90x50 | 0.3 | 3 lines |
| Filled triangle | 90x50 | 1.0 | 2,250 |
| Full frame clear |