U8g2 vs Adafruit SSD1306 for OLED Animations — Which Library?
I get this question every workshop week: “Should I install Adafruit SSD1306 or U8g2 for animations?” Both draw white pixels on the same 0.96" panel. Both can blink a logo. The difference shows up when the Uno is out of SRAM, when the seller shipped an SH1106 labeled as SSD1306, or when you want thirty tiny frames instead of three.
This is the comparison I wish I had printed on the lab wall. It is biased toward animation work — not toward drawing a single “Hello” string — and it includes side-by-side blink sketches, migration notes, and a recommendation matrix you can trust for Uno vs ESP32.
The short answer
- Start with Adafruit SSD1306 + GFX if you are teaching beginners, using examples from
Adafruit guides, or exporting from tools that target
drawBitmapand GFX primitives. - Prefer U8g2 when you need SH1106 support in one constructor name, tighter RAM options, huge font selection, or page-buffer mode on a memory-starved AVR board.
- On ESP32 with plenty of RAM, either is fine; pick the ecosystem your exporters and teammates already use.
I still keep both libraries installed. Switching mid-project is a few hours of renaming calls — not a rewrite of your art.
How they think about the screen
Adafruit SSD1306: full framebuffer by default
You allocate (or get) a 128×64 / 8 = 1024-byte RAM buffer. Every drawPixel,
drawBitmap, and print edits that buffer. display.display() pushes the
whole buffer over I2C/SPI. Mental model: paint a canvas, then photograph it to the glass.
That model is easy to reason about for animation: clear → draw frame N → display → wait → next frame. It also costs a fixed 1 KB of precious Uno SRAM whether your frame is busy or empty.
U8g2: constructor chooses the buffer strategy
U8g2 constructors encode the chip, resolution, bus, and buffer mode. Common ones for makers:
- Full buffer (
*fvariants) — similar idea to Adafruit: large RAM, single send after drawing. - Page buffer / 1-page (
*1) — only a strip of rows lives in RAM. You loop withfirstPage/nextPageand redraw the scene for each page. - 2-page (
*2) — middle ground.
// examples — names vary by board/bus; check U8g2 examples
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// full buffer (~1 KB+ housekeeping)
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// page buffer — much less RAM, redraw per page
Page mode is the reason U8g2 fans love AVR. The trade-off: animation code that builds a complex scene must be callable repeatedly inside the page loop. Casual “draw once then display” does not map 1:1.
Drawing bitmaps: drawBitmap vs XBM helpers
Adafruit path most animation tools emit:
display.drawBitmap(0, 0, frame0, 128, 64, SSD1306_WHITE);
display.display();
Bytes are usually horizontal MSB packing matching Adafruit / image2cpp style. U8g2 often wants XBM
bit order (vertical bits in classic XBM) or you use drawXBMP / drawBitmap
carefully with the right format flag. Practical rule from support emails: do not paste an Adafruit
array into U8g2 and assume the image is upright. Re-export for the target library, or convert
packing once with a known-good script.
If you already generate frames in OLED Animation Maker, pick the Adafruit or U8g2 export explicitly so bit order matches. That single choice prevents the classic “my logo is shredded spaghetti” afternoon.
Speed: what actually limits you
For 128×64 over hardware I2C, the bus and the full-buffer transfer dominate — not your C++ circle math. Adafruit full buffer and U8g2 full buffer feel similar on Uno when both send ~1 KB each frame. Differences show up when:
- You redraw a huge scene inside every U8g2 page (CPU bound on AVR).
- You call Adafruit
display()every millisecond while also doing Serial prints. - You run SPI OLEDs — both libraries get faster transfers; then CPU drawing cost matters more.
In class I target 15–25 FPS for simple blinks. Above that, human eyes rarely thank you on a tiny monochrome panel, and USB power noise plus long jumpers create mystery flicker people blame on “the wrong library.”
Flash and RAM: Uno vs ESP32
| Constraint | Adafruit SSD1306 (typical) | U8g2 (typical) |
|---|---|---|
| Uno RAM for frame buffer | ~1024 B full buffer (fixed) | ~1024 B full, or much less in page mode |
| Uno flash libraries + fonts | GFX + driver: moderate | Can grow fast if you pull large fonts |
| ESP32 RAM | Comfortable | Comfortable; still avoid shipping unused fonts |
| Two 128×64 panels on Uno | Often too tight | Page mode can save the project |
| PROGMEM animation frames | Same math for both — frames still cost 1 KB each full screen | Same |
Remember: choosing U8g2 does not shrink your PROGMEM art. A 12-frame 128×64 loop is still ~12 KB of flash either way. Library choice mainly changes the live buffer and the driver feature set. For frame-count math see Uno OLED memory limits.
SH1106 and odd modules
Vendor modules mislabeled as SSD1306 but shipping SH1106 are everywhere. Symptom: image looks shifted by about two pixels or wraps weirdly. U8g2’s strength here is a clear constructor for SH1106 so the driver maps columns correctly. Adafruit users often move to Adafruit_SH110X or hack an X offset.
If a kit is mixed (some SSD1306, some SH1106), U8g2 constructors per board avoid shipping “magic +2” firmware variants. For the deeper hardware story, read SH1106 vs SSD1306.
Learning curve and community
- Adafruit: Excellent beginner docs, CircuitPython cousins, GFX tutorials everywhere. Function names map cleanly to what teachers already show on projectors.
- U8g2: Steeper first hour (constructors look loud), then very powerful. Font list is unmatched. Page loop surprises people coming from Adafruit.
For a first week Arduino course, I stay on Adafruit so students are not debugging constructors while learning I2C. For a product firmware pass on AVR, I often migrate the UI to U8g2 page mode to claw back RAM for sensors.
Side-by-side: simple 2-frame blink
Same idea — two PROGMEM bitmaps named eyeOpen and eyeClosed (replace with your
exported arrays). Timing via millis(), not delay().
Adafruit SSD1306 version
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
// placeholder — paste real PROGMEM arrays from your exporter
extern const unsigned char eyeOpen[];
extern const unsigned char eyeClosed[];
unsigned long lastSwap = 0;
bool lidClosed = false;
void setup() {
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) for (;;);
display.clearDisplay();
display.display();
}
void loop() {
unsigned long now = millis();
if (now - lastSwap > (lidClosed ? 120UL : 900UL)) {
lastSwap = now;
lidClosed = !lidClosed;
display.clearDisplay();
display.drawBitmap(0, 0, lidClosed ? eyeClosed : eyeOpen, 128, 64, SSD1306_WHITE);
display.display();
}
}
U8g2 full-buffer version
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
extern const unsigned char eyeOpen[];
extern const unsigned char eyeClosed[];
unsigned long lastSwap = 0;
bool lidClosed = false;
void setup() {
u8g2.begin();
}
void loop() {
unsigned long now = millis();
if (now - lastSwap > (lidClosed ? 120UL : 900UL)) {
lastSwap = now;
lidClosed = !lidClosed;
u8g2.clearBuffer();
// use the draw call that matches your export bit order
u8g2.drawXBMP(0, 0, 128, 64, lidClosed ? eyeClosed : eyeOpen);
u8g2.sendBuffer();
}
}
U8g2 page-buffer version (RAM saver)
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
extern const unsigned char eyeOpen[];
extern const unsigned char eyeClosed[];
unsigned long lastSwap = 0;
bool lidClosed = false;
void drawFrame() {
const unsigned char* bmp = lidClosed ? eyeClosed : eyeOpen;
u8g2.firstPage();
do {
u8g2.drawXBMP(0, 0, 128, 64, bmp);
} while (u8g2.nextPage());
}
void setup() {
u8g2.begin();
drawFrame();
}
void loop() {
unsigned long now = millis();
if (now - lastSwap > (lidClosed ? 120UL : 900UL)) {
lastSwap = now;
lidClosed = !lidClosed;
drawFrame();
}
}
Notice the page-mode sketch redraws the bitmap inside the do…while. If your “frame” is
actually fifty shape calls (robot eyes from vectors), wrap them in a function and call that function
inside the page loop — that is the main architectural shift coming from Adafruit.
Parts wiring (same for both)
| Part | Arduino Uno | ESP32 (common) |
|---|---|---|
| OLED VCC | 5V (typical) | 3.3V |
| OLED GND | GND | GND |
| SDA | A4 | 21 |
| SCL | A5 | 22 |
Library choice will not fix swapped SDA/SCL. Run an I2C scanner once before blaming drivers.
Migration tips (Adafruit → U8g2)
- Pick the constructor for your controller (SSD1306 vs SH1106) and bus (HW I2C).
- Replace
clearDisplay→clearBuffer(full) or rely on page loop clears. - Replace
display.display()→sendBuffer()ornextPagecompletion. - Map GFX calls:
drawLine/drawCircle/drawFramenames are similar but not identical — keep a cheat sheet open for one afternoon. - Re-export bitmaps; verify with a single static logo before porting the animation scheduler.
- Keep your
millis()state machine. Only the paint layer should change.
Migration the other way (U8g2 → Adafruit) is common when a teammate’s exporter only emits Adafruit code. Budget an hour to rename sends and confirm SH1106 units still look aligned.
When I use each (workshop rules of thumb)
- Hour-of-code robot eyes with circles: Adafruit — matches the shapes tutorial and most paste-ready sketches.
- Avatar blink with 8 cropped bitmaps on Uno + ultrasonic + servo: U8g2 page mode — reclaim RAM.
- ESP32 dashboard with Wi-Fi: Adafruit is fine; U8g2 if you need fancy fonts / SH1106 kits.
- Unknown ebay OLED in a mixed bin: try U8g2 SH1106 constructor before declaring the panel dead.
- Sharing code with MicroPython kids later: concepts transfer either way; Adafruit’s buffer model is closer to
framebuf.
Recommendation matrix
| Project situation | Pick | Why |
|---|---|---|
| Beginner + Adafruit docs | Adafruit | Lowest friction, great examples |
| Uno RAM almost full | U8g2 page | Small live buffer |
| Confirmed SH1106 hardware | U8g2 (or SH110X) | First-class constructor |
| Tool exports Adafruit arrays | Adafruit | Bit order already matched |
| Need many built-in fonts | U8g2 | Font ecosystem |
| Dual OLED animation on Uno | U8g2 page / or one panel | Two Adafruit buffers hurt |
| ESP32 + lots of RAM | Either | Team preference wins |
| Vector robot eyes only | Adafruit or U8g2 full | Page mode forces redraw boilerplate |
Common mistakes I see
- Installing U8g2 but still calling Adafruit APIs from a copied blog sketch.
- Using page mode while drawing only outside the page loop (blank or partial updates).
- Assuming U8g2 “is always faster” — full buffer transfers are still full buffer transfers.
- Linking every font header “just in case” and blowing flash on Uno.
- Never calling
display()/sendBuffer()after drawing (blank forever — library does not matter).
Fonts, HUDs, and mixed UI + animation
Pure bitmap animations barely care which library you pick. Mixed UIs do. If your robot shows a status line under the eyes, Adafruit’s default font is fine and tiny in flash. If you want a readable 12-pixel UI font plus icons, U8g2’s catalog saves you from shipping homemade fonts — but only include the one or two faces you call, or the linker still has to carry their tables on many setups.
A pattern that works on both: animate eyes in the upper band, reserve a 12–14 pixel strip at the bottom
for text, and redraw text only when values change. That dirty-flag approach matters more than micro-
optimizing the driver. Students who clear and reprint a long String every frame “because the
library is slow” usually invent their own performance problem.
Partial updates and dirty regions (expectations check)
Makers sometimes ask for “only send the pupil pixels.” Neither library makes true region patches trivial on classic SSD1306 I2C the way a modern GPU would. You usually rebuild the buffer (or pages) and send a full or page-sized chunk. If you need lower bus traffic, reduce resolution of the art (crop sprites), drop FPS, or move to SPI. Do not expect a library rename to invent a free partial-DMA path on Uno hardware.
Where U8g2 page mode helps is RAM, not magical bandwidth. You still pay CPU to recreate the scene for each page. Complex vector robot faces can feel softer in page mode on 16 MHz AVR; full buffer may look snappier if you still have the SRAM — measure with a wall-clock blink period rather than forum lore.
Team and toolchain considerations
If half your GitHub issues paste Adafruit snippets, staying on Adafruit reduces support load even when U8g2 is “better” on paper. If you already standardized on PlatformIO library specs for U8g2 across ESP boards, do not yank that for one blog sketch. Consistency beats purity. Export pipelines (maker tools, CI that checks sketch size) should name the target library explicitly so PRs do not mix bit orders.
FAQ
Can I use both libraries in one sketch?
Technically yes, practically a mess on Uno (two drivers, two buffers). Pick one per firmware image.
Which library does OLED Animation Maker target?
The tool can export for Adafruit SSD1306 and U8g2 — choose the matching option so bitmap packing lines up with your sketch.
Is Adafruit_SH110X better than U8g2 for SH1106?
Both work. If the rest of your project is already Adafruit GFX, SH110X keeps API familiarity. If you want one library for many panel types, U8g2 is broader.
Do animations look smoother on U8g2?
Smoothness comes from frame timing and how much you transfer each update — not from the brand name on the include. Same art, same bus, similar FPS for full buffers.
What about SPI OLEDs?
Both libraries support SPI variants. Wiring changes (CS/DC/RES); the animation scheduler stays the same. SPI often frees you from I2C bandwidth complaints.
Related
- Create SSD1306 animations
- PROGMEM frame limits on Uno
- SH1106 vs SSD1306
- GFX / U8g2 shapes
- Robot eyes with Adafruit GFX
- ESP32 OLED animation guide
- Image to byte array
Export frames for the library you chose
Draw or import animation frames, then download Adafruit or U8g2-ready code without hand-packing bits.
Open oledanimationmaker.com →