Pure Electronics

←Index

SSD1306 OLED I2C Displays

by Professor Petabyte

 

SSD1306 OLED I2C screens are and available in a number of sizes.

ResolutionDescriptionTypical Size
128x64Most common and widely supported0.96", 1.3"
128x32Narrower variant, used where height is limited0.91"
96x16Very small, used in minimal displays~0.69"
72x40 Less common, non-standard sizeCustom

This page contains a few notes explaining how such drivers work.

Writing an I2C driver for an SSD1306 OLED display (the most common SSD display) involves a structured process that includes initializing the I2C interface, sending initialization commands to the display, and writing display data.

Notes

Tip

If coding for a non-standard resolution (e.g., 72x40), make sure to:

Step-by-Step Procedure to Write an I2C Driver for SSD1306

  1. Understand the Hardware

  2. Setup I2C Communication

    Initialize the I2C peripheral of your microcontroller.
  3. Define SSD1306 Command and Control Bytes

    #define SSD1306_ADDR         0x3C << 1 // Shifted for HAL (Hardware Abstraction Layer)
    #define SSD1306_CMD          0x00
    #define SSD1306_DATA         0x40
                    
  4. Write I2C Instructions

    void SSD1306_WriteCommand(uint8_t cmd) {
        uint8_t data[2] = {SSD1306_CMD, cmd};
        HAL_I2C_Master_Transmit(&hi2c1, SSD1306_ADDR, data, 2, HAL_MAX_DELAY);
    }
    
    void SSD1306_WriteData(uint8_t* data, size_t size) {
        uint8_t buffer[size + 1];
        buffer[0] = SSD1306_DATA;
        memcpy(&buffer[1], data, size);
        HAL_I2C_Master_Transmit(&hi2c1, SSD1306_ADDR, buffer, size + 1, HAL_MAX_DELAY);
    }
    
                    
  5. Send Initialization Sequence

    Send a set of predefined SSD1306 commands to initialize the display.
    void SSD1306_Init(void) {
        HAL_Delay(100); // Wait after power-up
        SSD1306_WriteCommand(0xAE); // Display OFF
    
        SSD1306_WriteCommand(0xD5); // Set display clock
        SSD1306_WriteCommand(0x80); // Suggested ratio
    
        SSD1306_WriteCommand(0xA8); // Set multiplex
        SSD1306_WriteCommand(0x3F); // 64MUX for 128x64
    
        SSD1306_WriteCommand(0xD3); // Set display offset
        SSD1306_WriteCommand(0x00);
    
        SSD1306_WriteCommand(0x40); // Set start line at 0
    
        SSD1306_WriteCommand(0x8D); // Charge pump
        SSD1306_WriteCommand(0x14);
    
        SSD1306_WriteCommand(0x20); // Memory addressing mode
        SSD1306_WriteCommand(0x00); // Horizontal mode
    
        SSD1306_WriteCommand(0xA1); // Segment remap
        SSD1306_WriteCommand(0xC8); // COM scan direction
    
        SSD1306_WriteCommand(0xDA); // COM pins hardware config
        SSD1306_WriteCommand(0x12);
    
        SSD1306_WriteCommand(0x81); // Contrast
        SSD1306_WriteCommand(0x7F);
    
        SSD1306_WriteCommand(0xD9); // Pre-charge
        SSD1306_WriteCommand(0xF1);
    
        SSD1306_WriteCommand(0xDB); // VCOM detect
        SSD1306_WriteCommand(0x40);
    
        SSD1306_WriteCommand(0xA4); // Resume to RAM content display
        SSD1306_WriteCommand(0xA6); // Normal display
    
        SSD1306_WriteCommand(0xAF); // Display ON
    }
    
  6. Write Framebuffer Functions

    #define SSD1306_WIDTH  128
    #define SSD1306_HEIGHT 64
    uint8_t framebuffer[SSD1306_WIDTH * SSD1306_HEIGHT / 8];
    
    void SSD1306_UpdateScreen(void) {
        for (uint8_t page = 0; page < 8; page++) {
            SSD1306_WriteCommand(0xB0 + page); // Set page address
            SSD1306_WriteCommand(0x00);        // Set lower column start
            SSD1306_WriteCommand(0x10);        // Set higher column start
            SSD1306_WriteData(&framebuffer[SSD1306_WIDTH * page], SSD1306_WIDTH);
        }
    }
    
  7. Draw Pixels to Framebuffer

    void SSD1306_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
        if (x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return;
        if (color)
            framebuffer[x + (y / 8) * SSD1306_WIDTH] |=  (1 << (y % 8));
        else
            framebuffer[x + (y / 8) * SSD1306_WIDTH] &= ~(1 << (y % 8));
    }
    

