Skip to content
Ships from Sayulita · Carbon-offset delivery JournalTrade ProgramEN / ES

How to display a bitmap on a 0.96 inch OLED?

To display a bitmap on a 0.96 inch OLED, you need to convert your image into a byte array that the OLED driver can interpret, then send that data over SPI or I2C to the display controller. The most common controller for these 128x64 monochrome OLEDs is the SSD1306, which expects pixel data in a specific format: each byte represents a vertical column of 8 pixels, with the least significant bit (LSB) at the top. So if you have a 128x64 pixel bitmap, you’re looking at 128 columns times 64 rows divided by 8 bits per byte, giving you exactly 1024 bytes of data. You can generate this byte array using tools like 0.96 inch 128x64 spi i2c oled display image converter software, or by writing a Python script with the Pillow library. The key is to match the byte order to the SSD1306’s page addressing mode, where each page is 8 rows tall. For a 128x64 display, you have 8 pages (0 to 7), and each page spans 128 columns. When you send the bitmap, you typically start at page 0, column 0, and send all 128 bytes for that page, then move to page 1, and so on. This is the foundation for displaying any static image, splash screen, or even animated frames if you update the buffer fast enough.

Let’s break down the hardware specifics. A typical 0.96 inch OLED module operates at 3.3V logic, though many have built-in voltage regulators that allow 5V input on the VCC pin. The display draws around 20mA to 30mA with all pixels lit, but in practice, with a typical bitmap that has around 30% to 50% white pixels, you’re looking at 10mA to 15mA. The SSD1306 supports both SPI and I2C interfaces. SPI is faster, with clock speeds up to 10 MHz, so you can update the entire 1024-byte buffer in under 1 millisecond. I2C is slower, typically running at 400 kHz or 100 kHz, and takes about 2.5 milliseconds to send the same data at 400 kHz. But I2C uses only two wires (SDA and SCL) plus power and ground, whereas SPI needs four wires (MOSI, SCK, CS, DC) plus reset. For most microcontroller projects, the choice depends on pin availability and speed requirements. If you’re using an Arduino Uno, SPI is more common because the hardware SPI pins are available, but you can bit-bang I2C on any two digital pins. For ESP32 or STM32, both interfaces work well, and you can even use DMA to send data without blocking the CPU.

Now, the actual bitmap conversion process. You start with a source image, which should be monochrome (black and white) and ideally 128x64 pixels. If your image is larger, you need to resize it. If it’s color, you need to convert it to grayscale, then apply a threshold to get pure black and white. The threshold value is critical: a value of 128 (out of 255) is standard, but you might need to adjust it based on the image’s contrast. For example, a logo with sharp edges works well with a threshold of 128, but a photograph might require adaptive thresholding to avoid losing details. Once you have the binary image, you need to rearrange the pixels into the vertical byte format. In Python, you can use the PIL library to load the image, convert it to '1' mode (1-bit pixels), then extract the raw data. The raw data from PIL gives you horizontal bytes, but the SSD1306 expects vertical bytes. So you need to transpose the data: for each column (x from 0 to 127), you take 8 rows (y from 0 to 7 for page 0, then 8 to 15 for page 1, etc.), and pack them into a single byte. The topmost pixel in the column becomes the LSB. This is a common pitfall: if you just send the raw PIL data, your image will appear rotated or scrambled. Many online tools, like the LCD Image Converter or the online image2cpp tool, handle this transposition automatically. You can also use the lcd_image_converter library for Arduino, which generates the byte array in the correct format.

Here’s a practical example of the byte array structure. Suppose you have a simple 8x8 pixel image that’s a solid square. In the SSD1306 format, that would be 8 bytes, one per column. Each byte would be 0xFF (all 8 pixels lit). For a 128x64 display, the full array is 1024 bytes. If you want to display a checkerboard pattern, the byte values would alternate between 0xAA (10101010 in binary) and 0x55 (01010101). The table below shows a small section of the byte array for a 16x16 pixel image, with the first 8 columns of page 0:

Column (x) 0 1 2 3 4 5 6 7
Byte value (hex) 0x81 0x42 0x24 0x18 0x18 0x24 0x42 0x81
Binary (pixels top to bottom) 10000001 01000010 00100100 00011000 00011000 00100100 01000010 10000001

