SSD1306 scrolling text on Arduino (and when to skip it)
Almost every SSD1306 tutorial shows scrolling text. Adafruit even ships
startscrollleft() / startscrollright() in the library.
It is useful. It is also the same effect everyone has seen on a cheap clock from 2014.
I still use hardware scroll for status lines and long sensor strings. For a name intro or a logo splash, I stopped. This post walks through all three approaches with real code, then gives a simple decision rule so you do not waste flash on the wrong effect.
What you need before any of this works
- Working I²C OLED (Hello World already shows text)
- Adafruit SSD1306 + GFX, or U8g2
- Known address (
0x3Cor0x3D)
If you do not have Hello World yet, start here: Arduino OLED I2C tutorial.
Method 1 — Hardware scroll
The SSD1306 can shift its own RAM. Your MCU prints once, then asks the chip to scroll.
display.clearDisplay();
display.setTextSize(2);
display.setCursor(0, 20);
display.print("HELLO");
display.display();
display.startscrollleft(0x00, 0x0F);
delay(4000);
display.stopscroll();
Pros: almost no CPU work, tiny sketch, works on Uno. Cons: you scroll whatever is already on the glass. You cannot make letters fall from the top or assemble from random pixels. Speed options are coarse. Diagonal helpers exist, but it still feels like the whole framebuffer is sliding.
Page arguments (0x00, 0x0F) select which horizontal bands scroll.
Wrong values make half the screen move and half stay — looks broken until you re-read the datasheet.
Method 2 — Software ticker
You redraw the string at a moving X each frame. More control. Still just text.
int x = 128;
const char* msg = "Temp 28.4 C Humidity 61% ";
void loop() {
display.clearDisplay();
display.setTextSize(1);
display.setTextWrap(false);
display.setCursor(x, 28);
display.print(msg);
display.display();
x -= 2;
if (x < -((int)strlen(msg) * 6)) x = 128;
delay(20);
}
Pros: pixel speed control, long messages, easy to update the string from sensors. Cons: you spend CPU clearing and reprinting. Visually you still get a marquee. Good for WiFi SSID, song titles, error strings.
Measure text width carefully if you use setTextSize(2) — character width doubles.
I have shipped tickers that “jump” because the reset threshold used size-1 math on size-2 text.
Method 3 — Frame-based name / logo animation
Frame animation costs flash. Roughly 1 KB per full 128×64 frame. You pay that cost when you want the text to build, not slide.
Examples I actually ship:
- Desk name plate that rains letters into place once on power-up
- Gadget boot screen where a logo fills in pixel by pixel
- Classroom demo of PROGMEM + drawBitmap without teaching hex editors first
That is the job of Name Assemble: type the string, pick a style, export the frame list. Same Adafruit / U8g2 stack. Different content in flash. Name walkthrough: Arduino OLED name animation.
Complete hardware-scroll demo you can flash today
This sketch shows a banner, scrolls it, stops, then prints a second line. Use it to confirm your panel supports the scroll commands before you invent anything fancier.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 20);
display.print(F("HELLO"));
display.display();
display.startscrollleft(0x00, 0x0F);
delay(3500);
display.stopscroll();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 28);
display.print(F("scroll done"));
display.display();
}
void loop() {}
If HELLO never moves, you may be on a clone controller, wrong library, or a panel that only partially implements scroll. Fall back to the software ticker below — it works on every SSD1306 I own.
Complete software ticker with millis timing
Blocking delay() is fine for demos. Real projects need buttons and sensors to keep working.
Here is a non-blocking marquee:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
const char* msg = " Temp 28.4C Hum 61% OK ";
int16_t x = 128;
uint32_t lastMs = 0;
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextWrap(false);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
}
void loop() {
if (millis() - lastMs < 30) return;
lastMs = millis();
display.clearDisplay();
display.setCursor(x, 28);
display.print(msg);
display.display();
x -= 2;
// size 1 ≈ 6 px per char
int16_t w = (int16_t)strlen(msg) * 6;
if (x < -w) x = 128;
// your button / sensor reads can go here safely
}
When I still choose scroll on purpose
- WiFi portal showing SSID + IP on one line
- Error strings longer than 21 characters at text size 1
- Classroom demos where flash is already full of sensor code
When I do not: product boot screens, gift badges, anything meant to feel “designed.” Those get Name Assemble frames instead.
Side-by-side comparison
| Hardware scroll | Software ticker | Name assemble | |
|---|---|---|---|
| Flash cost | Very low | Very low | High (many frames) |
| CPU cost | Very low | Medium | Medium (playback) |
| Look | Whole screen slides | Marquee text | Pixels build shape |
| Best for | Demos, banners | Live status | Boot / gift intro |
| Uno friendly | Yes | Yes | Short only |
Quick decision rule
- Long changing text → software ticker
- Tiny sketch, any text already on screen → hardware
startscroll* - One splash that should feel designed → name / logo assemble animation
- Uno + long particle style → shorten frames or use ESP32
Mixing both is fine. Scroll a status line under a static icon. Play a 2-second assemble intro, then drop into your normal UI.
Common scroll bugs I still see in forums
- Forgetting
stopscroll()before drawing a new UI — leftover motion looks cursed - Calling scroll before
display()— nothing visible to scroll - SH1106 modules that ignore hardware scroll quirks — test on your real panel
- Blocking
delay()in ticker loops so buttons never respond — usemillis()timing
See assemble styles next to a live OLED mockup
Type a name, click Outside Random or Pixel Rain, export code if you like it.
Open Name Assemble →Related searches: ssd1306 scrolling text · arduino oled scrolling text · startscrollleft · oled text animation · ssd1306 name animation
Frequently asked questions
Is startscrollleft bad?
No. It is perfect for short demos and status tickers. It just cannot do particle-style intros.
Does name animation need more flash than scrolling text?
Yes. Scrolling prints a string. Assemble stores many bitmaps. Plan flash budget, especially on Uno.
Can U8g2 do hardware scroll the same way?
APIs differ. U8g2 has its own helpers. Concept is similar: either move buffer content or redraw text each frame.
Library comparison for animations?
See U8g2 vs Adafruit.
Why do examples pass 0x0F if a 128×64 panel only has pages 0–7?
Adafruit’s demo often copies startscrollleft(0x00, 0x0F). On a 64-pixel SSD1306 the real pages are 0 through 7 (8 pixels each). Prefer startscrollleft(0, 7) for a full-height scroll, and a tighter range such as (4, 7) when a title in the top pages must stay still. A 128×32 panel only has pages 0–3.
My size-2 ticker jumps instead of looping. What width should I use?
Do not assume 6 pixels per character once setTextSize(2) is on. Size 2 is 12 pixels wide. Call getTextBounds() and reset x when it passes -w. The jump is that threshold, not a broken OLED.
Hardware scroll works on my SSD1306 and does nothing on another module. Same sketch.
Some SH1106 boards ignore or botch the SSD1306 scroll commands, and they are 132×64 internally so a normal frame can already look shifted. If stopscroll and a software ticker still fail, treat it as the wrong controller. The software marquee does not need the scroll command at all.
Can I scroll a status line and still draw a bitmap?
Call stopscroll() first. Drawing while the controller is scrolling smears the next display(). A name-assemble intro should not run underneath startscrollleft. Play the frames, stop, then start a ticker only if the UI still needs one.
Scroll the status line, leave the title put
A full-screen hardware scroll is why people get bored of startscrollleft. The useful version keeps rows 0–15 still and slides only the lower half. On a 128×64 SSD1306 each page is 8 pixels, so pages 0 and 1 are the title and pages 4–7 are a safe band for a long SSID. I print both strings, push the buffer once, then start the scroll. The controller moves its own RAM after that; the Uno is free.
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print(F("Lab node")); // pages 0-1, not scrolled
display.setCursor(0, 40);
display.print(F("SSID Lab-Guest 192.168.1.40"));
display.display();
display.startscrollleft(4, 7); // pages 4-7 only
// later, before any new UI:
// display.stopscroll();
// display.clearDisplay();
Wiring for this demo is the same 4-pin I2C hookup as the rest of the site: Uno/Nano SDA A4, SCL A5, address 0x3C or 0x3D, reset -1. Mega moves SDA/SCL to pins 20 and 21. ESP32 uses GPIO21 and GPIO22 with the module on 3.3 V. ESP8266 NodeMCU uses D2 and D1. None of that changes the page numbers. A 128×32 display does: only pages 0–3 exist, so startscrollleft(4, 7) scrolls nothing and looks like a dead command.
If the title slides too, the start page is wrong. Page 0 begins at y=0, page 4 at y=32. Text drawn at y=20 already sits in page 2. Put the frozen line at y=0 and the moving line at y=40 or lower, then pass that split into startscrollleft. Call stopscroll() before the next clearDisplay(), or the following frame shears.
I still do not use this effect for a gift name. Hardware scroll cannot rain pixels into a word. That is the frame animation, at about 1024 bytes per full frame. The scroll above costs a string in flash and is the right tool when the text changes every time someone joins the network.
Marquee bugs with a specific cause
- Half the letters move, half smear into the title. The start or stop page overlaps the title. On 128×64 use pages 0–7 only.
(4, 7)leaves y=0 alone if you actually drew the title in pages 0–1.(0, 0x0F)from a copied demo scrolls every page the chip will accept, which is the whole glass. - Software ticker resets too early at text size 2. The wrap math still uses 6×
strlen. Size 2 is 12 pixels per glyph. Measure withgetTextBounds(msg, 0, 0, &x1, &y1, &w, &h)and setx = 128whenx < -((int16_t)w). - Scroll never starts, Serial is silent, glass is black. That is not a scroll bug.
beginfailed, the address jumper is 0x3D while the sketch says 0x3C, or RES is held low because the constructor was given a real GPIO on a 4-pin board. Prove Hello World before you time a marquee. - Ticker is smooth, then a sensor read makes it stutter. A blocking DHT or Wi-Fi call inside the 30 ms slot. Keep the marquee on
millis(), and let a slow sensor update acharbuffer on a one-second timer. Hardware scroll avoids this because the chip moves pixels while you block — which is also why you muststopscroll()before you draw the result. - SH1106 module: SSD1306 scroll does nothing or looks offset. Fall back to the software ticker. Those panels are 132 columns internally; an SSD1306 constructor already shifts the still image by about two pixels, and the hardware scroll command is a second disappointment. Fix the driver first.
- Contrast drops the moment scroll starts. Unrelated commands got copied into
loop(). Set contrast once afterbegin(SSD1306_SETCONTRASTthen0xCF). Scrolling does not dim the panel. A value of 0 does.
Same marquee, different boards
| Board | I2C | What I watch |
|---|---|---|
| Uno / Nano | A4 / A5, 5V | Hardware scroll and a software ticker both fit. Frame-based names run out of flash first |
| Mega 2560 | 20 / 21, 5V | More flash for a name animation if you outgrow scroll. Move the I2C wires or the glass stays black |
| ESP8266 NodeMCU | D2 / D1, 3.3V | Wire.begin(D2, D1) before begin. Software ticker has room to spare |
| ESP32 | GPIO21 / GPIO22, 3.3V | Use this when you want both a long ticker and a multi-frame intro. SPI only if a later bitmap loop needs the speed |
I2C at 400 kHz is enough to redraw a one-line ticker. You are not pushing a fresh 1024-byte bitmap every frame unless you clear and reprint the whole buffer yourself — which the software ticker does, and hardware scroll does not. That is the speed difference that matters here. SPI’s extra wires help a full-frame animation loop; they do not make startscrollleft look better.
Breadboard noise shows up as a ticker that freezes for a second and then jumps. Reseat SDA and SCL before you tune FRAME_MS. A duplicate device at 0x3C (a second OLED, an untouched address jumper) makes begin fail or the scroll stutter. One display per address.