What does "Charge Pump" Mean?

A charge pump is a type of electronic circuit that generates a higher voltage (or sometimes a negative voltage) from a lower voltage without using inductors like traditional power supplies.

In the context of an OLED display like the SSD1306, the charge pump is used to generate the higher voltage needed to drive the OLED pixels, since the logic voltage (e.g. 3.3V or 5V) is too low to do that directly.

Why Is It Needed in SSD1306?

OLED displays typically require 7-8 volts to properly light up the organic materials in the pixels. But microcontrollers like Arduino, STM32, or ESP32 supply only 3.3V or 5V.

So the SSD1306 includes an internal charge pump that:

Charge Pump Command: 0x8D

This command tells the SSD1306 whether to enable or disable the internal charge pump.

0x8D [val]
Values
0x10Disable charge pump
0x14Enable charge pump

Example in Init Sequence (MicroPython, Arduino, C)

uint8_t init_sequence[] = {
    0xAE,       // Display OFF
    0x8D, 0x14, // Enable charge pump
    0xAF        // Display ON
};
To see how such an initailisation sequence would be implemented, have a look at the function "init_display(self)" in the 'Fully Working Example' below. That technique can be adapted to supply all sorts of initialisation commands to all sorts of I2C devices (not limited to just displays).

Summary

Term Description
SSD1306 Provides ~7.5V from 3.3V/5V to power OLED pixels
Enable Command 0x8D, 0x14
Disable Command 0x8D, 0x10

What About the 72x40 Pixel specifically?

The 72x40 SSD1306 OLED display is a low-resolution variant of the popular SSD1306 family, typically using I2C communication and controlled via commands sent to the SSD1306 driver chip. Although your display is 72x40, the command set is identical to the standard SSD1306 (typically 128x64 or similar). The only difference is the display RAM mapping—commands and functions remain the same.

Here is a list of SSD1306 command bytes grouped by purpose:

Fundamental Commands

Command Hex Description
Set Contrast 0x81 + 1 byte Sets brightness level (0-255)
Entire Display ON 0xA4 Resume from RAM content display
Entire Display ON (Override) 0xA5 Entire display ON, ignores RAM
Set Normal Display 0xA6 Normal display (0=black, 1=white)
Set Inverse Display 0xA7 Inverse display (0=white, 1=black)
Display OFF 0xAE Turns display off
Display ON 0xAF Turns display on

Addressing Commands

Command Hex Description
Set Memory Addressing Mode 0x20 + 1 byte 0x00: Horizontal, 0x01: Vertical, 0x02
Set Column Address 0x21 + start + end (0-127 typical, for 72x40 use 0-71)
Set Page Address 0x22 + start + end Page range (0-7 for 64px high; use 0-4 for 40px)

Scrolling Commands

Command Hex Description
Right Horizontal Scroll 0x26 + 6 bytes Setup right scroll
Left Horizontal Scroll 0x27 + 6 bytes Setup left scroll
Vertical & Right Scroll 0x29 + 6 bytes Combo scroll
Vertical & Left Scroll 0x2A + 6 bytes Combo scroll
Activate Scroll 0x2F Start scrolling
Deactivate Scroll 0x2E Stop scrolling
Set Vertical Scroll Area 0xA3 + 2 byte (top fixed area, scroll area rows)

Hardware Configuration

