ssd1306 oled arduino progmem

Bitmap Renders Inverted or as Noise on a 128×64 OLED

A bitmap that shows as a negative, a solid block or random noise on an SSD1306 OLED: what each symptom means, the AVR PROGMEM trap, and how to fix them.

· Mohammed Aquib Ansari

There are two OLED bitmap failures I see over and over on forums. In one, the image is there but it’s a photographic negative. In the other, there’s no image at all, just static. They feel similar (“my bitmap is broken”), but they have completely different causes, and the fix for one does nothing for the other.

If your image is there but scrambled, sideways or mirrored, that’s a byte-layout problem, covered in SSD1306 bitmap scrambled or sideways. This post covers the other two.

Inverted: the image is right, but the light is wrong

On an OLED a bit set to 1 means “lit”, and a lit pixel is the bright one. So when a converter turns your artwork into bits, it has to decide which pixels are “on”. The usual default is that bright pixels are on.

That’s fine for a white logo on black. Most logos, though, are drawn dark on a white background. Convert one of those with the default and the background, the bright part, becomes thousands of lit pixels, while your logo becomes the unlit hole in the middle. On screen it’s a negative.

Here’s a capital F as drawBitmap() wants it, followed by the same F converted from dark-on-white artwork without inverting:

const uint8_t f_lit[]  PROGMEM = { 0xFC, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80, 0x00 };
const uint8_t f_dark[] PROGMEM = { 0x03, 0x7F, 0x7F, 0x0F, 0x7F, 0x7F, 0x7F, 0xFF };

Each byte in the second array is the bitwise NOT of the matching byte in the first. You can fix it at any of three points:

  1. In the converter. Turn on Invert, or flip the threshold logic, so dark pixels become lit. This is the right fix if the bitmap is always drawn the same way.
  2. In the draw call. Adafruit GFX’s drawBitmap(x, y, bmp, w, h, color) only draws the 1 bits and leaves the 0 bits alone, so you can fillRect() a white area and draw the bitmap with SSD1306_BLACK. Or use the overload with a background colour, drawBitmap(x, y, bmp, w, h, SSD1306_BLACK, SSD1306_WHITE), which paints both.
  3. On the whole panel. display.invertDisplay(true) inverts the entire screen in hardware. It’s handy for a “selected” flash effect, but it inverts your text as well, so it’s usually not what you want for a single logo.

The solid rectangle variant

If you get a lit rectangle where the image should be, with no visible picture, you’re probably converting a transparent PNG. Transparent pixels still carry RGB values, which are often black. Depending on how the converter treats alpha, the whole background can land on one side of the threshold and come out as a solid block.

The fix is to decide what transparent should mean before thresholding. The image to C array converter has a colour for this (“Transparent →”): set it to black for a lit logo, or to white if you’re inverting.

U8g2 has its own twist

U8g2’s drawXBMP() draws in the current draw colour, and by default it’s in solid bitmap mode, which paints the 0 bits as well. If your bitmap is overwriting text behind it with a black box, call u8g2.setBitmapMode(1) for transparent mode. If the bitmap itself is inverted, u8g2.setDrawColor(0) swaps which bits light up.

Noise: the bytes the display gets aren’t your image

Random static is a different problem. The display is faithfully drawing bytes, but they aren’t the bytes of your image. These are the causes I’ve hit, roughly in order of how often:

1. const without PROGMEM on an AVR board

This one is nasty because nothing warns you. Adafruit GFX has two drawBitmap() overloads:

void drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h, uint16_t color);  // reads flash
void drawBitmap(int16_t x, int16_t y, uint8_t *bitmap, int16_t w, int16_t h, uint16_t color);        // reads RAM

A const uint8_t logo[] matches the first one, which reads with pgm_read_byte(). That’s correct only if the array really is in flash. On an Uno or Nano, a const array without PROGMEM is copied into RAM at startup. The library then reads flash at what is actually a RAM address, which means whatever program code happens to sit there. The result is noise.

Fix: add PROGMEM (const uint8_t logo[] PROGMEM = { … }). On ESP32, RP2040 and other 32-bit boards there’s no separate flash address space, so this bug can’t happen there, and PROGMEM is just an empty macro.

2. The display buffer was never allocated

Adafruit_SSD1306 allocates its 1,024-byte frame buffer in begin(). An Uno has 2 KB of RAM in total, so a big sketch can leave too little, and then begin() returns false. A sketch that ignores that return value keeps drawing into a buffer that doesn’t exist. Always check it:

if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
  Serial.println(F("SSD1306 allocation failed"));
  for (;;) {}
}

If it fails, the fix is to free RAM: wrap string literals in F(), move tables to PROGMEM, or switch to U8g2’s page-buffer mode (constructors ending in _1), which keeps only one 128-byte page in RAM.

3. The wrong format entirely

An RGB565 array meant for a colour TFT, or a 4-bit grayscale array, drawn with a 1-bit call comes out as noise with a faint structure, often repeating horizontal bands. Check that the converter output really is 1-bit.

4. An SH1106 on an SSD1306 driver

If the noise is a vertical strip along one edge and the rest of the image is roughly right but shifted by 2 pixels, your “SSD1306” is really an SH1106. Use an SH1106 driver.

5. Corrupted I²C traffic

Long jumper wires, no pull-ups, or a 400 kHz-or-faster bus on a breadboard can corrupt bytes in transit. That shows up as speckles that change between frames, rather than a stable wrong picture. Try Wire.setClock(100000) and shorter wires.

A quick way to tell which one you have

  • Stable negative image: invert, in the converter or in the draw call.
  • Solid block: transparency or threshold. Set the transparent colour and check the preview.
  • Stable noise on an Uno or Nano: PROGMEM first, then check begin().
  • Noise that changes every frame: wiring and bus speed.
  • Strip down one edge: SH1106.

When I’m not sure, I regenerate the bitmap in the converter with the settings I think I used, check its preview (which is decoded from the generated bytes), and compare the first few bytes with what’s in my sketch. If the preview looks right and the bytes match, the array isn’t the problem. It’s the way the sketch reads it.