This pattern creates a small X shape in the top-left corner of the display. The first byte (0x81) means the top pixel and the bottom pixel of that 8-pixel column are lit, while the middle six are off. This is exactly how the SSD1306 interprets the data. When you send this byte array to the display, you need to set the correct memory addressing mode. The SSD1306 supports three modes: horizontal, vertical, and page. For bitmap display, page mode is simplest. You send a command to set the page start address (0xB0 to 0xB7 for pages 0 to 7), then set the column start and end addresses (0x00 to 0x7F for columns 0 to 127), then send the data bytes. The command sequence for initializing the display is well documented: you send 0xAE (display off), 0xD5 (set display clock divide ratio), 0x80, 0xA8 (set multiplex ratio), 0x3F (for 64 rows), 0xD3 (set display offset), 0x00, 0x40 (set start line to 0), 0x8D (enable charge pump), 0x14, 0x20 (set memory addressing mode), 0x00 (horizontal mode), 0xA1 (set segment remap to 1), 0xC8 (set COM scan direction to remapped), 0xDA (set COM pins hardware configuration), 0x12, 0x81 (set contrast), 0xCF (contrast value), 0xD9 (set pre-charge period), 0xF1, 0xDB (set VCOMH deselect level), 0x40, 0xA4 (display on resume), 0xA6 (normal display), 0xAF (display on). After that, you can send bitmap data.

Performance considerations matter. If you’re updating the bitmap frequently, like in an animation, you should use double buffering. Allocate a 1024-byte buffer in RAM, draw your bitmap into that buffer, then send the entire buffer to the display in one shot. This avoids flickering because the display updates in a single frame. On an Arduino Uno, which has only 2KB of SRAM, a 1024-byte buffer is half your RAM, so you need to be careful. On an ESP32, with 520KB of SRAM, it’s trivial. For SPI, you can use the SPI.transfer() function in a loop, but it’s faster to use a block transfer with SPI.transfer(buffer, size) if your library supports it. For I2C, you send data in chunks of 32 bytes or less, because the I2C buffer on most microcontrollers is limited. The Adafruit SSD1306 library, for example, sends data in 16-byte chunks. This means sending 1024 bytes takes about 64 I2C transactions, each with overhead. If you’re using a faster microcontroller like an STM32 at 72 MHz, you can push SPI at 10 MHz and update the display at 100 frames per second, but the OLED’s internal refresh rate is around 100 Hz, so you’re limited by the panel itself.

Real-world data on display performance: The SSD1306’s frame rate is typically 100 Hz to 120 Hz, meaning you can update the bitmap every 8 to 10 milliseconds. However, the human eye perceives smooth motion at 30 FPS, so you don’t need to update faster than that for most applications. For a static bitmap, you only need to send the data once. The display retains the image because it’s a matrix of capacitors that hold the charge, but it does need periodic refresh from the internal oscillator. The SSD1306 handles this automatically, so you don’t need to send the bitmap repeatedly. Power consumption is another factor: if you’re running on a battery, you can put the display to sleep with the 0xAE command, which cuts power to the OLED panel. The typical sleep current is under 10 µA, while active mode is around 20 mA. For a bitmap that’s mostly black, the power draw is lower because black pixels are off (the OLED pixels are current-driven, so black means no current). White pixels are on and draw about 100 µA per pixel at full brightness, but with 128x64 pixels, that’s 8192 pixels. If all are white, the total current is around 800 mA, which is unrealistic. In practice, the display driver limits the current, and a full white screen draws about 20 mA to 30 mA. A typical bitmap with 50% white pixels draws around 15 mA.

Software libraries make this easier. The most popular is the Adafruit SSD1306 library, which works with both SPI and I2C. It includes a drawBitmap() function that takes a byte array and draws it at a specified position. The library expects the bitmap data in the same vertical byte format. You can also use the U8g2 library, which supports a wide range of displays and includes font rendering and bitmap functions. U8g2 has a function drawXBM() that draws a monochrome bitmap from a byte array. The difference is that U8g2 uses a different memory layout for some displays, so you need to check the documentation. For the SSD1306, both libraries work, but U8g2 is more flexible if you’re using multiple display types. If you’re writing your own code, you can use the Wire library for I2C or the SPI library for SPI. The I2C address is typically 0x3C or 0x3D, depending on the module’s address pin. Most modules default to 0x3C. For SPI, the CS (chip select) pin is usually connected to a digital output, and the DC (data/command) pin selects between commands and data. When DC is low, the next byte is a command; when high, it’s data.