Command Hex Description
Set Display Start Line 0x40-0x7F Sets RAM start line (0-63)
Set Segment Remap 0xA0 or 0xA1 Mirror display horizontally
Set COM Output Scan Direction 0xC0 or 0xC8 Mirror vertically
Set Multiplex Ratio 0xA8 + 1 byte Typically 0x27 for 40px height
Set Display Offset 0xD3 + 1 byte Vertical offset
Set COM Pins Hardware Config 0xDA + 1 byte For 40px: use 0x02 (sequential COM)

Timing & Power

Command Hex Description
Set Display Clock Divide Ratio 0xD5 + 1 byte Oscillator setting
Set Pre-charge Period 0xD9 + 1 byte Charge time for pixels
Set VCOMH Deselect Level 0xDB + 1 byte Voltage setting
NOP 0xE3 No Operation

Charge Pump Control

Command Hex Description
Charge Pump Setting 0x8D + 1 byte 0x14=Enable, 0x10=Disable

Initialisation Template (Example for 72x40)

cmds = [
    0xAE,        # Display OFF
    0x20, 0x00,  # Horizontal addressing mode
    0xB0,        # Page 0 start address
    0xC8,        # COM scan direction
    0x00,        # Low column start
    0x10,        # High column start
    0x40,        # Display start line
    0x81, 0x8F,  # Contrast
    0xA1,        # Segment remap
    0xA6,        # Normal display
    0xA8, 0x27,  # Multiplex ratio (for 40px height)
    0xD3, 0x00,  # Display offset
    0xD5, 0x80,  # Display clock
    0xD9, 0xF1,  # Pre-charge
    0xDA, 0x02,  # COM pins hardware config for 40px
    0xDB, 0x20,  # VCOMH
    0x8D, 0x14,  # Charge pump ON
    0xAF         # Display ON
]

        

Where are the commands to Draw Lines, Rects etc.

Commands like line(), rect(), fill_rect(), pixel() etc. do not exist in the SSD1306 hardware itself — and they are not part of the SSD1306 command set. These are software-level drawing functions provided by driver libraries, not the display controller.

They come from graphics libraries that sit on top of the SSD1306 driver. Here are the key layers:

  1. SSD1306 Chip Command Set

  2. Display Buffer

  3. Graphics Libraries (e.g. MicroPython's framebuf, Adafruit's GFX, or Arduino's U8g2)

  4. Display Driver (e.g. ssd1306.py)

Example in MicroPython

If you're using MicroPython on a Raspberry Pi Pico:

from machine import I2C, Pin
import ssd1306

i2c = I2C(0, scl=Pin(5), sda=Pin(4))
oled = ssd1306.SSD1306_I2C(72, 40, i2c)

oled.fill(0)
oled.line(0, 0, 71, 39, 1)
oled.rect(10, 5, 30, 20, 1)
oled.fill_rect(15, 10, 10, 10, 1)
oled.show()

Here's what happens

Summary

Feature Exists in SSD1306? Provided by
oled.line() NO Graphics library
oled.rect() NO Graphics library
oled.pixel() NO Graphics library
oled.show() Translates to SSD1306 commands Driver
oled.fil() NO Clears buffer (library)

Fully Working Example

Driver:
lib/SSD1306_i2c72x40_Only_PHG.py

# MicroPython driver for 72x40 pixel SSD1306 OLED
# using I2C interface only by Paul Geare 8 June 2025

from micropython import const
import framebuf

# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)

# FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            0x21, # Set column address...
            0x00, #    to start at column 0
            0x47, #    and end at column 71
            0x22, # Set page address...
            0x00, #    to start at page 0
            0x04, #    and end at page 4
            0xD3, # Set display offset...
            0x00, #    0 X-offset
            0x40, # Set display start line to 0
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 28
        x1 = self.width + 27
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)




Demo Script:
SSD1306_i2c72x40_Demo.py

from machine import Pin, I2C
#from machine import RTC
from SSD1306_i2c72x40_Only_PHG import SSD1306_I2C
from time import sleep
import sys

def clear_blink(blinks):
    for i in range (blinks):
        oled.fill(1)
        oled.show()
        sleep(0.1)
        oled.fill(0)
        oled.show()
        sleep(0.1)
    
