How to program a 0.95 inch OLED display with SPI?
How to program a 0.95 inch OLED display with SPI
To program a 0.95 inch OLED display with SPI, you need to wire it to a microcontroller like an Arduino or ESP32 using four SPI lines (SCK, MOSI, DC, and CS) plus power and ground, then initialize it with a specific command sequence and send pixel data in RGB565 format. The 0.95 inch 96x64 color oled display typically uses a SSD1351 or similar driver chip, which runs at 3.3V logic and requires a 5V supply for the OLED panel itself. Most modules have a built-in regulator, so you can power it from 3.3V to 5V, but the SPI signals must be 3.3V tolerant. If you're using a 5V Arduino, you'll need a level shifter or voltage divider on the SPI lines to avoid frying the driver IC.
Let's get into the nuts and bolts. The display resolution is 96x64 pixels, which means 6,144 pixels total. Each pixel is 16-bit color (RGB565), so the frame buffer size is 96 * 64 * 2 = 12,288 bytes. That's small enough to fit in the RAM of most microcontrollers, but you can also use a partial update approach if memory is tight. The SPI clock speed can go up to 10 MHz on most modules, but start with 4 MHz to avoid signal integrity issues on breadboard wiring. The command set for the SSD1351 is well documented: you send a command byte with DC low, then parameter bytes with DC high. For example, to set the column address range, you send 0x15 followed by start and end columns (0 to 95). Similarly, 0x75 sets the row range (0 to 63). Then you can send pixel data in a burst using the 0x5C write RAM command.
Here's a practical wiring table for a typical Arduino Uno connection:
| OLED Pin | Function | Arduino Pin | Notes |
|---|---|---|---|
| GND | Ground | GND | Common ground with MCU |
| VCC | Power (3.3-5V) | 5V | Module has regulator, 5V is fine |
| SCK | SPI Clock | 13 | Hardware SPI SCK |
| MOSI | SPI Data | 11 | Master Out Slave In |
| DC | Data/Command | 9 | Low = command, High = data |
| CS | Chip Select | 10 | Active low, pull high to disable |
| RST | Reset | 8 | Optional, but recommended |
The initialization sequence is critical. After power-up, you need to hold the reset pin low for at least 10 ms, then set it high. Then send these commands in order: unlock the driver (0xFD, 0x12), set display off (0xAE), set clock divider (0xB3, 0xF1), set multiplex ratio (0xCA, 0x3F for 64 rows), set display offset (0xA2, 0x00), set start line (0xA1, 0x00), set remap (0xA0, 0x74 for RGB order and horizontal addressing), set GPIO (0xB5, 0x00), set VCOMH (0xBE, 0x05), set contrast (0xC1, 0xC8, 0x80, 0xC8 for R, G, B), set master contrast (0xC7, 0x0F), set VSL (0xB4, 0xA0, 0xB5, 0x55), set precharge (0xB1, 0x32), set precharge period (0xB2, 0x01, 0x00), set display on (0xAF). After that, you can clear the screen by writing black pixels (0x00, 0x00) to all 12,288 bytes.
For actual pixel drawing, you need to calculate the 16-bit color value. RGB565 uses 5 bits for red, 6 bits for green, and 5 bits for blue. So a 24-bit color like red (255,0,0) becomes 0xF800 in 16-bit. Green (0,255,0) is 0x07E0, blue (0,0,255) is 0x001F. White is 0xFFFF, black is 0x0000. When sending pixel data over SPI, you send the high byte first, then the low byte. For a 96x64 display, you can set the column and row bounds once, then send all pixels in a single burst. This is much faster than setting the address for each pixel individually. At 4 MHz SPI, sending 12,288 bytes takes about 24.6 ms, so you can achieve around 40 frames per second for full-screen updates. If you only update a small region, you can set the column and row range to that area and send fewer bytes, which boosts the frame rate significantly.
One common pitfall is the data/command pin timing. Some libraries toggle DC between each byte, but the SSD1351 allows you to send multiple command bytes with DC low, then multiple data bytes with DC high. For example, after setting the column range, you can send the start and end columns as two bytes with DC high. This reduces SPI overhead. Also, the reset pin is not strictly required if you have a stable power supply, but it's safer to use it because the display might not initialize properly if the power ramps slowly. I've seen cases where omitting the reset causes the display to show random patterns until you manually toggle it.
If you're using an ESP32, the SPI pins are different. The default VSPI uses MOSI on GPIO 23, SCK on GPIO 18, and MISO on GPIO 19 (which you don't need for this display). You can assign CS and DC to any GPIO. The ESP32 runs at 3.3V logic, so no level shifting is needed. The initialization code is the same, but you need to set the SPI clock frequency to 8 MHz or higher if your wiring allows. The ESP32's SPI hardware can handle up to 40 MHz, but the display module's PCB traces and the flex cable limit the practical speed. Start with 8 MHz and increase if you see no glitches. For a 96x64 display, 8 MHz gives a full-screen update time of 12.3 ms, which is 81 FPS theoretically, but the MCU overhead and other tasks will reduce that to about 30-40 FPS in practice.
Power consumption is another factor. The OLED panel draws about 20-30 mA when all pixels are white, and less than 1 mA when black. The driver IC adds about 5 mA. So total current is around 25-35 mA at 5V. If you're running on batteries, you can put the display in sleep mode (0xAE command) when not in use, which drops the current to under 1 mA. The SSD1351 also has a partial display mode where you can turn off rows to save power, but that's rarely used in small displays.
For graphics, you can use the Adafruit GFX library or write your own low-level routines. The Adafruit library for SSD1351 is a good starting point, but it's bloated for a 96x64 display. You can strip it down to just the essentials: setAddrWindow, writePixel, and fillScreen. If you need text, you can include a 5x7 font bitmap and write a function to draw characters pixel by pixel. For images, you can convert them to 16-bit RGB565 arrays using a tool like Image2LCD or LCD Image Converter. The image size for full screen is 96 * 64 * 2 = 12,288 bytes, which fits in the flash of most microcontrollers. For example, an Arduino Uno has 32 KB of flash, so you can store about 2 full-screen images. An ESP32 has 4 MB of flash, so you can store hundreds.
SPI timing is straightforward but needs attention. The SSD1351 requires a minimum SCK low and high time of 50 ns each, which corresponds to a maximum clock of 10 MHz. But the module's PCB layout and the flex cable capacitance can cause signal degradation above 8 MHz. I've tested multiple modules and found that 6 MHz is the sweet spot for reliability. If you see ghosting or missing pixels, lower the clock speed. Also, make sure the CS line is high when not in use, and that you don't leave it floating. Some modules have a built-in pull-up on CS, but it's better to drive it explicitly.
Another detail is the display orientation. The SSD1351 supports hardware mirroring and rotation through the remap command (0xA0). The default orientation is with the flex cable at the bottom. If you need to rotate the display 180 degrees, you can set the remap to 0x76 instead of 0x74. This flips both the column and row order. You can also set the column and row start addresses to shift the image. For example, if you want to center a 96x64 image on a larger panel, you can set the column offset using the 0xA2 command. But for this specific display, the active area is exactly 96x64, so no offset is needed.
When writing your own library, the key functions are: writeCommand(uint8_t cmd) which pulls DC low, sends the byte via SPI, and pulls DC high; writeData(uint8_t data) which keeps DC high and sends the byte; and setAddrWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) which sends 0x15 with x0 and x1, then 0x75 with y0 and y1. Then you can call writeData for each pixel. For speed, you can use a buffer in RAM and update the display in one go. The buffer size is 12,288 bytes, which is manageable on most 32-bit MCUs but might be tight on an Arduino Uno (2 KB RAM). In that case, you can update the display in chunks, like 8 rows at a time, using a smaller buffer of 96 * 8 * 2 = 1,536 bytes. This fits in the Uno's RAM with room for other variables.
I've also seen issues with the reset sequence. If you don't wait long enough after power-up, the display might not respond to commands. The datasheet says the VDD (logic) must be stable for at least 100 µs before releasing reset, and the VCI (panel voltage) must be stable for 10 ms. If you're using a single power supply, the internal regulator takes some time to stabilize. A safe approach is to hold reset low for 10 ms after power-up, then set it high, then wait 100 ms before sending any commands. This ensures the driver IC is ready.
For debugging, you can use a logic analyzer to check the SPI signals. The command bytes should be preceded by DC low, and data bytes by DC high. The CS line should go low before the first SCK edge and high after the last SCK edge. If you see the display showing random colors, it's usually because the initialization sequence is wrong or the power supply is noisy. Adding a 10 µF capacitor between VCC and GND near the display module can help. Also, check that the SPI mode is 0 (CPOL=0, CPHA=0), which is the default for most Arduino SPI libraries.
If you want to display text, you can use a simple 5x7 font. Each character takes 5 bytes (one byte per column, with bits representing rows). For a 96x64 display, you can fit 19 characters per row (96 / 5 = 19.2, so 19 with 1 pixel spacing) and 9 rows (64 / 7 = 9.14, so 9 with 1 pixel spacing). That's 171 characters total. You can write a function that takes a character and its x,y position, then reads the font bitmap and sets the corresponding pixels. For a more readable font, you can use a 8x8 font, which gives 12 characters per row and 8 rows, or 96 characters total. The trade-off is readability vs. information density.
For animations, you can use double buffering if you have enough RAM. Store the current frame in one buffer and the next frame in another, then swap them. This avoids tearing artifacts. Since the display doesn't have a built-in frame buffer, you need to send all pixels every frame. If you're using an ESP32 with PSRAM, you can store multiple frames and switch between them. For simple animations like a bouncing ball, you can just update the ball's position and redraw the affected area, which is much faster than redrawing the entire screen.
One more thing: the SPI bus can be shared with other devices as long as each has its own CS line. If you have an SD card or another SPI device on the same bus, make sure the CS of the OLED is high when you're talking to the other device. Some drivers have a bug where they leave CS low, which can cause bus contention. Always set CS high after each transaction.
In terms of code structure, a typical program looks like this: initialize SPI (set clock, mode, bit order), initialize the OLED with the command sequence, clear the display, then in the main loop, draw something and update the display. For a simple test, you can fill the screen with a color, then draw a rectangle, a circle, and some text. This verifies that the SPI communication, color format, and addressing are all correct. If you see the rectangle but not the circle, the issue is likely in the circle drawing algorithm, not the display driver.
Finally, if you're using a library like U8g2, it supports the SSD1351 with a 96x64 resolution. But U8g2 is designed for monochrome displays and doesn't support color. For color, you need a library like TFT_eSPI or Adafruit_SSD1351. TFT_eSPI is highly optimized and supports multiple displays, but you need to configure the pins in a header file. It also supports SPI DMA on ESP32, which can push pixels at up to 40 MHz without CPU intervention. This is useful for high frame rate animations. The downside is that it's more complex to set up, and the configuration file can be intimidating for beginners.