Bitmaps can be compressed for storage. If you have multiple bitmaps, like a font or icons, you can store them in PROGMEM on an Arduino to save RAM. The PROGMEM keyword stores data in flash memory, which is 32KB on an Uno. Each bitmap takes 1024 bytes, so you can fit about 31 full-screen bitmaps. For a font, you typically store each character as a small bitmap, like 8x8 pixels, which is 8 bytes per character. A full ASCII set of 96 characters takes 768 bytes. You can also use run-length encoding (RLE) to compress bitmaps, especially if they have large areas of solid color. For example, a bitmap with a white background and a small black icon might compress to 200 bytes instead of 1024. The SSD1306 doesn’t support decompression, so you need to decompress in software before sending. This adds CPU overhead but saves flash space.

Hardware quirks to watch out for: Some 0.96 inch OLED modules have a built-in level shifter for 5V logic, but others don’t. If you’re using a 5V microcontroller like an Arduino Uno, you need to check the module’s datasheet. The SSD1306 itself is 3.3V, but many modules include a 3.3V regulator and level shifters, so you can power them with 5V and use 5V logic. If you’re using a 3.3V microcontroller like an ESP32, you’re fine. The I2C pull-up resistors are usually 4.7k ohms on the module, but if you’re using long wires, you might need to add external pull-ups. For SPI, the maximum clock speed depends on the wiring. With short wires (under 10 cm), you can run at 10 MHz. With longer wires, you might need to lower the speed to 4 MHz or 1 MHz to avoid signal integrity issues. The display’s contrast can be adjusted with the 0x81 command, followed by a value from 0 to 255. A value of 0x7F (127) is typical, but you can increase it to 0xFF for maximum brightness, though this increases power consumption. The display also has a built-in charge pump that generates the high voltage needed for the OLED pixels. You can disable the charge pump with the 0x8D command, but then the display won’t work.

For a practical example, let’s say you want to display a 128x64 bitmap of a company logo. You’d convert the logo to a 1-bit BMP file using an image editor, then use a tool like image2cpp (available online) to generate the byte array. The tool outputs a C array like const unsigned char logo [] PROGMEM = {0x00, 0x00, ...};. You then copy that into your Arduino sketch. In the setup function, you initialize the display with display.begin(SSD1306_SWITCHCAPVCC, 0x3C) for I2C or display.begin(SSD1306_SWITCHCAPVCC, cs, dc, rst) for SPI. Then in the loop, you call display.clearDisplay() to clear the buffer, display.drawBitmap(0, 0, logo, 128, 64, WHITE) to draw the bitmap, and display.display() to send the buffer to the display. The drawBitmap function expects the data in the correct format, so you don’t need to worry about transposition. The display.display() function sends the entire 1024-byte buffer. If you want to update only part of the display, you can use display.drawBitmap() with a smaller bitmap and call display.display() only on that region, but the library doesn’t support partial updates directly. You can implement it by setting the display’s column and page addresses manually.

Data on typical use cases: In embedded systems, bitmaps are used for splash screens, user interface elements, and even simple animations. For example, a weather station might display a bitmap of a sun or cloud. A game might use sprites that are 16x16 pixels, which is 32 bytes each. A 128x64 bitmap can also be used for a QR code, which requires a high contrast ratio. The OLED’s contrast ratio is over 2000:1, so it’s excellent for QR codes. The viewing angle is 160 degrees, which is better than LCDs. The response time is under 10 microseconds, so there’s no motion blur. These characteristics make the 0.96 inch OLED ideal for displaying bitmaps in portable devices, wearables, and industrial controls. The operating temperature range is typically -40°C to 85°C, so it works in harsh environments. The display’s lifetime is around 100,000 hours, which is about 11 years of continuous use, though the brightness degrades over time, especially if you run it at full contrast.

One more technical detail: The SSD1306 has a built-in 128x64-bit SRAM buffer, which means the display controller stores the bitmap data internally. When you send data, it’s written to this buffer. The buffer is then scanned by the internal driver to light up the pixels. This means you don’t need to refresh the bitmap continuously; the controller does it automatically. However, if you want to change the image, you need to overwrite the buffer. The buffer is organized in pages, as I mentioned. You can also use the horizontal addressing mode, where you send data row by row, but the vertical mode is more common for bitmaps. The choice of addressing mode affects how you generate the