SSD1306_bus = 0
SSD1306_scl = machine.Pin(17)
SSD1306_sda = machine.Pin(16)
SSD1306_freq = 200000
SSD1306_width = 72
SSD1306_height = 40
SSD1306_i2c = machine.I2C(SSD1306_bus,scl=SSD1306_scl, sda=SSD1306_sda, freq=SSD1306_freq)
SSD1306_addr = 0X3C

oled = SSD1306_I2C(SSD1306_width, SSD1306_height, SSD1306_i2c, SSD1306_addr)

clear_blink(5)

#while True:oled.fill(0)
oled.text("Bus "+str(SSD1306_bus)+",", 0, 0)
oled.text("Addr"+str(SSD1306_addr), 0, 8)
oled.text("SDA GP16", 0, 16)
oled.text("SCL GP17", 0, 24)
oled.text("PicoPower", 0, 32)
oled.show()
sleep(2)
clear_blink(3)

while True:
    for x in range(0,72,4):
        y=int((40/72)*x)
        oled.line(x,0,72,y,0)
        oled.line(72,y,72-x,40,0)
        oled.line(72-x,40,0,40-y,0)
        oled.line(0,40-y,x,0,0)
        oled.show()

    for x in range(0,72,4):
        y=int((40/72)*x)
        oled.line(x,0,72,y,1)
        oled.line(72,y,72-x,40,1)
        oled.line(72-x,40,0,40-y,1)
        oled.line(0,40-y,x,0,1)
        oled.show()


OLED (Organic Light Emitting Diode) displays, especially small ones like SSD1306-based modules (used frequently in Arduino and embedded projects), are typically controlled via commands sent over I2C or SPI. These commands configure and control the display. Below is a breakdown of common OLED commands, their parameters, and what they do, specifically for SSD1306 (one of the most common OLED controllers):

General Structure

Each command is usually a single byte, followed by 0 or more data bytes (parameters). Commands are sent with a control byte that tells the display you are sending a command (0x00) or data (0x40).

Common OLED Commands (SSD1306)

CommandNameParametersDescription
0xAEDisplay OFFNoneTurns off the display
0xAFDisplay ONNoneTurns on the display
0xD5Set Display Clock Divide Ratio / Oscillator Frequency1 byte: `[A[3:0]B[3:0]]`
0xA8Set Multiplex Ratio1 byte: 0x3F (for 128x64)Number of active rows
0xD3Set Display Offset1 byte: 0x00Vertical shift of display
0x40Set Display Start LineNone or 1 byteStart line from RAM
0x8DCharge Pump Setting1 byte: 0x14 = Enable, 0x10 = DisableControls internal charge pump
0xA1Segment RemapNoneFlip horizontal (mirror X)
0xC8COM Output Scan DirectionNoneFlip vertical (mirror Y)
0xDACOM Pins Hardware Configuration1 byte: 0x12 for 128x64Configures COM pin layout
0x81Set Contrast Control1 byte: 0-255Adjusts brightness
0xA4Entire Display ON (resume RAM content)NoneUses display RAM
0xA5Entire Display ON (ignore RAM)NoneAll pixels ON
0xA6Set Normal DisplayNoneNormal display (white on black)
0xA7Set Inverse DisplayNoneInverts pixels (black on white)
0x20Set Memory Addressing Mode1 byte: 0x00 (Horizontal), 0x01 (Vertical), 0x02 (Page)Controls RAM addressing mode
0x21Set Column Address2 bytes: start, endColumn range for addressing
0x22Set Page Address2 bytes: start, endPage range (each page = 8 rows)
0x2EDeactivate ScrollNoneStops any scrolling
0x2FActivate ScrollNoneStarts scrolling
0x26 / 0x27Right/Left Horizontal Scroll6 bytesSetup scroll direction and speed
0x00-0x0FLower Column AddressLow nibble of column
0x10-0x1FHigher Column AddressHigh nibble of column

Drawing Pixels: Data vs Command

To draw, send 0x40 followed by 8-bit data (1 byte = 8 vertical pixels).

Each column is addressed via 0x21 and 0x22 commands to set column and page range.

Example Initialization Sequence (128x64 OLED)

