ssd1306 oled arduino esp32

SSD1306 Bitmap Scrambled or Sideways? It's the Draw Mode and Byte Order

Why a bitmap shows up scrambled, sideways, sheared or mirrored on an SSD1306 OLED, how to tell the cause from the pattern, and the one setting that fixes each.

· Mohammed Aquib Ansari

The first time I put a logo on an SSD1306, the code compiled, the display lit up, and what I got looked like a barcode that had been through a shredder. Nothing was wrong with the image or the wiring. The bytes were in a different layout from the one my draw call expected.

This keeps happening because there are at least three reasonable ways to pack a 1-bit image into bytes, and converters, libraries and the SSD1306 controller each use a different one. The good news is that every mismatch produces its own pattern, so you can usually tell what went wrong just by looking at the screen.

The three layouts, using one 8×8 letter

Here’s a capital F, 8 pixels square:

######..
#.......
#.......
####....
#.......
#.......
#.......
........

Horizontal, most significant bit first. This is what Adafruit GFX drawBitmap() expects. Each byte holds 8 pixels of a row, and bit 7 is the leftmost:

const uint8_t f_h[] PROGMEM = { 0xFC, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80, 0x00 };

XBM, least significant bit first. This is what U8g2 drawXBMP() and Adafruit drawXBitmap() expect. The rows are the same, but bit 0 is the leftmost pixel:

const uint8_t f_xbm[] PROGMEM = { 0x3F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x01, 0x00 };

Vertical pages. This is the SSD1306’s own display RAM layout, and it’s what image2cpp calls “Vertical – 1 bit per pixel”. Each byte is 8 pixels of a column, and bit 0 is the top:

const uint8_t f_v[] PROGMEM = { 0x7F, 0x09, 0x09, 0x09, 0x01, 0x01, 0x00, 0x00 };

All three are the same F. Only one of them is right for a given draw call.

Sideways: vertical bytes fed to drawBitmap()

Pass f_v to drawBitmap() and this is what you get:

.#######
....#..#
....#..#
....#..#
.......#
.......#
........
........

That’s the F flipped across its diagonal. Each column byte is being read as a row byte, so the image comes out transposed. On a full 128×64 logo, every 8×8 tile is transposed and the tiles end up in the wrong places too. Small images look sideways, and big ones look scrambled.

Fix: convert with the horizontal layout. If you really are writing straight into the display buffer, keep vertical pages and memcpy them in, one 128-byte page at a time. Don’t call drawBitmap() on them.

Mirrored in 8-pixel chunks: XBM vs drawBitmap()

Pass f_xbm to drawBitmap():

..######
.......#
.......#
....####
.......#
.......#
.......#
........

The F is mirrored left to right. On wider images it’s more confusing than that: each 8-pixel group is mirrored on its own, but the groups stay in order. Text turns into short reversed fragments, and diagonal lines turn into a sawtooth.

Fix: match the bit order to the call. drawBitmap() takes MSB-first data, while drawXBitmap() and U8g2’s drawXBMP() take XBM. Either switch the call or regenerate the array.

Sheared diagonally: the width doesn’t match

If the image leans and wraps around the screen, with each row shifted a bit further than the one above, the layout is fine but the width is wrong.

Rows are padded to whole bytes, so a 120-pixel-wide image uses 15 bytes per row. Call drawBitmap(0, 0, logo, 128, 64, WHITE) on it and the library reads 16 bytes per row. Each row then starts one byte (8 pixels) further into the data than it should, and the error builds up row by row into a slant.

Fix: pass the image’s real width and height, not the screen size. A converter that emits #define LOGO_WIDTH and #define LOGO_HEIGHT alongside the array makes this mistake hard to make.

Actually rotated 90°: the display or the source

If the image is intact but rotated a quarter turn, the bytes are probably fine:

  • setRotation() in Adafruit GFX swaps the logical width and height: at rotation 1 a 128×64 screen becomes 64×128. A sketch you copied may set it.
  • Portrait source art on a landscape panel will look sideways if it was drawn for a module mounted the other way. Rotate it in the converter rather than in an image editor, so the output keeps the panel’s 128×64 size.
  • U8g2 sets rotation in the constructor (U8G2_R0 to U8G2_R3). Check which one your sketch uses.

Two problems that look like layout bugs but aren’t

Every other line, or a squashed image. If the display shows your image on alternate rows, or squashed into half the height, the library was probably set up for a 128×32 panel while the module is 128×64 (or the other way round). The two sizes need different controller pin settings. Check Adafruit_SSD1306 display(128, 64, …) or your U8g2 constructor.

A 2-pixel shift and a garbage column on the right. Many 1.3” modules sold as “SSD1306” actually use an SH1106. That chip has 132 columns of RAM with the visible 128 offset by two, so an SSD1306 driver draws everything shifted, with a stripe of noise down one edge. Use an SH1106 driver: Adafruit_SH110X, or U8g2’s U8G2_SH1106_128X64_… constructors.

How I check before flashing

I generate the array with the image to C array converter and look at the preview first. That preview is decoded from the generated bytes, not from the source image. If I’ve picked vertical pages when I meant horizontal, the preview shows exactly the transposed mess the OLED would, before I’ve spent a minute compiling and uploading. It also places the image on the actual panel size, so an image that’s too wide shows up cropped there instead of sheared on the hardware.

For the full workflow, from sizing a logo to thresholds and dithering to the PROGMEM rules on AVR, see display a custom logo on an SSD1306.