0xAE       // Display OFF
0xD5, 0x80 // Set display clock
0xA8, 0x3F // Set multiplex ratio (0x3F = 64)
0xD3, 0x00 // Set display offset
0x40       // Set start line = 0
0x8D, 0x14 // Charge pump ON
0x20, 0x00 // Horizontal addressing mode
0xA1       // Segment remap (mirror horizontally)
0xC8       // COM scan direction (mirror vertically)
0xDA, 0x12 // COM pins config
0x81, 0xCF // Set contrast
0xD9, 0xF1 // Pre-charge period
0xDB, 0x40 // VCOMH deselect level
0xA4       // Display follows RAM content
0xA6       // Normal display
0xAF       // Display ON

Notes

SSD1306 graphics commands using Micropython

Graphics-level commands available in MicroPython (or CircuitPython) when using the SSD1306 OLED driver. These higher-level methods are part of the ssd1306 module or compatible drivers (like framebuf), and they let you draw graphics primitives like lines, rectangles, and text.

Here's a guide to the common SSD1306 graphics commands in MicroPython:

Setting Up the Display

from machine import I2C, Pin
import ssd1306

i2c = I2C(0, scl=Pin(22), sda=Pin(21))  # Adjust pins for your board
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

Graphics Methods (Drawing Commands)

MethodDescription
oled.pixel(x, y, color)Set a single pixel. color: 1 = on, 0 = off
oled.line(x0, y0, x1, y1, color)Draw a line from (x0, y0) to (x1, y1)
oled.hline(x, y, width, color)Draw a horizontal line
oled.vline(x, y, height, color)Draw a vertical line
oled.rect(x, y, w, h, color)Draw a rectangle outline
oled.fill_rect(x, y, w, h, color)Draw a filled rectangle
oled.fill(color)Fill the entire screen (1 = white/on, 0 = black/off)
oled.text(string, x, y, color=1)Draw ASCII text at (x, y)
oled.scroll(dx, dy)Scroll the screen by dx and dy pixels
oled.blit(framebuf, x, y)Copy another framebuffer (image or font) onto the display
oled.show()Send all drawing to the display (must be called to update screen)

Example Usage

oled.fill(0)  # Clear screen
oled.text("Hello", 0, 0)
oled.line(0, 10, 127, 10, 1)
oled.rect(10, 20, 50, 30, 1)
oled.fill_rect(70, 20, 50, 30, 1)
oled.pixel(64, 32, 1)
oled.show()

Notes

Refreshing the Display

Always call oled.show() after drawing, or nothing will appear on screen.

The blit() method is a powerful way to copy graphics from one framebuffer (image, shape, font, etc.) to another — e.g. onto a SSD1306 OLED display.

blit() Syntax

oled.blit(source_framebuf, x, y)

source_framebuf: A FrameBuffer object (e.g., a sprite or bitmap image).
x, y: Top-left corner on the OLED where you want to place it.

What is a FrameBuffer?

A FrameBuffer is essentially a 2D array of pixel data, representing an image or graphical object. The ssd1306 driver uses one internally, and you can create your own to hold:

Creating a FrameBuffer Example

Here's how to draw a simple 8x8 bitmap and blit() it onto your OLED:

import framebuf

8x8 smiley face bitmap (1 bit per pixel)

smiley = bytearray([
    0b00111100,
    0b01000010,
    0b10100101,
    0b10000001,
    0b10100101,
    0b10011001,
    0b01000010,
    0b00111100
])

# Create a framebuffer for it
fb = framebuf.FrameBuffer(smiley, 8, 8, framebuf.MONO_HLSB)

# Draw it to the OLED
oled.blit(fb, 60, 30)
oled.show()

framebuf Formats

FormatDescription
framebuf.MONO_HLSB1-bit (monochrome), horizontal row order, LSB first — used for SSD1306
framebuf.MONO_HMSB1-bit, horizontal row order, MSB first
framebuf.MONO_VLSB1-bit, vertical row order, LSB first (some other displays)
framebuf.RGB56516-bit color (used for color TFTs)

Use MONO_HLSB for most OLEDs like SSD1306.

When to Use blit()

Tips




© 2025 Professor Petabyte